mirror of
https://github.com/penpot/penpot-mcp.git
synced 2026-04-25 11:18:37 +00:00
Use Streamable HTTP and SSE transports in MCP server instead of stdio
(This is necessary to ensure a single instance, which is required for a well-defined websocket port)
This commit is contained in:
parent
7faca70aa7
commit
16167e2758
@ -7,8 +7,8 @@ This document explains how to test the basic WebSocket connection between the MC
|
||||
The system consists of two main components:
|
||||
|
||||
1. **MCP Server** (`mcp-server/`):
|
||||
- Runs as a traditional MCP server (stdio transport)
|
||||
- Also runs a WebSocket server on port 8080
|
||||
- Runs as an MCP server (stdio transport)
|
||||
- Runs a WebSocket server on port 8080
|
||||
- Basic WebSocket connection handling (protocol to be defined later)
|
||||
|
||||
2. **Penpot Plugin** (`penpot-plugin/`):
|
||||
|
||||
@ -4,7 +4,8 @@ A Model Context Protocol (MCP) server that provides Penpot integration capabilit
|
||||
|
||||
## Overview
|
||||
|
||||
This MCP server implements a clean, object-oriented architecture that allows easy extension with new tools. It currently includes a demonstration tool and provides a foundation for adding Penpot-specific functionality.
|
||||
This MCP server implements a clean, object-oriented architecture that allows easy extension with new tools.
|
||||
It currently includes a demonstration tool and provides a foundation for adding Penpot-specific functionality.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@ -35,9 +36,22 @@ npm run dev
|
||||
|
||||
**Production Mode** (requires build first):
|
||||
```bash
|
||||
npm run start
|
||||
npm start
|
||||
```
|
||||
|
||||
**With Custom Port**:
|
||||
```bash
|
||||
npm start -- --port 8080
|
||||
# OR in development
|
||||
npm run dev -- --port 8080
|
||||
# OR directly
|
||||
node dist/index.js --port 8080
|
||||
```
|
||||
|
||||
**Available Options**:
|
||||
- `--port, -p <number>`: Port number for the HTTP/SSE server (default: 4401)
|
||||
- `--help, -h`: Show help message
|
||||
|
||||
## Available Commands
|
||||
|
||||
| Command | Description |
|
||||
@ -51,59 +65,60 @@ npm run start
|
||||
|
||||
## Claude Desktop Integration
|
||||
|
||||
To use this MCP server with Claude Desktop, you need to add it to Claude's configuration file.
|
||||
The MCP server now supports both modern Streamable HTTP and legacy SSE transports, providing compatibility with various MCP clients.
|
||||
|
||||
### 1. Locate Claude Desktop Config
|
||||
### 1. Start the Server
|
||||
|
||||
First, build and start the MCP server:
|
||||
|
||||
```bash
|
||||
cd mcp-server
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
By default, the server runs on port 4401 and provides:
|
||||
- **Modern Streamable HTTP endpoint**: `http://localhost:4401/mcp`
|
||||
- **Legacy SSE endpoint**: `http://localhost:4401/sse`
|
||||
|
||||
### 2. Configure Claude Desktop
|
||||
|
||||
For Claude Desktop integration, you'll need to use a proxy since Claude Desktop requires stdio transport.
|
||||
|
||||
**Option A: Using mcp-remote (Recommended)**
|
||||
|
||||
Install mcp-remote globally if you haven't already:
|
||||
```bash
|
||||
npm install -g mcp-remote
|
||||
```
|
||||
|
||||
Add this to your Claude Desktop configuration file:
|
||||
|
||||
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
**Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
|
||||
|
||||
### 2. Add Server Configuration
|
||||
|
||||
Edit the config file to include your MCP server:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"penpot": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"/path/to/your/penpot-mcp/mcp-server/dist/index.js"
|
||||
]
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "http://localhost:4401/sse", "--allow-http"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
- Replace `/path/to/your/penpot-mcp/mcp-server/dist/index.js` with the actual absolute path to your built server
|
||||
- Ensure you've run `npm run build` before adding to Claude Desktop
|
||||
- On Windows, use forward slashes `/` or double backslashes `\\` in the path
|
||||
**Option B: Direct HTTP Integration (for other MCP clients)**
|
||||
|
||||
### 3. Alternative Development Setup
|
||||
For MCP clients that support HTTP transport directly, use:
|
||||
- Modern clients: `http://localhost:4401/mcp`
|
||||
- Legacy clients: `http://localhost:4401/sse`
|
||||
|
||||
For development, you can also run the server directly with ts-node:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"penpot-mcp-server": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"--loader", "ts-node/esm",
|
||||
"/path/to/your/penpot-mcp/mcp-server/src/index.ts"
|
||||
],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Restart Claude Desktop
|
||||
### 3. Restart Claude Desktop
|
||||
|
||||
After updating the configuration file, restart Claude Desktop completely for the changes to take effect.
|
||||
|
||||
### 5. Verify Integration
|
||||
### 4. Verify Integration
|
||||
|
||||
Once Claude Desktop restarts, you should be able to use the MCP server's tools in your conversations. You can test with the included `hello_world` tool:
|
||||
|
||||
|
||||
1391
mcp-server/package-lock.json
generated
1391
mcp-server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -19,13 +19,15 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^0.4.0",
|
||||
"class-validator": "^0.14.0",
|
||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"express": "^4.18.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/ws": "^8.5.10",
|
||||
"prettier": "^3.0.0",
|
||||
|
||||
@ -1,7 +1,24 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Penpot MCP Server with HTTP and SSE Transport Support
|
||||
*
|
||||
* This server implementation supports both modern Streamable HTTP and legacy SSE transports
|
||||
* instead of the traditional stdio transport. This provides better compatibility with
|
||||
* web-based MCP clients and allows for more flexible deployment scenarios.
|
||||
*
|
||||
* Transport Endpoints:
|
||||
* - Modern Streamable HTTP: POST/GET/DELETE /mcp
|
||||
* - Legacy SSE: GET /sse and POST /messages
|
||||
* - WebSocket (for plugin communication): ws://localhost:8080
|
||||
*
|
||||
* Usage:
|
||||
* - Default port: node dist/index.js (runs on 4401)
|
||||
* - Custom port: node dist/index.js --port 8080
|
||||
* - Help: node dist/index.js --help
|
||||
*/
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { WebSocketServer, WebSocket } from "ws";
|
||||
|
||||
@ -19,22 +36,38 @@ class PenpotMcpServer {
|
||||
private readonly tools: Map<string, Tool>;
|
||||
private readonly wsServer: WebSocketServer;
|
||||
private readonly connectedClients: Set<WebSocket> = new Set();
|
||||
private app: any; // Express app
|
||||
private readonly port: number;
|
||||
|
||||
// Store transports for each session type
|
||||
private readonly transports = {
|
||||
streamable: {} as Record<string, any>, // StreamableHTTPServerTransport
|
||||
sse: {} as Record<string, any> // SSEServerTransport
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new Penpot MCP server instance.
|
||||
*
|
||||
* @param port - The port number for the HTTP/SSE server
|
||||
*/
|
||||
constructor() {
|
||||
this.server = new Server({
|
||||
name: "penpot-mcp-server",
|
||||
version: "1.0.0",
|
||||
capabilities: {
|
||||
tools: {},
|
||||
constructor(port: number = 4401) {
|
||||
this.port = port;
|
||||
this.server = new Server(
|
||||
{
|
||||
name: "penpot-mcp-server",
|
||||
version: "1.0.0",
|
||||
},
|
||||
});
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.tools = new Map<string, Tool>();
|
||||
this.wsServer = new WebSocketServer({ port: 8080 });
|
||||
this.setupHandlers();
|
||||
|
||||
this.setupMcpHandlers();
|
||||
this.setupWebSocketHandlers();
|
||||
this.registerTools();
|
||||
}
|
||||
@ -61,7 +94,7 @@ class PenpotMcpServer {
|
||||
* Configures handlers for tool listing and execution requests
|
||||
* according to the MCP specification.
|
||||
*/
|
||||
private setupHandlers(): void {
|
||||
private setupMcpHandlers(): void {
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: Array.from(this.tools.values()).map((tool) => tool.definition),
|
||||
@ -85,6 +118,124 @@ class PenpotMcpServer {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
// 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];
|
||||
}
|
||||
};
|
||||
|
||||
// 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");
|
||||
|
||||
// Create SSE transport for legacy clients
|
||||
const transport = new SSEServerTransport('/messages', res);
|
||||
this.transports.sse[transport.sessionId] = transport;
|
||||
|
||||
res.on("close", () => {
|
||||
delete this.transports.sse[transport.sessionId];
|
||||
});
|
||||
|
||||
await this.server.connect(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];
|
||||
|
||||
if (transport) {
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
} else {
|
||||
res.status(400).send('No transport found for sessionId');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up WebSocket connection handlers for plugin communication.
|
||||
*
|
||||
@ -115,18 +266,29 @@ class PenpotMcpServer {
|
||||
console.error("WebSocket server started on port 8080");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts the MCP server using stdio transport.
|
||||
* Starts the MCP server using HTTP and SSE transports.
|
||||
*
|
||||
* This method establishes the communication channel and begins
|
||||
* listening for MCP protocol messages.
|
||||
* This method establishes the HTTP server and begins listening
|
||||
* for both modern and legacy MCP protocol connections.
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error("Penpot MCP Server started successfully");
|
||||
console.error("WebSocket server is listening on ws://localhost:8080");
|
||||
// Import express as ES module and setup HTTP endpoints
|
||||
const { default: express } = await import('express');
|
||||
this.app = express();
|
||||
this.app.use(express.json());
|
||||
|
||||
this.setupHttpEndpoints();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.app.listen(this.port, () => {
|
||||
console.error(`Penpot MCP Server started successfully on port ${this.port}`);
|
||||
console.error(`Modern Streamable HTTP endpoint: http://localhost:${this.port}/mcp`);
|
||||
console.error(`Legacy SSE endpoint: http://localhost:${this.port}/sse`);
|
||||
console.error("WebSocket server is listening on ws://localhost:8080");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,7 +300,30 @@ class PenpotMcpServer {
|
||||
*/
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
const server = new PenpotMcpServer();
|
||||
// Parse command line arguments for port configuration
|
||||
const args = process.argv.slice(2);
|
||||
let port = 4401; // Default port
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--port' || args[i] === '-p') {
|
||||
if (i + 1 < args.length) {
|
||||
const portArg = parseInt(args[i + 1], 10);
|
||||
if (!isNaN(portArg) && portArg > 0 && portArg <= 65535) {
|
||||
port = portArg;
|
||||
} else {
|
||||
console.error("Invalid port number. Using default port 4401.");
|
||||
}
|
||||
}
|
||||
} else if (args[i] === '--help' || args[i] === '-h') {
|
||||
console.log("Usage: node dist/index.js [options]");
|
||||
console.log("Options:");
|
||||
console.log(" --port, -p <number> Port number for the HTTP/SSE server (default: 4401)");
|
||||
console.log(" --help, -h Show this help message");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
const server = new PenpotMcpServer(port);
|
||||
await server.start();
|
||||
|
||||
// Keep the process alive
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user