16 KiB
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:
pnpx @modelcontextprotocol/server-everything
Connection id:
everything.default
Proxy mode:
wf-mcp serve
Working
tools/listshows Everything tools throughwf-mcp.tools/callworks for normal tools such aseverything.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-operationcompletes through the proxy as a normal tool call.toggle-simulated-loggingandtoggle-subscriber-updatesreturn 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:
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-linksreturns raw upstream URIs such as:demo://resource/dynamic/text/2wf-mcpcannot read that raw URI. The manually namespaced URI works for normal dynamic resources:demo://everything/default/resource/dynamic/text/2 -
Session resource links from
gzip-file-as-resourceare not currently usable throughwf-mcp. The tool returned:demo://resource/session/probe.txtNeither the raw URI nor the manually namespaced URI was readable through
wf-mcpduring the live probe. Directeverythingread also failed in this Codex session, so this may be session-affinity, lifecycle, or returned-link behavior rather than only URI rewriting. -
simulate-research-queryis discovered, but calling it fails cleanly: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.defaultand reloadingwf-mcp, the server-side admin inventory showed the new proxy tools immediately. After restarting Codex, the UI could showplaywright.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 updatedtools/listwithout 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_resultscontains pure typed helpers for rewritingmcp.types.ResourceLinkcontent 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_linkURIs 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:
- https://modelcontextprotocol.io/specification/2025-11-25/server/tools
- https://modelcontextprotocol.io/specification/2025-11-25/server/resources
- https://modelcontextprotocol.io/specification/2025-11-25/server/prompts
- https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress
Installed local packages checked:
fastmcp==3.3.0mcp==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:
ToolListChangedNotificationResourceListChangedNotificationPromptListChangedNotificationResourceUpdatedNotificationProgressNotificationLoggingMessageNotificationTaskStatusNotification
FastMCP low-level server advertises notification support for all three list changed capabilities by default:
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 conveniencectx.info,ctx.warning,ctx.error, etc.ctx.enable_components(...),ctx.disable_components(...), andctx.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.progressTokennotifications/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:
- Unified proxy mode currently advertises the FastMCP server surface it can serve locally, not a union of the mounted upstream server capability objects.
- 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 advertiseslistChanged=truefor its own local surface and still does not forward upstream notifications.
Current truthful interpretation:
tools.listChanged,resources.listChanged, andprompts.listChangedare truthful for local proxy reload behavior becausewf.admin.reload_configemits those notifications to the current downstream session.- They do not imply upstream list-changed notifications are relayed.
resources.subscriberemainsfalse, 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:
WfMcpService
-> EventBus
-> InMemoryEventSink
-> later: McpSessionNotificationSink
Implemented local notification pieces:
- pure internal-event to
mcp.types.ServerNotificationmapping - recording sink for protocol projection tests
- FastMCP
Contextsink for request-scoped notification emission wf.admin.reload_configsends 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.
- Done: add a pure mapping layer from internal events to MCP notification objects.
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.
- 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.
- Done: add a FastMCP
Contextnotification sink.
This sink can call:
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.
- Partly done: wire local admin operations first.
Best first live target:
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.
- 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
- 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?