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 { 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 { WebSocketServer, WebSocket } from "ws";
|
||||||
|
|
||||||
import { Tool } from "./interfaces/Tool.js";
|
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 { name, arguments: args } = request.params;
|
||||||
|
|
||||||
const tool = this.tools.get(name);
|
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 { validate, ValidationError } from "class-validator";
|
||||||
import { plainToClass } from "class-transformer";
|
import { plainToClass } from "class-transformer";
|
||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
|
import { ToolResponse } from "./ToolResponse";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the contract for MCP tool implementations.
|
* 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)
|
* @param args - The arguments passed to the tool (validated by implementation)
|
||||||
* @returns A promise that resolves to the tool's execution result
|
* @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,
|
* This method handles the unknown args from the MCP protocol,
|
||||||
* validates them, and delegates to the type-safe implementation.
|
* 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 {
|
try {
|
||||||
// Transform plain object to class instance
|
// Transform plain object to class instance
|
||||||
const argsInstance = plainToClass(this.ArgsClass, args as object);
|
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
|
* @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 { TypeSafeTool } from "../interfaces/Tool.js";
|
||||||
import "reflect-metadata";
|
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.
|
* 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
|
* @param args - The validated HelloWorldArgs instance
|
||||||
*/
|
*/
|
||||||
protected async executeTypeSafe(args: HelloWorldArgs): Promise<{ content: Array<{ type: string; text: string }> }> {
|
protected async executeTypeSafe(args: HelloWorldArgs): Promise<ToolResponse> {
|
||||||
return {
|
return new TextResponse(
|
||||||
content: [
|
`Hello, ${args.name}! This greeting was generated with full type safety and automatic validation.`
|
||||||
{
|
);
|
||||||
type: "text",
|
|
||||||
text: `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 { TypeSafeTool } from "../interfaces/Tool.js";
|
||||||
import { PluginTaskPrintText, PluginTaskPrintTextParams } from "../interfaces/PluginTask.js";
|
import { PluginTaskPrintText, PluginTaskPrintTextParams } from "../interfaces/PluginTask.js";
|
||||||
|
import type { ToolResponse } from "../interfaces/ToolResponse.js";
|
||||||
|
import { TextResponse } from "../interfaces/ToolResponse.js";
|
||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,7 +52,7 @@ export class ToolPrintText extends TypeSafeTool<PrintTextArgs> {
|
|||||||
*
|
*
|
||||||
* @param args - The validated PrintTextArgs instance
|
* @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 {
|
try {
|
||||||
// Create the plugin task
|
// Create the plugin task
|
||||||
const taskParams = new PluginTaskPrintTextParams(args.text);
|
const taskParams = new PluginTaskPrintTextParams(args.text);
|
||||||
@ -58,14 +60,9 @@ export class ToolPrintText extends TypeSafeTool<PrintTextArgs> {
|
|||||||
|
|
||||||
// Check if there are connected clients
|
// Check if there are connected clients
|
||||||
if (this.connectedClients.size === 0) {
|
if (this.connectedClients.size === 0) {
|
||||||
return {
|
return new TextResponse(
|
||||||
content: [
|
`No Penpot plugin instances are currently connected. Please ensure the plugin is running and connected.`
|
||||||
{
|
);
|
||||||
type: "text",
|
|
||||||
text: `No Penpot plugin instances are currently connected. Please ensure the plugin is running and connected.`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send task to all connected clients
|
// Send task to all connected clients
|
||||||
@ -81,34 +78,17 @@ export class ToolPrintText extends TypeSafeTool<PrintTextArgs> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (sentCount === 0) {
|
if (sentCount === 0) {
|
||||||
return {
|
return new TextResponse(
|
||||||
content: [
|
`All connected plugin instances appear to be disconnected. No text was created.`
|
||||||
{
|
);
|
||||||
type: "text",
|
|
||||||
text: `All connected plugin instances appear to be disconnected. No text was created.`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return new TextResponse(
|
||||||
content: [
|
`Successfully sent text creation task to ${sentCount} connected plugin instance(s). Text "${args.text}" should now appear in Penpot.`
|
||||||
{
|
);
|
||||||
type: "text",
|
|
||||||
text: `Successfully sent text creation task to ${sentCount} connected plugin instance(s). Text "${args.text}" should now appear in Penpot.`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||||
return {
|
return new TextResponse(`Failed to create text in Penpot: ${errorMessage}`);
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: `Failed to create text in Penpot: ${errorMessage}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user