fastmcp 1.0.2 → 1.2.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 CHANGED
@@ -10,8 +10,9 @@ A TypeScript framework for building [MCP](https://modelcontextprotocol.io/) serv
10
10
  - Simple Tool, Resource, Prompt definition
11
11
  - Full TypeScript support
12
12
  - Built-in error handling
13
- - Built-in CLI for testing and debugging
14
- - Built-in support for SSE
13
+ - Built-in CLI for [testing](#test-with-mcp-cli) and [debugging](#inspect-with-mcp-inspector)
14
+ - Built-in support for [SSE](#sse)
15
+ - Built-in [progress notifications](#progress)
15
16
 
16
17
  ## Installation
17
18
 
@@ -109,6 +110,58 @@ server.addTool({
109
110
  });
110
111
  ```
111
112
 
113
+ #### Errors
114
+
115
+ The errors that are meant to be shown to the user should be thrown as `UserError` instances:
116
+
117
+ ```js
118
+ import { UserError } from "fastmcp";
119
+
120
+ server.addTool({
121
+ name: "download",
122
+ description: "Download a file",
123
+ parameters: z.object({
124
+ url: z.string(),
125
+ }),
126
+ execute: async (args) => {
127
+ if (args.url.startsWith("https://example.com")) {
128
+ throw new UserError("This URL is not allowed");
129
+ }
130
+
131
+ return "done";
132
+ },
133
+ });
134
+ ```
135
+
136
+ #### Progress
137
+
138
+ Tools can report progress by calling `reportProgress` in the context object:
139
+
140
+ ```js
141
+ server.addTool({
142
+ name: "download",
143
+ description: "Download a file",
144
+ parameters: z.object({
145
+ url: z.string(),
146
+ }),
147
+ execute: async (args, { reportProgress }) => {
148
+ reportProgress({
149
+ progress: 0,
150
+ total: 100,
151
+ });
152
+
153
+ // ...
154
+
155
+ reportProgress({
156
+ progress: 100,
157
+ total: 100,
158
+ });
159
+
160
+ return "done";
161
+ },
162
+ });
163
+ ```
164
+
112
165
  ### Resources
113
166
 
114
167
  [Resources](https://modelcontextprotocol.io/docs/concepts/resources) represent any kind of data that an MCP server wants to make available to clients. This can include:
package/dist/FastMCP.d.ts CHANGED
@@ -1,11 +1,35 @@
1
1
  import { z } from 'zod';
2
2
 
3
+ declare abstract class FastMCPError extends Error {
4
+ constructor(message?: string);
5
+ }
6
+ type Extra = unknown;
7
+ type Extras = Record<string, Extra>;
8
+ declare class UnexpectedStateError extends FastMCPError {
9
+ extras?: Extras;
10
+ constructor(message: string, extras?: Extras);
11
+ }
12
+ declare class UserError extends UnexpectedStateError {
13
+ }
3
14
  type ToolParameters = z.ZodTypeAny;
15
+ type Progress = {
16
+ /**
17
+ * The progress thus far. This should increase every time progress is made, even if the total is unknown.
18
+ */
19
+ progress: number;
20
+ /**
21
+ * Total number of items to process (or total progress required), if known.
22
+ */
23
+ total?: number;
24
+ };
25
+ type Context = {
26
+ reportProgress: (progress: Progress) => Promise<void>;
27
+ };
4
28
  type Tool<Params extends ToolParameters = ToolParameters> = {
5
29
  name: string;
6
30
  description?: string;
7
31
  parameters?: Params;
8
- execute: (args: z.infer<Params>) => Promise<unknown>;
32
+ execute: (args: z.infer<Params>, context: Context) => Promise<unknown>;
9
33
  };
10
34
  type Resource = {
11
35
  uri: string;
@@ -66,4 +90,4 @@ declare class FastMCP {
66
90
  stop(): Promise<void>;
67
91
  }
68
92
 
69
- export { FastMCP };
93
+ export { FastMCP, UserError };
package/dist/FastMCP.js CHANGED
@@ -14,6 +14,22 @@ import {
14
14
  } from "@modelcontextprotocol/sdk/types.js";
15
15
  import { zodToJsonSchema } from "zod-to-json-schema";
16
16
  import http from "http";
17
+ var FastMCPError = class extends Error {
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = new.target.name;
21
+ }
22
+ };
23
+ var UnexpectedStateError = class extends FastMCPError {
24
+ extras;
25
+ constructor(message, extras) {
26
+ super(message);
27
+ this.name = new.target.name;
28
+ this.extras = extras;
29
+ }
30
+ };
31
+ var UserError = class extends UnexpectedStateError {
32
+ };
17
33
  var FastMCP = class {
18
34
  constructor(options) {
19
35
  this.options = options;
@@ -60,45 +76,66 @@ var FastMCP = class {
60
76
  })
61
77
  };
62
78
  });
63
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
64
- const tool = this.#tools.find(
65
- (tool2) => tool2.name === request.params.name
66
- );
67
- if (!tool) {
68
- throw new McpError(
69
- ErrorCode.MethodNotFound,
70
- `Unknown tool: ${request.params.name}`
79
+ server.setRequestHandler(
80
+ CallToolRequestSchema,
81
+ async (request, ...extra) => {
82
+ const tool = this.#tools.find(
83
+ (tool2) => tool2.name === request.params.name
71
84
  );
72
- }
73
- let args = void 0;
74
- if (tool.parameters) {
75
- const parsed = tool.parameters.safeParse(request.params.arguments);
76
- if (!parsed.success) {
85
+ if (!tool) {
77
86
  throw new McpError(
78
- ErrorCode.InvalidRequest,
79
- `Invalid ${request.params.name} arguments`
87
+ ErrorCode.MethodNotFound,
88
+ `Unknown tool: ${request.params.name}`
80
89
  );
81
90
  }
82
- args = parsed.data;
83
- }
84
- let result;
85
- try {
86
- result = await tool.execute(args);
87
- } catch (error) {
88
- return {
89
- content: [{ type: "text", text: `Error: ${error}` }],
90
- isError: true
91
- };
92
- }
93
- if (typeof result === "string") {
91
+ let args = void 0;
92
+ if (tool.parameters) {
93
+ const parsed = tool.parameters.safeParse(request.params.arguments);
94
+ if (!parsed.success) {
95
+ throw new McpError(
96
+ ErrorCode.InvalidRequest,
97
+ `Invalid ${request.params.name} arguments`
98
+ );
99
+ }
100
+ args = parsed.data;
101
+ }
102
+ const progressToken = request.params?._meta?.progressToken;
103
+ let result;
104
+ try {
105
+ const reportProgress = async (progress) => {
106
+ await server.notification({
107
+ method: "notifications/progress",
108
+ params: {
109
+ ...progress,
110
+ progressToken
111
+ }
112
+ });
113
+ };
114
+ result = await tool.execute(args, {
115
+ reportProgress
116
+ });
117
+ } catch (error) {
118
+ if (error instanceof UserError) {
119
+ return {
120
+ content: [{ type: "text", text: error.message }],
121
+ isError: true
122
+ };
123
+ }
124
+ return {
125
+ content: [{ type: "text", text: `Error: ${error}` }],
126
+ isError: true
127
+ };
128
+ }
129
+ if (typeof result === "string") {
130
+ return {
131
+ content: [{ type: "text", text: result }]
132
+ };
133
+ }
94
134
  return {
95
- content: [{ type: "text", text: result }]
135
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
96
136
  };
97
137
  }
98
- return {
99
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
100
- };
101
- });
138
+ );
102
139
  }
103
140
  setupResourceHandlers(server) {
104
141
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
@@ -313,6 +350,7 @@ var FastMCP = class {
313
350
  }
314
351
  };
315
352
  export {
316
- FastMCP
353
+ FastMCP,
354
+ UserError
317
355
  };
318
356
  //# sourceMappingURL=FastMCP.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/FastMCP.ts"],"sourcesContent":["import { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { SSEServerTransport } from \"@modelcontextprotocol/sdk/server/sse.js\";\nimport {\n CallToolRequestSchema,\n ErrorCode,\n GetPromptRequestSchema,\n ListPromptsRequestSchema,\n ListResourcesRequestSchema,\n ListToolsRequestSchema,\n McpError,\n ReadResourceRequestSchema,\n ServerCapabilities,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { zodToJsonSchema } from \"zod-to-json-schema\";\nimport type { z } from \"zod\";\nimport http from \"http\";\n\ntype ToolParameters = z.ZodTypeAny;\n\ntype Tool<Params extends ToolParameters = ToolParameters> = {\n name: string;\n description?: string;\n parameters?: Params;\n execute: (args: z.infer<Params>) => Promise<unknown>;\n};\n\ntype Resource = {\n uri: string;\n name: string;\n description?: string;\n mimeType?: string;\n load: () => Promise<{ text: string } | { blob: string }>;\n};\n\ntype PromptArgument = Readonly<{\n name: string;\n description?: string;\n required?: boolean;\n}>;\n\ntype ArgumentsToObject<T extends PromptArgument[]> = {\n [K in T[number][\"name\"]]: Extract<\n T[number],\n { name: K }\n >[\"required\"] extends true\n ? string\n : string | undefined;\n};\n\ntype Prompt<\n Arguments extends PromptArgument[] = PromptArgument[],\n Args = ArgumentsToObject<Arguments>,\n> = {\n name: string;\n description?: string;\n arguments?: Arguments;\n load: (args: Args) => Promise<string>;\n};\n\ntype ServerOptions = {\n name: string;\n version: `${number}.${number}.${number}`;\n};\n\nexport class FastMCP {\n #tools: Tool[];\n #resources: Resource[];\n #prompts: Prompt[];\n #server: Server | null = null;\n #options: ServerOptions;\n\n constructor(public options: ServerOptions) {\n this.#options = options;\n this.#tools = [];\n this.#resources = [];\n this.#prompts = [];\n }\n\n private setupHandlers(server: Server) {\n this.setupErrorHandling(server);\n\n if (this.#tools.length) {\n this.setupToolHandlers(server);\n }\n\n if (this.#resources.length) {\n this.setupResourceHandlers(server);\n }\n\n if (this.#prompts.length) {\n this.setupPromptHandlers(server);\n }\n }\n\n private setupErrorHandling(server: Server) {\n server.onerror = (error) => {\n console.error(\"[MCP Error]\", error);\n };\n process.on(\"SIGINT\", async () => {\n await server.close();\n process.exit(0);\n });\n }\n\n private setupToolHandlers(server: Server) {\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n return {\n tools: this.#tools.map((tool) => {\n return {\n name: tool.name,\n description: tool.description,\n inputSchema: tool.parameters\n ? zodToJsonSchema(tool.parameters)\n : undefined,\n };\n }),\n };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const tool = this.#tools.find(\n (tool) => tool.name === request.params.name,\n );\n\n if (!tool) {\n throw new McpError(\n ErrorCode.MethodNotFound,\n `Unknown tool: ${request.params.name}`,\n );\n }\n\n let args: any = undefined;\n\n if (tool.parameters) {\n const parsed = tool.parameters.safeParse(request.params.arguments);\n\n if (!parsed.success) {\n throw new McpError(\n ErrorCode.InvalidRequest,\n `Invalid ${request.params.name} arguments`,\n );\n }\n\n args = parsed.data;\n }\n\n let result: any;\n\n try {\n result = await tool.execute(args);\n } catch (error) {\n return {\n content: [{ type: \"text\", text: `Error: ${error}` }],\n isError: true,\n };\n }\n\n if (typeof result === \"string\") {\n return {\n content: [{ type: \"text\", text: result }],\n };\n }\n\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n });\n }\n\n private setupResourceHandlers(server: Server) {\n server.setRequestHandler(ListResourcesRequestSchema, async () => {\n return {\n resources: this.#resources.map((resource) => {\n return {\n uri: resource.uri,\n name: resource.name,\n mimeType: resource.mimeType,\n };\n }),\n };\n });\n\n server.setRequestHandler(ReadResourceRequestSchema, async (request) => {\n const resource = this.#resources.find(\n (resource) => resource.uri === request.params.uri,\n );\n\n if (!resource) {\n throw new McpError(\n ErrorCode.MethodNotFound,\n `Unknown resource: ${request.params.uri}`,\n );\n }\n\n let result: Awaited<ReturnType<Resource[\"load\"]>>;\n\n try {\n result = await resource.load();\n } catch (error) {\n throw new McpError(\n ErrorCode.InternalError,\n `Error reading resource: ${error}`,\n {\n uri: resource.uri,\n },\n );\n }\n\n return {\n contents: [\n {\n uri: resource.uri,\n mimeType: resource.mimeType,\n ...result,\n },\n ],\n };\n });\n }\n\n private setupPromptHandlers(server: Server) {\n server.setRequestHandler(ListPromptsRequestSchema, async () => {\n return {\n prompts: this.#prompts.map((prompt) => {\n return {\n name: prompt.name,\n description: prompt.description,\n arguments: prompt.arguments,\n };\n }),\n };\n });\n\n server.setRequestHandler(GetPromptRequestSchema, async (request) => {\n const prompt = this.#prompts.find(\n (prompt) => prompt.name === request.params.name,\n );\n\n if (!prompt) {\n throw new McpError(\n ErrorCode.MethodNotFound,\n `Unknown prompt: ${request.params.name}`,\n );\n }\n\n const args = request.params.arguments;\n\n if (prompt.arguments) {\n for (const arg of prompt.arguments) {\n if (arg.required && !(args && arg.name in args)) {\n throw new McpError(\n ErrorCode.InvalidRequest,\n `Missing required argument: ${arg.name}`,\n );\n }\n }\n }\n\n let result: Awaited<ReturnType<Prompt[\"load\"]>>;\n\n try {\n result = await prompt.load(args as Record<string, string | undefined>);\n } catch (error) {\n throw new McpError(\n ErrorCode.InternalError,\n `Error loading prompt: ${error}`,\n );\n }\n\n return {\n description: prompt.description,\n messages: [\n {\n role: \"user\",\n content: { type: \"text\", text: result },\n },\n ],\n };\n });\n }\n\n public addTool<Params extends ToolParameters>(tool: Tool<Params>) {\n this.#tools.push(tool as unknown as Tool);\n }\n\n public addResource(resource: Resource) {\n this.#resources.push(resource);\n }\n\n public addPrompt<const Args extends PromptArgument[]>(prompt: Prompt<Args>) {\n this.#prompts.push(prompt);\n }\n\n #httpServer: http.Server | null = null;\n\n public async start(\n options:\n | { transportType: \"stdio\" }\n | {\n transportType: \"sse\";\n sse: { endpoint: `/${string}`; port: number };\n } = {\n transportType: \"stdio\",\n },\n ) {\n const capabilities: ServerCapabilities = {};\n\n if (this.#tools.length) {\n capabilities.tools = {};\n }\n\n if (this.#resources.length) {\n capabilities.resources = {};\n }\n\n if (this.#prompts.length) {\n capabilities.prompts = {};\n }\n\n this.#server = new Server(\n { name: this.#options.name, version: this.#options.version },\n { capabilities },\n );\n\n this.setupHandlers(this.#server);\n\n if (options.transportType === \"stdio\") {\n const transport = new StdioServerTransport();\n\n await this.#server.connect(transport);\n\n console.error(`server is running on stdio`);\n } else if (options.transportType === \"sse\") {\n let activeTransport: SSEServerTransport | null = null;\n\n /**\n * Adopted from https://dev.classmethod.jp/articles/mcp-sse/\n */\n this.#httpServer = http.createServer(async (req, res) => {\n if (req.method === \"GET\" && req.url === options.sse.endpoint) {\n const transport = new SSEServerTransport(\"/messages\", res);\n\n activeTransport = transport;\n\n if (!this.#server) {\n throw new Error(\"Server not initialized\");\n }\n\n await this.#server.connect(transport);\n\n res.on(\"close\", () => {\n console.log(\"SSE connection closed\");\n if (activeTransport === transport) {\n activeTransport = null;\n }\n });\n\n this.startSending(transport);\n return;\n }\n\n if (req.method === \"POST\" && req.url?.startsWith(\"/messages\")) {\n if (!activeTransport) {\n res.writeHead(400).end(\"No active transport\");\n return;\n }\n await activeTransport.handlePostMessage(req, res);\n return;\n }\n\n res.writeHead(404).end();\n });\n\n this.#httpServer.listen(options.sse.port, \"0.0.0.0\");\n\n console.error(\n `server is running on SSE at http://localhost:${options.sse.port}${options.sse.endpoint}`,\n );\n } else {\n throw new Error(\"Invalid transport type\");\n }\n }\n\n /**\n * @see https://dev.classmethod.jp/articles/mcp-sse/\n */\n private async startSending(transport: SSEServerTransport) {\n try {\n await transport.send({\n jsonrpc: \"2.0\",\n method: \"sse/connection\",\n params: { message: \"SSE Connection established\" },\n });\n\n let messageCount = 0;\n const interval = setInterval(async () => {\n messageCount++;\n\n const message = `Message ${messageCount} at ${new Date().toISOString()}`;\n\n try {\n await transport.send({\n jsonrpc: \"2.0\",\n method: \"sse/message\",\n params: { data: message },\n });\n\n console.log(`Sent: ${message}`);\n\n if (messageCount === 10) {\n clearInterval(interval);\n\n await transport.send({\n jsonrpc: \"2.0\",\n method: \"sse/complete\",\n params: { message: \"Stream completed\" },\n });\n console.log(\"Stream completed\");\n }\n } catch (error) {\n console.error(\"Error sending message:\", error);\n clearInterval(interval);\n }\n }, 1000);\n } catch (error) {\n console.error(\"Error in startSending:\", error);\n }\n }\n\n public async stop() {\n if (this.#httpServer) {\n this.#httpServer.close();\n }\n }\n}\n"],"mappings":"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","names":["tool","resource","prompt"]}
1
+ {"version":3,"sources":["../src/FastMCP.ts"],"sourcesContent":["import { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { SSEServerTransport } from \"@modelcontextprotocol/sdk/server/sse.js\";\nimport {\n CallToolRequestSchema,\n ErrorCode,\n GetPromptRequestSchema,\n ListPromptsRequestSchema,\n ListResourcesRequestSchema,\n ListToolsRequestSchema,\n McpError,\n ReadResourceRequestSchema,\n ServerCapabilities,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { zodToJsonSchema } from \"zod-to-json-schema\";\nimport type { z } from \"zod\";\nimport http from \"http\";\n\nabstract class FastMCPError extends Error {\n public constructor(message?: string) {\n super(message);\n this.name = new.target.name;\n }\n}\n\ntype Extra = unknown;\n\ntype Extras = Record<string, Extra>;\n\nclass UnexpectedStateError extends FastMCPError {\n public extras?: Extras;\n\n public constructor(message: string, extras?: Extras) {\n super(message);\n this.name = new.target.name;\n this.extras = extras;\n }\n}\n\nexport class UserError extends UnexpectedStateError {}\n\ntype ToolParameters = z.ZodTypeAny;\n\ntype Progress = {\n /**\n * The progress thus far. This should increase every time progress is made, even if the total is unknown.\n */\n progress: number;\n /**\n * Total number of items to process (or total progress required), if known.\n */\n total?: number;\n};\n\ntype Context = {\n reportProgress: (progress: Progress) => Promise<void>;\n};\n\ntype Tool<Params extends ToolParameters = ToolParameters> = {\n name: string;\n description?: string;\n parameters?: Params;\n execute: (args: z.infer<Params>, context: Context) => Promise<unknown>;\n};\n\ntype Resource = {\n uri: string;\n name: string;\n description?: string;\n mimeType?: string;\n load: () => Promise<{ text: string } | { blob: string }>;\n};\n\ntype PromptArgument = Readonly<{\n name: string;\n description?: string;\n required?: boolean;\n}>;\n\ntype ArgumentsToObject<T extends PromptArgument[]> = {\n [K in T[number][\"name\"]]: Extract<\n T[number],\n { name: K }\n >[\"required\"] extends true\n ? string\n : string | undefined;\n};\n\ntype Prompt<\n Arguments extends PromptArgument[] = PromptArgument[],\n Args = ArgumentsToObject<Arguments>,\n> = {\n name: string;\n description?: string;\n arguments?: Arguments;\n load: (args: Args) => Promise<string>;\n};\n\ntype ServerOptions = {\n name: string;\n version: `${number}.${number}.${number}`;\n};\n\nexport class FastMCP {\n #tools: Tool[];\n #resources: Resource[];\n #prompts: Prompt[];\n #server: Server | null = null;\n #options: ServerOptions;\n\n constructor(public options: ServerOptions) {\n this.#options = options;\n this.#tools = [];\n this.#resources = [];\n this.#prompts = [];\n }\n\n private setupHandlers(server: Server) {\n this.setupErrorHandling(server);\n\n if (this.#tools.length) {\n this.setupToolHandlers(server);\n }\n\n if (this.#resources.length) {\n this.setupResourceHandlers(server);\n }\n\n if (this.#prompts.length) {\n this.setupPromptHandlers(server);\n }\n }\n\n private setupErrorHandling(server: Server) {\n server.onerror = (error) => {\n console.error(\"[MCP Error]\", error);\n };\n process.on(\"SIGINT\", async () => {\n await server.close();\n process.exit(0);\n });\n }\n\n private setupToolHandlers(server: Server) {\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n return {\n tools: this.#tools.map((tool) => {\n return {\n name: tool.name,\n description: tool.description,\n inputSchema: tool.parameters\n ? zodToJsonSchema(tool.parameters)\n : undefined,\n };\n }),\n };\n });\n\n server.setRequestHandler(\n CallToolRequestSchema,\n async (request, ...extra) => {\n const tool = this.#tools.find(\n (tool) => tool.name === request.params.name,\n );\n\n if (!tool) {\n throw new McpError(\n ErrorCode.MethodNotFound,\n `Unknown tool: ${request.params.name}`,\n );\n }\n\n let args: any = undefined;\n\n if (tool.parameters) {\n const parsed = tool.parameters.safeParse(request.params.arguments);\n\n if (!parsed.success) {\n throw new McpError(\n ErrorCode.InvalidRequest,\n `Invalid ${request.params.name} arguments`,\n );\n }\n\n args = parsed.data;\n }\n\n const progressToken = request.params?._meta?.progressToken;\n\n let result: any;\n\n try {\n const reportProgress = async (progress: Progress) => {\n await server.notification({\n method: \"notifications/progress\",\n params: {\n ...progress,\n progressToken,\n },\n });\n };\n\n result = await tool.execute(args, {\n reportProgress,\n });\n } catch (error) {\n if (error instanceof UserError) {\n return {\n content: [{ type: \"text\", text: error.message }],\n isError: true,\n };\n }\n\n return {\n content: [{ type: \"text\", text: `Error: ${error}` }],\n isError: true,\n };\n }\n\n if (typeof result === \"string\") {\n return {\n content: [{ type: \"text\", text: result }],\n };\n }\n\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n },\n );\n }\n\n private setupResourceHandlers(server: Server) {\n server.setRequestHandler(ListResourcesRequestSchema, async () => {\n return {\n resources: this.#resources.map((resource) => {\n return {\n uri: resource.uri,\n name: resource.name,\n mimeType: resource.mimeType,\n };\n }),\n };\n });\n\n server.setRequestHandler(ReadResourceRequestSchema, async (request) => {\n const resource = this.#resources.find(\n (resource) => resource.uri === request.params.uri,\n );\n\n if (!resource) {\n throw new McpError(\n ErrorCode.MethodNotFound,\n `Unknown resource: ${request.params.uri}`,\n );\n }\n\n let result: Awaited<ReturnType<Resource[\"load\"]>>;\n\n try {\n result = await resource.load();\n } catch (error) {\n throw new McpError(\n ErrorCode.InternalError,\n `Error reading resource: ${error}`,\n {\n uri: resource.uri,\n },\n );\n }\n\n return {\n contents: [\n {\n uri: resource.uri,\n mimeType: resource.mimeType,\n ...result,\n },\n ],\n };\n });\n }\n\n private setupPromptHandlers(server: Server) {\n server.setRequestHandler(ListPromptsRequestSchema, async () => {\n return {\n prompts: this.#prompts.map((prompt) => {\n return {\n name: prompt.name,\n description: prompt.description,\n arguments: prompt.arguments,\n };\n }),\n };\n });\n\n server.setRequestHandler(GetPromptRequestSchema, async (request) => {\n const prompt = this.#prompts.find(\n (prompt) => prompt.name === request.params.name,\n );\n\n if (!prompt) {\n throw new McpError(\n ErrorCode.MethodNotFound,\n `Unknown prompt: ${request.params.name}`,\n );\n }\n\n const args = request.params.arguments;\n\n if (prompt.arguments) {\n for (const arg of prompt.arguments) {\n if (arg.required && !(args && arg.name in args)) {\n throw new McpError(\n ErrorCode.InvalidRequest,\n `Missing required argument: ${arg.name}`,\n );\n }\n }\n }\n\n let result: Awaited<ReturnType<Prompt[\"load\"]>>;\n\n try {\n result = await prompt.load(args as Record<string, string | undefined>);\n } catch (error) {\n throw new McpError(\n ErrorCode.InternalError,\n `Error loading prompt: ${error}`,\n );\n }\n\n return {\n description: prompt.description,\n messages: [\n {\n role: \"user\",\n content: { type: \"text\", text: result },\n },\n ],\n };\n });\n }\n\n public addTool<Params extends ToolParameters>(tool: Tool<Params>) {\n this.#tools.push(tool as unknown as Tool);\n }\n\n public addResource(resource: Resource) {\n this.#resources.push(resource);\n }\n\n public addPrompt<const Args extends PromptArgument[]>(prompt: Prompt<Args>) {\n this.#prompts.push(prompt);\n }\n\n #httpServer: http.Server | null = null;\n\n public async start(\n options:\n | { transportType: \"stdio\" }\n | {\n transportType: \"sse\";\n sse: { endpoint: `/${string}`; port: number };\n } = {\n transportType: \"stdio\",\n },\n ) {\n const capabilities: ServerCapabilities = {};\n\n if (this.#tools.length) {\n capabilities.tools = {};\n }\n\n if (this.#resources.length) {\n capabilities.resources = {};\n }\n\n if (this.#prompts.length) {\n capabilities.prompts = {};\n }\n\n this.#server = new Server(\n { name: this.#options.name, version: this.#options.version },\n { capabilities },\n );\n\n this.setupHandlers(this.#server);\n\n if (options.transportType === \"stdio\") {\n const transport = new StdioServerTransport();\n\n await this.#server.connect(transport);\n\n console.error(`server is running on stdio`);\n } else if (options.transportType === \"sse\") {\n let activeTransport: SSEServerTransport | null = null;\n\n /**\n * Adopted from https://dev.classmethod.jp/articles/mcp-sse/\n */\n this.#httpServer = http.createServer(async (req, res) => {\n if (req.method === \"GET\" && req.url === options.sse.endpoint) {\n const transport = new SSEServerTransport(\"/messages\", res);\n\n activeTransport = transport;\n\n if (!this.#server) {\n throw new Error(\"Server not initialized\");\n }\n\n await this.#server.connect(transport);\n\n res.on(\"close\", () => {\n console.log(\"SSE connection closed\");\n if (activeTransport === transport) {\n activeTransport = null;\n }\n });\n\n this.startSending(transport);\n return;\n }\n\n if (req.method === \"POST\" && req.url?.startsWith(\"/messages\")) {\n if (!activeTransport) {\n res.writeHead(400).end(\"No active transport\");\n return;\n }\n await activeTransport.handlePostMessage(req, res);\n return;\n }\n\n res.writeHead(404).end();\n });\n\n this.#httpServer.listen(options.sse.port, \"0.0.0.0\");\n\n console.error(\n `server is running on SSE at http://localhost:${options.sse.port}${options.sse.endpoint}`,\n );\n } else {\n throw new Error(\"Invalid transport type\");\n }\n }\n\n /**\n * @see https://dev.classmethod.jp/articles/mcp-sse/\n */\n private async startSending(transport: SSEServerTransport) {\n try {\n await transport.send({\n jsonrpc: \"2.0\",\n method: \"sse/connection\",\n params: { message: \"SSE Connection established\" },\n });\n\n let messageCount = 0;\n const interval = setInterval(async () => {\n messageCount++;\n\n const message = `Message ${messageCount} at ${new Date().toISOString()}`;\n\n try {\n await transport.send({\n jsonrpc: \"2.0\",\n method: \"sse/message\",\n params: { data: message },\n });\n\n console.log(`Sent: ${message}`);\n\n if (messageCount === 10) {\n clearInterval(interval);\n\n await transport.send({\n jsonrpc: \"2.0\",\n method: \"sse/complete\",\n params: { message: \"Stream completed\" },\n });\n console.log(\"Stream completed\");\n }\n } catch (error) {\n console.error(\"Error sending message:\", error);\n clearInterval(interval);\n }\n }, 1000);\n } catch (error) {\n console.error(\"Error in startSending:\", error);\n }\n }\n\n public async stop() {\n if (this.#httpServer) {\n this.#httpServer.close();\n }\n }\n}\n"],"mappings":"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","names":["tool","resource","prompt"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastmcp",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "main": "dist/fastmcp.js",
5
5
  "scripts": {
6
6
  "build": "tsup",
@@ -1,10 +1,12 @@
1
- import { FastMCP } from "./FastMCP.js";
1
+ import { FastMCP, UserError } from "./FastMCP.js";
2
2
  import { z } from "zod";
3
- import { test, expect } from "vitest";
3
+ import { test, expect, vi } from "vitest";
4
4
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
5
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
6
6
  import { getRandomPort } from "get-port-please";
7
7
  import { EventSource } from "eventsource";
8
+ import { setTimeout as delay } from "timers/promises";
9
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
8
10
 
9
11
  // @ts-expect-error - figure out how to use --experimental-eventsource with vitest
10
12
  global.EventSource = EventSource;
@@ -98,6 +100,167 @@ test("adds tools", async () => {
98
100
  });
99
101
  });
100
102
 
103
+ test("calls a tool", async () => {
104
+ await runWithTestServer({
105
+ start: async () => {
106
+ const server = new FastMCP({
107
+ name: "Test",
108
+ version: "1.0.0",
109
+ });
110
+
111
+ server.addTool({
112
+ name: "add",
113
+ description: "Add two numbers",
114
+ parameters: z.object({
115
+ a: z.number(),
116
+ b: z.number(),
117
+ }),
118
+ execute: async (args) => {
119
+ return args.a + args.b;
120
+ },
121
+ });
122
+
123
+ return server;
124
+ },
125
+ run: async ({ client }) => {
126
+ expect(
127
+ await client.callTool({
128
+ name: "add",
129
+ arguments: {
130
+ a: 1,
131
+ b: 2,
132
+ },
133
+ }),
134
+ ).toEqual({
135
+ content: [{ type: "text", text: "3" }],
136
+ });
137
+ },
138
+ });
139
+ });
140
+
141
+ test("handles UserError errors", async () => {
142
+ await runWithTestServer({
143
+ start: async () => {
144
+ const server = new FastMCP({
145
+ name: "Test",
146
+ version: "1.0.0",
147
+ });
148
+
149
+ server.addTool({
150
+ name: "add",
151
+ description: "Add two numbers",
152
+ parameters: z.object({
153
+ a: z.number(),
154
+ b: z.number(),
155
+ }),
156
+ execute: async (args) => {
157
+ throw new UserError("Something went wrong");
158
+ },
159
+ });
160
+
161
+ return server;
162
+ },
163
+ run: async ({ client }) => {
164
+ expect(
165
+ await client.callTool({
166
+ name: "add",
167
+ arguments: {
168
+ a: 1,
169
+ b: 2,
170
+ },
171
+ }),
172
+ ).toEqual({
173
+ content: [{ type: "text", text: "Something went wrong" }],
174
+ isError: true,
175
+ });
176
+ },
177
+ });
178
+ });
179
+
180
+ test("calling an unknown tool throws McpError with MethodNotFound code", async () => {
181
+ await runWithTestServer({
182
+ start: async () => {
183
+ const server = new FastMCP({
184
+ name: "Test",
185
+ version: "1.0.0",
186
+ });
187
+
188
+ return server;
189
+ },
190
+ run: async ({ client }) => {
191
+ try {
192
+ await client.callTool({
193
+ name: "add",
194
+ arguments: {
195
+ a: 1,
196
+ b: 2,
197
+ },
198
+ });
199
+ } catch (error) {
200
+ expect(error).toBeInstanceOf(McpError);
201
+
202
+ // @ts-expect-error - we know that error is an McpError
203
+ expect(error.code).toBe(ErrorCode.MethodNotFound);
204
+ }
205
+ },
206
+ });
207
+ });
208
+
209
+ test("tracks tool progress", async () => {
210
+ await runWithTestServer({
211
+ start: async () => {
212
+ const server = new FastMCP({
213
+ name: "Test",
214
+ version: "1.0.0",
215
+ });
216
+
217
+ server.addTool({
218
+ name: "add",
219
+ description: "Add two numbers",
220
+ parameters: z.object({
221
+ a: z.number(),
222
+ b: z.number(),
223
+ }),
224
+ execute: async (args, { reportProgress }) => {
225
+ reportProgress({
226
+ progress: 0,
227
+ total: 10,
228
+ });
229
+
230
+ await delay(100);
231
+
232
+ return args.a + args.b;
233
+ },
234
+ });
235
+
236
+ return server;
237
+ },
238
+ run: async ({ client }) => {
239
+ const onProgress = vi.fn();
240
+
241
+ await client.callTool(
242
+ {
243
+ name: "add",
244
+ arguments: {
245
+ a: 1,
246
+ b: 2,
247
+ },
248
+ },
249
+ undefined,
250
+ {
251
+ onprogress: onProgress,
252
+ },
253
+ );
254
+
255
+ expect(onProgress).toHaveBeenCalledTimes(1);
256
+ expect(onProgress).toHaveBeenCalledWith({
257
+ progress: 0,
258
+ total: 10,
259
+ });
260
+ },
261
+ });
262
+ });
263
+
101
264
  test("adds resources", async () => {
102
265
  await runWithTestServer({
103
266
  start: async () => {
package/src/FastMCP.ts CHANGED
@@ -16,13 +16,51 @@ import { zodToJsonSchema } from "zod-to-json-schema";
16
16
  import type { z } from "zod";
17
17
  import http from "http";
18
18
 
19
+ abstract class FastMCPError extends Error {
20
+ public constructor(message?: string) {
21
+ super(message);
22
+ this.name = new.target.name;
23
+ }
24
+ }
25
+
26
+ type Extra = unknown;
27
+
28
+ type Extras = Record<string, Extra>;
29
+
30
+ class UnexpectedStateError extends FastMCPError {
31
+ public extras?: Extras;
32
+
33
+ public constructor(message: string, extras?: Extras) {
34
+ super(message);
35
+ this.name = new.target.name;
36
+ this.extras = extras;
37
+ }
38
+ }
39
+
40
+ export class UserError extends UnexpectedStateError {}
41
+
19
42
  type ToolParameters = z.ZodTypeAny;
20
43
 
44
+ type Progress = {
45
+ /**
46
+ * The progress thus far. This should increase every time progress is made, even if the total is unknown.
47
+ */
48
+ progress: number;
49
+ /**
50
+ * Total number of items to process (or total progress required), if known.
51
+ */
52
+ total?: number;
53
+ };
54
+
55
+ type Context = {
56
+ reportProgress: (progress: Progress) => Promise<void>;
57
+ };
58
+
21
59
  type Tool<Params extends ToolParameters = ToolParameters> = {
22
60
  name: string;
23
61
  description?: string;
24
62
  parameters?: Params;
25
- execute: (args: z.infer<Params>) => Promise<unknown>;
63
+ execute: (args: z.infer<Params>, context: Context) => Promise<unknown>;
26
64
  };
27
65
 
28
66
  type Resource = {
@@ -118,54 +156,78 @@ export class FastMCP {
118
156
  };
119
157
  });
120
158
 
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}`,
159
+ server.setRequestHandler(
160
+ CallToolRequestSchema,
161
+ async (request, ...extra) => {
162
+ const tool = this.#tools.find(
163
+ (tool) => tool.name === request.params.name,
130
164
  );
131
- }
132
-
133
- let args: any = undefined;
134
165
 
135
- if (tool.parameters) {
136
- const parsed = tool.parameters.safeParse(request.params.arguments);
137
-
138
- if (!parsed.success) {
166
+ if (!tool) {
139
167
  throw new McpError(
140
- ErrorCode.InvalidRequest,
141
- `Invalid ${request.params.name} arguments`,
168
+ ErrorCode.MethodNotFound,
169
+ `Unknown tool: ${request.params.name}`,
142
170
  );
143
171
  }
144
172
 
145
- args = parsed.data;
146
- }
173
+ let args: any = undefined;
147
174
 
148
- let result: any;
175
+ if (tool.parameters) {
176
+ const parsed = tool.parameters.safeParse(request.params.arguments);
149
177
 
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
- }
178
+ if (!parsed.success) {
179
+ throw new McpError(
180
+ ErrorCode.InvalidRequest,
181
+ `Invalid ${request.params.name} arguments`,
182
+ );
183
+ }
184
+
185
+ args = parsed.data;
186
+ }
187
+
188
+ const progressToken = request.params?._meta?.progressToken;
189
+
190
+ let result: any;
191
+
192
+ try {
193
+ const reportProgress = async (progress: Progress) => {
194
+ await server.notification({
195
+ method: "notifications/progress",
196
+ params: {
197
+ ...progress,
198
+ progressToken,
199
+ },
200
+ });
201
+ };
202
+
203
+ result = await tool.execute(args, {
204
+ reportProgress,
205
+ });
206
+ } catch (error) {
207
+ if (error instanceof UserError) {
208
+ return {
209
+ content: [{ type: "text", text: error.message }],
210
+ isError: true,
211
+ };
212
+ }
213
+
214
+ return {
215
+ content: [{ type: "text", text: `Error: ${error}` }],
216
+ isError: true,
217
+ };
218
+ }
219
+
220
+ if (typeof result === "string") {
221
+ return {
222
+ content: [{ type: "text", text: result }],
223
+ };
224
+ }
158
225
 
159
- if (typeof result === "string") {
160
226
  return {
161
- content: [{ type: "text", text: result }],
227
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
162
228
  };
163
- }
164
-
165
- return {
166
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
167
- };
168
- });
229
+ },
230
+ );
169
231
  }
170
232
 
171
233
  private setupResourceHandlers(server: Server) {