diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md index f786cce..38185d0 100644 --- a/.serena/memories/project_overview.md +++ b/.serena/memories/project_overview.md @@ -39,8 +39,9 @@ penpot-mcp/ ### Adding a new Tool -* Implement the tool class in `mcp-server/src/tools/` following the `Tool` interface. -* IMPORTANT: Do not catch any exceptions in the `executeCore` method. Let them propagate to be handled centrally. +1. Implement the tool class in `mcp-server/src/tools/` following the `Tool` interface. + IMPORTANT: Do not catch any exceptions in the `executeCore` method. Let them propagate to be handled centrally. +2. Register the tool in `PenpotMcpServer`. Look at `PrintTextTool` as an example. diff --git a/mcp-server/data/prompts.yml b/mcp-server/data/prompts.yml index 06ce675..f1f590d 100644 --- a/mcp-server/data/prompts.yml +++ b/mcp-server/data/prompts.yml @@ -10,7 +10,7 @@ initial_instructions: | One of the key tools is the execute_code tool, which allows you to run JavaScript code using the Penpot Plugin API directly in the connected project. - When writing code, a key object is the `penpot` object which provides key functionality: + When writing code, a key object is the `penpot` object (which is of type `Penpot`). It provides useful functionality: * `penpot.selection` provides the list of elements the user has selected in the Penpot UI. If it is unclear which elements to work on, you can ask the user to select them for you. * Generation of CSS content for elements: @@ -24,7 +24,7 @@ initial_instructions: | type?: "html" | "svg"; }): string; - For example, to generate CSS for the selected elements, you can execute this: + For example, to generate CSS for the currently selected elements, you can execute this: return penpot.generateStyle(penpot.selection, { type: "css", withChildren: true }); You have hereby read the 'Penpot High-Level Overview' and need not use a tool to read it again. diff --git a/mcp-server/src/ApiDocs.ts b/mcp-server/src/ApiDocs.ts index 1df4208..ab8f551 100644 --- a/mcp-server/src/ApiDocs.ts +++ b/mcp-server/src/ApiDocs.ts @@ -3,7 +3,7 @@ import * as fs from "fs"; import * as path from "path"; /** - * Represents a single API type with its documentation. + * Represents a single type/interface defined in the Penpot API */ export class ApiType { private readonly name: string; @@ -24,6 +24,13 @@ export class ApiType { return this.name; } + /** + * Returns the overview text of this API type (which all signature/type declarations) + */ + getOverviewText() { + return this.overview; + } + /** * Creates a single markdown text document from all parts of this API type. * diff --git a/mcp-server/src/PenpotMcpServer.ts b/mcp-server/src/PenpotMcpServer.ts index 5654cf8..9ecbd1b 100644 --- a/mcp-server/src/PenpotMcpServer.ts +++ b/mcp-server/src/PenpotMcpServer.ts @@ -10,6 +10,7 @@ import { ConfigurationLoader } from "./ConfigurationLoader"; import { createLogger } from "./logger"; import { Tool } from "./Tool"; import { HighLevelOverviewTool } from "./tools/HighLevelOverviewTool"; +import { PenpotApiInfoTool } from "./tools/PenpotApiInfoTool"; export class PenpotMcpServer { private readonly logger = createLogger("PenpotMcpServer"); @@ -56,6 +57,7 @@ export class PenpotMcpServer { new PrintTextTool(this), new ExecuteCodeTool(this), new HighLevelOverviewTool(this), + new PenpotApiInfoTool(this), ]; for (const tool of toolInstances) { diff --git a/mcp-server/src/tools/ExecuteCodeTool.ts b/mcp-server/src/tools/ExecuteCodeTool.ts index b791de3..0150341 100644 --- a/mcp-server/src/tools/ExecuteCodeTool.ts +++ b/mcp-server/src/tools/ExecuteCodeTool.ts @@ -40,16 +40,19 @@ export class ExecuteCodeTool extends Tool { 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 " + + "Executes JavaScript code in the Penpot plugin context.\n" + + "IMPORTANT: Before using this tool, make sure you have read the 'Penpot High-Level Overview' and know " + + "which Penpot API functionality is necessary and how to use it.\n" + + "You have access two main objects: `penpot` (the Penpot API, of type `Penpot`) and `storage` (an object in which arbitrary " + "data can be stored, simply by adding a new attribute; stored attributes can be referenced in future calls " + "to this tool, so any intermediate results that could come in handy later should be stored in `storage` " + "instead of just a fleeting variable).\n" + "The tool call returns the value of the concluding `return` statement, if any.\n" + "Any output that you generate via the `console` object will be returned to you; so you may use this" + - "to track what your code is doing, but you should only do so only if there is an actual need!.\n" + - "In general, try a simple approach first, and only if it fails, try more complex code that involves " + - "handling different cases (in particular error cases)." + "to track what your code is doing, but you should only do so only if there is an actual need for this! " + + "IMPORTANT: Don't use logging prematurely!\n" + + "VERY IMPORTANT: In general, try a simple approach first, and only if it fails, try more complex code that involves " + + "handling different cases (in particular error cases) and that applies logging." ); } diff --git a/mcp-server/src/tools/PenpotApiInfoTool.ts b/mcp-server/src/tools/PenpotApiInfoTool.ts new file mode 100644 index 0000000..38d99b0 --- /dev/null +++ b/mcp-server/src/tools/PenpotApiInfoTool.ts @@ -0,0 +1,85 @@ +import { z } from "zod"; +import { Tool } from "../Tool"; +import "reflect-metadata"; +import type { ToolResponse } from "../ToolResponse"; +import { TextResponse } from "../ToolResponse"; +import { PenpotMcpServer } from "../PenpotMcpServer"; +import { ApiDocs } from "../ApiDocs"; + +/** + * Arguments class for the PenpotApiInfo tool with validation decorators. + */ +export class PenpotApiInfoArgs { + static schema = { + type: z.string().min(1, "Type name cannot be empty"), + member: z.string().optional(), + }; + + /** + * The API type name to retrieve information for. + */ + type!: string; + + /** + * The specific member name to retrieve (optional). + */ + member?: string; +} + +/** + * Tool for retrieving Penpot API documentation information. + * + * This tool provides access to API type documentation loaded from YAML files, + * allowing retrieval of either full type documentation or specific member details. + */ +export class PenpotApiInfoTool extends Tool { + private static readonly MAX_FULL_TEXT_CHARS = 2000; + private readonly apiDocs: ApiDocs; + + /** + * Creates a new PenpotApiInfo tool instance. + * + * @param mcpServer - The MCP server instance + */ + constructor(mcpServer: PenpotMcpServer) { + super(mcpServer, PenpotApiInfoArgs.schema); + this.apiDocs = new ApiDocs(); + } + + public getToolName(): string { + return "penpot_api_info"; + } + + public getToolDescription(): string { + return "Retrieves Penpot API documentation for types and their members"; + } + + protected async executeCore(args: PenpotApiInfoArgs): Promise { + const apiType = this.apiDocs.getType(args.type); + + if (!apiType) { + throw new Error(`API type "${args.type}" not found`); + } + + if (args.member) { + // return specific member documentation + const memberDoc = apiType.getMember(args.member); + if (!memberDoc) { + throw new Error(`Member "${args.member}" not found in type "${args.type}"`); + } + return new TextResponse(memberDoc); + } else { + // return full text or overview based on length + const fullText = apiType.getFullText(); + if (fullText.length <= PenpotApiInfoTool.MAX_FULL_TEXT_CHARS) { + return new TextResponse(fullText); + } else { + return new TextResponse( + apiType.getOverviewText() + + "\n\nMember details not provided (too long). " + + "Call this tool with a member name for more information." + ); + } + } + } +}