mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-03 13:00:37 +00:00
Imported from andrewyng/aisuite@1b4bbf303e (contents of its platform/ directory, hoisted to the repo root). Development history prior to this commit lives in that repository. Co-authored-by: Devika <devikaverma11@gmail.com>
166 lines
5.6 KiB
Python
166 lines
5.6 KiB
Python
"""Tests for MCP (C1): config loading/merge, tool wrapping + bridge, and REST.
|
|
|
|
No live MCP subprocess is needed — the connection layer is exercised by stubbing the call
|
|
coroutine; a live-server smoke test is documented in the plan instead.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from coworker.mcp import build_callables, load_mcp_servers, tool_name
|
|
from coworker.mcp.config import MCPServerDef
|
|
from coworker.secrets import SecretStore
|
|
from coworker.server.app import create_app
|
|
from coworker.server.manager import SessionManager
|
|
|
|
|
|
def _write_json(path, data):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(data), encoding="utf-8")
|
|
|
|
|
|
def _fake_tool(name, schema=None, description="desc"):
|
|
return SimpleNamespace(
|
|
name=name,
|
|
description=description,
|
|
inputSchema=schema
|
|
or {
|
|
"type": "object",
|
|
"properties": {"path": {"type": "string"}},
|
|
"required": ["path"],
|
|
},
|
|
)
|
|
|
|
|
|
# -- config --------------------------------------------------------------------
|
|
def test_load_merges_global_and_workspace(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state"))
|
|
_write_json(
|
|
tmp_path / "state" / "mcp.json",
|
|
{
|
|
"mcpServers": {
|
|
"fs": {"command": "echo", "args": ["global"], "enabled": True},
|
|
"docs": {"type": "http", "url": "https://x/mcp", "enabled": False},
|
|
}
|
|
},
|
|
)
|
|
ws = tmp_path / "ws"
|
|
_write_json(
|
|
ws / ".coworker" / "mcp.json",
|
|
{
|
|
"mcpServers": {
|
|
"fs": {
|
|
"command": "echo",
|
|
"args": ["workspace-wins"],
|
|
}, # overrides global
|
|
}
|
|
},
|
|
)
|
|
|
|
servers = {s.name: s for s in load_mcp_servers(ws, secrets=SecretStore())}
|
|
assert servers["fs"].args == ["workspace-wins"]
|
|
assert servers["fs"].transport == "stdio"
|
|
assert servers["docs"].transport == "http" and servers["docs"].enabled is False
|
|
assert servers["docs"].requires_approval is True # default
|
|
|
|
|
|
def test_var_resolution(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state"))
|
|
monkeypatch.setenv("DOCS_TOKEN", "sekret")
|
|
_write_json(
|
|
tmp_path / "state" / "mcp.json",
|
|
{
|
|
"mcpServers": {
|
|
"docs": {
|
|
"type": "http",
|
|
"url": "https://x/mcp",
|
|
"headers": {"Authorization": "Bearer ${DOCS_TOKEN}"},
|
|
},
|
|
}
|
|
},
|
|
)
|
|
docs = load_mcp_servers(None, secrets=SecretStore())[0]
|
|
assert docs.headers["Authorization"] == "Bearer sekret"
|
|
|
|
|
|
# -- tool wrapping + bridge ----------------------------------------------------
|
|
def test_tool_name_sanitizes():
|
|
assert tool_name("fs", "read_file") == "mcp__fs__read_file"
|
|
assert "." not in tool_name("a.b", "c.d")
|
|
|
|
|
|
def test_schema_and_metadata():
|
|
server = MCPServerDef(name="fs", transport="stdio", requires_approval=True)
|
|
fns = build_callables(
|
|
server, [_fake_tool("read_file")], lambda t, a: None, asyncio.new_event_loop()
|
|
)
|
|
fn = fns[0]
|
|
assert fn.__name__ == "mcp__fs__read_file"
|
|
meta = fn.__aisuite_tool_metadata__
|
|
assert meta.category == "mcp" and meta.requires_approval is True
|
|
schema = fn.__coworker_schema__["function"]
|
|
assert schema["name"] == "mcp__fs__read_file"
|
|
assert schema["parameters"]["required"] == ["path"]
|
|
|
|
|
|
def test_include_exclude_filter():
|
|
server = MCPServerDef(name="fs", transport="stdio", include_tools=["read_file"])
|
|
fns = build_callables(
|
|
server,
|
|
[_fake_tool("read_file"), _fake_tool("delete_file")],
|
|
lambda t, a: None,
|
|
asyncio.new_event_loop(),
|
|
)
|
|
assert [f.__name__ for f in fns] == ["mcp__fs__read_file"]
|
|
|
|
|
|
async def test_bridge_invokes_session_on_loop():
|
|
loop = asyncio.get_running_loop()
|
|
seen = []
|
|
|
|
async def call_async(tool, args):
|
|
seen.append((tool, args))
|
|
return {"echo": args}
|
|
|
|
server = MCPServerDef(name="fs", transport="stdio")
|
|
fn = build_callables(server, [_fake_tool("read_file")], call_async, loop)[0]
|
|
# The engine runs tools via to_thread; the wrapper bridges back to this loop.
|
|
result = await asyncio.to_thread(fn, path="a.txt")
|
|
assert result == {"echo": {"path": "a.txt"}}
|
|
assert seen == [("read_file", {"path": "a.txt"})]
|
|
|
|
|
|
# -- REST ----------------------------------------------------------------------
|
|
def test_rest_crud(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state"))
|
|
manager = SessionManager(data_dir=tmp_path / "data")
|
|
client = TestClient(create_app(manager))
|
|
|
|
assert client.get("/v1/mcp").json()["servers"] == []
|
|
|
|
r = client.post(
|
|
"/v1/mcp",
|
|
json={
|
|
"name": "fs",
|
|
"config": {"command": "echo", "args": ["x"], "env": {"SECRET": "shh"}},
|
|
},
|
|
)
|
|
assert r.json()["ok"] is True
|
|
|
|
servers = client.get("/v1/mcp").json()["servers"]
|
|
assert servers[0]["name"] == "fs" and servers[0]["status"] == "configured"
|
|
assert servers[0]["config"]["env"]["SECRET"] == "***" # redacted
|
|
|
|
assert client.patch("/v1/mcp/fs", json={"enabled": False}).json()["ok"] is True
|
|
assert client.get("/v1/mcp").json()["servers"][0]["enabled"] is False
|
|
|
|
assert client.delete("/v1/mcp/fs").json()["ok"] is True
|
|
assert client.get("/v1/mcp").json()["servers"] == []
|
|
assert client.delete("/v1/mcp/fs").json()["ok"] is False
|