@casys/mcp-server 0.12.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 +89 -23
- package/mod.ts +55 -18
- package/package.json +2 -2
- package/src/auth/mod.ts +9 -1
- package/src/auth/multi-tenant-middleware.ts +236 -0
- package/src/auth/types.ts +12 -1
- package/src/inspector/launcher.ts +6 -2
- package/src/{concurrent-server.ts → mcp-app.ts} +239 -68
- package/src/middleware/mod.ts +1 -1
- package/src/middleware/rate-limit.ts +1 -1
- package/src/middleware/types.ts +1 -1
- package/src/observability/metrics.ts +1 -1
- package/src/types.ts +142 -3
- package/src/ui/viewer-utils.ts +7 -1
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* McpApp — Hono-style framework for MCP servers
|
|
3
3
|
*
|
|
4
4
|
* High-performance MCP server with built-in concurrency control,
|
|
5
5
|
* backpressure, and optional sampling support.
|
|
6
6
|
*
|
|
7
7
|
* Wraps the official @modelcontextprotocol/sdk with production-ready
|
|
8
|
-
*
|
|
8
|
+
* middleware, auth, and observability features.
|
|
9
9
|
*
|
|
10
|
-
* @module lib/server/
|
|
10
|
+
* @module lib/server/mcp-app
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
14
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
15
|
import {
|
|
16
|
+
type CallToolRequest,
|
|
16
17
|
CallToolRequestSchema,
|
|
17
|
-
ListToolsRequestSchema,
|
|
18
18
|
ListResourcesRequestSchema,
|
|
19
|
+
ListToolsRequestSchema,
|
|
20
|
+
type ReadResourceRequest,
|
|
19
21
|
ReadResourceRequestSchema,
|
|
20
22
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
21
23
|
import { Hono } from "hono";
|
|
@@ -45,9 +47,11 @@ import { createScopeMiddleware } from "./auth/scope-middleware.js";
|
|
|
45
47
|
import { createAuthProviderFromConfig, loadAuthConfig } from "./auth/config.js";
|
|
46
48
|
import type { AuthProvider } from "./auth/provider.js";
|
|
47
49
|
import type {
|
|
48
|
-
|
|
50
|
+
FetchHandler,
|
|
49
51
|
HttpRateLimitContext,
|
|
50
52
|
HttpServerOptions,
|
|
53
|
+
McpAppOptions,
|
|
54
|
+
McpAppsClientCapability,
|
|
51
55
|
MCPResource,
|
|
52
56
|
MCPTool,
|
|
53
57
|
QueueMetrics,
|
|
@@ -56,8 +60,12 @@ import type {
|
|
|
56
60
|
StructuredToolResult,
|
|
57
61
|
ToolHandler,
|
|
58
62
|
} from "./types.js";
|
|
59
|
-
import {
|
|
60
|
-
|
|
63
|
+
import {
|
|
64
|
+
getMcpAppsCapability,
|
|
65
|
+
MCP_APP_MIME_TYPE,
|
|
66
|
+
MCP_APP_URI_SCHEME,
|
|
67
|
+
} from "./types.js";
|
|
68
|
+
import { discoverViewers, resolveViewerDistPath } from "./ui/viewer-utils.js";
|
|
61
69
|
import type { DirEntry, DiscoverViewersFS } from "./ui/viewer-utils.js";
|
|
62
70
|
import { buildCspHeader, injectCspMetaTag } from "./security/csp.js";
|
|
63
71
|
import { ServerMetrics } from "./observability/metrics.js";
|
|
@@ -157,7 +165,7 @@ async function readBodyWithLimit(
|
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
/**
|
|
160
|
-
*
|
|
168
|
+
* McpApp provides a high-performance MCP server
|
|
161
169
|
*
|
|
162
170
|
* Features:
|
|
163
171
|
* - Wraps official @modelcontextprotocol/sdk
|
|
@@ -169,7 +177,7 @@ async function readBodyWithLimit(
|
|
|
169
177
|
*
|
|
170
178
|
* @example
|
|
171
179
|
* ```typescript
|
|
172
|
-
* const server = new
|
|
180
|
+
* const server = new McpApp({
|
|
173
181
|
* name: "my-server",
|
|
174
182
|
* version: "1.0.0",
|
|
175
183
|
* maxConcurrent: 5,
|
|
@@ -180,7 +188,7 @@ async function readBodyWithLimit(
|
|
|
180
188
|
* await server.start();
|
|
181
189
|
* ```
|
|
182
190
|
*/
|
|
183
|
-
export class
|
|
191
|
+
export class McpApp {
|
|
184
192
|
private mcpServer: McpServer;
|
|
185
193
|
private requestQueue: RequestQueue;
|
|
186
194
|
private rateLimiter: RateLimiter | null = null;
|
|
@@ -188,7 +196,7 @@ export class ConcurrentMCPServer {
|
|
|
188
196
|
private samplingBridge: SamplingBridge | null = null;
|
|
189
197
|
private tools = new Map<string, ToolWithHandler>();
|
|
190
198
|
private resources = new Map<string, RegisteredResourceInfo>();
|
|
191
|
-
private options:
|
|
199
|
+
private options: McpAppOptions;
|
|
192
200
|
private started = false;
|
|
193
201
|
private resourceHandlersInstalled = false;
|
|
194
202
|
|
|
@@ -222,7 +230,7 @@ export class ConcurrentMCPServer {
|
|
|
222
230
|
windowMs: 60_000,
|
|
223
231
|
});
|
|
224
232
|
|
|
225
|
-
constructor(options:
|
|
233
|
+
constructor(options: McpAppOptions) {
|
|
226
234
|
this.options = options;
|
|
227
235
|
|
|
228
236
|
// Create SDK MCP server
|
|
@@ -300,26 +308,29 @@ export class ConcurrentMCPServer {
|
|
|
300
308
|
});
|
|
301
309
|
|
|
302
310
|
// resources/read — serve resource content by URI
|
|
303
|
-
server.setRequestHandler(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
311
|
+
server.setRequestHandler(
|
|
312
|
+
ReadResourceRequestSchema,
|
|
313
|
+
async (request: ReadResourceRequest) => {
|
|
314
|
+
const uri = request.params.uri;
|
|
315
|
+
const info = this.resources.get(uri);
|
|
316
|
+
if (!info) {
|
|
317
|
+
throw new Error(`Resource not found: ${uri}`);
|
|
318
|
+
}
|
|
309
319
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
320
|
+
try {
|
|
321
|
+
const content = await info.handler(new URL(uri));
|
|
322
|
+
const finalContent = this.applyResourceCsp(content);
|
|
323
|
+
return { contents: [finalContent] };
|
|
324
|
+
} catch (error) {
|
|
325
|
+
this.log(
|
|
326
|
+
`[ERROR] Resource handler failed for ${uri}: ${
|
|
327
|
+
error instanceof Error ? error.message : String(error)
|
|
328
|
+
}`,
|
|
329
|
+
);
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
);
|
|
323
334
|
|
|
324
335
|
this.resourceHandlersInstalled = true;
|
|
325
336
|
this.log("Resources capability pre-declared (expectResources: true)");
|
|
@@ -342,20 +353,24 @@ export class ConcurrentMCPServer {
|
|
|
342
353
|
});
|
|
343
354
|
|
|
344
355
|
// tools/call handler (delegates to middleware pipeline)
|
|
345
|
-
server.setRequestHandler(
|
|
346
|
-
|
|
347
|
-
|
|
356
|
+
server.setRequestHandler(
|
|
357
|
+
CallToolRequestSchema,
|
|
358
|
+
async (request: CallToolRequest) => {
|
|
359
|
+
const toolName = request.params.name;
|
|
360
|
+
const args = request.params.arguments || {};
|
|
348
361
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
362
|
+
let result: unknown;
|
|
363
|
+
try {
|
|
364
|
+
result = await this.executeToolCall(toolName, args);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
return this.handleToolError(error, toolName);
|
|
367
|
+
}
|
|
355
368
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
369
|
+
// Serialization errors are framework bugs, not tool errors —
|
|
370
|
+
// let them propagate
|
|
371
|
+
return this.buildToolCallResult(toolName, result);
|
|
372
|
+
},
|
|
373
|
+
);
|
|
359
374
|
}
|
|
360
375
|
|
|
361
376
|
/**
|
|
@@ -370,7 +385,7 @@ export class ConcurrentMCPServer {
|
|
|
370
385
|
): void {
|
|
371
386
|
if (this.started) {
|
|
372
387
|
throw new Error(
|
|
373
|
-
"[
|
|
388
|
+
"[McpApp] Cannot register tools after server started. " +
|
|
374
389
|
"Call registerTools() before start() or startHttp().",
|
|
375
390
|
);
|
|
376
391
|
}
|
|
@@ -403,7 +418,7 @@ export class ConcurrentMCPServer {
|
|
|
403
418
|
registerTool(tool: MCPTool, handler: ToolHandler): void {
|
|
404
419
|
if (this.started) {
|
|
405
420
|
throw new Error(
|
|
406
|
-
"[
|
|
421
|
+
"[McpApp] Cannot register tools after server started. " +
|
|
407
422
|
"Call registerTool() before start() or startHttp().",
|
|
408
423
|
);
|
|
409
424
|
}
|
|
@@ -481,7 +496,9 @@ export class ConcurrentMCPServer {
|
|
|
481
496
|
unregisterTool(toolName: string): boolean {
|
|
482
497
|
const deleted = this.tools.delete(toolName);
|
|
483
498
|
if (deleted) {
|
|
484
|
-
this.log(
|
|
499
|
+
this.log(
|
|
500
|
+
`Unregistered tool: ${toolName} (remaining: ${this.tools.size})`,
|
|
501
|
+
);
|
|
485
502
|
}
|
|
486
503
|
return deleted;
|
|
487
504
|
}
|
|
@@ -513,7 +530,7 @@ export class ConcurrentMCPServer {
|
|
|
513
530
|
use(middleware: Middleware): this {
|
|
514
531
|
if (this.started) {
|
|
515
532
|
throw new Error(
|
|
516
|
-
"[
|
|
533
|
+
"[McpApp] Cannot add middleware after server started. " +
|
|
517
534
|
"Call use() before start() or startHttp().",
|
|
518
535
|
);
|
|
519
536
|
}
|
|
@@ -593,7 +610,7 @@ export class ConcurrentMCPServer {
|
|
|
593
610
|
): Promise<MiddlewareResult> {
|
|
594
611
|
if (!this.middlewareRunner) {
|
|
595
612
|
throw new Error(
|
|
596
|
-
"[
|
|
613
|
+
"[McpApp] Pipeline not built. Call start() or startHttp() first.",
|
|
597
614
|
);
|
|
598
615
|
}
|
|
599
616
|
|
|
@@ -698,7 +715,7 @@ export class ConcurrentMCPServer {
|
|
|
698
715
|
// Check for duplicate
|
|
699
716
|
if (this.resources.has(resource.uri)) {
|
|
700
717
|
throw new Error(
|
|
701
|
-
`[
|
|
718
|
+
`[McpApp] Resource already registered: ${resource.uri}`,
|
|
702
719
|
);
|
|
703
720
|
}
|
|
704
721
|
|
|
@@ -760,7 +777,7 @@ export class ConcurrentMCPServer {
|
|
|
760
777
|
|
|
761
778
|
if (missingHandlers.length > 0) {
|
|
762
779
|
throw new Error(
|
|
763
|
-
`[
|
|
780
|
+
`[McpApp] Missing handlers for resources:\n` +
|
|
764
781
|
missingHandlers.map((uri) => ` - ${uri}`).join("\n"),
|
|
765
782
|
);
|
|
766
783
|
}
|
|
@@ -775,7 +792,7 @@ export class ConcurrentMCPServer {
|
|
|
775
792
|
|
|
776
793
|
if (duplicateUris.length > 0) {
|
|
777
794
|
throw new Error(
|
|
778
|
-
`[
|
|
795
|
+
`[McpApp] Resources already registered:\n` +
|
|
779
796
|
duplicateUris.map((uri) => ` - ${uri}`).join("\n"),
|
|
780
797
|
);
|
|
781
798
|
}
|
|
@@ -786,7 +803,7 @@ export class ConcurrentMCPServer {
|
|
|
786
803
|
if (!handler) {
|
|
787
804
|
// Should never happen after validation, but defensive check
|
|
788
805
|
throw new Error(
|
|
789
|
-
`[
|
|
806
|
+
`[McpApp] Handler disappeared for ${resource.uri}`,
|
|
790
807
|
);
|
|
791
808
|
}
|
|
792
809
|
this.registerResource(resource, handler);
|
|
@@ -808,7 +825,9 @@ export class ConcurrentMCPServer {
|
|
|
808
825
|
*/
|
|
809
826
|
registerViewers(config: RegisterViewersConfig): RegisterViewersSummary {
|
|
810
827
|
if (!config.prefix) {
|
|
811
|
-
throw new Error(
|
|
828
|
+
throw new Error(
|
|
829
|
+
"[McpApp] registerViewers: prefix is required",
|
|
830
|
+
);
|
|
812
831
|
}
|
|
813
832
|
|
|
814
833
|
// Resolve viewer list: explicit or auto-discovered
|
|
@@ -826,7 +845,11 @@ export class ConcurrentMCPServer {
|
|
|
826
845
|
const skipped: string[] = [];
|
|
827
846
|
|
|
828
847
|
for (const viewerName of viewerNames) {
|
|
829
|
-
const distPath = resolveViewerDistPath(
|
|
848
|
+
const distPath = resolveViewerDistPath(
|
|
849
|
+
config.moduleUrl,
|
|
850
|
+
viewerName,
|
|
851
|
+
config.exists,
|
|
852
|
+
);
|
|
830
853
|
|
|
831
854
|
if (!distPath) {
|
|
832
855
|
this.log(
|
|
@@ -856,7 +879,9 @@ export class ConcurrentMCPServer {
|
|
|
856
879
|
text: html,
|
|
857
880
|
};
|
|
858
881
|
if (config.csp) {
|
|
859
|
-
(content as unknown as Record<string, unknown>)._meta = {
|
|
882
|
+
(content as unknown as Record<string, unknown>)._meta = {
|
|
883
|
+
ui: { csp: config.csp },
|
|
884
|
+
};
|
|
860
885
|
}
|
|
861
886
|
return content;
|
|
862
887
|
},
|
|
@@ -866,7 +891,9 @@ export class ConcurrentMCPServer {
|
|
|
866
891
|
}
|
|
867
892
|
|
|
868
893
|
if (registered.length > 0) {
|
|
869
|
-
this.log(
|
|
894
|
+
this.log(
|
|
895
|
+
`Registered ${registered.length} viewer(s): ${registered.join(", ")}`,
|
|
896
|
+
);
|
|
870
897
|
}
|
|
871
898
|
|
|
872
899
|
return { registered, skipped };
|
|
@@ -911,8 +938,8 @@ export class ConcurrentMCPServer {
|
|
|
911
938
|
*/
|
|
912
939
|
private cleanupSessions(): void {
|
|
913
940
|
const now = Date.now();
|
|
914
|
-
const ttlWithGrace =
|
|
915
|
-
|
|
941
|
+
const ttlWithGrace = McpApp.SESSION_TTL_MS +
|
|
942
|
+
McpApp.SESSION_GRACE_PERIOD_MS;
|
|
916
943
|
let cleaned = 0;
|
|
917
944
|
for (const [sessionId, session] of this.sessions) {
|
|
918
945
|
if (now - session.lastActivity > ttlWithGrace) {
|
|
@@ -998,7 +1025,7 @@ export class ConcurrentMCPServer {
|
|
|
998
1025
|
*
|
|
999
1026
|
* @example
|
|
1000
1027
|
* ```typescript
|
|
1001
|
-
* const server = new
|
|
1028
|
+
* const server = new McpApp({ name: "my-server", version: "1.0.0" });
|
|
1002
1029
|
* server.registerTools(tools, handlers);
|
|
1003
1030
|
* server.registerResource(resource, handler);
|
|
1004
1031
|
*
|
|
@@ -1038,7 +1065,7 @@ export class ConcurrentMCPServer {
|
|
|
1038
1065
|
const requireAuth = options.requireAuth ?? false;
|
|
1039
1066
|
if (requireAuth && !this.authProvider) {
|
|
1040
1067
|
throw new Error(
|
|
1041
|
-
"[
|
|
1068
|
+
"[McpApp] HTTP auth is required (requireAuth=true) but no auth provider is configured.",
|
|
1042
1069
|
);
|
|
1043
1070
|
}
|
|
1044
1071
|
if (!this.authProvider && !requireAuth) {
|
|
@@ -1411,9 +1438,9 @@ export class ConcurrentMCPServer {
|
|
|
1411
1438
|
}
|
|
1412
1439
|
|
|
1413
1440
|
// Guard against session exhaustion
|
|
1414
|
-
if (this.sessions.size >=
|
|
1441
|
+
if (this.sessions.size >= McpApp.MAX_SESSIONS) {
|
|
1415
1442
|
this.cleanupSessions();
|
|
1416
|
-
if (this.sessions.size >=
|
|
1443
|
+
if (this.sessions.size >= McpApp.MAX_SESSIONS) {
|
|
1417
1444
|
return c.json({
|
|
1418
1445
|
jsonrpc: "2.0",
|
|
1419
1446
|
id,
|
|
@@ -1442,7 +1469,9 @@ export class ConcurrentMCPServer {
|
|
|
1442
1469
|
name: this.options.name,
|
|
1443
1470
|
version: this.options.version,
|
|
1444
1471
|
},
|
|
1445
|
-
...(this.options.instructions
|
|
1472
|
+
...(this.options.instructions
|
|
1473
|
+
? { instructions: this.options.instructions }
|
|
1474
|
+
: {}),
|
|
1446
1475
|
},
|
|
1447
1476
|
}),
|
|
1448
1477
|
{
|
|
@@ -1513,7 +1542,9 @@ export class ConcurrentMCPServer {
|
|
|
1513
1542
|
} catch (rethrown) {
|
|
1514
1543
|
this.log(
|
|
1515
1544
|
`Error executing tool ${toolName}: ${
|
|
1516
|
-
rethrown instanceof Error
|
|
1545
|
+
rethrown instanceof Error
|
|
1546
|
+
? rethrown.message
|
|
1547
|
+
: String(rethrown)
|
|
1517
1548
|
}`,
|
|
1518
1549
|
);
|
|
1519
1550
|
const errorMessage = rethrown instanceof Error
|
|
@@ -1663,6 +1694,37 @@ export class ConcurrentMCPServer {
|
|
|
1663
1694
|
// deno-lint-ignore no-explicit-any
|
|
1664
1695
|
app.post("/", handleMcpPost as any);
|
|
1665
1696
|
|
|
1697
|
+
// Embedded mode: skip serve(), surface the Hono fetch handler to the
|
|
1698
|
+
// caller and let them mount it inside their own framework (Fresh, Hono,
|
|
1699
|
+
// Express, etc.). The session cleanup timer + post-init still runs so
|
|
1700
|
+
// SSE clients and sessions are managed identically to the serve() path.
|
|
1701
|
+
if (options.embedded) {
|
|
1702
|
+
if (!options.embeddedHandlerCallback) {
|
|
1703
|
+
throw new Error(
|
|
1704
|
+
"[McpApp] embedded=true requires embeddedHandlerCallback",
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
// deno-lint-ignore no-explicit-any
|
|
1708
|
+
options.embeddedHandlerCallback(app.fetch as any);
|
|
1709
|
+
this.started = true;
|
|
1710
|
+
this.sessionCleanupTimer = setInterval(
|
|
1711
|
+
() => this.cleanupSessions(),
|
|
1712
|
+
McpApp.SESSION_CLEANUP_INTERVAL_MS,
|
|
1713
|
+
);
|
|
1714
|
+
unrefTimer(this.sessionCleanupTimer as unknown as number);
|
|
1715
|
+
this.log(
|
|
1716
|
+
`HTTP handler ready (embedded mode — no port bound, max concurrent: ${
|
|
1717
|
+
this.options.maxConcurrent ?? 10
|
|
1718
|
+
})`,
|
|
1719
|
+
);
|
|
1720
|
+
return {
|
|
1721
|
+
shutdown: async () => {
|
|
1722
|
+
await this.stop();
|
|
1723
|
+
},
|
|
1724
|
+
addr: { hostname: "embedded", port: 0 },
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1666
1728
|
// Start server
|
|
1667
1729
|
this.httpServer = serve(
|
|
1668
1730
|
{
|
|
@@ -1683,7 +1745,7 @@ export class ConcurrentMCPServer {
|
|
|
1683
1745
|
// Start session cleanup timer (prevents unbounded memory growth)
|
|
1684
1746
|
this.sessionCleanupTimer = setInterval(
|
|
1685
1747
|
() => this.cleanupSessions(),
|
|
1686
|
-
|
|
1748
|
+
McpApp.SESSION_CLEANUP_INTERVAL_MS,
|
|
1687
1749
|
);
|
|
1688
1750
|
// Don't block Deno from exiting because of cleanup timer
|
|
1689
1751
|
unrefTimer(this.sessionCleanupTimer as unknown as number);
|
|
@@ -1714,6 +1776,67 @@ export class ConcurrentMCPServer {
|
|
|
1714
1776
|
};
|
|
1715
1777
|
}
|
|
1716
1778
|
|
|
1779
|
+
/**
|
|
1780
|
+
* Build the HTTP middleware stack and return its fetch handler without
|
|
1781
|
+
* binding a port. Use this when you want to mount the MCP HTTP layer
|
|
1782
|
+
* inside another HTTP framework (Fresh, Hono, Express, Cloudflare Workers,
|
|
1783
|
+
* etc.) instead of giving up port ownership to {@link startHttp}.
|
|
1784
|
+
*
|
|
1785
|
+
* The returned handler accepts a Web Standard {@link Request} and returns
|
|
1786
|
+
* a Web Standard {@link Response}. It exposes the same routes as
|
|
1787
|
+
* {@link startHttp}: `POST /mcp`, `GET /mcp` (SSE), `GET /health`,
|
|
1788
|
+
* `GET /metrics`, and `GET /.well-known/oauth-protected-resource`.
|
|
1789
|
+
*
|
|
1790
|
+
* Auth, multi-tenant middleware, scope checks, and rate limiting are all
|
|
1791
|
+
* applied identically. The session cleanup timer and OTel hooks are
|
|
1792
|
+
* started, so the server is fully live after this returns — just without
|
|
1793
|
+
* its own listening socket.
|
|
1794
|
+
*
|
|
1795
|
+
* Multi-tenant SaaS pattern: cache one `McpApp` per tenant
|
|
1796
|
+
* and call `getFetchHandler()` once per server, then dispatch each
|
|
1797
|
+
* inbound request to the right cached handler from your framework's
|
|
1798
|
+
* routing layer.
|
|
1799
|
+
*
|
|
1800
|
+
* @example
|
|
1801
|
+
* ```typescript
|
|
1802
|
+
* // In a Fresh route at routes/mcp/[...path].tsx
|
|
1803
|
+
* const server = new McpApp({ name: "my-mcp", version: "1.0.0" });
|
|
1804
|
+
* server.registerTools(tools, handlers);
|
|
1805
|
+
* const handler = await server.getFetchHandler({
|
|
1806
|
+
* requireAuth: true,
|
|
1807
|
+
* auth: { provider: myAuthProvider },
|
|
1808
|
+
* });
|
|
1809
|
+
* // Later, in your route handler:
|
|
1810
|
+
* return await handler(ctx.req);
|
|
1811
|
+
* ```
|
|
1812
|
+
*
|
|
1813
|
+
* @param options - Same as {@link startHttp}, minus `port`/`hostname`/`onListen`.
|
|
1814
|
+
* @returns A Web Standard fetch handler.
|
|
1815
|
+
*/
|
|
1816
|
+
async getFetchHandler(
|
|
1817
|
+
options: Omit<HttpServerOptions, "port" | "hostname" | "onListen"> = {},
|
|
1818
|
+
): Promise<FetchHandler> {
|
|
1819
|
+
let captured: FetchHandler | null = null;
|
|
1820
|
+
await this.startHttp({
|
|
1821
|
+
// port/hostname are unused in embedded mode but the type requires them.
|
|
1822
|
+
// Pass sentinel values that would never bind even if used.
|
|
1823
|
+
port: 0,
|
|
1824
|
+
...options,
|
|
1825
|
+
embedded: true,
|
|
1826
|
+
embeddedHandlerCallback: (handler) => {
|
|
1827
|
+
captured = handler;
|
|
1828
|
+
},
|
|
1829
|
+
});
|
|
1830
|
+
if (!captured) {
|
|
1831
|
+
// Defensive: startHttp should always invoke the callback synchronously
|
|
1832
|
+
// before returning. If it didn't, something is structurally wrong.
|
|
1833
|
+
throw new Error(
|
|
1834
|
+
"[McpApp] getFetchHandler: embedded callback was not invoked",
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
return captured;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1717
1840
|
/**
|
|
1718
1841
|
* Send a JSON-RPC message to all SSE clients in a session
|
|
1719
1842
|
* Used for server-initiated notifications and requests
|
|
@@ -1790,6 +1913,43 @@ export class ConcurrentMCPServer {
|
|
|
1790
1913
|
return this.samplingBridge;
|
|
1791
1914
|
}
|
|
1792
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
|
+
|
|
1793
1953
|
/**
|
|
1794
1954
|
* Get queue metrics for monitoring
|
|
1795
1955
|
*/
|
|
@@ -1948,7 +2108,12 @@ export class ConcurrentMCPServer {
|
|
|
1948
2108
|
* without re-wrapping. This supports proxy/gateway patterns.
|
|
1949
2109
|
*/
|
|
1950
2110
|
// deno-lint-ignore no-explicit-any
|
|
1951
|
-
private isPreformattedResult(
|
|
2111
|
+
private isPreformattedResult(
|
|
2112
|
+
result: unknown,
|
|
2113
|
+
): result is {
|
|
2114
|
+
content: Array<{ type: string; text: string }>;
|
|
2115
|
+
_meta?: Record<string, unknown>;
|
|
2116
|
+
} {
|
|
1952
2117
|
if (!result || typeof result !== "object") return false;
|
|
1953
2118
|
const obj = result as Record<string, unknown>;
|
|
1954
2119
|
return Array.isArray(obj.content) &&
|
|
@@ -2055,7 +2220,9 @@ export class ConcurrentMCPServer {
|
|
|
2055
2220
|
} catch (mapperError) {
|
|
2056
2221
|
this.log(
|
|
2057
2222
|
`toolErrorMapper threw for tool ${toolName}: ${
|
|
2058
|
-
mapperError instanceof Error
|
|
2223
|
+
mapperError instanceof Error
|
|
2224
|
+
? mapperError.message
|
|
2225
|
+
: String(mapperError)
|
|
2059
2226
|
} (original error: ${
|
|
2060
2227
|
error instanceof Error ? error.message : String(error)
|
|
2061
2228
|
})`,
|
|
@@ -2114,7 +2281,11 @@ export interface RegisterViewersConfig {
|
|
|
2114
2281
|
humanName?: (viewerName: string) => string;
|
|
2115
2282
|
/** MCP Apps CSP — declares external domains the viewer needs (tiles, APIs, CDNs).
|
|
2116
2283
|
* Uses McpUiCsp from @casys/mcp-compose (resourceDomains, connectDomains). */
|
|
2117
|
-
csp?: {
|
|
2284
|
+
csp?: {
|
|
2285
|
+
resourceDomains?: string[];
|
|
2286
|
+
connectDomains?: string[];
|
|
2287
|
+
frameDomains?: string[];
|
|
2288
|
+
};
|
|
2118
2289
|
}
|
|
2119
2290
|
|
|
2120
2291
|
/** Summary returned by registerViewers() */
|
package/src/middleware/mod.ts
CHANGED
package/src/middleware/types.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Server Metrics Collector for @casys/mcp-server
|
|
3
3
|
*
|
|
4
4
|
* In-memory counters, histograms, and gauges with Prometheus text format export.
|
|
5
|
-
* Designed to be embedded in
|
|
5
|
+
* Designed to be embedded in McpApp — no external dependencies.
|
|
6
6
|
*
|
|
7
7
|
* @module lib/server/observability/metrics
|
|
8
8
|
*/
|