@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/LICENSE +21 -0
- package/dist/completion.js +214 -0
- package/dist/conversation.js +139 -0
- package/dist/index.d.ts +733 -0
- package/dist/index.js +18 -0
- package/dist/log.js +213 -0
- package/dist/progress.js +84 -0
- package/dist/sampling.js +130 -0
- package/dist/security.js +339 -0
- package/dist/server.js +876 -0
- package/dist/types.js +252 -0
- package/dist/uri.js +120 -0
- package/package.json +53 -0
- package/src/completion.js +214 -0
- package/src/conversation.js +139 -0
- package/src/index.d.ts +733 -0
- package/src/index.js +18 -0
- package/src/log.js +213 -0
- package/src/progress.js +84 -0
- package/src/sampling.js +130 -0
- package/src/security.js +339 -0
- package/src/server.js +876 -0
- package/src/types.js +252 -0
- package/src/uri.js +120 -0
package/src/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
|
+
}
|