notifications/tools/list_changed doesnt reload codex? codex bug?
This commit is contained in:
+6
-2
@@ -36,7 +36,9 @@ async def downstream_message_handler(message: object) -> None:
|
||||
downstream_seen.append(message.root.method)
|
||||
|
||||
|
||||
upstream_transport = FastMCPTransport(server) # any transport will do, tried python stdio, and fastmcp, both reproduce.
|
||||
upstream_transport = FastMCPTransport(
|
||||
server
|
||||
) # any transport will do, tried python stdio, and fastmcp, both reproduce.
|
||||
|
||||
|
||||
async def prog() -> None:
|
||||
@@ -60,7 +62,9 @@ async def prog() -> None:
|
||||
"notifications/message",
|
||||
]
|
||||
assert upstream_seen == seen
|
||||
assert downstream_seen == seen, "bug here: notifications should be forwarded by the proxy"
|
||||
assert downstream_seen == seen, (
|
||||
"bug here: notifications should be forwarded by the proxy"
|
||||
)
|
||||
|
||||
|
||||
asyncio.run(prog())
|
||||
|
||||
@@ -93,6 +93,14 @@ Tool simulate-research-query requires task augmentation (taskSupport: 'required'
|
||||
- Sampling was not visible as a direct listed tool in the current Everything
|
||||
server inventory.
|
||||
|
||||
- Dynamic tool-list adoption is harness-dependent. On 2026-05-17, after
|
||||
enabling `playwright.default` and reloading `wf-mcp`, the server-side admin
|
||||
inventory showed the new proxy tools immediately. After restarting Codex, the
|
||||
UI could show `playwright.default.browser_navigate`, but the current model turn
|
||||
still did not receive a callable tool binding for it. That means a host can
|
||||
observe an updated `tools/list` without rebuilding the tool schema supplied to
|
||||
an already-running model interaction.
|
||||
|
||||
### Current Classification
|
||||
|
||||
- Proxy tool projection: healthy for ordinary tools.
|
||||
@@ -109,6 +117,9 @@ Tool simulate-research-query requires task augmentation (taskSupport: 'required'
|
||||
MCP task support is implemented.
|
||||
- Notifications/logging/subscriptions: unverified; current client did not show
|
||||
forwarded notifications.
|
||||
- Dynamic tool-list adoption: server-side updates work, but LLM harnesses may
|
||||
keep a stale or separately-materialized callable tool schema even after a UI
|
||||
refresh or reconnect.
|
||||
|
||||
## Next Test Targets
|
||||
|
||||
|
||||
@@ -99,6 +99,36 @@ classified as a proxy bug.
|
||||
Task-required tools are discoverable but not usable through our current surface.
|
||||
This is a real protocol-support gap, not an ordinary tool-call issue.
|
||||
|
||||
### 5. LLM harnesses may not adopt dynamic tool changes reliably
|
||||
|
||||
`wf-mcp` can expose new tools after reload, but that does not guarantee an agent
|
||||
harness rebuilds the callable tool schema for the model turn that is already in
|
||||
flight.
|
||||
|
||||
Observed on 2026-05-17 with Codex:
|
||||
|
||||
- `playwright.default` was enabled
|
||||
- `wf.admin.reload_config` remounted it successfully
|
||||
- `wf.admin.list_proxy_tools` showed the new tools
|
||||
- after reconnect, the Codex UI showed `playwright.default.browser_navigate`
|
||||
- the model turn still did not receive a callable binding for that new tool
|
||||
|
||||
This is a common Codex / Claude Code / general LLM harness class of problem, not
|
||||
just a proxy-server problem. The host may refresh `tools/list` for display or
|
||||
inspection while model invocation still uses a previously materialized tool
|
||||
schema.
|
||||
|
||||
Design consequence: do not make core workflows depend on newly registered MCP
|
||||
tools becoming callable immediately. Prefer stable control tools plus explicit
|
||||
inspection/call paths when the client must work reliably across harnesses.
|
||||
|
||||
When the exposed tool catalog grows large or changes often, FastMCP's search
|
||||
transform is a good mitigation: keep a stable pinned control/workflow spine
|
||||
visible, and use `search_tools` plus its synthetic `call_tool` for the changing
|
||||
rest of the catalog. Do not confuse that synthetic raw-tool caller with a
|
||||
future workflow-capability test tool; testing a normalized `NodeSpec` contract
|
||||
is a separate operation and should also remain pinned once it exists.
|
||||
|
||||
## What Is Probably Not Worth Owning Yet
|
||||
|
||||
Do **not** rush to implement a custom full protocol relay for:
|
||||
|
||||
@@ -47,9 +47,11 @@ wf.workflow.run_deployment
|
||||
```
|
||||
|
||||
This is important because MCP clients may not reliably refresh `tools/list` when
|
||||
new workflow artifacts are saved. A stable `run_deployment` tool lets an LLM
|
||||
test saved workflows immediately without requiring dynamic tool registration or
|
||||
tool-list notifications to work perfectly.
|
||||
new workflow artifacts are saved. More specifically, LLM harnesses may refresh
|
||||
or display a changed tool list without rebuilding the callable tool schema used
|
||||
for the current model turn. A stable `run_deployment` tool lets an LLM test saved
|
||||
workflows immediately without requiring dynamic tool registration or tool-list
|
||||
notifications to work perfectly.
|
||||
|
||||
This control surface should not fork between backend service layers. The public
|
||||
server now has one exposure style, but internally it still combines:
|
||||
|
||||
@@ -32,7 +32,9 @@ def register_service_admin_tools(
|
||||
}
|
||||
|
||||
def name(local_name: str) -> str:
|
||||
visible_name = legacy_name_map.get(local_name, local_name) if legacy_names else local_name
|
||||
visible_name = (
|
||||
legacy_name_map.get(local_name, local_name) if legacy_names else local_name
|
||||
)
|
||||
return visible_name if namespace is None else f"{namespace}.{visible_name}"
|
||||
|
||||
if include_connection_tools:
|
||||
|
||||
@@ -24,4 +24,3 @@ def create_broker_server(service: WfMcpService) -> FastMCP:
|
||||
register_broker_resources(server, service)
|
||||
register_broker_prompts(server, service)
|
||||
return server
|
||||
|
||||
|
||||
@@ -24,18 +24,21 @@ from .tools import (
|
||||
)
|
||||
from .reload_events import ProxyReloadResult, reload_change_events
|
||||
|
||||
_ADMIN_TOOL_NAMES = [
|
||||
_SEARCH_ALWAYS_VISIBLE_TOOL_NAMES = [
|
||||
# Stable discovery/control spine.
|
||||
f"{ADMIN_NAMESPACE}.list_sources",
|
||||
f"{ADMIN_NAMESPACE}.list_connections",
|
||||
f"{ADMIN_NAMESPACE}.get_connection_statuses",
|
||||
f"{ADMIN_NAMESPACE}.get_config",
|
||||
f"{ADMIN_NAMESPACE}.reload_config",
|
||||
f"{ADMIN_NAMESPACE}.list_proxy_tools",
|
||||
f"{ADMIN_NAMESPACE}.get_proxy_tool",
|
||||
f"{ADMIN_NAMESPACE}.add_connection",
|
||||
f"{ADMIN_NAMESPACE}.update_connection",
|
||||
f"{ADMIN_NAMESPACE}.enable_connection",
|
||||
f"{ADMIN_NAMESPACE}.disable_connection",
|
||||
f"{ADMIN_NAMESPACE}.remove_connection",
|
||||
# Stable workflow control surface. Keep future workflow-capability test
|
||||
# tools pinned here too; they are distinct from raw MCP tool execution.
|
||||
"wf.workflow.list_artifacts",
|
||||
"wf.workflow.inspect_artifact",
|
||||
"wf.workflow.list_deployments",
|
||||
"wf.workflow.validate_deployment",
|
||||
"wf.workflow.run_deployment",
|
||||
]
|
||||
|
||||
|
||||
@@ -81,7 +84,7 @@ class ProxyRuntime:
|
||||
self.server.add_transform(PromptsAsTools(self.server))
|
||||
if search_tools:
|
||||
self.server.add_transform(
|
||||
BM25SearchTransform(always_visible=_ADMIN_TOOL_NAMES)
|
||||
BM25SearchTransform(always_visible=_SEARCH_ALWAYS_VISIBLE_TOOL_NAMES)
|
||||
)
|
||||
|
||||
def current_config(self) -> BrokerConfig:
|
||||
|
||||
@@ -101,6 +101,49 @@ def test_server_can_hide_admin_tools() -> None:
|
||||
asyncio.run(run_proxy())
|
||||
|
||||
|
||||
def test_server_search_mode_pins_stable_control_and_workflow_tools() -> None:
|
||||
config = BrokerConfig(
|
||||
store_root=local_temp_root() / "server_search_mode_store",
|
||||
connections=[
|
||||
ConnectionConfig(
|
||||
id="fixture.personal",
|
||||
server="fixture",
|
||||
account="personal",
|
||||
metadata={
|
||||
"transport": "stdio",
|
||||
"command": sys.executable,
|
||||
"args": [fixture_server_path()],
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
async def run_proxy() -> None:
|
||||
client = create_server_client(config, search_tools=True)
|
||||
async with client:
|
||||
tools = await client.list_tools()
|
||||
names = [tool.name for tool in tools]
|
||||
|
||||
assert "search_tools" in names
|
||||
assert "call_tool" in names
|
||||
assert "wf.admin.list_sources" in names
|
||||
assert "wf.admin.list_connections" in names
|
||||
assert "wf.admin.get_connection_statuses" in names
|
||||
assert "wf.admin.reload_config" in names
|
||||
assert "wf.admin.list_proxy_tools" in names
|
||||
assert "wf.admin.get_proxy_tool" in names
|
||||
assert "wf.workflow.list_artifacts" in names
|
||||
assert "wf.workflow.inspect_artifact" in names
|
||||
assert "wf.workflow.list_deployments" in names
|
||||
assert "wf.workflow.validate_deployment" in names
|
||||
assert "wf.workflow.run_deployment" in names
|
||||
|
||||
assert "wf.admin.call_tool" not in names
|
||||
assert "fixture.personal.echo_tool" not in names
|
||||
|
||||
asyncio.run(run_proxy())
|
||||
|
||||
|
||||
def test_workflow_tools_have_human_metadata() -> None:
|
||||
config = BrokerConfig(
|
||||
store_root=local_temp_root() / "unified_metadata_store",
|
||||
|
||||
@@ -88,12 +88,14 @@ def test_service_rejects_reserved_connection_ids() -> None:
|
||||
def test_service_installs_builtin_stdlib_specs_by_default() -> None:
|
||||
service = WfMcpService(store=FileStore(local_temp_root() / "builtin_store"))
|
||||
|
||||
assert "wf.std.runtime_error" in service.capability_sources[
|
||||
"wf.std"
|
||||
].capabilities.node_specs
|
||||
assert "wf.mcp.call_tool" in service.capability_sources[
|
||||
"wf.mcp"
|
||||
].capabilities.node_specs
|
||||
assert (
|
||||
"wf.std.runtime_error"
|
||||
in service.capability_sources["wf.std"].capabilities.node_specs
|
||||
)
|
||||
assert (
|
||||
"wf.mcp.call_tool"
|
||||
in service.capability_sources["wf.mcp"].capabilities.node_specs
|
||||
)
|
||||
|
||||
|
||||
def test_service_lists_all_capability_sources_with_owned_capability_names() -> None:
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
"id": "playwright.default",
|
||||
"server": "playwright",
|
||||
"account": "default",
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"metadata": {
|
||||
"transport": "stdio",
|
||||
"command": "pnpx",
|
||||
|
||||
Reference in New Issue
Block a user