218 lines
7.5 KiB
Python
218 lines
7.5 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import shlex
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from pyMCPBroker.app import create_app
|
|
from pyMCPBroker.broker import Broker
|
|
from pyMCPBroker.config import load_config
|
|
|
|
|
|
|
|
def make_config(tmp_path: Path) -> Path:
|
|
server = Path(__file__).with_name("fake_mcp_server.py")
|
|
command = f"{shlex.quote(sys.executable)} {shlex.quote(str(server))}"
|
|
config = {
|
|
"tree": [
|
|
{
|
|
"path": "/repo",
|
|
"type": "node",
|
|
"summary": "Repository operations",
|
|
"children": [
|
|
{
|
|
"path": "/repo/read",
|
|
"type": "node",
|
|
"summary": "Read repository data",
|
|
"source": {
|
|
"backend": "stdio",
|
|
"command": command,
|
|
"tool_filter": ["get_*", "list_*", "!delete_*"],
|
|
"path_aliases": {"get_file_contents": "get_file"},
|
|
"tool_overrides": {
|
|
"get_file_contents": {
|
|
"summary": "Read one file from a repository",
|
|
"max_output_chars": 1200,
|
|
"example_args": {
|
|
"owner": "myorg",
|
|
"repo": "demo-repo",
|
|
"ref": "main",
|
|
"filePath": "README.md",
|
|
},
|
|
}
|
|
},
|
|
},
|
|
}
|
|
],
|
|
}
|
|
]
|
|
}
|
|
path = tmp_path / "config.json"
|
|
path.write_text(json.dumps(config), encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def test_meta_end_to_end(tmp_path: Path) -> None:
|
|
cfg = load_config(make_config(tmp_path))
|
|
broker = Broker(cfg)
|
|
app = create_app(broker)
|
|
|
|
with TestClient(app) as client:
|
|
r = client.post("/meta_tree", json={"path": "/"})
|
|
assert r.status_code == 200
|
|
assert r.json()["children"][0]["path"] == "/repo"
|
|
|
|
r = client.post("/meta_tree", json={"path": "/repo/read"})
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
child_summaries = {child["path"]: child["summary"] for child in body["children"]}
|
|
assert "/repo/read/get_file" in child_summaries
|
|
assert "/repo/read/list_branches" in child_summaries
|
|
assert child_summaries["/repo/read/list_branches"] == "List all branches in a repository for a given owner and repo."
|
|
|
|
r = client.post("/meta_desc", json={"path": "/repo/read/get_file"})
|
|
body = r.json()
|
|
assert body["summary"] == "Read one file from a repository"
|
|
assert body["args_schema"]["required"] == ["owner", "repo", "ref", "filePath"]
|
|
assert body["example_args"]["repo"] == "demo-repo"
|
|
|
|
r = client.post(
|
|
"/meta_call",
|
|
json={
|
|
"path": "/repo/read/get_file",
|
|
"args": {"owner": "acme", "repo": "demo", "ref": "main", "filePath": "README.md"},
|
|
},
|
|
)
|
|
body = r.json()
|
|
assert body["ok"] is True
|
|
assert body["result"]["truncated"] is True
|
|
assert body["result"]["result"]["structuredContent"]["repo"] == "demo"
|
|
|
|
|
|
def test_meta_call_invalid_args(tmp_path: Path) -> None:
|
|
cfg = load_config(make_config(tmp_path))
|
|
broker = Broker(cfg)
|
|
app = create_app(broker)
|
|
|
|
with TestClient(app) as client:
|
|
r = client.post("/meta_call", json={"path": "/repo/read/get_file", "args": {"owner": "acme"}})
|
|
body = r.json()
|
|
assert r.status_code == 400
|
|
assert body["error_code"] == "invalid_arguments"
|
|
assert "required property" in body["message"]
|
|
|
|
|
|
def test_meta_tree_rejects_tool_path(tmp_path: Path) -> None:
|
|
cfg = load_config(make_config(tmp_path))
|
|
broker = Broker(cfg)
|
|
app = create_app(broker)
|
|
|
|
with TestClient(app) as client:
|
|
r = client.post("/meta_tree", json={"path": "/repo/read/get_file"})
|
|
assert r.status_code == 400
|
|
assert r.json()["error_code"] == "not_a_node"
|
|
|
|
|
|
def test_secret_auth(tmp_path: Path) -> None:
|
|
cfg = load_config(make_config(tmp_path))
|
|
broker = Broker(cfg)
|
|
app = create_app(broker, shared_secret="sekret")
|
|
|
|
with TestClient(app) as client:
|
|
assert client.post("/meta_tree", json={"path": "/"}).status_code == 401
|
|
assert client.post("/meta_tree", json={"path": "/"}, headers={"Authorization": "Bearer sekret"}).status_code == 200
|
|
|
|
|
|
def test_no_filter_exposes_all_tools(tmp_path: Path) -> None:
|
|
server = Path(__file__).with_name("fake_mcp_server.py")
|
|
command = f"{shlex.quote(sys.executable)} {shlex.quote(str(server))}"
|
|
path = tmp_path / "config.json"
|
|
path.write_text(
|
|
json.dumps(
|
|
{
|
|
"tree": [
|
|
{
|
|
"path": "/repo",
|
|
"type": "node",
|
|
"source": {"backend": "stdio", "command": command},
|
|
}
|
|
]
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
cfg = load_config(path)
|
|
broker = Broker(cfg)
|
|
app = create_app(broker)
|
|
|
|
with TestClient(app) as client:
|
|
r = client.post("/meta_tree", json={"path": "/repo"})
|
|
body = r.json()
|
|
child_paths = {child["path"] for child in body["children"]}
|
|
assert "/repo/get_file_contents" in child_paths
|
|
assert "/repo/delete_file" in child_paths
|
|
|
|
|
|
def test_explicit_root_is_optional(tmp_path: Path) -> None:
|
|
path = tmp_path / "config.json"
|
|
path.write_text(
|
|
json.dumps(
|
|
{
|
|
"tree": {
|
|
"path": "/",
|
|
"type": "node",
|
|
"summary": "Configured root",
|
|
"children": [
|
|
{"path": "/repo", "type": "node", "summary": "Repository operations"}
|
|
],
|
|
}
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
cfg = load_config(path)
|
|
broker = Broker(cfg)
|
|
app = create_app(broker)
|
|
|
|
with TestClient(app) as client:
|
|
r = client.post("/meta_tree", json={"path": "/"})
|
|
assert r.status_code == 200
|
|
assert r.json()["children"][0]["path"] == "/repo"
|
|
|
|
|
|
def test_summary_falls_back_to_description_when_title_is_missing(tmp_path: Path) -> None:
|
|
server = Path(__file__).with_name("fake_mcp_server.py")
|
|
command = f"{shlex.quote(sys.executable)} {shlex.quote(str(server))}"
|
|
path = tmp_path / "config.json"
|
|
path.write_text(
|
|
json.dumps(
|
|
{
|
|
"tree": [
|
|
{
|
|
"path": "/repo",
|
|
"type": "node",
|
|
"source": {
|
|
"backend": "stdio",
|
|
"command": command,
|
|
"tool_filter": ["list_*"],
|
|
},
|
|
}
|
|
]
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
cfg = load_config(path)
|
|
broker = Broker(cfg)
|
|
app = create_app(broker)
|
|
|
|
with TestClient(app) as client:
|
|
r = client.post("/meta_tree", json={"path": "/repo"})
|
|
body = r.json()
|
|
assert r.status_code == 200
|
|
assert body["children"][0]["path"] == "/repo/list_branches"
|
|
assert body["children"][0]["summary"] == "List all branches in a repository for a given owner and repo."
|