@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 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`) instead. Both paths are served from the same `McpServer` instance and the same registered tools, so supporting the newer revision requires no configuration.
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 to the protocol revision it actually speaks.
42
- *
43
- * Requests are classified once at the boundary with the SDK's own
44
- * `classifyInboundRequest`:
45
- *
46
- * - **2025-era traffic** (everything without a `2026-07-28` per-request
47
- * envelope — which is all traffic from today's MCP clients) is served over
48
- * `NodeStreamableHTTPServerTransport` with `enableJsonResponse: true`, the
49
- * same wiring and the same plain-JSON responses this package has always
50
- * produced.
51
- * - **`2026-07-28` traffic** is served by `createMcpHandler` with
52
- * `legacy: 'reject'`, so the modern revision is handled by the SDK's
53
- * stateless core rather than the transport above. Malformed requests are
54
- * routed here too, so the SDK emits its own spec-defined rejection instead of
55
- * this package inventing one.
56
- *
57
- * Splitting on the classifier — instead of letting `createMcpHandler` serve
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
- const isLegacy = (ctx, body) => {
91
- return (0, _modelcontextprotocol_server.classifyInboundRequest)({
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
- }).kind === "legacy";
98
- };
99
- return async (ctx, body) => {
100
- if (!isLegacy(ctx, body)) {
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,