document current behavior of notif and capabilities
This commit is contained in:
@@ -133,7 +133,7 @@ Official spec pages checked:
|
||||
|
||||
Installed local packages checked:
|
||||
|
||||
- `fastmcp==3.2.4`
|
||||
- `fastmcp==3.3.0`
|
||||
- `mcp==1.27.0`
|
||||
|
||||
### Protocol Notifications
|
||||
@@ -194,6 +194,79 @@ The obvious notification path is request/session scoped through FastMCP
|
||||
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
|
||||
|
||||
## 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:
|
||||
@@ -298,12 +371,12 @@ Expected outcomes:
|
||||
- clients that ignore notifications can still manually call list/search tools
|
||||
- no workflow or broker correctness depends on notification delivery
|
||||
|
||||
6. Only after local list-changed works, investigate upstream notification
|
||||
forwarding.
|
||||
6. Done: investigate baseline upstream notification forwarding.
|
||||
|
||||
Upstream forwarding is a separate problem because mounted proxies may or may
|
||||
not already forward upstream notifications. It also needs namespaced resource
|
||||
URIs for `notifications/resources/updated`.
|
||||
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
|
||||
|
||||
@@ -312,13 +385,13 @@ URIs for `notifications/resources/updated`.
|
||||
- `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: needs a focused FastMCP proxy
|
||||
behavior test before implementation.
|
||||
- Upstream-to-downstream notification forwarding: proven absent for ordinary
|
||||
mounted proxy tool calls; needs an explicit bridge if we want it.
|
||||
|
||||
### Open Questions
|
||||
|
||||
- Can a mounted FastMCP proxy forward upstream notifications to the downstream
|
||||
client automatically, or only notifications generated by local handlers?
|
||||
- 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
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
# MCP Protocol Relay Design
|
||||
|
||||
## Goal
|
||||
|
||||
Make `wf-mcp` a truthful high-quality MCP proxy, not merely a tool forwarder.
|
||||
|
||||
The next phase is about understanding and then projecting advanced MCP protocol
|
||||
behavior correctly:
|
||||
|
||||
- upstream notifications
|
||||
- server capability advertisement
|
||||
- downstream client capability relay
|
||||
- later request forwarding for client-side features such as elicitation, roots,
|
||||
sampling, and task-augmented requests
|
||||
|
||||
The immediate next work is still **investigation first**, because advertising a
|
||||
capability before we can bridge it end to end would make `wf-mcp` lie to clients.
|
||||
|
||||
## Current State
|
||||
|
||||
`wf-mcp` already has:
|
||||
|
||||
- a unified MCP surface
|
||||
- proxy mounting with stable public names
|
||||
- local list-changed notifications after reload
|
||||
- ordinary tool/resource/prompt proxying
|
||||
- resource-link URI rewriting for ordinary tool-returned `ResourceLink`s
|
||||
|
||||
Known remaining uncertainty:
|
||||
|
||||
- which upstream notifications FastMCP proxying already forwards
|
||||
- which advanced MCP requests FastMCP can already bridge
|
||||
- how initialization capabilities should be projected through a broker with
|
||||
multiple enabled upstream connections
|
||||
|
||||
## Core Distinction
|
||||
|
||||
MCP has two different capability directions.
|
||||
|
||||
### Server capabilities
|
||||
|
||||
These are advertised by `wf-mcp` to its downstream client:
|
||||
|
||||
- `tools`
|
||||
- `resources`
|
||||
- `prompts`
|
||||
- `logging`
|
||||
- `completions`
|
||||
- `tasks`
|
||||
|
||||
`wf-mcp` may advertise a server capability only when the unified server can
|
||||
actually serve it. The correct eventual shape is a **selected union**:
|
||||
|
||||
- local capabilities implemented by `wf-mcp`
|
||||
- plus enabled upstream server capabilities that `wf-mcp` can proxy safely
|
||||
|
||||
### Client capabilities
|
||||
|
||||
These are advertised by the downstream client to `wf-mcp`:
|
||||
|
||||
- `roots`
|
||||
- `sampling`
|
||||
- `elicitation`
|
||||
- client-side `tasks`
|
||||
|
||||
`wf-mcp` must not advertise these as its own server capabilities. Instead, when
|
||||
an upstream server needs one of them, `wf-mcp` may relay the downstream client's
|
||||
real capabilities upstream only if the corresponding request path can also be
|
||||
forwarded correctly.
|
||||
|
||||
Examples:
|
||||
|
||||
- If the downstream client supports elicitation and `wf-mcp` can relay
|
||||
`elicitation/create`, an upstream server may use elicitation through the
|
||||
proxy.
|
||||
- If the downstream client supports roots and `wf-mcp` can relay `roots/list`,
|
||||
an upstream server may request roots through the proxy.
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Truthful advertisement**
|
||||
Never advertise a capability solely because some upstream declares it.
|
||||
|
||||
2. **Direction-aware relay**
|
||||
Keep server-side projection and client-side relay as separate concepts.
|
||||
|
||||
3. **Selected union, not blind union**
|
||||
Disabled upstreams and unsupported bridge paths must not inflate the public
|
||||
capability set.
|
||||
|
||||
4. **Protocol first, workflow later**
|
||||
This phase improves MCP proxy behavior. Workflow artifacts and planner-facing
|
||||
source models should not absorb this complexity.
|
||||
|
||||
5. **Use official MCP boundary types**
|
||||
Model protocol behavior with `mcp.types` capability and notification models
|
||||
where possible. Do not create parallel ad hoc protocol dicts.
|
||||
|
||||
6. **Consent at source connection time**
|
||||
A connected proxy source should normally be able to use the capabilities it
|
||||
declares, but powerful client-side relays should be visible to the user when
|
||||
the source is connected, similar to an app-permissions screen. Later admin UI
|
||||
may disable individual relays such as sampling, roots, or elicitation for a
|
||||
source without changing the base protocol model.
|
||||
|
||||
## Phase 1: Upstream Notification Inventory
|
||||
|
||||
### Questions
|
||||
|
||||
1. Does FastMCP already forward upstream:
|
||||
- `notifications/tools/list_changed`
|
||||
- `notifications/resources/list_changed`
|
||||
- `notifications/prompts/list_changed`
|
||||
- `notifications/resources/updated`
|
||||
- `notifications/message`
|
||||
- `notifications/progress`
|
||||
- `notifications/tasks/status`
|
||||
2. If some notifications are forwarded, are payloads transformed correctly?
|
||||
3. For resource updates, are resource URIs projected into downstream namespace
|
||||
form or leaked as raw upstream URIs?
|
||||
4. Do local client surfaces we can inspect, such as Inspector or Codex, visibly
|
||||
react to the forwarded notifications?
|
||||
|
||||
### Method
|
||||
|
||||
1. Extend the fixture server only where needed to emit deterministic events.
|
||||
2. Probe direct upstream behavior first.
|
||||
3. Probe the same behavior through `wf-mcp`.
|
||||
4. Record observed behavior in `docs/mcp_protocol_proxy_inventory.md`.
|
||||
5. Do not implement forwarding until the behavior gap is proven.
|
||||
|
||||
### Expected Outputs
|
||||
|
||||
- a table of each notification kind:
|
||||
- direct upstream behavior
|
||||
- through-proxy behavior
|
||||
- payload correctness
|
||||
- whether action is needed
|
||||
- regression tests for any behavior we decide to rely on
|
||||
|
||||
## Phase 2: Capability Negotiation Inventory
|
||||
|
||||
### Questions
|
||||
|
||||
1. What server capabilities does the current unified `wf-mcp` server advertise?
|
||||
2. What server capabilities do configured upstreams advertise?
|
||||
3. What downstream client capabilities are visible to `wf-mcp` during
|
||||
initialization?
|
||||
4. Which advanced features can FastMCP already bridge today?
|
||||
5. Which advertised upstream capabilities would be false for `wf-mcp` to expose
|
||||
until request forwarding is added?
|
||||
|
||||
### Expected Outputs
|
||||
|
||||
- a server-capability projection matrix
|
||||
- a client-capability relay matrix
|
||||
- a concrete list of:
|
||||
- already safe to advertise
|
||||
- bridgeable with small glue
|
||||
- unsupported until a larger forwarding subsystem exists
|
||||
|
||||
## Intended Architecture
|
||||
|
||||
Likely later modules:
|
||||
|
||||
```text
|
||||
wf_mcp.protocol_relay/
|
||||
server_capabilities.py
|
||||
client_capabilities.py
|
||||
notifications.py
|
||||
```
|
||||
|
||||
Conceptually:
|
||||
|
||||
```text
|
||||
enabled upstream capabilities
|
||||
+
|
||||
local wf-mcp capabilities
|
||||
|
|
||||
v
|
||||
ServerCapabilityProjection
|
||||
|
|
||||
v
|
||||
downstream initialize result
|
||||
|
||||
downstream client capabilities
|
||||
|
|
||||
v
|
||||
ClientCapabilityRelay
|
||||
|
|
||||
v
|
||||
selected upstream initialize requests
|
||||
```
|
||||
|
||||
The exact module names may change after the inventory. The separation should
|
||||
not:
|
||||
|
||||
- projecting what `wf-mcp` offers as a server
|
||||
- relaying what the downstream client offers to upstream servers
|
||||
|
||||
Mixed upstream support should not be hidden. MCP's top-level capability object is
|
||||
coarse, so `wf-mcp` should eventually expose per-source and, where useful,
|
||||
per-capability detail through its admin inventory tools even when the public
|
||||
server capability advertisement is a selected union.
|
||||
|
||||
## Feature Notes
|
||||
|
||||
### Elicitation
|
||||
|
||||
Elicitation is a client capability, not a server capability. Supporting it
|
||||
through `wf-mcp` means forwarding upstream elicitation requests to the
|
||||
downstream client and returning the response.
|
||||
|
||||
### Roots
|
||||
|
||||
Roots are also a client capability. They represent client-exposed filesystem or
|
||||
workspace roots that a server may query. Supporting roots through `wf-mcp` means
|
||||
relaying `roots/list`, not pretending `wf-mcp` itself owns roots.
|
||||
|
||||
### Sampling
|
||||
|
||||
Sampling is another client capability. It lets a server ask the client-side LLM
|
||||
to generate content. This is powerful and should remain explicit because it can
|
||||
cross trust boundaries.
|
||||
|
||||
### Tasks
|
||||
|
||||
Tasks exist in both directions:
|
||||
|
||||
- server tasks: downstream client task-augments calls into `wf-mcp`
|
||||
- client tasks: upstream servers task-augment requests they send toward the
|
||||
downstream client
|
||||
|
||||
Treat these as separate negotiated capabilities. Do not assume “tasks supported”
|
||||
is one global boolean.
|
||||
|
||||
### MCP Apps And `_meta.ui`
|
||||
|
||||
`_meta.ui` belongs to the MCP Apps extension layer, not the base server/client
|
||||
capability split above. `wf-mcp` should still preserve and proxy it faithfully.
|
||||
|
||||
Expected proxy behavior:
|
||||
|
||||
- preserve upstream `_meta.ui` values
|
||||
- rewrite referenced UI resource URIs when they cross a namespace boundary, the
|
||||
same way ordinary proxied resource URIs are projected
|
||||
- do not rely on `_meta.ui` for core protocol correctness, because clients may
|
||||
ignore extension metadata they do not understand
|
||||
|
||||
Possible later use:
|
||||
|
||||
- `wf-mcp` may expose its own source-consent, login/logout, catalog, and toggle
|
||||
surfaces as MCP Apps UI resources/tools
|
||||
- that is a UI delivery choice, not a reason to mix UI metadata into the core
|
||||
capability negotiation model
|
||||
|
||||
## Non-Goals For The Next Pass
|
||||
|
||||
- Do not blindly union all upstream capability dicts.
|
||||
- Do not build a custom protocol stack around FastMCP before measuring what it
|
||||
already forwards.
|
||||
- Do not solve workflow-artifact dependency validation here.
|
||||
- Do not build UI controls here.
|
||||
- Do not claim support for elicitation, roots, sampling, or tasks until an
|
||||
end-to-end relay path is demonstrated.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
The next pass is successful when we can answer, with evidence:
|
||||
|
||||
1. Which upstream notifications already survive the proxy?
|
||||
2. Which server capabilities may `wf-mcp` advertise truthfully today?
|
||||
3. Which downstream client capabilities can safely be relayed upstream today?
|
||||
4. What exact small implementation should happen next, instead of guessing?
|
||||
@@ -94,7 +94,9 @@ class ProxyNamespace(Transform):
|
||||
return f"ProxyNamespace({self._connection_id!r})"
|
||||
|
||||
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
|
||||
return [tool.model_copy(update={"name": self._name(tool.name)}) for tool in tools]
|
||||
return [
|
||||
tool.model_copy(update={"name": self._name(tool.name)}) for tool in tools
|
||||
]
|
||||
|
||||
async def get_tool(
|
||||
self,
|
||||
@@ -172,7 +174,9 @@ class ProxyNamespace(Transform):
|
||||
return (
|
||||
None
|
||||
if template is None
|
||||
else template.model_copy(update={"uri_template": self._uri(template.uri_template)})
|
||||
else template.model_copy(
|
||||
update={"uri_template": self._uri(template.uri_template)}
|
||||
)
|
||||
)
|
||||
|
||||
def _name(self, name: str) -> str:
|
||||
|
||||
Vendored
+16
-2
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, TypedDict
|
||||
from typing import Annotated, Any, TypedDict
|
||||
|
||||
import mcp.types as mcp_types
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.server.fastmcp import Context, FastMCP
|
||||
from pydantic import AnyUrl
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
@@ -36,6 +37,19 @@ async def resource_link_tool() -> list[mcp_types.ResourceLink]:
|
||||
]
|
||||
|
||||
|
||||
@server.tool(title="Emit notifications tool")
|
||||
async def emit_notifications_tool(ctx: Context[Any, Any, Any]) -> dict[str, bool]:
|
||||
"""Emit protocol notifications so proxy relay behavior can be tested."""
|
||||
await ctx.request_context.session.send_tool_list_changed()
|
||||
await ctx.request_context.session.send_resource_list_changed()
|
||||
await ctx.request_context.session.send_prompt_list_changed()
|
||||
await ctx.request_context.session.send_resource_updated(
|
||||
AnyUrl("fixture://docs/welcome")
|
||||
)
|
||||
await ctx.info("fixture emitted notifications")
|
||||
return {"emitted": True}
|
||||
|
||||
|
||||
@server.resource(
|
||||
"fixture://docs/welcome",
|
||||
name="resource.welcome",
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
import mcp.types as mcp_types
|
||||
import pytest
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.client.stdio import StdioServerParameters, stdio_client
|
||||
|
||||
from wf_mcp.models import BrokerConfig, ConnectionConfig
|
||||
from wf_mcp.transparent_proxy import create_transparent_proxy_client
|
||||
|
||||
from .test_support import fixture_server_path, local_temp_root
|
||||
|
||||
|
||||
def test_fixture_server_initialize_capabilities_are_observable_directly() -> None:
|
||||
async def inspect_capabilities() -> mcp_types.ServerCapabilities:
|
||||
params = StdioServerParameters(
|
||||
command=sys.executable,
|
||||
args=[fixture_server_path()],
|
||||
)
|
||||
async with stdio_client(params) as (read_stream, write_stream):
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
result = await session.initialize()
|
||||
return result.capabilities
|
||||
|
||||
try:
|
||||
capabilities = asyncio.run(inspect_capabilities())
|
||||
except PermissionError as exc:
|
||||
pytest.skip(f"stdio MCP transport is not permitted in this environment: {exc}")
|
||||
|
||||
assert capabilities.tools is not None
|
||||
assert capabilities.tools.listChanged is False
|
||||
assert capabilities.resources is not None
|
||||
assert capabilities.resources.subscribe is False
|
||||
assert capabilities.resources.listChanged is False
|
||||
assert capabilities.prompts is not None
|
||||
assert capabilities.prompts.listChanged is False
|
||||
assert capabilities.logging is None
|
||||
|
||||
|
||||
def test_unified_proxy_initialize_capabilities_reflect_local_surface() -> None:
|
||||
config = BrokerConfig(
|
||||
store_root=local_temp_root() / "protocol_capabilities_store",
|
||||
connections=[
|
||||
ConnectionConfig(
|
||||
id="fixture.personal",
|
||||
server="fixture",
|
||||
account="personal",
|
||||
metadata={
|
||||
"transport": "stdio",
|
||||
"command": sys.executable,
|
||||
"args": [fixture_server_path()],
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
async def inspect_capabilities() -> mcp_types.ServerCapabilities:
|
||||
client = create_transparent_proxy_client(config)
|
||||
async with client:
|
||||
initialize_result = client.initialize_result
|
||||
assert initialize_result is not None
|
||||
return initialize_result.capabilities
|
||||
|
||||
try:
|
||||
capabilities = asyncio.run(inspect_capabilities())
|
||||
except PermissionError as exc:
|
||||
pytest.skip(f"stdio MCP transport is not permitted in this environment: {exc}")
|
||||
|
||||
assert capabilities.tools is not None
|
||||
assert capabilities.tools.listChanged is True
|
||||
assert capabilities.resources is not None
|
||||
assert capabilities.resources.subscribe is False
|
||||
assert capabilities.resources.listChanged is True
|
||||
assert capabilities.prompts is not None
|
||||
assert capabilities.prompts.listChanged is True
|
||||
assert capabilities.logging is not None
|
||||
@@ -0,0 +1,107 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
import mcp.types as mcp_types
|
||||
import pytest
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.client.stdio import StdioServerParameters, stdio_client
|
||||
|
||||
from wf_mcp.models import BrokerConfig, ConnectionConfig
|
||||
from wf_mcp.transparent_proxy import create_transparent_proxy_client
|
||||
|
||||
from .test_support import fixture_server_path, local_temp_root
|
||||
|
||||
|
||||
NotificationProbe = Callable[
|
||||
[Callable[[mcp_types.ServerNotification], None]],
|
||||
Awaitable[None],
|
||||
]
|
||||
|
||||
|
||||
def _notification_methods(
|
||||
notifications: list[mcp_types.ServerNotification],
|
||||
) -> list[str]:
|
||||
return [notification.root.method for notification in notifications]
|
||||
|
||||
|
||||
async def _capture_notifications(
|
||||
probe: NotificationProbe,
|
||||
) -> list[mcp_types.ServerNotification]:
|
||||
notifications: list[mcp_types.ServerNotification] = []
|
||||
|
||||
def record(notification: mcp_types.ServerNotification) -> None:
|
||||
notifications.append(notification)
|
||||
|
||||
await probe(record)
|
||||
return notifications
|
||||
|
||||
|
||||
def test_fixture_server_emits_observable_protocol_notifications_directly() -> None:
|
||||
async def probe(record: Callable[[mcp_types.ServerNotification], None]) -> None:
|
||||
async def message_handler(message: object) -> None:
|
||||
if isinstance(message, mcp_types.ServerNotification):
|
||||
record(message)
|
||||
|
||||
params = StdioServerParameters(
|
||||
command=sys.executable,
|
||||
args=[fixture_server_path()],
|
||||
)
|
||||
async with stdio_client(params) as (read_stream, write_stream):
|
||||
async with ClientSession(
|
||||
read_stream,
|
||||
write_stream,
|
||||
message_handler=message_handler,
|
||||
) as session:
|
||||
await session.initialize()
|
||||
await session.call_tool("emit_notifications_tool")
|
||||
|
||||
try:
|
||||
notifications = asyncio.run(_capture_notifications(probe))
|
||||
except PermissionError as exc:
|
||||
pytest.skip(f"stdio MCP transport is not permitted in this environment: {exc}")
|
||||
|
||||
methods = _notification_methods(notifications)
|
||||
assert "notifications/tools/list_changed" in methods
|
||||
assert "notifications/resources/list_changed" in methods
|
||||
assert "notifications/prompts/list_changed" in methods
|
||||
assert "notifications/resources/updated" in methods
|
||||
assert "notifications/message" in methods
|
||||
|
||||
|
||||
def test_transparent_proxy_does_not_relay_upstream_protocol_notifications_yet() -> None:
|
||||
config = BrokerConfig(
|
||||
store_root=local_temp_root() / "protocol_relay_store",
|
||||
connections=[
|
||||
ConnectionConfig(
|
||||
id="fixture.personal",
|
||||
server="fixture",
|
||||
account="personal",
|
||||
metadata={
|
||||
"transport": "stdio",
|
||||
"command": sys.executable,
|
||||
"args": [fixture_server_path()],
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
async def probe(record: Callable[[mcp_types.ServerNotification], None]) -> None:
|
||||
async def message_handler(message: object) -> None:
|
||||
if isinstance(message, mcp_types.ServerNotification):
|
||||
record(message)
|
||||
|
||||
client = create_transparent_proxy_client(config)
|
||||
client._session_kwargs["message_handler"] = message_handler
|
||||
async with client:
|
||||
await client.call_tool("fixture.personal.emit_notifications_tool")
|
||||
|
||||
try:
|
||||
notifications = asyncio.run(_capture_notifications(probe))
|
||||
except PermissionError as exc:
|
||||
pytest.skip(f"stdio MCP transport is not permitted in this environment: {exc}")
|
||||
|
||||
methods = _notification_methods(notifications)
|
||||
assert methods == []
|
||||
@@ -83,16 +83,15 @@ def test_transparent_proxy_lists_and_calls_upstream_tools() -> None:
|
||||
proxy_tools_payload = _structured(proxy_tools_result)
|
||||
proxy_tools = proxy_tools_payload["tools"]
|
||||
assert proxy_tools_payload["nextCursor"] is None
|
||||
assert proxy_tools_payload["total"] == 2
|
||||
assert len(proxy_tools) == 2
|
||||
assert proxy_tools_payload["total"] == 3
|
||||
assert len(proxy_tools) == 3
|
||||
assert proxy_tools[0]["proxy_name"] == "fixture.personal.echo_tool"
|
||||
assert proxy_tools[0]["connection_id"] == "fixture.personal"
|
||||
assert proxy_tools[0]["local_name"] == "echo_tool"
|
||||
assert proxy_tools[0]["enabled"] is True
|
||||
assert (
|
||||
proxy_tools[1]["proxy_name"]
|
||||
== "fixture.personal.resource_link_tool"
|
||||
)
|
||||
proxy_names = [tool["proxy_name"] for tool in proxy_tools]
|
||||
assert "fixture.personal.emit_notifications_tool" in proxy_names
|
||||
assert "fixture.personal.resource_link_tool" in proxy_names
|
||||
|
||||
proxy_tool_result = await client.call_tool(
|
||||
"wf.admin.get_proxy_tool",
|
||||
@@ -302,7 +301,7 @@ def test_transparent_proxy_proxy_tool_listing_supports_filters_and_cursor() -> N
|
||||
first_page = _structured(first_page_result)
|
||||
assert len(first_page["tools"]) == 1
|
||||
assert first_page["nextCursor"] is not None
|
||||
assert first_page["total"] == 4
|
||||
assert first_page["total"] == 6
|
||||
|
||||
second_page_result = await client.call_tool(
|
||||
"wf.admin.list_proxy_tools",
|
||||
|
||||
Reference in New Issue
Block a user