@mctx-ai/mcp-server 0.3.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/dist/server.js ADDED
@@ -0,0 +1,876 @@
1
+ /**
2
+ * Core MCP Server Implementation
3
+ *
4
+ * Provides createServer() factory that returns a Cloudflare Worker-compatible
5
+ * app with tool/resource/prompt registration and JSON-RPC 2.0 routing.
6
+ */
7
+
8
+ import { buildInputSchema } from "./types.js";
9
+ import { matchUri, isTemplate } from "./uri.js";
10
+ import { PROGRESS_DEFAULTS } from "./progress.js";
11
+ import { generateCompletions } from "./completion.js";
12
+ import { getLogBuffer, clearLogBuffer, setLogLevel } from "./log.js";
13
+ import {
14
+ sanitizeError as securitySanitizeError,
15
+ validateRequestSize,
16
+ validateResponseSize,
17
+ validateUriScheme,
18
+ canonicalizePath,
19
+ sanitizeInput,
20
+ } from "./security.js";
21
+
22
+ /**
23
+ * HTTP Security Headers
24
+ *
25
+ * Defense in depth: Multiple security headers protect against common web attacks
26
+ * - X-Content-Type-Options: Prevents MIME type sniffing
27
+ * - Content-Security-Policy: Restricts resource loading (none for JSON API)
28
+ * - X-Frame-Options: Prevents clickjacking
29
+ */
30
+ const SECURITY_HEADERS = {
31
+ "Content-Type": "application/json",
32
+ "X-Content-Type-Options": "nosniff",
33
+ "Content-Security-Policy": "default-src 'none'",
34
+ "X-Frame-Options": "DENY",
35
+ };
36
+
37
+ /**
38
+ * Safe JSON serialization handling circular refs, BigInt, Date
39
+ * @param {*} value - Value to serialize
40
+ * @returns {string} JSON string
41
+ */
42
+ function safeSerialize(value) {
43
+ const seen = new WeakSet();
44
+
45
+ return JSON.stringify(value, (key, val) => {
46
+ // Handle null/undefined
47
+ if (val === undefined) return null;
48
+ if (val === null) return null;
49
+
50
+ // Handle primitives
51
+ if (typeof val !== "object") {
52
+ if (typeof val === "bigint") return val.toString();
53
+ return val;
54
+ }
55
+
56
+ // Handle Date
57
+ if (val instanceof Date) {
58
+ return val.toISOString();
59
+ }
60
+
61
+ // Handle circular references
62
+ if (seen.has(val)) {
63
+ return "[Circular]";
64
+ }
65
+ seen.add(val);
66
+
67
+ return val;
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Sanitize error messages for production
73
+ * Removes stack traces and redacts sensitive patterns
74
+ * @param {Error} error - Error object
75
+ * @returns {string} Sanitized message
76
+ */
77
+ function sanitizeError(error) {
78
+ // Determine if we're in production mode
79
+ // Check NODE_ENV or default to production (fail secure)
80
+ const isProduction =
81
+ !process.env.NODE_ENV || process.env.NODE_ENV === "production";
82
+
83
+ return securitySanitizeError(error, isProduction);
84
+ }
85
+
86
+ /**
87
+ * Parse pagination cursor
88
+ * @param {string|undefined} cursor - Base64 encoded offset
89
+ * @returns {number} Offset number
90
+ */
91
+ function parseCursor(cursor) {
92
+ if (!cursor) return 0;
93
+
94
+ try {
95
+ const decoded = Buffer.from(cursor, "base64").toString("utf-8");
96
+ const offset = parseInt(decoded, 10);
97
+ return isNaN(offset) || offset < 0 ? 0 : offset;
98
+ } catch {
99
+ return 0;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Create pagination cursor
105
+ * @param {number} offset - Offset number
106
+ * @returns {string} Base64 encoded cursor
107
+ */
108
+ function createCursor(offset) {
109
+ return Buffer.from(String(offset), "utf-8").toString("base64");
110
+ }
111
+
112
+ /**
113
+ * Paginate items array
114
+ * @param {Array} items - Items to paginate
115
+ * @param {string|undefined} cursor - Pagination cursor
116
+ * @param {number} pageSize - Items per page (default 50)
117
+ * @returns {{items: Array, nextCursor?: string}} Paginated result
118
+ */
119
+ function paginate(items, cursor, pageSize = 50) {
120
+ const offset = parseCursor(cursor);
121
+ const paginatedItems = items.slice(offset, offset + pageSize);
122
+
123
+ const result = { items: paginatedItems };
124
+
125
+ // Add nextCursor if more items exist
126
+ if (offset + pageSize < items.length) {
127
+ result.nextCursor = createCursor(offset + pageSize);
128
+ }
129
+
130
+ return result;
131
+ }
132
+
133
+ /**
134
+ * Create MCP server instance
135
+ * @returns {Object} Server app with registration methods and fetch handler
136
+ */
137
+ export function createServer() {
138
+ // Internal registries
139
+ const tools = new Map();
140
+ const resources = new Map();
141
+ const prompts = new Map();
142
+
143
+ // Client capabilities (set during initialization, not implemented yet)
144
+ // NOTE: In HTTP/stateless mode, sampling requires bidirectional communication
145
+ // which isn't available. This would work in WebSocket/SSE transport.
146
+ // const _clientCapabilities = { sampling: false };
147
+
148
+ /**
149
+ * Register a tool
150
+ * @param {string} name - Tool name
151
+ * @param {Function} handler - Tool handler function
152
+ * @returns {Object} App instance (for chaining)
153
+ */
154
+ function tool(name, handler) {
155
+ if (typeof handler !== "function") {
156
+ throw new Error(`Tool handler for "${name}" must be a function`);
157
+ }
158
+
159
+ tools.set(name, handler);
160
+ return app;
161
+ }
162
+
163
+ /**
164
+ * Register a resource
165
+ * @param {string} uri - Resource URI (may contain {param} templates)
166
+ * @param {Function} handler - Resource handler function
167
+ * @returns {Object} App instance (for chaining)
168
+ */
169
+ function resource(uri, handler) {
170
+ if (typeof handler !== "function") {
171
+ throw new Error(`Resource handler for "${uri}" must be a function`);
172
+ }
173
+
174
+ resources.set(uri, handler);
175
+ return app;
176
+ }
177
+
178
+ /**
179
+ * Register a prompt
180
+ * @param {string} name - Prompt name
181
+ * @param {Function} handler - Prompt handler function
182
+ * @returns {Object} App instance (for chaining)
183
+ */
184
+ function prompt(name, handler) {
185
+ if (typeof handler !== "function") {
186
+ throw new Error(`Prompt handler for "${name}" must be a function`);
187
+ }
188
+
189
+ prompts.set(name, handler);
190
+ return app;
191
+ }
192
+
193
+ /**
194
+ * Handle tools/list request
195
+ * @param {Object} params - Request params
196
+ * @returns {Object} List of tools with pagination
197
+ */
198
+ function handleToolsList(params = {}) {
199
+ const toolsList = Array.from(tools.entries()).map(([name, handler]) => {
200
+ const tool = {
201
+ name,
202
+ description: handler.description || "",
203
+ inputSchema: buildInputSchema(handler.input),
204
+ };
205
+
206
+ return tool;
207
+ });
208
+
209
+ const { items, nextCursor } = paginate(toolsList, params.cursor);
210
+
211
+ const result = { tools: items };
212
+ if (nextCursor) result.nextCursor = nextCursor;
213
+
214
+ return result;
215
+ }
216
+
217
+ /**
218
+ * Handle tools/call request
219
+ * @param {Object} params - Request params
220
+ * @param {Object} [meta] - Request metadata (_meta field)
221
+ * @returns {Promise<Object>} Tool result
222
+ */
223
+ async function handleToolsCall(params, meta = {}) {
224
+ const { name, arguments: args } = params;
225
+
226
+ if (!name) {
227
+ throw new Error("Tool name is required");
228
+ }
229
+
230
+ const handler = tools.get(name);
231
+ if (!handler) {
232
+ throw new Error(`Tool "${name}" not found`);
233
+ }
234
+
235
+ // Validate arguments exist
236
+ if (args === undefined || args === null) {
237
+ throw new Error("Tool arguments are required");
238
+ }
239
+
240
+ // Sanitize arguments to prevent prototype pollution
241
+ const sanitizedArgs = sanitizeInput(args);
242
+
243
+ try {
244
+ // Create ask function for sampling support
245
+ // NOTE: In stateless HTTP mode, sendRequest callback isn't available.
246
+ // Sampling requires bidirectional communication (WebSocket/SSE).
247
+ // For now, ask is null in HTTP mode.
248
+ const ask = null; // TODO: Enable when streaming transport is added
249
+
250
+ // Check if handler is a generator function (robust against minification)
251
+ function isGeneratorFunction(fn) {
252
+ if (!fn || !fn.constructor) return false;
253
+ const name = fn.constructor.name;
254
+ if (name === "GeneratorFunction" || name === "AsyncGeneratorFunction")
255
+ return true;
256
+ const proto = Object.getPrototypeOf(fn);
257
+ return proto && proto[Symbol.toStringTag] === "GeneratorFunction";
258
+ }
259
+
260
+ const isGenerator = isGeneratorFunction(handler);
261
+
262
+ if (isGenerator) {
263
+ // Execute generator with progress tracking
264
+ return await executeGeneratorHandler(handler, sanitizedArgs, ask, meta);
265
+ }
266
+
267
+ // Execute regular handler (support both sync and async)
268
+ // Pass ask as second argument
269
+ const result = await handler(sanitizedArgs, ask);
270
+
271
+ // Wrap result based on type
272
+ if (typeof result === "string") {
273
+ return {
274
+ content: [{ type: "text", text: result }],
275
+ };
276
+ }
277
+
278
+ // For objects/arrays, serialize safely
279
+ const serialized = safeSerialize(result);
280
+ return {
281
+ content: [{ type: "text", text: serialized }],
282
+ };
283
+ } catch (error) {
284
+ // Return error as content with isError flag
285
+ const sanitized = sanitizeError(error);
286
+ return {
287
+ content: [{ type: "text", text: sanitized }],
288
+ isError: true,
289
+ };
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Execute generator-based handler with progress tracking
295
+ * @private
296
+ * @param {GeneratorFunction} handler - Generator handler
297
+ * @param {Object} args - Tool arguments
298
+ * @param {Function|null} ask - Ask function (or null if not supported)
299
+ * @param {Object} meta - Request metadata
300
+ * @returns {Promise<Object>} Tool result
301
+ */
302
+ async function executeGeneratorHandler(handler, args, ask, meta) {
303
+ const progressToken = meta.progressToken;
304
+ const startTime = Date.now();
305
+ let yieldCount = 0;
306
+
307
+ // NOTE: In HTTP mode, progress notifications are collected but can't be sent
308
+ // until the request completes. In a streaming transport (WebSocket/SSE),
309
+ // these would be sent immediately as notifications.
310
+ const progressNotifications = [];
311
+
312
+ try {
313
+ // Execute generator using iterator protocol to capture return value
314
+ const iterator = handler(args, ask);
315
+ let iterResult = await iterator.next();
316
+
317
+ while (!iterResult.done) {
318
+ yieldCount++;
319
+
320
+ // Enforce max yields guardrail
321
+ if (yieldCount > PROGRESS_DEFAULTS.maxYields) {
322
+ throw new Error(
323
+ `Generator exceeded maximum yields (${PROGRESS_DEFAULTS.maxYields})`,
324
+ );
325
+ }
326
+
327
+ // Enforce max execution time guardrail
328
+ const elapsed = Date.now() - startTime;
329
+ if (elapsed > PROGRESS_DEFAULTS.maxExecutionTime) {
330
+ throw new Error(
331
+ `Generator exceeded maximum execution time (${PROGRESS_DEFAULTS.maxExecutionTime}ms)`,
332
+ );
333
+ }
334
+
335
+ // Check if yielded value is a progress notification
336
+ const value = iterResult.value;
337
+ if (value && typeof value === "object" && value.type === "progress") {
338
+ // Store progress notification
339
+ // In streaming mode, would send via progressToken
340
+ if (progressToken) {
341
+ progressNotifications.push({
342
+ progressToken,
343
+ ...value,
344
+ });
345
+ }
346
+ }
347
+
348
+ // Get next value
349
+ iterResult = await iterator.next();
350
+ }
351
+
352
+ // Return final result from generator's return value (not last yielded value)
353
+ const finalResult = iterResult.value;
354
+
355
+ if (typeof finalResult === "string") {
356
+ return {
357
+ content: [{ type: "text", text: finalResult }],
358
+ };
359
+ }
360
+
361
+ const serialized = safeSerialize(finalResult);
362
+ return {
363
+ content: [{ type: "text", text: serialized }],
364
+ };
365
+ } catch (error) {
366
+ const sanitized = sanitizeError(error);
367
+ return {
368
+ content: [{ type: "text", text: sanitized }],
369
+ isError: true,
370
+ };
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Handle resources/list request
376
+ * @param {Object} params - Request params
377
+ * @returns {Object} List of static resources with pagination
378
+ */
379
+ function handleResourcesList(params = {}) {
380
+ // Only return static resources (not templates)
381
+ const resourcesList = Array.from(resources.entries())
382
+ .filter(([uri]) => !isTemplate(uri))
383
+ .map(([uri, handler]) => ({
384
+ uri,
385
+ name: handler.name || uri,
386
+ description: handler.description || "",
387
+ mimeType: handler.mimeType || "text/plain",
388
+ }));
389
+
390
+ const { items, nextCursor } = paginate(resourcesList, params.cursor);
391
+
392
+ const result = { resources: items };
393
+ if (nextCursor) result.nextCursor = nextCursor;
394
+
395
+ return result;
396
+ }
397
+
398
+ /**
399
+ * Handle resources/templates/list request
400
+ * @param {Object} params - Request params
401
+ * @returns {Object} List of resource templates with pagination
402
+ */
403
+ function handleResourceTemplatesList(params = {}) {
404
+ // Only return template resources (containing {param})
405
+ const templatesList = Array.from(resources.entries())
406
+ .filter(([uri]) => isTemplate(uri))
407
+ .map(([uriTemplate, handler]) => ({
408
+ uriTemplate,
409
+ name: handler.name || uriTemplate,
410
+ description: handler.description || "",
411
+ mimeType: handler.mimeType || "text/plain",
412
+ }));
413
+
414
+ const { items, nextCursor } = paginate(templatesList, params.cursor);
415
+
416
+ const result = { resourceTemplates: items };
417
+ if (nextCursor) result.nextCursor = nextCursor;
418
+
419
+ return result;
420
+ }
421
+
422
+ /**
423
+ * Handle resources/read request
424
+ * @param {Object} params - Request params
425
+ * @returns {Promise<Object>} Resource content
426
+ */
427
+ async function handleResourcesRead(params) {
428
+ const { uri } = params;
429
+
430
+ if (!uri) {
431
+ throw new Error("Resource URI is required");
432
+ }
433
+
434
+ // Validate URI scheme (prevent dangerous schemes like file://, javascript:, data:)
435
+ if (!validateUriScheme(uri)) {
436
+ throw new Error(
437
+ `Invalid URI scheme: only http:// and https:// are allowed`,
438
+ );
439
+ }
440
+
441
+ // Canonicalize path to prevent traversal attacks
442
+ const canonicalUri = canonicalizePath(uri);
443
+
444
+ // Find matching resource (try exact match first, then templates)
445
+ let handler = null;
446
+ let extractedParams = {};
447
+
448
+ // Try exact match first (use canonical URI for matching)
449
+ if (resources.has(canonicalUri)) {
450
+ handler = resources.get(canonicalUri);
451
+ } else {
452
+ // Try template matching using uri.js module
453
+ for (const [registeredUri, h] of resources.entries()) {
454
+ const match = matchUri(registeredUri, canonicalUri);
455
+ if (match) {
456
+ handler = h;
457
+ extractedParams = match.params || {};
458
+ break;
459
+ }
460
+ }
461
+ }
462
+
463
+ if (!handler) {
464
+ throw new Error(`Resource "${uri}" not found`);
465
+ }
466
+
467
+ try {
468
+ // Create ask function (null in HTTP mode)
469
+ const ask = null;
470
+
471
+ // Sanitize extracted params to prevent prototype pollution
472
+ const sanitizedParams = sanitizeInput(extractedParams);
473
+
474
+ // Execute handler with sanitized params and ask
475
+ const result = await handler(sanitizedParams, ask);
476
+
477
+ // Wrap result as resource content
478
+ const mimeType = handler.mimeType || "text/plain";
479
+
480
+ if (typeof result === "string") {
481
+ return {
482
+ contents: [
483
+ {
484
+ uri,
485
+ mimeType,
486
+ text: result,
487
+ },
488
+ ],
489
+ };
490
+ }
491
+
492
+ // For binary data (Buffer, Uint8Array)
493
+ if (result instanceof Buffer || result instanceof Uint8Array) {
494
+ const base64 = Buffer.from(result).toString("base64");
495
+ return {
496
+ contents: [
497
+ {
498
+ uri,
499
+ mimeType,
500
+ blob: base64,
501
+ },
502
+ ],
503
+ };
504
+ }
505
+
506
+ // For objects, serialize as JSON
507
+ const serialized = safeSerialize(result);
508
+ return {
509
+ contents: [
510
+ {
511
+ uri,
512
+ mimeType: "application/json",
513
+ text: serialized,
514
+ },
515
+ ],
516
+ };
517
+ } catch (error) {
518
+ throw new Error(
519
+ `Failed to read resource "${uri}": ${sanitizeError(error)}`,
520
+ );
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Handle prompts/list request
526
+ * @param {Object} params - Request params
527
+ * @returns {Object} List of prompts with pagination
528
+ */
529
+ function handlePromptsList(params = {}) {
530
+ const promptsList = Array.from(prompts.entries()).map(([name, handler]) => {
531
+ // Build arguments schema from handler.input
532
+ const argumentsList = handler.input
533
+ ? Object.entries(handler.input).map(([argName, schema]) => ({
534
+ name: argName,
535
+ description: schema.description || "",
536
+ required: schema._required === true,
537
+ }))
538
+ : [];
539
+
540
+ return {
541
+ name,
542
+ description: handler.description || "",
543
+ arguments: argumentsList,
544
+ };
545
+ });
546
+
547
+ const { items, nextCursor } = paginate(promptsList, params.cursor);
548
+
549
+ const result = { prompts: items };
550
+ if (nextCursor) result.nextCursor = nextCursor;
551
+
552
+ return result;
553
+ }
554
+
555
+ /**
556
+ * Handle prompts/get request
557
+ * @param {Object} params - Request params
558
+ * @returns {Promise<Object>} Prompt messages
559
+ */
560
+ async function handlePromptsGet(params) {
561
+ const { name, arguments: args } = params;
562
+
563
+ if (!name) {
564
+ throw new Error("Prompt name is required");
565
+ }
566
+
567
+ const handler = prompts.get(name);
568
+ if (!handler) {
569
+ throw new Error(`Prompt "${name}" not found`);
570
+ }
571
+
572
+ try {
573
+ // Create ask function (null in HTTP mode)
574
+ const ask = null;
575
+
576
+ // Sanitize arguments to prevent prototype pollution
577
+ const sanitizedArgs = sanitizeInput(args || {});
578
+
579
+ // Execute handler with sanitized args
580
+ const result = await handler(sanitizedArgs, ask);
581
+
582
+ // If handler returns a string, wrap as user message
583
+ if (typeof result === "string") {
584
+ return {
585
+ messages: [
586
+ {
587
+ role: "user",
588
+ content: {
589
+ type: "text",
590
+ text: result,
591
+ },
592
+ },
593
+ ],
594
+ };
595
+ }
596
+
597
+ // If handler returns messages array, wrap it
598
+ if (Array.isArray(result)) {
599
+ return { messages: result };
600
+ }
601
+
602
+ // If handler returns object with messages (from conversation()), pass through
603
+ if (result && result.messages) {
604
+ return result;
605
+ }
606
+
607
+ // Otherwise serialize and wrap as user message
608
+ const serialized = safeSerialize(result);
609
+ return {
610
+ messages: [
611
+ {
612
+ role: "user",
613
+ content: {
614
+ type: "text",
615
+ text: serialized,
616
+ },
617
+ },
618
+ ],
619
+ };
620
+ } catch (error) {
621
+ throw new Error(
622
+ `Failed to get prompt "${name}": ${sanitizeError(error)}`,
623
+ );
624
+ }
625
+ }
626
+
627
+ /**
628
+ * Handle completion/complete request
629
+ * @param {Object} params - Request params
630
+ * @returns {Object} Completion suggestions
631
+ */
632
+ function handleCompletionComplete(params) {
633
+ const { ref, argument } = params;
634
+
635
+ if (!ref || !ref.type) {
636
+ return {
637
+ completion: {
638
+ values: [],
639
+ hasMore: false,
640
+ },
641
+ };
642
+ }
643
+
644
+ // Determine which registry to use based on ref type
645
+ let registeredItems;
646
+ if (ref.type === "ref/prompt-argument") {
647
+ // Convert prompts Map to object for generateCompletions
648
+ registeredItems = Object.fromEntries(prompts);
649
+ } else if (ref.type === "ref/resource") {
650
+ // Convert resources Map to object for generateCompletions
651
+ registeredItems = Object.fromEntries(resources);
652
+ } else {
653
+ return {
654
+ completion: {
655
+ values: [],
656
+ hasMore: false,
657
+ },
658
+ };
659
+ }
660
+
661
+ return generateCompletions(registeredItems, ref, argument?.value);
662
+ }
663
+
664
+ /**
665
+ * Handle logging/setLevel request
666
+ * @param {Object} params - Request params
667
+ * @returns {Object} Empty success result
668
+ */
669
+ function handleLoggingSetLevel(params) {
670
+ const { level } = params;
671
+
672
+ if (!level) {
673
+ throw new Error("Log level is required");
674
+ }
675
+
676
+ setLogLevel(level);
677
+
678
+ return {}; // Success
679
+ }
680
+
681
+ /**
682
+ * Route JSON-RPC request to appropriate handler
683
+ * @param {Object} request - JSON-RPC request
684
+ * @returns {Promise<Object>} Response result
685
+ */
686
+ async function route(request) {
687
+ const { method, params, _meta } = request;
688
+
689
+ switch (method) {
690
+ case "tools/list":
691
+ return handleToolsList(params);
692
+
693
+ case "tools/call":
694
+ return await handleToolsCall(params, _meta);
695
+
696
+ case "resources/list":
697
+ return handleResourcesList(params);
698
+
699
+ case "resources/read":
700
+ return await handleResourcesRead(params);
701
+
702
+ case "resources/templates/list":
703
+ return handleResourceTemplatesList(params);
704
+
705
+ case "prompts/list":
706
+ return handlePromptsList(params);
707
+
708
+ case "prompts/get":
709
+ return await handlePromptsGet(params);
710
+
711
+ case "notifications/cancelled":
712
+ // Silent acknowledgment - no response for notifications
713
+ return null;
714
+
715
+ case "completion/complete":
716
+ return handleCompletionComplete(params);
717
+
718
+ case "logging/setLevel":
719
+ return handleLoggingSetLevel(params);
720
+
721
+ default: {
722
+ {
723
+ const error = new Error("Method not found");
724
+ error.code = -32601;
725
+ throw error;
726
+ }
727
+ }
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Cloudflare Worker fetch handler
733
+ * @param {Request} request - HTTP request
734
+ * @param {Object} _env - Environment variables
735
+ * @param {Object} _ctx - Execution context
736
+ * @returns {Promise<Response>} HTTP response
737
+ */
738
+ async function fetch(request, _env, _ctx) {
739
+ // Only accept POST requests
740
+ if (request.method !== "POST") {
741
+ return new Response(
742
+ JSON.stringify({
743
+ jsonrpc: "2.0",
744
+ error: {
745
+ code: -32600,
746
+ message: "Invalid Request - Only POST method is supported",
747
+ },
748
+ id: null,
749
+ }),
750
+ {
751
+ status: 405,
752
+ headers: SECURITY_HEADERS,
753
+ },
754
+ );
755
+ }
756
+
757
+ let rpcRequest;
758
+ let rawBody;
759
+
760
+ try {
761
+ // Get raw body text for size validation
762
+ rawBody = await request.text();
763
+
764
+ // Validate request size before parsing (prevent DoS)
765
+ validateRequestSize(rawBody);
766
+
767
+ // Parse JSON body
768
+ rpcRequest = JSON.parse(rawBody);
769
+ } catch {
770
+ // Parse error
771
+ return new Response(
772
+ JSON.stringify({
773
+ jsonrpc: "2.0",
774
+ error: {
775
+ code: -32700,
776
+ message: "Parse error - Invalid JSON",
777
+ },
778
+ id: null,
779
+ }),
780
+ {
781
+ status: 400,
782
+ headers: SECURITY_HEADERS,
783
+ },
784
+ );
785
+ }
786
+
787
+ // Validate JSON-RPC structure
788
+ if (!rpcRequest.method || typeof rpcRequest.method !== "string") {
789
+ return new Response(
790
+ JSON.stringify({
791
+ jsonrpc: "2.0",
792
+ error: {
793
+ code: -32600,
794
+ message: "Invalid Request - Missing or invalid method",
795
+ },
796
+ id: rpcRequest.id || null,
797
+ }),
798
+ {
799
+ status: 400,
800
+ headers: SECURITY_HEADERS,
801
+ },
802
+ );
803
+ }
804
+
805
+ try {
806
+ // Route request
807
+ const result = await route(rpcRequest);
808
+
809
+ // Flush log buffer
810
+ // NOTE: In HTTP mode, logs are buffered but can't be sent as notifications
811
+ // mid-request. In a streaming transport (WebSocket/SSE), these would be
812
+ // sent as notifications/message events during handler execution.
813
+ getLogBuffer();
814
+ clearLogBuffer();
815
+
816
+ // TODO: When streaming transport is added, send buffered logs as notifications
817
+ // For now, just clear them since we can't send them in stateless HTTP mode
818
+
819
+ // For notifications (no id), return 204 No Content
820
+ if (!("id" in rpcRequest)) {
821
+ return new Response(null, { status: 204 });
822
+ }
823
+
824
+ // Build response object
825
+ const responseBody = {
826
+ jsonrpc: "2.0",
827
+ id: rpcRequest.id,
828
+ result,
829
+ };
830
+
831
+ // Validate response size before sending (prevent DoS)
832
+ validateResponseSize(responseBody);
833
+
834
+ // Return JSON-RPC success response
835
+ return new Response(JSON.stringify(responseBody), {
836
+ status: 200,
837
+ headers: SECURITY_HEADERS,
838
+ });
839
+ } catch (error) {
840
+ // Check if error is JSON-RPC error (has code and message)
841
+ const isJsonRpcError =
842
+ error && typeof error === "object" && "code" in error;
843
+
844
+ const rpcError = isJsonRpcError
845
+ ? error
846
+ : {
847
+ code: -32603,
848
+ message: sanitizeError(
849
+ error instanceof Error ? error : new Error(String(error)),
850
+ ),
851
+ };
852
+
853
+ return new Response(
854
+ JSON.stringify({
855
+ jsonrpc: "2.0",
856
+ id: rpcRequest.id || null,
857
+ error: rpcError,
858
+ }),
859
+ {
860
+ status: 200, // JSON-RPC errors use 200 status with error object
861
+ headers: SECURITY_HEADERS,
862
+ },
863
+ );
864
+ }
865
+ }
866
+
867
+ // Create app object
868
+ const app = {
869
+ tool,
870
+ resource,
871
+ prompt,
872
+ fetch,
873
+ };
874
+
875
+ return app;
876
+ }