@ttoss/http-server-mcp 0.27.1 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATIONS.md +35 -0
- package/README.md +123 -1
- package/dist/index.cjs +252 -37
- package/dist/index.d.cts +238 -4
- package/dist/index.d.mts +238 -4
- package/dist/index.mjs +251 -39
- package/package.json +1 -1
package/MIGRATIONS.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Migrations
|
|
2
2
|
|
|
3
|
+
## The `2026-07-28` revision needs `createMcpServer`
|
|
4
|
+
|
|
5
|
+
That revision is served only when `createMcpRouter` is given a
|
|
6
|
+
`createMcpServer` factory.
|
|
7
|
+
|
|
8
|
+
```diff
|
|
9
|
+
+const buildServer = () => {
|
|
10
|
+
+ const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
11
|
+
+ registerEverything(mcpServer);
|
|
12
|
+
+ return mcpServer;
|
|
13
|
+
+};
|
|
14
|
+
+
|
|
15
|
+
-createMcpRouter(mcpServer, {
|
|
16
|
+
+createMcpRouter(buildServer(), {
|
|
17
|
+
+ createMcpServer: buildServer,
|
|
18
|
+
auth: { ... },
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Why it cannot default to the server you already pass.** The negotiated
|
|
23
|
+
revision is instance state: serving one `2026-07-28` request marks that
|
|
24
|
+
`McpServer` modern for good, and it then validates every later message against
|
|
25
|
+
that revision. One instance serving both eras is pinned by the first client to
|
|
26
|
+
speak the newer one, after which every 2025-era request is answered
|
|
27
|
+
`-32602 Request is missing the required _meta envelope for protocol revision
|
|
28
|
+
2026-07-28` at **HTTP 200** for the life of the process — a status and body a
|
|
29
|
+
client reads as "no tools" rather than as a fault. The SDK's own serving
|
|
30
|
+
entries take a factory and call it once per request for this reason.
|
|
31
|
+
|
|
32
|
+
**What you will observe if you miss this.** A client speaking `2026-07-28` gets
|
|
33
|
+
`400` with `-32022 Unsupported protocol version` and `data.supported` listing
|
|
34
|
+
the 2025-era revisions, which it can renegotiate from. Only deployments
|
|
35
|
+
actually receiving that traffic are affected; every client without the
|
|
36
|
+
per-request envelope is served exactly as before.
|
|
37
|
+
|
|
3
38
|
## `tools/list` is no longer public by default
|
|
4
39
|
|
|
5
40
|
`auth.publicMethods` now defaults to `['initialize']` instead of
|
package/README.md
CHANGED
|
@@ -478,6 +478,78 @@ register({
|
|
|
478
478
|
|
|
479
479
|
**`buildContext`** injects request-scoped values (DB clients, tenant IDs) into every handler call without threading them through each individual tool. It receives the full `ToolCallContext` so context can vary by identity or by call args.
|
|
480
480
|
|
|
481
|
+
## MCP Apps (interactive UIs)
|
|
482
|
+
|
|
483
|
+
The [`io.modelcontextprotocol/ui`](https://github.com/modelcontextprotocol/ext-apps) extension lets a tool result render as an interactive view instead of text. The view is a `ui://` resource holding an HTML document, which the host loads into a sandboxed iframe and talks to over `postMessage`; the tool points at it through its `_meta`.
|
|
484
|
+
|
|
485
|
+
The extension asks nothing of the transport, so `createMcpRouter` serves it as-is on both protocol revisions. `registerAppResource` owns the parts that are easy to get wrong — the `ui://` scheme, the exact MIME type, `_meta.ui` on both the declaration and the read result, and the deprecated flat linkage key that is still legal until the extension reaches GA.
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
import {
|
|
489
|
+
McpServer,
|
|
490
|
+
registerAppResource,
|
|
491
|
+
UI_EXTENSION_ID,
|
|
492
|
+
UI_RESOURCE_MIME_TYPE,
|
|
493
|
+
z,
|
|
494
|
+
} from '@ttoss/http-server-mcp';
|
|
495
|
+
|
|
496
|
+
const server = new McpServer(
|
|
497
|
+
{ name: 'weather', version: '1.0.0' },
|
|
498
|
+
{
|
|
499
|
+
capabilities: {
|
|
500
|
+
extensions: { [UI_EXTENSION_ID]: { mimeTypes: [UI_RESOURCE_MIME_TYPE] } },
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
const dashboard = registerAppResource({
|
|
506
|
+
server,
|
|
507
|
+
name: 'weather_dashboard',
|
|
508
|
+
uri: 'ui://weather/dashboard',
|
|
509
|
+
description: 'Interactive weather dashboard',
|
|
510
|
+
html: dashboardHtml, // a string, or ({ uri }) => string built per read
|
|
511
|
+
ui: {
|
|
512
|
+
csp: { connectDomains: ['https://api.openweathermap.org'] },
|
|
513
|
+
prefersBorder: true,
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
server.registerTool(
|
|
518
|
+
'get-weather',
|
|
519
|
+
{
|
|
520
|
+
description: 'Get the weather for a location',
|
|
521
|
+
inputSchema: { location: z.string() },
|
|
522
|
+
_meta: dashboard.toolMeta(),
|
|
523
|
+
},
|
|
524
|
+
async ({ location }) => {
|
|
525
|
+
const forecast = await fetchForecast(location);
|
|
526
|
+
return {
|
|
527
|
+
// Text stays meaningful: it is what a host without Apps renders.
|
|
528
|
+
content: [{ type: 'text', text: summarise(forecast) }],
|
|
529
|
+
structuredContent: forecast,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
);
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
`toolMeta()` is the linkage for **every** registration path — `registerTool`, [`registerToolFromSchema`](#registertoolfromschemaserver-params), and a [`GatedToolDef`](#creategatedtoolregistraroptions) all take `_meta`:
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
registerToolFromSchema(server, {
|
|
539
|
+
name: 'refresh-dashboard',
|
|
540
|
+
description: 'Refresh dashboard data',
|
|
541
|
+
// Callable by the view, hidden from the model.
|
|
542
|
+
_meta: dashboard.toolMeta({ visibility: ['app'] }),
|
|
543
|
+
handler: async () => ({ content: [{ type: 'text', text: 'refreshed' }] }),
|
|
544
|
+
});
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Register the linkage unconditionally
|
|
548
|
+
|
|
549
|
+
The spec suggests checking the client's declared capability before registering UI-enabled tools. Do not: that read is only reliable on the `2026-07-28` revision, where the capability arrives in each request's envelope. In this package's default [stateless mode](#stateless-vs-stateful-mode) there is no remembered `initialize` for a 2025-era client, so a capability-gated registration silently drops the view for every one of them.
|
|
550
|
+
|
|
551
|
+
Declaring it always is also strictly more compatible — a host without Apps support ignores `_meta` and renders the tool's `content`, which is the extension's own graceful-degradation contract. Keep every tool's text result meaningful on its own and the fallback takes care of itself.
|
|
552
|
+
|
|
481
553
|
## Issuing tokens for MCP clients
|
|
482
554
|
|
|
483
555
|
The `auth` option above covers the **resource-server** half of MCP authorization — it verifies tokens issued by an external authorization server (Cognito, Auth0, …). To make your own first-party server _issue_ the tokens an MCP client runs the full OAuth flow against, add the [`@ttoss/http-server-auth`](https://ttoss.dev/docs/modules/packages/http-server-auth) plugin's `oauthServer()` and pair it with `createMcpRouter({ auth: { verifyToken } })` so one deployment both issues and verifies tokens. See the [OAuth Authorization Server](https://ttoss.dev/docs/engineering/guidelines/oauth-authorization-server) guideline.
|
|
@@ -582,6 +654,7 @@ The `GatedToolDef` passed to `register` has:
|
|
|
582
654
|
- `requiredScope` — Single scope that must appear in `identity.scopes`.
|
|
583
655
|
- `inputSchema` — Zod field map or `ZodObject`, forwarded to `server.registerTool`.
|
|
584
656
|
- `gates` (`Array<(ctx: ToolCallContext) => void | Promise<void>>`, optional) — Per-tool guards appended after the global `gates`. Receive the full `ToolCallContext` enabling arg-conditional authorization.
|
|
657
|
+
- `_meta` (`Record<string, unknown>`, optional) — Tool metadata forwarded verbatim on `tools/list`; how a gated tool links to an [MCP Apps](#mcp-apps-interactive-uis) view.
|
|
585
658
|
- `method` — Async handler. Receives merged call args + `buildContext` output.
|
|
586
659
|
|
|
587
660
|
**`ToolCallContext`** is the object passed to gates, `buildContext`, and `onError`:
|
|
@@ -590,6 +663,33 @@ The `GatedToolDef` passed to `register` has:
|
|
|
590
663
|
- `args` (`Record<string, unknown>`) — The validated tool input (post SDK parse).
|
|
591
664
|
- `handler` (`string`) — The tool name, for error attribution.
|
|
592
665
|
|
|
666
|
+
### `registerAppResource(params)`
|
|
667
|
+
|
|
668
|
+
Registers an [MCP Apps](#mcp-apps-interactive-uis) view: a `ui://` resource whose HTML a host renders for a linked tool's result.
|
|
669
|
+
|
|
670
|
+
**Parameters (`params`):**
|
|
671
|
+
|
|
672
|
+
- `server` (`McpServer`) — The MCP server to register the resource on.
|
|
673
|
+
- `name` (`string`) — Resource name, as listed by `resources/list`.
|
|
674
|
+
- `uri` (`string`) — Resource URI. Must use the `ui://` scheme, or the call throws.
|
|
675
|
+
- `description` (`string`, optional) — What the view does and when a host should render it.
|
|
676
|
+
- `html` (`string | ({ uri: URL }) => string | Promise<string>`) — The view's HTML5 document, or a builder invoked per `resources/read`.
|
|
677
|
+
- `ui` (`UiResourceMeta`, optional) — Rendering and security configuration relayed to the host as `_meta.ui`:
|
|
678
|
+
- `csp` (`UiResourceCsp`) — Origins the view may reach, mapped onto the iframe's Content Security Policy: `connectDomains` (`connect-src`), `resourceDomains` (scripts/styles/images/fonts/media), `frameDomains` (`frame-src`), `baseUriDomains` (`base-uri`). An omitted list is the restrictive default, not "no restriction".
|
|
679
|
+
- `permissions` (`UiResourcePermissions`) — Browser capabilities requested for the view (`camera`, `microphone`, `geolocation`, `clipboardWrite`), each as `{}`. A host _may_ grant them, so feature-detect in the view rather than assuming.
|
|
680
|
+
- `domain` (`string`) — Dedicated sandbox origin, for views needing a stable one (OAuth callbacks, CORS, API key allowlists). The format is host-specific.
|
|
681
|
+
- `prefersBorder` (`boolean`) — Whether the host should draw a border and background around the view.
|
|
682
|
+
|
|
683
|
+
**Returns:** `RegisteredAppResource`
|
|
684
|
+
|
|
685
|
+
- `uri` (`string`) — The URI tools link to.
|
|
686
|
+
- `resource` (`RegisteredResource`) — The SDK's registration handle, for `update`/`disable`/`remove`.
|
|
687
|
+
- `toolMeta(params?)` — Builds the `_meta` bag linking a tool to this view, writing both `_meta.ui.resourceUri` and the deprecated flat `_meta['ui/resourceUri']`. Takes `visibility` (`Array<'model' | 'app'>`, optional — the spec's default is both) and `_meta` (`Record<string, unknown>`, optional) to merge further entries in.
|
|
688
|
+
|
|
689
|
+
### `UI_EXTENSION_ID` / `UI_RESOURCE_MIME_TYPE`
|
|
690
|
+
|
|
691
|
+
`'io.modelcontextprotocol/ui'` and `'text/html;profile=mcp-app'`. Use the first as the `capabilities.extensions` key when advertising Apps support on the `McpServer`, and the second as the MIME type a host negotiates.
|
|
692
|
+
|
|
593
693
|
### `registerToolFromSchema(server, params)`
|
|
594
694
|
|
|
595
695
|
Registers a tool using a **plain JSON Schema** object for `inputSchema` instead of a Zod shape.
|
|
@@ -603,6 +703,7 @@ Use this when tool definitions are shared between the MCP server and an AI SDK a
|
|
|
603
703
|
- `params.description` (`string`, optional) — Human-readable description
|
|
604
704
|
- `params.inputSchema` (`JsonObjectSchema`, optional) — Plain JSON Schema object (defaults to `{ type: 'object', properties: {} }`)
|
|
605
705
|
- `params.validateArguments` (`boolean`, optional) — Enforce `inputSchema` on `tools/call` (default: `false`); see [Argument validation](#argument-validation)
|
|
706
|
+
- `params._meta` (`Record<string, unknown>`, optional) — Tool metadata forwarded verbatim on `tools/list`; how a tool links to an [MCP Apps](#mcp-apps-interactive-uis) view
|
|
606
707
|
- `params.handler` (`(args: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>`) — Tool handler receiving the raw request arguments
|
|
607
708
|
|
|
608
709
|
**Returns:** `void`
|
|
@@ -805,7 +906,27 @@ app.listen(3000);
|
|
|
805
906
|
|
|
806
907
|
This package implements the [Model Context Protocol](https://spec.modelcontextprotocol.io/) over HTTP using JSON responses (no SSE streaming), and serves each request over the protocol revision it actually speaks.
|
|
807
908
|
|
|
808
|
-
Requests are classified once at the boundary. Traffic from today's MCP clients is served over `NodeStreamableHTTPServerTransport` with `enableJsonResponse: true`, adapting Koa's context-based middleware to the SDK's Node.js request/response expectations. Requests carrying the `2026-07-28` revision's per-request envelope are served by that revision's stateless core (`createMcpHandler`)
|
|
909
|
+
Requests are classified once at the boundary, and the classifier's answer is three-way. Traffic from today's MCP clients is served over `NodeStreamableHTTPServerTransport` with `enableJsonResponse: true`, adapting Koa's context-based middleware to the SDK's Node.js request/response expectations. Requests carrying the `2026-07-28` revision's per-request envelope are served by that revision's stateless core (`createMcpHandler`). Requests the classifier refuses outright are answered with its own rejection — the status, code, message and data it chose — rather than passed to either era's handler.
|
|
910
|
+
|
|
911
|
+
### Serving the `2026-07-28` revision
|
|
912
|
+
|
|
913
|
+
Set `createMcpServer` to a factory returning an `McpServer` with the same tools registered:
|
|
914
|
+
|
|
915
|
+
```typescript
|
|
916
|
+
const buildServer = () => {
|
|
917
|
+
const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
918
|
+
registerEverything(mcpServer);
|
|
919
|
+
return mcpServer;
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const mcpRouter = createMcpRouter(buildServer(), {
|
|
923
|
+
createMcpServer: buildServer,
|
|
924
|
+
});
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
It has to be a factory, and cannot default to the server the router was given, because the negotiated revision is instance state: serving one `2026-07-28` request marks that `McpServer` modern for good, and it then validates every later message against that revision. One instance serving both eras is pinned by the first client to speak the newer one, after which every 2025-era request is answered `-32602 Request is missing the required _meta envelope…` at HTTP 200 for the life of the process. The SDK's own serving entries take a factory and call it once per request for the same reason.
|
|
928
|
+
|
|
929
|
+
Without `createMcpServer`, `2026-07-28` requests get the unsupported-protocol-version error listing the revisions this endpoint does serve, so that client renegotiates and no other client is affected.
|
|
809
930
|
|
|
810
931
|
**Supported HTTP methods:**
|
|
811
932
|
|
|
@@ -954,3 +1075,4 @@ newest first.
|
|
|
954
1075
|
- [MCP Documentation](https://modelcontextprotocol.io)
|
|
955
1076
|
- [MCP Specification](https://spec.modelcontextprotocol.io/)
|
|
956
1077
|
- [MCP SDK TypeScript](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
1078
|
+
- [MCP Apps extension (`io.modelcontextprotocol/ui`)](https://github.com/modelcontextprotocol/ext-apps)
|
package/dist/index.cjs
CHANGED
|
@@ -36,41 +36,88 @@ var header = (ctx, name) => {
|
|
|
36
36
|
const value = ctx.get(name);
|
|
37
37
|
return value === "" ? void 0 : value;
|
|
38
38
|
};
|
|
39
|
+
/** The id to echo in an error answer, by the SDK's own rule. */
|
|
40
|
+
var echoableRequestId = body => {
|
|
41
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) return null;
|
|
42
|
+
const {
|
|
43
|
+
id,
|
|
44
|
+
method
|
|
45
|
+
} = body;
|
|
46
|
+
if (typeof method !== "string") return null;
|
|
47
|
+
return typeof id === "string" || typeof id === "number" ? id : null;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Writes a JSON-RPC error straight to the Node response: `createMcpRouter` sets
|
|
51
|
+
* `ctx.respond = false` after serving, so Koa never writes `ctx.body`.
|
|
52
|
+
*/
|
|
53
|
+
var emitJsonRpcError = (ctx, answer, body) => {
|
|
54
|
+
ctx.res.writeHead(answer.httpStatus, {
|
|
55
|
+
"content-type": "application/json"
|
|
56
|
+
});
|
|
57
|
+
ctx.res.end(JSON.stringify({
|
|
58
|
+
jsonrpc: "2.0",
|
|
59
|
+
error: {
|
|
60
|
+
code: answer.code,
|
|
61
|
+
message: answer.message,
|
|
62
|
+
...(answer.data !== void 0 && {
|
|
63
|
+
data: answer.data
|
|
64
|
+
})
|
|
65
|
+
},
|
|
66
|
+
id: echoableRequestId(body)
|
|
67
|
+
}));
|
|
68
|
+
};
|
|
69
|
+
/** The SDK's own ladder rejection, emitted with the status the SDK chose. */
|
|
70
|
+
var rejectionAnswer = rejection => {
|
|
71
|
+
return {
|
|
72
|
+
httpStatus: rejection.httpStatus,
|
|
73
|
+
code: rejection.code,
|
|
74
|
+
message: rejection.message,
|
|
75
|
+
...(rejection.data !== void 0 && {
|
|
76
|
+
data: rejection.data
|
|
77
|
+
})
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* The answer to `2026-07-28` traffic without `createMcpServer`. `data.supported`
|
|
82
|
+
* is what lets the client renegotiate onto a revision this endpoint serves.
|
|
83
|
+
*/
|
|
84
|
+
var REVISION_NOT_SERVED_ANSWER = {
|
|
85
|
+
httpStatus: 400,
|
|
86
|
+
code: _modelcontextprotocol_server.ProtocolErrorCode.UnsupportedProtocolVersion,
|
|
87
|
+
message: "Unsupported protocol version: this endpoint serves the 2025-era MCP revisions only. Pass createMcpServer to createMcpRouter to serve protocol revision 2026-07-28.",
|
|
88
|
+
data: {
|
|
89
|
+
supported: [..._modelcontextprotocol_server.SUPPORTED_PROTOCOL_VERSIONS]
|
|
90
|
+
}
|
|
91
|
+
};
|
|
39
92
|
/**
|
|
40
93
|
* Builds the per-request serving function used by `createMcpRouter`, routing
|
|
41
|
-
* each request
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* both eras through its built-in legacy fallback — is what keeps 2025-era
|
|
59
|
-
* responses byte-compatible. That fallback constructs its transport with only
|
|
60
|
-
* `sessionIdGenerator: undefined` and no way to pass `enableJsonResponse`, so
|
|
61
|
-
* every response it produces is SSE-framed (`text/event-stream`) regardless of
|
|
62
|
-
* the handler's `responseMode`. Owning the legacy branch keeps `application/json`.
|
|
94
|
+
* each request by the SDK's three-way `classifyInboundRequest` outcome.
|
|
95
|
+
*
|
|
96
|
+
* **Why the modern era needs `createMcpServer` rather than the `server` above.**
|
|
97
|
+
* The negotiated revision is *instance* state: the SDK marks an instance modern
|
|
98
|
+
* when it serves one modern request, and it then validates every later message
|
|
99
|
+
* against `2026-07-28` — there is no per-request era consult. One instance
|
|
100
|
+
* serving both eras is therefore pinned by the first client to speak the newer
|
|
101
|
+
* revision, after which every 2025-era request is answered `-32602 Request is
|
|
102
|
+
* missing the required _meta envelope…` at HTTP 200 for the life of the
|
|
103
|
+
* process. The SDK's own entries take a factory and call it per request for
|
|
104
|
+
* this reason (they also `close()` the instance, which a shared one cannot
|
|
105
|
+
* survive). Without the factory, `2026-07-28` traffic gets
|
|
106
|
+
* {@link REVISION_NOT_SERVED_ANSWER} and no other client is affected.
|
|
107
|
+
*
|
|
108
|
+
* **Why not `createMcpHandler`'s built-in legacy fallback.** It constructs its
|
|
109
|
+
* transport with no way to pass `enableJsonResponse`, so every response it
|
|
110
|
+
* produces is SSE-framed. Owning the legacy branch keeps `application/json`.
|
|
63
111
|
*/
|
|
64
112
|
var createMcpRequestServer = ({
|
|
113
|
+
createMcpServer,
|
|
65
114
|
server,
|
|
66
115
|
sessionIdGenerator
|
|
67
116
|
}) => {
|
|
68
117
|
const isStateful = sessionIdGenerator !== void 0;
|
|
69
|
-
const modernHandler = (0, _modelcontextprotocol_node.toNodeHandler)((0, _modelcontextprotocol_server.createMcpHandler)(
|
|
70
|
-
return server;
|
|
71
|
-
}, {
|
|
118
|
+
const modernHandler = createMcpServer ? (0, _modelcontextprotocol_node.toNodeHandler)((0, _modelcontextprotocol_server.createMcpHandler)(createMcpServer, {
|
|
72
119
|
legacy: "reject"
|
|
73
|
-
}));
|
|
120
|
+
})) : void 0;
|
|
74
121
|
let sharedTransport;
|
|
75
122
|
if (isStateful) {
|
|
76
123
|
sharedTransport = new _modelcontextprotocol_node.NodeStreamableHTTPServerTransport({
|
|
@@ -87,17 +134,23 @@ var createMcpRequestServer = ({
|
|
|
87
134
|
statelessQueue = result.catch(() => {});
|
|
88
135
|
return result;
|
|
89
136
|
};
|
|
90
|
-
|
|
91
|
-
|
|
137
|
+
return async (ctx, body) => {
|
|
138
|
+
const outcome = (0, _modelcontextprotocol_server.classifyInboundRequest)({
|
|
92
139
|
httpMethod: ctx.method,
|
|
93
140
|
protocolVersionHeader: header(ctx, "mcp-protocol-version"),
|
|
94
141
|
mcpMethodHeader: header(ctx, "mcp-method"),
|
|
95
142
|
mcpNameHeader: header(ctx, "mcp-name"),
|
|
96
143
|
body
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
144
|
+
});
|
|
145
|
+
if (outcome.kind === "reject") {
|
|
146
|
+
emitJsonRpcError(ctx, rejectionAnswer(outcome), body);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (outcome.kind === "modern") {
|
|
150
|
+
if (modernHandler === void 0) {
|
|
151
|
+
emitJsonRpcError(ctx, REVISION_NOT_SERVED_ANSWER, body);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
101
154
|
await modernHandler(ctx.req, ctx.res, body);
|
|
102
155
|
return;
|
|
103
156
|
}
|
|
@@ -120,6 +173,160 @@ var createMcpRequestServer = ({
|
|
|
120
173
|
};
|
|
121
174
|
};
|
|
122
175
|
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/registerAppResource.ts
|
|
178
|
+
/**
|
|
179
|
+
* Identifier of the MCP Apps extension (`io.modelcontextprotocol/ui`), used as
|
|
180
|
+
* the key under `capabilities.extensions` on both sides of the handshake.
|
|
181
|
+
*
|
|
182
|
+
* A client declares Apps support with it; a server may advertise the same key
|
|
183
|
+
* so a host can discover the support without inspecting tool metadata:
|
|
184
|
+
*
|
|
185
|
+
* ```typescript
|
|
186
|
+
* new McpServer(
|
|
187
|
+
* { name: 'weather', version: '1.0.0' },
|
|
188
|
+
* {
|
|
189
|
+
* capabilities: {
|
|
190
|
+
* extensions: {
|
|
191
|
+
* [UI_EXTENSION_ID]: { mimeTypes: [UI_RESOURCE_MIME_TYPE] },
|
|
192
|
+
* },
|
|
193
|
+
* },
|
|
194
|
+
* }
|
|
195
|
+
* );
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
var UI_EXTENSION_ID = "io.modelcontextprotocol/ui";
|
|
199
|
+
/** MIME type every MCP Apps HTML resource is served as. */
|
|
200
|
+
var UI_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
201
|
+
/**
|
|
202
|
+
* Deprecated flat tool metadata key for the linked resource URI. Written
|
|
203
|
+
* alongside `_meta.ui.resourceUri` because both forms are legal until the
|
|
204
|
+
* extension reaches GA, at which point this key is removed from the spec.
|
|
205
|
+
*/
|
|
206
|
+
var LEGACY_RESOURCE_URI_KEY = "ui/resourceUri";
|
|
207
|
+
/**
|
|
208
|
+
* Registers an MCP Apps view: a `ui://` resource a tool result is rendered
|
|
209
|
+
* with, per the `io.modelcontextprotocol/ui` extension.
|
|
210
|
+
*
|
|
211
|
+
* The extension asks nothing of the transport — a view is an ordinary MCP
|
|
212
|
+
* resource, and the tool linkage is ordinary tool `_meta` — so this is
|
|
213
|
+
* registration sugar over `registerResource`, not new protocol surface. What
|
|
214
|
+
* it owns is the part that is easy to get wrong: the `ui://` scheme, the exact
|
|
215
|
+
* MIME type, `_meta.ui` on both the declaration and the read result, and
|
|
216
|
+
* writing the deprecated flat linkage key alongside the current one.
|
|
217
|
+
*
|
|
218
|
+
* **Register the linkage unconditionally.** The extension's negotiation is
|
|
219
|
+
* per-request and only reliably readable on the `2026-07-28` revision; in this
|
|
220
|
+
* package's default stateless 2025-era mode there is no remembered
|
|
221
|
+
* `initialize` to read a client capability from. Gating registration on that
|
|
222
|
+
* capability would therefore silently drop the view for every 2025-era client.
|
|
223
|
+
* A host without Apps support simply ignores `_meta`, so keep each tool's
|
|
224
|
+
* `content` meaningful on its own and let the text result be the fallback.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```typescript
|
|
228
|
+
* import {
|
|
229
|
+
* McpServer,
|
|
230
|
+
* registerAppResource,
|
|
231
|
+
* UI_EXTENSION_ID,
|
|
232
|
+
* UI_RESOURCE_MIME_TYPE,
|
|
233
|
+
* z,
|
|
234
|
+
* } from '@ttoss/http-server-mcp';
|
|
235
|
+
*
|
|
236
|
+
* const server = new McpServer(
|
|
237
|
+
* { name: 'weather', version: '1.0.0' },
|
|
238
|
+
* {
|
|
239
|
+
* capabilities: {
|
|
240
|
+
* extensions: { [UI_EXTENSION_ID]: { mimeTypes: [UI_RESOURCE_MIME_TYPE] } },
|
|
241
|
+
* },
|
|
242
|
+
* }
|
|
243
|
+
* );
|
|
244
|
+
*
|
|
245
|
+
* const dashboard = registerAppResource({
|
|
246
|
+
* server,
|
|
247
|
+
* name: 'weather_dashboard',
|
|
248
|
+
* uri: 'ui://weather/dashboard',
|
|
249
|
+
* description: 'Interactive weather dashboard',
|
|
250
|
+
* html: dashboardHtml,
|
|
251
|
+
* ui: {
|
|
252
|
+
* csp: { connectDomains: ['https://api.openweathermap.org'] },
|
|
253
|
+
* prefersBorder: true,
|
|
254
|
+
* },
|
|
255
|
+
* });
|
|
256
|
+
*
|
|
257
|
+
* server.registerTool(
|
|
258
|
+
* 'get-weather',
|
|
259
|
+
* {
|
|
260
|
+
* description: 'Get the weather for a location',
|
|
261
|
+
* inputSchema: { location: z.string() },
|
|
262
|
+
* _meta: dashboard.toolMeta(),
|
|
263
|
+
* },
|
|
264
|
+
* async ({ location }) => {
|
|
265
|
+
* const forecast = await fetchForecast(location);
|
|
266
|
+
* return {
|
|
267
|
+
* // Text stays meaningful: it is what a host without Apps renders.
|
|
268
|
+
* content: [{ type: 'text', text: summarise(forecast) }],
|
|
269
|
+
* structuredContent: forecast,
|
|
270
|
+
* };
|
|
271
|
+
* }
|
|
272
|
+
* );
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
var registerAppResource = ({
|
|
276
|
+
server,
|
|
277
|
+
name,
|
|
278
|
+
uri,
|
|
279
|
+
description,
|
|
280
|
+
html,
|
|
281
|
+
ui
|
|
282
|
+
}) => {
|
|
283
|
+
if (!uri.startsWith("ui://")) throw new Error(`An MCP Apps resource URI must use the ui:// scheme (received "${uri}").`);
|
|
284
|
+
const meta = ui === void 0 ? void 0 : {
|
|
285
|
+
ui
|
|
286
|
+
};
|
|
287
|
+
const resource = server.registerResource(name, uri, {
|
|
288
|
+
description,
|
|
289
|
+
mimeType: UI_RESOURCE_MIME_TYPE,
|
|
290
|
+
...(meta === void 0 ? {} : {
|
|
291
|
+
_meta: meta
|
|
292
|
+
})
|
|
293
|
+
}, async requestedUri => {
|
|
294
|
+
const text = typeof html === "string" ? html : await html({
|
|
295
|
+
uri: requestedUri
|
|
296
|
+
});
|
|
297
|
+
return {
|
|
298
|
+
contents: [{
|
|
299
|
+
uri: requestedUri.href,
|
|
300
|
+
mimeType: UI_RESOURCE_MIME_TYPE,
|
|
301
|
+
text,
|
|
302
|
+
...(meta === void 0 ? {} : {
|
|
303
|
+
_meta: meta
|
|
304
|
+
})
|
|
305
|
+
}]
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
const toolMeta = ({
|
|
309
|
+
visibility,
|
|
310
|
+
_meta
|
|
311
|
+
} = {}) => {
|
|
312
|
+
return {
|
|
313
|
+
..._meta,
|
|
314
|
+
ui: {
|
|
315
|
+
resourceUri: uri,
|
|
316
|
+
...(visibility === void 0 ? {} : {
|
|
317
|
+
visibility
|
|
318
|
+
})
|
|
319
|
+
},
|
|
320
|
+
[LEGACY_RESOURCE_URI_KEY]: uri
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
return {
|
|
324
|
+
uri,
|
|
325
|
+
resource,
|
|
326
|
+
toolMeta
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
|
|
123
330
|
//#endregion
|
|
124
331
|
//#region src/registerToolFromSchema.ts
|
|
125
332
|
/**
|
|
@@ -163,7 +370,7 @@ var toStandardSchema = ({
|
|
|
163
370
|
*
|
|
164
371
|
* @param server - The `McpServer` instance to register the tool on.
|
|
165
372
|
* @param params - Tool configuration including name, description, inputSchema,
|
|
166
|
-
* validateArguments, and handler.
|
|
373
|
+
* validateArguments, `_meta`, and handler.
|
|
167
374
|
*
|
|
168
375
|
* @example
|
|
169
376
|
* ```typescript
|
|
@@ -194,14 +401,16 @@ var registerToolFromSchema = (server, params) => {
|
|
|
194
401
|
properties: {}
|
|
195
402
|
},
|
|
196
403
|
validateArguments = false,
|
|
197
|
-
handler
|
|
404
|
+
handler,
|
|
405
|
+
_meta
|
|
198
406
|
} = params;
|
|
199
407
|
server.registerTool(name, {
|
|
200
408
|
description,
|
|
201
409
|
inputSchema: toStandardSchema({
|
|
202
410
|
inputSchema,
|
|
203
411
|
validateArguments
|
|
204
|
-
})
|
|
412
|
+
}),
|
|
413
|
+
_meta
|
|
205
414
|
}, async args => {
|
|
206
415
|
return handler(args);
|
|
207
416
|
});
|
|
@@ -316,7 +525,8 @@ var createGatedToolRegistrar = ({
|
|
|
316
525
|
};
|
|
317
526
|
server.registerTool(def.name, {
|
|
318
527
|
description: def.description,
|
|
319
|
-
inputSchema: def.inputSchema
|
|
528
|
+
inputSchema: def.inputSchema,
|
|
529
|
+
_meta: def._meta
|
|
320
530
|
}, handler);
|
|
321
531
|
};
|
|
322
532
|
return {
|
|
@@ -535,6 +745,7 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
535
745
|
path = "/mcp",
|
|
536
746
|
aliases = [],
|
|
537
747
|
sessionIdGenerator,
|
|
748
|
+
createMcpServer,
|
|
538
749
|
apiBaseUrl,
|
|
539
750
|
getApiHeaders,
|
|
540
751
|
auth
|
|
@@ -542,7 +753,8 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
542
753
|
const needsContext = apiBaseUrl !== void 0 || getApiHeaders !== void 0 || auth !== void 0;
|
|
543
754
|
const serveRequest = createMcpRequestServer({
|
|
544
755
|
server,
|
|
545
|
-
sessionIdGenerator
|
|
756
|
+
sessionIdGenerator,
|
|
757
|
+
createMcpServer
|
|
546
758
|
});
|
|
547
759
|
const router = new _ttoss_http_server.Router();
|
|
548
760
|
let authCheck;
|
|
@@ -642,11 +854,14 @@ Object.defineProperty(exports, 'McpServer', {
|
|
|
642
854
|
return _modelcontextprotocol_server.McpServer;
|
|
643
855
|
}
|
|
644
856
|
});
|
|
857
|
+
exports.UI_EXTENSION_ID = UI_EXTENSION_ID;
|
|
858
|
+
exports.UI_RESOURCE_MIME_TYPE = UI_RESOURCE_MIME_TYPE;
|
|
645
859
|
exports.apiCall = apiCall;
|
|
646
860
|
exports.checkScopes = checkScopes;
|
|
647
861
|
exports.createGatedToolRegistrar = createGatedToolRegistrar;
|
|
648
862
|
exports.createMcpRouter = createMcpRouter;
|
|
649
863
|
exports.getIdentity = getIdentity;
|
|
864
|
+
exports.registerAppResource = registerAppResource;
|
|
650
865
|
exports.registerToolFromSchema = registerToolFromSchema;
|
|
651
866
|
Object.defineProperty(exports, 'z', {
|
|
652
867
|
enumerable: true,
|