mirror of
https://github.com/penpot/penpot-mcp.git
synced 2026-07-20 21:17:54 +00:00
Switch from low-level Server to McpServer
This commit is contained in:
parent
e0efe2b110
commit
b94f29fcf1
@ -1,264 +1,130 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { CallToolRequestSchema, CallToolResult, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
|
||||
import { ToolInterface } from "./Tool";
|
||||
import { HelloWorldTool } from "./tools/HelloWorldTool";
|
||||
import { PrintTextTool } from "./tools/PrintTextTool";
|
||||
import { ExecuteCodeTool } from "./tools/ExecuteCodeTool";
|
||||
import { PluginBridge } from "./PluginBridge";
|
||||
import { ConfigurationLoader } from "./ConfigurationLoader";
|
||||
import { createLogger } from "./logger";
|
||||
import { Tool } from "./Tool";
|
||||
|
||||
/**
|
||||
* Penpot MCP server implementation with HTTP and SSE Transport Support
|
||||
*/
|
||||
export class PenpotMcpServer {
|
||||
private readonly logger = createLogger("PenpotMcpServer");
|
||||
private readonly server: Server;
|
||||
private readonly tools: Map<string, ToolInterface>;
|
||||
private readonly configLoader: ConfigurationLoader;
|
||||
private app: any; // Express app
|
||||
private readonly server: McpServer;
|
||||
private readonly tools: Map<string, Tool<any>>;
|
||||
public readonly configLoader: ConfigurationLoader;
|
||||
private app: any;
|
||||
private readonly port: number;
|
||||
public readonly pluginBridge: PluginBridge;
|
||||
|
||||
// Store transports for each session type
|
||||
private readonly transports = {
|
||||
streamable: {} as Record<string, any>, // StreamableHTTPServerTransport
|
||||
sse: {} as Record<string, any>, // SSEServerTransport
|
||||
streamable: {} as Record<string, StreamableHTTPServerTransport>,
|
||||
sse: {} as Record<string, SSEServerTransport>,
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new Penpot MCP server instance.
|
||||
*
|
||||
* @param port - The port number for the HTTP/SSE server
|
||||
* @param webSocketPort - The port number for the plugin bridge's WebSocket server
|
||||
*/
|
||||
constructor(port: number = 4401, webSocketPort: number = 8080) {
|
||||
this.configLoader = new ConfigurationLoader();
|
||||
this.port = port;
|
||||
this.server = new Server(
|
||||
|
||||
const instructions = this.configLoader.getInitialInstructions();
|
||||
this.logger.info("Instructions: %s", instructions ?? "<none>");
|
||||
this.server = new McpServer(
|
||||
{
|
||||
name: "penpot-mcp-server",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
instructions: instructions,
|
||||
}
|
||||
);
|
||||
|
||||
this.tools = new Map<string, ToolInterface>();
|
||||
this.configLoader = new ConfigurationLoader();
|
||||
this.tools = new Map<string, Tool<any>>();
|
||||
this.pluginBridge = new PluginBridge(webSocketPort);
|
||||
|
||||
this.setupMcpHandlers();
|
||||
this.registerTools();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all available tools with the server.
|
||||
*
|
||||
* This method instantiates tool implementations and adds them to
|
||||
* the internal registry for later execution.
|
||||
*/
|
||||
private registerTools(): void {
|
||||
const toolInstances: ToolInterface[] = [new HelloWorldTool(this), new PrintTextTool(this), new ExecuteCodeTool(this)];
|
||||
const toolInstances: Tool<any>[] = [
|
||||
new HelloWorldTool(this),
|
||||
new PrintTextTool(this),
|
||||
new ExecuteCodeTool(this),
|
||||
];
|
||||
|
||||
for (const tool of toolInstances) {
|
||||
this.tools.set(tool.definition.name, tool);
|
||||
}
|
||||
}
|
||||
const toolName = tool.getToolName();
|
||||
this.tools.set(toolName, tool);
|
||||
|
||||
/**
|
||||
* Sets up the MCP protocol request handlers.
|
||||
*
|
||||
* Configures handlers for tool listing and execution requests
|
||||
* according to the MCP specification.
|
||||
*/
|
||||
private setupMcpHandlers(): void {
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: Array.from(this.tools.values()).map((tool) => tool.definition),
|
||||
};
|
||||
});
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request): Promise<CallToolResult> => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
const tool = this.tools.get(name);
|
||||
if (!tool) {
|
||||
throw new Error(`Tool "${name}" not found`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await tool.execute(args);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
throw new Error(`Tool execution failed: ${errorMessage}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up HTTP endpoints for modern Streamable HTTP and legacy SSE transports.
|
||||
*
|
||||
* Provides backwards compatibility by supporting both transport mechanisms
|
||||
* for different client capabilities.
|
||||
*/
|
||||
private setupHttpEndpoints(): void {
|
||||
// Modern Streamable HTTP endpoint
|
||||
this.app.all("/mcp", async (req: any, res: any) => {
|
||||
await this.handleStreamableHttpRequest(req, res);
|
||||
});
|
||||
|
||||
// Legacy SSE endpoint for older clients
|
||||
this.app.get("/sse", async (req: any, res: any) => {
|
||||
await this.handleSseConnection(req, res);
|
||||
});
|
||||
|
||||
// Legacy message endpoint for older clients
|
||||
this.app.post("/messages", async (req: any, res: any) => {
|
||||
await this.handleSseMessage(req, res);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Streamable HTTP requests for modern MCP clients.
|
||||
*
|
||||
* Provides session management and request routing for the new
|
||||
* streamable HTTP transport protocol.
|
||||
*/
|
||||
private async handleStreamableHttpRequest(req: any, res: any): Promise<void> {
|
||||
const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
||||
const { randomUUID } = await import("node:crypto");
|
||||
const { isInitializeRequest } = await import("@modelcontextprotocol/sdk/types.js");
|
||||
|
||||
// Check for existing session ID
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
let transport: any;
|
||||
|
||||
if (sessionId && this.transports.streamable[sessionId]) {
|
||||
// Reuse existing transport
|
||||
transport = this.transports.streamable[sessionId];
|
||||
} else if (!sessionId && isInitializeRequest(req.body)) {
|
||||
// New initialization request
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
onsessioninitialized: (sessionId: string) => {
|
||||
// Store the transport by session ID
|
||||
this.transports.streamable[sessionId] = transport;
|
||||
// Register each tool with McpServer
|
||||
this.logger.info(`Registering tool: ${toolName}`);
|
||||
this.server.registerTool(
|
||||
toolName,
|
||||
{
|
||||
description: tool.getToolDescription(),
|
||||
inputSchema: tool.getInputSchema(),
|
||||
},
|
||||
// DNS rebinding protection is disabled by default for backwards compatibility
|
||||
// If running locally, consider enabling:
|
||||
// enableDnsRebindingProtection: true,
|
||||
// allowedHosts: ['127.0.0.1'],
|
||||
});
|
||||
|
||||
// Clean up transport when closed
|
||||
transport.onclose = () => {
|
||||
if (transport.sessionId) {
|
||||
delete this.transports.streamable[transport.sessionId];
|
||||
async (args) => {
|
||||
return tool.execute(args);
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to the MCP server
|
||||
await this.server.connect(transport);
|
||||
} else {
|
||||
// Invalid request
|
||||
res.status(400).json({
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32000,
|
||||
message: "Bad Request: No valid session ID provided",
|
||||
},
|
||||
id: null,
|
||||
});
|
||||
return;
|
||||
);
|
||||
}
|
||||
|
||||
// Handle the request
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles SSE connection establishment for legacy MCP clients.
|
||||
*
|
||||
* Creates and manages Server-Sent Events transport for older
|
||||
* clients that don't support the streamable HTTP protocol.
|
||||
*/
|
||||
private async handleSseConnection(req: any, res: any): Promise<void> {
|
||||
const { SSEServerTransport } = await import("@modelcontextprotocol/sdk/server/sse.js");
|
||||
private setupHttpEndpoints(): void {
|
||||
this.app.all("/mcp", async (req: any, res: any) => {
|
||||
const { randomUUID } = await import("node:crypto");
|
||||
|
||||
// Create SSE transport for legacy clients
|
||||
const transport = new SSEServerTransport("/messages", res);
|
||||
this.transports.sse[transport.sessionId] = transport;
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
|
||||
res.on("close", () => {
|
||||
delete this.transports.sse[transport.sessionId];
|
||||
if (sessionId && this.transports.streamable[sessionId]) {
|
||||
transport = this.transports.streamable[sessionId];
|
||||
} else {
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
onsessioninitialized: (id: string) => {
|
||||
this.transports.streamable[id] = transport;
|
||||
},
|
||||
});
|
||||
|
||||
transport.onclose = () => {
|
||||
if (transport.sessionId) {
|
||||
delete this.transports.streamable[transport.sessionId];
|
||||
}
|
||||
};
|
||||
|
||||
await this.server.connect(transport);
|
||||
}
|
||||
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
});
|
||||
|
||||
await this.server.connect(transport);
|
||||
}
|
||||
this.app.get("/sse", async (_req: any, res: any) => {
|
||||
const transport = new SSEServerTransport("/messages", res);
|
||||
this.transports.sse[transport.sessionId] = transport;
|
||||
|
||||
/**
|
||||
* Handles POST message requests for legacy SSE clients.
|
||||
*
|
||||
* Routes messages to the appropriate SSE transport based on
|
||||
* the provided session identifier.
|
||||
*/
|
||||
private async handleSseMessage(req: any, res: any): Promise<void> {
|
||||
const sessionId = req.query.sessionId as string;
|
||||
const transport = this.transports.sse[sessionId];
|
||||
res.on("close", () => {
|
||||
delete this.transports.sse[transport.sessionId];
|
||||
});
|
||||
|
||||
if (transport) {
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
} else {
|
||||
res.status(400).send("No transport found for sessionId");
|
||||
}
|
||||
}
|
||||
await this.server.connect(transport);
|
||||
});
|
||||
|
||||
/**
|
||||
* Starts the MCP server using HTTP and SSE transports.
|
||||
*
|
||||
* This method establishes the HTTP server and begins listening
|
||||
* for both modern and legacy MCP protocol connections.
|
||||
*/
|
||||
/**
|
||||
* Displays initial instructions from the configuration.
|
||||
*
|
||||
* Loads and logs the initial instructions for the MCP server,
|
||||
* providing helpful information to users about available capabilities
|
||||
* and usage guidelines.
|
||||
*/
|
||||
private displayInitialInstructions(): void {
|
||||
const initialInstructions = this.configLoader.getInitialInstructions();
|
||||
|
||||
if (initialInstructions) {
|
||||
this.logger.info("=".repeat(80));
|
||||
this.logger.info("INITIAL INSTRUCTIONS");
|
||||
this.logger.info("=".repeat(80));
|
||||
|
||||
// Split instructions by lines and log each one separately for better formatting
|
||||
const lines = initialInstructions.split('\n');
|
||||
for (const line of lines) {
|
||||
this.logger.info(line);
|
||||
this.app.post("/messages", async (req: any, res: any) => {
|
||||
const sessionId = req.query.sessionId as string;
|
||||
const transport = this.transports.sse[sessionId];
|
||||
|
||||
if (transport) {
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
} else {
|
||||
res.status(400).send("No transport found for sessionId");
|
||||
}
|
||||
|
||||
this.logger.info("=".repeat(80));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configuration loader instance.
|
||||
*
|
||||
* @returns The configuration loader for accessing prompts and settings
|
||||
*/
|
||||
public getConfigurationLoader(): ConfigurationLoader {
|
||||
return this.configLoader;
|
||||
});
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
// Import express as ES module and setup HTTP endpoints
|
||||
const { default: express } = await import("express");
|
||||
this.app = express();
|
||||
this.app.use(express.json());
|
||||
@ -267,14 +133,10 @@ export class PenpotMcpServer {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.app.listen(this.port, () => {
|
||||
this.logger.info(`Penpot MCP Server started successfully on port ${this.port}`);
|
||||
this.logger.info(`Penpot MCP Server started on port ${this.port}`);
|
||||
this.logger.info(`Modern Streamable HTTP endpoint: http://localhost:${this.port}/mcp`);
|
||||
this.logger.info(`Legacy SSE endpoint: http://localhost:${this.port}/sse`);
|
||||
this.logger.info("WebSocket server is listening on ws://localhost:8080");
|
||||
|
||||
// Display initial instructions if configured
|
||||
this.displayInitialInstructions();
|
||||
|
||||
this.logger.info("WebSocket server is on ws://localhost:8080");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,38 +1,17 @@
|
||||
import { z } from "zod";
|
||||
import { Tool as MCPTool } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { validate, ValidationError } from "class-validator";
|
||||
import { plainToClass } from "class-transformer";
|
||||
import "reflect-metadata";
|
||||
import { TextResponse, ToolResponse } from "./ToolResponse";
|
||||
import type { PenpotMcpServer } from "./PenpotMcpServer";
|
||||
import { createLogger } from "./logger";
|
||||
|
||||
/**
|
||||
* Base interface for MCP tool implementations.
|
||||
*
|
||||
* This interface maintains compatibility with the MCP protocol.
|
||||
* Most implementations should extend the Tool abstract class instead.
|
||||
* An empty arguments class for tools that do not require any parameters.
|
||||
*/
|
||||
export interface ToolInterface {
|
||||
/**
|
||||
* The tool's unique identifier and metadata definition.
|
||||
*/
|
||||
readonly definition: MCPTool;
|
||||
|
||||
/**
|
||||
* Executes the tool's primary functionality with provided arguments.
|
||||
*
|
||||
* @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<ToolResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for schema generation from class properties.
|
||||
*/
|
||||
interface PropertyMetadata {
|
||||
type: "string" | "number" | "boolean" | "array" | "object";
|
||||
description: string;
|
||||
required: boolean;
|
||||
export class EmptyToolArgs {
|
||||
static schema = {};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,28 +22,14 @@ interface PropertyMetadata {
|
||||
*
|
||||
* @template TArgs - The strongly-typed arguments class for this tool
|
||||
*/
|
||||
export abstract class Tool<TArgs extends object> implements ToolInterface {
|
||||
private _definition: MCPTool | undefined;
|
||||
export abstract class Tool<TArgs extends object> {
|
||||
private readonly logger = createLogger("Tool");
|
||||
|
||||
protected constructor(
|
||||
protected mcpServer: PenpotMcpServer,
|
||||
private ArgsClass: new () => TArgs
|
||||
private inputSchema: z.ZodRawShape
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Gets the tool definition with automatically generated JSON schema.
|
||||
*/
|
||||
get definition(): MCPTool {
|
||||
if (!this._definition) {
|
||||
this._definition = {
|
||||
name: this.getToolName(),
|
||||
description: this.getToolDescription(),
|
||||
inputSchema: this.generateInputSchema(),
|
||||
};
|
||||
}
|
||||
return this._definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the tool with automatic validation and type safety.
|
||||
*
|
||||
@ -73,132 +38,35 @@ export abstract class Tool<TArgs extends object> implements ToolInterface {
|
||||
*/
|
||||
async execute(args: unknown): Promise<ToolResponse> {
|
||||
try {
|
||||
// transform plain object to class instance
|
||||
const argsInstance = plainToClass(this.ArgsClass, args as object);
|
||||
this.logger.info("Executing tool: %s", this.getToolName());
|
||||
|
||||
// validate using class-validator decorators
|
||||
const errors = await validate(argsInstance);
|
||||
if (errors.length > 0) {
|
||||
const errorMessages = this.formatValidationErrors(errors);
|
||||
throw new Error(`Validation failed: ${errorMessages.join(", ")}`);
|
||||
}
|
||||
let argsInstance: TArgs = args as TArgs;
|
||||
this.logger.debug("Tool args: %o", argsInstance);
|
||||
|
||||
// execute the actual tool logic
|
||||
return await this.executeCore(argsInstance);
|
||||
let result = await this.executeCore(argsInstance);
|
||||
|
||||
this.logger.info("Tool execution completed: %s", this.getToolName());
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return new TextResponse(`Tool execution failed: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates JSON schema from class-validator decorators and property metadata.
|
||||
*/
|
||||
private generateInputSchema() {
|
||||
const instance = new this.ArgsClass();
|
||||
const properties: Record<string, any> = {};
|
||||
const required: string[] = [];
|
||||
|
||||
const propertyNames = this.getPropertyNames(instance);
|
||||
|
||||
for (const propName of propertyNames) {
|
||||
const metadata = this.getPropertyMetadata(this.ArgsClass, propName);
|
||||
properties[propName] = {
|
||||
type: metadata.type,
|
||||
description: metadata.description,
|
||||
};
|
||||
|
||||
if (metadata.required) {
|
||||
required.push(propName);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: "object" as const,
|
||||
properties,
|
||||
required,
|
||||
additionalProperties: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all property names from a class instance.
|
||||
*/
|
||||
private getPropertyNames(instance: TArgs): string[] {
|
||||
const prototype = Object.getPrototypeOf(instance);
|
||||
const propertyNames: string[] = [];
|
||||
|
||||
propertyNames.push(...Object.getOwnPropertyNames(instance));
|
||||
propertyNames.push(...Object.getOwnPropertyNames(prototype));
|
||||
|
||||
return propertyNames.filter(
|
||||
(name) => name !== "constructor" && !name.startsWith("_") && typeof (instance as any)[name] !== "function"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts property metadata from class-validator decorators.
|
||||
*/
|
||||
private getPropertyMetadata(target: any, propertyKey: string): PropertyMetadata {
|
||||
const validationMetadata = Reflect.getMetadata("class-validator:storage", target) || {};
|
||||
const constraints = validationMetadata.validationMetadatas || [];
|
||||
|
||||
let isRequired = true;
|
||||
let type: PropertyMetadata["type"] = "string";
|
||||
let description = `${propertyKey} parameter`;
|
||||
|
||||
for (const constraint of constraints) {
|
||||
if (constraint.propertyName === propertyKey) {
|
||||
switch (constraint.type) {
|
||||
case "isOptional":
|
||||
isRequired = false;
|
||||
break;
|
||||
case "isString":
|
||||
type = "string";
|
||||
break;
|
||||
case "isNumber":
|
||||
type = "number";
|
||||
break;
|
||||
case "isBoolean":
|
||||
type = "boolean";
|
||||
break;
|
||||
case "isArray":
|
||||
type = "array";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback type inference
|
||||
if (
|
||||
propertyKey.toLowerCase().includes("count") ||
|
||||
propertyKey.toLowerCase().includes("number") ||
|
||||
propertyKey.toLowerCase().includes("amount")
|
||||
) {
|
||||
type = "number";
|
||||
}
|
||||
|
||||
return { type, description, required: isRequired };
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats validation errors into human-readable messages.
|
||||
*/
|
||||
private formatValidationErrors(errors: ValidationError[]): string[] {
|
||||
return errors.map((error) => {
|
||||
const constraints = Object.values(error.constraints || {});
|
||||
return `${error.property}: ${constraints.join(", ")}`;
|
||||
});
|
||||
public getInputSchema() {
|
||||
return this.inputSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tool's unique name.
|
||||
*/
|
||||
protected abstract getToolName(): string;
|
||||
public abstract getToolName(): string;
|
||||
|
||||
/**
|
||||
* Returns the tool's description.
|
||||
*/
|
||||
protected abstract getToolDescription(): string;
|
||||
public abstract getToolDescription(): string;
|
||||
|
||||
/**
|
||||
* Executes the tool's core logic.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { IsNotEmpty, IsString } from "class-validator";
|
||||
import { z } from "zod";
|
||||
import { Tool } from "../Tool";
|
||||
import type { ToolResponse } from "../ToolResponse";
|
||||
import { TextResponse } from "../ToolResponse";
|
||||
@ -11,11 +11,13 @@ import { ExecuteCodeTaskParams } from "@penpot-mcp/common";
|
||||
* Arguments class for ExecuteCodeTool
|
||||
*/
|
||||
export class ExecuteCodeArgs {
|
||||
static schema = {
|
||||
code: z.string().min(1, "Code cannot be empty"),
|
||||
};
|
||||
|
||||
/**
|
||||
* The JavaScript code to execute in the plugin context.
|
||||
*/
|
||||
@IsString({ message: "Code must be a string" })
|
||||
@IsNotEmpty({ message: "Code cannot be empty" })
|
||||
code!: string;
|
||||
}
|
||||
|
||||
@ -29,14 +31,14 @@ export class ExecuteCodeTool extends Tool<ExecuteCodeArgs> {
|
||||
* @param mcpServer - The MCP server instance
|
||||
*/
|
||||
constructor(mcpServer: PenpotMcpServer) {
|
||||
super(mcpServer, ExecuteCodeArgs);
|
||||
super(mcpServer, ExecuteCodeArgs.schema);
|
||||
}
|
||||
|
||||
protected getToolName(): string {
|
||||
public getToolName(): string {
|
||||
return "execute_code";
|
||||
}
|
||||
|
||||
protected getToolDescription(): string {
|
||||
public getToolDescription(): string {
|
||||
return (
|
||||
"Executes JavaScript code in the Penpot plugin context. " +
|
||||
"Two objects are available: `penpot` (the Penpot API) and `storage` (an object in which arbitrary " +
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { IsNotEmpty, IsString } from "class-validator";
|
||||
import { z } from "zod";
|
||||
import { Tool } from "../Tool";
|
||||
import "reflect-metadata";
|
||||
import type { ToolResponse } from "../ToolResponse";
|
||||
@ -9,11 +9,13 @@ import { PenpotMcpServer } from "../PenpotMcpServer";
|
||||
* Arguments class for the HelloWorld tool with validation decorators.
|
||||
*/
|
||||
export class HelloWorldArgs {
|
||||
static schema = {
|
||||
name: z.string(),
|
||||
};
|
||||
|
||||
/**
|
||||
* The name to include in the greeting message.
|
||||
*/
|
||||
@IsString({ message: "Name must be a string" })
|
||||
@IsNotEmpty({ message: "Name cannot be empty" })
|
||||
name!: string;
|
||||
}
|
||||
|
||||
@ -28,14 +30,14 @@ export class HelloWorldTool extends Tool<HelloWorldArgs> {
|
||||
* @param mcpServer - The MCP server instance
|
||||
*/
|
||||
constructor(mcpServer: PenpotMcpServer) {
|
||||
super(mcpServer, HelloWorldArgs);
|
||||
super(mcpServer, HelloWorldArgs.schema);
|
||||
}
|
||||
|
||||
protected getToolName(): string {
|
||||
public getToolName(): string {
|
||||
return "hello_world";
|
||||
}
|
||||
|
||||
protected getToolDescription(): string {
|
||||
public getToolDescription(): string {
|
||||
return "Returns a personalized greeting message with the provided name";
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { IsNotEmpty, IsString } from "class-validator";
|
||||
import { z } from "zod";
|
||||
import { Tool } from "../Tool";
|
||||
import type { ToolResponse } from "../ToolResponse";
|
||||
import { TextResponse } from "../ToolResponse";
|
||||
@ -11,11 +11,13 @@ import { PrintTextTaskParams } from "@penpot-mcp/common";
|
||||
* Arguments class for the PrintText tool with validation decorators.
|
||||
*/
|
||||
export class PrintTextArgs {
|
||||
static schema = {
|
||||
text: z.string().min(1, "Text cannot be empty"),
|
||||
};
|
||||
|
||||
/**
|
||||
* The text to create in Penpot.
|
||||
*/
|
||||
@IsString({ message: "Text must be a string" })
|
||||
@IsNotEmpty({ message: "Text cannot be empty" })
|
||||
text!: string;
|
||||
}
|
||||
|
||||
@ -32,14 +34,14 @@ export class PrintTextTool extends Tool<PrintTextArgs> {
|
||||
* @param mcpServer - The MCP server instance
|
||||
*/
|
||||
constructor(mcpServer: PenpotMcpServer) {
|
||||
super(mcpServer, PrintTextArgs);
|
||||
super(mcpServer, PrintTextArgs.schema);
|
||||
}
|
||||
|
||||
protected getToolName(): string {
|
||||
public getToolName(): string {
|
||||
return "print_text";
|
||||
}
|
||||
|
||||
protected getToolDescription(): string {
|
||||
public getToolDescription(): string {
|
||||
return "Creates text in Penpot at the viewport center and selects it";
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user