Files
hermes-agent/tests/tools/test_mcp_tool_progress.py
T
Teknium 60affe7fe2 feat(mcp): forward MCP notifications/progress as live tool.progress events
MCP tool calls now send _meta.progressToken, so servers that emit
notifications/progress (Meshy refine, creative-lab) report live progress.
The executor binds a per-call relay; the MCP handler forwards each
notification to tool_progress_callback as a throttled (<=1 per 500 ms,
final always) tool.progress event. Surfaces:
- gateway event stream + legacy TUI gateway: tool.progress
  {tool_id, name, progress, total, percent, message} (contract registered)
- Desktop: running tool row shows '42% · <message>' beside its timer;
  plugins subscribe with ctx.onEvent('tool.progress')
- CLI: the running tool spinner shows the same text
Documented in the Desktop plugin SDK guide.
2026-10-20 04:41:58 -07:00

60 lines
2.2 KiB
Python

"""An MCP server's ``notifications/progress`` during ``tools/call`` reaches the agent's
``tool_progress_callback`` as throttled ``tool.progress`` events for that call id. Real stdio child,
real config, real SDK progress token round trip."""
import json
import sys
import textwrap
import pytest
SERVER = textwrap.dedent('''\
import asyncio
from mcp.server.mcpserver import MCPServer, Context
app = MCPServer("prog")
@app.tool()
async def slow(ctx: Context) -> str:
for i in range(1, 21):
await asyncio.sleep(0.1)
await ctx.report_progress(i * 5, 100, f"stage {i}")
return "done"
app.run()
''')
@pytest.mark.no_isolate
@pytest.mark.platforms("posix")
def test_mcp_progress_notifications_reach_tool_progress_callback(monkeypatch, tmp_path):
pytest.importorskip("mcp.server.mcpserver")
home = tmp_path / "home"
home.mkdir()
(home / "server.py").write_text(SERVER, encoding="utf-8")
(home / "config.yaml").write_text(
f"mcp_servers:\n prog:\n command: {json.dumps(sys.executable)}\n args: [{json.dumps(str(home / 'server.py'))}]\n",
encoding="utf-8")
monkeypatch.setenv("HERMES_HOME", str(home))
from agent.tool_progress_relay import bound_tool_progress
from tools import mcp_tool_discovery as disc
from tools import mcp_tool_loop as _loop
from tools.registry import registry
events = []
try:
assert "mcp__prog__slow" in disc.discover_mcp_tools()
with bound_tool_progress(lambda *a, **kw: events.append((a, kw)), "mcp__prog__slow", "call-7"):
result = registry.dispatch("mcp__prog__slow", {})
assert "done" in result
finally:
from tools.mcp_tool_lifecycle import shutdown_mcp_servers
shutdown_mcp_servers()
_loop._stop_mcp_loop()
assert events and all(a[0] == "tool.progress" and a[1] == "mcp__prog__slow" for a, _ in events)
assert {kw["tool_call_id"] for _, kw in events} == {"call-7"}
# 20 notifications over ~2 s, throttled to <= one per 500 ms, the final 100% always delivered.
assert 2 <= len(events) <= 8
assert events[-1][1]["percent"] == 100 and events[-1][0][2] == "stage 20"