# MCP Protocol Proxy Inventory This document records observed behavior of the `wf-mcp` server against real upstream MCP servers. It is not a design spec. It is a fact log for deciding which proxy features need explicit implementation. ## Live Everything Server Probe Probe date: 2026-05-13 Server command in config: ```text pnpx @modelcontextprotocol/server-everything ``` Connection id: ```text everything.default ``` Proxy mode: ```text wf-mcp serve ``` ### Working - `tools/list` shows Everything tools through `wf-mcp`. - `tools/call` works for normal tools such as `everything.default.echo`. - Tool metadata is preserved for proxy inventory, including title, description, and JSON input schema. - Annotated text content is preserved. - Tiny image content returns through the proxy. - Structured content returns structured JSON. - `trigger-long-running-operation` completes through the proxy as a normal tool call. - `toggle-simulated-logging` and `toggle-subscriber-updates` return success. - Native listed resources are namespaced by FastMCP mount/proxy transforms. - Native listed resource templates are namespaced by FastMCP mount/proxy transforms. - Namespaced static and dynamic resources can be read through `wf-mcp`. Example listed resource mapping: ```text upstream: demo://resource/static/document/instructions.md proxied: demo://everything/default/resource/static/document/instructions.md ``` ### Gaps - Resource links embedded inside tool results are not rewritten by the proxy. For example, `get-resource-links` returns raw upstream URIs such as: ```text demo://resource/dynamic/text/2 ``` `wf-mcp` cannot read that raw URI. The manually namespaced URI works for normal dynamic resources: ```text demo://everything/default/resource/dynamic/text/2 ``` - Session resource links from `gzip-file-as-resource` are not currently usable through `wf-mcp`. The tool returned: ```text demo://resource/session/probe.txt ``` Neither the raw URI nor the manually namespaced URI was readable through `wf-mcp` during the live probe. Direct `everything` read also failed in this Codex session, so this may be session-affinity, lifecycle, or returned-link behavior rather than only URI rewriting. - `simulate-research-query` is discovered, but calling it fails cleanly: ```text Tool simulate-research-query requires task augmentation (taskSupport: 'required') ``` This is a task/protocol support gap, not an ordinary tool-call failure. - Logging and subscriber update tools return success, but no separate logging or resource-update notification was visible in the Codex tool channel during this probe. - 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. - Proxy resource list/read projection: healthy for listed resources and templates. - Tool-result resource links: ordinary embedded resource URIs are now rewritten by a local proxy transform so downstream clients receive namespaced URIs. - `wf_mcp.proxy_results` contains pure typed helpers for rewriting `mcp.types.ResourceLink` content and a small FastMCP workaround transform that wraps proxied tools until upstream FastMCP handles this projection itself. - Session resources: unresolved; needs a focused test because session affinity may matter. - Tasks: unsupported; task-required tools should remain clearly diagnosed until 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 - Add automated tests for listed resource/template namespacing through unified mode. - Keep the fixture regression proving ordinary tool-returned `resource_link` URIs are rewritten into downstream-facing namespaced URIs. - Investigate session resource lifecycle separately from URI rewriting. - Inventory current FastMCP/MCP SDK APIs for tasks, logging notifications, and resource update notifications before implementing protocol forwarding. ## Notification Inventory Spec version checked: 2025-11-25 Official spec pages checked: - - - - Installed local packages checked: - `fastmcp==3.3.0` - `mcp==1.27.0` ### Protocol Notifications The MCP spec defines these relevant server-to-client notifications: | Event | MCP notification | Notes | | --- | --- | --- | | Tool catalog changed | `notifications/tools/list_changed` | Requires server `tools.listChanged` capability. | | Resource catalog changed | `notifications/resources/list_changed` | Requires server `resources.listChanged` capability. | | Prompt catalog changed | `notifications/prompts/list_changed` | Requires server `prompts.listChanged` capability. | | Specific resource updated | `notifications/resources/updated` | Requires resource subscription flow. Params include resource `uri`. | | Request progress | `notifications/progress` | Requires an active request with `_meta.progressToken`. | | Logging message | `notifications/message` | Uses MCP logging level and client logging preferences. | | Task status | `notifications/tasks/status` | Used by MCP task-augmented execution. | The spec also defines client-to-server requests that matter for notifications: | Request | Purpose | | --- | --- | | `resources/subscribe` | Client subscribes to updates for a specific resource URI. | | `resources/unsubscribe` | Client stops receiving updates for a specific resource URI. | | request `_meta.progressToken` | Client asks for progress notifications for that request. | ### FastMCP / SDK Reality MCP SDK has concrete notification types in `mcp.types`, including: - `ToolListChangedNotification` - `ResourceListChangedNotification` - `PromptListChangedNotification` - `ResourceUpdatedNotification` - `ProgressNotification` - `LoggingMessageNotification` - `TaskStatusNotification` FastMCP low-level server advertises notification support for all three list changed capabilities by default: ```text prompts_changed=True resources_changed=True tools_changed=True ``` FastMCP `Context` exposes request-scoped helpers: - `ctx.send_notification(notification)` - `ctx.report_progress(progress, total=None, message=None)` - `ctx.log(...)`, plus convenience `ctx.info`, `ctx.warning`, `ctx.error`, etc. - `ctx.enable_components(...)`, `ctx.disable_components(...)`, and `ctx.reset_visibility(...)`, which send list-changed notifications for the current session. Important constraint: no global `FastMCP.notify_all(...)` style API was found. The obvious notification path is request/session scoped through FastMCP `Context` or lower-level server sessions. This matters because our internal event bus can emit outside a currently executing MCP tool call, while MCP notifications need a connected client session. ### Observed Upstream Relay Behavior Fixture probe date: 2026-05-16 The fixture server exposes `emit_notifications_tool`, which emits these notifications during one ordinary tool call: | Notification | Direct upstream client | Through unified proxy | | --- | --- | --- | | `notifications/tools/list_changed` | observed | not observed | | `notifications/resources/list_changed` | observed | not observed | | `notifications/prompts/list_changed` | observed | not observed | | `notifications/resources/updated` | observed | not observed | | `notifications/message` | observed | not observed | This is covered by `tests/wf_mcp/test_protocol_relay.py`. The current conclusion is narrow but important: ordinary mounted proxy tool calls do **not** automatically relay upstream server notifications to the downstream client, even though the same upstream emits those notifications correctly to a direct MCP client. The upstream logging notification is consumed locally enough for FastMCP to log it, but it is not re-emitted downstream. Still unproven: - `notifications/progress`, because it requires a request with `_meta.progressToken` - `notifications/tasks/status`, because task-augmented execution is not implemented yet - whether future explicit relay code should project list-changed notifications one-for-one, coalesce them, or translate them into local catalog refresh events first - whether future explicit relay code should use FastMCP's public proxy hooks or a small local wrapper around the upstream message handler ## Capability Negotiation Inventory Fixture probe date: 2026-05-16 This pass compares one direct fixture-server initialization with the same fixture mounted through unified proxy mode. | Capability field | Direct fixture server | Unified proxy | | --- | --- | --- | | `tools.listChanged` | `false` | `true` | | `resources.subscribe` | `false` | `false` | | `resources.listChanged` | `false` | `true` | | `prompts.listChanged` | `false` | `true` | | `logging` | absent | present | | `extensions["io.modelcontextprotocol/ui"]` | absent | present | This is covered by `tests/wf_mcp/test_protocol_capabilities.py`. Two conclusions matter: 1. Unified proxy mode currently advertises the FastMCP server surface it can serve locally, not a union of the mounted upstream server capability objects. 2. The missing upstream notification relay is not explained by current capability negotiation. In the same fixture, direct upstream calls receive notifications even though the fixture advertises `listChanged=false`, while unified proxy advertises `listChanged=true` for its own local surface and still does not forward upstream notifications. Current truthful interpretation: - `tools.listChanged`, `resources.listChanged`, and `prompts.listChanged` are truthful for **local** proxy reload behavior because `wf.admin.reload_config` emits those notifications to the current downstream session. - They do not imply upstream list-changed notifications are relayed. - `resources.subscribe` remains `false`, which matches current lack of subscription ownership/relay. - The public initialize result does not yet expose per-upstream differences; that still belongs in future admin inventory rather than the coarse top-level capability object. ### Mapping From wf-mcp Events Current internal events that can map to MCP notifications: | Internal event | Candidate MCP notification | Caveat | | --- | --- | --- | | `tools_changed` | `notifications/tools/list_changed` | Needs a session-aware sink. | | `resources_changed` | `notifications/resources/list_changed` | Needs a session-aware sink. | | `prompts_changed` | `notifications/prompts/list_changed` | Needs a session-aware sink. | | future `resource_updated` | `notifications/resources/updated` | Requires subscription ownership and namespaced URI mapping. | | future `workflow_run_progress` | `notifications/progress` | Only valid when a caller supplied a progress token. | | future broker log events | `notifications/message` | Needs client logging level behavior. | | future workflow task events | `notifications/tasks/status` | Should wait for task execution support. | ### Implementation Guidance Do not emit MCP notifications directly from `WfMcpService`. Recommended shape: ```text WfMcpService -> EventBus -> InMemoryEventSink -> later: McpSessionNotificationSink ``` Implemented local notification pieces: - pure internal-event to `mcp.types.ServerNotification` mapping - recording sink for protocol projection tests - FastMCP `Context` sink for request-scoped notification emission - `wf.admin.reload_config` sends tool/resource/prompt list-changed notifications to the current client session This is still not a global broadcast system. It only emits through an active request context, which matches FastMCP's available public API today. ### Concrete Notification Plan Implement notifications in this order. 1. Done: add a pure mapping layer from internal events to MCP notification objects. ```text tools_changed -> ToolListChangedNotification resources_changed -> ResourceListChangedNotification prompts_changed -> PromptListChangedNotification ``` This layer should not know about FastMCP sessions. It should be easy to test with plain `mcp.types` objects. 1. Done: add a fake/test notification sink. The first sink should only record which MCP notification objects would be sent. This proves the event-to-notification mapping without depending on Codex, Inspector, stdio behavior, or Streamable HTTP behavior. 1. Done: add a FastMCP `Context` notification sink. This sink can call: ```text ctx.send_notification(...) ``` It is only valid while handling a request that has an active FastMCP context. This should be treated as a session-scoped projection, not a global broadcast system. 1. Partly done: wire local admin operations first. Best first live target: ```text wf.admin.reload_config ``` Current behavior: - performs the reload - sends tool/resource/prompt list-changed MCP notifications to the current client session when a FastMCP context is available Remaining cleanup: - also emit internal catalog/tool/resource/prompt change events from the transparent runtime path, so broker history and protocol notifications share one source of truth This is intentionally local. It does not require solving upstream notification forwarding. 1. Verify with Inspector/Codex. Expected outcomes: - clients that honor list-changed notifications refresh their tool/resource/ prompt lists - clients that ignore notifications can still manually call list/search tools - no workflow or broker correctness depends on notification delivery 1. Done: investigate baseline upstream notification forwarding. Direct fixture-server tests show that list-changed, resource-updated, and logging notifications are emitted upstream but are not forwarded automatically through mounted proxy tool calls. Any relay support here will be explicit work, and `notifications/resources/updated` will also need namespaced resource URIs. ### Deferred Notification Work - `notifications/resources/updated`: requires explicit resource subscription ownership and URI rewriting. - `notifications/progress`: requires an active request `_meta.progressToken`. - `notifications/message`: should respect client logging level behavior. - `notifications/tasks/status`: should wait for task-augmented execution. - Upstream-to-downstream notification forwarding: proven absent for ordinary mounted proxy tool calls; needs an explicit bridge if we want it. ### Open Questions - Which upstream notifications should be forwarded one-for-one, and which ones should be converted into local catalog refresh events first? - Can we attach a durable notification sink to every active FastMCP session without relying on private APIs? - Does Codex surface list-changed, resource-updated, logging, or progress notifications from an MCP server in this environment? - For streamable HTTP, does FastMCP event-store support make notification replay possible for disconnected clients?