improve tools backend description forwaring

This commit is contained in:
2026-04-14 01:43:46 +02:00
parent fd085cbe87
commit 1bf07da725
6 changed files with 91 additions and 7 deletions

View File

@@ -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`.

View File

@@ -1,2 +1,2 @@
__all__ = ['__version__']
__version__ = '0.2.2'
__version__ = '0.2.3'

View File

@@ -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,

View File

@@ -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"

View File

@@ -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

View File

@@ -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."