@matthesketh/utopia-ai 0.7.1 → 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/mcp/index.cjs +106 -17
- package/dist/mcp/index.d.cts +13 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +106 -17
- 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/mcp/index.cjs
CHANGED
|
@@ -58,14 +58,17 @@ function createMCPServer(config) {
|
|
|
58
58
|
return { jsonrpc: "2.0", id: request.id, result };
|
|
59
59
|
} catch (err) {
|
|
60
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
|
+
}
|
|
61
68
|
return {
|
|
62
69
|
jsonrpc: "2.0",
|
|
63
70
|
id: request.id,
|
|
64
|
-
error: {
|
|
65
|
-
code: rpcErr.code ?? -32603,
|
|
66
|
-
message: rpcErr.message ?? "Internal error",
|
|
67
|
-
data: rpcErr.data
|
|
68
|
-
}
|
|
71
|
+
error: { code: -32603, message: "Internal error" }
|
|
69
72
|
};
|
|
70
73
|
}
|
|
71
74
|
}
|
|
@@ -93,12 +96,22 @@ function createMCPServer(config) {
|
|
|
93
96
|
}))
|
|
94
97
|
};
|
|
95
98
|
case "tools/call": {
|
|
96
|
-
const params = request.params;
|
|
97
|
-
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);
|
|
98
105
|
if (!tool) {
|
|
99
|
-
throw makeError(-32602, `Unknown tool: ${
|
|
106
|
+
throw makeError(-32602, `Unknown tool: ${name}`);
|
|
100
107
|
}
|
|
101
|
-
|
|
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");
|
|
111
|
+
}
|
|
112
|
+
const argRecord = args;
|
|
113
|
+
validateAgainstSchema(tool.definition.inputSchema, argRecord, name);
|
|
114
|
+
return tool.handler(argRecord);
|
|
102
115
|
}
|
|
103
116
|
case "resources/list":
|
|
104
117
|
return {
|
|
@@ -110,12 +123,16 @@ function createMCPServer(config) {
|
|
|
110
123
|
}))
|
|
111
124
|
};
|
|
112
125
|
case "resources/read": {
|
|
113
|
-
const params = request.params;
|
|
114
|
-
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);
|
|
115
132
|
if (!resource) {
|
|
116
|
-
throw makeError(-32602, `Unknown resource: ${
|
|
133
|
+
throw makeError(-32602, `Unknown resource: ${uri}`);
|
|
117
134
|
}
|
|
118
|
-
const content = await resource.handler(
|
|
135
|
+
const content = await resource.handler(uri);
|
|
119
136
|
return { contents: [content] };
|
|
120
137
|
}
|
|
121
138
|
case "prompts/list":
|
|
@@ -127,12 +144,17 @@ function createMCPServer(config) {
|
|
|
127
144
|
}))
|
|
128
145
|
};
|
|
129
146
|
case "prompts/get": {
|
|
130
|
-
const params = request.params;
|
|
131
|
-
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);
|
|
132
153
|
if (!prompt) {
|
|
133
|
-
throw makeError(-32602, `Unknown prompt: ${
|
|
154
|
+
throw makeError(-32602, `Unknown prompt: ${name}`);
|
|
134
155
|
}
|
|
135
|
-
|
|
156
|
+
const args = params.arguments ?? {};
|
|
157
|
+
return prompt.handler(args);
|
|
136
158
|
}
|
|
137
159
|
case "ping":
|
|
138
160
|
return {};
|
|
@@ -159,10 +181,46 @@ function matchesTemplate(pattern, uri) {
|
|
|
159
181
|
function makeError(code, message) {
|
|
160
182
|
return { code, message };
|
|
161
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
|
+
}
|
|
162
216
|
|
|
163
217
|
// src/mcp/client.ts
|
|
164
218
|
function createMCPClient(config) {
|
|
165
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
|
+
}
|
|
166
224
|
async function rpc(method, params) {
|
|
167
225
|
const request = {
|
|
168
226
|
jsonrpc: "2.0",
|
|
@@ -177,6 +235,10 @@ function createMCPClient(config) {
|
|
|
177
235
|
...config.headers
|
|
178
236
|
},
|
|
179
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",
|
|
180
242
|
signal: AbortSignal.timeout(3e4)
|
|
181
243
|
});
|
|
182
244
|
if (!response.ok) {
|
|
@@ -250,6 +312,8 @@ function createMCPClient(config) {
|
|
|
250
312
|
// src/mcp/handler.ts
|
|
251
313
|
function createMCPHandler(server, options) {
|
|
252
314
|
const corsOrigin = options?.corsOrigin;
|
|
315
|
+
const allowedOrigins = options?.allowedOrigins;
|
|
316
|
+
const authorize = options?.authorize;
|
|
253
317
|
return async (req, res) => {
|
|
254
318
|
if (corsOrigin) {
|
|
255
319
|
res.setHeader("Access-Control-Allow-Origin", corsOrigin);
|
|
@@ -261,6 +325,31 @@ function createMCPHandler(server, options) {
|
|
|
261
325
|
res.end();
|
|
262
326
|
return;
|
|
263
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
|
+
}
|
|
264
353
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
265
354
|
if (url.pathname.endsWith("/sse") && req.method === "GET") {
|
|
266
355
|
handleSSE(server, req, res);
|
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
|
@@ -30,14 +30,17 @@ function createMCPServer(config) {
|
|
|
30
30
|
return { jsonrpc: "2.0", id: request.id, result };
|
|
31
31
|
} catch (err) {
|
|
32
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
|
+
}
|
|
33
40
|
return {
|
|
34
41
|
jsonrpc: "2.0",
|
|
35
42
|
id: request.id,
|
|
36
|
-
error: {
|
|
37
|
-
code: rpcErr.code ?? -32603,
|
|
38
|
-
message: rpcErr.message ?? "Internal error",
|
|
39
|
-
data: rpcErr.data
|
|
40
|
-
}
|
|
43
|
+
error: { code: -32603, message: "Internal error" }
|
|
41
44
|
};
|
|
42
45
|
}
|
|
43
46
|
}
|
|
@@ -65,12 +68,22 @@ function createMCPServer(config) {
|
|
|
65
68
|
}))
|
|
66
69
|
};
|
|
67
70
|
case "tools/call": {
|
|
68
|
-
const params = request.params;
|
|
69
|
-
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);
|
|
70
77
|
if (!tool) {
|
|
71
|
-
throw makeError(-32602, `Unknown tool: ${
|
|
78
|
+
throw makeError(-32602, `Unknown tool: ${name}`);
|
|
72
79
|
}
|
|
73
|
-
|
|
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");
|
|
83
|
+
}
|
|
84
|
+
const argRecord = args;
|
|
85
|
+
validateAgainstSchema(tool.definition.inputSchema, argRecord, name);
|
|
86
|
+
return tool.handler(argRecord);
|
|
74
87
|
}
|
|
75
88
|
case "resources/list":
|
|
76
89
|
return {
|
|
@@ -82,12 +95,16 @@ function createMCPServer(config) {
|
|
|
82
95
|
}))
|
|
83
96
|
};
|
|
84
97
|
case "resources/read": {
|
|
85
|
-
const params = request.params;
|
|
86
|
-
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);
|
|
87
104
|
if (!resource) {
|
|
88
|
-
throw makeError(-32602, `Unknown resource: ${
|
|
105
|
+
throw makeError(-32602, `Unknown resource: ${uri}`);
|
|
89
106
|
}
|
|
90
|
-
const content = await resource.handler(
|
|
107
|
+
const content = await resource.handler(uri);
|
|
91
108
|
return { contents: [content] };
|
|
92
109
|
}
|
|
93
110
|
case "prompts/list":
|
|
@@ -99,12 +116,17 @@ function createMCPServer(config) {
|
|
|
99
116
|
}))
|
|
100
117
|
};
|
|
101
118
|
case "prompts/get": {
|
|
102
|
-
const params = request.params;
|
|
103
|
-
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);
|
|
104
125
|
if (!prompt) {
|
|
105
|
-
throw makeError(-32602, `Unknown prompt: ${
|
|
126
|
+
throw makeError(-32602, `Unknown prompt: ${name}`);
|
|
106
127
|
}
|
|
107
|
-
|
|
128
|
+
const args = params.arguments ?? {};
|
|
129
|
+
return prompt.handler(args);
|
|
108
130
|
}
|
|
109
131
|
case "ping":
|
|
110
132
|
return {};
|
|
@@ -131,10 +153,46 @@ function matchesTemplate(pattern, uri) {
|
|
|
131
153
|
function makeError(code, message) {
|
|
132
154
|
return { code, message };
|
|
133
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
|
+
}
|
|
134
188
|
|
|
135
189
|
// src/mcp/client.ts
|
|
136
190
|
function createMCPClient(config) {
|
|
137
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
|
+
}
|
|
138
196
|
async function rpc(method, params) {
|
|
139
197
|
const request = {
|
|
140
198
|
jsonrpc: "2.0",
|
|
@@ -149,6 +207,10 @@ function createMCPClient(config) {
|
|
|
149
207
|
...config.headers
|
|
150
208
|
},
|
|
151
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",
|
|
152
214
|
signal: AbortSignal.timeout(3e4)
|
|
153
215
|
});
|
|
154
216
|
if (!response.ok) {
|
|
@@ -222,6 +284,8 @@ function createMCPClient(config) {
|
|
|
222
284
|
// src/mcp/handler.ts
|
|
223
285
|
function createMCPHandler(server, options) {
|
|
224
286
|
const corsOrigin = options?.corsOrigin;
|
|
287
|
+
const allowedOrigins = options?.allowedOrigins;
|
|
288
|
+
const authorize = options?.authorize;
|
|
225
289
|
return async (req, res) => {
|
|
226
290
|
if (corsOrigin) {
|
|
227
291
|
res.setHeader("Access-Control-Allow-Origin", corsOrigin);
|
|
@@ -233,6 +297,31 @@ function createMCPHandler(server, options) {
|
|
|
233
297
|
res.end();
|
|
234
298
|
return;
|
|
235
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
|
+
}
|
|
236
325
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
237
326
|
if (url.pathname.endsWith("/sse") && req.method === "GET") {
|
|
238
327
|
handleSSE(server, req, res);
|