mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 05:50:44 +08:00
143 lines
5.6 KiB
Python
143 lines
5.6 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
import textwrap
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from oclaw.runtime.tools.mcp.runtime import McpProcessRuntime
|
|
|
|
|
|
class McpRuntimeTests(unittest.TestCase):
|
|
def _write_server(self, body: str) -> str:
|
|
td = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
|
|
self.addCleanup(td.cleanup)
|
|
p = Path(td.name) / "mcp_server.py"
|
|
p.write_text(textwrap.dedent(body), encoding="utf-8")
|
|
return str(p)
|
|
|
|
def test_health_and_tools_list_jsonrpc(self) -> None:
|
|
script = self._write_server(
|
|
"""
|
|
import json
|
|
import sys
|
|
|
|
for line in sys.stdin:
|
|
req = json.loads(line)
|
|
rid = req.get("id")
|
|
method = req.get("method")
|
|
if method == "initialize":
|
|
print(json.dumps({"jsonrpc": "2.0", "id": rid, "result": {"protocolVersion": "2024-11-05", "capabilities": {}}}), flush=True)
|
|
continue
|
|
if method == "notifications/initialized":
|
|
continue
|
|
if method == "tools/list":
|
|
print(json.dumps({"jsonrpc": "2.0", "id": rid, "result": {"tools": [{"name": "ping", "description": "Ping", "inputSchema": {"type": "object", "properties": {}}}]}}), flush=True)
|
|
continue
|
|
print(json.dumps({"jsonrpc": "2.0", "id": rid, "error": {"code": -32601, "message": "method not found"}}), flush=True)
|
|
"""
|
|
)
|
|
rt = McpProcessRuntime(["python", script], timeout_s=2)
|
|
try:
|
|
health = rt.health()
|
|
self.assertTrue(health.get("ok"))
|
|
self.assertEqual(health.get("status"), "ok")
|
|
tools = rt.tools_list()
|
|
self.assertTrue(tools.get("ok"))
|
|
self.assertTrue(isinstance(tools.get("tools"), list))
|
|
finally:
|
|
rt.stop()
|
|
|
|
def test_timeout_returns_error_code(self) -> None:
|
|
script = self._write_server(
|
|
"""
|
|
import json
|
|
import sys
|
|
import time
|
|
|
|
for line in sys.stdin:
|
|
_ = json.loads(line)
|
|
time.sleep(0.3)
|
|
print(json.dumps({"ok": True}), flush=True)
|
|
"""
|
|
)
|
|
rt = McpProcessRuntime(["python", script], timeout_s=0.05)
|
|
try:
|
|
out = rt.request({"op": "health"})
|
|
self.assertFalse(bool(out.get("ok")))
|
|
self.assertEqual(str(out.get("error_code") or ""), "mcp_runtime_timeout")
|
|
finally:
|
|
rt.stop()
|
|
|
|
@patch("oclaw.runtime.operations.mcp_env.mcp_local_env_merged", return_value={"MCP_ONLY_FROM_FILE": "fileval"})
|
|
def test_mcp_local_env_keys_passed_without_allowlist_name(self, _mock_merged: object) -> None:
|
|
os.environ["MCP_ONLY_FROM_FILE"] = "liveval"
|
|
try:
|
|
env = McpProcessRuntime._build_runtime_env([])
|
|
assert env is not None
|
|
self.assertEqual(env.get("MCP_ONLY_FROM_FILE"), "liveval")
|
|
finally:
|
|
os.environ.pop("MCP_ONLY_FROM_FILE", None)
|
|
|
|
def test_empty_allowlist_keeps_path_for_subprocess(self) -> None:
|
|
script = self._write_server(
|
|
"""
|
|
import json
|
|
import sys
|
|
|
|
for line in sys.stdin:
|
|
_ = json.loads(line)
|
|
print(json.dumps({"ok": True, "status": "up"}), flush=True)
|
|
"""
|
|
)
|
|
rt = McpProcessRuntime(["python", script], timeout_s=2, env_allowlist=[])
|
|
try:
|
|
out = rt.request({"op": "health"})
|
|
self.assertTrue(bool(out.get("ok")), out)
|
|
finally:
|
|
rt.stop()
|
|
|
|
def test_jsonrpc_mcp_flow_tools_and_call(self) -> None:
|
|
script = self._write_server(
|
|
"""
|
|
import json
|
|
import sys
|
|
|
|
for line in sys.stdin:
|
|
req = json.loads(line)
|
|
rid = req.get("id")
|
|
method = req.get("method")
|
|
if method == "initialize":
|
|
print(json.dumps({"jsonrpc": "2.0", "id": rid, "result": {"protocolVersion": "2024-11-05", "capabilities": {}}}), flush=True)
|
|
continue
|
|
if method == "notifications/initialized":
|
|
continue
|
|
if method == "tools/list":
|
|
print(json.dumps({"jsonrpc": "2.0", "id": rid, "result": {"tools": [{"name": "ping", "description": "Ping", "inputSchema": {"type": "object", "properties": {}}}]}}), flush=True)
|
|
continue
|
|
if method == "tools/call":
|
|
params = req.get("params") or {}
|
|
args = params.get("arguments") if isinstance(params, dict) else {}
|
|
print(json.dumps({"jsonrpc": "2.0", "id": rid, "result": {"content": [{"type": "text", "text": str((args or {}).get("msg") or "")}]}}), flush=True)
|
|
continue
|
|
print(json.dumps({"jsonrpc": "2.0", "id": rid, "error": {"code": -32601, "message": "method not found"}}), flush=True)
|
|
"""
|
|
)
|
|
rt = McpProcessRuntime(["python", script], timeout_s=2)
|
|
try:
|
|
tools = rt.tools_list()
|
|
self.assertTrue(bool(tools.get("ok")), tools)
|
|
self.assertEqual(len(tools.get("tools") or []), 1)
|
|
res = rt.call_tool("ping", {"msg": "hello"})
|
|
self.assertTrue(bool(res.get("ok")), res)
|
|
content = (((res.get("result") or {}).get("content")) or [{}])[0]
|
|
self.assertEqual(str(content.get("text") or ""), "hello")
|
|
finally:
|
|
rt.stop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|
|
|