-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Add a pluggable server extension API with MCP Apps #3003
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Kludex
wants to merge
10
commits into
main
Choose a base branch
from
extension-api-sep-2133
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,444
−24
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
2c7fc6e
Surface the SEP-2133 extensions capability map
Kludex 68b9c7e
Add the pluggable Extension API to MCPServer
Kludex c8684e9
Add the MCP Apps extension (io.modelcontextprotocol/ui)
Kludex 51ad100
Add the Tasks extension (io.modelcontextprotocol/tasks)
Kludex ff285f4
Add apps and tasks example stories and migration notes
Kludex 42d6900
Make extensions construction-only and improve the tasks example
Kludex 7ca1530
Clarify Tasks opt-in and per-tool task_support scope
Kludex aa78671
Merge remote-tracking branch 'origin/main' into extension-api-sep-2133
Kludex cb2c456
Drop the Tasks extension; defer to a SEP-2663 rewrite
Kludex 0f440b1
Address extension-API review: layering, enforcement, version gating, …
Kludex File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -407,6 +407,40 @@ On `ClientSession`, `call_tool` / `get_prompt` / `read_resource` still return th | |||||
|
|
||||||
| For protocol 2026-07-28 over Streamable HTTP, a tool's input-schema property may carry an `x-mcp-header` annotation. When a tool the client has listed is called, each annotated argument is mirrored into an `Mcp-Param-<name>` request header (string verbatim, integer as decimal, boolean as `true`/`false`, base64-sentinel-wrapped when not header-safe; `null`/absent arguments are omitted). The argument is also left in the request body. `list_tools` caches a tool's annotations, so list a tool before calling it to enable mirroring; a tool the client never listed emits no `Mcp-Param-*` headers. Other transports ignore the annotation. | ||||||
|
|
||||||
| ### Server extensions API (SEP-2133) | ||||||
|
|
||||||
| `MCPServer` now accepts opt-in extensions that bundle MCP behaviour behind a | ||||||
| reverse-DNS identifier and advertise it under `ServerCapabilities.extensions` | ||||||
| (the 2026-07-28 capability map). An extension subclasses `mcp.server.extension.Extension` | ||||||
| and overrides only the contribution methods it needs: `tools()`/`resources()`/`methods()` | ||||||
| (additive) and `intercept_tool_call()` (wraps `tools/call`). The `identifier` must be a | ||||||
| `vendor-prefix/name` string, enforced when the subclass is defined. Pass instances at | ||||||
| construction: | ||||||
|
|
||||||
| ```python | ||||||
| from mcp.server.mcpserver import MCPServer | ||||||
| from mcp.server.apps import Apps | ||||||
|
|
||||||
| mcp = MCPServer("demo", extensions=[Apps()]) | ||||||
| ``` | ||||||
|
|
||||||
| The reference extension is `mcp.server.apps.Apps` (`io.modelcontextprotocol/ui`): | ||||||
| it binds a tool to a `ui://` UI resource via `_meta.ui.resourceUri`, and | ||||||
| `client_supports_apps(ctx)` gates the SEP-2133 text-only fallback (checking the | ||||||
| client advertised the `text/html;profile=mcp-app` MIME type). | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Apps fallback docs describe MIME-type gating too narrowly. Current behavior also treats missing Prompt for AI agents
Suggested change
|
||||||
|
|
||||||
| A `MethodBinding` may set `protocol_versions` to scope an extension method to | ||||||
| specific wire versions; a request at any other version is `METHOD_NOT_FOUND`. An | ||||||
| extension handler can call `mcp.server.mcpserver.require_client_extension(ctx, identifier)` | ||||||
| to reject a request with the `-32021` (missing required client capability) error | ||||||
| when the client did not declare the extension. | ||||||
|
|
||||||
| Clients advertise extension support with the new `Client(extensions=...)` / | ||||||
| `ClientSession(extensions=...)` argument, mirrored into `ClientCapabilities.extensions`. | ||||||
| The extensions capability map is negotiated over `server/discover` (modern path); | ||||||
| a legacy `initialize` handshake does not carry it. Extensions are off by default | ||||||
| and never alter behaviour unless registered. | ||||||
|
|
||||||
| ### `McpError` renamed to `MCPError` | ||||||
|
|
||||||
| The `McpError` exception class has been renamed to `MCPError` for consistent naming with the MCP acronym style used throughout the SDK. | ||||||
|
|
||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,40 @@ | ||
| # apps | ||
|
|
||
| MCP Apps: a tool result carries a `_meta.ui` reference to a `ui://` resource | ||
| that the host renders as an interactive surface. The story will register a | ||
| `@ui` resource and return it from a tool. | ||
| MCP Apps: a tool carries a `_meta.ui.resourceUri` reference to a `ui://` | ||
| resource that the host renders as an interactive surface. The server opts in via | ||
| the `Apps` extension (`io.modelcontextprotocol/ui`); the client negotiates it by | ||
| advertising the `text/html;profile=mcp-app` MIME type. | ||
|
|
||
| **Status: not yet implemented** ([#2896](https://github.com/modelcontextprotocol/python-sdk/issues/2896)). | ||
| The `extensions` capability map is not yet surfaced on `MCPServer`, so a server | ||
| cannot advertise Apps support and a client cannot negotiate it. | ||
| ## Run it | ||
|
|
||
| ```bash | ||
| # stdio (default — the client spawns the server as a subprocess) | ||
| uv run python -m stories.apps.client | ||
|
|
||
| # HTTP — the client self-hosts the server on a free port, runs, then tears it down | ||
| uv run python -m stories.apps.client --http | ||
| ``` | ||
|
|
||
| ## What to look at | ||
|
|
||
| - `server.py` `MCPServer("apps-example", extensions=[apps])` — the extension | ||
| advertises `io.modelcontextprotocol/ui` under `ServerCapabilities.extensions` | ||
| and contributes the UI-bound tool and its `ui://` resource. `MCPServer` itself | ||
| never learns about "ui"; it applies a closed set of contributions. | ||
| - `server.py` `@apps.tool(resource_uri=...)` — stamps `_meta.ui.resourceUri` on | ||
| the tool; `add_html_resource` registers the matching `ui://` resource at | ||
| `text/html;profile=mcp-app`. | ||
| - `server.py` `client_supports_apps(ctx)` — SEP-2133 graceful degradation: a | ||
| client that did not negotiate Apps gets a text-only result. | ||
| - `client.py` `Client(target, extensions={...})` — the client advertises Apps | ||
| support so the server returns the UI-enabled result, then reads the tool's | ||
| `_meta.ui.resourceUri` and fetches that resource. | ||
|
|
||
| ## Spec | ||
|
|
||
| [MCP Apps — extensions](https://modelcontextprotocol.io/specification/draft/extensions/apps) | ||
| · [SEP-2133 — extensions capability](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2133) | ||
|
|
||
| ## See also | ||
|
|
||
| `custom_methods/` (registering a non-spec method without an extension). |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| """Negotiate MCP Apps, discover a tool's `ui://` UI, fetch it, and call the tool.""" | ||
|
|
||
| from mcp_types import TextContent, TextResourceContents | ||
|
|
||
| from mcp.client import Client | ||
| from mcp.server.apps import APP_MIME_TYPE, EXTENSION_ID | ||
| from stories._harness import Target, run_client | ||
|
|
||
|
|
||
| async def main(target: Target, *, mode: str = "auto") -> None: | ||
| # Advertise MCP Apps support so the server returns the UI-enabled result; a | ||
| # client that omits this gets the text-only fallback (graceful degradation). | ||
| async with Client(target, mode=mode, extensions={EXTENSION_ID: {"mimeTypes": [APP_MIME_TYPE]}}) as client: | ||
| # The extensions capability map rides `server/discover` (modern only). On a | ||
| # legacy connection (today's stdio) it is absent, so assert it only when present. | ||
| if client.server_capabilities.extensions is not None: | ||
| assert client.server_capabilities.extensions == {EXTENSION_ID: {}}, client.server_capabilities.extensions | ||
|
|
||
| listed = await client.list_tools() | ||
| tool = next(t for t in listed.tools if t.name == "get_time") | ||
| assert tool.meta is not None, tool | ||
| assert tool.meta["ui"]["resourceUri"] == "ui://get-time/app.html", tool.meta | ||
|
|
||
| ui = await client.read_resource("ui://get-time/app.html") | ||
| contents = ui.contents[0] | ||
| assert isinstance(contents, TextResourceContents) | ||
| assert contents.mime_type == APP_MIME_TYPE, contents.mime_type | ||
|
|
||
| result = await client.call_tool("get_time", {}) | ||
| assert isinstance(result.content[0], TextContent) | ||
| assert result.content[0].text == "2026-06-26T00:00:00Z", result.content[0].text | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| run_client(main) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| """MCP Apps: a tool bound to a `ui://` resource the host renders as an interactive surface. | ||
|
|
||
| `Apps` is an opt-in `Extension` passed to `MCPServer(extensions=[...])`. The | ||
| `@apps.tool(resource_uri=...)` decorator stamps `_meta.ui.resourceUri` onto the | ||
| tool; `add_html_resource` registers the matching `ui://` HTML resource. The tool | ||
| degrades gracefully: `client_supports_apps(ctx)` reports whether the client | ||
| negotiated Apps, so it returns text-only output otherwise. | ||
| """ | ||
|
|
||
| from mcp.server.apps import Apps, client_supports_apps | ||
| from mcp.server.mcpserver import MCPServer | ||
| from mcp.server.mcpserver.context import Context | ||
| from stories._hosting import run_server_from_args | ||
|
|
||
| RESOURCE_URI = "ui://get-time/app.html" | ||
| CLOCK_HTML = """<!doctype html> | ||
| <title>Current time</title> | ||
| <h1 id="now">…</h1> | ||
| <script> | ||
| window.addEventListener("message", (event) => { | ||
| const text = event.data?.result?.content?.[0]?.text; | ||
| if (text) document.getElementById("now").textContent = text; | ||
| }); | ||
| </script> | ||
| """ | ||
|
|
||
|
|
||
| def build_server() -> MCPServer: | ||
| apps = Apps() | ||
|
|
||
| @apps.tool(resource_uri=RESOURCE_URI, title="Get Time", description="Return the current time.") | ||
| def get_time(ctx: Context) -> str: | ||
| now = "2026-06-26T00:00:00Z" | ||
| if not client_supports_apps(ctx): | ||
| return f"The time is {now}." | ||
| return now | ||
|
|
||
| apps.add_html_resource(RESOURCE_URI, CLOCK_HTML, title="Clock") | ||
| return MCPServer("apps-example", extensions=[apps]) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| run_server_from_args(build_server) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,24 @@ | ||
| # tasks | ||
|
|
||
| The `io.modelcontextprotocol/tasks` extension: long-running work registered | ||
| with `@task`, polled via `tasks/get`, updated mid-flight, and cancelled with | ||
| `tasks/cancel`. The story will show a task that outlives the request that | ||
| started it. | ||
| Task-augmented execution: a requestor augments a `tools/call` with a `task`, the | ||
| receiver returns a `CreateTaskResult` immediately, and the requestor polls | ||
| `tasks/get` and retrieves the deferred result. | ||
|
|
||
| **Status: not yet implemented.** The extension types exist but the `extensions` | ||
| capability map is not yet surfaced on `MCPServer`, and the runtime trails the | ||
| release. The TypeScript SDK deliberately removed its tasks example pending the | ||
| same work. | ||
| **Status: deferred.** Tasks ship in 2026-07-28 as | ||
| [SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/seps/2663-tasks-extension.md), | ||
| an `io.modelcontextprotocol/tasks` extension that is wire-incompatible with the | ||
| 2025-11-25 in-core design still carried (types-only) in `mcp_types`. The runtime | ||
| needs to be built to the SEP — server-decided augmentation (ignoring the legacy | ||
| `params.task`), the `{tasks/get, tasks/update, tasks/cancel}` method set, the | ||
| `resultType: "task"` envelope, `execution.taskSupport` gating, and `ttlMs` | ||
| fields — so it lands in a separate PR with the conformance `tasks-*` scenarios | ||
| wired in. | ||
|
|
||
| ## Spec | ||
|
|
||
| [Tasks — basic utilities](https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks) | ||
| [SEP-2663 — Tasks extension](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/seps/2663-tasks-extension.md) | ||
| · [SEP-2133 — extensions capability](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2133) | ||
|
|
||
| ## See also | ||
|
|
||
| `apps/` (the additive half of the extension API). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: Migration docs overstate when extension identifier validation runs. Instance-assigned identifiers are validated at extension registration, not only at subclass definition.
Prompt for AI agents