From e714caaef2585faacbd49f0121f9ee08dd4aa6ff Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 11 Sep 2025 13:55:33 +0200 Subject: [PATCH] Improve handling of tool responses, adding explicit classes --- mcp-server/src/index.ts | 4 +- mcp-server/src/interfaces/Tool.ts | 7 ++-- mcp-server/src/interfaces/ToolResponse.ts | 20 ++++++++++ mcp-server/src/tools/HelloWorldTool.ts | 17 ++++---- mcp-server/src/tools/ToolPrintText.ts | 48 +++++++---------------- 5 files changed, 47 insertions(+), 49 deletions(-) create mode 100644 mcp-server/src/interfaces/ToolResponse.ts diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index a473877..4f999c3 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -19,7 +19,7 @@ */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; +import { CallToolRequestSchema, CallToolResult, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { WebSocketServer, WebSocket } from "ws"; import { Tool } from "./interfaces/Tool.js"; @@ -100,7 +100,7 @@ class PenpotMcpServer { }; }); - this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + this.server.setRequestHandler(CallToolRequestSchema, async (request): Promise => { const { name, arguments: args } = request.params; const tool = this.tools.get(name); diff --git a/mcp-server/src/interfaces/Tool.ts b/mcp-server/src/interfaces/Tool.ts index d4bcb1e..9a8f07c 100644 --- a/mcp-server/src/interfaces/Tool.ts +++ b/mcp-server/src/interfaces/Tool.ts @@ -2,6 +2,7 @@ import { Tool as MCPTool } from "@modelcontextprotocol/sdk/types.js"; import { validate, ValidationError } from "class-validator"; import { plainToClass } from "class-transformer"; import "reflect-metadata"; +import { ToolResponse } from "./ToolResponse"; /** * Defines the contract for MCP tool implementations. @@ -21,7 +22,7 @@ export interface Tool { * @param args - The arguments passed to the tool (validated by implementation) * @returns A promise that resolves to the tool's execution result */ - execute(args: unknown): Promise<{ content: Array<{ type: string; text: string }> }>; + execute(args: unknown): Promise; } /** @@ -66,7 +67,7 @@ export abstract class TypeSafeTool implements Tool { * This method handles the unknown args from the MCP protocol, * validates them, and delegates to the type-safe implementation. */ - async execute(args: unknown): Promise<{ content: Array<{ type: string; text: string }> }> { + async execute(args: unknown): Promise { try { // Transform plain object to class instance const argsInstance = plainToClass(this.ArgsClass, args as object); @@ -207,5 +208,5 @@ export abstract class TypeSafeTool implements Tool { * * @param args - The validated, strongly-typed arguments */ - protected abstract executeTypeSafe(args: TArgs): Promise<{ content: Array<{ type: string; text: string }> }>; + protected abstract executeTypeSafe(args: TArgs): Promise; } diff --git a/mcp-server/src/interfaces/ToolResponse.ts b/mcp-server/src/interfaces/ToolResponse.ts new file mode 100644 index 0000000..15ee74c --- /dev/null +++ b/mcp-server/src/interfaces/ToolResponse.ts @@ -0,0 +1,20 @@ +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +type CallToolContent = CallToolResult["content"][number]; +type TextItem = Extract; + +class TextContent implements TextItem { + [x: string]: unknown; + readonly type = "text" as const; + constructor(public text: string) {} +} + +export class TextResponse implements CallToolResult { + [x: string]: unknown; + content: CallToolContent[]; // <- IMPORTANT: protocol’s union + constructor(text: string) { + this.content = [new TextContent(text)]; + } +} + +export type ToolResponse = TextResponse; diff --git a/mcp-server/src/tools/HelloWorldTool.ts b/mcp-server/src/tools/HelloWorldTool.ts index d248971..b86f5d5 100644 --- a/mcp-server/src/tools/HelloWorldTool.ts +++ b/mcp-server/src/tools/HelloWorldTool.ts @@ -1,6 +1,8 @@ -import { IsString, IsNotEmpty } from "class-validator"; +import { IsNotEmpty, IsString } from "class-validator"; import { TypeSafeTool } from "../interfaces/Tool.js"; import "reflect-metadata"; +import type { ToolResponse } from "../interfaces/ToolResponse.js"; +import { TextResponse } from "../interfaces/ToolResponse.js"; /** * Arguments class for the HelloWorld tool with validation decorators. @@ -41,14 +43,9 @@ export class HelloWorldTool extends TypeSafeTool { * * @param args - The validated HelloWorldArgs instance */ - protected async executeTypeSafe(args: HelloWorldArgs): Promise<{ content: Array<{ type: string; text: string }> }> { - return { - content: [ - { - type: "text", - text: `Hello, ${args.name}! This greeting was generated with full type safety and automatic validation.`, - }, - ], - }; + protected async executeTypeSafe(args: HelloWorldArgs): Promise { + return new TextResponse( + `Hello, ${args.name}! This greeting was generated with full type safety and automatic validation.` + ); } } diff --git a/mcp-server/src/tools/ToolPrintText.ts b/mcp-server/src/tools/ToolPrintText.ts index 9a45dcc..ce85607 100644 --- a/mcp-server/src/tools/ToolPrintText.ts +++ b/mcp-server/src/tools/ToolPrintText.ts @@ -1,6 +1,8 @@ -import { IsString, IsNotEmpty } from "class-validator"; +import { IsNotEmpty, IsString } from "class-validator"; import { TypeSafeTool } from "../interfaces/Tool.js"; import { PluginTaskPrintText, PluginTaskPrintTextParams } from "../interfaces/PluginTask.js"; +import type { ToolResponse } from "../interfaces/ToolResponse.js"; +import { TextResponse } from "../interfaces/ToolResponse.js"; import "reflect-metadata"; /** @@ -50,7 +52,7 @@ export class ToolPrintText extends TypeSafeTool { * * @param args - The validated PrintTextArgs instance */ - protected async executeTypeSafe(args: PrintTextArgs): Promise<{ content: Array<{ type: string; text: string }> }> { + protected async executeTypeSafe(args: PrintTextArgs): Promise { try { // Create the plugin task const taskParams = new PluginTaskPrintTextParams(args.text); @@ -58,14 +60,9 @@ export class ToolPrintText extends TypeSafeTool { // Check if there are connected clients if (this.connectedClients.size === 0) { - return { - content: [ - { - type: "text", - text: `No Penpot plugin instances are currently connected. Please ensure the plugin is running and connected.`, - }, - ], - }; + return new TextResponse( + `No Penpot plugin instances are currently connected. Please ensure the plugin is running and connected.` + ); } // Send task to all connected clients @@ -81,34 +78,17 @@ export class ToolPrintText extends TypeSafeTool { }); if (sentCount === 0) { - return { - content: [ - { - type: "text", - text: `All connected plugin instances appear to be disconnected. No text was created.`, - }, - ], - }; + return new TextResponse( + `All connected plugin instances appear to be disconnected. No text was created.` + ); } - return { - content: [ - { - type: "text", - text: `Successfully sent text creation task to ${sentCount} connected plugin instance(s). Text "${args.text}" should now appear in Penpot.`, - }, - ], - }; + return new TextResponse( + `Successfully sent text creation task to ${sentCount} connected plugin instance(s). Text "${args.text}" should now appear in Penpot.` + ); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; - return { - content: [ - { - type: "text", - text: `Failed to create text in Penpot: ${errorMessage}`, - }, - ], - }; + return new TextResponse(`Failed to create text in Penpot: ${errorMessage}`); } } }