@matthesketh/utopia-ai 0.7.0 → 0.8.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/ai-KmtRQe5L.d.ts +1 -1
- package/dist/index.cjs +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/index.cjs +146 -23
- package/dist/mcp/index.d.cts +13 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +146 -23
- package/package.json +1 -1
package/dist/ai-KmtRQe5L.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as ChatRequest, e as ChatResponse, C as ChatChunk, E as EmbeddingRequest, f as EmbeddingResponse, c as ChatMessage, l as ToolDefinition, j as ToolCall, a as AIHooks, R as RetryConfig, A as AIAdapter } from './types-FSnS43LM
|
|
1
|
+
import { d as ChatRequest, e as ChatResponse, C as ChatChunk, E as EmbeddingRequest, f as EmbeddingResponse, c as ChatMessage, l as ToolDefinition, j as ToolCall, a as AIHooks, R as RetryConfig, A as AIAdapter } from './types-FSnS43LM';
|
|
2
2
|
|
|
3
3
|
interface AI {
|
|
4
4
|
/** Send a chat completion request. */
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/mcp/index.cjs
CHANGED
|
@@ -43,19 +43,32 @@ function createMCPServer(config) {
|
|
|
43
43
|
(config.prompts ?? []).map((p) => [p.definition.name, p])
|
|
44
44
|
);
|
|
45
45
|
async function handleRequest(request) {
|
|
46
|
+
if (!request || request.jsonrpc !== "2.0" || typeof request.method !== "string") {
|
|
47
|
+
return {
|
|
48
|
+
jsonrpc: "2.0",
|
|
49
|
+
id: request?.id ?? null,
|
|
50
|
+
error: {
|
|
51
|
+
code: -32600,
|
|
52
|
+
message: 'Invalid Request: must include jsonrpc "2.0" and a string method'
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
46
56
|
try {
|
|
47
57
|
const result = await dispatch(request);
|
|
48
58
|
return { jsonrpc: "2.0", id: request.id, result };
|
|
49
59
|
} catch (err) {
|
|
50
60
|
const rpcErr = err;
|
|
61
|
+
if (typeof rpcErr.code === "number") {
|
|
62
|
+
return {
|
|
63
|
+
jsonrpc: "2.0",
|
|
64
|
+
id: request.id,
|
|
65
|
+
error: { code: rpcErr.code, message: rpcErr.message ?? "Error", data: rpcErr.data }
|
|
66
|
+
};
|
|
67
|
+
}
|
|
51
68
|
return {
|
|
52
69
|
jsonrpc: "2.0",
|
|
53
70
|
id: request.id,
|
|
54
|
-
error: {
|
|
55
|
-
code: rpcErr.code ?? -32603,
|
|
56
|
-
message: rpcErr.message ?? "Internal error",
|
|
57
|
-
data: rpcErr.data
|
|
58
|
-
}
|
|
71
|
+
error: { code: -32603, message: "Internal error" }
|
|
59
72
|
};
|
|
60
73
|
}
|
|
61
74
|
}
|
|
@@ -83,12 +96,22 @@ function createMCPServer(config) {
|
|
|
83
96
|
}))
|
|
84
97
|
};
|
|
85
98
|
case "tools/call": {
|
|
86
|
-
const params = request.params;
|
|
87
|
-
const
|
|
99
|
+
const params = asParamsObject(request.params, "tools/call");
|
|
100
|
+
const name = params.name;
|
|
101
|
+
if (typeof name !== "string") {
|
|
102
|
+
throw makeError(-32602, 'tools/call requires a string "name"');
|
|
103
|
+
}
|
|
104
|
+
const tool = toolMap.get(name);
|
|
88
105
|
if (!tool) {
|
|
89
|
-
throw makeError(-32602, `Unknown tool: ${
|
|
106
|
+
throw makeError(-32602, `Unknown tool: ${name}`);
|
|
107
|
+
}
|
|
108
|
+
const args = params.arguments ?? {};
|
|
109
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) {
|
|
110
|
+
throw makeError(-32602, "tool arguments must be an object");
|
|
90
111
|
}
|
|
91
|
-
|
|
112
|
+
const argRecord = args;
|
|
113
|
+
validateAgainstSchema(tool.definition.inputSchema, argRecord, name);
|
|
114
|
+
return tool.handler(argRecord);
|
|
92
115
|
}
|
|
93
116
|
case "resources/list":
|
|
94
117
|
return {
|
|
@@ -100,12 +123,16 @@ function createMCPServer(config) {
|
|
|
100
123
|
}))
|
|
101
124
|
};
|
|
102
125
|
case "resources/read": {
|
|
103
|
-
const params = request.params;
|
|
104
|
-
const
|
|
126
|
+
const params = asParamsObject(request.params, "resources/read");
|
|
127
|
+
const uri = params.uri;
|
|
128
|
+
if (typeof uri !== "string") {
|
|
129
|
+
throw makeError(-32602, 'resources/read requires a string "uri"');
|
|
130
|
+
}
|
|
131
|
+
const resource = findResource(uri);
|
|
105
132
|
if (!resource) {
|
|
106
|
-
throw makeError(-32602, `Unknown resource: ${
|
|
133
|
+
throw makeError(-32602, `Unknown resource: ${uri}`);
|
|
107
134
|
}
|
|
108
|
-
const content = await resource.handler(
|
|
135
|
+
const content = await resource.handler(uri);
|
|
109
136
|
return { contents: [content] };
|
|
110
137
|
}
|
|
111
138
|
case "prompts/list":
|
|
@@ -117,12 +144,17 @@ function createMCPServer(config) {
|
|
|
117
144
|
}))
|
|
118
145
|
};
|
|
119
146
|
case "prompts/get": {
|
|
120
|
-
const params = request.params;
|
|
121
|
-
const
|
|
147
|
+
const params = asParamsObject(request.params, "prompts/get");
|
|
148
|
+
const name = params.name;
|
|
149
|
+
if (typeof name !== "string") {
|
|
150
|
+
throw makeError(-32602, 'prompts/get requires a string "name"');
|
|
151
|
+
}
|
|
152
|
+
const prompt = promptMap.get(name);
|
|
122
153
|
if (!prompt) {
|
|
123
|
-
throw makeError(-32602, `Unknown prompt: ${
|
|
154
|
+
throw makeError(-32602, `Unknown prompt: ${name}`);
|
|
124
155
|
}
|
|
125
|
-
|
|
156
|
+
const args = params.arguments ?? {};
|
|
157
|
+
return prompt.handler(args);
|
|
126
158
|
}
|
|
127
159
|
case "ping":
|
|
128
160
|
return {};
|
|
@@ -149,10 +181,46 @@ function matchesTemplate(pattern, uri) {
|
|
|
149
181
|
function makeError(code, message) {
|
|
150
182
|
return { code, message };
|
|
151
183
|
}
|
|
184
|
+
function asParamsObject(params, ctx) {
|
|
185
|
+
if (params === null || typeof params !== "object" || Array.isArray(params)) {
|
|
186
|
+
throw makeError(-32602, `Invalid params for ${ctx}: expected an object`);
|
|
187
|
+
}
|
|
188
|
+
return params;
|
|
189
|
+
}
|
|
190
|
+
function validateAgainstSchema(schema, args, toolName) {
|
|
191
|
+
if (!schema || typeof schema !== "object") return;
|
|
192
|
+
const s = schema;
|
|
193
|
+
if (Array.isArray(s.required)) {
|
|
194
|
+
for (const key of s.required) {
|
|
195
|
+
if (typeof key === "string" && !(key in args)) {
|
|
196
|
+
throw makeError(-32602, `Missing required argument "${key}" for tool "${toolName}"`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (s.properties && typeof s.properties === "object") {
|
|
201
|
+
for (const [key, prop] of Object.entries(s.properties)) {
|
|
202
|
+
const value = args[key];
|
|
203
|
+
if (value === void 0 || value === null) continue;
|
|
204
|
+
const expected = prop?.type;
|
|
205
|
+
if (!expected) continue;
|
|
206
|
+
const ok = expected === "number" || expected === "integer" ? typeof value === "number" : expected === "array" ? Array.isArray(value) : expected === "object" ? typeof value === "object" && !Array.isArray(value) : typeof value === expected;
|
|
207
|
+
if (!ok) {
|
|
208
|
+
throw makeError(
|
|
209
|
+
-32602,
|
|
210
|
+
`Argument "${key}" for tool "${toolName}" must be of type ${expected}`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
152
216
|
|
|
153
217
|
// src/mcp/client.ts
|
|
154
218
|
function createMCPClient(config) {
|
|
155
219
|
let requestId = 0;
|
|
220
|
+
const parsedUrl = new URL(config.url);
|
|
221
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
222
|
+
throw new Error(`MCP client URL must be http(s): ${config.url}`);
|
|
223
|
+
}
|
|
156
224
|
async function rpc(method, params) {
|
|
157
225
|
const request = {
|
|
158
226
|
jsonrpc: "2.0",
|
|
@@ -167,6 +235,10 @@ function createMCPClient(config) {
|
|
|
167
235
|
...config.headers
|
|
168
236
|
},
|
|
169
237
|
body: JSON.stringify(request),
|
|
238
|
+
// never follow redirects: a 3xx to another host would otherwise re-send
|
|
239
|
+
// config.headers (often a bearer token) to that host, leaking the
|
|
240
|
+
// credential and enabling an ssrf pivot.
|
|
241
|
+
redirect: "error",
|
|
170
242
|
signal: AbortSignal.timeout(3e4)
|
|
171
243
|
});
|
|
172
244
|
if (!response.ok) {
|
|
@@ -239,16 +311,45 @@ function createMCPClient(config) {
|
|
|
239
311
|
|
|
240
312
|
// src/mcp/handler.ts
|
|
241
313
|
function createMCPHandler(server, options) {
|
|
242
|
-
const corsOrigin = options?.corsOrigin
|
|
314
|
+
const corsOrigin = options?.corsOrigin;
|
|
315
|
+
const allowedOrigins = options?.allowedOrigins;
|
|
316
|
+
const authorize = options?.authorize;
|
|
243
317
|
return async (req, res) => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
318
|
+
if (corsOrigin) {
|
|
319
|
+
res.setHeader("Access-Control-Allow-Origin", corsOrigin);
|
|
320
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
321
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
322
|
+
}
|
|
247
323
|
if (req.method === "OPTIONS") {
|
|
248
324
|
res.writeHead(204);
|
|
249
325
|
res.end();
|
|
250
326
|
return;
|
|
251
327
|
}
|
|
328
|
+
const origin = req.headers.origin;
|
|
329
|
+
if (allowedOrigins && origin !== void 0 && !allowedOrigins.includes(origin)) {
|
|
330
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
331
|
+
res.end("Forbidden");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (authorize) {
|
|
335
|
+
let allowed = false;
|
|
336
|
+
try {
|
|
337
|
+
allowed = await authorize(req);
|
|
338
|
+
} catch {
|
|
339
|
+
allowed = false;
|
|
340
|
+
}
|
|
341
|
+
if (!allowed) {
|
|
342
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
343
|
+
res.end(
|
|
344
|
+
JSON.stringify({
|
|
345
|
+
jsonrpc: "2.0",
|
|
346
|
+
id: null,
|
|
347
|
+
error: { code: -32600, message: "Unauthorized" }
|
|
348
|
+
})
|
|
349
|
+
);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
252
353
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
253
354
|
if (url.pathname.endsWith("/sse") && req.method === "GET") {
|
|
254
355
|
handleSSE(server, req, res);
|
|
@@ -270,6 +371,18 @@ async function handlePost(server, req, res) {
|
|
|
270
371
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
271
372
|
res.end(JSON.stringify(response));
|
|
272
373
|
} catch (err) {
|
|
374
|
+
const errObj = err;
|
|
375
|
+
if (errObj.statusCode === 413) {
|
|
376
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
377
|
+
res.end(
|
|
378
|
+
JSON.stringify({
|
|
379
|
+
jsonrpc: "2.0",
|
|
380
|
+
id: null,
|
|
381
|
+
error: { code: -32600, message: "Payload too large" }
|
|
382
|
+
})
|
|
383
|
+
);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
273
386
|
const data = err instanceof SyntaxError ? err.message : "Invalid request";
|
|
274
387
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
275
388
|
res.end(
|
|
@@ -303,10 +416,20 @@ data: ${endpointUrl}
|
|
|
303
416
|
clearInterval(keepAlive);
|
|
304
417
|
});
|
|
305
418
|
}
|
|
306
|
-
|
|
419
|
+
var DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
|
|
420
|
+
function readBody(req, maxSize = DEFAULT_MAX_BODY_SIZE) {
|
|
307
421
|
return new Promise((resolve, reject) => {
|
|
308
422
|
const chunks = [];
|
|
309
|
-
|
|
423
|
+
let size = 0;
|
|
424
|
+
req.on("data", (chunk) => {
|
|
425
|
+
size += chunk.length;
|
|
426
|
+
if (size > maxSize) {
|
|
427
|
+
req.removeAllListeners("data");
|
|
428
|
+
reject(Object.assign(new Error("Payload too large"), { statusCode: 413 }));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
chunks.push(chunk);
|
|
432
|
+
});
|
|
310
433
|
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
311
434
|
req.on("error", reject);
|
|
312
435
|
});
|
package/dist/mcp/index.d.cts
CHANGED
|
@@ -174,6 +174,19 @@ declare function createMCPClient(config: MCPClientConfig): MCPClient;
|
|
|
174
174
|
|
|
175
175
|
interface MCPHandlerOptions {
|
|
176
176
|
corsOrigin?: string;
|
|
177
|
+
/**
|
|
178
|
+
* allow-list of permitted `Origin` header values. when set, any request
|
|
179
|
+
* carrying an `Origin` not in the list is rejected with 403. this is the
|
|
180
|
+
* primary defence against dns-rebinding attacks on a locally-bound server.
|
|
181
|
+
*/
|
|
182
|
+
allowedOrigins?: string[];
|
|
183
|
+
/**
|
|
184
|
+
* authorisation gate run before any request is dispatched to the server.
|
|
185
|
+
* return false (or throw) to reject with 401. an mcp server exposes tool
|
|
186
|
+
* execution, so it must not be reachable on a network without an authz
|
|
187
|
+
* check — there is intentionally no default-allow for remote callers.
|
|
188
|
+
*/
|
|
189
|
+
authorize?: (req: IncomingMessage) => boolean | Promise<boolean>;
|
|
177
190
|
}
|
|
178
191
|
/**
|
|
179
192
|
* Create a Node.js HTTP handler for an MCP server.
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -174,6 +174,19 @@ declare function createMCPClient(config: MCPClientConfig): MCPClient;
|
|
|
174
174
|
|
|
175
175
|
interface MCPHandlerOptions {
|
|
176
176
|
corsOrigin?: string;
|
|
177
|
+
/**
|
|
178
|
+
* allow-list of permitted `Origin` header values. when set, any request
|
|
179
|
+
* carrying an `Origin` not in the list is rejected with 403. this is the
|
|
180
|
+
* primary defence against dns-rebinding attacks on a locally-bound server.
|
|
181
|
+
*/
|
|
182
|
+
allowedOrigins?: string[];
|
|
183
|
+
/**
|
|
184
|
+
* authorisation gate run before any request is dispatched to the server.
|
|
185
|
+
* return false (or throw) to reject with 401. an mcp server exposes tool
|
|
186
|
+
* execution, so it must not be reachable on a network without an authz
|
|
187
|
+
* check — there is intentionally no default-allow for remote callers.
|
|
188
|
+
*/
|
|
189
|
+
authorize?: (req: IncomingMessage) => boolean | Promise<boolean>;
|
|
177
190
|
}
|
|
178
191
|
/**
|
|
179
192
|
* Create a Node.js HTTP handler for an MCP server.
|
package/dist/mcp/index.js
CHANGED
|
@@ -15,19 +15,32 @@ function createMCPServer(config) {
|
|
|
15
15
|
(config.prompts ?? []).map((p) => [p.definition.name, p])
|
|
16
16
|
);
|
|
17
17
|
async function handleRequest(request) {
|
|
18
|
+
if (!request || request.jsonrpc !== "2.0" || typeof request.method !== "string") {
|
|
19
|
+
return {
|
|
20
|
+
jsonrpc: "2.0",
|
|
21
|
+
id: request?.id ?? null,
|
|
22
|
+
error: {
|
|
23
|
+
code: -32600,
|
|
24
|
+
message: 'Invalid Request: must include jsonrpc "2.0" and a string method'
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
18
28
|
try {
|
|
19
29
|
const result = await dispatch(request);
|
|
20
30
|
return { jsonrpc: "2.0", id: request.id, result };
|
|
21
31
|
} catch (err) {
|
|
22
32
|
const rpcErr = err;
|
|
33
|
+
if (typeof rpcErr.code === "number") {
|
|
34
|
+
return {
|
|
35
|
+
jsonrpc: "2.0",
|
|
36
|
+
id: request.id,
|
|
37
|
+
error: { code: rpcErr.code, message: rpcErr.message ?? "Error", data: rpcErr.data }
|
|
38
|
+
};
|
|
39
|
+
}
|
|
23
40
|
return {
|
|
24
41
|
jsonrpc: "2.0",
|
|
25
42
|
id: request.id,
|
|
26
|
-
error: {
|
|
27
|
-
code: rpcErr.code ?? -32603,
|
|
28
|
-
message: rpcErr.message ?? "Internal error",
|
|
29
|
-
data: rpcErr.data
|
|
30
|
-
}
|
|
43
|
+
error: { code: -32603, message: "Internal error" }
|
|
31
44
|
};
|
|
32
45
|
}
|
|
33
46
|
}
|
|
@@ -55,12 +68,22 @@ function createMCPServer(config) {
|
|
|
55
68
|
}))
|
|
56
69
|
};
|
|
57
70
|
case "tools/call": {
|
|
58
|
-
const params = request.params;
|
|
59
|
-
const
|
|
71
|
+
const params = asParamsObject(request.params, "tools/call");
|
|
72
|
+
const name = params.name;
|
|
73
|
+
if (typeof name !== "string") {
|
|
74
|
+
throw makeError(-32602, 'tools/call requires a string "name"');
|
|
75
|
+
}
|
|
76
|
+
const tool = toolMap.get(name);
|
|
60
77
|
if (!tool) {
|
|
61
|
-
throw makeError(-32602, `Unknown tool: ${
|
|
78
|
+
throw makeError(-32602, `Unknown tool: ${name}`);
|
|
79
|
+
}
|
|
80
|
+
const args = params.arguments ?? {};
|
|
81
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) {
|
|
82
|
+
throw makeError(-32602, "tool arguments must be an object");
|
|
62
83
|
}
|
|
63
|
-
|
|
84
|
+
const argRecord = args;
|
|
85
|
+
validateAgainstSchema(tool.definition.inputSchema, argRecord, name);
|
|
86
|
+
return tool.handler(argRecord);
|
|
64
87
|
}
|
|
65
88
|
case "resources/list":
|
|
66
89
|
return {
|
|
@@ -72,12 +95,16 @@ function createMCPServer(config) {
|
|
|
72
95
|
}))
|
|
73
96
|
};
|
|
74
97
|
case "resources/read": {
|
|
75
|
-
const params = request.params;
|
|
76
|
-
const
|
|
98
|
+
const params = asParamsObject(request.params, "resources/read");
|
|
99
|
+
const uri = params.uri;
|
|
100
|
+
if (typeof uri !== "string") {
|
|
101
|
+
throw makeError(-32602, 'resources/read requires a string "uri"');
|
|
102
|
+
}
|
|
103
|
+
const resource = findResource(uri);
|
|
77
104
|
if (!resource) {
|
|
78
|
-
throw makeError(-32602, `Unknown resource: ${
|
|
105
|
+
throw makeError(-32602, `Unknown resource: ${uri}`);
|
|
79
106
|
}
|
|
80
|
-
const content = await resource.handler(
|
|
107
|
+
const content = await resource.handler(uri);
|
|
81
108
|
return { contents: [content] };
|
|
82
109
|
}
|
|
83
110
|
case "prompts/list":
|
|
@@ -89,12 +116,17 @@ function createMCPServer(config) {
|
|
|
89
116
|
}))
|
|
90
117
|
};
|
|
91
118
|
case "prompts/get": {
|
|
92
|
-
const params = request.params;
|
|
93
|
-
const
|
|
119
|
+
const params = asParamsObject(request.params, "prompts/get");
|
|
120
|
+
const name = params.name;
|
|
121
|
+
if (typeof name !== "string") {
|
|
122
|
+
throw makeError(-32602, 'prompts/get requires a string "name"');
|
|
123
|
+
}
|
|
124
|
+
const prompt = promptMap.get(name);
|
|
94
125
|
if (!prompt) {
|
|
95
|
-
throw makeError(-32602, `Unknown prompt: ${
|
|
126
|
+
throw makeError(-32602, `Unknown prompt: ${name}`);
|
|
96
127
|
}
|
|
97
|
-
|
|
128
|
+
const args = params.arguments ?? {};
|
|
129
|
+
return prompt.handler(args);
|
|
98
130
|
}
|
|
99
131
|
case "ping":
|
|
100
132
|
return {};
|
|
@@ -121,10 +153,46 @@ function matchesTemplate(pattern, uri) {
|
|
|
121
153
|
function makeError(code, message) {
|
|
122
154
|
return { code, message };
|
|
123
155
|
}
|
|
156
|
+
function asParamsObject(params, ctx) {
|
|
157
|
+
if (params === null || typeof params !== "object" || Array.isArray(params)) {
|
|
158
|
+
throw makeError(-32602, `Invalid params for ${ctx}: expected an object`);
|
|
159
|
+
}
|
|
160
|
+
return params;
|
|
161
|
+
}
|
|
162
|
+
function validateAgainstSchema(schema, args, toolName) {
|
|
163
|
+
if (!schema || typeof schema !== "object") return;
|
|
164
|
+
const s = schema;
|
|
165
|
+
if (Array.isArray(s.required)) {
|
|
166
|
+
for (const key of s.required) {
|
|
167
|
+
if (typeof key === "string" && !(key in args)) {
|
|
168
|
+
throw makeError(-32602, `Missing required argument "${key}" for tool "${toolName}"`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (s.properties && typeof s.properties === "object") {
|
|
173
|
+
for (const [key, prop] of Object.entries(s.properties)) {
|
|
174
|
+
const value = args[key];
|
|
175
|
+
if (value === void 0 || value === null) continue;
|
|
176
|
+
const expected = prop?.type;
|
|
177
|
+
if (!expected) continue;
|
|
178
|
+
const ok = expected === "number" || expected === "integer" ? typeof value === "number" : expected === "array" ? Array.isArray(value) : expected === "object" ? typeof value === "object" && !Array.isArray(value) : typeof value === expected;
|
|
179
|
+
if (!ok) {
|
|
180
|
+
throw makeError(
|
|
181
|
+
-32602,
|
|
182
|
+
`Argument "${key}" for tool "${toolName}" must be of type ${expected}`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
124
188
|
|
|
125
189
|
// src/mcp/client.ts
|
|
126
190
|
function createMCPClient(config) {
|
|
127
191
|
let requestId = 0;
|
|
192
|
+
const parsedUrl = new URL(config.url);
|
|
193
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
194
|
+
throw new Error(`MCP client URL must be http(s): ${config.url}`);
|
|
195
|
+
}
|
|
128
196
|
async function rpc(method, params) {
|
|
129
197
|
const request = {
|
|
130
198
|
jsonrpc: "2.0",
|
|
@@ -139,6 +207,10 @@ function createMCPClient(config) {
|
|
|
139
207
|
...config.headers
|
|
140
208
|
},
|
|
141
209
|
body: JSON.stringify(request),
|
|
210
|
+
// never follow redirects: a 3xx to another host would otherwise re-send
|
|
211
|
+
// config.headers (often a bearer token) to that host, leaking the
|
|
212
|
+
// credential and enabling an ssrf pivot.
|
|
213
|
+
redirect: "error",
|
|
142
214
|
signal: AbortSignal.timeout(3e4)
|
|
143
215
|
});
|
|
144
216
|
if (!response.ok) {
|
|
@@ -211,16 +283,45 @@ function createMCPClient(config) {
|
|
|
211
283
|
|
|
212
284
|
// src/mcp/handler.ts
|
|
213
285
|
function createMCPHandler(server, options) {
|
|
214
|
-
const corsOrigin = options?.corsOrigin
|
|
286
|
+
const corsOrigin = options?.corsOrigin;
|
|
287
|
+
const allowedOrigins = options?.allowedOrigins;
|
|
288
|
+
const authorize = options?.authorize;
|
|
215
289
|
return async (req, res) => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
290
|
+
if (corsOrigin) {
|
|
291
|
+
res.setHeader("Access-Control-Allow-Origin", corsOrigin);
|
|
292
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
293
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
294
|
+
}
|
|
219
295
|
if (req.method === "OPTIONS") {
|
|
220
296
|
res.writeHead(204);
|
|
221
297
|
res.end();
|
|
222
298
|
return;
|
|
223
299
|
}
|
|
300
|
+
const origin = req.headers.origin;
|
|
301
|
+
if (allowedOrigins && origin !== void 0 && !allowedOrigins.includes(origin)) {
|
|
302
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
303
|
+
res.end("Forbidden");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (authorize) {
|
|
307
|
+
let allowed = false;
|
|
308
|
+
try {
|
|
309
|
+
allowed = await authorize(req);
|
|
310
|
+
} catch {
|
|
311
|
+
allowed = false;
|
|
312
|
+
}
|
|
313
|
+
if (!allowed) {
|
|
314
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
315
|
+
res.end(
|
|
316
|
+
JSON.stringify({
|
|
317
|
+
jsonrpc: "2.0",
|
|
318
|
+
id: null,
|
|
319
|
+
error: { code: -32600, message: "Unauthorized" }
|
|
320
|
+
})
|
|
321
|
+
);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
224
325
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
225
326
|
if (url.pathname.endsWith("/sse") && req.method === "GET") {
|
|
226
327
|
handleSSE(server, req, res);
|
|
@@ -242,6 +343,18 @@ async function handlePost(server, req, res) {
|
|
|
242
343
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
243
344
|
res.end(JSON.stringify(response));
|
|
244
345
|
} catch (err) {
|
|
346
|
+
const errObj = err;
|
|
347
|
+
if (errObj.statusCode === 413) {
|
|
348
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
349
|
+
res.end(
|
|
350
|
+
JSON.stringify({
|
|
351
|
+
jsonrpc: "2.0",
|
|
352
|
+
id: null,
|
|
353
|
+
error: { code: -32600, message: "Payload too large" }
|
|
354
|
+
})
|
|
355
|
+
);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
245
358
|
const data = err instanceof SyntaxError ? err.message : "Invalid request";
|
|
246
359
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
247
360
|
res.end(
|
|
@@ -275,10 +388,20 @@ data: ${endpointUrl}
|
|
|
275
388
|
clearInterval(keepAlive);
|
|
276
389
|
});
|
|
277
390
|
}
|
|
278
|
-
|
|
391
|
+
var DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
|
|
392
|
+
function readBody(req, maxSize = DEFAULT_MAX_BODY_SIZE) {
|
|
279
393
|
return new Promise((resolve, reject) => {
|
|
280
394
|
const chunks = [];
|
|
281
|
-
|
|
395
|
+
let size = 0;
|
|
396
|
+
req.on("data", (chunk) => {
|
|
397
|
+
size += chunk.length;
|
|
398
|
+
if (size > maxSize) {
|
|
399
|
+
req.removeAllListeners("data");
|
|
400
|
+
reject(Object.assign(new Error("Payload too large"), { statusCode: 413 }));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
chunks.push(chunk);
|
|
404
|
+
});
|
|
282
405
|
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
283
406
|
req.on("error", reject);
|
|
284
407
|
});
|