From 63cdbac3e89e75d5edc65fc9f5feaa3f8d44ae81 Mon Sep 17 00:00:00 2001 From: Makoq <913088741@qq.com> Date: Fri, 23 May 2025 02:44:50 +0800 Subject: [PATCH] feat: transport --- README.md | 28 ++++---- package.json | 4 +- src/index.ts | 1 + src/utils/createServer.ts | 37 ++++++++-- template/README.md.ejs | 6 ++ template/package.json.ejs | 23 +++++-- template/src/{ => server}/high.ts.ejs | 16 +---- template/src/{ => server}/low.ts.ejs | 15 +--- template/src/sse.ts.ejs | 98 +++++++++++++++++++++++++++ template/src/stdio.ts.ejs | 17 +++++ template/tsconfig.json | 15 ---- template/tsconfig.json.ejs | 20 ++++++ 12 files changed, 212 insertions(+), 68 deletions(-) rename template/src/{ => server}/high.ts.ejs (72%) rename template/src/{ => server}/low.ts.ejs (93%) create mode 100644 template/src/sse.ts.ejs create mode 100644 template/src/stdio.ts.ejs delete mode 100644 template/tsconfig.json create mode 100644 template/tsconfig.json.ejs diff --git a/README.md b/README.md index 5666dca..c0fb154 100644 --- a/README.md +++ b/README.md @@ -21,19 +21,11 @@ A command - line tool for quickly creating standardized MCP (Model Context Proto 🔧 Intelligent Configuration - Automatically generates package.json and TypeScript configurations -## Quick Start -```bash -npx create-ts-mcp-server your-mcp-server-name -``` - - - -## 🚀 Usage Guide +## 🚀 Quick Start ### Create a Project ```bash # by npx - npx create-ts-mcp-server your-server-name ``` @@ -49,6 +41,15 @@ npx create-ts-mcp-server your-server-name Low-Level use: It is a low-level interface, which is suitable for developers who need to customize the implementation details. (Use arrow keys) ❯ High-Level API Low-Level API +# config your server transport type +? What is the transport type of your server? + Standard Input/Output (stdio):The stdio transport enables communication through standard +input and output streams. This is particularly useful for local integrations and command-line + tools. + Server-Sent Events (SSE):SSE transport enables server-to-client streaming with HTTP POST +requests for client-to-server communication. +❯ Standard Input/Output (stdio) + Server-Sent Events (SSE) # success message ✔ MCP server created successfully! @@ -68,7 +69,9 @@ npm install # install dependencies npm run build # build project -npm run inspector # debug your server +npm run inspector # stdio: type debug your server + +npx tsx ./src/index.ts # sse: run your sse server ``` ### Directory Structure @@ -76,8 +79,9 @@ The typical project structure generated is as follows: ``` your-server-name/ ├── src/ -│   ├── index.ts      # Service entry file -├── test/              +│   ├── server     +│    ├── server.ts # mcp server +│   ├── index.ts     # Service entry file ├── package.json └── tsconfig.json ``` diff --git a/package.json b/package.json index 813206a..656dcb6 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "create-ts-mcp-server", - "version": "0.0.12", + "version": "0.0.13", "description": "CLI tool to create new MCP servers", "type": "module", - + "types": "build/index.d.ts", "scripts": { "build": "tsc && shx chmod +x build/index.js", "prepare": "npm run build", diff --git a/src/index.ts b/src/index.ts index bb00e65..62d9107 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ const program = new Command() .option("-n, --name ", "Name of the server") .option("-d, --description ", "Description of the server") .option("-lv, --level ", "API level of the server") + .option("-t, --transport ", "Transport type of the server") .action(createServer); program.parse(); diff --git a/src/utils/createServer.ts b/src/utils/createServer.ts index 632f973..9fdb0c4 100644 --- a/src/utils/createServer.ts +++ b/src/utils/createServer.ts @@ -42,6 +42,16 @@ export async function createServer(directory: string, options: any = {}) { ], when: !options.level, }, + { + type: "list", + name: "transport", + message: "What is the transport type of your server?\n Standard Input/Output (stdio):The stdio transport enables communication through standard input and output streams. This is particularly useful for local integrations and command-line tools.\n Server-Sent Events (SSE):SSE transport enables server-to-client streaming with HTTP POST requests for client-to-server communication.", + choices:[ + "Standard Input/Output (stdio)", + "Server-Sent Events (SSE)" + ], + when: !options.transport, + } ]; const answers = await inquirer.prompt(questions); @@ -49,6 +59,7 @@ export async function createServer(directory: string, options: any = {}) { name: options.name || answers.name, description: options.description || answers.description, level: options.level || answers.level, + transport: options.transport || answers.transport, }; const spinner = ora("Creating MCP server...").start(); @@ -68,6 +79,7 @@ export async function createServer(directory: string, options: any = {}) { ? `.${(file.slice(8) as string).replace(".ejs", "")}` : (file as string).replace(".ejs", "") ); + const targetDir = path.dirname(targetPath); // Create subdirectories if needed fs.mkdirSync(targetDir, { recursive: true }); @@ -77,7 +89,7 @@ export async function createServer(directory: string, options: any = {}) { // Use EJS to render the template content = ejs.render(content, config); - // Write processed file + // shake files if(file.includes(".ejs")){ if(config.level === "Low-Level API"&& file.includes("high.ts.ejs")){ continue; @@ -85,11 +97,24 @@ export async function createServer(directory: string, options: any = {}) { if(config.level === "High-Level API"&& file.includes("low.ts.ejs")){ continue; } + if(config.transport === "Standard Input/Output (stdio)"&& file.includes("sse.ts.ejs")){ + continue; + } + if(config.transport === "Server-Sent Events (SSE)"&& file.includes("stdio.ts.ejs")){ + continue; + } } - fs.writeFileSync(targetPath, content); - if(file.includes("high")||file.includes("low")){ - fs.renameSync(targetPath,targetPath.replace(/(high|low)/,"index")) + // Write processed file + fs.writeFileSync(targetPath, content); + // rename files + const lowOrHigh = new RegExp(/(high|low)/) + if(lowOrHigh.test(file as string)){ + fs.renameSync(targetPath,targetPath.replace(lowOrHigh,"server")) + } + const sseOrStdio = new RegExp(/(sse|stdio)/) + if(sseOrStdio.test(file as string)){ + fs.renameSync(targetPath,targetPath.replace(sseOrStdio,"index")) } } @@ -110,8 +135,8 @@ export async function createServer(directory: string, options: any = {}) { ) ); } catch (e) { - // spinner.fail(chalk.red("Failed to create MCP server")); - console.log(e); + spinner.fail(chalk.red("Failed to create MCP server")); + console.error(e); process.exit(1); } } diff --git a/template/README.md.ejs b/template/README.md.ejs index 962cb55..c6cdcec 100644 --- a/template/README.md.ejs +++ b/template/README.md.ejs @@ -44,6 +44,7 @@ npm run watch ### Debugging +#### Stdio Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script: ```bash @@ -51,3 +52,8 @@ npm run inspector ``` The Inspector will provide a URL to access debugging tools in your browser. + +#### SSE +``` +npx tsx src/index.ts +``` \ No newline at end of file diff --git a/template/package.json.ejs b/template/package.json.ejs index 6ec1fa7..27725c6 100644 --- a/template/package.json.ejs +++ b/template/package.json.ejs @@ -11,17 +11,32 @@ "build" ], "scripts": { - "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", - "prepare": "npm run build", - "watch": "tsc --watch", - "inspector": "npx @modelcontextprotocol/inspector build/index.js" + <% if (transport==='Server-Sent Events (SSE)') { %> + "dev": "ts-node-dev --respawn --transpile-only src/index.ts", + "build": "tsc", + "start": "node build/index.js" + <% } %> + <% if (transport==='Standard Input/Output (stdio)') { %> + "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", + "prepare": "npm run build", + "watch": "tsc --watch", + "inspector": "npx @modelcontextprotocol/inspector build/index.js" + <% } %> }, "dependencies": { + <% if (transport==='Server-Sent Events (SSE)') { %> + "express": "^5.0.1", + <% } %> "@modelcontextprotocol/sdk": "1.11.4", "@modelcontextprotocol/inspector": "0.10.2", "zod": "^3.24.3" }, "devDependencies": { + <% if (transport==='Server-Sent Events (SSE)') { %> + "@types/express": "^5.0.1", + "ts-node-dev":"^2.0.0", + <% } %> + "tsx": "^4.16.5", "@types/node": "^20.11.24", "typescript": "^5.3.3" } diff --git a/template/src/high.ts.ejs b/template/src/server/high.ts.ejs similarity index 72% rename from template/src/high.ts.ejs rename to template/src/server/high.ts.ejs index 2aac7d6..0ff79db 100644 --- a/template/src/high.ts.ejs +++ b/template/src/server/high.ts.ejs @@ -5,7 +5,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { z } from "zod"; // Create an MCP server -const server = new McpServer({ +export const server = new McpServer({ name: "Demo", version: "1.0.0" }); @@ -44,17 +44,3 @@ server.prompt( }] }) ); - -/** - * Start the server using stdio transport. - * This allows the server to communicate via standard input/output streams. - */ -async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); -} - -main().catch((error) => { - console.error("Server error:", error); - process.exit(1); -}); \ No newline at end of file diff --git a/template/src/low.ts.ejs b/template/src/server/low.ts.ejs similarity index 93% rename from template/src/low.ts.ejs rename to template/src/server/low.ts.ejs index 101a553..1f12425 100644 --- a/template/src/low.ts.ejs +++ b/template/src/server/low.ts.ejs @@ -38,7 +38,7 @@ const notes: { [id: string]: Note } = { * Create an MCP server with capabilities for resources (to list/read notes), * tools (to create new notes), and prompts (to summarize notes). */ -const server = new Server( + export const server = new Server( { name: "<%= name %>", version: "0.1.0", @@ -207,16 +207,3 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => { }; }); -/** - * Start the server using stdio transport. - * This allows the server to communicate via standard input/output streams. - */ -async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); -} - -main().catch((error) => { - console.error("Server error:", error); - process.exit(1); -}); diff --git a/template/src/sse.ts.ejs b/template/src/sse.ts.ejs new file mode 100644 index 0000000..5dd4639 --- /dev/null +++ b/template/src/sse.ts.ejs @@ -0,0 +1,98 @@ +import express, { Request, Response } from 'express'; +import { z } from 'zod'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { server } from './server/server.js' + +const app = express(); +app.use(express.json()); + +// Store transports by session ID +const transports: Record = {}; + +// SSE endpoint for establishing the stream +app.get('/mcp', async (req: Request, res: Response) => { + console.log('Received GET request to /sse (establishing SSE stream)'); + + try { + // Create a new SSE transport for the client + // The endpoint for POST messages is '/messages' + const transport = new SSEServerTransport('/messages', res); + + // Store the transport by session ID + const sessionId = transport.sessionId; + transports[sessionId] = transport; + + // Set up onclose handler to clean up transport when closed + transport.onclose = () => { + console.log(`SSE transport closed for session ${sessionId}`); + delete transports[sessionId]; + }; + + // Connect the transport to the MCP server + await server.connect(transport); + + console.log(`Established SSE stream with session ID: ${sessionId}`); + } catch (error) { + console.error('Error establishing SSE stream:', error); + if (!res.headersSent) { + res.status(500).send('Error establishing SSE stream'); + } + } +}); + +// Messages endpoint for receiving client JSON-RPC requests +app.post('/messages', async (req: Request, res: Response) => { + console.log('Received POST request to /messages'); + + // Extract session ID from URL query parameter + // In the SSE protocol, this is added by the client based on the endpoint event + const sessionId = req.query.sessionId as string | undefined; + + if (!sessionId) { + console.error('No session ID provided in request URL'); + res.status(400).send('Missing sessionId parameter'); + return; + } + + const transport = transports[sessionId]; + if (!transport) { + console.error(`No active transport found for session ID: ${sessionId}`); + res.status(404).send('Session not found'); + return; + } + + try { + // Handle the POST message with the transport + await transport.handlePostMessage(req, res, req.body); + } catch (error) { + console.error('Error handling request:', error); + if (!res.headersSent) { + res.status(500).send('Error handling request'); + } + } +}); + +// Start the server +const PORT = 3000; +app.listen(PORT, () => { + console.log(`Simple SSE Server (deprecated protocol version 2024-11-05) listening on port ${PORT}`); +}); + +// Handle server shutdown +process.on('SIGINT', async () => { + console.log('Shutting down server...'); + + // Close all active transports to properly clean up resources + for (const sessionId in transports) { + try { + console.log(`Closing transport for session ${sessionId}`); + await transports[sessionId].close(); + delete transports[sessionId]; + } catch (error) { + console.error(`Error closing transport for session ${sessionId}:`, error); + } + } + console.log('Server shutdown complete'); + process.exit(0); +}); \ No newline at end of file diff --git a/template/src/stdio.ts.ejs b/template/src/stdio.ts.ejs new file mode 100644 index 0000000..a2fde28 --- /dev/null +++ b/template/src/stdio.ts.ejs @@ -0,0 +1,17 @@ +#!/usr/bin/env node +import { server } from './server/server.js' +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; + +/** + * Start the server using stdio transport. + * This allows the server to communicate via standard input/output streams. + */ +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch((error) => { + console.error("Server error:", error); + process.exit(1); +}); diff --git a/template/tsconfig.json b/template/tsconfig.json deleted file mode 100644 index a14bee0..0000000 --- a/template/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "Node16", - "moduleResolution": "Node16", - "outDir": "./build", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} diff --git a/template/tsconfig.json.ejs b/template/tsconfig.json.ejs new file mode 100644 index 0000000..8d309af --- /dev/null +++ b/template/tsconfig.json.ejs @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + <% if (transport==='Server-Sent Events (SSE)') { %> + "target": "es6", + "module": "commonjs", + <% } %> + <% if (transport==='Standard Input/Output (stdio)') { %> + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + <% } %> + "strict": true, + "outDir": "./build", + "rootDir": "./src", + "esModuleInterop": true, + "skipLibCheck": true, + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +}