@casys/mcp-server 0.13.0 → 0.14.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 +52 -0
- package/mod.ts +10 -1
- package/package.json +2 -2
- package/src/mcp-app.ts +43 -1
- package/src/types.ts +111 -0
package/README.md
CHANGED
|
@@ -331,6 +331,58 @@ server.registerResource(
|
|
|
331
331
|
);
|
|
332
332
|
```
|
|
333
333
|
|
|
334
|
+
#### Capability negotiation (clients that don't support MCP Apps)
|
|
335
|
+
|
|
336
|
+
Not every MCP client renders UI resources. Clients that do advertise the
|
|
337
|
+
[MCP Apps extension](https://github.com/modelcontextprotocol/ext-apps) in their
|
|
338
|
+
capabilities (per the SDK 1.29 `extensions` field). Read it from a tool handler
|
|
339
|
+
to decide between rich UI and a text-only fallback:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import { MCP_APP_MIME_TYPE, McpApp } from "@casys/mcp-server";
|
|
343
|
+
|
|
344
|
+
const app = new McpApp({ name: "weather-server", version: "1.0.0" });
|
|
345
|
+
|
|
346
|
+
app.registerTool(
|
|
347
|
+
{
|
|
348
|
+
name: "get-weather",
|
|
349
|
+
description: "Get the weather forecast for a city",
|
|
350
|
+
inputSchema: {
|
|
351
|
+
type: "object",
|
|
352
|
+
properties: { city: { type: "string" } },
|
|
353
|
+
required: ["city"],
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
async ({ city }) => {
|
|
357
|
+
const forecast = await fetchForecast(city);
|
|
358
|
+
const cap = app.getClientMcpAppsCapability();
|
|
359
|
+
|
|
360
|
+
if (cap?.mimeTypes?.includes(MCP_APP_MIME_TYPE)) {
|
|
361
|
+
// Rich UI: small text summary + interactive resource
|
|
362
|
+
return {
|
|
363
|
+
content: [{ type: "text", text: `Forecast for ${city} loaded` }],
|
|
364
|
+
_meta: { ui: { resourceUri: `ui://weather/${city}` } },
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Text-only fallback for clients that can't render the UI
|
|
369
|
+
return {
|
|
370
|
+
content: [{ type: "text", text: formatForecastAsText(forecast) }],
|
|
371
|
+
};
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
`getClientMcpAppsCapability()` returns `undefined` before the client has
|
|
377
|
+
completed its initialize handshake, when the client doesn't advertise MCP Apps
|
|
378
|
+
support, or when the advertised capability is malformed. The standalone
|
|
379
|
+
`getMcpAppsCapability(clientCapabilities)` function is also exported for use
|
|
380
|
+
against arbitrary capability objects.
|
|
381
|
+
|
|
382
|
+
The constants `MCP_APPS_EXTENSION_ID` (`"io.modelcontextprotocol/ui"`) and
|
|
383
|
+
`MCP_APPS_PROTOCOL_VERSION` (`"2026-01-26"`) are exported for agents that need
|
|
384
|
+
to introspect the protocol target directly.
|
|
385
|
+
|
|
334
386
|
---
|
|
335
387
|
|
|
336
388
|
## API Reference
|
package/mod.ts
CHANGED
|
@@ -102,7 +102,16 @@ export type {
|
|
|
102
102
|
export type { McpAppOptions as ConcurrentServerOptions } from "./src/types.js";
|
|
103
103
|
|
|
104
104
|
// MCP Apps constants & viewer utilities
|
|
105
|
-
export {
|
|
105
|
+
export {
|
|
106
|
+
MCP_APP_MIME_TYPE,
|
|
107
|
+
MCP_APPS_EXTENSION_ID,
|
|
108
|
+
MCP_APPS_PROTOCOL_VERSION,
|
|
109
|
+
} from "./src/types.js";
|
|
110
|
+
|
|
111
|
+
// MCP Apps capability negotiation (per ext-apps spec 2026-01-26 + SDK 1.29 extensions)
|
|
112
|
+
export { getMcpAppsCapability } from "./src/types.js";
|
|
113
|
+
export type { McpAppsClientCapability } from "./src/types.js";
|
|
114
|
+
|
|
106
115
|
export type {
|
|
107
116
|
RegisterViewersConfig,
|
|
108
117
|
RegisterViewersSummary,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@casys/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Production-ready MCP server framework with concurrency control, auth, and observability",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "mod.ts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"test": "tsx --test src/**/*_test.ts"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
13
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
14
14
|
"hono": "^4.0.0",
|
|
15
15
|
"ajv": "^8.17.1",
|
|
16
16
|
"jose": "^6.0.0",
|
package/src/mcp-app.ts
CHANGED
|
@@ -51,6 +51,7 @@ import type {
|
|
|
51
51
|
HttpRateLimitContext,
|
|
52
52
|
HttpServerOptions,
|
|
53
53
|
McpAppOptions,
|
|
54
|
+
McpAppsClientCapability,
|
|
54
55
|
MCPResource,
|
|
55
56
|
MCPTool,
|
|
56
57
|
QueueMetrics,
|
|
@@ -59,7 +60,11 @@ import type {
|
|
|
59
60
|
StructuredToolResult,
|
|
60
61
|
ToolHandler,
|
|
61
62
|
} from "./types.js";
|
|
62
|
-
import {
|
|
63
|
+
import {
|
|
64
|
+
getMcpAppsCapability,
|
|
65
|
+
MCP_APP_MIME_TYPE,
|
|
66
|
+
MCP_APP_URI_SCHEME,
|
|
67
|
+
} from "./types.js";
|
|
63
68
|
import { discoverViewers, resolveViewerDistPath } from "./ui/viewer-utils.js";
|
|
64
69
|
import type { DirEntry, DiscoverViewersFS } from "./ui/viewer-utils.js";
|
|
65
70
|
import { buildCspHeader, injectCspMetaTag } from "./security/csp.js";
|
|
@@ -1908,6 +1913,43 @@ export class McpApp {
|
|
|
1908
1913
|
return this.samplingBridge;
|
|
1909
1914
|
}
|
|
1910
1915
|
|
|
1916
|
+
/**
|
|
1917
|
+
* Read the MCP Apps capability advertised by the connected client.
|
|
1918
|
+
*
|
|
1919
|
+
* Returns the capability object (possibly empty `{}`) when the client
|
|
1920
|
+
* advertised support for MCP Apps via its `extensions` capability,
|
|
1921
|
+
* or `undefined` when:
|
|
1922
|
+
* - the client did not send capabilities yet (called before initialize)
|
|
1923
|
+
* - the client did not advertise the MCP Apps extension at all
|
|
1924
|
+
* - the client sent a malformed extension value
|
|
1925
|
+
*
|
|
1926
|
+
* Use this from a tool handler to decide whether to return a UI
|
|
1927
|
+
* resource (`_meta.ui`) or a text-only fallback. Hosts that don't
|
|
1928
|
+
* support MCP Apps will silently drop the `_meta.ui` field, but
|
|
1929
|
+
* checking explicitly lets you serve a richer text response when
|
|
1930
|
+
* the UI path isn't available.
|
|
1931
|
+
*
|
|
1932
|
+
* @returns MCP Apps capability or `undefined` if not supported.
|
|
1933
|
+
*
|
|
1934
|
+
* @example
|
|
1935
|
+
* ```typescript
|
|
1936
|
+
* const cap = app.getClientMcpAppsCapability();
|
|
1937
|
+
* if (cap?.mimeTypes?.includes(MCP_APP_MIME_TYPE)) {
|
|
1938
|
+
* return {
|
|
1939
|
+
* content: [{ type: "text", text: summary }],
|
|
1940
|
+
* _meta: { ui: { resourceUri: "ui://my-app/dashboard" } },
|
|
1941
|
+
* };
|
|
1942
|
+
* }
|
|
1943
|
+
* return { content: [{ type: "text", text: detailedTextFallback }] };
|
|
1944
|
+
* ```
|
|
1945
|
+
*
|
|
1946
|
+
* @see {@link getMcpAppsCapability} for the standalone reader
|
|
1947
|
+
* @see {@link MCP_APPS_EXTENSION_ID} for the extension key
|
|
1948
|
+
*/
|
|
1949
|
+
getClientMcpAppsCapability(): McpAppsClientCapability | undefined {
|
|
1950
|
+
return getMcpAppsCapability(this.mcpServer.server.getClientCapabilities());
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1911
1953
|
/**
|
|
1912
1954
|
* Get queue metrics for monitoring
|
|
1913
1955
|
*/
|
package/src/types.ts
CHANGED
|
@@ -251,6 +251,117 @@ export const MCP_APP_MIME_TYPE = "text/html;profile=mcp-app" as const;
|
|
|
251
251
|
/** URI scheme for MCP Apps resources */
|
|
252
252
|
export const MCP_APP_URI_SCHEME = "ui:" as const;
|
|
253
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Well-known extension identifier for the MCP Apps protocol.
|
|
256
|
+
*
|
|
257
|
+
* Clients advertise MCP Apps support by including this key in
|
|
258
|
+
* `clientCapabilities.extensions` (per the MCP SDK 1.29 extensions
|
|
259
|
+
* feature). Servers read it via {@link getMcpAppsCapability} to decide
|
|
260
|
+
* whether to register UI-rendering tools or fall back to text-only.
|
|
261
|
+
*
|
|
262
|
+
* @see {@link https://github.com/modelcontextprotocol/ext-apps | MCP Apps spec}
|
|
263
|
+
*/
|
|
264
|
+
export const MCP_APPS_EXTENSION_ID = "io.modelcontextprotocol/ui" as const;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* MCP Apps protocol spec version this package targets.
|
|
268
|
+
*
|
|
269
|
+
* Matches the dated spec at
|
|
270
|
+
* https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
|
|
271
|
+
*
|
|
272
|
+
* Bump this constant in the same commit that adopts a newer dated spec.
|
|
273
|
+
*/
|
|
274
|
+
export const MCP_APPS_PROTOCOL_VERSION = "2026-01-26" as const;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* MCP Apps capability advertised by a client.
|
|
278
|
+
*
|
|
279
|
+
* Returned by {@link getMcpAppsCapability} after reading
|
|
280
|
+
* `clientCapabilities.extensions[MCP_APPS_EXTENSION_ID]`.
|
|
281
|
+
*
|
|
282
|
+
* The capability object is intentionally minimal — the spec keeps it
|
|
283
|
+
* extensible by adding fields rather than removing them, so consumers
|
|
284
|
+
* MUST tolerate unknown fields.
|
|
285
|
+
*/
|
|
286
|
+
export interface McpAppsClientCapability {
|
|
287
|
+
/**
|
|
288
|
+
* MIME types the client can render as MCP Apps.
|
|
289
|
+
*
|
|
290
|
+
* Typically includes `"text/html;profile=mcp-app"`. An empty or
|
|
291
|
+
* absent array means the client advertised support but listed no
|
|
292
|
+
* concrete mime types — defensively assume nothing.
|
|
293
|
+
*/
|
|
294
|
+
mimeTypes?: string[];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Read the MCP Apps capability from a client's advertised capabilities.
|
|
299
|
+
*
|
|
300
|
+
* Best-effort, defensive reader. Returns `undefined` for any of:
|
|
301
|
+
* - `null` / `undefined` input
|
|
302
|
+
* - `clientCapabilities` without an `extensions` field
|
|
303
|
+
* - `extensions` without the {@link MCP_APPS_EXTENSION_ID} key
|
|
304
|
+
* - extension value that is not a plain object (string, number, null, ...)
|
|
305
|
+
*
|
|
306
|
+
* Malformed `mimeTypes` (wrong type, non-string entries) are silently
|
|
307
|
+
* filtered rather than thrown — agents reading this function need a
|
|
308
|
+
* predictable contract that never crashes downstream consumers on
|
|
309
|
+
* untrusted client data.
|
|
310
|
+
*
|
|
311
|
+
* **Validation scope:** this function only validates the *type* of
|
|
312
|
+
* `mimeTypes` entries (must be string). It does NOT validate that the
|
|
313
|
+
* strings look like valid mime types (e.g. empty strings or garbage
|
|
314
|
+
* content pass through). Consumers should compare against known
|
|
315
|
+
* constants like {@link MCP_APP_MIME_TYPE} via `.includes()` rather
|
|
316
|
+
* than treating the array as a generic allowlist.
|
|
317
|
+
*
|
|
318
|
+
* @param clientCapabilities - The `ClientCapabilities` object from the
|
|
319
|
+
* MCP SDK initialize handshake. May be `null` or `undefined` if the
|
|
320
|
+
* client never sent capabilities.
|
|
321
|
+
* @returns The MCP Apps capability if the client advertised support,
|
|
322
|
+
* otherwise `undefined`.
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* ```typescript
|
|
326
|
+
* const cap = getMcpAppsCapability(client.getClientCapabilities());
|
|
327
|
+
* if (cap?.mimeTypes?.includes(MCP_APP_MIME_TYPE)) {
|
|
328
|
+
* // register UI-rendering tools
|
|
329
|
+
* } else {
|
|
330
|
+
* // register text-only fallback tools
|
|
331
|
+
* }
|
|
332
|
+
* ```
|
|
333
|
+
*/
|
|
334
|
+
export function getMcpAppsCapability(
|
|
335
|
+
clientCapabilities:
|
|
336
|
+
| (Record<string, unknown> & { extensions?: Record<string, unknown> })
|
|
337
|
+
| null
|
|
338
|
+
| undefined,
|
|
339
|
+
): McpAppsClientCapability | undefined {
|
|
340
|
+
if (clientCapabilities === null || clientCapabilities === undefined) {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
const extensions = clientCapabilities.extensions;
|
|
344
|
+
if (extensions === null || typeof extensions !== "object") {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
const raw = extensions[MCP_APPS_EXTENSION_ID];
|
|
348
|
+
if (raw === null || typeof raw !== "object") {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
// We have a capability object — extract known fields defensively.
|
|
352
|
+
const result: McpAppsClientCapability = {};
|
|
353
|
+
const rawMimeTypes = (raw as Record<string, unknown>).mimeTypes;
|
|
354
|
+
if (Array.isArray(rawMimeTypes)) {
|
|
355
|
+
const validMimeTypes = rawMimeTypes.filter(
|
|
356
|
+
(m): m is string => typeof m === "string",
|
|
357
|
+
);
|
|
358
|
+
if (validMimeTypes.length > 0) {
|
|
359
|
+
result.mimeTypes = validMimeTypes;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
|
|
254
365
|
// ============================================
|
|
255
366
|
// MCP Tool Types
|
|
256
367
|
// ============================================
|