@prefecthq/fastmcp-ts 0.0.2-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +309 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/cli/index.cjs +45522 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/client.d.ts +543 -0
- package/dist/client.js +1758 -0
- package/dist/client.js.map +1 -0
- package/dist/server.d.ts +1023 -0
- package/dist/server.js +2863 -0
- package/dist/server.js.map +1 -0
- package/dist/zod-P5QUYVPB.js +14015 -0
- package/dist/zod-P5QUYVPB.js.map +1 -0
- package/package.json +110 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,2863 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__export
|
|
3
|
+
} from "./chunk-PZ5AY32C.js";
|
|
4
|
+
|
|
5
|
+
// src/server/FastMCP.ts
|
|
6
|
+
import { Server } from "@modelcontextprotocol/sdk/server";
|
|
7
|
+
import {
|
|
8
|
+
ListToolsRequestSchema,
|
|
9
|
+
CallToolRequestSchema,
|
|
10
|
+
ListResourcesRequestSchema,
|
|
11
|
+
ListResourceTemplatesRequestSchema,
|
|
12
|
+
ReadResourceRequestSchema,
|
|
13
|
+
ListPromptsRequestSchema,
|
|
14
|
+
GetPromptRequestSchema,
|
|
15
|
+
McpError as McpError3,
|
|
16
|
+
ErrorCode as ErrorCode3
|
|
17
|
+
} from "@modelcontextprotocol/sdk/types";
|
|
18
|
+
import { randomUUID } from "crypto";
|
|
19
|
+
|
|
20
|
+
// src/server/auth/types.ts
|
|
21
|
+
var AuthorizationError = class extends Error {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "AuthorizationError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// src/server/context.ts
|
|
29
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
30
|
+
var contextStore = new AsyncLocalStorage();
|
|
31
|
+
var SESSION_CLOSE_CALLBACKS_KEY = "__fastmcp_session_close_callbacks";
|
|
32
|
+
function createContext(server, requestId, progressToken, auth, sessionState) {
|
|
33
|
+
async function log(level, message, loggerName) {
|
|
34
|
+
await server.sendLoggingMessage({
|
|
35
|
+
level,
|
|
36
|
+
data: message,
|
|
37
|
+
...loggerName !== void 0 ? { logger: loggerName } : {}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
auth,
|
|
42
|
+
requestId,
|
|
43
|
+
log,
|
|
44
|
+
debug: (msg, logger) => log("debug", msg, logger),
|
|
45
|
+
info: (msg, logger) => log("info", msg, logger),
|
|
46
|
+
notice: (msg, logger) => log("notice", msg, logger),
|
|
47
|
+
warning: (msg, logger) => log("warning", msg, logger),
|
|
48
|
+
error: (msg, logger) => log("error", msg, logger),
|
|
49
|
+
critical: (msg, logger) => log("critical", msg, logger),
|
|
50
|
+
alert: (msg, logger) => log("alert", msg, logger),
|
|
51
|
+
emergency: (msg, logger) => log("emergency", msg, logger),
|
|
52
|
+
async reportProgress(progress, total, message) {
|
|
53
|
+
if (progressToken === void 0) return;
|
|
54
|
+
await server.notification({
|
|
55
|
+
method: "notifications/progress",
|
|
56
|
+
params: {
|
|
57
|
+
progressToken,
|
|
58
|
+
progress,
|
|
59
|
+
...total !== void 0 ? { total } : {},
|
|
60
|
+
...message !== void 0 ? { message } : {}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
async sample(params) {
|
|
65
|
+
const caps = server.getClientCapabilities();
|
|
66
|
+
if (!caps?.sampling) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"[fastmcp] Client does not support sampling. Ensure the client advertises the sampling capability before calling ctx.sample()."
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const result = await server.createMessage({
|
|
72
|
+
messages: params.messages,
|
|
73
|
+
maxTokens: params.maxTokens ?? 1024,
|
|
74
|
+
...params.systemPrompt !== void 0 ? { systemPrompt: params.systemPrompt } : {},
|
|
75
|
+
...params.temperature !== void 0 ? { temperature: params.temperature } : {},
|
|
76
|
+
...params.stopSequences !== void 0 ? { stopSequences: params.stopSequences } : {},
|
|
77
|
+
...params.modelPreferences !== void 0 ? { modelPreferences: params.modelPreferences } : {}
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
role: "assistant",
|
|
81
|
+
content: result.content,
|
|
82
|
+
model: result.model,
|
|
83
|
+
stopReason: result.stopReason
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
async elicit(message, schema) {
|
|
87
|
+
const caps = server.getClientCapabilities();
|
|
88
|
+
if (!caps?.elicitation) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"[fastmcp] Client does not support elicitation. Ensure the client advertises the elicitation capability before calling ctx.elicit()."
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const result = await server.elicitInput({
|
|
94
|
+
message,
|
|
95
|
+
// Cast: our simplified ElicitationSchema is a subset of the SDK's PrimitiveSchemaDefinition union
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
97
|
+
requestedSchema: schema
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
action: result.action,
|
|
101
|
+
content: result.content
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
async listRoots() {
|
|
105
|
+
const caps = server.getClientCapabilities();
|
|
106
|
+
if (!caps?.roots) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
"[fastmcp] Client does not support roots. Ensure the client advertises the roots capability before calling ctx.listRoots()."
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const result = await server.listRoots();
|
|
112
|
+
return result.roots;
|
|
113
|
+
},
|
|
114
|
+
getState: (key) => sessionState.get(key),
|
|
115
|
+
setState: (key, value) => {
|
|
116
|
+
sessionState.set(key, value);
|
|
117
|
+
},
|
|
118
|
+
deleteState: (key) => {
|
|
119
|
+
sessionState.delete(key);
|
|
120
|
+
},
|
|
121
|
+
resolveToolName: (name) => name,
|
|
122
|
+
onClose(callback) {
|
|
123
|
+
const existing = sessionState.get(SESSION_CLOSE_CALLBACKS_KEY) ?? [];
|
|
124
|
+
sessionState.set(SESSION_CLOSE_CALLBACKS_KEY, [...existing, callback]);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/server/middleware.ts
|
|
130
|
+
import { McpError, ErrorCode, CancelledNotificationSchema } from "@modelcontextprotocol/sdk/types";
|
|
131
|
+
var METHOD_HOOK_KEY = {
|
|
132
|
+
"tools/call": "onCallTool",
|
|
133
|
+
"tools/list": "onListTools",
|
|
134
|
+
"resources/read": "onReadResource",
|
|
135
|
+
"resources/list": "onListResources",
|
|
136
|
+
"resources/templates/list": "onListResourceTemplates",
|
|
137
|
+
"prompts/get": "onGetPrompt",
|
|
138
|
+
"prompts/list": "onListPrompts"
|
|
139
|
+
};
|
|
140
|
+
function selectHook(mw, method) {
|
|
141
|
+
const key = METHOD_HOOK_KEY[method];
|
|
142
|
+
const specific = key ? mw[key] : void 0;
|
|
143
|
+
if (specific) return specific.bind(mw);
|
|
144
|
+
if (mw.onRequest) return mw.onRequest.bind(mw);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
function runMiddlewareChain(middleware, method, request, mcpContext, handler) {
|
|
148
|
+
const hooks = [];
|
|
149
|
+
for (const mw of middleware) {
|
|
150
|
+
const hook = selectHook(mw, method);
|
|
151
|
+
if (hook) hooks.push(hook);
|
|
152
|
+
}
|
|
153
|
+
const ctx = { method, request, mcpContext };
|
|
154
|
+
function dispatch(i) {
|
|
155
|
+
if (i >= hooks.length) return handler();
|
|
156
|
+
return hooks[i](ctx, () => dispatch(i + 1));
|
|
157
|
+
}
|
|
158
|
+
return dispatch(0);
|
|
159
|
+
}
|
|
160
|
+
var LoggingMiddleware = class {
|
|
161
|
+
constructor(emit = console.log) {
|
|
162
|
+
this.emit = emit;
|
|
163
|
+
}
|
|
164
|
+
emit;
|
|
165
|
+
async onRequest(ctx, next) {
|
|
166
|
+
const t0 = Date.now();
|
|
167
|
+
this.emit(`[fastmcp] \u2192 ${ctx.method}`);
|
|
168
|
+
try {
|
|
169
|
+
const result = await next();
|
|
170
|
+
this.emit(`[fastmcp] \u2190 ${ctx.method} (${Date.now() - t0}ms)`);
|
|
171
|
+
return result;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
this.emit(
|
|
174
|
+
`[fastmcp] \u2717 ${ctx.method} (${Date.now() - t0}ms): ${err instanceof Error ? err.message : String(err)}`
|
|
175
|
+
);
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
var CachingMiddleware = class {
|
|
181
|
+
constructor(ttl = 6e4, _keyFn) {
|
|
182
|
+
this.ttl = ttl;
|
|
183
|
+
this._keyFn = _keyFn;
|
|
184
|
+
}
|
|
185
|
+
ttl;
|
|
186
|
+
_keyFn;
|
|
187
|
+
_cache = /* @__PURE__ */ new Map();
|
|
188
|
+
async onRequest(ctx, next) {
|
|
189
|
+
const key = this._keyFn ? this._keyFn(ctx) : `${ctx.method}:${JSON.stringify(ctx.request)}`;
|
|
190
|
+
const entry = this._cache.get(key);
|
|
191
|
+
if (entry && entry.expiresAt > Date.now()) return entry.value;
|
|
192
|
+
const result = await next();
|
|
193
|
+
this._cache.set(key, { value: result, expiresAt: Date.now() + this.ttl });
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
var RateLimitingMiddleware = class {
|
|
198
|
+
constructor(limit, windowMs = 6e4) {
|
|
199
|
+
this.limit = limit;
|
|
200
|
+
this.windowMs = windowMs;
|
|
201
|
+
this._tokens = limit;
|
|
202
|
+
this._lastRefill = Date.now();
|
|
203
|
+
}
|
|
204
|
+
limit;
|
|
205
|
+
windowMs;
|
|
206
|
+
_tokens;
|
|
207
|
+
_lastRefill;
|
|
208
|
+
async onRequest(ctx, next) {
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
if (now - this._lastRefill >= this.windowMs) {
|
|
211
|
+
this._tokens = this.limit;
|
|
212
|
+
this._lastRefill = now;
|
|
213
|
+
}
|
|
214
|
+
if (this._tokens <= 0) {
|
|
215
|
+
throw new McpError(ErrorCode.InvalidRequest, "Rate limit exceeded");
|
|
216
|
+
}
|
|
217
|
+
this._tokens--;
|
|
218
|
+
return next();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
var SizeLimitingMiddleware = class {
|
|
222
|
+
constructor(maxBytes) {
|
|
223
|
+
this.maxBytes = maxBytes;
|
|
224
|
+
}
|
|
225
|
+
maxBytes;
|
|
226
|
+
async onRequest(ctx, next) {
|
|
227
|
+
const result = await next();
|
|
228
|
+
const size = Buffer.byteLength(JSON.stringify(result), "utf8");
|
|
229
|
+
if (size > this.maxBytes) {
|
|
230
|
+
throw new McpError(
|
|
231
|
+
ErrorCode.InternalError,
|
|
232
|
+
`Response size (${size} bytes) exceeds limit (${this.maxBytes} bytes)`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
var ErrorNormalizationMiddleware = class {
|
|
239
|
+
async onCallTool(ctx, next) {
|
|
240
|
+
try {
|
|
241
|
+
return await next();
|
|
242
|
+
} catch (err) {
|
|
243
|
+
if (err instanceof McpError) throw err;
|
|
244
|
+
return {
|
|
245
|
+
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
|
|
246
|
+
isError: true
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async onRequest(ctx, next) {
|
|
251
|
+
try {
|
|
252
|
+
return await next();
|
|
253
|
+
} catch (err) {
|
|
254
|
+
if (err instanceof McpError) throw err;
|
|
255
|
+
throw new McpError(
|
|
256
|
+
ErrorCode.InternalError,
|
|
257
|
+
err instanceof Error ? err.message : String(err)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
var CancellationMiddleware = class {
|
|
263
|
+
_inFlight = /* @__PURE__ */ new Map();
|
|
264
|
+
setup(server) {
|
|
265
|
+
server.setNotificationHandler(CancelledNotificationSchema, (notification) => {
|
|
266
|
+
const requestId = String(notification.params.requestId);
|
|
267
|
+
this._inFlight.get(requestId)?.abort();
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
async onRequest(ctx, next) {
|
|
271
|
+
const { requestId } = ctx.mcpContext;
|
|
272
|
+
if (!requestId) return next();
|
|
273
|
+
const controller = new AbortController();
|
|
274
|
+
this._inFlight.set(requestId, controller);
|
|
275
|
+
const abortPromise = new Promise((_, reject) => {
|
|
276
|
+
controller.signal.addEventListener(
|
|
277
|
+
"abort",
|
|
278
|
+
() => reject(new McpError(ErrorCode.InternalError, "Request was cancelled by the client"))
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
try {
|
|
282
|
+
return await Promise.race([next(), abortPromise]);
|
|
283
|
+
} finally {
|
|
284
|
+
this._inFlight.delete(requestId);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// src/server/transform.ts
|
|
290
|
+
function applyTransformChain(value, transforms, pick) {
|
|
291
|
+
let current = value;
|
|
292
|
+
for (const t of transforms) {
|
|
293
|
+
if (current === null) break;
|
|
294
|
+
const next = pick(t, current);
|
|
295
|
+
if (next !== void 0) current = next;
|
|
296
|
+
}
|
|
297
|
+
return current;
|
|
298
|
+
}
|
|
299
|
+
function renameTool(originalName, newName) {
|
|
300
|
+
return {
|
|
301
|
+
transformTool: (v) => v.name === originalName ? { ...v, name: newName } : v
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function redescribeTool(toolName, description) {
|
|
305
|
+
return {
|
|
306
|
+
transformTool: (v) => v.name === toolName ? { ...v, description } : v
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
var FilterTransform = class {
|
|
310
|
+
constructor(predicates) {
|
|
311
|
+
this.predicates = predicates;
|
|
312
|
+
}
|
|
313
|
+
predicates;
|
|
314
|
+
transformTool(v) {
|
|
315
|
+
return this.predicates.tools?.(v) ?? true ? v : null;
|
|
316
|
+
}
|
|
317
|
+
transformResource(v) {
|
|
318
|
+
return this.predicates.resources?.(v) ?? true ? v : null;
|
|
319
|
+
}
|
|
320
|
+
transformResourceTemplate(v) {
|
|
321
|
+
const pred = this.predicates.resourceTemplates ?? this.predicates.resources;
|
|
322
|
+
return pred?.(v) ?? true ? v : null;
|
|
323
|
+
}
|
|
324
|
+
transformPrompt(v) {
|
|
325
|
+
return this.predicates.prompts?.(v) ?? true ? v : null;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
var NamespaceTransform = class {
|
|
329
|
+
constructor(prefix) {
|
|
330
|
+
this.prefix = prefix;
|
|
331
|
+
}
|
|
332
|
+
prefix;
|
|
333
|
+
transformTool(v) {
|
|
334
|
+
return { ...v, name: `${this.prefix}${v.name}` };
|
|
335
|
+
}
|
|
336
|
+
transformResource(v) {
|
|
337
|
+
return { ...v, name: `${this.prefix}${v.name}` };
|
|
338
|
+
}
|
|
339
|
+
transformResourceTemplate(v) {
|
|
340
|
+
return { ...v, name: `${this.prefix}${v.name}` };
|
|
341
|
+
}
|
|
342
|
+
transformPrompt(v) {
|
|
343
|
+
return { ...v, name: `${this.prefix}${v.name}` };
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
var ResourcesAsTools = class {
|
|
347
|
+
synthesizeTools(resources) {
|
|
348
|
+
return [
|
|
349
|
+
{
|
|
350
|
+
name: "list_resources",
|
|
351
|
+
description: "List all available MCP resources",
|
|
352
|
+
inputSchema: { type: "object", properties: {} },
|
|
353
|
+
handler: () => resources.map((r) => ({ uri: r.uri, name: r.name, mimeType: r.mimeType }))
|
|
354
|
+
}
|
|
355
|
+
];
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
var PromptsAsTools = class {
|
|
359
|
+
synthesizeTools(_resources, prompts) {
|
|
360
|
+
return [
|
|
361
|
+
{
|
|
362
|
+
name: "list_prompts",
|
|
363
|
+
description: "List all available MCP prompts",
|
|
364
|
+
inputSchema: { type: "object", properties: {} },
|
|
365
|
+
handler: () => prompts.map((p) => ({ name: p.name, description: p.description }))
|
|
366
|
+
}
|
|
367
|
+
];
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
var VersionFilter = class {
|
|
371
|
+
constructor(version) {
|
|
372
|
+
this.version = version;
|
|
373
|
+
}
|
|
374
|
+
version;
|
|
375
|
+
_matches(tags) {
|
|
376
|
+
return tags.includes(this.version);
|
|
377
|
+
}
|
|
378
|
+
transformTool(v) {
|
|
379
|
+
return this._matches(v.tags) ? v : null;
|
|
380
|
+
}
|
|
381
|
+
transformResource(v) {
|
|
382
|
+
return this._matches(v.tags) ? v : null;
|
|
383
|
+
}
|
|
384
|
+
transformResourceTemplate(v) {
|
|
385
|
+
return this._matches(v.tags) ? v : null;
|
|
386
|
+
}
|
|
387
|
+
transformPrompt(v) {
|
|
388
|
+
return this._matches(v.tags) ? v : null;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// src/server/tool.ts
|
|
393
|
+
import { McpError as McpError2, ErrorCode as ErrorCode2 } from "@modelcontextprotocol/sdk/types";
|
|
394
|
+
var Image = class {
|
|
395
|
+
constructor(buffer, mimeType) {
|
|
396
|
+
this.buffer = buffer;
|
|
397
|
+
this.mimeType = mimeType;
|
|
398
|
+
}
|
|
399
|
+
buffer;
|
|
400
|
+
mimeType;
|
|
401
|
+
};
|
|
402
|
+
var File = class {
|
|
403
|
+
constructor(buffer, name, mimeType) {
|
|
404
|
+
this.buffer = buffer;
|
|
405
|
+
this.name = name;
|
|
406
|
+
this.mimeType = mimeType;
|
|
407
|
+
}
|
|
408
|
+
buffer;
|
|
409
|
+
name;
|
|
410
|
+
mimeType;
|
|
411
|
+
};
|
|
412
|
+
var ToolResult = class {
|
|
413
|
+
constructor(result) {
|
|
414
|
+
this.result = result;
|
|
415
|
+
}
|
|
416
|
+
result;
|
|
417
|
+
};
|
|
418
|
+
function convertResult(value) {
|
|
419
|
+
if (value instanceof ToolResult) {
|
|
420
|
+
return value.result;
|
|
421
|
+
}
|
|
422
|
+
if (value instanceof Image) {
|
|
423
|
+
return {
|
|
424
|
+
content: [
|
|
425
|
+
{
|
|
426
|
+
type: "image",
|
|
427
|
+
data: Buffer.from(value.buffer).toString("base64"),
|
|
428
|
+
mimeType: value.mimeType
|
|
429
|
+
}
|
|
430
|
+
]
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (value instanceof File) {
|
|
434
|
+
return {
|
|
435
|
+
content: [
|
|
436
|
+
{
|
|
437
|
+
type: "resource",
|
|
438
|
+
resource: {
|
|
439
|
+
uri: `file://${value.name}`,
|
|
440
|
+
mimeType: value.mimeType,
|
|
441
|
+
blob: Buffer.from(value.buffer).toString("base64")
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
]
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (value === void 0 || value === null) {
|
|
448
|
+
return { content: [] };
|
|
449
|
+
}
|
|
450
|
+
if (Array.isArray(value)) {
|
|
451
|
+
console.warn(
|
|
452
|
+
"[fastmcp] Tool returned an array \u2014 structuredContent will not be set (MCP spec requires a plain object). Wrap in an object or use ToolResult to suppress this warning."
|
|
453
|
+
);
|
|
454
|
+
return {
|
|
455
|
+
content: [{ type: "text", text: JSON.stringify(value) }]
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
if (typeof value === "object") {
|
|
459
|
+
const json = JSON.stringify(value);
|
|
460
|
+
return {
|
|
461
|
+
content: [{ type: "text", text: json }],
|
|
462
|
+
structuredContent: value
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
content: [{ type: "text", text: String(value) }]
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
async function toJsonSchema(schema, context) {
|
|
470
|
+
const maybeNative = schema;
|
|
471
|
+
if (typeof maybeNative.toJsonSchema === "function") {
|
|
472
|
+
try {
|
|
473
|
+
const result = maybeNative.toJsonSchema();
|
|
474
|
+
if (result !== null && typeof result === "object") {
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
} catch {
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
const { z } = await import("./zod-P5QUYVPB.js");
|
|
482
|
+
return z.toJSONSchema(
|
|
483
|
+
schema
|
|
484
|
+
);
|
|
485
|
+
} catch {
|
|
486
|
+
}
|
|
487
|
+
const where = context ? ` for ${context}` : "";
|
|
488
|
+
console.warn(
|
|
489
|
+
`[fastmcp] Could not auto-generate JSON schema${where}. Sending { type: 'object' } to clients. Provide an explicit 'inputSchema' in ToolConfig to suppress this warning.`
|
|
490
|
+
);
|
|
491
|
+
return { type: "object" };
|
|
492
|
+
}
|
|
493
|
+
async function validateInput(schema, input, throwAsProtocolError = false) {
|
|
494
|
+
const result = await schema["~standard"].validate(input);
|
|
495
|
+
if (result.issues) {
|
|
496
|
+
const messages = result.issues.map((i) => i.message).join("; ");
|
|
497
|
+
if (throwAsProtocolError) {
|
|
498
|
+
throw new McpError2(ErrorCode2.InvalidParams, `Validation failed: ${messages}`);
|
|
499
|
+
}
|
|
500
|
+
throw new Error(`Validation failed: ${messages}`);
|
|
501
|
+
}
|
|
502
|
+
return result.value;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/server/resource.ts
|
|
506
|
+
var ResourceResult = class {
|
|
507
|
+
constructor(contents) {
|
|
508
|
+
this.contents = contents;
|
|
509
|
+
}
|
|
510
|
+
contents;
|
|
511
|
+
};
|
|
512
|
+
function convertResourceResult(value, uri, mimeType) {
|
|
513
|
+
if (value instanceof ResourceResult) {
|
|
514
|
+
return { contents: value.contents };
|
|
515
|
+
}
|
|
516
|
+
if (value instanceof Buffer || value instanceof Uint8Array) {
|
|
517
|
+
return {
|
|
518
|
+
contents: [
|
|
519
|
+
{
|
|
520
|
+
uri,
|
|
521
|
+
mimeType: mimeType ?? "application/octet-stream",
|
|
522
|
+
blob: Buffer.from(value).toString("base64")
|
|
523
|
+
}
|
|
524
|
+
]
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
if (value === null || value === void 0) {
|
|
528
|
+
return { contents: [{ uri, mimeType: mimeType ?? "text/plain", text: "" }] };
|
|
529
|
+
}
|
|
530
|
+
if (typeof value === "object") {
|
|
531
|
+
return {
|
|
532
|
+
contents: [{ uri, mimeType: mimeType ?? "application/json", text: JSON.stringify(value) }]
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
return { contents: [{ uri, mimeType: mimeType ?? "text/plain", text: String(value) }] };
|
|
536
|
+
}
|
|
537
|
+
function isUriTemplate(uri) {
|
|
538
|
+
return uri.includes("{");
|
|
539
|
+
}
|
|
540
|
+
function matchTemplate(uriTemplate, uri) {
|
|
541
|
+
const qIdx = uri.indexOf("?");
|
|
542
|
+
const baseUri = qIdx >= 0 ? uri.slice(0, qIdx) : uri;
|
|
543
|
+
const queryString = qIdx >= 0 ? uri.slice(qIdx + 1) : "";
|
|
544
|
+
const queryParamNames = [];
|
|
545
|
+
const baseTemplate = uriTemplate.replace(/\{[?][^}]+\}/g, (match) => {
|
|
546
|
+
match.slice(2, -1).split(",").map((s) => s.trim()).forEach((n) => queryParamNames.push(n));
|
|
547
|
+
return "";
|
|
548
|
+
});
|
|
549
|
+
const names = [];
|
|
550
|
+
const segments = baseTemplate.split(/(\{[^}]+\})/g);
|
|
551
|
+
let pattern = "^";
|
|
552
|
+
for (const seg of segments) {
|
|
553
|
+
if (seg.startsWith("{") && seg.endsWith("}")) {
|
|
554
|
+
const inner = seg.slice(1, -1);
|
|
555
|
+
if (inner.endsWith("*")) {
|
|
556
|
+
names.push(inner.slice(0, -1));
|
|
557
|
+
pattern += "(.+)";
|
|
558
|
+
} else {
|
|
559
|
+
names.push(inner);
|
|
560
|
+
pattern += "([^/?]+)";
|
|
561
|
+
}
|
|
562
|
+
} else if (seg) {
|
|
563
|
+
pattern += seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
pattern += "$";
|
|
567
|
+
const pathMatch = baseUri.match(new RegExp(pattern));
|
|
568
|
+
if (!pathMatch) return null;
|
|
569
|
+
const params = {};
|
|
570
|
+
names.forEach((name, i) => {
|
|
571
|
+
params[name] = decodeURIComponent(pathMatch[i + 1]);
|
|
572
|
+
});
|
|
573
|
+
if (queryParamNames.length > 0 && queryString) {
|
|
574
|
+
const sp = new URLSearchParams(queryString);
|
|
575
|
+
for (const name of queryParamNames) {
|
|
576
|
+
const val = sp.get(name);
|
|
577
|
+
if (val !== null) params[name] = val;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return params;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/server/apps/types.ts
|
|
584
|
+
var UI_EXTENSION_KEY = "io.modelcontextprotocol/ui";
|
|
585
|
+
|
|
586
|
+
// src/server/prompt.ts
|
|
587
|
+
var PromptResult = class {
|
|
588
|
+
constructor(messages, description) {
|
|
589
|
+
this.messages = messages;
|
|
590
|
+
this.description = description;
|
|
591
|
+
}
|
|
592
|
+
messages;
|
|
593
|
+
description;
|
|
594
|
+
};
|
|
595
|
+
function isPromptMessage(value) {
|
|
596
|
+
if (value === null || typeof value !== "object") return false;
|
|
597
|
+
const v = value;
|
|
598
|
+
if (v.role !== "user" && v.role !== "assistant") return false;
|
|
599
|
+
if (!v.content || typeof v.content !== "object") return false;
|
|
600
|
+
return typeof v.content.type === "string";
|
|
601
|
+
}
|
|
602
|
+
function convertPromptResult(value) {
|
|
603
|
+
if (value instanceof PromptResult) {
|
|
604
|
+
return { description: value.description, messages: value.messages };
|
|
605
|
+
}
|
|
606
|
+
if (typeof value === "string") {
|
|
607
|
+
return { messages: [{ role: "user", content: { type: "text", text: value } }] };
|
|
608
|
+
}
|
|
609
|
+
if (isPromptMessage(value)) {
|
|
610
|
+
return { messages: [value] };
|
|
611
|
+
}
|
|
612
|
+
if (Array.isArray(value) && value.every(isPromptMessage)) {
|
|
613
|
+
return { messages: value };
|
|
614
|
+
}
|
|
615
|
+
throw new Error(
|
|
616
|
+
`Prompt handler returned an unsupported value. Expected string, PromptMessage, PromptMessage[], or PromptResult.`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/server/FastMCP.ts
|
|
621
|
+
function prefixResourceUri(uri, prefix) {
|
|
622
|
+
const idx = uri.indexOf("://");
|
|
623
|
+
if (idx === -1) return `${prefix}/${uri}`;
|
|
624
|
+
return `${uri.slice(0, idx + 3)}${prefix}/${uri.slice(idx + 3)}`;
|
|
625
|
+
}
|
|
626
|
+
function inferDescription(name) {
|
|
627
|
+
return name.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").trim().toLowerCase();
|
|
628
|
+
}
|
|
629
|
+
function toAccessToken(authInfo) {
|
|
630
|
+
if (!authInfo) return void 0;
|
|
631
|
+
return {
|
|
632
|
+
token: authInfo.token,
|
|
633
|
+
clientId: authInfo.clientId || void 0,
|
|
634
|
+
scopes: authInfo.scopes,
|
|
635
|
+
expiresAt: authInfo.expiresAt,
|
|
636
|
+
claims: authInfo.extra ?? {}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
var _cliEnvToken;
|
|
640
|
+
async function resolveCliEnvToken(verifier) {
|
|
641
|
+
if (!verifier) return void 0;
|
|
642
|
+
if (_cliEnvToken !== void 0) return _cliEnvToken ?? void 0;
|
|
643
|
+
const raw = process.env["FASTMCP_CLI_AUTH_TOKEN"];
|
|
644
|
+
if (!raw) {
|
|
645
|
+
_cliEnvToken = null;
|
|
646
|
+
return void 0;
|
|
647
|
+
}
|
|
648
|
+
try {
|
|
649
|
+
_cliEnvToken = await verifier.verify(raw);
|
|
650
|
+
return _cliEnvToken;
|
|
651
|
+
} catch {
|
|
652
|
+
_cliEnvToken = null;
|
|
653
|
+
return void 0;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function runAuthCheck(check, token) {
|
|
657
|
+
if (!token) throw new McpError3(ErrorCode3.InvalidRequest, "Authentication required");
|
|
658
|
+
try {
|
|
659
|
+
await check(token);
|
|
660
|
+
} catch (err) {
|
|
661
|
+
if (err instanceof AuthorizationError) {
|
|
662
|
+
throw new McpError3(ErrorCode3.InvalidRequest, err.message);
|
|
663
|
+
}
|
|
664
|
+
throw err;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
var FastMCP = class {
|
|
668
|
+
name;
|
|
669
|
+
version;
|
|
670
|
+
_auth;
|
|
671
|
+
_oauth;
|
|
672
|
+
_toolsPageSize;
|
|
673
|
+
_resourcesPageSize;
|
|
674
|
+
_tools = /* @__PURE__ */ new Map();
|
|
675
|
+
_staticResources = /* @__PURE__ */ new Map();
|
|
676
|
+
_templateResources = /* @__PURE__ */ new Map();
|
|
677
|
+
_prompts = /* @__PURE__ */ new Map();
|
|
678
|
+
_promptsPageSize;
|
|
679
|
+
_middleware;
|
|
680
|
+
_transforms;
|
|
681
|
+
_primaryState = /* @__PURE__ */ new Map();
|
|
682
|
+
_httpServer = null;
|
|
683
|
+
_address = null;
|
|
684
|
+
_sessions = /* @__PURE__ */ new Map();
|
|
685
|
+
// Primary server used by connect() and stdio
|
|
686
|
+
_primaryServer;
|
|
687
|
+
_toolRegisteredCallbacks = [];
|
|
688
|
+
_resourceRegisteredCallbacks = [];
|
|
689
|
+
_promptRegisteredCallbacks = [];
|
|
690
|
+
_proxyCloseCallbacks = [];
|
|
691
|
+
_mountedChildren = /* @__PURE__ */ new Set();
|
|
692
|
+
constructor(options) {
|
|
693
|
+
this.name = options.name;
|
|
694
|
+
this.version = options.version ?? "0.0.1";
|
|
695
|
+
this._auth = options.auth;
|
|
696
|
+
this._oauth = options.oauth;
|
|
697
|
+
this._toolsPageSize = options.toolsPageSize ?? 50;
|
|
698
|
+
this._resourcesPageSize = options.resourcesPageSize ?? 50;
|
|
699
|
+
this._promptsPageSize = options.promptsPageSize ?? 50;
|
|
700
|
+
this._middleware = options.middleware ? [...options.middleware] : [];
|
|
701
|
+
this._transforms = options.transforms ? [...options.transforms] : [];
|
|
702
|
+
this._primaryServer = this._makeServer();
|
|
703
|
+
}
|
|
704
|
+
_hasUiComponents() {
|
|
705
|
+
for (const t of this._tools.values()) {
|
|
706
|
+
if (t.config.ui) return true;
|
|
707
|
+
}
|
|
708
|
+
for (const r of this._staticResources.values()) {
|
|
709
|
+
if (r.config.ui || r.config.uri.startsWith("ui://")) return true;
|
|
710
|
+
}
|
|
711
|
+
for (const r of this._templateResources.values()) {
|
|
712
|
+
if (r.config.uri.startsWith("ui://")) return true;
|
|
713
|
+
}
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
/** Create a new Server instance with all request handlers wired up. */
|
|
717
|
+
_makeServer(sessionState) {
|
|
718
|
+
const state = sessionState ?? this._primaryState;
|
|
719
|
+
const extensions = this._hasUiComponents() ? { [UI_EXTENSION_KEY]: {} } : void 0;
|
|
720
|
+
const server = new Server(
|
|
721
|
+
{ name: this.name, version: this.version },
|
|
722
|
+
{ capabilities: { tools: { listChanged: true }, resources: { listChanged: true }, prompts: { listChanged: true }, logging: {}, ...extensions ? { extensions } : {} } }
|
|
723
|
+
);
|
|
724
|
+
for (const mw of this._middleware) mw.setup?.(server);
|
|
725
|
+
this._setupHandlers(server, state);
|
|
726
|
+
return server;
|
|
727
|
+
}
|
|
728
|
+
async _resolveToken(authInfo) {
|
|
729
|
+
return toAccessToken(authInfo) ?? await resolveCliEnvToken(this._auth);
|
|
730
|
+
}
|
|
731
|
+
_setupHandlers(server, sessionState) {
|
|
732
|
+
server.setRequestHandler(ListToolsRequestSchema, async (req, extra) => {
|
|
733
|
+
const token = await this._resolveToken(extra.authInfo);
|
|
734
|
+
const ctx = createContext(server, extra.requestId !== void 0 ? String(extra.requestId) : void 0, void 0, token, sessionState);
|
|
735
|
+
return contextStore.run(
|
|
736
|
+
ctx,
|
|
737
|
+
() => runMiddlewareChain(this._middleware, "tools/list", req.params, ctx, async () => {
|
|
738
|
+
const clientIsUiCapable = !!server.getClientCapabilities()?.extensions?.[UI_EXTENSION_KEY];
|
|
739
|
+
const allVisible = (await Promise.all(
|
|
740
|
+
[...this._tools.values()].map(async (t) => {
|
|
741
|
+
if (t.config.disabled) return null;
|
|
742
|
+
if (t.config.ui?.visibility && !t.config.ui.visibility.includes("model")) return null;
|
|
743
|
+
if (!t.config.auth) return t;
|
|
744
|
+
if (!token) return null;
|
|
745
|
+
try {
|
|
746
|
+
await t.config.auth(token);
|
|
747
|
+
return t;
|
|
748
|
+
} catch {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
})
|
|
752
|
+
)).filter((t) => t !== null);
|
|
753
|
+
const transformedTools = this._applyTransformsToTools(allVisible);
|
|
754
|
+
const { resourceViews, promptViews } = await this._getVisibleViews(token);
|
|
755
|
+
const synthesized = this._buildSynthesizedTools(resourceViews, promptViews);
|
|
756
|
+
const allEntries = [
|
|
757
|
+
...transformedTools.map(
|
|
758
|
+
(t) => ({ kind: "registered", name: t.name, title: t.title, description: t.description, config: t.config })
|
|
759
|
+
),
|
|
760
|
+
...synthesized.map(
|
|
761
|
+
(s) => ({ kind: "synthesized", name: s.name, title: s.title, description: s.description, inputSchema: s.inputSchema })
|
|
762
|
+
)
|
|
763
|
+
];
|
|
764
|
+
const pageSize = this._toolsPageSize;
|
|
765
|
+
const cursorName = req.params?.cursor ? Buffer.from(req.params.cursor, "base64url").toString() : null;
|
|
766
|
+
let startIdx = 0;
|
|
767
|
+
if (cursorName !== null) {
|
|
768
|
+
const idx = allEntries.findIndex((e) => e.name === cursorName);
|
|
769
|
+
if (idx < 0) throw new McpError3(ErrorCode3.InvalidParams, "Invalid or expired cursor");
|
|
770
|
+
startIdx = idx + 1;
|
|
771
|
+
}
|
|
772
|
+
const page = allEntries.slice(startIdx, startIdx + pageSize);
|
|
773
|
+
const nextCursor = startIdx + pageSize < allEntries.length ? Buffer.from(page[page.length - 1].name).toString("base64url") : void 0;
|
|
774
|
+
const tools = await Promise.all(
|
|
775
|
+
page.map(async (entry) => {
|
|
776
|
+
if (entry.kind === "synthesized") {
|
|
777
|
+
return {
|
|
778
|
+
name: entry.name,
|
|
779
|
+
...entry.title !== void 0 ? { title: entry.title } : {},
|
|
780
|
+
description: entry.description,
|
|
781
|
+
inputSchema: entry.inputSchema ?? { type: "object" }
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
const t = entry.config;
|
|
785
|
+
const inputSchema = t.inputSchema ?? (t.input ? await toJsonSchema(t.input, `tool "${t.name}" input`) : { type: "object" });
|
|
786
|
+
const outputSchema = t.outputSchema ?? (t.output ? await toJsonSchema(t.output, `tool "${t.name}" output`) : void 0);
|
|
787
|
+
const uiMeta = clientIsUiCapable && t.ui ? {
|
|
788
|
+
resourceUri: t.ui.resourceUri ?? `ui://${t.name}`,
|
|
789
|
+
...t.ui.visibility ? { visibility: t.ui.visibility } : {}
|
|
790
|
+
} : void 0;
|
|
791
|
+
return {
|
|
792
|
+
name: entry.name,
|
|
793
|
+
...entry.title !== void 0 ? { title: entry.title } : {},
|
|
794
|
+
description: entry.description,
|
|
795
|
+
inputSchema,
|
|
796
|
+
...outputSchema ? { outputSchema } : {},
|
|
797
|
+
...uiMeta ? { _meta: { ui: uiMeta } } : {}
|
|
798
|
+
};
|
|
799
|
+
})
|
|
800
|
+
);
|
|
801
|
+
return { tools, ...nextCursor ? { nextCursor } : {} };
|
|
802
|
+
})
|
|
803
|
+
);
|
|
804
|
+
});
|
|
805
|
+
server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
806
|
+
const requestedName = req.params.name;
|
|
807
|
+
const token = await this._resolveToken(extra.authInfo);
|
|
808
|
+
const { resourceViews, promptViews } = await this._getVisibleViews(token);
|
|
809
|
+
const synthesizedList = this._buildSynthesizedTools(resourceViews, promptViews);
|
|
810
|
+
const synthTool = synthesizedList.find((s) => s.name === requestedName);
|
|
811
|
+
if (synthTool) {
|
|
812
|
+
if (synthTool.auth) await runAuthCheck(synthTool.auth, token);
|
|
813
|
+
const ctx2 = createContext(
|
|
814
|
+
server,
|
|
815
|
+
extra.requestId !== void 0 ? String(extra.requestId) : void 0,
|
|
816
|
+
extra._meta?.progressToken,
|
|
817
|
+
token,
|
|
818
|
+
sessionState
|
|
819
|
+
);
|
|
820
|
+
try {
|
|
821
|
+
return await contextStore.run(
|
|
822
|
+
ctx2,
|
|
823
|
+
() => runMiddlewareChain(this._middleware, "tools/call", req.params, ctx2, async () => {
|
|
824
|
+
let executePromise = Promise.resolve(
|
|
825
|
+
synthTool.handler(req.params.arguments ?? {})
|
|
826
|
+
);
|
|
827
|
+
if (synthTool.timeout) {
|
|
828
|
+
const ms = synthTool.timeout;
|
|
829
|
+
let timer;
|
|
830
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
831
|
+
timer = setTimeout(
|
|
832
|
+
() => reject(new Error(`Tool "${requestedName}" timed out after ${ms}ms`)),
|
|
833
|
+
ms
|
|
834
|
+
);
|
|
835
|
+
});
|
|
836
|
+
executePromise = Promise.race([executePromise, timeoutPromise]).finally(
|
|
837
|
+
() => clearTimeout(timer)
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
return convertResult(await executePromise);
|
|
841
|
+
})
|
|
842
|
+
);
|
|
843
|
+
} catch (err) {
|
|
844
|
+
if (err instanceof McpError3) throw err;
|
|
845
|
+
return {
|
|
846
|
+
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
|
|
847
|
+
isError: true
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
let tool;
|
|
852
|
+
if (this._transforms.length > 0) {
|
|
853
|
+
for (const t of this._tools.values()) {
|
|
854
|
+
const view = applyTransformChain(
|
|
855
|
+
{ name: t.config.name, description: t.config.description, tags: t.config.tags ?? [] },
|
|
856
|
+
this._transforms,
|
|
857
|
+
(tr, v) => tr.transformTool?.(v)
|
|
858
|
+
);
|
|
859
|
+
if (view && view.name === requestedName) {
|
|
860
|
+
tool = t;
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (!tool) tool = this._tools.get(requestedName);
|
|
866
|
+
if (!tool || tool.config.disabled) {
|
|
867
|
+
throw new McpError3(ErrorCode3.InvalidParams, `Unknown tool: "${requestedName}"`);
|
|
868
|
+
}
|
|
869
|
+
if (tool.config.auth) await runAuthCheck(tool.config.auth, token);
|
|
870
|
+
const resolvedTool = tool;
|
|
871
|
+
const rawArgs = req.params.arguments ?? {};
|
|
872
|
+
const ctx = createContext(
|
|
873
|
+
server,
|
|
874
|
+
extra.requestId !== void 0 ? String(extra.requestId) : void 0,
|
|
875
|
+
extra._meta?.progressToken,
|
|
876
|
+
token,
|
|
877
|
+
sessionState
|
|
878
|
+
);
|
|
879
|
+
try {
|
|
880
|
+
return await contextStore.run(
|
|
881
|
+
ctx,
|
|
882
|
+
() => runMiddlewareChain(this._middleware, "tools/call", req.params, ctx, async () => {
|
|
883
|
+
const args = resolvedTool.config.input ? await validateInput(resolvedTool.config.input, rawArgs, true) : rawArgs;
|
|
884
|
+
let executePromise = Promise.resolve(resolvedTool.handler(args));
|
|
885
|
+
if (resolvedTool.config.timeout) {
|
|
886
|
+
const ms = resolvedTool.config.timeout;
|
|
887
|
+
let timer;
|
|
888
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
889
|
+
timer = setTimeout(
|
|
890
|
+
() => reject(new Error(`Tool "${requestedName}" timed out after ${ms}ms`)),
|
|
891
|
+
ms
|
|
892
|
+
);
|
|
893
|
+
});
|
|
894
|
+
executePromise = Promise.race([executePromise, timeoutPromise]).finally(
|
|
895
|
+
() => clearTimeout(timer)
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
let resultValue = await executePromise;
|
|
899
|
+
if (resolvedTool.config.output) resultValue = await validateInput(resolvedTool.config.output, resultValue);
|
|
900
|
+
const callResult = convertResult(resultValue);
|
|
901
|
+
if (resolvedTool.config.ui) {
|
|
902
|
+
const clientIsUi = !!server.getClientCapabilities()?.extensions?.[UI_EXTENSION_KEY];
|
|
903
|
+
if (!clientIsUi && callResult.structuredContent !== void 0) {
|
|
904
|
+
return { content: [{ type: "text", text: "[UI not available in this client]" }] };
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return callResult;
|
|
908
|
+
})
|
|
909
|
+
);
|
|
910
|
+
} catch (err) {
|
|
911
|
+
if (err instanceof McpError3) throw err;
|
|
912
|
+
return {
|
|
913
|
+
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
|
|
914
|
+
isError: true
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
server.setRequestHandler(ListResourcesRequestSchema, async (req, extra) => {
|
|
919
|
+
const token = await this._resolveToken(extra.authInfo);
|
|
920
|
+
const ctx = createContext(server, extra.requestId !== void 0 ? String(extra.requestId) : void 0, void 0, token, sessionState);
|
|
921
|
+
return contextStore.run(
|
|
922
|
+
ctx,
|
|
923
|
+
() => runMiddlewareChain(this._middleware, "resources/list", req.params, ctx, async () => {
|
|
924
|
+
const allVisible = (await Promise.all(
|
|
925
|
+
[...this._staticResources.values()].map(async (r) => {
|
|
926
|
+
if (r.config.disabled) return null;
|
|
927
|
+
if (!r.config.auth) return r;
|
|
928
|
+
if (!token) return null;
|
|
929
|
+
try {
|
|
930
|
+
await r.config.auth(token);
|
|
931
|
+
return r;
|
|
932
|
+
} catch {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
})
|
|
936
|
+
)).filter((r) => r !== null);
|
|
937
|
+
const transformedResources = this._applyTransformsToResources(allVisible);
|
|
938
|
+
const pageSize = this._resourcesPageSize;
|
|
939
|
+
const cursorUri = req.params?.cursor ? Buffer.from(req.params.cursor, "base64url").toString() : null;
|
|
940
|
+
let startIdx = 0;
|
|
941
|
+
if (cursorUri !== null) {
|
|
942
|
+
const idx = transformedResources.findIndex((r) => r.uri === cursorUri);
|
|
943
|
+
if (idx < 0) throw new McpError3(ErrorCode3.InvalidParams, "Invalid or expired cursor");
|
|
944
|
+
startIdx = idx + 1;
|
|
945
|
+
}
|
|
946
|
+
const page = transformedResources.slice(startIdx, startIdx + pageSize);
|
|
947
|
+
const nextCursor = startIdx + pageSize < transformedResources.length ? Buffer.from(page[page.length - 1].uri).toString("base64url") : void 0;
|
|
948
|
+
const clientIsUiCapableRes = !!server.getClientCapabilities()?.extensions?.[UI_EXTENSION_KEY];
|
|
949
|
+
return {
|
|
950
|
+
resources: page.map((r) => ({
|
|
951
|
+
uri: r.uri,
|
|
952
|
+
name: r.name,
|
|
953
|
+
...r.config.title !== void 0 ? { title: r.config.title } : {},
|
|
954
|
+
...r.config.description !== void 0 ? { description: r.config.description } : {},
|
|
955
|
+
...r.config.mimeType !== void 0 ? { mimeType: r.config.mimeType } : {},
|
|
956
|
+
...r.config.size !== void 0 ? { size: r.config.size } : {},
|
|
957
|
+
...r.config.annotations !== void 0 ? { annotations: r.config.annotations } : {},
|
|
958
|
+
...clientIsUiCapableRes && r.config.ui ? { _meta: { ui: r.config.ui } } : {}
|
|
959
|
+
})),
|
|
960
|
+
...nextCursor ? { nextCursor } : {}
|
|
961
|
+
};
|
|
962
|
+
})
|
|
963
|
+
);
|
|
964
|
+
});
|
|
965
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async (req, extra) => {
|
|
966
|
+
const token = await this._resolveToken(extra.authInfo);
|
|
967
|
+
const ctx = createContext(server, extra.requestId !== void 0 ? String(extra.requestId) : void 0, void 0, token, sessionState);
|
|
968
|
+
return contextStore.run(
|
|
969
|
+
ctx,
|
|
970
|
+
() => runMiddlewareChain(this._middleware, "resources/templates/list", req.params, ctx, async () => {
|
|
971
|
+
const allVisible = (await Promise.all(
|
|
972
|
+
[...this._templateResources.values()].map(async (r) => {
|
|
973
|
+
if (r.config.disabled) return null;
|
|
974
|
+
if (!r.config.auth) return r;
|
|
975
|
+
if (!token) return null;
|
|
976
|
+
try {
|
|
977
|
+
await r.config.auth(token);
|
|
978
|
+
return r;
|
|
979
|
+
} catch {
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
})
|
|
983
|
+
)).filter((r) => r !== null);
|
|
984
|
+
const transformedTemplates = this._applyTransformsToResourceTemplates(allVisible);
|
|
985
|
+
const pageSize = this._resourcesPageSize;
|
|
986
|
+
const cursorUri = req.params?.cursor ? Buffer.from(req.params.cursor, "base64url").toString() : null;
|
|
987
|
+
let startIdx = 0;
|
|
988
|
+
if (cursorUri !== null) {
|
|
989
|
+
const idx = transformedTemplates.findIndex((r) => r.uri === cursorUri);
|
|
990
|
+
if (idx < 0) throw new McpError3(ErrorCode3.InvalidParams, "Invalid or expired cursor");
|
|
991
|
+
startIdx = idx + 1;
|
|
992
|
+
}
|
|
993
|
+
const page = transformedTemplates.slice(startIdx, startIdx + pageSize);
|
|
994
|
+
const nextCursor = startIdx + pageSize < transformedTemplates.length ? Buffer.from(page[page.length - 1].uri).toString("base64url") : void 0;
|
|
995
|
+
return {
|
|
996
|
+
resourceTemplates: page.map((r) => ({
|
|
997
|
+
uriTemplate: r.uri,
|
|
998
|
+
name: r.name,
|
|
999
|
+
...r.config.title !== void 0 ? { title: r.config.title } : {},
|
|
1000
|
+
...r.config.description !== void 0 ? { description: r.config.description } : {},
|
|
1001
|
+
...r.config.mimeType !== void 0 ? { mimeType: r.config.mimeType } : {},
|
|
1002
|
+
...r.config.annotations !== void 0 ? { annotations: r.config.annotations } : {}
|
|
1003
|
+
})),
|
|
1004
|
+
...nextCursor ? { nextCursor } : {}
|
|
1005
|
+
};
|
|
1006
|
+
})
|
|
1007
|
+
);
|
|
1008
|
+
});
|
|
1009
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (req, extra) => {
|
|
1010
|
+
const requestedUri = req.params.uri;
|
|
1011
|
+
const token = await this._resolveToken(extra.authInfo);
|
|
1012
|
+
let resource = this._staticResources.get(requestedUri);
|
|
1013
|
+
let templateParams;
|
|
1014
|
+
if (!resource) {
|
|
1015
|
+
for (const r of this._templateResources.values()) {
|
|
1016
|
+
if (r.config.disabled) continue;
|
|
1017
|
+
const params = matchTemplate(r.config.uri, requestedUri);
|
|
1018
|
+
if (params !== null) {
|
|
1019
|
+
resource = r;
|
|
1020
|
+
templateParams = params;
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (!resource && this._transforms.length > 0) {
|
|
1026
|
+
for (const r of this._staticResources.values()) {
|
|
1027
|
+
if (r.config.disabled) continue;
|
|
1028
|
+
const view = applyTransformChain(
|
|
1029
|
+
{ uri: r.config.uri, name: r.config.name ?? r.config.uri, tags: r.config.tags ?? [], mimeType: r.config.mimeType, title: r.config.title },
|
|
1030
|
+
this._transforms,
|
|
1031
|
+
(t, v) => t.transformResource?.(v)
|
|
1032
|
+
);
|
|
1033
|
+
if (view && view.uri === requestedUri) {
|
|
1034
|
+
resource = r;
|
|
1035
|
+
break;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (!resource && this._transforms.length > 0) {
|
|
1040
|
+
for (const r of this._templateResources.values()) {
|
|
1041
|
+
if (r.config.disabled) continue;
|
|
1042
|
+
const view = applyTransformChain(
|
|
1043
|
+
{ uri: r.config.uri, name: r.config.name ?? r.config.uri, tags: r.config.tags ?? [], mimeType: r.config.mimeType, title: r.config.title },
|
|
1044
|
+
this._transforms,
|
|
1045
|
+
(t, v) => t.transformResourceTemplate?.(v)
|
|
1046
|
+
);
|
|
1047
|
+
if (view) {
|
|
1048
|
+
const params = matchTemplate(view.uri, requestedUri);
|
|
1049
|
+
if (params !== null) {
|
|
1050
|
+
resource = r;
|
|
1051
|
+
templateParams = params;
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (!resource || resource.config.disabled) {
|
|
1058
|
+
throw new McpError3(ErrorCode3.InvalidParams, `Unknown resource: "${requestedUri}"`);
|
|
1059
|
+
}
|
|
1060
|
+
if (resource.config.auth) await runAuthCheck(resource.config.auth, token);
|
|
1061
|
+
const ctx = createContext(
|
|
1062
|
+
server,
|
|
1063
|
+
extra.requestId !== void 0 ? String(extra.requestId) : void 0,
|
|
1064
|
+
extra._meta?.progressToken,
|
|
1065
|
+
token,
|
|
1066
|
+
sessionState
|
|
1067
|
+
);
|
|
1068
|
+
return contextStore.run(
|
|
1069
|
+
ctx,
|
|
1070
|
+
() => runMiddlewareChain(this._middleware, "resources/read", req.params, ctx, async () => {
|
|
1071
|
+
let executePromise = Promise.resolve(resource.handler(templateParams));
|
|
1072
|
+
if (resource.config.timeout) {
|
|
1073
|
+
const ms = resource.config.timeout;
|
|
1074
|
+
let timer;
|
|
1075
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1076
|
+
timer = setTimeout(
|
|
1077
|
+
() => reject(new Error(`Resource "${requestedUri}" timed out after ${ms}ms`)),
|
|
1078
|
+
ms
|
|
1079
|
+
);
|
|
1080
|
+
});
|
|
1081
|
+
executePromise = Promise.race([executePromise, timeoutPromise]).finally(
|
|
1082
|
+
() => clearTimeout(timer)
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
const result = await executePromise;
|
|
1086
|
+
return convertResourceResult(result, requestedUri, resource.config.mimeType);
|
|
1087
|
+
})
|
|
1088
|
+
);
|
|
1089
|
+
});
|
|
1090
|
+
server.setRequestHandler(ListPromptsRequestSchema, async (req, extra) => {
|
|
1091
|
+
const token = await this._resolveToken(extra.authInfo);
|
|
1092
|
+
const ctx = createContext(server, extra.requestId !== void 0 ? String(extra.requestId) : void 0, void 0, token, sessionState);
|
|
1093
|
+
return contextStore.run(
|
|
1094
|
+
ctx,
|
|
1095
|
+
() => runMiddlewareChain(this._middleware, "prompts/list", req.params, ctx, async () => {
|
|
1096
|
+
const allVisible = (await Promise.all(
|
|
1097
|
+
[...this._prompts.values()].map(async (p) => {
|
|
1098
|
+
if (p.config.disabled) return null;
|
|
1099
|
+
if (!p.config.auth) return p;
|
|
1100
|
+
if (!token) return null;
|
|
1101
|
+
try {
|
|
1102
|
+
await p.config.auth(token);
|
|
1103
|
+
return p;
|
|
1104
|
+
} catch {
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
})
|
|
1108
|
+
)).filter((p) => p !== null);
|
|
1109
|
+
const transformedPrompts = this._applyTransformsToPrompts(allVisible);
|
|
1110
|
+
const pageSize = this._promptsPageSize;
|
|
1111
|
+
const cursorName = req.params?.cursor ? Buffer.from(req.params.cursor, "base64url").toString() : null;
|
|
1112
|
+
let startIdx = 0;
|
|
1113
|
+
if (cursorName !== null) {
|
|
1114
|
+
const idx = transformedPrompts.findIndex((p) => p.name === cursorName);
|
|
1115
|
+
if (idx < 0) throw new McpError3(ErrorCode3.InvalidParams, "Invalid or expired cursor");
|
|
1116
|
+
startIdx = idx + 1;
|
|
1117
|
+
}
|
|
1118
|
+
const page = transformedPrompts.slice(startIdx, startIdx + pageSize);
|
|
1119
|
+
const nextCursor = startIdx + pageSize < transformedPrompts.length ? Buffer.from(page[page.length - 1].name).toString("base64url") : void 0;
|
|
1120
|
+
return {
|
|
1121
|
+
prompts: page.map((p) => ({
|
|
1122
|
+
name: p.name,
|
|
1123
|
+
...p.config.title !== void 0 ? { title: p.config.title } : {},
|
|
1124
|
+
description: p.description,
|
|
1125
|
+
...p.config.arguments?.length ? { arguments: p.config.arguments } : {}
|
|
1126
|
+
})),
|
|
1127
|
+
...nextCursor ? { nextCursor } : {}
|
|
1128
|
+
};
|
|
1129
|
+
})
|
|
1130
|
+
);
|
|
1131
|
+
});
|
|
1132
|
+
server.setRequestHandler(GetPromptRequestSchema, async (req, extra) => {
|
|
1133
|
+
const requestedName = req.params.name;
|
|
1134
|
+
let prompt;
|
|
1135
|
+
if (this._transforms.length > 0) {
|
|
1136
|
+
for (const p of this._prompts.values()) {
|
|
1137
|
+
const view = applyTransformChain(
|
|
1138
|
+
{ name: p.config.name, description: p.config.description, tags: p.config.tags ?? [] },
|
|
1139
|
+
this._transforms,
|
|
1140
|
+
(t, v) => t.transformPrompt?.(v)
|
|
1141
|
+
);
|
|
1142
|
+
if (view && view.name === requestedName) {
|
|
1143
|
+
prompt = p;
|
|
1144
|
+
break;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (!prompt) prompt = this._prompts.get(requestedName);
|
|
1149
|
+
if (!prompt || prompt.config.disabled) {
|
|
1150
|
+
throw new McpError3(ErrorCode3.InvalidParams, `Unknown prompt: "${requestedName}"`);
|
|
1151
|
+
}
|
|
1152
|
+
const resolvedPrompt = prompt;
|
|
1153
|
+
const token = await this._resolveToken(extra.authInfo);
|
|
1154
|
+
if (resolvedPrompt.config.auth) await runAuthCheck(resolvedPrompt.config.auth, token);
|
|
1155
|
+
const suppliedArgs = req.params.arguments ?? {};
|
|
1156
|
+
for (const arg of resolvedPrompt.config.arguments ?? []) {
|
|
1157
|
+
if (arg.required && !(arg.name in suppliedArgs)) {
|
|
1158
|
+
throw new McpError3(
|
|
1159
|
+
ErrorCode3.InvalidParams,
|
|
1160
|
+
`Missing required argument "${arg.name}" for prompt "${requestedName}"`
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
const ctx = createContext(
|
|
1165
|
+
server,
|
|
1166
|
+
extra.requestId !== void 0 ? String(extra.requestId) : void 0,
|
|
1167
|
+
extra._meta?.progressToken,
|
|
1168
|
+
token,
|
|
1169
|
+
sessionState
|
|
1170
|
+
);
|
|
1171
|
+
return contextStore.run(
|
|
1172
|
+
ctx,
|
|
1173
|
+
() => runMiddlewareChain(this._middleware, "prompts/get", req.params, ctx, async () => {
|
|
1174
|
+
let executePromise = Promise.resolve(resolvedPrompt.handler(suppliedArgs));
|
|
1175
|
+
if (resolvedPrompt.config.timeout) {
|
|
1176
|
+
const ms = resolvedPrompt.config.timeout;
|
|
1177
|
+
let timer;
|
|
1178
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1179
|
+
timer = setTimeout(
|
|
1180
|
+
() => reject(new Error(`Prompt "${requestedName}" timed out after ${ms}ms`)),
|
|
1181
|
+
ms
|
|
1182
|
+
);
|
|
1183
|
+
});
|
|
1184
|
+
executePromise = Promise.race([executePromise, timeoutPromise]).finally(
|
|
1185
|
+
() => clearTimeout(timer)
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
return convertPromptResult(await executePromise);
|
|
1189
|
+
})
|
|
1190
|
+
);
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
_notifyToolListChanged() {
|
|
1194
|
+
this._primaryServer.sendToolListChanged().catch(() => {
|
|
1195
|
+
});
|
|
1196
|
+
for (const { server } of this._sessions.values()) {
|
|
1197
|
+
server.sendToolListChanged().catch(() => {
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
_notifyResourceListChanged() {
|
|
1202
|
+
this._primaryServer.sendResourceListChanged().catch(() => {
|
|
1203
|
+
});
|
|
1204
|
+
for (const { server } of this._sessions.values()) {
|
|
1205
|
+
server.sendResourceListChanged().catch(() => {
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
_notifyPromptListChanged() {
|
|
1210
|
+
this._primaryServer.sendPromptListChanged().catch(() => {
|
|
1211
|
+
});
|
|
1212
|
+
for (const { server } of this._sessions.values()) {
|
|
1213
|
+
server.sendPromptListChanged().catch(() => {
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
// Implementation (handler typed as any to satisfy both overload signatures)
|
|
1218
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1219
|
+
tool(config, handler) {
|
|
1220
|
+
const registered = { config, handler };
|
|
1221
|
+
this._tools.set(config.name, registered);
|
|
1222
|
+
this._notifyToolListChanged();
|
|
1223
|
+
for (const cb of this._toolRegisteredCallbacks) cb(registered);
|
|
1224
|
+
}
|
|
1225
|
+
prompt(config, handler) {
|
|
1226
|
+
const name = config.name ?? handler.name;
|
|
1227
|
+
if (!name) throw new Error("Prompt name must be provided in config or inferrable from the handler function name");
|
|
1228
|
+
const description = config.description ?? inferDescription(name);
|
|
1229
|
+
const resolvedConfig = { ...config, name, description };
|
|
1230
|
+
const registered = { config: resolvedConfig, handler };
|
|
1231
|
+
this._prompts.set(name, registered);
|
|
1232
|
+
this._notifyPromptListChanged();
|
|
1233
|
+
for (const cb of this._promptRegisteredCallbacks) cb(registered);
|
|
1234
|
+
}
|
|
1235
|
+
resource(config, handler) {
|
|
1236
|
+
const registered = { config, handler };
|
|
1237
|
+
if (isUriTemplate(config.uri)) {
|
|
1238
|
+
this._templateResources.set(config.uri, registered);
|
|
1239
|
+
} else {
|
|
1240
|
+
this._staticResources.set(config.uri, registered);
|
|
1241
|
+
}
|
|
1242
|
+
this._notifyResourceListChanged();
|
|
1243
|
+
for (const cb of this._resourceRegisteredCallbacks) cb(registered);
|
|
1244
|
+
}
|
|
1245
|
+
_removeTool(name) {
|
|
1246
|
+
if (!this._tools.has(name)) return false;
|
|
1247
|
+
this._tools.delete(name);
|
|
1248
|
+
this._notifyToolListChanged();
|
|
1249
|
+
return true;
|
|
1250
|
+
}
|
|
1251
|
+
_removeResource(uri) {
|
|
1252
|
+
if (this._staticResources.has(uri)) {
|
|
1253
|
+
this._staticResources.delete(uri);
|
|
1254
|
+
this._notifyResourceListChanged();
|
|
1255
|
+
return true;
|
|
1256
|
+
}
|
|
1257
|
+
if (this._templateResources.has(uri)) {
|
|
1258
|
+
this._templateResources.delete(uri);
|
|
1259
|
+
this._notifyResourceListChanged();
|
|
1260
|
+
return true;
|
|
1261
|
+
}
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
_removePrompt(name) {
|
|
1265
|
+
if (!this._prompts.has(name)) return false;
|
|
1266
|
+
this._prompts.delete(name);
|
|
1267
|
+
this._notifyPromptListChanged();
|
|
1268
|
+
return true;
|
|
1269
|
+
}
|
|
1270
|
+
/** Add a transform to the pipeline. Applied to list responses in registration order. */
|
|
1271
|
+
transform(t) {
|
|
1272
|
+
this._transforms.push(t);
|
|
1273
|
+
return this;
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Dispatch a tool call through this server's middleware chain using an inherited context.
|
|
1277
|
+
* Used by parent servers to honour child-level middleware when routing mounted tool calls.
|
|
1278
|
+
*/
|
|
1279
|
+
async _dispatchTool(name, rawArgs, ctx) {
|
|
1280
|
+
const tool = this._tools.get(name);
|
|
1281
|
+
if (!tool || tool.config.disabled) {
|
|
1282
|
+
throw new McpError3(ErrorCode3.InvalidParams, `Unknown tool: "${name}"`);
|
|
1283
|
+
}
|
|
1284
|
+
if (tool.config.auth) await runAuthCheck(tool.config.auth, ctx.auth);
|
|
1285
|
+
try {
|
|
1286
|
+
return await contextStore.run(
|
|
1287
|
+
ctx,
|
|
1288
|
+
() => runMiddlewareChain(this._middleware, "tools/call", { name, arguments: rawArgs }, ctx, async () => {
|
|
1289
|
+
const args = tool.config.input ? await validateInput(tool.config.input, rawArgs, true) : rawArgs;
|
|
1290
|
+
let executePromise = Promise.resolve(tool.handler(args));
|
|
1291
|
+
if (tool.config.timeout) {
|
|
1292
|
+
const ms = tool.config.timeout;
|
|
1293
|
+
let timer;
|
|
1294
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1295
|
+
timer = setTimeout(
|
|
1296
|
+
() => reject(new Error(`Tool "${name}" timed out after ${ms}ms`)),
|
|
1297
|
+
ms
|
|
1298
|
+
);
|
|
1299
|
+
});
|
|
1300
|
+
executePromise = Promise.race([executePromise, timeoutPromise]).finally(
|
|
1301
|
+
() => clearTimeout(timer)
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
let resultValue = await executePromise;
|
|
1305
|
+
if (tool.config.output) resultValue = await validateInput(tool.config.output, resultValue);
|
|
1306
|
+
return convertResult(resultValue);
|
|
1307
|
+
})
|
|
1308
|
+
);
|
|
1309
|
+
} catch (err) {
|
|
1310
|
+
if (err instanceof McpError3) throw err;
|
|
1311
|
+
return {
|
|
1312
|
+
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
|
|
1313
|
+
isError: true
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Run a resource read through this server's middleware chain using an inherited context.
|
|
1319
|
+
* Returns the raw handler result so the parent can apply convertResourceResult with the
|
|
1320
|
+
* actual requested URI (important for template resources).
|
|
1321
|
+
*/
|
|
1322
|
+
async _dispatchResource(uri, params, ctx) {
|
|
1323
|
+
const resource = isUriTemplate(uri) ? this._templateResources.get(uri) : this._staticResources.get(uri);
|
|
1324
|
+
if (!resource || resource.config.disabled) {
|
|
1325
|
+
throw new McpError3(ErrorCode3.InvalidParams, `Unknown resource: "${uri}"`);
|
|
1326
|
+
}
|
|
1327
|
+
if (resource.config.auth) await runAuthCheck(resource.config.auth, ctx.auth);
|
|
1328
|
+
return contextStore.run(
|
|
1329
|
+
ctx,
|
|
1330
|
+
() => runMiddlewareChain(this._middleware, "resources/read", { uri }, ctx, async () => {
|
|
1331
|
+
let executePromise = Promise.resolve(resource.handler(params));
|
|
1332
|
+
if (resource.config.timeout) {
|
|
1333
|
+
const ms = resource.config.timeout;
|
|
1334
|
+
let timer;
|
|
1335
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1336
|
+
timer = setTimeout(
|
|
1337
|
+
() => reject(new Error(`Resource "${uri}" timed out after ${ms}ms`)),
|
|
1338
|
+
ms
|
|
1339
|
+
);
|
|
1340
|
+
});
|
|
1341
|
+
executePromise = Promise.race([executePromise, timeoutPromise]).finally(
|
|
1342
|
+
() => clearTimeout(timer)
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
return await executePromise;
|
|
1346
|
+
})
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Dispatch a prompt render through this server's middleware chain using an inherited context.
|
|
1351
|
+
*/
|
|
1352
|
+
async _dispatchPrompt(name, args, ctx) {
|
|
1353
|
+
const prompt = this._prompts.get(name);
|
|
1354
|
+
if (!prompt || prompt.config.disabled) {
|
|
1355
|
+
throw new McpError3(ErrorCode3.InvalidParams, `Unknown prompt: "${name}"`);
|
|
1356
|
+
}
|
|
1357
|
+
if (prompt.config.auth) await runAuthCheck(prompt.config.auth, ctx.auth);
|
|
1358
|
+
for (const arg of prompt.config.arguments ?? []) {
|
|
1359
|
+
if (arg.required && !(arg.name in args)) {
|
|
1360
|
+
throw new McpError3(
|
|
1361
|
+
ErrorCode3.InvalidParams,
|
|
1362
|
+
`Missing required argument "${arg.name}" for prompt "${name}"`
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return contextStore.run(
|
|
1367
|
+
ctx,
|
|
1368
|
+
() => runMiddlewareChain(this._middleware, "prompts/get", { name, arguments: args }, ctx, async () => {
|
|
1369
|
+
let executePromise = Promise.resolve(prompt.handler(args));
|
|
1370
|
+
if (prompt.config.timeout) {
|
|
1371
|
+
const ms = prompt.config.timeout;
|
|
1372
|
+
let timer;
|
|
1373
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1374
|
+
timer = setTimeout(
|
|
1375
|
+
() => reject(new Error(`Prompt "${name}" timed out after ${ms}ms`)),
|
|
1376
|
+
ms
|
|
1377
|
+
);
|
|
1378
|
+
});
|
|
1379
|
+
executePromise = Promise.race([executePromise, timeoutPromise]).finally(
|
|
1380
|
+
() => clearTimeout(timer)
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
return convertPromptResult(await executePromise);
|
|
1384
|
+
})
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
_mirrorTool(child, tool, prefix) {
|
|
1388
|
+
const originalName = tool.config.name;
|
|
1389
|
+
const key = prefix ? `${prefix}_${originalName}` : originalName;
|
|
1390
|
+
if (this._tools.has(key)) throw new Error(`Tool name collision on mount: "${key}" is already registered`);
|
|
1391
|
+
const forwardedConfig = { ...tool.config, name: key, input: void 0, output: void 0 };
|
|
1392
|
+
this.tool(forwardedConfig, (args) => {
|
|
1393
|
+
const parentCtx = contextStore.getStore();
|
|
1394
|
+
const childCtx = prefix ? { ...parentCtx, resolveToolName: (name) => `${prefix}_${name}` } : parentCtx;
|
|
1395
|
+
return child._dispatchTool(originalName, args, childCtx).then((result) => new ToolResult(result));
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
_mirrorResource(child, resource, prefix) {
|
|
1399
|
+
const name = resource.config.name ?? resource.config.uri;
|
|
1400
|
+
const originalUri = resource.config.uri;
|
|
1401
|
+
const targetUri = prefix ? prefixResourceUri(originalUri, prefix) : originalUri;
|
|
1402
|
+
if (this._staticResources.has(targetUri) || this._templateResources.has(targetUri)) {
|
|
1403
|
+
throw new Error(`Resource URI collision on mount: "${targetUri}" is already registered`);
|
|
1404
|
+
}
|
|
1405
|
+
const forwardedConfig = {
|
|
1406
|
+
...resource.config,
|
|
1407
|
+
uri: targetUri,
|
|
1408
|
+
name: prefix ? `${prefix}_${name}` : name
|
|
1409
|
+
};
|
|
1410
|
+
this.resource(forwardedConfig, (params) => {
|
|
1411
|
+
const ctx = contextStore.getStore();
|
|
1412
|
+
return child._dispatchResource(originalUri, params, ctx);
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
_mirrorPrompt(child, prompt, prefix) {
|
|
1416
|
+
const originalName = prompt.config.name;
|
|
1417
|
+
const key = prefix ? `${prefix}_${originalName}` : originalName;
|
|
1418
|
+
if (this._prompts.has(key)) throw new Error(`Prompt name collision on mount: "${key}" is already registered`);
|
|
1419
|
+
const forwardedConfig = { ...prompt.config, name: key };
|
|
1420
|
+
this.prompt(forwardedConfig, (args) => {
|
|
1421
|
+
const ctx = contextStore.getStore();
|
|
1422
|
+
return child._dispatchPrompt(originalName, args ?? {}, ctx).then((result) => new PromptResult(result.messages, result.description));
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
/** Mount a child server onto this server. All tools, resources, and prompts from the child become accessible via this server. Pass a prefix to namespace names and prevent collisions. */
|
|
1426
|
+
mount(child, prefix) {
|
|
1427
|
+
if (child === this) throw new Error("Cannot mount a server onto itself");
|
|
1428
|
+
if (this._mountedChildren.has(child)) return this;
|
|
1429
|
+
this._mountedChildren.add(child);
|
|
1430
|
+
for (const tool of child._tools.values()) this._mirrorTool(child, tool, prefix);
|
|
1431
|
+
for (const resource of child._staticResources.values()) this._mirrorResource(child, resource, prefix);
|
|
1432
|
+
for (const resource of child._templateResources.values()) this._mirrorResource(child, resource, prefix);
|
|
1433
|
+
for (const prompt of child._prompts.values()) this._mirrorPrompt(child, prompt, prefix);
|
|
1434
|
+
child._toolRegisteredCallbacks.push((tool) => this._mirrorTool(child, tool, prefix));
|
|
1435
|
+
child._resourceRegisteredCallbacks.push((resource) => this._mirrorResource(child, resource, prefix));
|
|
1436
|
+
child._promptRegisteredCallbacks.push((prompt) => this._mirrorPrompt(child, prompt, prefix));
|
|
1437
|
+
this._proxyCloseCallbacks.push(async () => {
|
|
1438
|
+
await Promise.all(child._proxyCloseCallbacks.map((cb) => cb().catch(() => {
|
|
1439
|
+
})));
|
|
1440
|
+
child._proxyCloseCallbacks.length = 0;
|
|
1441
|
+
});
|
|
1442
|
+
return this;
|
|
1443
|
+
}
|
|
1444
|
+
/** Register a callback invoked when this server is closed — used by proxy connections. */
|
|
1445
|
+
_addCloseCallback(cb) {
|
|
1446
|
+
this._proxyCloseCallbacks.push(cb);
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Register a provider (FastMCPApp or FastMCP) on this server.
|
|
1450
|
+
* All tools, resources, and prompts from the provider are mounted without a prefix.
|
|
1451
|
+
* For FastMCPApp, pass provider.server; for FastMCP, pass directly.
|
|
1452
|
+
*/
|
|
1453
|
+
addProvider(provider) {
|
|
1454
|
+
const child = "server" in provider ? provider.server : provider;
|
|
1455
|
+
return this.mount(child);
|
|
1456
|
+
}
|
|
1457
|
+
_applyTransformsToTools(tools) {
|
|
1458
|
+
return tools.flatMap((tool) => {
|
|
1459
|
+
const view = applyTransformChain(
|
|
1460
|
+
{ name: tool.config.name, title: tool.config.title, description: tool.config.description, tags: tool.config.tags ?? [] },
|
|
1461
|
+
this._transforms,
|
|
1462
|
+
(t, v) => t.transformTool?.(v)
|
|
1463
|
+
);
|
|
1464
|
+
return view ? [{ name: view.name, title: view.title, description: view.description, originalName: tool.config.name, config: tool.config, handler: tool.handler }] : [];
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
_applyTransformsToResources(resources) {
|
|
1468
|
+
return resources.flatMap((r) => {
|
|
1469
|
+
const resolvedName = r.config.name ?? r.config.uri;
|
|
1470
|
+
const view = applyTransformChain(
|
|
1471
|
+
{ uri: r.config.uri, name: resolvedName, tags: r.config.tags ?? [], mimeType: r.config.mimeType, title: r.config.title },
|
|
1472
|
+
this._transforms,
|
|
1473
|
+
(t, v) => t.transformResource?.(v)
|
|
1474
|
+
);
|
|
1475
|
+
return view ? [{ uri: view.uri, name: view.name, originalUri: r.config.uri, config: r.config, handler: r.handler }] : [];
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
_applyTransformsToResourceTemplates(templates) {
|
|
1479
|
+
return templates.flatMap((r) => {
|
|
1480
|
+
const resolvedName = r.config.name ?? r.config.uri;
|
|
1481
|
+
const view = applyTransformChain(
|
|
1482
|
+
{ uri: r.config.uri, name: resolvedName, tags: r.config.tags ?? [], mimeType: r.config.mimeType, title: r.config.title },
|
|
1483
|
+
this._transforms,
|
|
1484
|
+
(t, v) => t.transformResourceTemplate?.(v)
|
|
1485
|
+
);
|
|
1486
|
+
return view ? [{ uri: view.uri, name: view.name, originalUri: r.config.uri, config: r.config, handler: r.handler }] : [];
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
_applyTransformsToPrompts(prompts) {
|
|
1490
|
+
return prompts.flatMap((p) => {
|
|
1491
|
+
const view = applyTransformChain(
|
|
1492
|
+
{ name: p.config.name, description: p.config.description, tags: p.config.tags ?? [] },
|
|
1493
|
+
this._transforms,
|
|
1494
|
+
(t, v) => t.transformPrompt?.(v)
|
|
1495
|
+
);
|
|
1496
|
+
return view ? [{ name: view.name, description: view.description, originalName: p.config.name, config: p.config, handler: p.handler }] : [];
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
_buildSynthesizedTools(resourceViews, promptViews) {
|
|
1500
|
+
if (this._transforms.length === 0) return [];
|
|
1501
|
+
const raw = this._transforms.flatMap((t) => t.synthesizeTools?.(resourceViews, promptViews) ?? []);
|
|
1502
|
+
return raw.flatMap((s) => {
|
|
1503
|
+
const view = applyTransformChain(
|
|
1504
|
+
{ name: s.name, title: s.title, description: s.description, tags: [] },
|
|
1505
|
+
this._transforms,
|
|
1506
|
+
(t, v) => t.transformTool?.(v)
|
|
1507
|
+
);
|
|
1508
|
+
return view ? [{ ...s, name: view.name, title: view.title, description: view.description }] : [];
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
async _getVisibleViews(token) {
|
|
1512
|
+
const visibleStatic = (await Promise.all(
|
|
1513
|
+
[...this._staticResources.values()].map(async (r) => {
|
|
1514
|
+
if (r.config.disabled) return null;
|
|
1515
|
+
if (!r.config.auth) return r;
|
|
1516
|
+
if (!token) return null;
|
|
1517
|
+
try {
|
|
1518
|
+
await r.config.auth(token);
|
|
1519
|
+
return r;
|
|
1520
|
+
} catch {
|
|
1521
|
+
return null;
|
|
1522
|
+
}
|
|
1523
|
+
})
|
|
1524
|
+
)).filter((r) => r !== null);
|
|
1525
|
+
const visibleTemplates = (await Promise.all(
|
|
1526
|
+
[...this._templateResources.values()].map(async (r) => {
|
|
1527
|
+
if (r.config.disabled) return null;
|
|
1528
|
+
if (!r.config.auth) return r;
|
|
1529
|
+
if (!token) return null;
|
|
1530
|
+
try {
|
|
1531
|
+
await r.config.auth(token);
|
|
1532
|
+
return r;
|
|
1533
|
+
} catch {
|
|
1534
|
+
return null;
|
|
1535
|
+
}
|
|
1536
|
+
})
|
|
1537
|
+
)).filter((r) => r !== null);
|
|
1538
|
+
const visiblePrompts = (await Promise.all(
|
|
1539
|
+
[...this._prompts.values()].map(async (p) => {
|
|
1540
|
+
if (p.config.disabled) return null;
|
|
1541
|
+
if (!p.config.auth) return p;
|
|
1542
|
+
if (!token) return null;
|
|
1543
|
+
try {
|
|
1544
|
+
await p.config.auth(token);
|
|
1545
|
+
return p;
|
|
1546
|
+
} catch {
|
|
1547
|
+
return null;
|
|
1548
|
+
}
|
|
1549
|
+
})
|
|
1550
|
+
)).filter((p) => p !== null);
|
|
1551
|
+
const resourceViews = [
|
|
1552
|
+
...this._applyTransformsToResources(visibleStatic),
|
|
1553
|
+
...this._applyTransformsToResourceTemplates(visibleTemplates)
|
|
1554
|
+
].map((r) => ({
|
|
1555
|
+
uri: r.uri,
|
|
1556
|
+
name: r.name,
|
|
1557
|
+
tags: r.config.tags ?? [],
|
|
1558
|
+
mimeType: r.config.mimeType,
|
|
1559
|
+
title: r.config.title
|
|
1560
|
+
}));
|
|
1561
|
+
const promptViews = this._applyTransformsToPrompts(visiblePrompts).map((p) => ({
|
|
1562
|
+
name: p.name,
|
|
1563
|
+
description: p.description,
|
|
1564
|
+
tags: p.config.tags ?? []
|
|
1565
|
+
}));
|
|
1566
|
+
return { resourceViews, promptViews };
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Add a middleware to the pipeline.
|
|
1570
|
+
*
|
|
1571
|
+
* Can be called at any point before the first request arrives. `setup()` is
|
|
1572
|
+
* called immediately on the primary server so that notification handlers and
|
|
1573
|
+
* other server-level side-effects are active before `connect()` / `run()` is
|
|
1574
|
+
* called. HTTP sessions created after `use()` will also have `setup()` called
|
|
1575
|
+
* via `_makeServer()`.
|
|
1576
|
+
*/
|
|
1577
|
+
use(mw) {
|
|
1578
|
+
this._middleware.push(mw);
|
|
1579
|
+
mw.setup?.(this._primaryServer);
|
|
1580
|
+
return this;
|
|
1581
|
+
}
|
|
1582
|
+
getContext() {
|
|
1583
|
+
const ctx = contextStore.getStore();
|
|
1584
|
+
if (!ctx) throw new Error("getContext() called outside of a request handler");
|
|
1585
|
+
return ctx;
|
|
1586
|
+
}
|
|
1587
|
+
/** The bound address after run() resolves for the http transport. Null for stdio or before run(). */
|
|
1588
|
+
get address() {
|
|
1589
|
+
return this._address;
|
|
1590
|
+
}
|
|
1591
|
+
async connect(transport) {
|
|
1592
|
+
this._primaryServer = this._makeServer();
|
|
1593
|
+
await this._primaryServer.connect(transport);
|
|
1594
|
+
}
|
|
1595
|
+
async run(options) {
|
|
1596
|
+
const rawTransport = process.env.MCP_TRANSPORT ?? options?.transport ?? "stdio";
|
|
1597
|
+
if (rawTransport !== "stdio" && rawTransport !== "http") {
|
|
1598
|
+
throw new Error(`Unknown transport: "${rawTransport}". Supported: stdio, http.`);
|
|
1599
|
+
}
|
|
1600
|
+
const transport = rawTransport;
|
|
1601
|
+
const port = options?.port ?? parseInt(process.env.MCP_PORT ?? process.env.PORT ?? "3000", 10);
|
|
1602
|
+
const host = options?.host ?? process.env.MCP_HOST ?? "0.0.0.0";
|
|
1603
|
+
const path = options?.path ?? process.env.MCP_PATH ?? "/mcp";
|
|
1604
|
+
if (transport === "stdio") {
|
|
1605
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio");
|
|
1606
|
+
await this.connect(new StdioServerTransport(options?.stdin, options?.stdout));
|
|
1607
|
+
} else if (this._oauth) {
|
|
1608
|
+
await this._runHttpOAuth(port, host, path);
|
|
1609
|
+
} else {
|
|
1610
|
+
await this._runHttpSimple(port, host, path);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
async _runHttpOAuth(port, host, path) {
|
|
1614
|
+
const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp");
|
|
1615
|
+
const express = (await import("express")).default;
|
|
1616
|
+
const { mcpAuthRouter } = await import("@modelcontextprotocol/sdk/server/auth/router");
|
|
1617
|
+
const { requireBearerAuth } = await import("@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth");
|
|
1618
|
+
const oauth = this._oauth;
|
|
1619
|
+
const app = express();
|
|
1620
|
+
const httpServer = await new Promise((resolve, reject) => {
|
|
1621
|
+
const srv = app.listen(port, host, () => resolve(srv));
|
|
1622
|
+
srv.on("error", reject);
|
|
1623
|
+
});
|
|
1624
|
+
const bound = httpServer.address();
|
|
1625
|
+
const issuerUrl = oauth.issuerUrl ?? new URL(`http://${bound.address}:${bound.port}`);
|
|
1626
|
+
app.use(
|
|
1627
|
+
mcpAuthRouter({
|
|
1628
|
+
provider: oauth.provider,
|
|
1629
|
+
issuerUrl,
|
|
1630
|
+
scopesSupported: oauth.scopes
|
|
1631
|
+
})
|
|
1632
|
+
);
|
|
1633
|
+
app.all(
|
|
1634
|
+
path,
|
|
1635
|
+
requireBearerAuth({ verifier: oauth.provider }),
|
|
1636
|
+
async (req, res, _next) => {
|
|
1637
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
1638
|
+
let mcpTransport;
|
|
1639
|
+
if (sessionId) {
|
|
1640
|
+
const existing = this._sessions.get(sessionId);
|
|
1641
|
+
if (!existing) {
|
|
1642
|
+
res.status(404).json({ error: "Session not found" });
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
mcpTransport = existing.transport;
|
|
1646
|
+
} else {
|
|
1647
|
+
const sessionState = /* @__PURE__ */ new Map();
|
|
1648
|
+
const sessionServer = this._makeServer(sessionState);
|
|
1649
|
+
mcpTransport = new StreamableHTTPServerTransport({
|
|
1650
|
+
sessionIdGenerator: () => randomUUID(),
|
|
1651
|
+
onsessioninitialized: (id) => {
|
|
1652
|
+
this._sessions.set(id, { transport: mcpTransport, server: sessionServer, state: sessionState });
|
|
1653
|
+
},
|
|
1654
|
+
onsessionclosed: (id) => {
|
|
1655
|
+
const session = this._sessions.get(id);
|
|
1656
|
+
if (session) {
|
|
1657
|
+
const callbacks = session.state.get(SESSION_CLOSE_CALLBACKS_KEY) ?? [];
|
|
1658
|
+
for (const cb of callbacks) cb();
|
|
1659
|
+
}
|
|
1660
|
+
this._sessions.delete(id);
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
await sessionServer.connect(mcpTransport);
|
|
1664
|
+
}
|
|
1665
|
+
await mcpTransport.handleRequest(req, res);
|
|
1666
|
+
}
|
|
1667
|
+
);
|
|
1668
|
+
this._httpServer = httpServer;
|
|
1669
|
+
this._address = { host: bound.address, port: bound.port, path };
|
|
1670
|
+
}
|
|
1671
|
+
async _runHttpSimple(port, host, path) {
|
|
1672
|
+
const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp");
|
|
1673
|
+
const { createServer } = await import("http");
|
|
1674
|
+
const auth = this._auth;
|
|
1675
|
+
const corsHeaders = {
|
|
1676
|
+
"Access-Control-Allow-Origin": "*",
|
|
1677
|
+
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
1678
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, Mcp-Session-Id"
|
|
1679
|
+
};
|
|
1680
|
+
const httpServer = createServer(async (req, res) => {
|
|
1681
|
+
if (req.method === "OPTIONS") {
|
|
1682
|
+
res.writeHead(204, corsHeaders).end();
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
if (req.url?.split("?")[0] !== path) {
|
|
1686
|
+
res.writeHead(404).end();
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
for (const [k, v] of Object.entries(corsHeaders)) res.setHeader(k, v);
|
|
1690
|
+
if (auth) {
|
|
1691
|
+
const authHeader = req.headers.authorization;
|
|
1692
|
+
const bearer = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
1693
|
+
if (!bearer) {
|
|
1694
|
+
res.writeHead(401, { "Content-Type": "application/json", "WWW-Authenticate": 'Bearer realm="mcp"' }).end(JSON.stringify({ error: "Missing bearer token" }));
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
try {
|
|
1698
|
+
const accessToken = await auth.verify(bearer);
|
|
1699
|
+
req.auth = {
|
|
1700
|
+
token: accessToken.token,
|
|
1701
|
+
clientId: accessToken.clientId ?? "",
|
|
1702
|
+
scopes: accessToken.scopes,
|
|
1703
|
+
expiresAt: accessToken.expiresAt,
|
|
1704
|
+
extra: accessToken.claims
|
|
1705
|
+
};
|
|
1706
|
+
} catch (err) {
|
|
1707
|
+
if (err instanceof AuthorizationError) {
|
|
1708
|
+
res.writeHead(403, { "Content-Type": "application/json" }).end(JSON.stringify({ error: err.message }));
|
|
1709
|
+
} else {
|
|
1710
|
+
res.writeHead(401, { "Content-Type": "application/json", "WWW-Authenticate": 'Bearer realm="mcp"' }).end(JSON.stringify({ error: err instanceof Error ? err.message : "Authentication failed" }));
|
|
1711
|
+
}
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
1716
|
+
let mcpTransport;
|
|
1717
|
+
if (sessionId) {
|
|
1718
|
+
const existing = this._sessions.get(sessionId);
|
|
1719
|
+
if (!existing) {
|
|
1720
|
+
res.writeHead(404, { "Content-Type": "application/json" }).end(JSON.stringify({ error: "Session not found" }));
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
mcpTransport = existing.transport;
|
|
1724
|
+
} else {
|
|
1725
|
+
const sessionState = /* @__PURE__ */ new Map();
|
|
1726
|
+
const sessionServer = this._makeServer(sessionState);
|
|
1727
|
+
mcpTransport = new StreamableHTTPServerTransport({
|
|
1728
|
+
sessionIdGenerator: () => randomUUID(),
|
|
1729
|
+
onsessioninitialized: (id) => {
|
|
1730
|
+
this._sessions.set(id, { transport: mcpTransport, server: sessionServer, state: sessionState });
|
|
1731
|
+
},
|
|
1732
|
+
onsessionclosed: (id) => {
|
|
1733
|
+
const session = this._sessions.get(id);
|
|
1734
|
+
if (session) {
|
|
1735
|
+
const callbacks = session.state.get(SESSION_CLOSE_CALLBACKS_KEY) ?? [];
|
|
1736
|
+
for (const cb of callbacks) cb();
|
|
1737
|
+
}
|
|
1738
|
+
this._sessions.delete(id);
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
await sessionServer.connect(mcpTransport);
|
|
1742
|
+
}
|
|
1743
|
+
await mcpTransport.handleRequest(req, res);
|
|
1744
|
+
});
|
|
1745
|
+
this._httpServer = httpServer;
|
|
1746
|
+
await new Promise((resolve, reject) => {
|
|
1747
|
+
httpServer.once("error", reject);
|
|
1748
|
+
httpServer.listen(port, host, resolve);
|
|
1749
|
+
});
|
|
1750
|
+
const bound = httpServer.address();
|
|
1751
|
+
this._address = { host: bound.address, port: bound.port, path };
|
|
1752
|
+
}
|
|
1753
|
+
async close() {
|
|
1754
|
+
await Promise.all(this._proxyCloseCallbacks.map((cb) => cb().catch(() => {
|
|
1755
|
+
})));
|
|
1756
|
+
this._proxyCloseCallbacks.length = 0;
|
|
1757
|
+
await Promise.all(
|
|
1758
|
+
[...this._sessions.values()].map(({ transport }) => transport.close().catch(() => {
|
|
1759
|
+
}))
|
|
1760
|
+
);
|
|
1761
|
+
this._sessions.clear();
|
|
1762
|
+
if (this._httpServer) {
|
|
1763
|
+
await new Promise((resolve, reject) => {
|
|
1764
|
+
this._httpServer.close((err) => err != null ? reject(err) : resolve());
|
|
1765
|
+
});
|
|
1766
|
+
this._httpServer = null;
|
|
1767
|
+
}
|
|
1768
|
+
this._address = null;
|
|
1769
|
+
await this._primaryServer.close();
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
|
|
1773
|
+
// src/server/auth/authorization.ts
|
|
1774
|
+
function requireScopes(...scopes) {
|
|
1775
|
+
return async (token) => {
|
|
1776
|
+
for (const scope of scopes) {
|
|
1777
|
+
if (!token.scopes.includes(scope)) {
|
|
1778
|
+
throw new AuthorizationError(`Missing required scope: "${scope}"`);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// src/server/auth/multiAuth.ts
|
|
1785
|
+
function multiAuth(...verifiers) {
|
|
1786
|
+
return {
|
|
1787
|
+
async verify(token) {
|
|
1788
|
+
const errors = [];
|
|
1789
|
+
for (const verifier of verifiers) {
|
|
1790
|
+
try {
|
|
1791
|
+
return await verifier.verify(token);
|
|
1792
|
+
} catch (err) {
|
|
1793
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
throw new Error(`All auth sources rejected the token: ${errors.join("; ")}`);
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// src/server/auth/verifiers/jwt.ts
|
|
1802
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
1803
|
+
function jwtVerifier(options) {
|
|
1804
|
+
const JWKS = createRemoteJWKSet(new URL(options.jwksUri));
|
|
1805
|
+
return {
|
|
1806
|
+
async verify(token) {
|
|
1807
|
+
const { payload } = await jwtVerify(token, JWKS, {
|
|
1808
|
+
issuer: options.issuer,
|
|
1809
|
+
audience: options.audience,
|
|
1810
|
+
clockTolerance: options.leeway ?? 0
|
|
1811
|
+
});
|
|
1812
|
+
const scopes = typeof payload.scope === "string" ? payload.scope.split(" ").filter(Boolean) : Array.isArray(payload.scope) ? payload.scope : [];
|
|
1813
|
+
return {
|
|
1814
|
+
token,
|
|
1815
|
+
clientId: typeof payload.sub === "string" ? payload.sub : void 0,
|
|
1816
|
+
scopes,
|
|
1817
|
+
expiresAt: payload.exp,
|
|
1818
|
+
claims: payload
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// src/server/auth/verifiers/introspection.ts
|
|
1825
|
+
function introspectionVerifier(options) {
|
|
1826
|
+
const cache = options.cacheTtl ? /* @__PURE__ */ new Map() : null;
|
|
1827
|
+
return {
|
|
1828
|
+
async verify(rawToken) {
|
|
1829
|
+
if (cache) {
|
|
1830
|
+
const cached = cache.get(rawToken);
|
|
1831
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
1832
|
+
return cached.token;
|
|
1833
|
+
}
|
|
1834
|
+
cache.delete(rawToken);
|
|
1835
|
+
}
|
|
1836
|
+
const credentials = Buffer.from(
|
|
1837
|
+
`${options.credentials.clientId}:${options.credentials.clientSecret}`
|
|
1838
|
+
).toString("base64");
|
|
1839
|
+
const response = await fetch(options.endpoint, {
|
|
1840
|
+
method: "POST",
|
|
1841
|
+
headers: {
|
|
1842
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1843
|
+
Authorization: `Basic ${credentials}`
|
|
1844
|
+
},
|
|
1845
|
+
body: new URLSearchParams({ token: rawToken })
|
|
1846
|
+
});
|
|
1847
|
+
if (!response.ok) {
|
|
1848
|
+
throw new Error(`Introspection endpoint returned ${response.status}`);
|
|
1849
|
+
}
|
|
1850
|
+
const data = await response.json();
|
|
1851
|
+
if (!data.active) {
|
|
1852
|
+
throw new Error("Token is not active");
|
|
1853
|
+
}
|
|
1854
|
+
const accessToken = {
|
|
1855
|
+
token: rawToken,
|
|
1856
|
+
clientId: data.client_id,
|
|
1857
|
+
scopes: data.scope ? data.scope.split(" ").filter(Boolean) : [],
|
|
1858
|
+
expiresAt: data.exp,
|
|
1859
|
+
claims: data
|
|
1860
|
+
};
|
|
1861
|
+
if (cache && options.cacheTtl) {
|
|
1862
|
+
cache.set(rawToken, {
|
|
1863
|
+
token: accessToken,
|
|
1864
|
+
expiresAt: Date.now() + options.cacheTtl * 1e3
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
return accessToken;
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// src/server/auth/verifiers/static.ts
|
|
1873
|
+
function staticTokenVerifier(map) {
|
|
1874
|
+
return {
|
|
1875
|
+
async verify(token) {
|
|
1876
|
+
const entry = map[token];
|
|
1877
|
+
if (!entry) {
|
|
1878
|
+
throw new Error("Unknown token");
|
|
1879
|
+
}
|
|
1880
|
+
if (entry.expiresAt !== void 0 && entry.expiresAt < Math.floor(Date.now() / 1e3)) {
|
|
1881
|
+
throw new Error("Token has expired");
|
|
1882
|
+
}
|
|
1883
|
+
return {
|
|
1884
|
+
token,
|
|
1885
|
+
clientId: entry.clientId,
|
|
1886
|
+
scopes: entry.scopes ?? [],
|
|
1887
|
+
expiresAt: entry.expiresAt,
|
|
1888
|
+
claims: entry.claims ?? {}
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
function debugTokenVerifier() {
|
|
1894
|
+
console.warn("[fastmcp] debugTokenVerifier() accepts all bearer tokens \u2014 never use in production");
|
|
1895
|
+
return {
|
|
1896
|
+
async verify(token) {
|
|
1897
|
+
if (!token) {
|
|
1898
|
+
throw new Error("Empty token");
|
|
1899
|
+
}
|
|
1900
|
+
return { token, scopes: [], claims: {} };
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// src/server/auth/oauth/provider.ts
|
|
1906
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1907
|
+
import { UnsupportedGrantTypeError } from "@modelcontextprotocol/sdk/server/auth/errors";
|
|
1908
|
+
function oauthProvider(options = {}) {
|
|
1909
|
+
const clients = /* @__PURE__ */ new Map();
|
|
1910
|
+
const codes = /* @__PURE__ */ new Map();
|
|
1911
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
1912
|
+
const clientsStore = {
|
|
1913
|
+
async getClient(clientId) {
|
|
1914
|
+
return clients.get(clientId);
|
|
1915
|
+
},
|
|
1916
|
+
async registerClient(client) {
|
|
1917
|
+
const full = client;
|
|
1918
|
+
clients.set(full.client_id, full);
|
|
1919
|
+
return full;
|
|
1920
|
+
}
|
|
1921
|
+
};
|
|
1922
|
+
return {
|
|
1923
|
+
clientsStore,
|
|
1924
|
+
async authorize(client, params, res) {
|
|
1925
|
+
if (options.onAuthorize) {
|
|
1926
|
+
return options.onAuthorize(client, params, res);
|
|
1927
|
+
}
|
|
1928
|
+
const code = randomUUID2();
|
|
1929
|
+
codes.set(code, { client, params });
|
|
1930
|
+
const redirectUrl = new URL(params.redirectUri);
|
|
1931
|
+
redirectUrl.searchParams.set("code", code);
|
|
1932
|
+
if (params.state) redirectUrl.searchParams.set("state", params.state);
|
|
1933
|
+
res.redirect(redirectUrl.toString());
|
|
1934
|
+
},
|
|
1935
|
+
async challengeForAuthorizationCode(_client, authorizationCode) {
|
|
1936
|
+
const data = codes.get(authorizationCode);
|
|
1937
|
+
if (!data) throw new Error("Invalid authorization code");
|
|
1938
|
+
return data.params.codeChallenge;
|
|
1939
|
+
},
|
|
1940
|
+
async exchangeAuthorizationCode(client, authorizationCode) {
|
|
1941
|
+
const data = codes.get(authorizationCode);
|
|
1942
|
+
if (!data) throw new Error("Invalid authorization code");
|
|
1943
|
+
if (data.client.client_id !== client.client_id) {
|
|
1944
|
+
throw new Error("Authorization code was not issued to this client");
|
|
1945
|
+
}
|
|
1946
|
+
codes.delete(authorizationCode);
|
|
1947
|
+
const token = randomUUID2();
|
|
1948
|
+
const ttl = options.tokenTtl ?? 3600;
|
|
1949
|
+
const requestedScopes = data.params.scopes ?? [];
|
|
1950
|
+
const grantedScopes = options.scopes !== void 0 ? requestedScopes.filter((s) => options.scopes.includes(s)) : requestedScopes;
|
|
1951
|
+
tokens.set(token, {
|
|
1952
|
+
clientId: client.client_id,
|
|
1953
|
+
scopes: grantedScopes,
|
|
1954
|
+
expiresAt: Math.floor(Date.now() / 1e3) + ttl
|
|
1955
|
+
});
|
|
1956
|
+
return {
|
|
1957
|
+
access_token: token,
|
|
1958
|
+
token_type: "bearer",
|
|
1959
|
+
expires_in: ttl,
|
|
1960
|
+
scope: grantedScopes.join(" ")
|
|
1961
|
+
};
|
|
1962
|
+
},
|
|
1963
|
+
async exchangeRefreshToken() {
|
|
1964
|
+
throw new UnsupportedGrantTypeError("Refresh tokens are not supported by this provider");
|
|
1965
|
+
},
|
|
1966
|
+
async verifyAccessToken(token) {
|
|
1967
|
+
const data = tokens.get(token);
|
|
1968
|
+
if (!data) throw new Error("Invalid token");
|
|
1969
|
+
if (data.expiresAt < Math.floor(Date.now() / 1e3)) throw new Error("Token has expired");
|
|
1970
|
+
return {
|
|
1971
|
+
token,
|
|
1972
|
+
clientId: data.clientId,
|
|
1973
|
+
scopes: data.scopes,
|
|
1974
|
+
expiresAt: data.expiresAt
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// src/server/auth/oauth/proxy.ts
|
|
1981
|
+
function oauthProxy(options) {
|
|
1982
|
+
const clients = /* @__PURE__ */ new Map();
|
|
1983
|
+
const clientsStore = {
|
|
1984
|
+
async getClient(clientId) {
|
|
1985
|
+
return clients.get(clientId);
|
|
1986
|
+
},
|
|
1987
|
+
// DCR for incoming MCP clients — stored locally, not proxied upstream
|
|
1988
|
+
async registerClient(client) {
|
|
1989
|
+
const full = client;
|
|
1990
|
+
clients.set(full.client_id, full);
|
|
1991
|
+
return full;
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
return {
|
|
1995
|
+
clientsStore,
|
|
1996
|
+
// Let the upstream server validate PKCE — we pass code_verifier through
|
|
1997
|
+
skipLocalPkceValidation: true,
|
|
1998
|
+
async authorize(_client, params, res) {
|
|
1999
|
+
const targetUrl = new URL(options.endpoints.authorizationUrl);
|
|
2000
|
+
targetUrl.searchParams.set("client_id", options.upstreamCredentials.clientId);
|
|
2001
|
+
targetUrl.searchParams.set("response_type", "code");
|
|
2002
|
+
targetUrl.searchParams.set("redirect_uri", params.redirectUri);
|
|
2003
|
+
targetUrl.searchParams.set("code_challenge", params.codeChallenge);
|
|
2004
|
+
targetUrl.searchParams.set("code_challenge_method", "S256");
|
|
2005
|
+
if (params.state) targetUrl.searchParams.set("state", params.state);
|
|
2006
|
+
if (params.scopes?.length) targetUrl.searchParams.set("scope", params.scopes.join(" "));
|
|
2007
|
+
res.redirect(targetUrl.toString());
|
|
2008
|
+
},
|
|
2009
|
+
async challengeForAuthorizationCode() {
|
|
2010
|
+
return "";
|
|
2011
|
+
},
|
|
2012
|
+
async exchangeAuthorizationCode(_client, authorizationCode, codeVerifier, redirectUri) {
|
|
2013
|
+
const body = new URLSearchParams({
|
|
2014
|
+
grant_type: "authorization_code",
|
|
2015
|
+
client_id: options.upstreamCredentials.clientId,
|
|
2016
|
+
code: authorizationCode
|
|
2017
|
+
});
|
|
2018
|
+
if (options.upstreamCredentials.clientSecret) {
|
|
2019
|
+
body.set("client_secret", options.upstreamCredentials.clientSecret);
|
|
2020
|
+
}
|
|
2021
|
+
if (codeVerifier) body.set("code_verifier", codeVerifier);
|
|
2022
|
+
if (redirectUri) body.set("redirect_uri", redirectUri);
|
|
2023
|
+
const response = await fetch(options.endpoints.tokenUrl, {
|
|
2024
|
+
method: "POST",
|
|
2025
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2026
|
+
body: body.toString()
|
|
2027
|
+
});
|
|
2028
|
+
if (!response.ok) {
|
|
2029
|
+
await response.body?.cancel();
|
|
2030
|
+
throw new Error(`Upstream token exchange failed: ${response.status}`);
|
|
2031
|
+
}
|
|
2032
|
+
return response.json();
|
|
2033
|
+
},
|
|
2034
|
+
async exchangeRefreshToken(_client, refreshToken, scopes) {
|
|
2035
|
+
const body = new URLSearchParams({
|
|
2036
|
+
grant_type: "refresh_token",
|
|
2037
|
+
client_id: options.upstreamCredentials.clientId,
|
|
2038
|
+
refresh_token: refreshToken
|
|
2039
|
+
});
|
|
2040
|
+
if (options.upstreamCredentials.clientSecret) {
|
|
2041
|
+
body.set("client_secret", options.upstreamCredentials.clientSecret);
|
|
2042
|
+
}
|
|
2043
|
+
if (scopes?.length) body.set("scope", scopes.join(" "));
|
|
2044
|
+
const response = await fetch(options.endpoints.tokenUrl, {
|
|
2045
|
+
method: "POST",
|
|
2046
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2047
|
+
body: body.toString()
|
|
2048
|
+
});
|
|
2049
|
+
if (!response.ok) {
|
|
2050
|
+
await response.body?.cancel();
|
|
2051
|
+
throw new Error(`Upstream token refresh failed: ${response.status}`);
|
|
2052
|
+
}
|
|
2053
|
+
return response.json();
|
|
2054
|
+
},
|
|
2055
|
+
verifyAccessToken: options.verifyAccessToken,
|
|
2056
|
+
...options.endpoints.revocationUrl ? {
|
|
2057
|
+
async revokeToken(_client, request) {
|
|
2058
|
+
const body = new URLSearchParams({
|
|
2059
|
+
token: request.token,
|
|
2060
|
+
client_id: options.upstreamCredentials.clientId
|
|
2061
|
+
});
|
|
2062
|
+
if (options.upstreamCredentials.clientSecret) {
|
|
2063
|
+
body.set("client_secret", options.upstreamCredentials.clientSecret);
|
|
2064
|
+
}
|
|
2065
|
+
if (request.token_type_hint) {
|
|
2066
|
+
body.set("token_type_hint", request.token_type_hint);
|
|
2067
|
+
}
|
|
2068
|
+
const response = await fetch(options.endpoints.revocationUrl, {
|
|
2069
|
+
method: "POST",
|
|
2070
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2071
|
+
body: body.toString()
|
|
2072
|
+
});
|
|
2073
|
+
if (!response.ok) {
|
|
2074
|
+
await response.body?.cancel();
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
} : {}
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/server/proxy.ts
|
|
2082
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index";
|
|
2083
|
+
import {
|
|
2084
|
+
CallToolResultSchema,
|
|
2085
|
+
ToolListChangedNotificationSchema,
|
|
2086
|
+
ResourceListChangedNotificationSchema,
|
|
2087
|
+
PromptListChangedNotificationSchema
|
|
2088
|
+
} from "@modelcontextprotocol/sdk/types";
|
|
2089
|
+
import { parseTemplate } from "url-template";
|
|
2090
|
+
async function buildProxyFromClient(client, options) {
|
|
2091
|
+
const cacheTtl = options?.cacheTtl ?? 3e4;
|
|
2092
|
+
const serverInfo = client.getServerVersion();
|
|
2093
|
+
const proxyName = options?.name ?? serverInfo?.name ?? "proxy";
|
|
2094
|
+
const proxyVersion = serverInfo?.version ?? "0.0.1";
|
|
2095
|
+
const proxy = new FastMCP({ name: proxyName, version: proxyVersion });
|
|
2096
|
+
const proxiedTools = /* @__PURE__ */ new Set();
|
|
2097
|
+
const proxiedResources = /* @__PURE__ */ new Set();
|
|
2098
|
+
const proxiedPrompts = /* @__PURE__ */ new Set();
|
|
2099
|
+
const lastSync = { tools: 0, resources: 0, prompts: 0 };
|
|
2100
|
+
async function resyncTools() {
|
|
2101
|
+
const tools = [];
|
|
2102
|
+
let cursor;
|
|
2103
|
+
do {
|
|
2104
|
+
const page = await client.listTools({ cursor });
|
|
2105
|
+
tools.push(...page.tools);
|
|
2106
|
+
cursor = page.nextCursor;
|
|
2107
|
+
} while (cursor);
|
|
2108
|
+
const incoming = new Set(tools.map((t) => t.name));
|
|
2109
|
+
for (const name of proxiedTools) {
|
|
2110
|
+
if (!incoming.has(name)) {
|
|
2111
|
+
proxy._removeTool(name);
|
|
2112
|
+
proxiedTools.delete(name);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
for (const tool of tools) {
|
|
2116
|
+
if (proxiedTools.has(tool.name)) {
|
|
2117
|
+
proxy._removeTool(tool.name);
|
|
2118
|
+
proxiedTools.delete(tool.name);
|
|
2119
|
+
}
|
|
2120
|
+
proxy.tool(
|
|
2121
|
+
{
|
|
2122
|
+
name: tool.name,
|
|
2123
|
+
...tool.title !== void 0 ? { title: tool.title } : {},
|
|
2124
|
+
description: tool.description ?? tool.name,
|
|
2125
|
+
inputSchema: tool.inputSchema ?? {}
|
|
2126
|
+
},
|
|
2127
|
+
async (args) => {
|
|
2128
|
+
const result = await client.callTool(
|
|
2129
|
+
{ name: tool.name, arguments: args },
|
|
2130
|
+
CallToolResultSchema
|
|
2131
|
+
);
|
|
2132
|
+
return new ToolResult(result);
|
|
2133
|
+
}
|
|
2134
|
+
);
|
|
2135
|
+
proxiedTools.add(tool.name);
|
|
2136
|
+
}
|
|
2137
|
+
lastSync.tools = Date.now();
|
|
2138
|
+
}
|
|
2139
|
+
async function resyncResources() {
|
|
2140
|
+
const incoming = /* @__PURE__ */ new Set();
|
|
2141
|
+
try {
|
|
2142
|
+
const resources = [];
|
|
2143
|
+
let cursor;
|
|
2144
|
+
do {
|
|
2145
|
+
const page = await client.listResources({ cursor });
|
|
2146
|
+
resources.push(...page.resources);
|
|
2147
|
+
cursor = page.nextCursor;
|
|
2148
|
+
} while (cursor);
|
|
2149
|
+
for (const resource of resources) {
|
|
2150
|
+
incoming.add(resource.uri);
|
|
2151
|
+
if (!proxiedResources.has(resource.uri)) {
|
|
2152
|
+
proxy.resource(
|
|
2153
|
+
{
|
|
2154
|
+
uri: resource.uri,
|
|
2155
|
+
name: resource.name,
|
|
2156
|
+
...resource.description !== void 0 ? { description: resource.description } : {},
|
|
2157
|
+
...resource.mimeType !== void 0 ? { mimeType: resource.mimeType } : {}
|
|
2158
|
+
},
|
|
2159
|
+
async () => {
|
|
2160
|
+
const result = await client.readResource({ uri: resource.uri });
|
|
2161
|
+
return new ResourceResult(
|
|
2162
|
+
result.contents
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
);
|
|
2166
|
+
proxiedResources.add(resource.uri);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
} catch {
|
|
2170
|
+
}
|
|
2171
|
+
try {
|
|
2172
|
+
const resourceTemplates = [];
|
|
2173
|
+
let templateCursor;
|
|
2174
|
+
do {
|
|
2175
|
+
const page = await client.listResourceTemplates({ cursor: templateCursor });
|
|
2176
|
+
resourceTemplates.push(...page.resourceTemplates);
|
|
2177
|
+
templateCursor = page.nextCursor;
|
|
2178
|
+
} while (templateCursor);
|
|
2179
|
+
for (const template of resourceTemplates) {
|
|
2180
|
+
const uriTemplate = template.uriTemplate;
|
|
2181
|
+
incoming.add(uriTemplate);
|
|
2182
|
+
if (!proxiedResources.has(uriTemplate)) {
|
|
2183
|
+
const expander = parseTemplate(uriTemplate);
|
|
2184
|
+
proxy.resource(
|
|
2185
|
+
{
|
|
2186
|
+
uri: uriTemplate,
|
|
2187
|
+
name: template.name,
|
|
2188
|
+
...template.description !== void 0 ? { description: template.description } : {},
|
|
2189
|
+
...template.mimeType !== void 0 ? { mimeType: template.mimeType } : {}
|
|
2190
|
+
},
|
|
2191
|
+
async (params) => {
|
|
2192
|
+
const actualUri = params ? expander.expand(params) : uriTemplate;
|
|
2193
|
+
const result = await client.readResource({ uri: actualUri });
|
|
2194
|
+
return new ResourceResult(
|
|
2195
|
+
result.contents
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
);
|
|
2199
|
+
proxiedResources.add(uriTemplate);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
} catch {
|
|
2203
|
+
}
|
|
2204
|
+
for (const uri of proxiedResources) {
|
|
2205
|
+
if (!incoming.has(uri)) {
|
|
2206
|
+
proxy._removeResource(uri);
|
|
2207
|
+
proxiedResources.delete(uri);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
lastSync.resources = Date.now();
|
|
2211
|
+
}
|
|
2212
|
+
async function resyncPrompts() {
|
|
2213
|
+
const prompts = [];
|
|
2214
|
+
let cursor;
|
|
2215
|
+
do {
|
|
2216
|
+
const page = await client.listPrompts({ cursor });
|
|
2217
|
+
prompts.push(...page.prompts);
|
|
2218
|
+
cursor = page.nextCursor;
|
|
2219
|
+
} while (cursor);
|
|
2220
|
+
const incoming = new Set(prompts.map((p) => p.name));
|
|
2221
|
+
for (const name of proxiedPrompts) {
|
|
2222
|
+
if (!incoming.has(name)) {
|
|
2223
|
+
proxy._removePrompt(name);
|
|
2224
|
+
proxiedPrompts.delete(name);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
for (const prompt of prompts) {
|
|
2228
|
+
if (!proxiedPrompts.has(prompt.name)) {
|
|
2229
|
+
proxy.prompt(
|
|
2230
|
+
{
|
|
2231
|
+
name: prompt.name,
|
|
2232
|
+
description: prompt.description,
|
|
2233
|
+
...prompt.arguments?.length ? { arguments: prompt.arguments } : {}
|
|
2234
|
+
},
|
|
2235
|
+
async (args) => {
|
|
2236
|
+
const result = await client.getPrompt({ name: prompt.name, arguments: args });
|
|
2237
|
+
return new PromptResult(result.messages, result.description);
|
|
2238
|
+
}
|
|
2239
|
+
);
|
|
2240
|
+
proxiedPrompts.add(prompt.name);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
lastSync.prompts = Date.now();
|
|
2244
|
+
}
|
|
2245
|
+
await Promise.all([
|
|
2246
|
+
resyncTools().catch(() => {
|
|
2247
|
+
}),
|
|
2248
|
+
resyncResources().catch(() => {
|
|
2249
|
+
}),
|
|
2250
|
+
resyncPrompts().catch(() => {
|
|
2251
|
+
})
|
|
2252
|
+
]);
|
|
2253
|
+
client.setNotificationHandler(ToolListChangedNotificationSchema, () => {
|
|
2254
|
+
resyncTools().catch(() => {
|
|
2255
|
+
});
|
|
2256
|
+
});
|
|
2257
|
+
client.setNotificationHandler(ResourceListChangedNotificationSchema, () => {
|
|
2258
|
+
resyncResources().catch(() => {
|
|
2259
|
+
});
|
|
2260
|
+
});
|
|
2261
|
+
client.setNotificationHandler(PromptListChangedNotificationSchema, () => {
|
|
2262
|
+
resyncPrompts().catch(() => {
|
|
2263
|
+
});
|
|
2264
|
+
});
|
|
2265
|
+
if (cacheTtl > 0) {
|
|
2266
|
+
proxy.use({
|
|
2267
|
+
async onListTools(ctx, next) {
|
|
2268
|
+
if (Date.now() - lastSync.tools > cacheTtl) await resyncTools().catch(() => {
|
|
2269
|
+
});
|
|
2270
|
+
return next();
|
|
2271
|
+
},
|
|
2272
|
+
async onListResources(ctx, next) {
|
|
2273
|
+
if (Date.now() - lastSync.resources > cacheTtl) await resyncResources().catch(() => {
|
|
2274
|
+
});
|
|
2275
|
+
return next();
|
|
2276
|
+
},
|
|
2277
|
+
async onListResourceTemplates(ctx, next) {
|
|
2278
|
+
if (Date.now() - lastSync.resources > cacheTtl) await resyncResources().catch(() => {
|
|
2279
|
+
});
|
|
2280
|
+
return next();
|
|
2281
|
+
},
|
|
2282
|
+
async onListPrompts(ctx, next) {
|
|
2283
|
+
if (Date.now() - lastSync.prompts > cacheTtl) await resyncPrompts().catch(() => {
|
|
2284
|
+
});
|
|
2285
|
+
return next();
|
|
2286
|
+
}
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
proxy._addCloseCallback(async () => {
|
|
2290
|
+
await client.close();
|
|
2291
|
+
});
|
|
2292
|
+
return proxy;
|
|
2293
|
+
}
|
|
2294
|
+
async function createProxy(config, name) {
|
|
2295
|
+
let transport;
|
|
2296
|
+
if (config.type === "stdio") {
|
|
2297
|
+
const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio");
|
|
2298
|
+
transport = new StdioClientTransport({
|
|
2299
|
+
command: config.command,
|
|
2300
|
+
args: config.args,
|
|
2301
|
+
env: config.env,
|
|
2302
|
+
cwd: config.cwd
|
|
2303
|
+
});
|
|
2304
|
+
} else {
|
|
2305
|
+
const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp");
|
|
2306
|
+
transport = new StreamableHTTPClientTransport(new URL(config.url), {
|
|
2307
|
+
requestInit: config.requestInit
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
const client = new Client(
|
|
2311
|
+
{ name: name ?? "fastmcp-proxy", version: "0.0.1" },
|
|
2312
|
+
{ capabilities: {} }
|
|
2313
|
+
);
|
|
2314
|
+
await client.connect(transport);
|
|
2315
|
+
return buildProxyFromClient(client, { cacheTtl: config.cacheTtl, name });
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
// src/server/apps/FastMCPApp.ts
|
|
2319
|
+
var FastMCPApp = class {
|
|
2320
|
+
server;
|
|
2321
|
+
constructor(options) {
|
|
2322
|
+
this.server = new FastMCP({ name: options.name, version: options.version });
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* Register an entry-point tool.
|
|
2326
|
+
* Entry-points are visible to the LLM (visibility: ['model', 'app']) and
|
|
2327
|
+
* automatically linked to a generated ui:// resource URI.
|
|
2328
|
+
*/
|
|
2329
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2330
|
+
entrypoint(config, handler) {
|
|
2331
|
+
const resourceUri = `ui://${config.name}`;
|
|
2332
|
+
const toolConfig = {
|
|
2333
|
+
name: config.name,
|
|
2334
|
+
description: config.description,
|
|
2335
|
+
...config.title !== void 0 ? { title: config.title } : {},
|
|
2336
|
+
ui: { visibility: ["model", "app"], resourceUri }
|
|
2337
|
+
};
|
|
2338
|
+
this.server.tool(toolConfig, handler);
|
|
2339
|
+
this.server.resource(
|
|
2340
|
+
{ uri: resourceUri, mimeType: "text/html;profile=mcp-app", name: config.name },
|
|
2341
|
+
() => `<!doctype html><html><head><meta charset="utf-8"></head><body><!-- fastmcp ui-runtime placeholder --></body></html>`
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
/**
|
|
2345
|
+
* Register a backend tool.
|
|
2346
|
+
* Backend tools are hidden from listTools by default (visibility: ['app']).
|
|
2347
|
+
*/
|
|
2348
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2349
|
+
backendTool(config, handler) {
|
|
2350
|
+
const visibility = config.visibility ?? ["app"];
|
|
2351
|
+
const toolConfig = {
|
|
2352
|
+
name: config.name,
|
|
2353
|
+
description: config.description,
|
|
2354
|
+
...config.title !== void 0 ? { title: config.title } : {},
|
|
2355
|
+
ui: { visibility }
|
|
2356
|
+
};
|
|
2357
|
+
this.server.tool(toolConfig, handler);
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Returns a reference to a tool that resolves to the correct external name
|
|
2361
|
+
* (including any mount prefix) when evaluated inside a request handler.
|
|
2362
|
+
* Call inside a handler function — not at definition time.
|
|
2363
|
+
*/
|
|
2364
|
+
toolRef(name) {
|
|
2365
|
+
const ctx = contextStore.getStore();
|
|
2366
|
+
if (!ctx) return name;
|
|
2367
|
+
return ctx.resolveToolName(name);
|
|
2368
|
+
}
|
|
2369
|
+
};
|
|
2370
|
+
|
|
2371
|
+
// src/server/apps/actionRef.ts
|
|
2372
|
+
function actionRef(name) {
|
|
2373
|
+
return contextStore.getStore()?.resolveToolName(name) ?? name;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
// src/server/apps/generative.ts
|
|
2377
|
+
import { runInNewContext } from "vm";
|
|
2378
|
+
|
|
2379
|
+
// src/server/apps/components.ts
|
|
2380
|
+
var components_exports = {};
|
|
2381
|
+
__export(components_exports, {
|
|
2382
|
+
Area: () => Area,
|
|
2383
|
+
Badge: () => Badge,
|
|
2384
|
+
Bar: () => Bar,
|
|
2385
|
+
Button: () => Button,
|
|
2386
|
+
COMPONENT_CATALOG: () => COMPONENT_CATALOG,
|
|
2387
|
+
Column: () => Column,
|
|
2388
|
+
ForEach: () => ForEach,
|
|
2389
|
+
Grid: () => Grid,
|
|
2390
|
+
If: () => If,
|
|
2391
|
+
Input: () => Input,
|
|
2392
|
+
Line: () => Line,
|
|
2393
|
+
Pie: () => Pie,
|
|
2394
|
+
Row: () => Row,
|
|
2395
|
+
Rx: () => Rx,
|
|
2396
|
+
Select: () => Select,
|
|
2397
|
+
Table: () => Table,
|
|
2398
|
+
Text: () => Text
|
|
2399
|
+
});
|
|
2400
|
+
var COMPONENT_CATALOG = [
|
|
2401
|
+
{ type: "column", description: "Vertical stack layout. Props: gap, align, padding. Children: any components." },
|
|
2402
|
+
{ type: "row", description: "Horizontal stack layout. Props: gap, align, justify, wrap. Children: any components." },
|
|
2403
|
+
{ type: "grid", description: "CSS grid layout. Props: columns, gap. Children: any components." },
|
|
2404
|
+
{ type: "text", description: "Text display. Props: content (string or Rx), variant (heading|body|caption|code), color." },
|
|
2405
|
+
{ type: "badge", description: "Status badge. Props: text, color, variant." },
|
|
2406
|
+
{ type: "table", description: "Data table. Props: columns (string[]), rows (string[][]). No children." },
|
|
2407
|
+
{ type: "chart-bar", description: "Bar chart. Props: data (object[]), xKey, yKey, title." },
|
|
2408
|
+
{ type: "chart-line", description: "Line chart. Props: data (object[]), xKey, yKey, title." },
|
|
2409
|
+
{ type: "chart-area", description: "Area chart. Props: data (object[]), xKey, yKey, title." },
|
|
2410
|
+
{ type: "chart-pie", description: "Pie chart. Props: data (object[]), labelKey, valueKey, title." },
|
|
2411
|
+
{ type: "input", description: "Form text input. Props: name, label, type (text|email|number|password), placeholder, required." },
|
|
2412
|
+
{ type: "select", description: "Dropdown select. Props: name, label, options (string[] or {value,label}[]), required." },
|
|
2413
|
+
{ type: "button", description: "Clickable button. Props: label, action (tool name string), variant, disabled." },
|
|
2414
|
+
{ type: "if", description: "Conditional rendering. branches: [{condition, node}], fallback." },
|
|
2415
|
+
{ type: "foreach", description: "Dynamic list. Props: items (binding expression). Children: [template]." },
|
|
2416
|
+
{ type: "rx", description: "Reactive value. Props: expression (client-side state binding). Embeddable as prop." }
|
|
2417
|
+
];
|
|
2418
|
+
function withChildren(type, props, children) {
|
|
2419
|
+
const node = { type };
|
|
2420
|
+
if (props && Object.keys(props).length > 0) node.props = props;
|
|
2421
|
+
if (children && children.length > 0) node.children = children;
|
|
2422
|
+
return node;
|
|
2423
|
+
}
|
|
2424
|
+
function withProps(type, props) {
|
|
2425
|
+
return { type, props };
|
|
2426
|
+
}
|
|
2427
|
+
function Column(props, children) {
|
|
2428
|
+
return withChildren("column", props, children);
|
|
2429
|
+
}
|
|
2430
|
+
function Row(props, children) {
|
|
2431
|
+
return withChildren("row", props, children);
|
|
2432
|
+
}
|
|
2433
|
+
function Grid(props, children) {
|
|
2434
|
+
return withChildren("grid", props, children);
|
|
2435
|
+
}
|
|
2436
|
+
function Text(content, extraProps) {
|
|
2437
|
+
return withProps("text", { content, ...extraProps });
|
|
2438
|
+
}
|
|
2439
|
+
function Badge(text, extraProps) {
|
|
2440
|
+
return withProps("badge", { text, ...extraProps });
|
|
2441
|
+
}
|
|
2442
|
+
function Table(props) {
|
|
2443
|
+
return withProps("table", props);
|
|
2444
|
+
}
|
|
2445
|
+
function Bar(props) {
|
|
2446
|
+
return withProps("chart-bar", props);
|
|
2447
|
+
}
|
|
2448
|
+
function Line(props) {
|
|
2449
|
+
return withProps("chart-line", props);
|
|
2450
|
+
}
|
|
2451
|
+
function Area(props) {
|
|
2452
|
+
return withProps("chart-area", props);
|
|
2453
|
+
}
|
|
2454
|
+
function Pie(props) {
|
|
2455
|
+
return withProps("chart-pie", props);
|
|
2456
|
+
}
|
|
2457
|
+
function Input(props) {
|
|
2458
|
+
return withProps("input", props);
|
|
2459
|
+
}
|
|
2460
|
+
function Select(props) {
|
|
2461
|
+
return withProps("select", props);
|
|
2462
|
+
}
|
|
2463
|
+
function Button(props) {
|
|
2464
|
+
return withProps("button", props);
|
|
2465
|
+
}
|
|
2466
|
+
function makeIfNode(branches, fallback) {
|
|
2467
|
+
const node = { type: "if", branches, ...fallback !== void 0 ? { fallback } : {} };
|
|
2468
|
+
Object.defineProperty(node, "elif", {
|
|
2469
|
+
enumerable: false,
|
|
2470
|
+
value(condition, child) {
|
|
2471
|
+
return makeIfNode([...branches, { condition, node: child }], fallback);
|
|
2472
|
+
}
|
|
2473
|
+
});
|
|
2474
|
+
Object.defineProperty(node, "else", {
|
|
2475
|
+
enumerable: false,
|
|
2476
|
+
value(child) {
|
|
2477
|
+
return makeIfNode(branches, child);
|
|
2478
|
+
}
|
|
2479
|
+
});
|
|
2480
|
+
return node;
|
|
2481
|
+
}
|
|
2482
|
+
function If(condition, then, fallback) {
|
|
2483
|
+
return makeIfNode([{ condition, node: then }], fallback);
|
|
2484
|
+
}
|
|
2485
|
+
function ForEach(items, template) {
|
|
2486
|
+
return { type: "foreach", props: { items }, children: [template] };
|
|
2487
|
+
}
|
|
2488
|
+
function Rx(expression) {
|
|
2489
|
+
return withProps("rx", { expression });
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// src/server/apps/generative.ts
|
|
2493
|
+
var GenerativeUI = class {
|
|
2494
|
+
server;
|
|
2495
|
+
constructor() {
|
|
2496
|
+
this.server = new FastMCP({ name: "generative-ui" });
|
|
2497
|
+
this.server.tool(
|
|
2498
|
+
{
|
|
2499
|
+
name: "search_components",
|
|
2500
|
+
description: "List available UI components with their types and descriptions"
|
|
2501
|
+
},
|
|
2502
|
+
() => new ToolResult({
|
|
2503
|
+
content: [{ type: "text", text: JSON.stringify(COMPONENT_CATALOG) }]
|
|
2504
|
+
})
|
|
2505
|
+
);
|
|
2506
|
+
this.server.tool(
|
|
2507
|
+
{
|
|
2508
|
+
name: "generate_ui",
|
|
2509
|
+
description: "Execute a UI component expression and return the component tree. Use search_components to discover available component APIs first.",
|
|
2510
|
+
inputSchema: {
|
|
2511
|
+
type: "object",
|
|
2512
|
+
properties: {
|
|
2513
|
+
code: { type: "string", description: "A JavaScript expression using the component builders (Column, Text, Button, etc.) that evaluates to a component tree" }
|
|
2514
|
+
},
|
|
2515
|
+
required: ["code"]
|
|
2516
|
+
}
|
|
2517
|
+
},
|
|
2518
|
+
(args) => {
|
|
2519
|
+
const { code } = args;
|
|
2520
|
+
const sandbox = { ...components_exports };
|
|
2521
|
+
delete sandbox["COMPONENT_CATALOG"];
|
|
2522
|
+
try {
|
|
2523
|
+
return runInNewContext(code, sandbox, { timeout: 2e3 });
|
|
2524
|
+
} catch (err) {
|
|
2525
|
+
throw new Error(`[generate_ui] ${err instanceof Error ? err.message : String(err)}`);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
};
|
|
2531
|
+
|
|
2532
|
+
// src/server/apps/providers/Approval.ts
|
|
2533
|
+
var Approval = class {
|
|
2534
|
+
server;
|
|
2535
|
+
constructor() {
|
|
2536
|
+
this.server = new FastMCP({ name: "approval" });
|
|
2537
|
+
this.server.tool(
|
|
2538
|
+
{
|
|
2539
|
+
name: "approval_request",
|
|
2540
|
+
description: "Present a confirm/deny approval card to the user",
|
|
2541
|
+
inputSchema: {
|
|
2542
|
+
type: "object",
|
|
2543
|
+
properties: {
|
|
2544
|
+
message: { type: "string", description: "The question or action to seek approval for" }
|
|
2545
|
+
},
|
|
2546
|
+
required: ["message"]
|
|
2547
|
+
},
|
|
2548
|
+
ui: { visibility: ["model", "app"] }
|
|
2549
|
+
},
|
|
2550
|
+
(args) => {
|
|
2551
|
+
const message = args.message ?? "Approve?";
|
|
2552
|
+
return Column({}, [
|
|
2553
|
+
Text(message),
|
|
2554
|
+
Button({ label: "Confirm", action: actionRef("approval_confirm") }),
|
|
2555
|
+
Button({ label: "Deny", action: actionRef("approval_deny"), variant: "secondary" })
|
|
2556
|
+
]);
|
|
2557
|
+
}
|
|
2558
|
+
);
|
|
2559
|
+
this.server.resource(
|
|
2560
|
+
{ uri: "ui://approval_request", mimeType: "text/html;profile=mcp-app", name: "approval_request" },
|
|
2561
|
+
() => `<!doctype html><html><head><meta charset="utf-8"></head><body><!-- fastmcp ui-runtime placeholder --></body></html>`
|
|
2562
|
+
);
|
|
2563
|
+
this.server.tool(
|
|
2564
|
+
{
|
|
2565
|
+
name: "approval_confirm",
|
|
2566
|
+
description: "Record a confirmed approval decision",
|
|
2567
|
+
ui: { visibility: ["app"] }
|
|
2568
|
+
},
|
|
2569
|
+
() => ({ decision: "approved" })
|
|
2570
|
+
);
|
|
2571
|
+
this.server.tool(
|
|
2572
|
+
{
|
|
2573
|
+
name: "approval_deny",
|
|
2574
|
+
description: "Record a denied approval decision",
|
|
2575
|
+
ui: { visibility: ["app"] }
|
|
2576
|
+
},
|
|
2577
|
+
() => ({ decision: "denied" })
|
|
2578
|
+
);
|
|
2579
|
+
}
|
|
2580
|
+
};
|
|
2581
|
+
|
|
2582
|
+
// src/server/apps/providers/Choice.ts
|
|
2583
|
+
var Choice = class {
|
|
2584
|
+
server;
|
|
2585
|
+
constructor() {
|
|
2586
|
+
this.server = new FastMCP({ name: "choice" });
|
|
2587
|
+
this.server.tool(
|
|
2588
|
+
{
|
|
2589
|
+
name: "choice_present",
|
|
2590
|
+
description: "Present a list of options for the user to choose from",
|
|
2591
|
+
inputSchema: {
|
|
2592
|
+
type: "object",
|
|
2593
|
+
properties: {
|
|
2594
|
+
question: { type: "string", description: "The question to ask the user" },
|
|
2595
|
+
options: { type: "array", items: { type: "string" }, description: "The options to present" }
|
|
2596
|
+
},
|
|
2597
|
+
required: ["question", "options"]
|
|
2598
|
+
},
|
|
2599
|
+
ui: { visibility: ["model", "app"] }
|
|
2600
|
+
},
|
|
2601
|
+
(args) => {
|
|
2602
|
+
const options = args.options ?? [];
|
|
2603
|
+
return Row(
|
|
2604
|
+
{},
|
|
2605
|
+
// Each button carries its option value as args so the host can
|
|
2606
|
+
// forward it to choice_select without extra state.
|
|
2607
|
+
options.map(
|
|
2608
|
+
(opt) => Button({ label: opt, action: actionRef("choice_select"), args: { option: opt } })
|
|
2609
|
+
)
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
);
|
|
2613
|
+
this.server.resource(
|
|
2614
|
+
{ uri: "ui://choice_present", mimeType: "text/html;profile=mcp-app", name: "choice_present" },
|
|
2615
|
+
() => `<!doctype html><html><head><meta charset="utf-8"></head><body><!-- fastmcp ui-runtime placeholder --></body></html>`
|
|
2616
|
+
);
|
|
2617
|
+
this.server.tool(
|
|
2618
|
+
{
|
|
2619
|
+
name: "choice_select",
|
|
2620
|
+
description: "Record the user's selection",
|
|
2621
|
+
ui: { visibility: ["app"] }
|
|
2622
|
+
},
|
|
2623
|
+
(args) => ({ selected: args.option })
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2626
|
+
};
|
|
2627
|
+
|
|
2628
|
+
// src/server/apps/providers/FileUpload.ts
|
|
2629
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2630
|
+
function makeInMemoryAdapter() {
|
|
2631
|
+
const store = /* @__PURE__ */ new Map();
|
|
2632
|
+
return {
|
|
2633
|
+
save: (handle, file) => store.set(handle, file),
|
|
2634
|
+
load: (handle) => store.get(handle),
|
|
2635
|
+
delete: (handle) => store.delete(handle)
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2638
|
+
var SESSION_HANDLES_KEY = "__fastmcp_file_handles";
|
|
2639
|
+
var FileUpload = class {
|
|
2640
|
+
server;
|
|
2641
|
+
_storage;
|
|
2642
|
+
constructor(options) {
|
|
2643
|
+
this._storage = options?.storage ?? makeInMemoryAdapter();
|
|
2644
|
+
this.server = new FastMCP({ name: "file-upload" });
|
|
2645
|
+
const storage = this._storage;
|
|
2646
|
+
this.server.tool(
|
|
2647
|
+
{
|
|
2648
|
+
name: "file_upload_open",
|
|
2649
|
+
description: "Open a file picker UI for the user to upload a file",
|
|
2650
|
+
inputSchema: {
|
|
2651
|
+
type: "object",
|
|
2652
|
+
properties: {
|
|
2653
|
+
prompt: { type: "string", description: "Instructions shown above the file picker" }
|
|
2654
|
+
}
|
|
2655
|
+
},
|
|
2656
|
+
ui: { visibility: ["model", "app"] }
|
|
2657
|
+
},
|
|
2658
|
+
(args) => {
|
|
2659
|
+
return Column({}, [
|
|
2660
|
+
Text(args.prompt ?? "Upload a file"),
|
|
2661
|
+
Button({ label: "Choose file", action: actionRef("file_upload_submit") })
|
|
2662
|
+
]);
|
|
2663
|
+
}
|
|
2664
|
+
);
|
|
2665
|
+
this.server.tool(
|
|
2666
|
+
{
|
|
2667
|
+
name: "file_upload_submit",
|
|
2668
|
+
description: "Receive and store uploaded file bytes server-side",
|
|
2669
|
+
inputSchema: {
|
|
2670
|
+
type: "object",
|
|
2671
|
+
properties: {
|
|
2672
|
+
name: { type: "string", description: "File name" },
|
|
2673
|
+
mimeType: { type: "string", description: "MIME type of the file" },
|
|
2674
|
+
data: { type: "string", description: "Base64-encoded file content" }
|
|
2675
|
+
},
|
|
2676
|
+
required: ["name", "mimeType", "data"]
|
|
2677
|
+
},
|
|
2678
|
+
ui: { visibility: ["app"] }
|
|
2679
|
+
},
|
|
2680
|
+
(args) => {
|
|
2681
|
+
const handle = randomUUID3();
|
|
2682
|
+
const fileData = Buffer.from(args.data, "base64");
|
|
2683
|
+
storage.save(handle, {
|
|
2684
|
+
handle,
|
|
2685
|
+
name: args.name,
|
|
2686
|
+
mimeType: args.mimeType,
|
|
2687
|
+
data: fileData
|
|
2688
|
+
});
|
|
2689
|
+
const ctx = contextStore.getStore();
|
|
2690
|
+
if (ctx) {
|
|
2691
|
+
const existing = ctx.getState(SESSION_HANDLES_KEY) ?? [];
|
|
2692
|
+
ctx.setState(SESSION_HANDLES_KEY, [...existing, handle]);
|
|
2693
|
+
ctx.onClose(() => storage.delete(handle));
|
|
2694
|
+
}
|
|
2695
|
+
const uri = `ui://files/${handle}`;
|
|
2696
|
+
return { handle, uri };
|
|
2697
|
+
}
|
|
2698
|
+
);
|
|
2699
|
+
this.server.tool(
|
|
2700
|
+
{
|
|
2701
|
+
name: "file_upload_delete",
|
|
2702
|
+
description: "Delete a previously uploaded file by its handle",
|
|
2703
|
+
inputSchema: {
|
|
2704
|
+
type: "object",
|
|
2705
|
+
properties: {
|
|
2706
|
+
handle: { type: "string", description: "The file handle returned by file_upload_submit" }
|
|
2707
|
+
},
|
|
2708
|
+
required: ["handle"]
|
|
2709
|
+
},
|
|
2710
|
+
ui: { visibility: ["app"] }
|
|
2711
|
+
},
|
|
2712
|
+
(args) => {
|
|
2713
|
+
const handle = args.handle;
|
|
2714
|
+
storage.delete(handle);
|
|
2715
|
+
const ctx = contextStore.getStore();
|
|
2716
|
+
if (ctx) {
|
|
2717
|
+
const existing = ctx.getState(SESSION_HANDLES_KEY) ?? [];
|
|
2718
|
+
ctx.setState(SESSION_HANDLES_KEY, existing.filter((h) => h !== handle));
|
|
2719
|
+
}
|
|
2720
|
+
return { deleted: handle };
|
|
2721
|
+
}
|
|
2722
|
+
);
|
|
2723
|
+
this.server.resource(
|
|
2724
|
+
{ uri: "ui://files/{handle}", name: "uploaded-file", mimeType: "application/octet-stream" },
|
|
2725
|
+
(params) => {
|
|
2726
|
+
const h = params?.handle;
|
|
2727
|
+
if (!h) throw new Error("Missing file handle");
|
|
2728
|
+
const file = storage.load(h);
|
|
2729
|
+
if (!file) throw new Error(`File not found: ${h}`);
|
|
2730
|
+
const uri = `ui://files/${h}`;
|
|
2731
|
+
if (file.mimeType.startsWith("text/")) {
|
|
2732
|
+
return new ResourceResult([{ uri, mimeType: file.mimeType, text: file.data.toString("utf8") }]);
|
|
2733
|
+
}
|
|
2734
|
+
return new ResourceResult([{ uri, mimeType: file.mimeType, blob: file.data.toString("base64") }]);
|
|
2735
|
+
}
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
};
|
|
2739
|
+
|
|
2740
|
+
// src/server/apps/providers/FormInput.ts
|
|
2741
|
+
function buildField(fieldName, fieldSchema, required) {
|
|
2742
|
+
const isRequired = required.includes(fieldName);
|
|
2743
|
+
const type = fieldSchema.type;
|
|
2744
|
+
if (Array.isArray(fieldSchema.enum)) {
|
|
2745
|
+
return Select({ name: fieldName, label: fieldName, options: fieldSchema.enum, required: isRequired });
|
|
2746
|
+
}
|
|
2747
|
+
if (type === "number" || type === "integer") {
|
|
2748
|
+
return Input({ name: fieldName, label: fieldName, type: "number", required: isRequired });
|
|
2749
|
+
}
|
|
2750
|
+
return Input({ name: fieldName, label: fieldName, type: "text", required: isRequired });
|
|
2751
|
+
}
|
|
2752
|
+
async function validateWithSchema(schema, data) {
|
|
2753
|
+
const result = await schema["~standard"].validate(data);
|
|
2754
|
+
if (result.issues && result.issues.length > 0) {
|
|
2755
|
+
const errors = {};
|
|
2756
|
+
for (const issue of result.issues) {
|
|
2757
|
+
const path = issue.path?.map((p) => String(typeof p === "object" ? p.key : p)).join(".") ?? "root";
|
|
2758
|
+
errors[path] = String(issue.message);
|
|
2759
|
+
}
|
|
2760
|
+
return { valid: false, errors };
|
|
2761
|
+
}
|
|
2762
|
+
return { valid: true, value: result.value };
|
|
2763
|
+
}
|
|
2764
|
+
var FormInput = class {
|
|
2765
|
+
server;
|
|
2766
|
+
constructor(options) {
|
|
2767
|
+
this.server = new FastMCP({ name: options.name });
|
|
2768
|
+
const { name, description, schema } = options;
|
|
2769
|
+
const submitName = `${name}_submit`;
|
|
2770
|
+
this.server.resource(
|
|
2771
|
+
{ uri: `ui://${name}`, mimeType: "text/html;profile=mcp-app", name },
|
|
2772
|
+
() => `<!doctype html><html><head><meta charset="utf-8"></head><body><!-- fastmcp ui-runtime placeholder --></body></html>`
|
|
2773
|
+
);
|
|
2774
|
+
this.server.tool(
|
|
2775
|
+
{ name, description, ui: { visibility: ["model", "app"] } },
|
|
2776
|
+
async () => {
|
|
2777
|
+
const jsonSchema = await toJsonSchema(schema, `form "${name}"`);
|
|
2778
|
+
const properties = jsonSchema.properties ?? {};
|
|
2779
|
+
const required = jsonSchema.required ?? [];
|
|
2780
|
+
const fields = Object.entries(properties).map(
|
|
2781
|
+
([fieldName, fieldSchema]) => buildField(fieldName, fieldSchema, required)
|
|
2782
|
+
);
|
|
2783
|
+
return Column({}, [...fields, Button({ label: "Submit", action: actionRef(submitName) })]);
|
|
2784
|
+
}
|
|
2785
|
+
);
|
|
2786
|
+
this.server.tool(
|
|
2787
|
+
{
|
|
2788
|
+
name: submitName,
|
|
2789
|
+
description: `Submit ${name} form data`,
|
|
2790
|
+
inputSchema: {
|
|
2791
|
+
type: "object",
|
|
2792
|
+
properties: {
|
|
2793
|
+
data: { type: "object", description: "Form field values keyed by field name" }
|
|
2794
|
+
},
|
|
2795
|
+
required: ["data"]
|
|
2796
|
+
},
|
|
2797
|
+
ui: { visibility: ["app"] }
|
|
2798
|
+
},
|
|
2799
|
+
async (args) => {
|
|
2800
|
+
const result = await validateWithSchema(schema, args.data);
|
|
2801
|
+
if (!result.valid) {
|
|
2802
|
+
return { errors: result.errors };
|
|
2803
|
+
}
|
|
2804
|
+
return result.value;
|
|
2805
|
+
}
|
|
2806
|
+
);
|
|
2807
|
+
}
|
|
2808
|
+
};
|
|
2809
|
+
export {
|
|
2810
|
+
Approval,
|
|
2811
|
+
Area,
|
|
2812
|
+
AuthorizationError,
|
|
2813
|
+
Badge,
|
|
2814
|
+
Bar,
|
|
2815
|
+
Button,
|
|
2816
|
+
CachingMiddleware,
|
|
2817
|
+
CancellationMiddleware,
|
|
2818
|
+
Choice,
|
|
2819
|
+
Column,
|
|
2820
|
+
ErrorNormalizationMiddleware,
|
|
2821
|
+
FastMCP,
|
|
2822
|
+
FastMCPApp,
|
|
2823
|
+
File,
|
|
2824
|
+
FileUpload,
|
|
2825
|
+
FilterTransform,
|
|
2826
|
+
ForEach,
|
|
2827
|
+
FormInput,
|
|
2828
|
+
GenerativeUI,
|
|
2829
|
+
Grid,
|
|
2830
|
+
If,
|
|
2831
|
+
Image,
|
|
2832
|
+
Input,
|
|
2833
|
+
Line,
|
|
2834
|
+
LoggingMiddleware,
|
|
2835
|
+
NamespaceTransform,
|
|
2836
|
+
Pie,
|
|
2837
|
+
PromptResult,
|
|
2838
|
+
PromptsAsTools,
|
|
2839
|
+
RateLimitingMiddleware,
|
|
2840
|
+
ResourceResult,
|
|
2841
|
+
ResourcesAsTools,
|
|
2842
|
+
Row,
|
|
2843
|
+
Rx,
|
|
2844
|
+
Select,
|
|
2845
|
+
SizeLimitingMiddleware,
|
|
2846
|
+
Table,
|
|
2847
|
+
Text,
|
|
2848
|
+
ToolResult,
|
|
2849
|
+
VersionFilter,
|
|
2850
|
+
actionRef,
|
|
2851
|
+
createProxy,
|
|
2852
|
+
debugTokenVerifier,
|
|
2853
|
+
introspectionVerifier,
|
|
2854
|
+
jwtVerifier,
|
|
2855
|
+
multiAuth,
|
|
2856
|
+
oauthProvider,
|
|
2857
|
+
oauthProxy,
|
|
2858
|
+
redescribeTool,
|
|
2859
|
+
renameTool,
|
|
2860
|
+
requireScopes,
|
|
2861
|
+
staticTokenVerifier
|
|
2862
|
+
};
|
|
2863
|
+
//# sourceMappingURL=server.js.map
|