|
1 |
| -import { |
2 |
| - CallToolRequestSchema, |
3 |
| - Tool, |
4 |
| -} from "@modelcontextprotocol/sdk/types.js"; |
5 | 1 | import { z } from "zod";
|
| 2 | +import { Tool as SDKTool } from "@modelcontextprotocol/sdk/types.js"; |
6 | 3 |
|
7 |
| -export interface BaseTool { |
| 4 | +export type ToolInputSchema<T> = { |
| 5 | + [K in keyof T]: { |
| 6 | + type: z.ZodType<T[K]>; |
| 7 | + description: string; |
| 8 | + }; |
| 9 | +}; |
| 10 | + |
| 11 | +export type ToolInput<T extends ToolInputSchema<any>> = { |
| 12 | + [K in keyof T]: z.infer<T[K]["type"]>; |
| 13 | +}; |
| 14 | + |
| 15 | +export interface ToolProtocol extends SDKTool { |
8 | 16 | name: string;
|
9 |
| - toolDefinition: Tool; |
10 |
| - toolCall(request: z.infer<typeof CallToolRequestSchema>): Promise<any>; |
| 17 | + description: string; |
| 18 | + toolDefinition: { |
| 19 | + name: string; |
| 20 | + description: string; |
| 21 | + inputSchema: { |
| 22 | + type: "object"; |
| 23 | + properties?: Record<string, unknown>; |
| 24 | + }; |
| 25 | + }; |
| 26 | + toolCall(request: { |
| 27 | + params: { name: string; arguments?: Record<string, unknown> }; |
| 28 | + }): Promise<{ |
| 29 | + content: Array<{ type: string; text: string }>; |
| 30 | + }>; |
11 | 31 | }
|
12 | 32 |
|
13 |
| -export abstract class BaseToolImplementation implements BaseTool { |
| 33 | +export abstract class MCPTool<TInput extends Record<string, any> = {}> |
| 34 | + implements ToolProtocol |
| 35 | +{ |
14 | 36 | abstract name: string;
|
15 |
| - abstract toolDefinition: Tool; |
16 |
| - abstract toolCall( |
17 |
| - request: z.infer<typeof CallToolRequestSchema> |
18 |
| - ): Promise<any>; |
| 37 | + abstract description: string; |
| 38 | + protected abstract schema: ToolInputSchema<TInput>; |
| 39 | + [key: string]: unknown; |
19 | 40 |
|
20 |
| - protected createSuccessResponse(data: any) { |
| 41 | + get inputSchema(): { type: "object"; properties?: Record<string, unknown> } { |
| 42 | + return { |
| 43 | + type: "object" as const, |
| 44 | + properties: Object.fromEntries( |
| 45 | + Object.entries(this.schema).map(([key, schema]) => [ |
| 46 | + key, |
| 47 | + { |
| 48 | + type: this.getJsonSchemaType(schema.type), |
| 49 | + description: schema.description, |
| 50 | + }, |
| 51 | + ]) |
| 52 | + ), |
| 53 | + }; |
| 54 | + } |
| 55 | + |
| 56 | + get toolDefinition() { |
| 57 | + return { |
| 58 | + name: this.name, |
| 59 | + description: this.description, |
| 60 | + inputSchema: this.inputSchema, |
| 61 | + }; |
| 62 | + } |
| 63 | + |
| 64 | + protected abstract execute(input: TInput): Promise<unknown>; |
| 65 | + |
| 66 | + async toolCall(request: { |
| 67 | + params: { name: string; arguments?: Record<string, unknown> }; |
| 68 | + }) { |
| 69 | + try { |
| 70 | + const args = request.params.arguments || {}; |
| 71 | + const validatedInput = await this.validateInput(args); |
| 72 | + const result = await this.execute(validatedInput); |
| 73 | + return this.createSuccessResponse(result); |
| 74 | + } catch (error) { |
| 75 | + return this.createErrorResponse(error as Error); |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + private async validateInput(args: Record<string, unknown>): Promise<TInput> { |
| 80 | + const zodSchema = z.object( |
| 81 | + Object.fromEntries( |
| 82 | + Object.entries(this.schema).map(([key, schema]) => [key, schema.type]) |
| 83 | + ) |
| 84 | + ); |
| 85 | + |
| 86 | + return zodSchema.parse(args) as TInput; |
| 87 | + } |
| 88 | + |
| 89 | + private getJsonSchemaType(zodType: z.ZodType<any>): string { |
| 90 | + if (zodType instanceof z.ZodString) return "string"; |
| 91 | + if (zodType instanceof z.ZodNumber) return "number"; |
| 92 | + if (zodType instanceof z.ZodBoolean) return "boolean"; |
| 93 | + if (zodType instanceof z.ZodArray) return "array"; |
| 94 | + if (zodType instanceof z.ZodObject) return "object"; |
| 95 | + return "string"; |
| 96 | + } |
| 97 | + |
| 98 | + protected createSuccessResponse(data: unknown) { |
21 | 99 | return {
|
22 | 100 | content: [{ type: "text", text: JSON.stringify(data) }],
|
23 | 101 | };
|
24 | 102 | }
|
25 | 103 |
|
26 |
| - protected createErrorResponse(error: Error | string) { |
27 |
| - const message = error instanceof Error ? error.message : error; |
| 104 | + protected createErrorResponse(error: Error) { |
28 | 105 | return {
|
29 |
| - content: [{ type: "error", text: message }], |
| 106 | + content: [{ type: "error", text: error.message }], |
30 | 107 | };
|
31 | 108 | }
|
| 109 | + |
| 110 | + protected async fetch<T>(url: string, init?: RequestInit): Promise<T> { |
| 111 | + const response = await fetch(url, init); |
| 112 | + if (!response.ok) { |
| 113 | + throw new Error(`HTTP error! status: ${response.status}`); |
| 114 | + } |
| 115 | + return response.json(); |
| 116 | + } |
32 | 117 | }
|
0 commit comments