mirror of
https://github.com/penpot/penpot-mcp.git
synced 2026-04-25 11:18:37 +00:00
Improve handling of tool responses, adding explicit classes
This commit is contained in:
parent
f5bdb1accb
commit
e714caaef2
@ -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);
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
20
mcp-server/src/interfaces/ToolResponse.ts
Normal file
20
mcp-server/src/interfaces/ToolResponse.ts
Normal 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: protocol’s union
|
||||
constructor(text: string) {
|
||||
this.content = [new TextContent(text)];
|
||||
}
|
||||
}
|
||||
|
||||
export type ToolResponse = TextResponse;
|
||||
@ -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.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user