fastmcp 1.0.0 → 1.0.2
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/.github/workflows/feature.yaml +37 -0
- package/.github/workflows/main.yaml +45 -0
- package/LICENSE +25 -0
- package/README.md +197 -0
- package/dist/FastMCP.d.ts +69 -0
- package/dist/FastMCP.js +318 -0
- package/dist/FastMCP.js.map +1 -0
- package/dist/bin/fastmcp.d.ts +1 -0
- package/dist/bin/fastmcp.js +42 -0
- package/dist/bin/fastmcp.js.map +1 -0
- package/eslint.config.js +3 -0
- package/package.json +59 -6
- package/src/FastMCP.test.ts +179 -0
- package/src/FastMCP.ts +436 -0
- package/src/bin/fastmcp.ts +49 -0
- package/tsconfig.json +3 -0
package/src/FastMCP.ts
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ErrorCode,
|
|
7
|
+
GetPromptRequestSchema,
|
|
8
|
+
ListPromptsRequestSchema,
|
|
9
|
+
ListResourcesRequestSchema,
|
|
10
|
+
ListToolsRequestSchema,
|
|
11
|
+
McpError,
|
|
12
|
+
ReadResourceRequestSchema,
|
|
13
|
+
ServerCapabilities,
|
|
14
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
16
|
+
import type { z } from "zod";
|
|
17
|
+
import http from "http";
|
|
18
|
+
|
|
19
|
+
type ToolParameters = z.ZodTypeAny;
|
|
20
|
+
|
|
21
|
+
type Tool<Params extends ToolParameters = ToolParameters> = {
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
parameters?: Params;
|
|
25
|
+
execute: (args: z.infer<Params>) => Promise<unknown>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type Resource = {
|
|
29
|
+
uri: string;
|
|
30
|
+
name: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
mimeType?: string;
|
|
33
|
+
load: () => Promise<{ text: string } | { blob: string }>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type PromptArgument = Readonly<{
|
|
37
|
+
name: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
required?: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
type ArgumentsToObject<T extends PromptArgument[]> = {
|
|
43
|
+
[K in T[number]["name"]]: Extract<
|
|
44
|
+
T[number],
|
|
45
|
+
{ name: K }
|
|
46
|
+
>["required"] extends true
|
|
47
|
+
? string
|
|
48
|
+
: string | undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type Prompt<
|
|
52
|
+
Arguments extends PromptArgument[] = PromptArgument[],
|
|
53
|
+
Args = ArgumentsToObject<Arguments>,
|
|
54
|
+
> = {
|
|
55
|
+
name: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
arguments?: Arguments;
|
|
58
|
+
load: (args: Args) => Promise<string>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type ServerOptions = {
|
|
62
|
+
name: string;
|
|
63
|
+
version: `${number}.${number}.${number}`;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export class FastMCP {
|
|
67
|
+
#tools: Tool[];
|
|
68
|
+
#resources: Resource[];
|
|
69
|
+
#prompts: Prompt[];
|
|
70
|
+
#server: Server | null = null;
|
|
71
|
+
#options: ServerOptions;
|
|
72
|
+
|
|
73
|
+
constructor(public options: ServerOptions) {
|
|
74
|
+
this.#options = options;
|
|
75
|
+
this.#tools = [];
|
|
76
|
+
this.#resources = [];
|
|
77
|
+
this.#prompts = [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private setupHandlers(server: Server) {
|
|
81
|
+
this.setupErrorHandling(server);
|
|
82
|
+
|
|
83
|
+
if (this.#tools.length) {
|
|
84
|
+
this.setupToolHandlers(server);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (this.#resources.length) {
|
|
88
|
+
this.setupResourceHandlers(server);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (this.#prompts.length) {
|
|
92
|
+
this.setupPromptHandlers(server);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private setupErrorHandling(server: Server) {
|
|
97
|
+
server.onerror = (error) => {
|
|
98
|
+
console.error("[MCP Error]", error);
|
|
99
|
+
};
|
|
100
|
+
process.on("SIGINT", async () => {
|
|
101
|
+
await server.close();
|
|
102
|
+
process.exit(0);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private setupToolHandlers(server: Server) {
|
|
107
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
108
|
+
return {
|
|
109
|
+
tools: this.#tools.map((tool) => {
|
|
110
|
+
return {
|
|
111
|
+
name: tool.name,
|
|
112
|
+
description: tool.description,
|
|
113
|
+
inputSchema: tool.parameters
|
|
114
|
+
? zodToJsonSchema(tool.parameters)
|
|
115
|
+
: undefined,
|
|
116
|
+
};
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
122
|
+
const tool = this.#tools.find(
|
|
123
|
+
(tool) => tool.name === request.params.name,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (!tool) {
|
|
127
|
+
throw new McpError(
|
|
128
|
+
ErrorCode.MethodNotFound,
|
|
129
|
+
`Unknown tool: ${request.params.name}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let args: any = undefined;
|
|
134
|
+
|
|
135
|
+
if (tool.parameters) {
|
|
136
|
+
const parsed = tool.parameters.safeParse(request.params.arguments);
|
|
137
|
+
|
|
138
|
+
if (!parsed.success) {
|
|
139
|
+
throw new McpError(
|
|
140
|
+
ErrorCode.InvalidRequest,
|
|
141
|
+
`Invalid ${request.params.name} arguments`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
args = parsed.data;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let result: any;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
result = await tool.execute(args);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
content: [{ type: "text", text: `Error: ${error}` }],
|
|
155
|
+
isError: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (typeof result === "string") {
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text: result }],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private setupResourceHandlers(server: Server) {
|
|
172
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
173
|
+
return {
|
|
174
|
+
resources: this.#resources.map((resource) => {
|
|
175
|
+
return {
|
|
176
|
+
uri: resource.uri,
|
|
177
|
+
name: resource.name,
|
|
178
|
+
mimeType: resource.mimeType,
|
|
179
|
+
};
|
|
180
|
+
}),
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
185
|
+
const resource = this.#resources.find(
|
|
186
|
+
(resource) => resource.uri === request.params.uri,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (!resource) {
|
|
190
|
+
throw new McpError(
|
|
191
|
+
ErrorCode.MethodNotFound,
|
|
192
|
+
`Unknown resource: ${request.params.uri}`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let result: Awaited<ReturnType<Resource["load"]>>;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
result = await resource.load();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
throw new McpError(
|
|
202
|
+
ErrorCode.InternalError,
|
|
203
|
+
`Error reading resource: ${error}`,
|
|
204
|
+
{
|
|
205
|
+
uri: resource.uri,
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
contents: [
|
|
212
|
+
{
|
|
213
|
+
uri: resource.uri,
|
|
214
|
+
mimeType: resource.mimeType,
|
|
215
|
+
...result,
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private setupPromptHandlers(server: Server) {
|
|
223
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
224
|
+
return {
|
|
225
|
+
prompts: this.#prompts.map((prompt) => {
|
|
226
|
+
return {
|
|
227
|
+
name: prompt.name,
|
|
228
|
+
description: prompt.description,
|
|
229
|
+
arguments: prompt.arguments,
|
|
230
|
+
};
|
|
231
|
+
}),
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
236
|
+
const prompt = this.#prompts.find(
|
|
237
|
+
(prompt) => prompt.name === request.params.name,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
if (!prompt) {
|
|
241
|
+
throw new McpError(
|
|
242
|
+
ErrorCode.MethodNotFound,
|
|
243
|
+
`Unknown prompt: ${request.params.name}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const args = request.params.arguments;
|
|
248
|
+
|
|
249
|
+
if (prompt.arguments) {
|
|
250
|
+
for (const arg of prompt.arguments) {
|
|
251
|
+
if (arg.required && !(args && arg.name in args)) {
|
|
252
|
+
throw new McpError(
|
|
253
|
+
ErrorCode.InvalidRequest,
|
|
254
|
+
`Missing required argument: ${arg.name}`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let result: Awaited<ReturnType<Prompt["load"]>>;
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
result = await prompt.load(args as Record<string, string | undefined>);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
throw new McpError(
|
|
266
|
+
ErrorCode.InternalError,
|
|
267
|
+
`Error loading prompt: ${error}`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
description: prompt.description,
|
|
273
|
+
messages: [
|
|
274
|
+
{
|
|
275
|
+
role: "user",
|
|
276
|
+
content: { type: "text", text: result },
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
public addTool<Params extends ToolParameters>(tool: Tool<Params>) {
|
|
284
|
+
this.#tools.push(tool as unknown as Tool);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
public addResource(resource: Resource) {
|
|
288
|
+
this.#resources.push(resource);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
public addPrompt<const Args extends PromptArgument[]>(prompt: Prompt<Args>) {
|
|
292
|
+
this.#prompts.push(prompt);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#httpServer: http.Server | null = null;
|
|
296
|
+
|
|
297
|
+
public async start(
|
|
298
|
+
options:
|
|
299
|
+
| { transportType: "stdio" }
|
|
300
|
+
| {
|
|
301
|
+
transportType: "sse";
|
|
302
|
+
sse: { endpoint: `/${string}`; port: number };
|
|
303
|
+
} = {
|
|
304
|
+
transportType: "stdio",
|
|
305
|
+
},
|
|
306
|
+
) {
|
|
307
|
+
const capabilities: ServerCapabilities = {};
|
|
308
|
+
|
|
309
|
+
if (this.#tools.length) {
|
|
310
|
+
capabilities.tools = {};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (this.#resources.length) {
|
|
314
|
+
capabilities.resources = {};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (this.#prompts.length) {
|
|
318
|
+
capabilities.prompts = {};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.#server = new Server(
|
|
322
|
+
{ name: this.#options.name, version: this.#options.version },
|
|
323
|
+
{ capabilities },
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
this.setupHandlers(this.#server);
|
|
327
|
+
|
|
328
|
+
if (options.transportType === "stdio") {
|
|
329
|
+
const transport = new StdioServerTransport();
|
|
330
|
+
|
|
331
|
+
await this.#server.connect(transport);
|
|
332
|
+
|
|
333
|
+
console.error(`server is running on stdio`);
|
|
334
|
+
} else if (options.transportType === "sse") {
|
|
335
|
+
let activeTransport: SSEServerTransport | null = null;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Adopted from https://dev.classmethod.jp/articles/mcp-sse/
|
|
339
|
+
*/
|
|
340
|
+
this.#httpServer = http.createServer(async (req, res) => {
|
|
341
|
+
if (req.method === "GET" && req.url === options.sse.endpoint) {
|
|
342
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
343
|
+
|
|
344
|
+
activeTransport = transport;
|
|
345
|
+
|
|
346
|
+
if (!this.#server) {
|
|
347
|
+
throw new Error("Server not initialized");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
await this.#server.connect(transport);
|
|
351
|
+
|
|
352
|
+
res.on("close", () => {
|
|
353
|
+
console.log("SSE connection closed");
|
|
354
|
+
if (activeTransport === transport) {
|
|
355
|
+
activeTransport = null;
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
this.startSending(transport);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (req.method === "POST" && req.url?.startsWith("/messages")) {
|
|
364
|
+
if (!activeTransport) {
|
|
365
|
+
res.writeHead(400).end("No active transport");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
await activeTransport.handlePostMessage(req, res);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
res.writeHead(404).end();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
this.#httpServer.listen(options.sse.port, "0.0.0.0");
|
|
376
|
+
|
|
377
|
+
console.error(
|
|
378
|
+
`server is running on SSE at http://localhost:${options.sse.port}${options.sse.endpoint}`,
|
|
379
|
+
);
|
|
380
|
+
} else {
|
|
381
|
+
throw new Error("Invalid transport type");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* @see https://dev.classmethod.jp/articles/mcp-sse/
|
|
387
|
+
*/
|
|
388
|
+
private async startSending(transport: SSEServerTransport) {
|
|
389
|
+
try {
|
|
390
|
+
await transport.send({
|
|
391
|
+
jsonrpc: "2.0",
|
|
392
|
+
method: "sse/connection",
|
|
393
|
+
params: { message: "SSE Connection established" },
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
let messageCount = 0;
|
|
397
|
+
const interval = setInterval(async () => {
|
|
398
|
+
messageCount++;
|
|
399
|
+
|
|
400
|
+
const message = `Message ${messageCount} at ${new Date().toISOString()}`;
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
await transport.send({
|
|
404
|
+
jsonrpc: "2.0",
|
|
405
|
+
method: "sse/message",
|
|
406
|
+
params: { data: message },
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
console.log(`Sent: ${message}`);
|
|
410
|
+
|
|
411
|
+
if (messageCount === 10) {
|
|
412
|
+
clearInterval(interval);
|
|
413
|
+
|
|
414
|
+
await transport.send({
|
|
415
|
+
jsonrpc: "2.0",
|
|
416
|
+
method: "sse/complete",
|
|
417
|
+
params: { message: "Stream completed" },
|
|
418
|
+
});
|
|
419
|
+
console.log("Stream completed");
|
|
420
|
+
}
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.error("Error sending message:", error);
|
|
423
|
+
clearInterval(interval);
|
|
424
|
+
}
|
|
425
|
+
}, 1000);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
console.error("Error in startSending:", error);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
public async stop() {
|
|
432
|
+
if (this.#httpServer) {
|
|
433
|
+
this.#httpServer.close();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import yargs from "yargs";
|
|
4
|
+
import { hideBin } from "yargs/helpers";
|
|
5
|
+
import { $ } from "execa";
|
|
6
|
+
|
|
7
|
+
await yargs(hideBin(process.argv))
|
|
8
|
+
.scriptName("fastmcp")
|
|
9
|
+
.command(
|
|
10
|
+
"dev <file>",
|
|
11
|
+
"Start a development server",
|
|
12
|
+
(yargs) => {
|
|
13
|
+
return yargs.positional("file", {
|
|
14
|
+
type: "string",
|
|
15
|
+
describe: "The path to the server file",
|
|
16
|
+
demandOption: true,
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
async (argv) => {
|
|
20
|
+
const command = argv.file.endsWith(".ts")
|
|
21
|
+
? ["npx", "tsx", argv.file]
|
|
22
|
+
: ["node", argv.file];
|
|
23
|
+
|
|
24
|
+
await $({
|
|
25
|
+
stdin: "inherit",
|
|
26
|
+
stdout: "inherit",
|
|
27
|
+
stderr: "inherit",
|
|
28
|
+
})`npx @wong2/mcp-cli ${command}`;
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
.command(
|
|
32
|
+
"inspect <file>",
|
|
33
|
+
"Inspect a server file",
|
|
34
|
+
(yargs) => {
|
|
35
|
+
return yargs.positional("file", {
|
|
36
|
+
type: "string",
|
|
37
|
+
describe: "The path to the server file",
|
|
38
|
+
demandOption: true,
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
async (argv) => {
|
|
42
|
+
await $({
|
|
43
|
+
stdout: "inherit",
|
|
44
|
+
stderr: "inherit",
|
|
45
|
+
})`npx @modelcontextprotocol/inspector node ${argv.file}`;
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
.help()
|
|
49
|
+
.parseAsync();
|
package/tsconfig.json
ADDED