mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
cli: fold MCP initialize instructions into agent prompt and /mcp
This commit is contained in:
@@ -1147,6 +1147,9 @@ def plugins_slash_cmd(state: ReplState, action: str | None, name: str | None) ->
|
||||
raise click.UsageError(str(exc)) from exc
|
||||
|
||||
|
||||
_MCP_PREVIEW_MAX_CHARS = 120
|
||||
|
||||
|
||||
def _print_mcp_status(state: ReplState) -> None:
|
||||
manager = state.mcp_manager
|
||||
if manager is None:
|
||||
@@ -1162,6 +1165,14 @@ def _print_mcp_status(state: ReplState) -> None:
|
||||
(conn for conn in manager.connections() if conn.name == name),
|
||||
None,
|
||||
)
|
||||
if connection is not None and connection.instructions:
|
||||
preview = " ".join(connection.instructions.split())
|
||||
if len(preview) > _MCP_PREVIEW_MAX_CHARS:
|
||||
preview = f"{preview[: _MCP_PREVIEW_MAX_CHARS - 3]}..."
|
||||
click.secho(
|
||||
f" instructions ({len(connection.instructions)} chars): {preview}",
|
||||
fg="bright_black",
|
||||
)
|
||||
if connection is not None and connection.error is not None:
|
||||
for line in connection.stderr_tail()[-3:]:
|
||||
click.secho(f" {line}", fg="bright_black")
|
||||
|
||||
@@ -237,14 +237,28 @@ class ReplState:
|
||||
session_state=self.session_state,
|
||||
)
|
||||
|
||||
def _mcp_instructions_text(self) -> str:
|
||||
"""Compose connected MCP servers' initialize ``instructions`` into one block.
|
||||
|
||||
Only meaningful when tools are on (MCP tools are registered under the
|
||||
same condition); the text is attributed per server so the model can
|
||||
weigh its provenance. Gated by ``[agent] mcp_instructions`` in callers.
|
||||
"""
|
||||
if self.mcp_manager is None or not self.tools_enabled:
|
||||
return ""
|
||||
parts = [f"MCP server {name!r} instructions:\n{text}" for name, text in self.mcp_manager.instructions()]
|
||||
return "\n\n".join(parts)
|
||||
|
||||
def _make_agent(self) -> ChatAgent:
|
||||
from plyngent.cli.limits import prompt_continue_limit_async
|
||||
from plyngent.config import compose_agent_system_content
|
||||
|
||||
agent_cfg = self.config.agent_config
|
||||
mcp_text = self._mcp_instructions_text() if agent_cfg.mcp_instructions else ""
|
||||
system_prompt = compose_agent_system_content(
|
||||
agent_cfg.system_prompt,
|
||||
agent_cfg.tool_directives,
|
||||
mcp_text,
|
||||
)
|
||||
on_limit = prompt_continue_limit_async if self.interactive_limits else None
|
||||
peak = 0
|
||||
|
||||
@@ -120,3 +120,111 @@ async def test_mcp_tools_registered_on_tools_rebuild(state: ReplState) -> None:
|
||||
assert out == "echo: hi"
|
||||
finally:
|
||||
await manager.aclose()
|
||||
|
||||
|
||||
async def test_slash_mcp_list_shows_instructions(
|
||||
state: ReplState,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
guidance = "Prefer `echo` for repeating text back."
|
||||
manager = McpManager(
|
||||
McpConfig(
|
||||
servers={
|
||||
"fake": _server_config(env={"PLYNGENT_MCP_FAKE_INSTRUCTIONS": guidance}),
|
||||
},
|
||||
disable=[],
|
||||
)
|
||||
)
|
||||
await manager.ensure_started()
|
||||
state.mcp_manager = manager
|
||||
try:
|
||||
assert await handle_slash(state, "/mcp list") is True
|
||||
out = capsys.readouterr().out
|
||||
assert f"instructions ({len(guidance)} chars)" in out
|
||||
assert guidance in out
|
||||
finally:
|
||||
await manager.aclose()
|
||||
|
||||
|
||||
async def _make_tools_state(
|
||||
tmp_path: Path,
|
||||
*,
|
||||
agent_toml: str,
|
||||
manager: McpManager,
|
||||
) -> tuple[ReplState, MemoryStore]:
|
||||
"""ReplState with tools on + a started MCP manager (agent built from TOML)."""
|
||||
memory = await MemoryStore.open(DatabaseConfig())
|
||||
provider = OpenAIProvider(access_key_or_token="sk-test")
|
||||
document = tomlkit.parse(agent_toml) if agent_toml else tomlkit.document()
|
||||
config = ConfigStore(path=tmp_path / "plyngent.toml", document=document)
|
||||
config.providers = {"local": provider}
|
||||
st = ReplState(
|
||||
config=config,
|
||||
memory=memory,
|
||||
workspace=tmp_path,
|
||||
provider_name="local",
|
||||
provider=provider,
|
||||
model="gpt-test",
|
||||
tools_enabled=True,
|
||||
mcp_manager=manager,
|
||||
)
|
||||
return st, memory
|
||||
|
||||
|
||||
async def test_mcp_instructions_folded_into_agent_system_prompt(tmp_path: Path) -> None:
|
||||
from plyngent.tools.catalog import catalog_scope
|
||||
|
||||
guidance = "Prefer `echo` for repeating text back to the user."
|
||||
manager = McpManager(
|
||||
McpConfig(
|
||||
servers={
|
||||
"fake": _server_config(env={"PLYNGENT_MCP_FAKE_INSTRUCTIONS": guidance}),
|
||||
},
|
||||
disable=[],
|
||||
)
|
||||
)
|
||||
await manager.ensure_started()
|
||||
try:
|
||||
with catalog_scope(empty=True):
|
||||
state, memory = await _make_tools_state(tmp_path, agent_toml="", manager=manager)
|
||||
try:
|
||||
prompt = state.agent.system_prompt
|
||||
assert prompt is not None
|
||||
assert "MCP server 'fake' instructions:" in prompt
|
||||
assert guidance in prompt
|
||||
finally:
|
||||
await memory.close()
|
||||
finally:
|
||||
await manager.aclose()
|
||||
|
||||
|
||||
async def test_mcp_instructions_flag_off_skips_server_text(tmp_path: Path) -> None:
|
||||
from plyngent.tools.catalog import catalog_scope
|
||||
|
||||
guidance = "Prefer `echo` for repeating text back to the user."
|
||||
manager = McpManager(
|
||||
McpConfig(
|
||||
servers={
|
||||
"fake": _server_config(env={"PLYNGENT_MCP_FAKE_INSTRUCTIONS": guidance}),
|
||||
},
|
||||
disable=[],
|
||||
)
|
||||
)
|
||||
await manager.ensure_started()
|
||||
try:
|
||||
with catalog_scope(empty=True):
|
||||
state, memory = await _make_tools_state(
|
||||
tmp_path,
|
||||
agent_toml="[agent]\nmcp_instructions = false\n",
|
||||
manager=manager,
|
||||
)
|
||||
try:
|
||||
prompt = state.agent.system_prompt
|
||||
assert prompt is not None
|
||||
assert guidance not in prompt
|
||||
assert "MCP server" not in prompt
|
||||
assert "### Workspace" in prompt # host tool playbook still composed
|
||||
finally:
|
||||
await memory.close()
|
||||
finally:
|
||||
await manager.aclose()
|
||||
|
||||
Reference in New Issue
Block a user