@ttoss/http-server-mcp 0.27.0 → 0.28.0

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/README.md CHANGED
@@ -371,7 +371,7 @@ The `WWW-Authenticate: Bearer resource_metadata="…"` header is how MCP clients
371
371
 
372
372
  The two behaviors the [MCP authorization spec](https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/authorization/) requires for client bootstrapping are built into the `auth` option, so you no longer need the hand-rolled middleware shown above:
373
373
 
374
- - **`publicMethods`** — JSON-RPC methods that bypass verification, read from the request body's `method` field. Defaults to `['initialize']`, which the spec sanctions so a client can complete the lifecycle handshake before it can discover the authorization server. Pass `[]` to require a token for every method, or a custom list to change the exempt set. Adding `tools/list` serves the full tool catalogue — every tool name, description, and input schema — to unauthenticated callers; see [`publicMethods` and OAuth clients](#publicmethods-and-oauth-clients) before doing so.
374
+ - **`publicMethods`** — JSON-RPC methods that bypass verification, read from the request body's `method` field. Defaults to `['initialize', 'server/discover']` — the lifecycle handshake of each protocol era, which the spec sanctions so a client can complete it before it can discover the authorization server. Pass `[]` to require a token for every method, or a custom list to change the exempt set. Adding `tools/list` serves the full tool catalogue — every tool name, description, and input schema — to unauthenticated callers; see [`publicMethods` and OAuth clients](#publicmethods-and-oauth-clients) before doing so.
375
375
  - **`resourceMetadataUrl`** — the URL a `401` advertises as `WWW-Authenticate: Bearer resource_metadata="<url>"` (RFC 9728) instead of a bare `Bearer`, pointing MCP clients at the protected-resource metadata document. **You normally do not set it**: when the document is served — i.e. both `resourceServerUrl` and `authorizationServerUrl` are configured — it defaults to the RFC 9728 location this router serves it at, so the header and the routes cannot drift apart. When the document is not served, the header stays a bare `Bearer` rather than naming a location with no route. The field exists for the one configuration where this router deliberately does not serve the document — an `oauthServer()` in the same deployment already answers that path, so this router is mounted without `resourceServerUrl`/`authorizationServerUrl` to avoid two routers on one path, and has nothing to derive from. Compute the value with `protectedResourceMetadataUrl` from [`@ttoss/auth-core`](https://ttoss.dev/docs/modules/packages/auth-core) rather than typing it, so both halves apply the same §3.1 rule.
376
376
 
377
377
  ```typescript
@@ -383,7 +383,7 @@ createMcpRouter(mcpServer, {
383
383
  resourceServerUrl: 'https://mcp.example.com',
384
384
  authorizationServerUrl:
385
385
  'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxx',
386
- // publicMethods defaults to ['initialize'].
386
+ // publicMethods defaults to ['initialize', 'server/discover'].
387
387
  },
388
388
  });
389
389
  ```
@@ -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.
@@ -502,7 +574,7 @@ Creates a Koa router configured to handle MCP protocol requests.
502
574
  - `auth.verifyToken` — Custom async token verifier `(token: string) => Promise<unknown>`
503
575
  - `auth.requiredScopes` — Router-level scope guard; returns 403 if any scope is missing
504
576
  - `auth.resourceServerUrl` + `auth.authorizationServerUrl` — Enable the protected-resource metadata document, served at both `/.well-known/oauth-protected-resource` and the RFC 9728 path-derived `/.well-known/oauth-protected-resource<path>`; the metadata `resource` is set to `resourceServerUrl + path` so clients following `resource` land on the actual MCP endpoint
505
- - `auth.publicMethods` — JSON-RPC methods that bypass verification (default `['initialize']`)
577
+ - `auth.publicMethods` — JSON-RPC methods that bypass verification (default `['initialize', 'server/discover']`)
506
578
  - `auth.resourceMetadataUrl` — URL advertised in the RFC 9728 `WWW-Authenticate: Bearer resource_metadata="…"` header on a 401. Defaults to the location derived from `resourceServerUrl` + `path` (the one this router serves), so the header cannot drift from the routes; set it only when a separate `oauthServer()` serves the document and this router is mounted without `resourceServerUrl`/`authorizationServerUrl`
507
579
  - `auth.resourceIndicator` — Expected `aud` value(s) (RFC 8707); rejects tokens minted for a different resource. See [Resource indicator validation](#resource-indicator-validation-rfc-8707)
508
580
 
@@ -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`
@@ -873,7 +974,7 @@ expect(call.status).toBe(200);
873
974
 
874
975
  ### `publicMethods` and OAuth clients
875
976
 
876
- `auth.publicMethods` defaults to `['initialize']`: the handshake is reachable without a token, nothing else is.
977
+ `auth.publicMethods` defaults to `['initialize', 'server/discover']`: the lifecycle handshake is reachable without a token, nothing else is. There are two entries because the exemption is about _the handshake_, not the method name — `initialize` is the 2025-era handshake and `server/discover` its `2026-07-28` replacement, since that revision removed `initialize` outright. Naming only one leaves the other era unable to negotiate at all.
877
978
 
878
979
  **Adding `tools/list` is a deliberate exposure.** It serves the full tool catalogue — every name, description, and input schema — to anyone who can reach the endpoint. Those leak internal resource names and domain vocabulary, and for an OpenAPI-derived server the schemas mirror the REST surface, so the catalogue becomes an unauthenticated map of the whole API including operations the caller could never invoke. It buys an OAuth client nothing, because the `401` is what starts the authorization flow and a client that lists tools anonymously still cannot call one. Open it only to serve callers that will never authenticate:
879
980
 
@@ -885,7 +986,7 @@ publicMethods: ['initialize', 'tools/list'],
885
986
 
886
987
  Either way the authorization flow works. Driving the official MCP client SDK against this router shows the RFC 9728 challenge resolving to the protected-resource document, the authorization server, and the redirect, whether the `401` arrives on `initialize` (`publicMethods: []`) or on the request after it (the default). `tests/unit/tests/oauth-client-flow.test.ts` asserts it.
887
988
 
888
- On the `2026-07-28` revision `initialize` does not exist — discovery is `server/discover`, which is not in the default set and therefore always requires a token. That is deliberate: RFC 9728 discovery is driven by the challenge itself.
989
+ **Whether the handshake should be public at all is a separate question.** `publicMethods: []` closes both eras, and the authorization flow still works — RFC 9728 discovery is driven by the challenge itself, so a client authenticates from its very first request instead of one step later.
889
990
 
890
991
  ## AWS Lambda Deployment
891
992
 
@@ -954,3 +1055,4 @@ newest first.
954
1055
  - [MCP Documentation](https://modelcontextprotocol.io)
955
1056
  - [MCP Specification](https://spec.modelcontextprotocol.io/)
956
1057
  - [MCP SDK TypeScript](https://github.com/modelcontextprotocol/typescript-sdk)
1058
+ - [MCP Apps extension (`io.modelcontextprotocol/ui`)](https://github.com/modelcontextprotocol/ext-apps)
package/dist/index.cjs CHANGED
@@ -120,6 +120,160 @@ var createMcpRequestServer = ({
120
120
  };
121
121
  };
122
122
 
123
+ //#endregion
124
+ //#region src/registerAppResource.ts
125
+ /**
126
+ * Identifier of the MCP Apps extension (`io.modelcontextprotocol/ui`), used as
127
+ * the key under `capabilities.extensions` on both sides of the handshake.
128
+ *
129
+ * A client declares Apps support with it; a server may advertise the same key
130
+ * so a host can discover the support without inspecting tool metadata:
131
+ *
132
+ * ```typescript
133
+ * new McpServer(
134
+ * { name: 'weather', version: '1.0.0' },
135
+ * {
136
+ * capabilities: {
137
+ * extensions: {
138
+ * [UI_EXTENSION_ID]: { mimeTypes: [UI_RESOURCE_MIME_TYPE] },
139
+ * },
140
+ * },
141
+ * }
142
+ * );
143
+ * ```
144
+ */
145
+ var UI_EXTENSION_ID = "io.modelcontextprotocol/ui";
146
+ /** MIME type every MCP Apps HTML resource is served as. */
147
+ var UI_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
148
+ /**
149
+ * Deprecated flat tool metadata key for the linked resource URI. Written
150
+ * alongside `_meta.ui.resourceUri` because both forms are legal until the
151
+ * extension reaches GA, at which point this key is removed from the spec.
152
+ */
153
+ var LEGACY_RESOURCE_URI_KEY = "ui/resourceUri";
154
+ /**
155
+ * Registers an MCP Apps view: a `ui://` resource a tool result is rendered
156
+ * with, per the `io.modelcontextprotocol/ui` extension.
157
+ *
158
+ * The extension asks nothing of the transport — a view is an ordinary MCP
159
+ * resource, and the tool linkage is ordinary tool `_meta` — so this is
160
+ * registration sugar over `registerResource`, not new protocol surface. What
161
+ * it owns is the part that is easy to get wrong: the `ui://` scheme, the exact
162
+ * MIME type, `_meta.ui` on both the declaration and the read result, and
163
+ * writing the deprecated flat linkage key alongside the current one.
164
+ *
165
+ * **Register the linkage unconditionally.** The extension's negotiation is
166
+ * per-request and only reliably readable on the `2026-07-28` revision; in this
167
+ * package's default stateless 2025-era mode there is no remembered
168
+ * `initialize` to read a client capability from. Gating registration on that
169
+ * capability would therefore silently drop the view for every 2025-era client.
170
+ * A host without Apps support simply ignores `_meta`, so keep each tool's
171
+ * `content` meaningful on its own and let the text result be the fallback.
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * import {
176
+ * McpServer,
177
+ * registerAppResource,
178
+ * UI_EXTENSION_ID,
179
+ * UI_RESOURCE_MIME_TYPE,
180
+ * z,
181
+ * } from '@ttoss/http-server-mcp';
182
+ *
183
+ * const server = new McpServer(
184
+ * { name: 'weather', version: '1.0.0' },
185
+ * {
186
+ * capabilities: {
187
+ * extensions: { [UI_EXTENSION_ID]: { mimeTypes: [UI_RESOURCE_MIME_TYPE] } },
188
+ * },
189
+ * }
190
+ * );
191
+ *
192
+ * const dashboard = registerAppResource({
193
+ * server,
194
+ * name: 'weather_dashboard',
195
+ * uri: 'ui://weather/dashboard',
196
+ * description: 'Interactive weather dashboard',
197
+ * html: dashboardHtml,
198
+ * ui: {
199
+ * csp: { connectDomains: ['https://api.openweathermap.org'] },
200
+ * prefersBorder: true,
201
+ * },
202
+ * });
203
+ *
204
+ * server.registerTool(
205
+ * 'get-weather',
206
+ * {
207
+ * description: 'Get the weather for a location',
208
+ * inputSchema: { location: z.string() },
209
+ * _meta: dashboard.toolMeta(),
210
+ * },
211
+ * async ({ location }) => {
212
+ * const forecast = await fetchForecast(location);
213
+ * return {
214
+ * // Text stays meaningful: it is what a host without Apps renders.
215
+ * content: [{ type: 'text', text: summarise(forecast) }],
216
+ * structuredContent: forecast,
217
+ * };
218
+ * }
219
+ * );
220
+ * ```
221
+ */
222
+ var registerAppResource = ({
223
+ server,
224
+ name,
225
+ uri,
226
+ description,
227
+ html,
228
+ ui
229
+ }) => {
230
+ if (!uri.startsWith("ui://")) throw new Error(`An MCP Apps resource URI must use the ui:// scheme (received "${uri}").`);
231
+ const meta = ui === void 0 ? void 0 : {
232
+ ui
233
+ };
234
+ const resource = server.registerResource(name, uri, {
235
+ description,
236
+ mimeType: UI_RESOURCE_MIME_TYPE,
237
+ ...(meta === void 0 ? {} : {
238
+ _meta: meta
239
+ })
240
+ }, async requestedUri => {
241
+ const text = typeof html === "string" ? html : await html({
242
+ uri: requestedUri
243
+ });
244
+ return {
245
+ contents: [{
246
+ uri: requestedUri.href,
247
+ mimeType: UI_RESOURCE_MIME_TYPE,
248
+ text,
249
+ ...(meta === void 0 ? {} : {
250
+ _meta: meta
251
+ })
252
+ }]
253
+ };
254
+ });
255
+ const toolMeta = ({
256
+ visibility,
257
+ _meta
258
+ } = {}) => {
259
+ return {
260
+ ..._meta,
261
+ ui: {
262
+ resourceUri: uri,
263
+ ...(visibility === void 0 ? {} : {
264
+ visibility
265
+ })
266
+ },
267
+ [LEGACY_RESOURCE_URI_KEY]: uri
268
+ };
269
+ };
270
+ return {
271
+ uri,
272
+ resource,
273
+ toolMeta
274
+ };
275
+ };
276
+
123
277
  //#endregion
124
278
  //#region src/registerToolFromSchema.ts
125
279
  /**
@@ -163,7 +317,7 @@ var toStandardSchema = ({
163
317
  *
164
318
  * @param server - The `McpServer` instance to register the tool on.
165
319
  * @param params - Tool configuration including name, description, inputSchema,
166
- * validateArguments, and handler.
320
+ * validateArguments, `_meta`, and handler.
167
321
  *
168
322
  * @example
169
323
  * ```typescript
@@ -194,14 +348,16 @@ var registerToolFromSchema = (server, params) => {
194
348
  properties: {}
195
349
  },
196
350
  validateArguments = false,
197
- handler
351
+ handler,
352
+ _meta
198
353
  } = params;
199
354
  server.registerTool(name, {
200
355
  description,
201
356
  inputSchema: toStandardSchema({
202
357
  inputSchema,
203
358
  validateArguments
204
- })
359
+ }),
360
+ _meta
205
361
  }, async args => {
206
362
  return handler(args);
207
363
  });
@@ -316,7 +472,8 @@ var createGatedToolRegistrar = ({
316
472
  };
317
473
  server.registerTool(def.name, {
318
474
  description: def.description,
319
- inputSchema: def.inputSchema
475
+ inputSchema: def.inputSchema,
476
+ _meta: def._meta
320
477
  }, handler);
321
478
  };
322
479
  return {
@@ -327,13 +484,23 @@ var createGatedToolRegistrar = ({
327
484
  //#endregion
328
485
  //#region src/index.ts
329
486
  /**
330
- * MCP lifecycle methods reachable before a client authenticates.
487
+ * MCP lifecycle handshakes reachable before a client authenticates.
488
+ *
489
+ * One entry per protocol era, because the exemption the MCP authorization spec
490
+ * sanctions is about *the handshake* — a client has to complete it before it
491
+ * can discover the authorization server — not about a method name:
492
+ *
493
+ * - `initialize` is the 2025-era handshake.
494
+ * - `server/discover` is its `2026-07-28` replacement. That revision removed
495
+ * `initialize` entirely, so naming only `initialize` left the modern era
496
+ * with no public method at all — the asymmetry ttoss/ttoss#1222 fixed.
497
+ *
498
+ * A future revision that renames the handshake again needs a matching entry
499
+ * here; nothing detects that automatically.
331
500
  *
332
- * Only `initialize`, which the MCP authorization spec sanctions: a client has
333
- * to complete the handshake before it can discover the authorization server.
334
- * `tools/list` is not here — see {@link McpAuthOptions.publicMethods}.
501
+ * `tools/list` is deliberately absent — see {@link McpAuthOptions.publicMethods}.
335
502
  */
336
- var DEFAULT_PUBLIC_METHODS = ["initialize"];
503
+ var DEFAULT_PUBLIC_METHODS = ["initialize", "server/discover"];
337
504
  /**
338
505
  * Asserts that a verified token's `aud` claim includes at least one of the
339
506
  * expected resource indicator values. `aud` may be a single string or an
@@ -632,11 +799,14 @@ Object.defineProperty(exports, 'McpServer', {
632
799
  return _modelcontextprotocol_server.McpServer;
633
800
  }
634
801
  });
802
+ exports.UI_EXTENSION_ID = UI_EXTENSION_ID;
803
+ exports.UI_RESOURCE_MIME_TYPE = UI_RESOURCE_MIME_TYPE;
635
804
  exports.apiCall = apiCall;
636
805
  exports.checkScopes = checkScopes;
637
806
  exports.createGatedToolRegistrar = createGatedToolRegistrar;
638
807
  exports.createMcpRouter = createMcpRouter;
639
808
  exports.getIdentity = getIdentity;
809
+ exports.registerAppResource = registerAppResource;
640
810
  exports.registerToolFromSchema = registerToolFromSchema;
641
811
  Object.defineProperty(exports, 'z', {
642
812
  enumerable: true,
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  /// <reference types="node" />
3
- import { CallToolResult, McpServer, McpServer as McpServer$1 } from "@modelcontextprotocol/server";
3
+ import { CallToolResult, McpServer, McpServer as McpServer$1, RegisteredResource } from "@modelcontextprotocol/server";
4
4
  import { Router } from "@ttoss/http-server";
5
5
  import accepts from "accepts";
6
6
  import { AsyncLocalStorage } from "async_hooks";
@@ -675,6 +675,197 @@ declare namespace Application {
675
675
  */
676
676
  declare const getIdentity: <T = unknown>() => T | undefined;
677
677
  //#endregion
678
+ //#region src/registerAppResource.d.ts
679
+ /**
680
+ * Identifier of the MCP Apps extension (`io.modelcontextprotocol/ui`), used as
681
+ * the key under `capabilities.extensions` on both sides of the handshake.
682
+ *
683
+ * A client declares Apps support with it; a server may advertise the same key
684
+ * so a host can discover the support without inspecting tool metadata:
685
+ *
686
+ * ```typescript
687
+ * new McpServer(
688
+ * { name: 'weather', version: '1.0.0' },
689
+ * {
690
+ * capabilities: {
691
+ * extensions: {
692
+ * [UI_EXTENSION_ID]: { mimeTypes: [UI_RESOURCE_MIME_TYPE] },
693
+ * },
694
+ * },
695
+ * }
696
+ * );
697
+ * ```
698
+ */
699
+ declare const UI_EXTENSION_ID = "io.modelcontextprotocol/ui";
700
+ /** MIME type every MCP Apps HTML resource is served as. */
701
+ declare const UI_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
702
+ /**
703
+ * Origins an app's view is allowed to reach, mapped by the host onto the
704
+ * iframe's Content Security Policy. Anything not declared is blocked: an
705
+ * omitted list is the restrictive default, not "no restriction".
706
+ */
707
+ interface UiResourceCsp {
708
+ /** Origins for network requests (`connect-src`). */
709
+ connectDomains?: string[];
710
+ /**
711
+ * Origins for scripts, styles, images, fonts and media (`script-src`,
712
+ * `style-src`, `img-src`, `font-src`, `media-src`).
713
+ */
714
+ resourceDomains?: string[];
715
+ /** Origins for nested iframes (`frame-src`). */
716
+ frameDomains?: string[];
717
+ /** Allowed document base URIs (`base-uri`). */
718
+ baseUriDomains?: string[];
719
+ }
720
+ /**
721
+ * Browser capabilities the view asks for. A host *may* grant them, so a view
722
+ * should feature-detect rather than assume: each key is a request, not a
723
+ * guarantee.
724
+ */
725
+ interface UiResourcePermissions {
726
+ camera?: Record<string, never>;
727
+ microphone?: Record<string, never>;
728
+ geolocation?: Record<string, never>;
729
+ clipboardWrite?: Record<string, never>;
730
+ }
731
+ /** Rendering and security configuration for an app's view (`_meta.ui`). */
732
+ interface UiResourceMeta {
733
+ /** Origins the view may reach; omitted means none. */
734
+ csp?: UiResourceCsp;
735
+ /** Browser capabilities requested for the view. */
736
+ permissions?: UiResourcePermissions;
737
+ /**
738
+ * Dedicated sandbox origin for the view, for cases needing a stable origin
739
+ * (OAuth callbacks, CORS, API key allowlists). The accepted format is
740
+ * host-specific; consult the host's documentation rather than deriving one.
741
+ */
742
+ domain?: string;
743
+ /** Whether the host should draw a border and background around the view. */
744
+ prefersBorder?: boolean;
745
+ }
746
+ /** Who may reach a tool. Defaults to both when left unset. */
747
+ type UiToolVisibility = 'model' | 'app';
748
+ /** Parameters for {@link registerAppResource}. */
749
+ interface RegisterAppResourceParams {
750
+ /** The `McpServer` instance to register the app resource on. */
751
+ server: McpServer$1;
752
+ /** Resource name, as listed by `resources/list`. */
753
+ name: string;
754
+ /** Resource URI. Must use the `ui://` scheme. */
755
+ uri: string;
756
+ /** What the view does and when a host should render it. */
757
+ description?: string;
758
+ /**
759
+ * The view's HTML5 document, or a builder invoked per `resources/read`.
760
+ * A builder receives the requested URI so one registration can serve
761
+ * variants of a document.
762
+ */
763
+ html: string | ((args: {
764
+ uri: URL;
765
+ }) => string | Promise<string>);
766
+ /** Rendering and security configuration relayed to the host as `_meta.ui`. */
767
+ ui?: UiResourceMeta;
768
+ }
769
+ /** Parameters for {@link RegisteredAppResource.toolMeta}. */
770
+ interface ToolMetaParams {
771
+ /**
772
+ * Who may reach the tool. Left unset, the spec's default applies — visible
773
+ * to the model *and* callable by the app.
774
+ */
775
+ visibility?: UiToolVisibility[];
776
+ /** Further `_meta` entries to merge in, e.g. vendor-namespaced keys. */
777
+ _meta?: Record<string, unknown>;
778
+ }
779
+ /** An app resource registered by {@link registerAppResource}. */
780
+ interface RegisteredAppResource {
781
+ /** The URI tools link to. */
782
+ uri: string;
783
+ /** The SDK's registration handle, for `update`/`disable`/`remove`. */
784
+ resource: RegisteredResource;
785
+ /**
786
+ * Builds the `_meta` bag that links a tool to this view. Pass it to
787
+ * `registerTool`, {@link registerToolFromSchema}, or a `GatedToolDef` —
788
+ * whichever registration path the tool uses.
789
+ */
790
+ toolMeta: (params?: ToolMetaParams) => Record<string, unknown>;
791
+ }
792
+ /**
793
+ * Registers an MCP Apps view: a `ui://` resource a tool result is rendered
794
+ * with, per the `io.modelcontextprotocol/ui` extension.
795
+ *
796
+ * The extension asks nothing of the transport — a view is an ordinary MCP
797
+ * resource, and the tool linkage is ordinary tool `_meta` — so this is
798
+ * registration sugar over `registerResource`, not new protocol surface. What
799
+ * it owns is the part that is easy to get wrong: the `ui://` scheme, the exact
800
+ * MIME type, `_meta.ui` on both the declaration and the read result, and
801
+ * writing the deprecated flat linkage key alongside the current one.
802
+ *
803
+ * **Register the linkage unconditionally.** The extension's negotiation is
804
+ * per-request and only reliably readable on the `2026-07-28` revision; in this
805
+ * package's default stateless 2025-era mode there is no remembered
806
+ * `initialize` to read a client capability from. Gating registration on that
807
+ * capability would therefore silently drop the view for every 2025-era client.
808
+ * A host without Apps support simply ignores `_meta`, so keep each tool's
809
+ * `content` meaningful on its own and let the text result be the fallback.
810
+ *
811
+ * @example
812
+ * ```typescript
813
+ * import {
814
+ * McpServer,
815
+ * registerAppResource,
816
+ * UI_EXTENSION_ID,
817
+ * UI_RESOURCE_MIME_TYPE,
818
+ * z,
819
+ * } from '@ttoss/http-server-mcp';
820
+ *
821
+ * const server = new McpServer(
822
+ * { name: 'weather', version: '1.0.0' },
823
+ * {
824
+ * capabilities: {
825
+ * extensions: { [UI_EXTENSION_ID]: { mimeTypes: [UI_RESOURCE_MIME_TYPE] } },
826
+ * },
827
+ * }
828
+ * );
829
+ *
830
+ * const dashboard = registerAppResource({
831
+ * server,
832
+ * name: 'weather_dashboard',
833
+ * uri: 'ui://weather/dashboard',
834
+ * description: 'Interactive weather dashboard',
835
+ * html: dashboardHtml,
836
+ * ui: {
837
+ * csp: { connectDomains: ['https://api.openweathermap.org'] },
838
+ * prefersBorder: true,
839
+ * },
840
+ * });
841
+ *
842
+ * server.registerTool(
843
+ * 'get-weather',
844
+ * {
845
+ * description: 'Get the weather for a location',
846
+ * inputSchema: { location: z.string() },
847
+ * _meta: dashboard.toolMeta(),
848
+ * },
849
+ * async ({ location }) => {
850
+ * const forecast = await fetchForecast(location);
851
+ * return {
852
+ * // Text stays meaningful: it is what a host without Apps renders.
853
+ * content: [{ type: 'text', text: summarise(forecast) }],
854
+ * structuredContent: forecast,
855
+ * };
856
+ * }
857
+ * );
858
+ * ```
859
+ */
860
+ declare const registerAppResource: ({
861
+ server,
862
+ name,
863
+ uri,
864
+ description,
865
+ html,
866
+ ui
867
+ }: RegisterAppResourceParams) => RegisteredAppResource;
868
+ //#endregion
678
869
  //#region src/registerToolFromSchema.d.ts
679
870
  /**
680
871
  * A plain JSON Schema object (draft-07 compatible) describing the shape of a
@@ -721,6 +912,19 @@ interface RegisterToolFromSchemaParams {
721
912
  * @default false
722
913
  */
723
914
  validateArguments?: boolean;
915
+ /**
916
+ * Tool metadata forwarded verbatim on `tools/list`.
917
+ *
918
+ * The MCP Apps extension links a tool to the view its result is rendered
919
+ * with through this field; build the bag with `toolMeta()` from
920
+ * `registerAppResource` rather than writing the keys by hand.
921
+ *
922
+ * @example
923
+ * ```typescript
924
+ * _meta: dashboard.toolMeta({ visibility: ['app'] }),
925
+ * ```
926
+ */
927
+ _meta?: Record<string, unknown>;
724
928
  /**
725
929
  * Tool handler invoked when the AI client calls the tool.
726
930
  * Receives the request arguments, validated against `inputSchema` only when
@@ -744,7 +948,7 @@ interface RegisterToolFromSchemaParams {
744
948
  *
745
949
  * @param server - The `McpServer` instance to register the tool on.
746
950
  * @param params - Tool configuration including name, description, inputSchema,
747
- * validateArguments, and handler.
951
+ * validateArguments, `_meta`, and handler.
748
952
  *
749
953
  * @example
750
954
  * ```typescript
@@ -801,7 +1005,12 @@ type GatedToolDef = {
801
1005
  * Each gate receives the full {@link ToolCallContext} (identity + args).
802
1006
  * Throw to reject the call; return (or resolve) to continue.
803
1007
  */
804
- gates?: Array<(ctx: ToolCallContext) => void | Promise<void>>; /** The tool handler. Receives merged call args + `buildContext` output. */
1008
+ gates?: Array<(ctx: ToolCallContext) => void | Promise<void>>;
1009
+ /**
1010
+ * Tool metadata forwarded verbatim on `tools/list` — how a gated tool links
1011
+ * to an MCP Apps view (`registerAppResource(...).toolMeta()`).
1012
+ */
1013
+ _meta?: Record<string, unknown>; /** The tool handler. Receives merged call args + `buildContext` output. */
805
1014
  method: (args: Record<string, unknown>) => Promise<unknown>;
806
1015
  };
807
1016
  /** Options for {@link createGatedToolRegistrar}. */
@@ -893,10 +1102,13 @@ interface McpAuthOptions {
893
1102
  /**
894
1103
  * JSON-RPC methods (read from `body.method`) that bypass verification.
895
1104
  *
896
- * `initialize` is public because the MCP authorization spec sanctions it: a
897
- * client completes the lifecycle handshake before it can discover the
898
- * authorization server. Nothing else is, so a server with `auth` configured
899
- * serves no tool metadata to an unauthenticated caller.
1105
+ * The lifecycle handshake is public because the MCP authorization spec
1106
+ * sanctions it: a client completes it before it can discover the
1107
+ * authorization server. That means one entry per protocol era —
1108
+ * `initialize` on 2025, `server/discover` on `2026-07-28`, which removed
1109
+ * `initialize` — so neither era is left unable to negotiate. Nothing else
1110
+ * is public, so a server with `auth` configured serves no tool metadata to
1111
+ * an unauthenticated caller.
900
1112
  *
901
1113
  * Opening `tools/list` is a deliberate choice, not a default. It serves the
902
1114
  * full tool catalogue — every name, description, and input schema — to
@@ -907,7 +1119,7 @@ interface McpAuthOptions {
907
1119
  * cannot call one. Set it only to serve callers that will never
908
1120
  * authenticate.
909
1121
  *
910
- * @default ['initialize']
1122
+ * @default ['initialize', 'server/discover']
911
1123
  *
912
1124
  * @example
913
1125
  * ```typescript
@@ -1135,9 +1347,10 @@ interface McpRouterOptions {
1135
1347
  * OAuth / JWT authentication configuration for the MCP endpoint.
1136
1348
  *
1137
1349
  * When set, incoming MCP requests must include a valid Bearer token in the
1138
- * `Authorization` header — except for `publicMethods` (by default just
1139
- * `initialize`), which bypasses verification so a client can complete the
1140
- * lifecycle handshake before authenticating. Invalid or missing tokens
1350
+ * `Authorization` header — except for `publicMethods` (by default the
1351
+ * lifecycle handshake of each protocol era: `initialize` and
1352
+ * `server/discover`), which bypass verification so a client can complete
1353
+ * the handshake before authenticating. Invalid or missing tokens
1141
1354
  * receive a `401` response with `WWW-Authenticate: Bearer` (or
1142
1355
  * `Bearer resource_metadata="..."` when `resourceMetadataUrl` is set, per
1143
1356
  * RFC 9728). Tokens that fail a `requiredScopes` check receive `403`.
@@ -1206,4 +1419,4 @@ interface McpRouterOptions {
1206
1419
  */
1207
1420
  declare const createMcpRouter: (server: McpServer$1, options?: McpRouterOptions) => Router<Application.DefaultState, Application.DefaultContext>;
1208
1421
  //#endregion
1209
- export { ApiCallOptions, CognitoUserPoolConfig, type CreateGatedToolRegistrarOptions, type GatedToolDef, type JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, type RegisterToolFromSchemaParams, type ToolCallContext, type ToolIdentity, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerToolFromSchema, z };
1422
+ export { ApiCallOptions, CognitoUserPoolConfig, type CreateGatedToolRegistrarOptions, type GatedToolDef, type JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, type RegisterAppResourceParams, type RegisterToolFromSchemaParams, type RegisteredAppResource, type ToolCallContext, type ToolIdentity, type ToolMetaParams, UI_EXTENSION_ID, UI_RESOURCE_MIME_TYPE, type UiResourceCsp, type UiResourceMeta, type UiResourcePermissions, type UiToolVisibility, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerAppResource, registerToolFromSchema, z };
package/dist/index.d.mts CHANGED
@@ -2,7 +2,7 @@
2
2
  /// <reference types="node" />
3
3
  import { Router } from "@ttoss/http-server";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
- import { CallToolResult, McpServer, McpServer as McpServer$1 } from "@modelcontextprotocol/server";
5
+ import { CallToolResult, McpServer, McpServer as McpServer$1, RegisteredResource } from "@modelcontextprotocol/server";
6
6
  import { z } from "zod";
7
7
  import accepts from "accepts";
8
8
  import { AsyncLocalStorage as AsyncLocalStorage$1 } from "async_hooks";
@@ -676,6 +676,197 @@ declare namespace Application {
676
676
  */
677
677
  declare const getIdentity: <T = unknown>() => T | undefined;
678
678
  //#endregion
679
+ //#region src/registerAppResource.d.ts
680
+ /**
681
+ * Identifier of the MCP Apps extension (`io.modelcontextprotocol/ui`), used as
682
+ * the key under `capabilities.extensions` on both sides of the handshake.
683
+ *
684
+ * A client declares Apps support with it; a server may advertise the same key
685
+ * so a host can discover the support without inspecting tool metadata:
686
+ *
687
+ * ```typescript
688
+ * new McpServer(
689
+ * { name: 'weather', version: '1.0.0' },
690
+ * {
691
+ * capabilities: {
692
+ * extensions: {
693
+ * [UI_EXTENSION_ID]: { mimeTypes: [UI_RESOURCE_MIME_TYPE] },
694
+ * },
695
+ * },
696
+ * }
697
+ * );
698
+ * ```
699
+ */
700
+ declare const UI_EXTENSION_ID = "io.modelcontextprotocol/ui";
701
+ /** MIME type every MCP Apps HTML resource is served as. */
702
+ declare const UI_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
703
+ /**
704
+ * Origins an app's view is allowed to reach, mapped by the host onto the
705
+ * iframe's Content Security Policy. Anything not declared is blocked: an
706
+ * omitted list is the restrictive default, not "no restriction".
707
+ */
708
+ interface UiResourceCsp {
709
+ /** Origins for network requests (`connect-src`). */
710
+ connectDomains?: string[];
711
+ /**
712
+ * Origins for scripts, styles, images, fonts and media (`script-src`,
713
+ * `style-src`, `img-src`, `font-src`, `media-src`).
714
+ */
715
+ resourceDomains?: string[];
716
+ /** Origins for nested iframes (`frame-src`). */
717
+ frameDomains?: string[];
718
+ /** Allowed document base URIs (`base-uri`). */
719
+ baseUriDomains?: string[];
720
+ }
721
+ /**
722
+ * Browser capabilities the view asks for. A host *may* grant them, so a view
723
+ * should feature-detect rather than assume: each key is a request, not a
724
+ * guarantee.
725
+ */
726
+ interface UiResourcePermissions {
727
+ camera?: Record<string, never>;
728
+ microphone?: Record<string, never>;
729
+ geolocation?: Record<string, never>;
730
+ clipboardWrite?: Record<string, never>;
731
+ }
732
+ /** Rendering and security configuration for an app's view (`_meta.ui`). */
733
+ interface UiResourceMeta {
734
+ /** Origins the view may reach; omitted means none. */
735
+ csp?: UiResourceCsp;
736
+ /** Browser capabilities requested for the view. */
737
+ permissions?: UiResourcePermissions;
738
+ /**
739
+ * Dedicated sandbox origin for the view, for cases needing a stable origin
740
+ * (OAuth callbacks, CORS, API key allowlists). The accepted format is
741
+ * host-specific; consult the host's documentation rather than deriving one.
742
+ */
743
+ domain?: string;
744
+ /** Whether the host should draw a border and background around the view. */
745
+ prefersBorder?: boolean;
746
+ }
747
+ /** Who may reach a tool. Defaults to both when left unset. */
748
+ type UiToolVisibility = 'model' | 'app';
749
+ /** Parameters for {@link registerAppResource}. */
750
+ interface RegisterAppResourceParams {
751
+ /** The `McpServer` instance to register the app resource on. */
752
+ server: McpServer$1;
753
+ /** Resource name, as listed by `resources/list`. */
754
+ name: string;
755
+ /** Resource URI. Must use the `ui://` scheme. */
756
+ uri: string;
757
+ /** What the view does and when a host should render it. */
758
+ description?: string;
759
+ /**
760
+ * The view's HTML5 document, or a builder invoked per `resources/read`.
761
+ * A builder receives the requested URI so one registration can serve
762
+ * variants of a document.
763
+ */
764
+ html: string | ((args: {
765
+ uri: URL;
766
+ }) => string | Promise<string>);
767
+ /** Rendering and security configuration relayed to the host as `_meta.ui`. */
768
+ ui?: UiResourceMeta;
769
+ }
770
+ /** Parameters for {@link RegisteredAppResource.toolMeta}. */
771
+ interface ToolMetaParams {
772
+ /**
773
+ * Who may reach the tool. Left unset, the spec's default applies — visible
774
+ * to the model *and* callable by the app.
775
+ */
776
+ visibility?: UiToolVisibility[];
777
+ /** Further `_meta` entries to merge in, e.g. vendor-namespaced keys. */
778
+ _meta?: Record<string, unknown>;
779
+ }
780
+ /** An app resource registered by {@link registerAppResource}. */
781
+ interface RegisteredAppResource {
782
+ /** The URI tools link to. */
783
+ uri: string;
784
+ /** The SDK's registration handle, for `update`/`disable`/`remove`. */
785
+ resource: RegisteredResource;
786
+ /**
787
+ * Builds the `_meta` bag that links a tool to this view. Pass it to
788
+ * `registerTool`, {@link registerToolFromSchema}, or a `GatedToolDef` —
789
+ * whichever registration path the tool uses.
790
+ */
791
+ toolMeta: (params?: ToolMetaParams) => Record<string, unknown>;
792
+ }
793
+ /**
794
+ * Registers an MCP Apps view: a `ui://` resource a tool result is rendered
795
+ * with, per the `io.modelcontextprotocol/ui` extension.
796
+ *
797
+ * The extension asks nothing of the transport — a view is an ordinary MCP
798
+ * resource, and the tool linkage is ordinary tool `_meta` — so this is
799
+ * registration sugar over `registerResource`, not new protocol surface. What
800
+ * it owns is the part that is easy to get wrong: the `ui://` scheme, the exact
801
+ * MIME type, `_meta.ui` on both the declaration and the read result, and
802
+ * writing the deprecated flat linkage key alongside the current one.
803
+ *
804
+ * **Register the linkage unconditionally.** The extension's negotiation is
805
+ * per-request and only reliably readable on the `2026-07-28` revision; in this
806
+ * package's default stateless 2025-era mode there is no remembered
807
+ * `initialize` to read a client capability from. Gating registration on that
808
+ * capability would therefore silently drop the view for every 2025-era client.
809
+ * A host without Apps support simply ignores `_meta`, so keep each tool's
810
+ * `content` meaningful on its own and let the text result be the fallback.
811
+ *
812
+ * @example
813
+ * ```typescript
814
+ * import {
815
+ * McpServer,
816
+ * registerAppResource,
817
+ * UI_EXTENSION_ID,
818
+ * UI_RESOURCE_MIME_TYPE,
819
+ * z,
820
+ * } from '@ttoss/http-server-mcp';
821
+ *
822
+ * const server = new McpServer(
823
+ * { name: 'weather', version: '1.0.0' },
824
+ * {
825
+ * capabilities: {
826
+ * extensions: { [UI_EXTENSION_ID]: { mimeTypes: [UI_RESOURCE_MIME_TYPE] } },
827
+ * },
828
+ * }
829
+ * );
830
+ *
831
+ * const dashboard = registerAppResource({
832
+ * server,
833
+ * name: 'weather_dashboard',
834
+ * uri: 'ui://weather/dashboard',
835
+ * description: 'Interactive weather dashboard',
836
+ * html: dashboardHtml,
837
+ * ui: {
838
+ * csp: { connectDomains: ['https://api.openweathermap.org'] },
839
+ * prefersBorder: true,
840
+ * },
841
+ * });
842
+ *
843
+ * server.registerTool(
844
+ * 'get-weather',
845
+ * {
846
+ * description: 'Get the weather for a location',
847
+ * inputSchema: { location: z.string() },
848
+ * _meta: dashboard.toolMeta(),
849
+ * },
850
+ * async ({ location }) => {
851
+ * const forecast = await fetchForecast(location);
852
+ * return {
853
+ * // Text stays meaningful: it is what a host without Apps renders.
854
+ * content: [{ type: 'text', text: summarise(forecast) }],
855
+ * structuredContent: forecast,
856
+ * };
857
+ * }
858
+ * );
859
+ * ```
860
+ */
861
+ declare const registerAppResource: ({
862
+ server,
863
+ name,
864
+ uri,
865
+ description,
866
+ html,
867
+ ui
868
+ }: RegisterAppResourceParams) => RegisteredAppResource;
869
+ //#endregion
679
870
  //#region src/registerToolFromSchema.d.ts
680
871
  /**
681
872
  * A plain JSON Schema object (draft-07 compatible) describing the shape of a
@@ -722,6 +913,19 @@ interface RegisterToolFromSchemaParams {
722
913
  * @default false
723
914
  */
724
915
  validateArguments?: boolean;
916
+ /**
917
+ * Tool metadata forwarded verbatim on `tools/list`.
918
+ *
919
+ * The MCP Apps extension links a tool to the view its result is rendered
920
+ * with through this field; build the bag with `toolMeta()` from
921
+ * `registerAppResource` rather than writing the keys by hand.
922
+ *
923
+ * @example
924
+ * ```typescript
925
+ * _meta: dashboard.toolMeta({ visibility: ['app'] }),
926
+ * ```
927
+ */
928
+ _meta?: Record<string, unknown>;
725
929
  /**
726
930
  * Tool handler invoked when the AI client calls the tool.
727
931
  * Receives the request arguments, validated against `inputSchema` only when
@@ -745,7 +949,7 @@ interface RegisterToolFromSchemaParams {
745
949
  *
746
950
  * @param server - The `McpServer` instance to register the tool on.
747
951
  * @param params - Tool configuration including name, description, inputSchema,
748
- * validateArguments, and handler.
952
+ * validateArguments, `_meta`, and handler.
749
953
  *
750
954
  * @example
751
955
  * ```typescript
@@ -802,7 +1006,12 @@ type GatedToolDef = {
802
1006
  * Each gate receives the full {@link ToolCallContext} (identity + args).
803
1007
  * Throw to reject the call; return (or resolve) to continue.
804
1008
  */
805
- gates?: Array<(ctx: ToolCallContext) => void | Promise<void>>; /** The tool handler. Receives merged call args + `buildContext` output. */
1009
+ gates?: Array<(ctx: ToolCallContext) => void | Promise<void>>;
1010
+ /**
1011
+ * Tool metadata forwarded verbatim on `tools/list` — how a gated tool links
1012
+ * to an MCP Apps view (`registerAppResource(...).toolMeta()`).
1013
+ */
1014
+ _meta?: Record<string, unknown>; /** The tool handler. Receives merged call args + `buildContext` output. */
806
1015
  method: (args: Record<string, unknown>) => Promise<unknown>;
807
1016
  };
808
1017
  /** Options for {@link createGatedToolRegistrar}. */
@@ -894,10 +1103,13 @@ interface McpAuthOptions {
894
1103
  /**
895
1104
  * JSON-RPC methods (read from `body.method`) that bypass verification.
896
1105
  *
897
- * `initialize` is public because the MCP authorization spec sanctions it: a
898
- * client completes the lifecycle handshake before it can discover the
899
- * authorization server. Nothing else is, so a server with `auth` configured
900
- * serves no tool metadata to an unauthenticated caller.
1106
+ * The lifecycle handshake is public because the MCP authorization spec
1107
+ * sanctions it: a client completes it before it can discover the
1108
+ * authorization server. That means one entry per protocol era —
1109
+ * `initialize` on 2025, `server/discover` on `2026-07-28`, which removed
1110
+ * `initialize` — so neither era is left unable to negotiate. Nothing else
1111
+ * is public, so a server with `auth` configured serves no tool metadata to
1112
+ * an unauthenticated caller.
901
1113
  *
902
1114
  * Opening `tools/list` is a deliberate choice, not a default. It serves the
903
1115
  * full tool catalogue — every name, description, and input schema — to
@@ -908,7 +1120,7 @@ interface McpAuthOptions {
908
1120
  * cannot call one. Set it only to serve callers that will never
909
1121
  * authenticate.
910
1122
  *
911
- * @default ['initialize']
1123
+ * @default ['initialize', 'server/discover']
912
1124
  *
913
1125
  * @example
914
1126
  * ```typescript
@@ -1136,9 +1348,10 @@ interface McpRouterOptions {
1136
1348
  * OAuth / JWT authentication configuration for the MCP endpoint.
1137
1349
  *
1138
1350
  * When set, incoming MCP requests must include a valid Bearer token in the
1139
- * `Authorization` header — except for `publicMethods` (by default just
1140
- * `initialize`), which bypasses verification so a client can complete the
1141
- * lifecycle handshake before authenticating. Invalid or missing tokens
1351
+ * `Authorization` header — except for `publicMethods` (by default the
1352
+ * lifecycle handshake of each protocol era: `initialize` and
1353
+ * `server/discover`), which bypass verification so a client can complete
1354
+ * the handshake before authenticating. Invalid or missing tokens
1142
1355
  * receive a `401` response with `WWW-Authenticate: Bearer` (or
1143
1356
  * `Bearer resource_metadata="..."` when `resourceMetadataUrl` is set, per
1144
1357
  * RFC 9728). Tokens that fail a `requiredScopes` check receive `403`.
@@ -1207,4 +1420,4 @@ interface McpRouterOptions {
1207
1420
  */
1208
1421
  declare const createMcpRouter: (server: McpServer$1, options?: McpRouterOptions) => Router<Application.DefaultState, Application.DefaultContext>;
1209
1422
  //#endregion
1210
- export { ApiCallOptions, CognitoUserPoolConfig, type CreateGatedToolRegistrarOptions, type GatedToolDef, type JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, type RegisterToolFromSchemaParams, type ToolCallContext, type ToolIdentity, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerToolFromSchema, z };
1423
+ export { ApiCallOptions, CognitoUserPoolConfig, type CreateGatedToolRegistrarOptions, type GatedToolDef, type JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, type RegisterAppResourceParams, type RegisterToolFromSchemaParams, type RegisteredAppResource, type ToolCallContext, type ToolIdentity, type ToolMetaParams, UI_EXTENSION_ID, UI_RESOURCE_MIME_TYPE, type UiResourceCsp, type UiResourceMeta, type UiResourcePermissions, type UiToolVisibility, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerAppResource, registerToolFromSchema, z };
package/dist/index.mjs CHANGED
@@ -117,6 +117,160 @@ var createMcpRequestServer = ({
117
117
  };
118
118
  };
119
119
 
120
+ //#endregion
121
+ //#region src/registerAppResource.ts
122
+ /**
123
+ * Identifier of the MCP Apps extension (`io.modelcontextprotocol/ui`), used as
124
+ * the key under `capabilities.extensions` on both sides of the handshake.
125
+ *
126
+ * A client declares Apps support with it; a server may advertise the same key
127
+ * so a host can discover the support without inspecting tool metadata:
128
+ *
129
+ * ```typescript
130
+ * new McpServer(
131
+ * { name: 'weather', version: '1.0.0' },
132
+ * {
133
+ * capabilities: {
134
+ * extensions: {
135
+ * [UI_EXTENSION_ID]: { mimeTypes: [UI_RESOURCE_MIME_TYPE] },
136
+ * },
137
+ * },
138
+ * }
139
+ * );
140
+ * ```
141
+ */
142
+ var UI_EXTENSION_ID = "io.modelcontextprotocol/ui";
143
+ /** MIME type every MCP Apps HTML resource is served as. */
144
+ var UI_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
145
+ /**
146
+ * Deprecated flat tool metadata key for the linked resource URI. Written
147
+ * alongside `_meta.ui.resourceUri` because both forms are legal until the
148
+ * extension reaches GA, at which point this key is removed from the spec.
149
+ */
150
+ var LEGACY_RESOURCE_URI_KEY = "ui/resourceUri";
151
+ /**
152
+ * Registers an MCP Apps view: a `ui://` resource a tool result is rendered
153
+ * with, per the `io.modelcontextprotocol/ui` extension.
154
+ *
155
+ * The extension asks nothing of the transport — a view is an ordinary MCP
156
+ * resource, and the tool linkage is ordinary tool `_meta` — so this is
157
+ * registration sugar over `registerResource`, not new protocol surface. What
158
+ * it owns is the part that is easy to get wrong: the `ui://` scheme, the exact
159
+ * MIME type, `_meta.ui` on both the declaration and the read result, and
160
+ * writing the deprecated flat linkage key alongside the current one.
161
+ *
162
+ * **Register the linkage unconditionally.** The extension's negotiation is
163
+ * per-request and only reliably readable on the `2026-07-28` revision; in this
164
+ * package's default stateless 2025-era mode there is no remembered
165
+ * `initialize` to read a client capability from. Gating registration on that
166
+ * capability would therefore silently drop the view for every 2025-era client.
167
+ * A host without Apps support simply ignores `_meta`, so keep each tool's
168
+ * `content` meaningful on its own and let the text result be the fallback.
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * import {
173
+ * McpServer,
174
+ * registerAppResource,
175
+ * UI_EXTENSION_ID,
176
+ * UI_RESOURCE_MIME_TYPE,
177
+ * z,
178
+ * } from '@ttoss/http-server-mcp';
179
+ *
180
+ * const server = new McpServer(
181
+ * { name: 'weather', version: '1.0.0' },
182
+ * {
183
+ * capabilities: {
184
+ * extensions: { [UI_EXTENSION_ID]: { mimeTypes: [UI_RESOURCE_MIME_TYPE] } },
185
+ * },
186
+ * }
187
+ * );
188
+ *
189
+ * const dashboard = registerAppResource({
190
+ * server,
191
+ * name: 'weather_dashboard',
192
+ * uri: 'ui://weather/dashboard',
193
+ * description: 'Interactive weather dashboard',
194
+ * html: dashboardHtml,
195
+ * ui: {
196
+ * csp: { connectDomains: ['https://api.openweathermap.org'] },
197
+ * prefersBorder: true,
198
+ * },
199
+ * });
200
+ *
201
+ * server.registerTool(
202
+ * 'get-weather',
203
+ * {
204
+ * description: 'Get the weather for a location',
205
+ * inputSchema: { location: z.string() },
206
+ * _meta: dashboard.toolMeta(),
207
+ * },
208
+ * async ({ location }) => {
209
+ * const forecast = await fetchForecast(location);
210
+ * return {
211
+ * // Text stays meaningful: it is what a host without Apps renders.
212
+ * content: [{ type: 'text', text: summarise(forecast) }],
213
+ * structuredContent: forecast,
214
+ * };
215
+ * }
216
+ * );
217
+ * ```
218
+ */
219
+ var registerAppResource = ({
220
+ server,
221
+ name,
222
+ uri,
223
+ description,
224
+ html,
225
+ ui
226
+ }) => {
227
+ if (!uri.startsWith("ui://")) throw new Error(`An MCP Apps resource URI must use the ui:// scheme (received "${uri}").`);
228
+ const meta = ui === void 0 ? void 0 : {
229
+ ui
230
+ };
231
+ const resource = server.registerResource(name, uri, {
232
+ description,
233
+ mimeType: UI_RESOURCE_MIME_TYPE,
234
+ ...(meta === void 0 ? {} : {
235
+ _meta: meta
236
+ })
237
+ }, async requestedUri => {
238
+ const text = typeof html === "string" ? html : await html({
239
+ uri: requestedUri
240
+ });
241
+ return {
242
+ contents: [{
243
+ uri: requestedUri.href,
244
+ mimeType: UI_RESOURCE_MIME_TYPE,
245
+ text,
246
+ ...(meta === void 0 ? {} : {
247
+ _meta: meta
248
+ })
249
+ }]
250
+ };
251
+ });
252
+ const toolMeta = ({
253
+ visibility,
254
+ _meta
255
+ } = {}) => {
256
+ return {
257
+ ..._meta,
258
+ ui: {
259
+ resourceUri: uri,
260
+ ...(visibility === void 0 ? {} : {
261
+ visibility
262
+ })
263
+ },
264
+ [LEGACY_RESOURCE_URI_KEY]: uri
265
+ };
266
+ };
267
+ return {
268
+ uri,
269
+ resource,
270
+ toolMeta
271
+ };
272
+ };
273
+
120
274
  //#endregion
121
275
  //#region src/registerToolFromSchema.ts
122
276
  /**
@@ -160,7 +314,7 @@ var toStandardSchema = ({
160
314
  *
161
315
  * @param server - The `McpServer` instance to register the tool on.
162
316
  * @param params - Tool configuration including name, description, inputSchema,
163
- * validateArguments, and handler.
317
+ * validateArguments, `_meta`, and handler.
164
318
  *
165
319
  * @example
166
320
  * ```typescript
@@ -191,14 +345,16 @@ var registerToolFromSchema = (server, params) => {
191
345
  properties: {}
192
346
  },
193
347
  validateArguments = false,
194
- handler
348
+ handler,
349
+ _meta
195
350
  } = params;
196
351
  server.registerTool(name, {
197
352
  description,
198
353
  inputSchema: toStandardSchema({
199
354
  inputSchema,
200
355
  validateArguments
201
- })
356
+ }),
357
+ _meta
202
358
  }, async args => {
203
359
  return handler(args);
204
360
  });
@@ -313,7 +469,8 @@ var createGatedToolRegistrar = ({
313
469
  };
314
470
  server.registerTool(def.name, {
315
471
  description: def.description,
316
- inputSchema: def.inputSchema
472
+ inputSchema: def.inputSchema,
473
+ _meta: def._meta
317
474
  }, handler);
318
475
  };
319
476
  return {
@@ -324,13 +481,23 @@ var createGatedToolRegistrar = ({
324
481
  //#endregion
325
482
  //#region src/index.ts
326
483
  /**
327
- * MCP lifecycle methods reachable before a client authenticates.
484
+ * MCP lifecycle handshakes reachable before a client authenticates.
485
+ *
486
+ * One entry per protocol era, because the exemption the MCP authorization spec
487
+ * sanctions is about *the handshake* — a client has to complete it before it
488
+ * can discover the authorization server — not about a method name:
489
+ *
490
+ * - `initialize` is the 2025-era handshake.
491
+ * - `server/discover` is its `2026-07-28` replacement. That revision removed
492
+ * `initialize` entirely, so naming only `initialize` left the modern era
493
+ * with no public method at all — the asymmetry ttoss/ttoss#1222 fixed.
494
+ *
495
+ * A future revision that renames the handshake again needs a matching entry
496
+ * here; nothing detects that automatically.
328
497
  *
329
- * Only `initialize`, which the MCP authorization spec sanctions: a client has
330
- * to complete the handshake before it can discover the authorization server.
331
- * `tools/list` is not here — see {@link McpAuthOptions.publicMethods}.
498
+ * `tools/list` is deliberately absent — see {@link McpAuthOptions.publicMethods}.
332
499
  */
333
- var DEFAULT_PUBLIC_METHODS = ["initialize"];
500
+ var DEFAULT_PUBLIC_METHODS = ["initialize", "server/discover"];
334
501
  /**
335
502
  * Asserts that a verified token's `aud` claim includes at least one of the
336
503
  * expected resource indicator values. `aud` may be a single string or an
@@ -623,4 +790,4 @@ var createMcpRouter = (server, options = {}) => {
623
790
  };
624
791
 
625
792
  //#endregion
626
- export { McpServer, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerToolFromSchema, z };
793
+ export { McpServer, UI_EXTENSION_ID, UI_RESOURCE_MIME_TYPE, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerAppResource, registerToolFromSchema, z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/http-server-mcp",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
5
5
  "keywords": [
6
6
  "ai",