mirror of
https://github.com/penpot/penpot-mcp.git
synced 2026-04-25 11:18:37 +00:00
Add initial instructions (loaded from yml file)
This commit is contained in:
parent
6bd4567db3
commit
e0efe2b110
24
mcp-server/data/prompts.yml
Normal file
24
mcp-server/data/prompts.yml
Normal file
@ -0,0 +1,24 @@
|
||||
# Prompts configuration for Penpot MCP Server
|
||||
# This file contains various prompts and instructions that can be used by the server
|
||||
|
||||
initial_instructions: |
|
||||
You have access to Penpot tools in order to interact with Penpot design projects directly.
|
||||
As a precondition, the user must connect a Penpot project to the MCP server using the Penpot MCP Plugin.
|
||||
|
||||
A Penpot project contains shapes and more general design elements (which we shall collectively refer to as "elements").
|
||||
|
||||
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:
|
||||
* `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:
|
||||
generateStyle(shapes: Shape[], options?: {
|
||||
type?: "css";
|
||||
withPrelude?: boolean;
|
||||
includeChildren?: boolean;
|
||||
}): string;
|
||||
* Generation of HTML/SVG content corresponding to elements:
|
||||
generateMarkup(shapes: Shape[], options?: {
|
||||
type?: "html" | "svg";
|
||||
}): string;
|
||||
27
mcp-server/package-lock.json
generated
27
mcp-server/package-lock.json
generated
@ -14,6 +14,7 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"express": "^4.18.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"pino": "^9.10.0",
|
||||
"pino-pretty": "^13.1.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
@ -21,6 +22,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/ws": "^8.5.10",
|
||||
"esbuild": "^0.19.0",
|
||||
@ -843,6 +845,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/js-yaml": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
@ -971,6 +980,12 @@
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
@ -1636,6 +1651,18 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --external:@modelcontextprotocol/* --external:ws --external:express --external:class-transformer --external:class-validator --external:reflect-metadata --external:pino --external:pino-pretty",
|
||||
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --external:@modelcontextprotocol/* --external:ws --external:express --external:class-transformer --external:class-validator --external:reflect-metadata --external:pino --external:pino-pretty --external:js-yaml",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"build:full": "npm run build && npm run build:types",
|
||||
"start": "node dist/index.js",
|
||||
@ -26,6 +26,7 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"express": "^4.18.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"pino": "^9.10.0",
|
||||
"pino-pretty": "^13.1.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
@ -33,6 +34,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/ws": "^8.5.10",
|
||||
"esbuild": "^0.19.0",
|
||||
|
||||
94
mcp-server/src/ConfigurationLoader.ts
Normal file
94
mcp-server/src/ConfigurationLoader.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { readFileSync, existsSync } from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import yaml from "js-yaml";
|
||||
import { createLogger } from "./logger.js";
|
||||
|
||||
/**
|
||||
* Interface defining the structure of the prompts configuration file.
|
||||
*/
|
||||
export interface PromptsConfig {
|
||||
/** Initial instructions displayed when the server starts or connects to a client */
|
||||
initial_instructions?: string;
|
||||
[key: string]: any; // Allow for future extension with additional prompt types
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration loader for prompts and server settings.
|
||||
*
|
||||
* Handles loading and parsing of YAML configuration files,
|
||||
* providing type-safe access to configuration values with
|
||||
* appropriate fallbacks for missing files or values.
|
||||
*/
|
||||
export class ConfigurationLoader {
|
||||
private readonly logger = createLogger("ConfigurationLoader");
|
||||
private readonly baseDir: string;
|
||||
private promptsConfig: PromptsConfig | null = null;
|
||||
|
||||
/**
|
||||
* Creates a new configuration loader instance.
|
||||
*
|
||||
* @param baseDir - Base directory for resolving configuration file paths
|
||||
*/
|
||||
constructor(baseDir?: string) {
|
||||
// Default to the directory containing this module
|
||||
this.baseDir = baseDir || dirname(fileURLToPath(import.meta.url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the prompts configuration from the YAML file.
|
||||
*
|
||||
* Reads and parses the prompts.yml file, providing cached access
|
||||
* to configuration values on subsequent calls.
|
||||
*
|
||||
* @returns The parsed prompts configuration object
|
||||
*/
|
||||
public getPromptsConfig(): PromptsConfig {
|
||||
if (this.promptsConfig !== null) {
|
||||
return this.promptsConfig;
|
||||
}
|
||||
|
||||
const promptsPath = join(this.baseDir, "..", "data", "prompts.yml");
|
||||
|
||||
if (!existsSync(promptsPath)) {
|
||||
this.logger.warn(`Prompts configuration file not found at ${promptsPath}, using defaults`);
|
||||
this.promptsConfig = {};
|
||||
return this.promptsConfig;
|
||||
}
|
||||
|
||||
try {
|
||||
const fileContent = readFileSync(promptsPath, "utf8");
|
||||
const parsedConfig = yaml.load(fileContent) as PromptsConfig;
|
||||
|
||||
this.promptsConfig = parsedConfig || {};
|
||||
this.logger.info(`Loaded prompts configuration from ${promptsPath}`);
|
||||
|
||||
return this.promptsConfig;
|
||||
} catch (error) {
|
||||
this.logger.error(error, `Failed to load prompts configuration from ${promptsPath}`);
|
||||
this.promptsConfig = {};
|
||||
return this.promptsConfig;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the initial instructions for the MCP server.
|
||||
*
|
||||
* @returns The initial instructions string, or undefined if not configured
|
||||
*/
|
||||
public getInitialInstructions(): string | undefined {
|
||||
const config = this.getPromptsConfig();
|
||||
return config.initial_instructions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the configuration from disk.
|
||||
*
|
||||
* Forces a fresh read of the configuration file on the next access,
|
||||
* useful for development or when configuration files are updated at runtime.
|
||||
*/
|
||||
public reloadConfiguration(): void {
|
||||
this.promptsConfig = null;
|
||||
this.logger.info("Configuration cache cleared, will reload on next access");
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ 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";
|
||||
|
||||
/**
|
||||
@ -15,6 +16,7 @@ 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 port: number;
|
||||
public readonly pluginBridge: PluginBridge;
|
||||
@ -46,6 +48,7 @@ export class PenpotMcpServer {
|
||||
);
|
||||
|
||||
this.tools = new Map<string, ToolInterface>();
|
||||
this.configLoader = new ConfigurationLoader();
|
||||
this.pluginBridge = new PluginBridge(webSocketPort);
|
||||
|
||||
this.setupMcpHandlers();
|
||||
@ -220,6 +223,40 @@ export class PenpotMcpServer {
|
||||
* 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.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");
|
||||
@ -234,6 +271,10 @@ export class PenpotMcpServer {
|
||||
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();
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user