Repository navigation
Build views and the interrupt gate on v2's own extensions #166
Description
#165 ports the package to mcp 2 by keeping both of these exactly as they were. That was the right call for a migration — nothing about them broke — but v2 has first-class primitives for both, and both are ours to maintain until we move.
The two halves are independent; either can be done without the other.
1. views.py on the Apps extension
mcp_runtime.views hand-rolls the MCP Apps wire format: it stamps _meta.ui.resourceUri on the tool, registers the HTML as a ui://<toolset>/<view> resource, and serves it as text/html;profile=mcp-app. v2 ships that as mcp.server.apps, and our constants already agree with it exactly — APP_MIME_TYPE is the same string, EXTENSION_ID is io.modelcontextprotocol/ui.
So this is not a bug to fix. What the extension adds over the hand-roll:
- Capability negotiation. An
Appsextension advertises support, andclient_supports_apps(ctx)lets a tool branch on whether the caller negotiated it. We advertise nothing, so a host cannot tell that our servers speak Apps, and a tool cannot degrade deliberately — it just returns the same thing either way and hopes. - Resource-level CSP, permissions and visibility (
ResourceCsp,ResourcePermissions,Visibility), none of which we set today. Our views are served with no declared CSP at all — worth a look on its own terms, given a view renders third-party tiles (ecmwf/dss-agentic-ai-services#194). ToolBinding/ResourceBindingin place of ourmodel_copy(update={"meta": ...})stamping and bareadd_resource.- It tracks the spec. Apps is a draft extension; the wire format can still move. Following the SDK's implementation means it moves under us rather than through us.
The scaffold (mcp_toolset) and js/mcp-view would want to follow, and docs/VIEWS.md with them.
2. The interrupt gate and elicitation
These are not the same mechanism, and the issue is the seam between them rather than a duplicate to delete:
mcp_agent.interrupt_gateis agent-side and model-initiated — the model calls theinterrupttool to put a question to the user, and the value it raises is already in AG-UI's terms.- v2's
Elicitis server-side and tool-initiated — a tool needs input mid-call and returns anInputRequiredResult, expecting the call to be retried with answers. At 2026-07-28 a server can no longer call the client, so this is the supported way for a tool to ask a question.
langchain.mcp.elicitation already drives that loop, and it answers each round with a LangGraph interrupt() — the same mechanism the gate uses. Since #165 wires MCPAdapter, which arms elicitation by default, a toolset tool that elicits would already raise an interrupt through our stack today.
The gap is the round trip. That interrupt's value is {"type": ..., "tool_name": ..., "requests": [...]} and the resume it waits for is {"responses": ...}. mcp_agent_api.events degrades correctly on the way out — a value it does not recognise goes out as input_required with the raw value under metadata — but nothing maps a client's answer back into the shape langchain.mcp expects, so resuming one would fail in _build_responses. Worth a test either way, because right now nothing exercises it.
What this would unlock is worth stating plainly: a tool that cannot proceed can ask, rather than returning a refusal and relying on the model to turn it into a question. The constraint refusals in the dss toolsets are the obvious candidate.
Order
Either half first. The views half is self-contained. The elicitation half should start with a failing test that a tool's elicitation survives the whole round trip, since that decides how much of the gate's plumbing is reusable.
Activity
feat!: mcp 2, and the tool bridge off the retired adapter (#165)
feat(views): serve views through mcp 2's Apps extension (#199)
feat: a toolset tool can ask the user a question (#201)