Add initial instructions (loaded from yml file)

This commit is contained in:
Dominik Jain 2025-09-20 15:54:37 +02:00 committed by Dominik Jain
parent 6bd4567db3
commit e0efe2b110
5 changed files with 189 additions and 1 deletions

View 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;

View File

@ -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",

View File

@ -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",

View 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");
}
}

View File

@ -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();
});
});