mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-09 08:57:55 +08:00
runtime: capture MCP initialize instructions per connection
This commit is contained in:
@@ -92,6 +92,8 @@ class McpServerConnection:
|
|||||||
error: str | None
|
error: str | None
|
||||||
tools: list[McpTool]
|
tools: list[McpTool]
|
||||||
tools_stale: bool
|
tools_stale: bool
|
||||||
|
# Server usage guidance from the initialize result (InitializeResult.instructions).
|
||||||
|
instructions: str | None
|
||||||
|
|
||||||
_config: McpServerConfig
|
_config: McpServerConfig
|
||||||
_proc: asyncio.subprocess.Process | None
|
_proc: asyncio.subprocess.Process | None
|
||||||
@@ -111,6 +113,7 @@ class McpServerConnection:
|
|||||||
self.error = None
|
self.error = None
|
||||||
self.tools = []
|
self.tools = []
|
||||||
self.tools_stale = False
|
self.tools_stale = False
|
||||||
|
self.instructions = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
@@ -258,7 +261,7 @@ class McpServerConnection:
|
|||||||
self._send(payload)
|
self._send(payload)
|
||||||
|
|
||||||
async def _initialize(self) -> None:
|
async def _initialize(self) -> None:
|
||||||
_ = await self._request(
|
result = await self._request(
|
||||||
"initialize",
|
"initialize",
|
||||||
{
|
{
|
||||||
"protocolVersion": MCP_PROTOCOL_VERSION,
|
"protocolVersion": MCP_PROTOCOL_VERSION,
|
||||||
@@ -266,6 +269,10 @@ class McpServerConnection:
|
|||||||
"clientInfo": {"name": MCP_CLIENT_NAME, "version": _client_version()},
|
"clientInfo": {"name": MCP_CLIENT_NAME, "version": _client_version()},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
# Optional server usage guidance (MCP spec: InitializeResult.instructions).
|
||||||
|
# Hosts surface this to the model when the server's tools are in play.
|
||||||
|
instructions = result.get("instructions")
|
||||||
|
self.instructions = instructions.strip() if isinstance(instructions, str) and instructions.strip() else None
|
||||||
await self._notify("notifications/initialized")
|
await self._notify("notifications/initialized")
|
||||||
|
|
||||||
async def list_tools(self) -> list[McpTool]:
|
async def list_tools(self) -> list[McpTool]:
|
||||||
@@ -343,6 +350,21 @@ class McpManager:
|
|||||||
rows.append((name, connection.status, len(connection.tools)))
|
rows.append((name, connection.status, len(connection.tools)))
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
def instructions(self) -> list[tuple[str, str]]:
|
||||||
|
"""(server, text) initialize ``instructions`` for every connected server.
|
||||||
|
|
||||||
|
Only non-empty guidance is returned, in config order. Hosts may fold
|
||||||
|
these into the agent system context (see cli.state.ReplState).
|
||||||
|
"""
|
||||||
|
rows: list[tuple[str, str]] = []
|
||||||
|
for name in self.enabled_servers():
|
||||||
|
connection = self._connections.get(name)
|
||||||
|
if connection is not None and connection.connected:
|
||||||
|
text = (connection.instructions or "").strip()
|
||||||
|
if text:
|
||||||
|
rows.append((name, text))
|
||||||
|
return rows
|
||||||
|
|
||||||
def connections(self) -> list[McpServerConnection]:
|
def connections(self) -> list[McpServerConnection]:
|
||||||
return list(self._connections.values())
|
return list(self._connections.values())
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
"""Minimal MCP stdio server used by the client tests (echo + sleep tools)."""
|
"""Minimal MCP stdio server used by the client tests (echo + sleep tools).
|
||||||
|
|
||||||
|
Set ``PLYNGENT_MCP_FAKE_INSTRUCTIONS`` (env) to have the server return that
|
||||||
|
text as ``InitializeResult.instructions`` (usage guidance).
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
_FAKE_INSTRUCTIONS = os.environ.get("PLYNGENT_MCP_FAKE_INSTRUCTIONS", "")
|
||||||
|
|
||||||
|
|
||||||
def respond(request_id: int, result: object) -> None:
|
def respond(request_id: int, result: object) -> None:
|
||||||
sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": request_id, "result": result}) + "\n")
|
sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": request_id, "result": result}) + "\n")
|
||||||
@@ -22,14 +29,14 @@ def main() -> None:
|
|||||||
if request_id is None:
|
if request_id is None:
|
||||||
continue # notification (notifications/initialized)
|
continue # notification (notifications/initialized)
|
||||||
if method == "initialize":
|
if method == "initialize":
|
||||||
respond(
|
result: dict[str, object] = {
|
||||||
int(request_id),
|
"protocolVersion": "2025-06-18",
|
||||||
{
|
"capabilities": {"tools": {}},
|
||||||
"protocolVersion": "2025-06-18",
|
"serverInfo": {"name": "fake-mcp", "version": "0.1.0"},
|
||||||
"capabilities": {"tools": {}},
|
}
|
||||||
"serverInfo": {"name": "fake-mcp", "version": "0.1.0"},
|
if _FAKE_INSTRUCTIONS:
|
||||||
},
|
result["instructions"] = _FAKE_INSTRUCTIONS
|
||||||
)
|
respond(int(request_id), result)
|
||||||
elif method == "tools/list":
|
elif method == "tools/list":
|
||||||
respond(
|
respond(
|
||||||
int(request_id),
|
int(request_id),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ async def test_connection_handshake_lists_and_calls_tools() -> None:
|
|||||||
try:
|
try:
|
||||||
assert connection.connected
|
assert connection.connected
|
||||||
assert connection.error is None
|
assert connection.error is None
|
||||||
|
assert connection.instructions is None
|
||||||
assert [tool.name for tool in connection.tools] == ["echo", "fail", "slow"]
|
assert [tool.name for tool in connection.tools] == ["echo", "fail", "slow"]
|
||||||
echo = connection.tools[0]
|
echo = connection.tools[0]
|
||||||
assert echo.description == "Echo the given text back."
|
assert echo.description == "Echo the given text back."
|
||||||
@@ -43,6 +44,20 @@ async def test_connection_handshake_lists_and_calls_tools() -> None:
|
|||||||
await connection.aclose()
|
await connection.aclose()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_initialize_captures_server_instructions() -> None:
|
||||||
|
guidance = "Use echo to repeat text back to the user."
|
||||||
|
connection = McpServerConnection(
|
||||||
|
"fake",
|
||||||
|
_server_config(env={"PLYNGENT_MCP_FAKE_INSTRUCTIONS": guidance}),
|
||||||
|
)
|
||||||
|
await connection.start()
|
||||||
|
try:
|
||||||
|
assert connection.connected
|
||||||
|
assert connection.instructions == guidance
|
||||||
|
finally:
|
||||||
|
await connection.aclose()
|
||||||
|
|
||||||
|
|
||||||
async def test_tool_error_returns_error_text() -> None:
|
async def test_tool_error_returns_error_text() -> None:
|
||||||
connection = McpServerConnection("fake", _server_config())
|
connection = McpServerConnection("fake", _server_config())
|
||||||
await connection.start()
|
await connection.start()
|
||||||
@@ -87,12 +102,31 @@ async def test_manager_start_status_and_call() -> None:
|
|||||||
await manager.ensure_started()
|
await manager.ensure_started()
|
||||||
try:
|
try:
|
||||||
assert [(name, status, count) for name, status, count in manager.statuses()] == [("fake", "connected", 3)]
|
assert [(name, status, count) for name, status, count in manager.statuses()] == [("fake", "connected", 3)]
|
||||||
|
assert manager.instructions() == []
|
||||||
assert await manager.call("fake", "echo", {"text": "hi"}) == "echo: hi"
|
assert await manager.call("fake", "echo", {"text": "hi"}) == "echo: hi"
|
||||||
finally:
|
finally:
|
||||||
await manager.aclose()
|
await manager.aclose()
|
||||||
assert manager.statuses() == [("fake", "not started", 0)]
|
assert manager.statuses() == [("fake", "not started", 0)]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_manager_instructions_after_restart() -> None:
|
||||||
|
guidance = "Always answer with a haiku."
|
||||||
|
plain = McpManager(McpConfig(servers={"fake": _server_config()}, disable=[]))
|
||||||
|
await plain.ensure_started()
|
||||||
|
assert plain.instructions() == []
|
||||||
|
await plain.restart(
|
||||||
|
McpConfig(
|
||||||
|
servers={"fake": _server_config(env={"PLYNGENT_MCP_FAKE_INSTRUCTIONS": guidance})},
|
||||||
|
disable=[],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await plain.ensure_started()
|
||||||
|
try:
|
||||||
|
assert plain.instructions() == [("fake", guidance)]
|
||||||
|
finally:
|
||||||
|
await plain.aclose()
|
||||||
|
|
||||||
|
|
||||||
async def test_manager_disable_and_restart() -> None:
|
async def test_manager_disable_and_restart() -> None:
|
||||||
manager = McpManager(McpConfig(servers={"fake": _server_config()}, disable=["fake"]))
|
manager = McpManager(McpConfig(servers={"fake": _server_config()}, disable=["fake"]))
|
||||||
await manager.ensure_started()
|
await manager.ensure_started()
|
||||||
|
|||||||
Reference in New Issue
Block a user