Improve handling of tool responses, adding explicit classes

This commit is contained in:
Dominik Jain 2025-09-11 13:55:33 +02:00
parent f5bdb1accb
commit e714caaef2
5 changed files with 47 additions and 49 deletions

View File

@ -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<CallToolResult> => {
const { name, arguments: args } = request.params;
const tool = this.tools.get(name);

View File

@ -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<ToolResponse>;
}
/**
@ -66,7 +67,7 @@ export abstract class TypeSafeTool<TArgs extends object> 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<ToolResponse> {
try {
// Transform plain object to class instance
const argsInstance = plainToClass(this.ArgsClass, args as object);
@ -207,5 +208,5 @@ export abstract class TypeSafeTool<TArgs extends object> 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<ToolResponse>;
}

View File

@ -0,0 +1,20 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
type CallToolContent = CallToolResult["content"][number];
type TextItem = Extract<CallToolContent, { type: "text" }>;
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: protocols union
constructor(text: string) {
this.content = [new TextContent(text)];
}
}
export type ToolResponse = TextResponse;

View File

@ -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<HelloWorldArgs> {
*
* @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<ToolResponse> {
return new TextResponse(
`Hello, ${args.name}! This greeting was generated with full type safety and automatic validation.`
);
}
}

View File

@ -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<PrintTextArgs> {
*
* @param args - The validated PrintTextArgs instance
*/
protected async executeTypeSafe(args: PrintTextArgs): Promise<{ content: Array<{ type: string; text: string }> }> {
protected async executeTypeSafe(args: PrintTextArgs): Promise<ToolResponse> {
try {
// Create the plugin task
const taskParams = new PluginTaskPrintTextParams(args.text);
@ -58,14 +60,9 @@ export class ToolPrintText extends TypeSafeTool<PrintTextArgs> {
// 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<PrintTextArgs> {
});
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}`);
}
}
}