improve tools backend description forwaring
This commit is contained in:
@@ -119,3 +119,8 @@ The repository includes a minimal `.vscode/` setup for running and debugging pyt
|
||||
## OpenAPI guidance for agents
|
||||
|
||||
The FastAPI/OpenAPI descriptions are intentionally directive. The broker hides business tools behind three stable entry tools, so agents should start with `meta_tree`, inspect with `meta_desc`, then execute with `meta_call`.
|
||||
|
||||
|
||||
## 0.2.3
|
||||
|
||||
- Prefer MCP tool descriptions over raw tool names when building child summaries in `meta_tree`.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
__all__ = ['__version__']
|
||||
__version__ = '0.2.2'
|
||||
__version__ = '0.2.3'
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
import re
|
||||
from typing import Any, Iterable
|
||||
|
||||
from .filters import is_allowed
|
||||
@@ -62,6 +63,33 @@ def join_path(parent: str, child_name: str) -> str:
|
||||
return f"/{child_name}" if parent == "/" else f"{parent}/{child_name}"
|
||||
|
||||
|
||||
def _humanize_tool_name(tool_name: str) -> str:
|
||||
text = tool_name.replace('_', ' ').strip()
|
||||
if not text:
|
||||
return tool_name
|
||||
return text[:1].upper() + text[1:]
|
||||
|
||||
|
||||
def _summarize_description(description: str, max_chars: int = 100) -> str:
|
||||
text = re.sub(r"\s+", " ", (description or "").strip())
|
||||
if not text:
|
||||
return ""
|
||||
parts = re.split(r"(?<=[.!?])\s+", text, maxsplit=1)
|
||||
summary = parts[0].strip()
|
||||
if len(summary) > max_chars:
|
||||
summary = summary[: max_chars - 1].rstrip() + "…"
|
||||
return summary
|
||||
|
||||
|
||||
def _tool_summary(tool_name: str, tool_meta: dict[str, Any]) -> str:
|
||||
return (
|
||||
tool_meta.get('_broker_summary')
|
||||
or tool_meta.get('title')
|
||||
or _summarize_description(tool_meta.get('description', ''))
|
||||
or _humanize_tool_name(tool_name)
|
||||
)
|
||||
|
||||
|
||||
def build_tree(config: RootConfig, raw_tools_by_backend: dict[str, list[dict[str, Any]]]) -> tuple[EntryIndex, SourceRegistry]:
|
||||
by_path: dict[str, Entry] = {}
|
||||
sources = SourceRegistry()
|
||||
@@ -113,7 +141,7 @@ def build_tree(config: RootConfig, raw_tools_by_backend: dict[str, list[dict[str
|
||||
if child_path in by_path:
|
||||
raise TreeError(f"Duplicate path: {child_path}")
|
||||
tool_meta = apply_tool_overrides(tool, source.tool_overrides.get(tool_name))
|
||||
summary = tool_meta.get("_broker_summary") or tool_meta.get("title") or tool_name
|
||||
summary = _tool_summary(tool_name, tool_meta)
|
||||
description = tool_meta.get("description", "")
|
||||
entry = ToolEntry(
|
||||
path=child_path,
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pyMCPBroker"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
description = "Small FastAPI MCP broker exposing stable meta-tools over stdio MCP sources"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -21,6 +21,19 @@ TOOLS = [
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "list_branches",
|
||||
"description": "List all branches in a repository for a given owner and repo.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": {"type": "string"},
|
||||
"repo": {"type": "string"}
|
||||
},
|
||||
"required": ["owner", "repo"],
|
||||
"additionalProperties": False
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "delete_file",
|
||||
"title": "Delete file",
|
||||
@@ -71,7 +84,7 @@ for line in sys.stdin:
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": {"tools": [TOOLS[0]], "nextCursor": "page-2"},
|
||||
"result": {"tools": [TOOLS[0], TOOLS[1]], "nextCursor": "page-2"},
|
||||
}
|
||||
)
|
||||
else:
|
||||
@@ -79,7 +92,7 @@ for line in sys.stdin:
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": {"tools": [TOOLS[1]]},
|
||||
"result": {"tools": [TOOLS[2]]},
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -30,7 +30,7 @@ def make_config(tmp_path: Path) -> Path:
|
||||
"source": {
|
||||
"backend": "stdio",
|
||||
"command": command,
|
||||
"tool_filter": ["get_*", "!delete_*"],
|
||||
"tool_filter": ["get_*", "list_*", "!delete_*"],
|
||||
"path_aliases": {"get_file_contents": "get_file"},
|
||||
"tool_overrides": {
|
||||
"get_file_contents": {
|
||||
@@ -67,7 +67,11 @@ def test_meta_end_to_end(tmp_path: Path) -> None:
|
||||
|
||||
r = client.post("/meta_tree", json={"path": "/repo/read"})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["children"][0]["path"] == "/repo/read/get_file"
|
||||
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()
|
||||
@@ -177,3 +181,37 @@ def test_explicit_root_is_optional(tmp_path: Path) -> None:
|
||||
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."
|
||||
|
||||
Reference in New Issue
Block a user