# Directory Structure ``` ├── .cursor │ └── rules │ └── create_mcp_server.mdc ├── .cursorrules ├── .env.example ├── .gitignore ├── cursor-kubernetes-server.sh ├── cursor-mcp-server.sh ├── cursor-pdf-server.sh ├── cursor-postgres-server.sh ├── examples │ └── sdk-readme.md ├── MCP_SERVER_DEVELOPMENT_GUIDE.md ├── package.json ├── README.md ├── run-mcp-server.sh ├── scripts │ ├── generate-cursor-commands.js │ └── run-server.js ├── src │ ├── index.ts │ ├── run-all.ts │ ├── servers │ │ ├── kubernetes-server │ │ │ ├── index.ts │ │ │ ├── kubernetes-api.ts │ │ │ └── kubernetes-server.ts │ │ ├── lease-pdf-server │ │ │ ├── index.ts │ │ │ ├── pdf-processor.ts │ │ │ ├── pdf-server.ts │ │ │ ├── README.md │ │ │ └── types.ts │ │ └── postgres-server │ │ ├── index.ts │ │ └── postgres-server.ts │ └── template │ └── mcp-server-template.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Node.js dependencies 2 | node_modules/ 3 | npm-debug.log 4 | yarn-debug.log 5 | yarn-error.log 6 | package-lock.json 7 | yarn.lock 8 | 9 | # TypeScript compiled output 10 | dist/ 11 | build/ 12 | *.tsbuildinfo 13 | 14 | # Environment variables 15 | .env 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | # Editor directories and files 22 | .idea/ 23 | .vscode/* 24 | !.vscode/extensions.json 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | *.suo 29 | *.ntvs* 30 | *.njsproj 31 | *.sln 32 | *.sw* 33 | 34 | # Logs 35 | logs/ 36 | *.log 37 | npm-debug.log* 38 | yarn-debug.log* 39 | yarn-error.log* 40 | pnpm-debug.log* 41 | 42 | # Operating System Files 43 | .DS_Store 44 | Thumbs.db 45 | ehthumbs.db 46 | Desktop.ini 47 | $RECYCLE.BIN/ 48 | ._* 49 | 50 | # Testing 51 | coverage/ 52 | .nyc_output/ 53 | 54 | # Temporary files 55 | *.tmp 56 | *.temp 57 | .cache/ 58 | tmp/ 59 | 60 | # unsupported formats 61 | *.pdf ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # Jira API Configuration 2 | JIRA_API_URL=https://your-domain.atlassian.net 3 | [email protected] 4 | JIRA_API_TOKEN=your-jira-api-token 5 | 6 | # You can generate an API token at: https://id.atlassian.com/manage-profile/security/api-tokens 7 | 8 | # GitHub API Configuration 9 | GITHUB_TOKEN=your-github-personal-access-token 10 | 11 | # You can generate a personal access token at: https://github.com/settings/tokens 12 | 13 | # PostgreSQL Configuration 14 | POSTGRES_HOST=localhost 15 | POSTGRES_PORT=5432 16 | POSTGRES_DB=your_database_name 17 | POSTGRES_USER=your_username 18 | POSTGRES_PASSWORD=your_password 19 | # POSTGRES_SSL_MODE=require # Uncomment if SSL is required 20 | # POSTGRES_MAX_CONNECTIONS=10 # Optional: limit connection pool size 21 | 22 | # Kubernetes Configuration 23 | DEFAULT_NAMESPACE=local # Default namespace to use when not specified ``` -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- ``` 1 | # MCP Servers Project Rules 2 | 3 | This project contains multiple Model Context Protocol (MCP) servers for Cursor IDE integration. 4 | 5 | ## Project Structure 6 | 7 | - `src/servers/`: Individual MCP server implementations 8 | - `src/template/`: Reusable template for creating new servers 9 | - `scripts/`: Automation scripts for setup and deployment 10 | - Shell scripts for Cursor IDE integration are auto-generated 11 | 12 | ## Core MCP Server Patterns 13 | 14 | ### 1. Server Architecture 15 | 16 | Every MCP server must: 17 | 18 | - Import `McpServer` from `@modelcontextprotocol/sdk/server/mcp.js` 19 | - Use `StdioServerTransport` for Cursor IDE communication 20 | - Export default server instance for testing/imports 21 | - Use `process.argv[1] === new URL(import.meta.url).pathname` for direct execution 22 | 23 | ### 2. Tool Definition Standards 24 | 25 | - Use Zod for parameter validation with descriptive messages 26 | - Parameter names in `snake_case` 27 | - Tool names follow patterns: `get_*`, `list_*`, `create_*`, `execute_*` 28 | - Optional namespace prefix: `mcp__` for core operations 29 | 30 | ### 3. Response Format (CRITICAL) 31 | 32 | ```typescript 33 | // Success response 34 | return { 35 | content: [ 36 | { type: "text", text: "Human-readable summary" }, 37 | { type: "text", text: JSON.stringify(data, null, 2) }, 38 | ], 39 | }; 40 | 41 | // Error response 42 | return { 43 | content: [{ type: "text", text: `Error: ${message}` }], 44 | isError: true, 45 | }; 46 | ``` 47 | 48 | Never use `type: "json"` or return raw objects. 49 | 50 | ### 4. Configuration Patterns 51 | 52 | - Use `dotenv` for environment variable loading 53 | - Support demo/fallback mode when external services unavailable 54 | - Environment variables: `{SERVICE}_{SETTING}` (uppercase) 55 | - Validate required configuration with helpful error messages 56 | 57 | ### 5. Error Handling 58 | 59 | - Wrap tool implementations in try-catch blocks 60 | - Log errors to console for debugging 61 | - Return user-friendly error messages 62 | - Use `isError: true` flag for error responses 63 | 64 | ### 6. Process Lifecycle 65 | 66 | Always include signal handlers: 67 | 68 | ```typescript 69 | process.on("SIGINT", async () => { 70 | console.log("Shutting down..."); 71 | await cleanup(); 72 | process.exit(0); 73 | }); 74 | ``` 75 | 76 | ## File Structure Requirements 77 | 78 | Each server must have: 79 | 80 | - `{server-name}.ts` - Main implementation 81 | - `types.ts` - TypeScript type definitions 82 | - `README.md` - Documentation with tools, parameters, examples 83 | - `{server-name}-api.ts` - External service logic (if applicable) 84 | 85 | ## Integration Requirements 86 | 87 | When adding new servers: 88 | 89 | 1. Add to `src/index.ts` servers array 90 | 2. Add to `src/run-all.ts` servers array 91 | 3. Add npm scripts to `package.json`: 92 | - `dev:{server}`: Development mode with ts-node 93 | - `start:{server}`: Production mode with compiled JS 94 | 4. Run `npm run setup` to generate Cursor integration scripts 95 | 96 | ## Documentation Standards 97 | 98 | ### README.md Structure 99 | 100 | - Features list 101 | - Tool documentation with parameters and returns 102 | - Configuration requirements 103 | - Usage examples 104 | - Dependencies 105 | 106 | ### Parameter Documentation 107 | 108 | - Clear descriptions with examples 109 | - Specify defaults for optional parameters 110 | - Include validation constraints 111 | - Use realistic examples in descriptions 112 | 113 | ## Development Workflow 114 | 115 | 1. Create server directory: `mkdir -p src/servers/new-server` 116 | 2. Implement core files using existing servers as reference 117 | 3. Register server in index files and package.json 118 | 4. Test with `npm run dev -- new-server` 119 | 5. Build and test production: `npm run build && npm run start:new-server` 120 | 6. Generate integration: `npm run setup` 121 | 7. Test in Cursor IDE with generated configuration 122 | 123 | ## Common Patterns by Server Type 124 | 125 | ### Database Servers (like postgres-server) 126 | 127 | - Connection pooling with cleanup 128 | - Demo mode with mock data 129 | - Parameterized queries for security 130 | - Transaction support where needed 131 | 132 | ### API Servers (like kubernetes-server) 133 | 134 | - Separate API layer in `*-api.ts` 135 | - Client configuration from environment 136 | - Resource management (connections, auth) 137 | - Namespace/scope parameter patterns 138 | 139 | ### Processing Servers (like pdf-server) 140 | 141 | - Input validation for file paths vs base64 142 | - Multiple output formats (file vs base64) 143 | - Processing result metadata 144 | - Stream handling for large files 145 | 146 | ## TypeScript Configuration 147 | 148 | Project uses ES modules with: 149 | 150 | - `"type": "module"` in package.json 151 | - `"module": "NodeNext"` in tsconfig.json 152 | - `.js` extensions in imports for compiled output 153 | - `--loader ts-node/esm` for development 154 | 155 | ## Testing and Validation 156 | 157 | Before production: 158 | 159 | - [ ] Server starts without errors 160 | - [ ] All tools accept expected parameters 161 | - [ ] Error cases return proper error responses 162 | - [ ] Demo mode works when external services unavailable 163 | - [ ] Cursor IDE integration script generated correctly 164 | - [ ] Documentation includes all tools and parameters 165 | 166 | ## Anti-Patterns to Avoid 167 | 168 | - Don't use unsupported response content types 169 | - Don't return raw objects without text wrapper 170 | - Don't skip parameter validation with Zod 171 | - Don't forget error handling with isError flag 172 | - Don't hardcode paths or configuration 173 | - Don't skip process signal handlers 174 | - Don't forget to export default server instance 175 | 176 | ## Cursor IDE Integration 177 | 178 | Generated shell scripts handle: 179 | 180 | - Working directory setup 181 | - Build process execution 182 | - Server startup with proper stdio 183 | - Absolute path resolution for configuration 184 | 185 | This project prioritizes consistency, reliability, and seamless Cursor IDE integration. 186 | ``` -------------------------------------------------------------------------------- /src/servers/lease-pdf-server/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # PDF MCP Server 2 | 3 | A Model Context Protocol (MCP) server that provides basic PDF reading and writing functionality. 4 | 5 | ## Features 6 | 7 | - **Read PDF**: Extract text content and form fields from PDF files 8 | - **Write PDF**: Create new PDFs or modify existing ones with new content 9 | 10 | ## Tools 11 | 12 | ### `read_pdf` 13 | 14 | Extracts content from PDF files. 15 | 16 | **Parameters:** 17 | 18 | - `input` (string): PDF file path or base64 encoded PDF content 19 | 20 | **Returns:** 21 | 22 | - Success status 23 | - Page count 24 | - Extracted text content 25 | - Form fields (if any) 26 | 27 | ### `write_pdf` 28 | 29 | Creates or modifies PDF files. 30 | 31 | **Parameters:** 32 | 33 | - `content` (object): 34 | - `text` (optional): Text content to add to PDF 35 | - `formFields` (optional): Form fields to update as key-value pairs 36 | - `templatePdf` (optional): Template PDF file path or base64 content to modify 37 | - `outputPath` (optional): Output file path (if not provided, returns base64) 38 | 39 | **Returns:** 40 | 41 | - Success status 42 | - Output file path (if specified) 43 | - Base64 encoded PDF (if no output path specified) 44 | 45 | ## Usage 46 | 47 | The server is designed to be used with an MCP client (like Cursor) where the AI handles the logic of what data to modify or fake. The server provides the basic PDF manipulation primitives. 48 | 49 | ### Example Workflow for Data Anonymization: 50 | 51 | 1. **Read PDF**: Use `read_pdf` to extract content from a lease contract 52 | 2. **AI Processing**: The MCP client (Cursor) uses AI to: 53 | - Identify sensitive data (names, addresses, financial amounts) 54 | - Generate realistic fake replacements 55 | - Maintain document structure and relationships 56 | 3. **Write PDF**: Use `write_pdf` to create the anonymized version 57 | 58 | ## Dependencies 59 | 60 | - `pdf-lib`: PDF creation and modification 61 | - `pdf-parse`: PDF text extraction 62 | - `@modelcontextprotocol/sdk`: MCP framework 63 | 64 | ## Running the Server 65 | 66 | ```bash 67 | # Development mode 68 | npm run dev:pdf 69 | 70 | # Production mode 71 | npm run start:pdf 72 | ``` 73 | 74 | ## Input Formats 75 | 76 | The server accepts PDF input in multiple formats: 77 | 78 | - File path: `/path/to/document.pdf` 79 | - Base64 with data URL: `data:application/pdf;base64,JVBERi0xLjQ...` 80 | - Raw base64: `JVBERi0xLjQ...` 81 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Servers for Cursor IDE 2 | 3 | This project hosts multiple Model-Context-Protocol (MCP) servers designed to work with the Cursor IDE. MCP servers allow Cursor to leverage external tools and functionalities through a standardized communication protocol. 4 | 5 | ## Table of Contents 6 | 7 | - [How To Use](#how-to-use) 8 | - [What is MCP?](#what-is-mcp) 9 | - [Project Structure](#project-structure) 10 | - [Available Servers](#available-servers) 11 | - [Server Configuration](#server-configuration) 12 | - [Available Tools](#available-tools) 13 | - [Running the Servers](#running-the-servers) 14 | - [Quick Start](#quick-start) 15 | - [Running a Server Using the Helper Script](#running-a-server-using-the-helper-script) 16 | - [Running a Single Server Manually](#running-a-single-server-manually) 17 | - [Running All Servers](#running-all-servers) 18 | - [List Available Servers](#list-available-servers) 19 | - [Testing Your MCP Server](#testing-your-mcp-server) 20 | - [Adding a New MCP Server](#adding-a-new-mcp-server) 21 | - [Understanding MCP Server Development](#understanding-mcp-server-development) 22 | - [Building the Project](#building-the-project) 23 | 24 | ## Prerequisites 25 | 26 | - Node.js (v16 or newer) 27 | - npm or yarn 28 | 29 | ## How To Use 30 | 31 | 1. Clone this repository: 32 | 33 | ``` 34 | git clone https://github.com/yourusername/mcp-servers.git 35 | cd mcp-servers 36 | ``` 37 | 38 | 2. Install dependencies and set up everything: 39 | 40 | ``` 41 | npm run setup 42 | ``` 43 | 44 | This command will: 45 | 46 | - Install all dependencies 47 | - Build the TypeScript project 48 | - Generate the necessary scripts for Cursor IDE integration 49 | - Provide instructions for setting up each server in Cursor 50 | 51 | 3. Configure Cursor IDE: 52 | 53 | - Open Cursor IDE 54 | - Go to Cursor Settings > Features > Mcp Servers 55 | - Click "Add New Mcp Server" 56 | - Enter a name for the server (e.g., "postgres") 57 | - For "Connection Type", select "command" 58 | - For "command", paste the path provided by the prepare script 59 | - Click "Save" 60 | 61 | 4. Environment Variables: 62 | 63 | - Copy the `.env.example` file to `.env` 64 | - Update the variables with your own credentials for each service 65 | 66 | 5. Use Mcp In Cursor IDE: 67 | 68 | - Open the composer 69 | - make sure you are using agent mode (claude 3.7 sonnet thinking is recommended) 70 | - submit the message you want to cursor 71 | 72 | ## What is MCP? 73 | 74 | Model Context Protocol (MCP) is an open protocol that standardizes how applications provide context to LLMs (Large Language Models). Think of MCP like a communication interface between Cursor IDE and external tools. MCP servers expose tools that can be used by Cursor IDE to enhance its capabilities. 75 | 76 | ## Project Structure 77 | 78 | ``` 79 | mcp-servers/ 80 | ├── src/ 81 | │ ├── servers/ # Individual MCP servers 82 | │ │ ├── postgres-server/ # PostgreSQL integration server 83 | │ │ │ └── postgres-server.ts 84 | │ │ ├── kubernetes-server/ # Kubernetes integration server 85 | │ │ │ └── kubernetes-server.ts 86 | │ │ ├── lease-pdf-server/ # PDF processing server 87 | │ │ │ └── pdf-server.ts 88 | │ │ └── ... (more servers) 89 | │ ├── template/ # Reusable templates 90 | │ │ └── mcp-server-template.ts 91 | │ ├── index.ts # Server runner utility 92 | │ └── run-all.ts # Script to run all servers 93 | ├── package.json 94 | └── tsconfig.json 95 | ``` 96 | 97 | ## Available Servers 98 | 99 | Currently, the following MCP servers are available: 100 | 101 | 1. **PostgreSQL Server** - Provides access to PostgreSQL databases for executing queries and retrieving schema information 102 | 2. **Kubernetes Server** - Provides access to Kubernetes clusters for managing pods, executing commands, and retrieving logs 103 | 3. **PDF Server** - Provides PDF document processing capabilities including text extraction, form field reading/writing, and PDF generation 104 | 105 | ## Server Configuration 106 | 107 | All servers are configured through environment variables. Create a `.env` file in the project root (or copy from `.env.example`) and configure the services you plan to use: 108 | 109 | ```bash 110 | # PostgreSQL Configuration (for PostgreSQL server) 111 | POSTGRES_HOST=localhost 112 | POSTGRES_PORT=5432 113 | POSTGRES_DB=your_database_name 114 | POSTGRES_USER=your_username 115 | POSTGRES_PASSWORD=your_password 116 | # POSTGRES_SSL_MODE=require # Uncomment if SSL is required 117 | # POSTGRES_MAX_CONNECTIONS=10 # Optional: limit connection pool size 118 | 119 | # Kubernetes Configuration (for Kubernetes server) 120 | KUBECONFIG=/path/to/your/kubeconfig 121 | # Alternative Kubernetes configuration: 122 | # KUBE_API_URL=https://your-kubernetes-api-server 123 | # KUBE_API_TOKEN=your-kubernetes-service-account-token 124 | 125 | # PDF Server requires no additional configuration 126 | ``` 127 | 128 | ## Available Tools 129 | 130 | ### PostgreSQL Server Tools 131 | 132 | - `mcp__get_database_info` - Retrieves information about the connected database 133 | - `mcp__list_tables` - Lists all tables in the current database schema 134 | - `mcp__get_table_structure` - Gets the column definitions for a specific table 135 | - `mcp__execute_query` - Executes a custom SQL query against the database 136 | 137 | ### Kubernetes Server Tools 138 | 139 | - `get_pods` - Retrieves pods from a specified namespace, with optional field and label selectors 140 | - `find_pods` - Finds pods matching a name pattern in a specified namespace 141 | - `kill_pod` - Deletes a pod in a specified namespace 142 | - `exec_in_pod` - Executes a command in a specified pod and container 143 | - `get_pod_logs` - Retrieves logs from a specified pod, with options for container, line count, and previous instance 144 | 145 | ### PDF Server Tools 146 | 147 | - `read_pdf` - Extracts text content and form field data from PDF documents 148 | 149 | - **Parameters:** `input` (string) - PDF file path or base64 encoded PDF content 150 | - **Returns:** JSON with success status, page count, extracted text, form fields, and metadata 151 | 152 | - `write_pdf` - Creates new PDFs or modifies existing ones with content and form field updates 153 | - **Parameters:** 154 | - `content` (object) - Content to write (text and/or form fields) 155 | - `templatePdf` (optional string) - Template PDF file path or base64 content 156 | - `outputPath` (optional string) - Output file path (returns base64 if not provided) 157 | - **Returns:** JSON with success status, output path, and base64 data (if applicable) 158 | 159 | ## Running the Servers 160 | 161 | ### Quick Start 162 | 163 | The setup command creates individual shell scripts for each server that can be used directly with Cursor IDE. 164 | After running `npm run setup`, you'll see instructions for each server configuration. 165 | 166 | ### Running a Server Using the Helper Script 167 | 168 | To run a specific server using the included helper script: 169 | 170 | ``` 171 | npm run server -- [server-name] 172 | ``` 173 | 174 | For example, to run the postgres server: 175 | 176 | ``` 177 | npm run server -- postgres 178 | ``` 179 | 180 | Or to run the PDF server: 181 | 182 | ``` 183 | npm run server -- pdf 184 | ``` 185 | 186 | This will automatically build the TypeScript code and start the server. 187 | 188 | ### Running a Single Server Manually 189 | 190 | To run a specific server manually: 191 | 192 | ``` 193 | npm run dev -- [server-name] 194 | ``` 195 | 196 | For example, to run the postgres server: 197 | 198 | ``` 199 | npm run dev -- postgres 200 | ``` 201 | 202 | Or to run the PDF server: 203 | 204 | ``` 205 | npm run dev -- pdf 206 | ``` 207 | 208 | ### Running All Servers 209 | 210 | To run all servers simultaneously: 211 | 212 | ``` 213 | npm run dev:all 214 | ``` 215 | 216 | ### List Available Servers 217 | 218 | To see a list of all available servers: 219 | 220 | ``` 221 | npm run dev -- --list 222 | ``` 223 | 224 | ## Testing Your MCP Server 225 | 226 | Before connecting to Cursor IDE, you can test your MCP server's functionality: 227 | 228 | 1. Build your TypeScript project: 229 | 230 | ``` 231 | npm run build 232 | ``` 233 | 234 | 2. Run the server: 235 | 236 | ``` 237 | npm run start:postgres 238 | ``` 239 | 240 | Or for the PDF server: 241 | 242 | ``` 243 | npm run start:pdf 244 | ``` 245 | 246 | 3. For convenience, this project includes a ready-to-use script for Cursor: 247 | 248 | ``` 249 | /path/to/mcp-servers/cursor-mcp-server.sh [server-name] 250 | ``` 251 | 252 | You can use this script path directly in your Cursor IDE configuration. If no server name is provided, it defaults to the postgres server. Examples: 253 | 254 | ``` 255 | # Run postgres server (default) 256 | /path/to/mcp-servers/cursor-mcp-server.sh 257 | 258 | # Run PDF server 259 | /path/to/mcp-servers/cursor-mcp-server.sh pdf 260 | 261 | # Run kubernetes server 262 | /path/to/mcp-servers/cursor-mcp-server.sh kubernetes 263 | ``` 264 | 265 | ## Adding a New MCP Server 266 | 267 | To add a new MCP server to this project: 268 | 269 | 1. Create a new directory for your server under `src/servers/`: 270 | 271 | ``` 272 | mkdir -p src/servers/my-new-server 273 | ``` 274 | 275 | 2. Create your server implementation: 276 | 277 | ```typescript 278 | // src/servers/my-new-server/my-new-server.ts 279 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 280 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 281 | import { z } from "zod"; 282 | 283 | // Create an MCP server 284 | const server = new McpServer({ 285 | name: "My New Server", 286 | version: "1.0.0", 287 | }); 288 | 289 | // Add your tools 290 | server.tool( 291 | "my-tool", 292 | { 293 | param1: z.string().describe("Parameter description"), 294 | param2: z.number().describe("Parameter description"), 295 | }, 296 | async ({ param1, param2 }) => { 297 | // Tool implementation 298 | return { 299 | content: [{ type: "text", text: `Result: ${param1} ${param2}` }], 300 | }; 301 | } 302 | ); 303 | 304 | // Start the server 305 | async function startServer() { 306 | const transport = new StdioServerTransport(); 307 | await server.connect(transport); 308 | console.log("My New Server started and ready to process requests"); 309 | } 310 | 311 | // Start the server if this file is run directly 312 | if (process.argv[1] === new URL(import.meta.url).pathname) { 313 | startServer(); 314 | } 315 | 316 | export default server; 317 | ``` 318 | 319 | 3. Add your server to the server list in `src/index.ts` and `src/run-all.ts`: 320 | 321 | ```typescript 322 | const servers = [ 323 | // ... existing servers 324 | { 325 | name: "my-new-server", 326 | displayName: "My New Server", 327 | path: join(__dirname, "servers/my-new-server/my-new-server.ts"), 328 | }, 329 | ]; 330 | ``` 331 | 332 | 4. Update the package.json scripts (optional): 333 | ```json 334 | "scripts": { 335 | // ... existing scripts 336 | "dev:my-new-server": "ts-node --esm src/servers/my-new-server/my-new-server.ts" 337 | } 338 | ``` 339 | 340 | ## Understanding MCP Server Development 341 | 342 | When developing an MCP server, keep in mind: 343 | 344 | 1. **Tools** are the primary way to expose functionality. Each tool should: 345 | 346 | - Have a unique name 347 | - Define parameters using Zod for validation 348 | - Return results in a standardized format 349 | 350 | 2. **Communication** happens via stdio for Cursor integration. 351 | 352 | 3. **Response Format** is critical - MCP servers must follow the exact format: 353 | 354 | - Tools should return content with `type: "text"` for text responses 355 | - Avoid using unsupported types like `type: "json"` directly 356 | - For structured data, convert to JSON string and use `type: "text"` 357 | - Example: 358 | ```typescript 359 | return { 360 | content: [ 361 | { type: "text", text: "Human-readable response" }, 362 | { type: "text", text: JSON.stringify(structuredData, null, 2) }, 363 | ], 364 | }; 365 | ``` 366 | 367 | ## Building the Project 368 | 369 | To build the project, run: 370 | 371 | ``` 372 | npm run build 373 | ``` 374 | ``` -------------------------------------------------------------------------------- /src/servers/lease-pdf-server/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export { default } from "./pdf-server.js"; 2 | ``` -------------------------------------------------------------------------------- /src/servers/kubernetes-server/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export { default } from "./kubernetes-server.js"; 2 | ``` -------------------------------------------------------------------------------- /src/servers/postgres-server/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import server from "./postgres-server.js"; 2 | 3 | // Export the server instance 4 | export default server; 5 | ``` -------------------------------------------------------------------------------- /cursor-pdf-server.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Script to run the pdf MCP server for Cursor IDE 4 | cd "$(dirname "$0")" 5 | 6 | # Ensure the TypeScript code is built 7 | npm run build 8 | 9 | # Run the server with node 10 | node dist/src/servers/lease-pdf-server/pdf-server.js 11 | ``` -------------------------------------------------------------------------------- /cursor-postgres-server.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Script to run the postgres MCP server for Cursor IDE 4 | cd "$(dirname "$0")" 5 | 6 | # Ensure the TypeScript code is built 7 | npm run build 8 | 9 | # Run the server with node 10 | node dist/src/servers/postgres-server/postgres-server.js 11 | ``` -------------------------------------------------------------------------------- /cursor-kubernetes-server.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Script to run the kubernetes MCP server for Cursor IDE 4 | cd "$(dirname "$0")" 5 | 6 | # Ensure the TypeScript code is built 7 | npm run build 8 | 9 | # Run the server with node 10 | node dist/src/servers/kubernetes-server/kubernetes-server.js 11 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "outDir": "dist", 11 | "rootDir": ".", 12 | "resolveJsonModule": true 13 | }, 14 | "include": [ 15 | "src/**/*" 16 | ], 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } ``` -------------------------------------------------------------------------------- /src/servers/lease-pdf-server/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface PdfReadResult { 2 | success: boolean; 3 | content?: { 4 | text: string; 5 | formFields?: Record<string, string>; 6 | pageCount: number; 7 | metadata?: { 8 | numrender?: number; 9 | info?: any; 10 | textLength: number; 11 | }; 12 | }; 13 | error?: string; 14 | } 15 | 16 | export interface PdfWriteRequest { 17 | content: { 18 | text?: string; 19 | formFields?: Record<string, string>; 20 | }; 21 | templatePdf?: string; // Base64 or file path to use as template 22 | } 23 | 24 | export interface PdfWriteResult { 25 | success: boolean; 26 | outputPath?: string; 27 | base64?: string; 28 | error?: string; 29 | } 30 | ``` -------------------------------------------------------------------------------- /cursor-mcp-server.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # This script is specifically for Cursor IDE to run the MCP server 4 | # It ensures proper working directory and environment 5 | # Usage: ./cursor-mcp-server.sh [server-name] 6 | # Default server: postgres 7 | 8 | # Change to the project directory 9 | cd "$(dirname "$0")" 10 | 11 | # Get server name from argument or default to postgres 12 | SERVER_NAME=${1:-postgres} 13 | 14 | # Ensure the TypeScript code is built 15 | npm run build > /dev/null 2>&1 16 | 17 | # Set the server path based on server name 18 | if [ "$SERVER_NAME" = "pdf" ]; then 19 | SERVER_PATH="dist/src/servers/lease-pdf-server/pdf-server.js" 20 | else 21 | SERVER_PATH="dist/src/servers/${SERVER_NAME}-server/${SERVER_NAME}-server.js" 22 | fi 23 | 24 | # Run the compiled JavaScript version of the server 25 | # No stdout/stderr redirection to ensure clean stdio communication 26 | node "$SERVER_PATH" ``` -------------------------------------------------------------------------------- /run-mcp-server.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Log file for debugging 4 | LOG_FILE="$HOME/mcp-server-cursor.log" 5 | 6 | # Get server name from argument or default to postgres 7 | SERVER_NAME=${1:-postgres} 8 | 9 | # Log start time and command 10 | echo "=== Starting MCP Server $(date) ===" > "$LOG_FILE" 11 | echo "Working directory: $(pwd)" >> "$LOG_FILE" 12 | echo "Command: $0 $@" >> "$LOG_FILE" 13 | echo "Server: $SERVER_NAME" >> "$LOG_FILE" 14 | echo "Environment:" >> "$LOG_FILE" 15 | env >> "$LOG_FILE" 16 | 17 | # Change to the project directory 18 | cd "$(dirname "$0")" 19 | echo "Changed to directory: $(pwd)" >> "$LOG_FILE" 20 | 21 | # Set the server path based on server name 22 | if [ "$SERVER_NAME" = "pdf" ]; then 23 | SERVER_PATH="src/servers/lease-pdf-server/pdf-server.ts" 24 | else 25 | SERVER_PATH="src/servers/${SERVER_NAME}-server/${SERVER_NAME}-server.ts" 26 | fi 27 | 28 | # Run the server with ts-node loader 29 | echo "Running server..." >> "$LOG_FILE" 30 | NODE_OPTIONS="--loader ts-node/esm" node "$SERVER_PATH" 2>> "$LOG_FILE" 31 | 32 | # Log exit code 33 | echo "Server exited with code: $?" >> "$LOG_FILE" ``` -------------------------------------------------------------------------------- /src/template/mcp-server-template.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | 4 | export interface McpServerConfig { 5 | name: string; 6 | version: string; 7 | } 8 | 9 | export class McpServerTemplate { 10 | protected server: McpServer; 11 | private transport: StdioServerTransport | null = null; 12 | private config: McpServerConfig; 13 | 14 | constructor(config: McpServerConfig) { 15 | this.config = config; 16 | this.server = new McpServer({ 17 | name: config.name, 18 | version: config.version, 19 | }); 20 | } 21 | 22 | /** 23 | * Initialize the server by defining tools and their handlers 24 | */ 25 | async initialize(): Promise<void> { 26 | // This method should be overridden by subclasses to define tools 27 | throw new Error( 28 | "Method not implemented: Subclasses should implement this method" 29 | ); 30 | } 31 | 32 | /** 33 | * Start the server by connecting to the stdio transport 34 | */ 35 | async start(): Promise<void> { 36 | try { 37 | // Register tools before starting 38 | await this.initialize(); 39 | 40 | // Set up stdio transport 41 | this.transport = new StdioServerTransport(); 42 | 43 | // Connect to the transport 44 | await this.server.connect(this.transport); 45 | 46 | console.log( 47 | `${this.config.name} v${this.config.version} MCP server started` 48 | ); 49 | } catch (error) { 50 | console.error("Error starting MCP server:", error); 51 | process.exit(1); 52 | } 53 | } 54 | 55 | /** 56 | * Stop the server 57 | */ 58 | async stop(): Promise<void> { 59 | if (this.transport) { 60 | await this.transport.close(); 61 | console.log(`${this.config.name} MCP server stopped`); 62 | } 63 | } 64 | } 65 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "mcp-servers", 3 | "version": "1.0.0", 4 | "description": "Multiple MCP servers for Cursor IDE", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "node dist/src/index.js", 10 | "start:all": "node dist/src/run-all.js", 11 | "start:postgres": "node dist/src/servers/postgres-server/postgres-server.js", 12 | "start:kubernetes": "node dist/src/servers/kubernetes-server/kubernetes-server.js", 13 | "start:pdf": "node dist/src/servers/lease-pdf-server/pdf-server.js", 14 | "setup": "npm install && npm run build && find . -name \"*.sh\" -exec chmod +x {} \\; && node scripts/generate-cursor-commands.js", 15 | "server": "node scripts/run-server.js", 16 | "dev": "NODE_OPTIONS=\"--loader ts-node/esm\" node --experimental-specifier-resolution=node src/index.ts", 17 | "dev:all": "NODE_OPTIONS=\"--loader ts-node/esm\" node --experimental-specifier-resolution=node src/run-all.ts", 18 | "dev:postgres": "NODE_OPTIONS=\"--loader ts-node/esm\" node src/servers/postgres-server/postgres-server.ts", 19 | "dev:kubernetes": "NODE_OPTIONS=\"--loader ts-node/esm\" node src/servers/kubernetes-server/kubernetes-server.ts", 20 | "dev:pdf": "NODE_OPTIONS=\"--loader ts-node/esm\" node src/servers/lease-pdf-server/pdf-server.ts" 21 | }, 22 | "keywords": [ 23 | "mcp", 24 | "cursor", 25 | "ide", 26 | "server" 27 | ], 28 | "author": "", 29 | "license": "ISC", 30 | "dependencies": { 31 | "@kubernetes/client-node": "^0.20.0", 32 | "@modelcontextprotocol/sdk": "^1.7.0", 33 | "@types/node": "^22.13.11", 34 | "dotenv": "^16.4.7", 35 | "node-fetch": "^3.3.2", 36 | "pdf-lib": "^1.17.1", 37 | "pdf-parse": "^1.1.1", 38 | "pg": "^8.14.1", 39 | "typescript": "^5.8.2", 40 | "zod": "^3.24.2" 41 | }, 42 | "devDependencies": { 43 | "@types/pdf-parse": "^1.1.5", 44 | "@types/pg": "^8.11.11", 45 | "ts-node": "^10.9.2" 46 | } 47 | } ``` -------------------------------------------------------------------------------- /src/run-all.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { spawn } from "child_process"; 2 | import { fileURLToPath } from "url"; 3 | import { dirname, join } from "path"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | // Define available servers 9 | const servers = [ 10 | { 11 | name: "PostgreSQL Server", 12 | path: join(__dirname, "servers/postgres-server/postgres-server.ts"), 13 | }, 14 | { 15 | name: "Kubernetes Server", 16 | path: join(__dirname, "servers/kubernetes-server/kubernetes-server.ts"), 17 | }, 18 | { 19 | name: "PDF Server", 20 | path: join(__dirname, "servers/lease-pdf-server/pdf-server.ts"), 21 | }, 22 | // Add more servers here as they are created 23 | ]; 24 | 25 | // Function to start a server 26 | function startServer(serverInfo: { name: string; path: string }) { 27 | console.log(`Starting ${serverInfo.name}...`); 28 | 29 | // Use ts-node to run the TypeScript file directly 30 | const serverProcess = spawn("npx", ["ts-node", "--esm", serverInfo.path], { 31 | stdio: "pipe", // Capture output 32 | detached: false, 33 | }); 34 | 35 | // Set up logging for the server 36 | serverProcess.stdout.on("data", (data) => { 37 | console.log(`[${serverInfo.name}] ${data.toString().trim()}`); 38 | }); 39 | 40 | serverProcess.stderr.on("data", (data) => { 41 | console.error(`[${serverInfo.name}] ERROR: ${data.toString().trim()}`); 42 | }); 43 | 44 | // Handle server exit 45 | serverProcess.on("exit", (code) => { 46 | console.log(`[${serverInfo.name}] exited with code ${code}`); 47 | }); 48 | 49 | return serverProcess; 50 | } 51 | 52 | // Function to start all servers 53 | function startAllServers() { 54 | console.log("Starting all MCP servers..."); 55 | 56 | const processes = servers.map(startServer); 57 | 58 | // Handle script termination 59 | process.on("SIGINT", () => { 60 | console.log("Shutting down all servers..."); 61 | processes.forEach((p) => { 62 | if (!p.killed) { 63 | p.kill("SIGINT"); 64 | } 65 | }); 66 | }); 67 | 68 | process.on("SIGTERM", () => { 69 | console.log("Shutting down all servers..."); 70 | processes.forEach((p) => { 71 | if (!p.killed) { 72 | p.kill("SIGTERM"); 73 | } 74 | }); 75 | }); 76 | 77 | console.log("All servers started. Press Ctrl+C to stop."); 78 | } 79 | 80 | // Start all servers if this script is run directly 81 | if (process.argv[1] === fileURLToPath(import.meta.url)) { 82 | startAllServers(); 83 | } 84 | ``` -------------------------------------------------------------------------------- /scripts/run-server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | import { spawn } from "child_process"; 4 | import { fileURLToPath } from "url"; 5 | import path from "path"; 6 | import fs from "fs"; 7 | 8 | // Get project root 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const projectRoot = path.resolve(__dirname, ".."); 12 | 13 | // Function to get available servers from package.json 14 | function getAvailableServers() { 15 | const packageJsonPath = path.join(projectRoot, "package.json"); 16 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); 17 | 18 | return Object.keys(packageJson.scripts) 19 | .filter((script) => script.startsWith("start:") && script !== "start:all") 20 | .map((script) => script.replace("start:", "")); 21 | } 22 | 23 | // Main function 24 | function main() { 25 | const args = process.argv.slice(2); 26 | const availableServers = getAvailableServers(); 27 | 28 | // Display help if no arguments or help is requested 29 | if (args.length === 0 || args[0] === "--help" || args[0] === "-h") { 30 | console.log("Usage: node scripts/run-server.js [server-name]"); 31 | console.log(""); 32 | console.log("Available servers:"); 33 | availableServers.forEach((server) => { 34 | console.log(` - ${server}`); 35 | }); 36 | console.log(""); 37 | console.log("Example: node scripts/run-server.js postgres"); 38 | return; 39 | } 40 | 41 | const serverName = args[0]; 42 | 43 | // Check if server exists 44 | if (!availableServers.includes(serverName)) { 45 | console.error(`Error: Server "${serverName}" not found.`); 46 | console.log("Available servers:"); 47 | availableServers.forEach((server) => { 48 | console.log(` - ${server}`); 49 | }); 50 | process.exit(1); 51 | } 52 | 53 | // Run the server 54 | console.log(`Starting ${serverName} server...`); 55 | 56 | // First build the TypeScript code 57 | const buildProcess = spawn("npm", ["run", "build"], { 58 | stdio: "inherit", 59 | cwd: projectRoot, 60 | }); 61 | 62 | buildProcess.on("close", (code) => { 63 | if (code !== 0) { 64 | console.error(`Error: Build failed with code ${code}`); 65 | process.exit(code); 66 | } 67 | 68 | // Then run the server 69 | const serverProcess = spawn("npm", ["run", `start:${serverName}`], { 70 | stdio: "inherit", 71 | cwd: projectRoot, 72 | }); 73 | 74 | serverProcess.on("close", (code) => { 75 | console.log(`Server exited with code ${code}`); 76 | process.exit(code); 77 | }); 78 | 79 | // Handle process termination 80 | process.on("SIGINT", () => { 81 | console.log("Received SIGINT. Shutting down server..."); 82 | serverProcess.kill("SIGINT"); 83 | }); 84 | 85 | process.on("SIGTERM", () => { 86 | console.log("Received SIGTERM. Shutting down server..."); 87 | serverProcess.kill("SIGTERM"); 88 | }); 89 | }); 90 | } 91 | 92 | // Run the main function 93 | main(); 94 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { spawn } from "child_process"; 2 | import { fileURLToPath } from "url"; 3 | import { dirname, join } from "path"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | // Define available servers 9 | const servers = [ 10 | { 11 | name: "postgres", 12 | displayName: "PostgreSQL Server", 13 | path: join(__dirname, "servers/postgres-server/postgres-server.ts"), 14 | }, 15 | { 16 | name: "kubernetes", 17 | displayName: "Kubernetes Server", 18 | path: join(__dirname, "servers/kubernetes-server/kubernetes-server.ts"), 19 | }, 20 | { 21 | name: "pdf", 22 | displayName: "PDF Server", 23 | path: join(__dirname, "servers/lease-pdf-server/pdf-server.ts"), 24 | }, 25 | // Add more servers here as they are created 26 | ]; 27 | 28 | // Function to start a server by name 29 | function startServerByName(serverName: string) { 30 | const server = servers.find((s) => s.name === serverName); 31 | 32 | if (!server) { 33 | console.error( 34 | `Server "${serverName}" not found. Available servers: ${servers 35 | .map((s) => s.name) 36 | .join(", ")}` 37 | ); 38 | process.exit(1); 39 | } 40 | 41 | console.log(`Starting ${server.displayName}...`); 42 | 43 | // Use ts-node to run the TypeScript file directly 44 | const serverProcess = spawn("npx", ["ts-node", "--esm", server.path], { 45 | stdio: "inherit", // Inherit stdio from parent process 46 | detached: false, 47 | }); 48 | 49 | // Handle server exit 50 | serverProcess.on("exit", (code) => { 51 | console.log(`${server.displayName} exited with code ${code}`); 52 | process.exit(code || 0); 53 | }); 54 | 55 | // Forward termination signals 56 | process.on("SIGINT", () => serverProcess.kill("SIGINT")); 57 | process.on("SIGTERM", () => serverProcess.kill("SIGTERM")); 58 | } 59 | 60 | // Function to list all available servers 61 | function listServers() { 62 | console.log("Available MCP servers:"); 63 | servers.forEach((server) => { 64 | console.log(`- ${server.name}: ${server.displayName}`); 65 | }); 66 | } 67 | 68 | // Main function to parse arguments and run the appropriate server 69 | function main() { 70 | const args = process.argv.slice(2); 71 | 72 | if (args.length === 0 || args[0] === "--help" || args[0] === "-h") { 73 | console.log("Usage: npm run dev -- [server-name]"); 74 | console.log(" npm run dev -- --list"); 75 | console.log("\nOptions:"); 76 | console.log(" --list, -l List all available servers"); 77 | console.log(" --help, -h Show this help message"); 78 | console.log("\nTo run all servers: npm run dev:all"); 79 | listServers(); 80 | return; 81 | } 82 | 83 | if (args[0] === "--list" || args[0] === "-l") { 84 | listServers(); 85 | return; 86 | } 87 | 88 | startServerByName(args[0]); 89 | } 90 | 91 | // Run the main function if this script is executed directly 92 | if (process.argv[1] === fileURLToPath(import.meta.url)) { 93 | main(); 94 | } 95 | ``` -------------------------------------------------------------------------------- /scripts/generate-cursor-commands.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | import fs from "fs"; 4 | import path from "path"; 5 | import { fileURLToPath } from "url"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const projectRoot = path.resolve(__dirname, ".."); 10 | 11 | // Create individual server script for each MCP server 12 | function createServerScripts() { 13 | // Read package.json to get the list of servers 14 | const packageJson = JSON.parse( 15 | fs.readFileSync(path.join(projectRoot, "package.json"), "utf8") 16 | ); 17 | 18 | // Extract start scripts for servers 19 | const serverScripts = Object.keys(packageJson.scripts) 20 | .filter((script) => script.startsWith("start:") && script !== "start:all") 21 | .map((script) => { 22 | const serverName = script.replace("start:", ""); 23 | const scriptPath = packageJson.scripts[script]; 24 | const jsPath = scriptPath.replace("node ", ""); 25 | 26 | return { 27 | serverName, 28 | scriptPath, 29 | jsPath, 30 | }; 31 | }); 32 | 33 | // Create script for each server 34 | serverScripts.forEach((server) => { 35 | const scriptName = `cursor-${server.serverName}-server.sh`; 36 | const scriptPath = path.join(projectRoot, scriptName); 37 | 38 | const scriptContent = `#!/bin/bash 39 | 40 | # Script to run the ${server.serverName} MCP server for Cursor IDE 41 | cd "$(dirname "$0")" 42 | 43 | # Ensure the TypeScript code is built 44 | npm run build 45 | 46 | # Run the server with node 47 | node ${server.jsPath} 48 | `; 49 | 50 | fs.writeFileSync(scriptPath, scriptContent); 51 | console.log(`Created ${scriptName}`); 52 | }); 53 | 54 | // Make all scripts executable 55 | serverScripts.forEach((server) => { 56 | const scriptName = `cursor-${server.serverName}-server.sh`; 57 | const scriptPath = path.join(projectRoot, scriptName); 58 | fs.chmodSync(scriptPath, "755"); 59 | }); 60 | 61 | // Generate workspace path (used for absolute paths in instructions) 62 | const workspacePath = process.cwd(); 63 | 64 | // Print instructions 65 | console.log("\n===== CURSOR IDE SETUP INSTRUCTIONS =====\n"); 66 | console.log("To use these MCP servers in Cursor IDE:"); 67 | console.log("1. Open Cursor IDE"); 68 | console.log("2. Go to Cursor Settings > Features > Model Context Protocol"); 69 | console.log("3. Edit the mcp.json file or paste this configuration:"); 70 | console.log("4. Add the following to your mcp.json file under mcpServers:\n"); 71 | 72 | console.log("```json"); 73 | console.log('"mcpServers": {'); 74 | 75 | serverScripts.forEach((server, index) => { 76 | const scriptName = `cursor-${server.serverName}-server.sh`; 77 | const absoluteScriptPath = path.join(workspacePath, scriptName); 78 | const isLast = index === serverScripts.length - 1; 79 | 80 | console.log(` "${server.serverName}": {`); 81 | console.log(` "command": "${absoluteScriptPath}",`); 82 | console.log(` "args": []`); 83 | console.log(` }${isLast ? "" : ","}`); 84 | }); 85 | 86 | console.log("}"); 87 | console.log("```\n"); 88 | 89 | console.log( 90 | "After adding these configurations, restart Cursor IDE and the MCP servers" 91 | ); 92 | console.log("will be available for use in the composer.\n"); 93 | } 94 | 95 | // Run the function 96 | createServerScripts(); 97 | ``` -------------------------------------------------------------------------------- /src/servers/lease-pdf-server/pdf-server.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { z } from "zod"; 4 | import { PdfProcessor } from "./pdf-processor.js"; 5 | 6 | // Create an MCP server 7 | const server = new McpServer({ 8 | name: "PDF Server", 9 | version: "1.0.0", 10 | }); 11 | 12 | const pdfProcessor = new PdfProcessor(); 13 | 14 | // Tool 1: Read PDF - extracts text and form field data 15 | server.tool( 16 | "read_pdf", 17 | { 18 | input: z.string().describe("PDF file path or base64 encoded PDF content"), 19 | }, 20 | async ({ input }) => { 21 | try { 22 | const result = await pdfProcessor.readPdf(input); 23 | 24 | if (!result.success) { 25 | return { 26 | content: [ 27 | { 28 | type: "text" as const, 29 | text: `Error reading PDF: ${result.error}`, 30 | }, 31 | ], 32 | isError: true, 33 | }; 34 | } 35 | 36 | const response = { 37 | success: true, 38 | pageCount: result.content?.pageCount || 0, 39 | text: result.content?.text || "", 40 | formFields: result.content?.formFields || {}, 41 | hasFormFields: 42 | !!result.content?.formFields && 43 | Object.keys(result.content.formFields).length > 0, 44 | }; 45 | 46 | return { 47 | content: [ 48 | { type: "text" as const, text: "PDF read successfully" }, 49 | { type: "text" as const, text: JSON.stringify(response, null, 2) }, 50 | ], 51 | }; 52 | } catch (error) { 53 | console.error("Error in read_pdf handler:", error); 54 | return { 55 | content: [ 56 | { 57 | type: "text" as const, 58 | text: `Error reading PDF: ${ 59 | error instanceof Error ? error.message : String(error) 60 | }`, 61 | }, 62 | ], 63 | isError: true, 64 | }; 65 | } 66 | } 67 | ); 68 | 69 | // Tool 2: Write PDF - creates or modifies PDF with new content 70 | server.tool( 71 | "write_pdf", 72 | { 73 | content: z 74 | .object({ 75 | text: z.string().optional().describe("Text content to add to PDF"), 76 | formFields: z 77 | .record(z.string()) 78 | .optional() 79 | .describe("Form fields to update as key-value pairs"), 80 | }) 81 | .describe("Content to write to PDF"), 82 | templatePdf: z 83 | .string() 84 | .optional() 85 | .describe("Template PDF file path or base64 content to modify"), 86 | outputPath: z 87 | .string() 88 | .optional() 89 | .describe("Output file path (if not provided, returns base64)"), 90 | }, 91 | async ({ content, templatePdf, outputPath }) => { 92 | try { 93 | const writeRequest = { 94 | content: content, 95 | templatePdf: templatePdf, 96 | }; 97 | 98 | const result = await pdfProcessor.writePdf(writeRequest, outputPath); 99 | 100 | if (!result.success) { 101 | return { 102 | content: [ 103 | { 104 | type: "text" as const, 105 | text: `Error writing PDF: ${result.error}`, 106 | }, 107 | ], 108 | isError: true, 109 | }; 110 | } 111 | 112 | const response = { 113 | success: true, 114 | outputPath: result.outputPath, 115 | hasBase64: !!result.base64, 116 | base64Length: result.base64 ? result.base64.length : 0, 117 | }; 118 | 119 | const responseContent = [ 120 | { type: "text" as const, text: "PDF written successfully" }, 121 | { type: "text" as const, text: JSON.stringify(response, null, 2) }, 122 | ]; 123 | 124 | // Include base64 data if no output path was specified 125 | if (result.base64 && !outputPath) { 126 | responseContent.push({ 127 | type: "text" as const, 128 | text: `Base64 PDF Data:\ndata:application/pdf;base64,${result.base64}`, 129 | }); 130 | } 131 | 132 | return { 133 | content: responseContent, 134 | }; 135 | } catch (error) { 136 | console.error("Error in write_pdf handler:", error); 137 | return { 138 | content: [ 139 | { 140 | type: "text" as const, 141 | text: `Error writing PDF: ${ 142 | error instanceof Error ? error.message : String(error) 143 | }`, 144 | }, 145 | ], 146 | isError: true, 147 | }; 148 | } 149 | } 150 | ); 151 | 152 | // Handle termination signals 153 | process.on("SIGINT", () => { 154 | console.log("Received SIGINT signal. Shutting down..."); 155 | process.exit(0); 156 | }); 157 | 158 | process.on("SIGTERM", () => { 159 | console.log("Received SIGTERM signal. Shutting down..."); 160 | process.exit(0); 161 | }); 162 | 163 | // Start the server 164 | async function startServer() { 165 | try { 166 | const transport = new StdioServerTransport(); 167 | await server.connect(transport); 168 | console.log("PDF Server started and ready to process requests"); 169 | } catch (error) { 170 | console.error("Failed to start server:", error); 171 | process.exit(1); 172 | } 173 | } 174 | 175 | // Start the server if this file is run directly 176 | if (process.argv[1] === new URL(import.meta.url).pathname) { 177 | startServer(); 178 | } 179 | 180 | export default server; 181 | ``` -------------------------------------------------------------------------------- /src/servers/kubernetes-server/kubernetes-api.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as k8s from "@kubernetes/client-node"; 2 | import { spawn } from "child_process"; 3 | import { config } from "dotenv"; 4 | 5 | // Load environment variables 6 | config(); 7 | 8 | // Set up Kubernetes client 9 | const kc = new k8s.KubeConfig(); 10 | kc.loadFromDefault(); // This will load from ~/.kube/config by default 11 | 12 | // Get the API clients 13 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api); 14 | const exec = new k8s.Exec(kc); 15 | 16 | // Type definitions 17 | export interface PodList { 18 | kind: string | undefined; 19 | apiVersion: string | undefined; 20 | metadata: any; 21 | items: any[]; // Changed from Pod[] to any[] to handle V1Pod[] 22 | } 23 | 24 | export interface Pod { 25 | metadata: { 26 | name: string; 27 | namespace: string; 28 | [key: string]: any; 29 | }; 30 | spec: any; 31 | status: { 32 | phase: string; 33 | conditions: any[]; 34 | containerStatuses: any[]; 35 | [key: string]: any; 36 | }; 37 | [key: string]: any; 38 | } 39 | 40 | export interface ExecResult { 41 | stdout: string; 42 | stderr: string; 43 | exitCode: number | null; 44 | } 45 | 46 | /** 47 | * Get pods in a namespace with optional label and field selectors 48 | * @param namespace Kubernetes namespace 49 | * @param labelSelector Label selector to filter pods 50 | * @param fieldSelector Field selector to filter pods 51 | */ 52 | export async function getPods( 53 | namespace: string = "local", 54 | labelSelector?: string, 55 | fieldSelector?: string 56 | ): Promise<PodList> { 57 | try { 58 | const response = await k8sApi.listNamespacedPod( 59 | namespace, 60 | undefined, // pretty 61 | undefined, // allowWatchBookmarks 62 | undefined, // _continue 63 | undefined, // fieldSelector 64 | fieldSelector, 65 | undefined, // includeUninitialized 66 | labelSelector, 67 | undefined, // limit 68 | undefined, // resourceVersion 69 | undefined, // resourceVersionMatch 70 | undefined, // timeoutSeconds 71 | undefined // watch 72 | ); 73 | 74 | return { 75 | kind: response.body.kind, 76 | apiVersion: response.body.apiVersion, 77 | metadata: response.body.metadata, 78 | items: response.body.items, 79 | }; 80 | } catch (error) { 81 | console.error(`Error getting pods in namespace ${namespace}:`, error); 82 | throw error; 83 | } 84 | } 85 | 86 | /** 87 | * Find pods by name pattern 88 | * @param namePattern Pod name pattern 89 | * @param namespace Kubernetes namespace 90 | */ 91 | export async function findPodsByName( 92 | namePattern: string, 93 | namespace: string = "local" 94 | ): Promise<PodList> { 95 | try { 96 | // Get all pods in the namespace 97 | const allPods = await getPods(namespace); 98 | 99 | // Filter pods by name pattern 100 | // Convert * wildcard to JavaScript RegExp 101 | const regexPattern = new RegExp( 102 | "^" + namePattern.replace(/\*/g, ".*") + "$" 103 | ); 104 | 105 | const filteredItems = allPods.items.filter((pod) => 106 | regexPattern.test(pod.metadata?.name || "") 107 | ); 108 | 109 | return { 110 | kind: allPods.kind, 111 | apiVersion: allPods.apiVersion, 112 | metadata: allPods.metadata, 113 | items: filteredItems, 114 | }; 115 | } catch (error) { 116 | console.error( 117 | `Error finding pods by name pattern ${namePattern} in namespace ${namespace}:`, 118 | error 119 | ); 120 | throw error; 121 | } 122 | } 123 | 124 | /** 125 | * Delete a pod 126 | * @param podName Pod name 127 | * @param namespace Kubernetes namespace 128 | * @param gracePeriodSeconds Grace period in seconds before force deletion 129 | */ 130 | export async function deletePod( 131 | podName: string, 132 | namespace: string = "local", 133 | gracePeriodSeconds?: number 134 | ): Promise<any> { 135 | try { 136 | // Create delete options 137 | const deleteOptions = new k8s.V1DeleteOptions(); 138 | if (gracePeriodSeconds !== undefined) { 139 | deleteOptions.gracePeriodSeconds = gracePeriodSeconds; 140 | } 141 | 142 | const response = await k8sApi.deleteNamespacedPod( 143 | podName, 144 | namespace, 145 | undefined, // pretty 146 | undefined, // dryRun 147 | gracePeriodSeconds, // gracePeriodSeconds 148 | undefined, // orphanDependents 149 | undefined, // propagationPolicy 150 | deleteOptions 151 | ); 152 | 153 | return response.body; 154 | } catch (error) { 155 | console.error( 156 | `Error deleting pod ${podName} in namespace ${namespace}:`, 157 | error 158 | ); 159 | throw error; 160 | } 161 | } 162 | 163 | /** 164 | * Execute a command in a pod 165 | * @param podName Pod name 166 | * @param command Command to execute (will be split by space) 167 | * @param namespace Kubernetes namespace 168 | * @param containerName Container name (if pod has multiple containers) 169 | */ 170 | export async function execCommandInPod( 171 | podName: string, 172 | command: string, 173 | namespace: string = "local", 174 | containerName?: string 175 | ): Promise<ExecResult> { 176 | try { 177 | // First, get the pod to find container name if not provided 178 | if (!containerName) { 179 | const pod = await k8sApi.readNamespacedPod(podName, namespace); 180 | // Use first container as default if it exists 181 | if ( 182 | pod.body.spec && 183 | pod.body.spec.containers && 184 | pod.body.spec.containers.length > 0 185 | ) { 186 | containerName = pod.body.spec.containers[0].name; 187 | } else { 188 | throw new Error(`No containers found in pod ${podName}`); 189 | } 190 | } 191 | 192 | // We'll use the kubectl exec command for reliability 193 | // Parse the command into array of arguments 194 | const cmd = command.split(/\s+/); 195 | 196 | return new Promise((resolve, reject) => { 197 | // Execute kubectl command 198 | const kubectl = spawn("kubectl", [ 199 | "exec", 200 | "-n", 201 | namespace, 202 | podName, 203 | ...(containerName ? ["-c", containerName] : []), 204 | "--", 205 | ...cmd, 206 | ]); 207 | 208 | let stdout = ""; 209 | let stderr = ""; 210 | 211 | kubectl.stdout.on("data", (data) => { 212 | stdout += data.toString(); 213 | }); 214 | 215 | kubectl.stderr.on("data", (data) => { 216 | stderr += data.toString(); 217 | }); 218 | 219 | kubectl.on("close", (code) => { 220 | resolve({ 221 | stdout, 222 | stderr, 223 | exitCode: code, 224 | }); 225 | }); 226 | 227 | kubectl.on("error", (err) => { 228 | reject(err); 229 | }); 230 | }); 231 | } catch (error) { 232 | console.error( 233 | `Error executing command in pod ${podName} in namespace ${namespace}:`, 234 | error 235 | ); 236 | throw error; 237 | } 238 | } 239 | 240 | /** 241 | * Get logs from a pod 242 | * @param podName Pod name 243 | * @param namespace Kubernetes namespace 244 | * @param containerName Container name (if pod has multiple containers) 245 | * @param tailLines Number of lines to fetch from the end 246 | * @param previous Get logs from previous terminated container instance 247 | */ 248 | export async function getPodLogs( 249 | podName: string, 250 | namespace: string = "local", 251 | containerName?: string, 252 | tailLines?: number, 253 | previous?: boolean 254 | ): Promise<string> { 255 | try { 256 | // First, get the pod to find container name if not provided 257 | if (!containerName) { 258 | const pod = await k8sApi.readNamespacedPod(podName, namespace); 259 | // Use first container as default if it exists 260 | if ( 261 | pod.body.spec && 262 | pod.body.spec.containers && 263 | pod.body.spec.containers.length > 0 264 | ) { 265 | containerName = pod.body.spec.containers[0].name; 266 | } else { 267 | throw new Error(`No containers found in pod ${podName}`); 268 | } 269 | } 270 | 271 | const response = await k8sApi.readNamespacedPodLog( 272 | podName, 273 | namespace, 274 | containerName, 275 | undefined, // follow 276 | undefined, // insecureSkipTLSVerifyBackend 277 | undefined, // pretty 278 | previous ? "true" : undefined, // previous - convert boolean to string 279 | undefined, // sinceSeconds 280 | undefined, // sinceTime 281 | tailLines, // tailLines 282 | undefined // timestamps 283 | ); 284 | 285 | return response.body; 286 | } catch (error) { 287 | console.error( 288 | `Error getting logs from pod ${podName} in namespace ${namespace}:`, 289 | error 290 | ); 291 | throw error; 292 | } 293 | } 294 | ``` -------------------------------------------------------------------------------- /src/servers/kubernetes-server/kubernetes-server.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { z } from "zod"; 4 | import { config } from "dotenv"; 5 | import * as K8sAPI from "./kubernetes-api.js"; 6 | 7 | // Load environment variables 8 | config(); 9 | 10 | // Create an MCP server 11 | const server = new McpServer({ 12 | name: "Kubernetes Server", 13 | version: "1.0.0", 14 | }); 15 | 16 | // Get pods tool 17 | server.tool( 18 | "get_pods", 19 | { 20 | namespace: z 21 | .string() 22 | .optional() 23 | .describe("Kubernetes namespace (default: local)"), 24 | label_selector: z 25 | .string() 26 | .optional() 27 | .describe("Label selector to filter pods (e.g. 'app=myapp')"), 28 | field_selector: z 29 | .string() 30 | .optional() 31 | .describe("Field selector to filter pods (e.g. 'status.phase=Running')"), 32 | }, 33 | async ({ namespace = "local", label_selector, field_selector }) => { 34 | try { 35 | const pods = await K8sAPI.getPods( 36 | namespace, 37 | label_selector, 38 | field_selector 39 | ); 40 | return { 41 | content: [ 42 | { 43 | type: "text", 44 | text: `Found ${pods.items.length} pods in namespace '${namespace}'.`, 45 | }, 46 | { 47 | type: "text", 48 | text: JSON.stringify(pods, null, 2), 49 | }, 50 | ], 51 | }; 52 | } catch (error) { 53 | console.error("Error in get_pods handler:", error); 54 | return { 55 | content: [ 56 | { 57 | type: "text", 58 | text: `Error fetching pods: ${ 59 | error instanceof Error ? error.message : String(error) 60 | }`, 61 | }, 62 | ], 63 | isError: true, 64 | }; 65 | } 66 | } 67 | ); 68 | 69 | // Find pods tool 70 | server.tool( 71 | "find_pods", 72 | { 73 | name_pattern: z 74 | .string() 75 | .describe( 76 | "Pod name pattern to search for (supports wildcards, e.g. 'nginx*')" 77 | ), 78 | namespace: z 79 | .string() 80 | .optional() 81 | .describe("Kubernetes namespace (default: local)"), 82 | }, 83 | async ({ name_pattern, namespace = "local" }) => { 84 | try { 85 | const pods = await K8sAPI.findPodsByName(name_pattern, namespace); 86 | return { 87 | content: [ 88 | { 89 | type: "text", 90 | text: `Found ${pods.items.length} pods matching '${name_pattern}' in namespace '${namespace}'.`, 91 | }, 92 | { 93 | type: "text", 94 | text: JSON.stringify(pods, null, 2), 95 | }, 96 | ], 97 | }; 98 | } catch (error) { 99 | console.error("Error in find_pods handler:", error); 100 | return { 101 | content: [ 102 | { 103 | type: "text", 104 | text: `Error finding pods: ${ 105 | error instanceof Error ? error.message : String(error) 106 | }`, 107 | }, 108 | ], 109 | isError: true, 110 | }; 111 | } 112 | } 113 | ); 114 | 115 | // Kill pod tool 116 | server.tool( 117 | "kill_pod", 118 | { 119 | pod_name: z.string().describe("Name of the pod to delete"), 120 | namespace: z 121 | .string() 122 | .optional() 123 | .describe("Kubernetes namespace (default: local)"), 124 | grace_period_seconds: z 125 | .number() 126 | .optional() 127 | .describe("Grace period in seconds before force deletion"), 128 | }, 129 | async ({ pod_name, namespace = "local", grace_period_seconds }) => { 130 | try { 131 | const result = await K8sAPI.deletePod( 132 | pod_name, 133 | namespace, 134 | grace_period_seconds 135 | ); 136 | return { 137 | content: [ 138 | { 139 | type: "text", 140 | text: `Successfully deleted pod '${pod_name}' in namespace '${namespace}'.`, 141 | }, 142 | { 143 | type: "text", 144 | text: JSON.stringify(result, null, 2), 145 | }, 146 | ], 147 | }; 148 | } catch (error) { 149 | console.error("Error in kill_pod handler:", error); 150 | return { 151 | content: [ 152 | { 153 | type: "text", 154 | text: `Error deleting pod: ${ 155 | error instanceof Error ? error.message : String(error) 156 | }`, 157 | }, 158 | ], 159 | isError: true, 160 | }; 161 | } 162 | } 163 | ); 164 | 165 | // Execute command in pod tool 166 | server.tool( 167 | "exec_in_pod", 168 | { 169 | pod_name: z.string().describe("Name of the pod"), 170 | command: z.string().describe("Command to execute (e.g. 'ls -la')"), 171 | container_name: z 172 | .string() 173 | .optional() 174 | .describe("Container name (if pod has multiple containers)"), 175 | namespace: z 176 | .string() 177 | .optional() 178 | .describe("Kubernetes namespace (default: local)"), 179 | }, 180 | async ({ pod_name, command, container_name, namespace = "local" }) => { 181 | try { 182 | const result = await K8sAPI.execCommandInPod( 183 | pod_name, 184 | command, 185 | namespace, 186 | container_name 187 | ); 188 | return { 189 | content: [ 190 | { 191 | type: "text", 192 | text: `Command execution results from pod '${pod_name}' in namespace '${namespace}':`, 193 | }, 194 | { 195 | type: "text", 196 | text: result.stdout, 197 | }, 198 | { 199 | type: "text", 200 | text: result.stderr ? `Error output: ${result.stderr}` : "", 201 | }, 202 | ], 203 | }; 204 | } catch (error) { 205 | console.error("Error in exec_in_pod handler:", error); 206 | return { 207 | content: [ 208 | { 209 | type: "text", 210 | text: `Error executing command in pod: ${ 211 | error instanceof Error ? error.message : String(error) 212 | }`, 213 | }, 214 | ], 215 | isError: true, 216 | }; 217 | } 218 | } 219 | ); 220 | 221 | // Get pod logs tool 222 | server.tool( 223 | "get_pod_logs", 224 | { 225 | pod_name: z.string().describe("Name of the pod"), 226 | container_name: z 227 | .string() 228 | .optional() 229 | .describe("Container name (if pod has multiple containers)"), 230 | namespace: z 231 | .string() 232 | .optional() 233 | .describe("Kubernetes namespace (default: local)"), 234 | tail_lines: z 235 | .number() 236 | .optional() 237 | .describe("Number of lines to fetch from the end"), 238 | previous: z 239 | .boolean() 240 | .optional() 241 | .describe("Get logs from previous terminated container instance"), 242 | }, 243 | async ({ 244 | pod_name, 245 | container_name, 246 | namespace = "local", 247 | tail_lines, 248 | previous, 249 | }) => { 250 | try { 251 | const logs = await K8sAPI.getPodLogs( 252 | pod_name, 253 | namespace, 254 | container_name, 255 | tail_lines, 256 | previous 257 | ); 258 | return { 259 | content: [ 260 | { 261 | type: "text", 262 | text: `Logs from pod '${pod_name}' in namespace '${namespace}':`, 263 | }, 264 | { 265 | type: "text", 266 | text: logs, 267 | }, 268 | ], 269 | }; 270 | } catch (error) { 271 | console.error("Error in get_pod_logs handler:", error); 272 | return { 273 | content: [ 274 | { 275 | type: "text", 276 | text: `Error getting pod logs: ${ 277 | error instanceof Error ? error.message : String(error) 278 | }`, 279 | }, 280 | ], 281 | isError: true, 282 | }; 283 | } 284 | } 285 | ); 286 | 287 | // Handle termination signals 288 | process.on("SIGINT", () => { 289 | console.log("Received SIGINT signal. Shutting down..."); 290 | process.exit(0); 291 | }); 292 | 293 | process.on("SIGTERM", () => { 294 | console.log("Received SIGTERM signal. Shutting down..."); 295 | process.exit(0); 296 | }); 297 | 298 | // Start the server 299 | async function startServer() { 300 | const transport = new StdioServerTransport(); 301 | await server.connect(transport); 302 | console.log("Kubernetes Server started and ready to process requests"); 303 | } 304 | 305 | // Start the server if this file is run directly 306 | if (import.meta.url === new URL(import.meta.url).href) { 307 | startServer(); 308 | } 309 | 310 | export default server; 311 | ``` -------------------------------------------------------------------------------- /src/servers/lease-pdf-server/pdf-processor.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { PDFDocument, PDFForm, PDFTextField, StandardFonts } from "pdf-lib"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | // Fix for pdf-parse ENOENT error - import directly from lib to avoid debug code 5 | // @ts-ignore - pdf-parse lib doesn't have proper types 6 | import * as pdfParse from "pdf-parse/lib/pdf-parse.js"; 7 | import { PdfReadResult, PdfWriteRequest, PdfWriteResult } from "./types.js"; 8 | 9 | export class PdfProcessor { 10 | /** 11 | * Custom page render function for better text extraction 12 | */ 13 | private renderPage(pageData: any) { 14 | // Enhanced render options for better text extraction 15 | const renderOptions = { 16 | // Normalize whitespace to standard spaces 17 | normalizeWhitespace: true, 18 | // Don't combine text items to preserve structure 19 | disableCombineTextItems: true, 20 | }; 21 | 22 | return pageData.getTextContent(renderOptions).then((textContent: any) => { 23 | let lastY: number | undefined; 24 | let text = ""; 25 | 26 | // Process each text item 27 | for (const item of textContent.items) { 28 | // Add line breaks when Y position changes significantly 29 | if (lastY !== undefined && Math.abs(lastY - item.transform[5]) > 1) { 30 | text += "\n"; 31 | } 32 | 33 | // Add the text content 34 | text += item.str; 35 | 36 | // Add space if this item doesn't end the line and next item is on same line 37 | if (item.hasEOL === false) { 38 | text += " "; 39 | } 40 | 41 | lastY = item.transform[5]; 42 | } 43 | 44 | return text; 45 | }); 46 | } 47 | 48 | /** 49 | * Read PDF content - extracts text and form fields with enhanced options 50 | */ 51 | async readPdf(input: string): Promise<PdfReadResult> { 52 | try { 53 | let pdfBuffer: Buffer; 54 | 55 | // Handle input as file path or base64 56 | if (input.startsWith("data:application/pdf;base64,")) { 57 | const base64Data = input.split(",")[1]; 58 | pdfBuffer = Buffer.from(base64Data, "base64"); 59 | } else if (input.length > 500) { 60 | // Assume it's base64 without data URL prefix 61 | pdfBuffer = Buffer.from(input, "base64"); 62 | } else { 63 | // Assume it's a file path 64 | if (!fs.existsSync(input)) { 65 | return { success: false, error: `File not found: ${input}` }; 66 | } 67 | pdfBuffer = fs.readFileSync(input); 68 | } 69 | 70 | // Enhanced parsing options for better text extraction 71 | const options = { 72 | // Parse all pages (0 = all pages) 73 | max: 0, 74 | // Use our custom page render function 75 | pagerender: this.renderPage, 76 | // Use a valid PDF.js version for better compatibility 77 | version: "v1.10.100" as const, 78 | }; 79 | 80 | console.log(`Starting PDF parsing with enhanced options...`); 81 | 82 | // Extract text content with enhanced options - use the fixed import 83 | const pdfData = await (pdfParse as any).default(pdfBuffer, options); 84 | 85 | console.log(`PDF parsing completed: 86 | - Total pages: ${pdfData.numpages} 87 | - Pages rendered: ${pdfData.numrender || "N/A"} 88 | - Text length: ${pdfData.text.length} characters 89 | - First 200 chars: ${pdfData.text.substring(0, 200)}...`); 90 | 91 | // Extract form fields using pdf-lib 92 | const pdfDoc = await PDFDocument.load(pdfBuffer); 93 | const form = pdfDoc.getForm(); 94 | const formFields: Record<string, string> = {}; 95 | 96 | try { 97 | const fields = form.getFields(); 98 | console.log(`Found ${fields.length} form fields`); 99 | 100 | fields.forEach((field: any) => { 101 | const fieldName = field.getName(); 102 | if (field instanceof PDFTextField) { 103 | formFields[fieldName] = field.getText() || ""; 104 | } else { 105 | // Handle other field types as needed 106 | formFields[fieldName] = field.toString(); 107 | } 108 | }); 109 | } catch (error) { 110 | // PDF might not have form fields, that's okay 111 | console.log("No form fields found or error reading form fields"); 112 | } 113 | 114 | return { 115 | success: true, 116 | content: { 117 | text: pdfData.text, 118 | formFields: 119 | Object.keys(formFields).length > 0 ? formFields : undefined, 120 | pageCount: pdfData.numpages, 121 | // Add additional metadata for debugging 122 | metadata: { 123 | numrender: pdfData.numrender, 124 | info: pdfData.info, 125 | textLength: pdfData.text.length, 126 | }, 127 | }, 128 | }; 129 | } catch (error) { 130 | console.error("PDF parsing error:", error); 131 | return { 132 | success: false, 133 | error: `Error reading PDF: ${ 134 | error instanceof Error ? error.message : String(error) 135 | }`, 136 | }; 137 | } 138 | } 139 | 140 | /** 141 | * Write/Create PDF - creates new PDF or modifies existing one 142 | */ 143 | async writePdf( 144 | request: PdfWriteRequest, 145 | outputPath?: string 146 | ): Promise<PdfWriteResult> { 147 | try { 148 | let pdfDoc: PDFDocument; 149 | 150 | // If template PDF provided, load it; otherwise create new document 151 | if (request.templatePdf) { 152 | let templateBuffer: Buffer; 153 | 154 | if (request.templatePdf.startsWith("data:application/pdf;base64,")) { 155 | const base64Data = request.templatePdf.split(",")[1]; 156 | templateBuffer = Buffer.from(base64Data, "base64"); 157 | } else if (request.templatePdf.length > 500) { 158 | templateBuffer = Buffer.from(request.templatePdf, "base64"); 159 | } else { 160 | if (!fs.existsSync(request.templatePdf)) { 161 | return { 162 | success: false, 163 | error: `Template file not found: ${request.templatePdf}`, 164 | }; 165 | } 166 | templateBuffer = fs.readFileSync(request.templatePdf); 167 | } 168 | 169 | pdfDoc = await PDFDocument.load(templateBuffer); 170 | } else { 171 | pdfDoc = await PDFDocument.create(); 172 | } 173 | 174 | // Update form fields if provided 175 | if (request.content.formFields) { 176 | try { 177 | const form = pdfDoc.getForm(); 178 | 179 | Object.entries(request.content.formFields).forEach( 180 | ([fieldName, value]) => { 181 | try { 182 | const field = form.getTextField(fieldName); 183 | field.setText(value); 184 | } catch (error) { 185 | console.log(`Could not update field ${fieldName}: ${error}`); 186 | } 187 | } 188 | ); 189 | } catch (error) { 190 | console.log("Error updating form fields:", error); 191 | } 192 | } 193 | 194 | // Add text content if provided and no template (multi-page text PDF) 195 | if (request.content.text && !request.templatePdf) { 196 | await this.addMultiPageText(pdfDoc, request.content.text); 197 | } 198 | 199 | // Generate PDF bytes 200 | const pdfBytes = await pdfDoc.save(); 201 | 202 | // Save to file if output path provided 203 | if (outputPath) { 204 | const dir = path.dirname(outputPath); 205 | if (!fs.existsSync(dir)) { 206 | fs.mkdirSync(dir, { recursive: true }); 207 | } 208 | fs.writeFileSync(outputPath, pdfBytes); 209 | 210 | return { 211 | success: true, 212 | outputPath: outputPath, 213 | }; 214 | } else { 215 | // Return as base64 216 | const base64 = Buffer.from(pdfBytes).toString("base64"); 217 | return { 218 | success: true, 219 | base64: base64, 220 | }; 221 | } 222 | } catch (error) { 223 | return { 224 | success: false, 225 | error: `Error writing PDF: ${ 226 | error instanceof Error ? error.message : String(error) 227 | }`, 228 | }; 229 | } 230 | } 231 | 232 | /** 233 | * Add multi-page text to a PDF document with proper pagination 234 | */ 235 | private async addMultiPageText( 236 | pdfDoc: PDFDocument, 237 | text: string 238 | ): Promise<void> { 239 | const font = await pdfDoc.embedFont(StandardFonts.Helvetica); 240 | const fontSize = 12; 241 | const lineHeight = fontSize * 1.5; // 1.5 line spacing for readability 242 | const pageMargin = 50; 243 | 244 | // Standard US Letter page dimensions 245 | const pageWidth = 612; 246 | const pageHeight = 792; 247 | const textWidth = pageWidth - pageMargin * 2; 248 | const textHeight = pageHeight - pageMargin * 2; 249 | const linesPerPage = Math.floor(textHeight / lineHeight); 250 | 251 | console.log(`Multi-page text setup: 252 | - Font size: ${fontSize} 253 | - Line height: ${lineHeight} 254 | - Lines per page: ${linesPerPage} 255 | - Text width: ${textWidth} 256 | - Text height: ${textHeight} 257 | - Page margins: ${pageMargin}`); 258 | 259 | // Split text into lines, preserving existing line breaks 260 | const lines = text.split("\n"); 261 | console.log(`Total lines to process: ${lines.length}`); 262 | 263 | let currentPage = pdfDoc.addPage([pageWidth, pageHeight]); 264 | let currentLineOnPage = 0; 265 | let pageCount = 1; 266 | 267 | for (let i = 0; i < lines.length; i++) { 268 | const line = lines[i]; 269 | 270 | // Check if we need a new page BEFORE processing the line 271 | if (currentLineOnPage >= linesPerPage) { 272 | console.log( 273 | `Creating new page at line ${i}, current line on page: ${currentLineOnPage}` 274 | ); 275 | currentPage = pdfDoc.addPage([pageWidth, pageHeight]); 276 | currentLineOnPage = 0; 277 | pageCount++; 278 | console.log(`Created page ${pageCount}`); 279 | } 280 | 281 | // Calculate Y position (top to bottom) 282 | const yPosition = 283 | pageHeight - pageMargin - currentLineOnPage * lineHeight; 284 | 285 | // Draw the line (simplified - no word wrapping for now) 286 | const textToDraw = line || " "; // Handle empty lines 287 | console.log( 288 | `Drawing line ${i + 1} on page ${pageCount}, line ${ 289 | currentLineOnPage + 1 290 | }: "${textToDraw.substring(0, 50)}..."` 291 | ); 292 | 293 | currentPage.drawText(textToDraw, { 294 | x: pageMargin, 295 | y: yPosition, 296 | font: font, 297 | size: fontSize, 298 | maxWidth: textWidth, 299 | }); 300 | 301 | currentLineOnPage++; 302 | } 303 | 304 | console.log( 305 | `Multi-page text complete: ${pageCount} pages created, ${lines.length} lines processed` 306 | ); 307 | } 308 | } 309 | ``` -------------------------------------------------------------------------------- /examples/sdk-readme.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP TypeScript SDK   2 | 3 | ## Table of Contents 4 | 5 | - [Overview](#overview) 6 | - [Installation](#installation) 7 | - [Quickstart](#quickstart) 8 | - [What is MCP?](#what-is-mcp) 9 | - [Core Concepts](#core-concepts) 10 | - [Server](#server) 11 | - [Resources](#resources) 12 | - [Tools](#tools) 13 | - [Prompts](#prompts) 14 | - [Running Your Server](#running-your-server) 15 | - [stdio](#stdio) 16 | - [HTTP with SSE](#http-with-sse) 17 | - [Testing and Debugging](#testing-and-debugging) 18 | - [Examples](#examples) 19 | - [Echo Server](#echo-server) 20 | - [SQLite Explorer](#sqlite-explorer) 21 | - [Advanced Usage](#advanced-usage) 22 | - [Low-Level Server](#low-level-server) 23 | - [Writing MCP Clients](#writing-mcp-clients) 24 | - [Server Capabilities](#server-capabilities) 25 | 26 | ## Overview 27 | 28 | The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This TypeScript SDK implements the full MCP specification, making it easy to: 29 | 30 | - Build MCP clients that can connect to any MCP server 31 | - Create MCP servers that expose resources, prompts and tools 32 | - Use standard transports like stdio and SSE 33 | - Handle all MCP protocol messages and lifecycle events 34 | 35 | ## Installation 36 | 37 | ```bash 38 | npm install @modelcontextprotocol/sdk 39 | ``` 40 | 41 | ## Quick Start 42 | 43 | Let's create a simple MCP server that exposes a calculator tool and some data: 44 | 45 | ```typescript 46 | import { 47 | McpServer, 48 | ResourceTemplate, 49 | } from "@modelcontextprotocol/sdk/server/mcp.js"; 50 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 51 | import { z } from "zod"; 52 | 53 | // Create an MCP server 54 | const server = new McpServer({ 55 | name: "Demo", 56 | version: "1.0.0", 57 | }); 58 | 59 | // Add a multiplication tool 60 | server.tool("multiply", { a: z.number(), b: z.number() }, async ({ a, b }) => ({ 61 | content: [{ type: "text", text: String(a * b) }], 62 | })); 63 | 64 | // Add a dynamic greeting resource 65 | server.resource( 66 | "greeting", 67 | new ResourceTemplate("greeting://{name}", { list: undefined }), 68 | async (uri, { name }) => ({ 69 | contents: [ 70 | { 71 | uri: uri.href, 72 | text: `Hello, ${name}!`, 73 | }, 74 | ], 75 | }) 76 | ); 77 | 78 | // Start receiving messages on stdin and sending messages on stdout 79 | const transport = new StdioServerTransport(); 80 | await server.connect(transport); 81 | ``` 82 | 83 | ## What is MCP? 84 | 85 | The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can: 86 | 87 | - Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context) 88 | - Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect) 89 | - Define interaction patterns through **Prompts** (reusable templates for LLM interactions) 90 | - And more! 91 | 92 | ## Core Concepts 93 | 94 | ### Server 95 | 96 | The McpServer is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing: 97 | 98 | ```typescript 99 | const server = new McpServer({ 100 | name: "My App", 101 | version: "1.0.0", 102 | }); 103 | ``` 104 | 105 | ### Resources 106 | 107 | Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects: 108 | 109 | ```typescript 110 | // Static resource 111 | server.resource("config", "config://app", async (uri) => ({ 112 | contents: [ 113 | { 114 | uri: uri.href, 115 | text: "App configuration here", 116 | }, 117 | ], 118 | })); 119 | 120 | // Dynamic resource with parameters 121 | server.resource( 122 | "user-profile", 123 | new ResourceTemplate("users://{userId}/profile", { list: undefined }), 124 | async (uri, { userId }) => ({ 125 | contents: [ 126 | { 127 | uri: uri.href, 128 | text: `Profile data for user ${userId}`, 129 | }, 130 | ], 131 | }) 132 | ); 133 | ``` 134 | 135 | ### Tools 136 | 137 | Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects: 138 | 139 | ```typescript 140 | // Simple tool with parameters 141 | server.tool( 142 | "calculate-bmi", 143 | { 144 | weightKg: z.number(), 145 | heightM: z.number(), 146 | }, 147 | async ({ weightKg, heightM }) => ({ 148 | content: [ 149 | { 150 | type: "text", 151 | text: String(weightKg / (heightM * heightM)), 152 | }, 153 | ], 154 | }) 155 | ); 156 | 157 | // Async tool with external API call 158 | server.tool("fetch-weather", { city: z.string() }, async ({ city }) => { 159 | const response = await fetch(`https://api.weather.com/${city}`); 160 | const data = await response.text(); 161 | return { 162 | content: [{ type: "text", text: data }], 163 | }; 164 | }); 165 | ``` 166 | 167 | ### Prompts 168 | 169 | Prompts are reusable templates that help LLMs interact with your server effectively: 170 | 171 | ```typescript 172 | server.prompt("review-code", { code: z.string() }, ({ code }) => ({ 173 | messages: [ 174 | { 175 | role: "user", 176 | content: { 177 | type: "text", 178 | text: `Please review this code:\n\n${code}`, 179 | }, 180 | }, 181 | ], 182 | })); 183 | ``` 184 | 185 | ## Running Your Server 186 | 187 | MCP servers in TypeScript need to be connected to a transport to communicate with clients. How you start the server depends on the choice of transport: 188 | 189 | ### stdio 190 | 191 | For command-line tools and direct integrations: 192 | 193 | ```typescript 194 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 195 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 196 | 197 | const server = new McpServer({ 198 | name: "example-server", 199 | version: "1.0.0", 200 | }); 201 | 202 | // ... set up server resources, tools, and prompts ... 203 | 204 | const transport = new StdioServerTransport(); 205 | await server.connect(transport); 206 | ``` 207 | 208 | ### HTTP with SSE 209 | 210 | For remote servers, start a web server with a Server-Sent Events (SSE) endpoint, and a separate endpoint for the client to send its messages to: 211 | 212 | ```typescript 213 | import express from "express"; 214 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 215 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; 216 | 217 | const server = new McpServer({ 218 | name: "example-server", 219 | version: "1.0.0", 220 | }); 221 | 222 | // ... set up server resources, tools, and prompts ... 223 | 224 | const app = express(); 225 | 226 | app.get("/sse", async (req, res) => { 227 | const transport = new SSEServerTransport("/messages", res); 228 | await server.connect(transport); 229 | }); 230 | 231 | app.post("/messages", async (req, res) => { 232 | // Note: to support multiple simultaneous connections, these messages will 233 | // need to be routed to a specific matching transport. (This logic isn't 234 | // implemented here, for simplicity.) 235 | await transport.handlePostMessage(req, res); 236 | }); 237 | 238 | app.listen(3001); 239 | ``` 240 | 241 | ### Testing and Debugging 242 | 243 | To test your server, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). See its README for more information. 244 | 245 | ## Examples 246 | 247 | ### Echo Server 248 | 249 | A simple server demonstrating resources, tools, and prompts: 250 | 251 | ```typescript 252 | import { 253 | McpServer, 254 | ResourceTemplate, 255 | } from "@modelcontextprotocol/sdk/server/mcp.js"; 256 | import { z } from "zod"; 257 | 258 | const server = new McpServer({ 259 | name: "Echo", 260 | version: "1.0.0", 261 | }); 262 | 263 | server.resource( 264 | "echo", 265 | new ResourceTemplate("echo://{message}", { list: undefined }), 266 | async (uri, { message }) => ({ 267 | contents: [ 268 | { 269 | uri: uri.href, 270 | text: `Resource echo: ${message}`, 271 | }, 272 | ], 273 | }) 274 | ); 275 | 276 | server.tool("echo", { message: z.string() }, async ({ message }) => ({ 277 | content: [{ type: "text", text: `Tool echo: ${message}` }], 278 | })); 279 | 280 | server.prompt("echo", { message: z.string() }, ({ message }) => ({ 281 | messages: [ 282 | { 283 | role: "user", 284 | content: { 285 | type: "text", 286 | text: `Please process this message: ${message}`, 287 | }, 288 | }, 289 | ], 290 | })); 291 | ``` 292 | 293 | ### SQLite Explorer 294 | 295 | A more complex example showing database integration: 296 | 297 | ```typescript 298 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 299 | import sqlite3 from "sqlite3"; 300 | import { promisify } from "util"; 301 | import { z } from "zod"; 302 | 303 | const server = new McpServer({ 304 | name: "SQLite Explorer", 305 | version: "1.0.0", 306 | }); 307 | 308 | // Helper to create DB connection 309 | const getDb = () => { 310 | const db = new sqlite3.Database("database.db"); 311 | return { 312 | all: promisify<string, any[]>(db.all.bind(db)), 313 | close: promisify(db.close.bind(db)), 314 | }; 315 | }; 316 | 317 | server.resource("schema", "schema://main", async (uri) => { 318 | const db = getDb(); 319 | try { 320 | const tables = await db.all( 321 | "SELECT sql FROM sqlite_master WHERE type='table'" 322 | ); 323 | return { 324 | contents: [ 325 | { 326 | uri: uri.href, 327 | text: tables.map((t: { sql: string }) => t.sql).join("\n"), 328 | }, 329 | ], 330 | }; 331 | } finally { 332 | await db.close(); 333 | } 334 | }); 335 | 336 | server.tool("query", { sql: z.string() }, async ({ sql }) => { 337 | const db = getDb(); 338 | try { 339 | const results = await db.all(sql); 340 | return { 341 | content: [ 342 | { 343 | type: "text", 344 | text: JSON.stringify(results, null, 2), 345 | }, 346 | ], 347 | }; 348 | } catch (err: unknown) { 349 | const error = err as Error; 350 | return { 351 | content: [ 352 | { 353 | type: "text", 354 | text: `Error: ${error.message}`, 355 | }, 356 | ], 357 | isError: true, 358 | }; 359 | } finally { 360 | await db.close(); 361 | } 362 | }); 363 | ``` 364 | 365 | ## Advanced Usage 366 | 367 | ### Low-Level Server 368 | 369 | For more control, you can use the low-level Server class directly: 370 | 371 | ```typescript 372 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 373 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 374 | import { 375 | ListPromptsRequestSchema, 376 | GetPromptRequestSchema, 377 | } from "@modelcontextprotocol/sdk/types.js"; 378 | 379 | const server = new Server( 380 | { 381 | name: "example-server", 382 | version: "1.0.0", 383 | }, 384 | { 385 | capabilities: { 386 | prompts: {}, 387 | }, 388 | } 389 | ); 390 | 391 | server.setRequestHandler(ListPromptsRequestSchema, async () => { 392 | return { 393 | prompts: [ 394 | { 395 | name: "example-prompt", 396 | description: "An example prompt template", 397 | arguments: [ 398 | { 399 | name: "arg1", 400 | description: "Example argument", 401 | required: true, 402 | }, 403 | ], 404 | }, 405 | ], 406 | }; 407 | }); 408 | 409 | server.setRequestHandler(GetPromptRequestSchema, async (request) => { 410 | if (request.params.name !== "example-prompt") { 411 | throw new Error("Unknown prompt"); 412 | } 413 | return { 414 | description: "Example prompt", 415 | messages: [ 416 | { 417 | role: "user", 418 | content: { 419 | type: "text", 420 | text: "Example prompt text", 421 | }, 422 | }, 423 | ], 424 | }; 425 | }); 426 | 427 | const transport = new StdioServerTransport(); 428 | await server.connect(transport); 429 | ``` 430 | 431 | ### Writing MCP Clients 432 | 433 | The SDK provides a high-level client interface: 434 | 435 | ```typescript 436 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 437 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 438 | 439 | const transport = new StdioClientTransport({ 440 | command: "node", 441 | args: ["server.js"], 442 | }); 443 | 444 | const client = new Client( 445 | { 446 | name: "example-client", 447 | version: "1.0.0", 448 | }, 449 | { 450 | capabilities: { 451 | prompts: {}, 452 | resources: {}, 453 | tools: {}, 454 | }, 455 | } 456 | ); 457 | 458 | await client.connect(transport); 459 | 460 | // List prompts 461 | const prompts = await client.listPrompts(); 462 | 463 | // Get a prompt 464 | const prompt = await client.getPrompt("example-prompt", { 465 | arg1: "value", 466 | }); 467 | 468 | // List resources 469 | const resources = await client.listResources(); 470 | 471 | // Read a resource 472 | const resource = await client.readResource("file:///example.txt"); 473 | 474 | // Call a tool 475 | const result = await client.callTool({ 476 | name: "example-tool", 477 | arguments: { 478 | arg1: "value", 479 | }, 480 | }); 481 | ``` 482 | 483 | ## Documentation 484 | 485 | - [Model Context Protocol documentation](https://modelcontextprotocol.io) 486 | - [MCP Specification](https://spec.modelcontextprotocol.io) 487 | - [Example Servers](https://github.com/modelcontextprotocol/servers) 488 | 489 | ## Contributing 490 | 491 | Issues and pull requests are welcome on GitHub at https://github.com/modelcontextprotocol/typescript-sdk. 492 | 493 | ## License 494 | 495 | This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details. 496 | ``` -------------------------------------------------------------------------------- /src/servers/postgres-server/postgres-server.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { z } from "zod"; 4 | import { config } from "dotenv"; 5 | import pkg from "pg"; 6 | const { Pool } = pkg; 7 | 8 | // Load environment variables 9 | config(); 10 | 11 | // Flag to track if we're in demo mode (no real DB connection) 12 | let demoMode = false; 13 | 14 | // Define interfaces for our demo data 15 | interface DemoUser { 16 | id: number; 17 | username: string; 18 | email: string; 19 | created_at: string; 20 | } 21 | 22 | interface DemoProduct { 23 | id: number; 24 | name: string; 25 | price: number; 26 | created_at: string; 27 | } 28 | 29 | interface DemoResult { 30 | rows: any[]; 31 | rowCount: number; 32 | } 33 | 34 | // Create the PostgreSQL pool with a connection timeout 35 | const pool = new Pool({ 36 | host: process.env.POSTGRES_HOST || "localhost", 37 | port: parseInt(process.env.POSTGRES_PORT || "5432"), 38 | database: process.env.POSTGRES_DB || "postgres", 39 | user: process.env.POSTGRES_USER || "postgres", 40 | password: process.env.POSTGRES_PASSWORD || "", 41 | ssl: 42 | process.env.POSTGRES_SSL_MODE === "require" 43 | ? { rejectUnauthorized: false } 44 | : undefined, 45 | max: parseInt(process.env.POSTGRES_MAX_CONNECTIONS || "10"), 46 | idleTimeoutMillis: 30000, 47 | connectionTimeoutMillis: 5000, // 5 second timeout 48 | }); 49 | 50 | // Add error handler 51 | pool.on("error", (err) => { 52 | console.error("Unexpected error on idle PostgreSQL client", err); 53 | }); 54 | 55 | // Create an MCP server 56 | const server = new McpServer({ 57 | name: "PostgreSQL Server", 58 | version: "1.0.0", 59 | }); 60 | 61 | // Get database info tool 62 | server.tool("mcp__get_database_info", {}, async () => { 63 | if (demoMode) { 64 | return { 65 | content: [ 66 | { 67 | type: "text", 68 | text: "Running in demo mode - no actual PostgreSQL connection.", 69 | }, 70 | { 71 | type: "text", 72 | text: JSON.stringify( 73 | { 74 | database_name: "demo_database", 75 | current_user: "demo_user", 76 | postgresql_version: "PostgreSQL 14.0 (Demo Version)", 77 | }, 78 | null, 79 | 2 80 | ), 81 | }, 82 | ], 83 | }; 84 | } 85 | 86 | try { 87 | const result = await pool.query(` 88 | SELECT current_database() as database_name, 89 | current_user as current_user, 90 | version() as postgresql_version 91 | `); 92 | 93 | return { 94 | content: [ 95 | { 96 | type: "text", 97 | text: `Connected to database: ${result.rows[0].database_name} as ${result.rows[0].current_user}`, 98 | }, 99 | { 100 | type: "text", 101 | text: JSON.stringify(result.rows[0], null, 2), 102 | }, 103 | ], 104 | }; 105 | } catch (error) { 106 | console.error("Error getting database info:", error); 107 | return { 108 | content: [ 109 | { 110 | type: "text", 111 | text: `Error getting database info: ${ 112 | error instanceof Error ? error.message : String(error) 113 | }`, 114 | }, 115 | ], 116 | isError: true, 117 | }; 118 | } 119 | }); 120 | 121 | // List tables tool 122 | server.tool("mcp__list_tables", {}, async () => { 123 | if (demoMode) { 124 | return { 125 | content: [ 126 | { 127 | type: "text", 128 | text: "Running in demo mode - showing sample tables.", 129 | }, 130 | { 131 | type: "text", 132 | text: JSON.stringify( 133 | [ 134 | { 135 | table_name: "users", 136 | table_schema: "public", 137 | table_type: "BASE TABLE", 138 | }, 139 | { 140 | table_name: "products", 141 | table_schema: "public", 142 | table_type: "BASE TABLE", 143 | }, 144 | { 145 | table_name: "orders", 146 | table_schema: "public", 147 | table_type: "BASE TABLE", 148 | }, 149 | ], 150 | null, 151 | 2 152 | ), 153 | }, 154 | ], 155 | }; 156 | } 157 | 158 | try { 159 | const result = await pool.query(` 160 | SELECT 161 | table_name, 162 | table_schema, 163 | table_type 164 | FROM 165 | information_schema.tables 166 | WHERE 167 | table_schema NOT IN ('pg_catalog', 'information_schema') 168 | ORDER BY 169 | table_schema, table_name 170 | `); 171 | 172 | return { 173 | content: [ 174 | { 175 | type: "text", 176 | text: `Found ${result.rows.length} tables.`, 177 | }, 178 | { 179 | type: "text", 180 | text: JSON.stringify(result.rows, null, 2), 181 | }, 182 | ], 183 | }; 184 | } catch (error) { 185 | console.error("Error listing tables:", error); 186 | return { 187 | content: [ 188 | { 189 | type: "text", 190 | text: `Error listing tables: ${ 191 | error instanceof Error ? error.message : String(error) 192 | }`, 193 | }, 194 | ], 195 | isError: true, 196 | }; 197 | } 198 | }); 199 | 200 | // Get table structure tool 201 | server.tool( 202 | "mcp__get_table_structure", 203 | { 204 | table_name: z.string().describe("The name of the table to examine"), 205 | }, 206 | async ({ table_name }) => { 207 | if (demoMode) { 208 | // Return different demo data based on the requested table 209 | let columns = []; 210 | if (table_name === "users") { 211 | columns = [ 212 | { 213 | column_name: "id", 214 | data_type: "integer", 215 | is_nullable: "NO", 216 | column_default: "nextval('users_id_seq'::regclass)", 217 | }, 218 | { 219 | column_name: "username", 220 | data_type: "character varying", 221 | is_nullable: "NO", 222 | column_default: null, 223 | }, 224 | { 225 | column_name: "email", 226 | data_type: "character varying", 227 | is_nullable: "NO", 228 | column_default: null, 229 | }, 230 | { 231 | column_name: "created_at", 232 | data_type: "timestamp", 233 | is_nullable: "NO", 234 | column_default: "CURRENT_TIMESTAMP", 235 | }, 236 | ]; 237 | } else if (table_name === "products") { 238 | columns = [ 239 | { 240 | column_name: "id", 241 | data_type: "integer", 242 | is_nullable: "NO", 243 | column_default: "nextval('products_id_seq'::regclass)", 244 | }, 245 | { 246 | column_name: "name", 247 | data_type: "character varying", 248 | is_nullable: "NO", 249 | column_default: null, 250 | }, 251 | { 252 | column_name: "price", 253 | data_type: "numeric", 254 | is_nullable: "NO", 255 | column_default: null, 256 | }, 257 | { 258 | column_name: "created_at", 259 | data_type: "timestamp", 260 | is_nullable: "NO", 261 | column_default: "CURRENT_TIMESTAMP", 262 | }, 263 | ]; 264 | } else { 265 | columns = [ 266 | { 267 | column_name: "id", 268 | data_type: "integer", 269 | is_nullable: "NO", 270 | column_default: "nextval('table_id_seq'::regclass)", 271 | }, 272 | { 273 | column_name: "name", 274 | data_type: "character varying", 275 | is_nullable: "NO", 276 | column_default: null, 277 | }, 278 | { 279 | column_name: "created_at", 280 | data_type: "timestamp", 281 | is_nullable: "NO", 282 | column_default: "CURRENT_TIMESTAMP", 283 | }, 284 | ]; 285 | } 286 | 287 | return { 288 | content: [ 289 | { 290 | type: "text", 291 | text: `Demo structure for table ${table_name}: ${columns.length} columns found.`, 292 | }, 293 | { 294 | type: "text", 295 | text: JSON.stringify(columns, null, 2), 296 | }, 297 | ], 298 | }; 299 | } 300 | 301 | try { 302 | const result = await pool.query( 303 | ` 304 | SELECT 305 | column_name, 306 | data_type, 307 | is_nullable, 308 | column_default 309 | FROM 310 | information_schema.columns 311 | WHERE 312 | table_name = $1 313 | ORDER BY 314 | ordinal_position 315 | `, 316 | [table_name] 317 | ); 318 | 319 | return { 320 | content: [ 321 | { 322 | type: "text", 323 | text: `Structure for table ${table_name}: ${result.rows.length} columns found.`, 324 | }, 325 | { 326 | type: "text", 327 | text: JSON.stringify(result.rows, null, 2), 328 | }, 329 | ], 330 | }; 331 | } catch (error) { 332 | console.error(`Error getting structure for table ${table_name}:`, error); 333 | return { 334 | content: [ 335 | { 336 | type: "text", 337 | text: `Error getting table structure: ${ 338 | error instanceof Error ? error.message : String(error) 339 | }`, 340 | }, 341 | ], 342 | isError: true, 343 | }; 344 | } 345 | } 346 | ); 347 | 348 | // Execute query tool 349 | server.tool( 350 | "mcp__execute_query", 351 | { 352 | query: z.string().describe("The SQL query to execute"), 353 | params: z 354 | .array(z.union([z.string(), z.number(), z.boolean(), z.null()])) 355 | .optional() 356 | .describe("Optional parameters for the query"), 357 | }, 358 | async ({ query, params }) => { 359 | if (demoMode) { 360 | // In demo mode, generate some fake results based on the query 361 | const lowerQuery = query.toLowerCase(); 362 | let demoResult: DemoResult = { rows: [], rowCount: 0 }; 363 | 364 | if (lowerQuery.includes("select") && lowerQuery.includes("users")) { 365 | const users: DemoUser[] = [ 366 | { 367 | id: 1, 368 | username: "john_doe", 369 | email: "[email protected]", 370 | created_at: "2023-01-01T00:00:00Z", 371 | }, 372 | { 373 | id: 2, 374 | username: "jane_smith", 375 | email: "[email protected]", 376 | created_at: "2023-01-02T00:00:00Z", 377 | }, 378 | ]; 379 | demoResult.rows = users; 380 | demoResult.rowCount = 2; 381 | } else if ( 382 | lowerQuery.includes("select") && 383 | lowerQuery.includes("products") 384 | ) { 385 | const products: DemoProduct[] = [ 386 | { 387 | id: 1, 388 | name: "Product A", 389 | price: 19.99, 390 | created_at: "2023-01-01T00:00:00Z", 391 | }, 392 | { 393 | id: 2, 394 | name: "Product B", 395 | price: 29.99, 396 | created_at: "2023-01-02T00:00:00Z", 397 | }, 398 | { 399 | id: 3, 400 | name: "Product C", 401 | price: 39.99, 402 | created_at: "2023-01-03T00:00:00Z", 403 | }, 404 | ]; 405 | demoResult.rows = products; 406 | demoResult.rowCount = 3; 407 | } else if ( 408 | lowerQuery.includes("insert") || 409 | lowerQuery.includes("update") || 410 | lowerQuery.includes("delete") 411 | ) { 412 | demoResult.rowCount = 1; 413 | } 414 | 415 | return { 416 | content: [ 417 | { 418 | type: "text", 419 | text: `Demo query executed successfully. Rows affected: ${demoResult.rowCount}`, 420 | }, 421 | { 422 | type: "text", 423 | text: JSON.stringify( 424 | { 425 | rows: demoResult.rows, 426 | rowCount: demoResult.rowCount, 427 | fields: 428 | demoResult.rows.length > 0 429 | ? Object.keys(demoResult.rows[0]).map((name) => ({ 430 | name, 431 | dataTypeID: 0, 432 | })) 433 | : [], 434 | }, 435 | null, 436 | 2 437 | ), 438 | }, 439 | ], 440 | }; 441 | } 442 | 443 | try { 444 | const result = await pool.query(query, params); 445 | 446 | const resultData = { 447 | rows: result.rows, 448 | rowCount: result.rowCount, 449 | fields: result.fields 450 | ? result.fields.map((f) => ({ 451 | name: f.name, 452 | dataTypeID: f.dataTypeID, 453 | })) 454 | : [], 455 | }; 456 | 457 | return { 458 | content: [ 459 | { 460 | type: "text", 461 | text: `Query executed successfully. Rows affected: ${result.rowCount}`, 462 | }, 463 | { 464 | type: "text", 465 | text: JSON.stringify(resultData, null, 2), 466 | }, 467 | ], 468 | }; 469 | } catch (error) { 470 | console.error("Error executing query:", error); 471 | return { 472 | content: [ 473 | { 474 | type: "text", 475 | text: `Error executing query: ${ 476 | error instanceof Error ? error.message : String(error) 477 | }`, 478 | }, 479 | ], 480 | isError: true, 481 | }; 482 | } 483 | } 484 | ); 485 | 486 | // Handle termination signals 487 | process.on("SIGINT", async () => { 488 | console.log("Received SIGINT signal. Shutting down..."); 489 | if (!demoMode) { 490 | await pool.end(); 491 | console.log("PostgreSQL connection pool closed"); 492 | } 493 | process.exit(0); 494 | }); 495 | 496 | process.on("SIGTERM", async () => { 497 | console.log("Received SIGTERM signal. Shutting down..."); 498 | if (!demoMode) { 499 | await pool.end(); 500 | console.log("PostgreSQL connection pool closed"); 501 | } 502 | process.exit(0); 503 | }); 504 | 505 | // Start the server 506 | async function startServer() { 507 | try { 508 | // Validate environment variables, but don't error out if missing 509 | const requiredEnvVars = [ 510 | "POSTGRES_DB", 511 | "POSTGRES_USER", 512 | "POSTGRES_PASSWORD", 513 | ]; 514 | const missingEnvVars = requiredEnvVars.filter( 515 | (envVar) => !process.env[envVar] 516 | ); 517 | 518 | if (missingEnvVars.length > 0) { 519 | console.warn( 520 | `Warning: Missing environment variables: ${missingEnvVars.join(", ")}` 521 | ); 522 | console.warn( 523 | "The server will run in demo mode without connecting to a real database." 524 | ); 525 | demoMode = true; 526 | } else { 527 | // Test the connection 528 | try { 529 | const client = await pool.connect(); 530 | console.log("Successfully connected to PostgreSQL"); 531 | client.release(); 532 | } catch (error) { 533 | console.warn("Failed to connect to PostgreSQL:", error); 534 | console.warn( 535 | "The server will run in demo mode without a real database connection." 536 | ); 537 | demoMode = true; 538 | } 539 | } 540 | 541 | // Start receiving messages on stdin and sending messages on stdout 542 | const transport = new StdioServerTransport(); 543 | await server.connect(transport); 544 | console.log( 545 | `PostgreSQL Server started in ${ 546 | demoMode ? "DEMO" : "PRODUCTION" 547 | } mode and ready to process requests` 548 | ); 549 | } catch (error) { 550 | console.error("Failed to start server:", error); 551 | process.exit(1); 552 | } 553 | } 554 | 555 | // Start the server if this file is run directly 556 | if (process.argv[1] === new URL(import.meta.url).pathname) { 557 | startServer(); 558 | } 559 | 560 | export default server; 561 | ``` -------------------------------------------------------------------------------- /MCP_SERVER_DEVELOPMENT_GUIDE.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Server Development Guide 2 | 3 | This guide provides comprehensive rules and best practices for building Model Context Protocol (MCP) servers, based on analysis of existing implementations in this project. 4 | 5 | ## Table of Contents 6 | 7 | 1. [Core Architecture Patterns](#core-architecture-patterns) 8 | 2. [Project Structure Requirements](#project-structure-requirements) 9 | 3. [Implementation Patterns](#implementation-patterns) 10 | 4. [Tool Development Guidelines](#tool-development-guidelines) 11 | 5. [Configuration and Environment](#configuration-and-environment) 12 | 6. [Error Handling Standards](#error-handling-standards) 13 | 7. [Documentation Requirements](#documentation-requirements) 14 | 8. [Testing and Deployment](#testing-and-deployment) 15 | 9. [Integration and Automation](#integration-and-automation) 16 | 10. [Advanced Patterns](#advanced-patterns) 17 | 18 | ## Core Architecture Patterns 19 | 20 | ### 1. MCP Server Foundation 21 | 22 | Every MCP server MUST follow this core structure: 23 | 24 | ```typescript 25 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 26 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 27 | import { z } from "zod"; 28 | 29 | // Create server instance 30 | const server = new McpServer({ 31 | name: "Your Server Name", 32 | version: "1.0.0", 33 | }); 34 | 35 | // Define tools using zod for validation 36 | server.tool( 37 | "tool_name", 38 | { 39 | param1: z.string().describe("Parameter description"), 40 | param2: z.number().optional().describe("Optional parameter"), 41 | }, 42 | async ({ param1, param2 }) => { 43 | // Tool implementation 44 | return { 45 | content: [ 46 | { type: "text", text: "Human-readable response" }, 47 | { type: "text", text: JSON.stringify(data, null, 2) }, 48 | ], 49 | }; 50 | } 51 | ); 52 | 53 | // Server lifecycle management 54 | async function startServer() { 55 | const transport = new StdioServerTransport(); 56 | await server.connect(transport); 57 | console.log("Server started and ready to process requests"); 58 | } 59 | 60 | // Export for testing and import 61 | export default server; 62 | 63 | // Start if run directly 64 | if (process.argv[1] === new URL(import.meta.url).pathname) { 65 | startServer(); 66 | } 67 | ``` 68 | 69 | ### 2. Response Format Standards 70 | 71 | **CRITICAL**: MCP servers must return responses in exact format: 72 | 73 | ```typescript 74 | // Standard successful response 75 | return { 76 | content: [ 77 | { type: "text", text: "Human-readable summary" }, 78 | { type: "text", text: JSON.stringify(structuredData, null, 2) }, 79 | ], 80 | }; 81 | 82 | // Error response 83 | return { 84 | content: [{ type: "text", text: `Error: ${errorMessage}` }], 85 | isError: true, 86 | }; 87 | ``` 88 | 89 | **DO NOT**: 90 | 91 | - Use unsupported content types like `type: "json"` 92 | - Return raw objects without text wrapper 93 | - Mix different response formats 94 | 95 | ## Project Structure Requirements 96 | 97 | ### 1. Directory Structure 98 | 99 | ``` 100 | src/servers/your-server/ 101 | ├── index.ts # Server entry point 102 | ├── your-server.ts # Main server implementation 103 | ├── your-server-api.ts # External API/logic layer (if needed) 104 | ├── types.ts # TypeScript type definitions 105 | ├── README.md # Server-specific documentation 106 | └── test/ # Server-specific tests 107 | ``` 108 | 109 | ### 2. Naming Conventions 110 | 111 | - **Server directory**: `kebab-case` (e.g., `postgres-server`, `pdf-server`) 112 | - **Main file**: `{server-name}.ts` (e.g., `postgres-server.ts`) 113 | - **Tool names**: `snake_case` with optional prefix (e.g., `mcp__get_data`, `execute_command`) 114 | - **Parameters**: `snake_case` (e.g., `table_name`, `namespace`) 115 | 116 | ### 3. File Requirements 117 | 118 | **Every server MUST have**: 119 | 120 | - Main server file with export default 121 | - TypeScript types file 122 | - README.md with tool documentation 123 | - Entry in `src/index.ts` and `src/run-all.ts` 124 | 125 | ## Implementation Patterns 126 | 127 | ### 1. Parameter Validation with Zod 128 | 129 | ```typescript 130 | // Required parameter 131 | param_name: z.string().describe("Clear description of what this parameter does"), 132 | 133 | // Optional parameter with default 134 | namespace: z.string().optional().describe("Kubernetes namespace (default: local)"), 135 | 136 | // Complex object parameter 137 | content: z.object({ 138 | text: z.string().optional(), 139 | formFields: z.record(z.string()).optional(), 140 | }).describe("Content structure"), 141 | 142 | // Array parameter 143 | params: z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])) 144 | .optional() 145 | .describe("Query parameters"), 146 | ``` 147 | 148 | ### 2. Configuration Management 149 | 150 | **Environment Variables Pattern**: 151 | 152 | ```typescript 153 | import { config } from "dotenv"; 154 | config(); // Load .env file 155 | 156 | // Define configuration with defaults 157 | const config = { 158 | host: process.env.SERVICE_HOST || "localhost", 159 | port: parseInt(process.env.SERVICE_PORT || "5432"), 160 | database: process.env.SERVICE_DB || "default_db", 161 | maxConnections: parseInt(process.env.SERVICE_MAX_CONNECTIONS || "10"), 162 | sslMode: process.env.SERVICE_SSL_MODE === "require", 163 | }; 164 | ``` 165 | 166 | **Demo Mode Pattern** (for servers that require external services): 167 | 168 | ```typescript 169 | let demoMode = false; 170 | 171 | // Check for required environment variables 172 | const requiredEnvVars = ["SERVICE_DB", "SERVICE_USER", "SERVICE_PASSWORD"]; 173 | const missingEnvVars = requiredEnvVars.filter((envVar) => !process.env[envVar]); 174 | 175 | if (missingEnvVars.length > 0) { 176 | console.warn(`Missing environment variables: ${missingEnvVars.join(", ")}`); 177 | console.warn("Running in demo mode with mock data."); 178 | demoMode = true; 179 | } 180 | ``` 181 | 182 | ### 3. External Service Integration 183 | 184 | **Separate API Layer Pattern**: 185 | 186 | ```typescript 187 | // your-server-api.ts 188 | export class YourServiceAPI { 189 | async getData(params): Promise<ResultType> { 190 | // External service logic 191 | } 192 | } 193 | 194 | // your-server.ts 195 | import * as YourAPI from "./your-server-api.js"; 196 | 197 | server.tool("get_data", schema, async (params) => { 198 | try { 199 | const result = await YourAPI.getData(params); 200 | return formatResponse(result); 201 | } catch (error) { 202 | return handleError(error); 203 | } 204 | }); 205 | ``` 206 | 207 | ## Tool Development Guidelines 208 | 209 | ### 1. Tool Naming Strategy 210 | 211 | **Categories of tools**: 212 | 213 | - **Read operations**: `get_*`, `list_*`, `find_*` 214 | - **Write operations**: `create_*`, `update_*`, `delete_*`, `execute_*` 215 | - **Utility operations**: `convert_*`, `parse_*`, `validate_*` 216 | 217 | **Examples**: 218 | 219 | - `mcp__get_database_info` (namespaced with mcp\_\_) 220 | - `get_pods` (generic name) 221 | - `execute_query` (action-oriented) 222 | 223 | ### 2. Parameter Design 224 | 225 | **Best Practices**: 226 | 227 | - Use descriptive parameter names 228 | - Provide clear descriptions for all parameters 229 | - Use optional parameters with sensible defaults 230 | - Group related parameters into objects 231 | - Validate parameter combinations in the handler 232 | 233 | ```typescript 234 | // Good parameter design 235 | server.tool( 236 | "execute_command", 237 | { 238 | pod_name: z 239 | .string() 240 | .describe("Name of the pod where command will be executed"), 241 | command: z.string().describe("Shell command to execute (e.g., 'ls -la')"), 242 | container_name: z 243 | .string() 244 | .optional() 245 | .describe("Container name (required for multi-container pods)"), 246 | namespace: z 247 | .string() 248 | .optional() 249 | .describe("Kubernetes namespace (default: 'local')"), 250 | timeout_seconds: z 251 | .number() 252 | .optional() 253 | .describe("Command timeout in seconds (default: 30)"), 254 | }, 255 | async ({ 256 | pod_name, 257 | command, 258 | container_name, 259 | namespace = "local", 260 | timeout_seconds = 30, 261 | }) => { 262 | // Implementation 263 | } 264 | ); 265 | ``` 266 | 267 | ### 3. Response Design 268 | 269 | **Multi-part responses for complex data**: 270 | 271 | ```typescript 272 | return { 273 | content: [ 274 | { 275 | type: "text", 276 | text: `Successfully processed ${items.length} items from ${source}`, 277 | }, 278 | { 279 | type: "text", 280 | text: JSON.stringify( 281 | { 282 | summary: { total: items.length, processed: results.length }, 283 | results: results, 284 | metadata: { timestamp: new Date().toISOString() }, 285 | }, 286 | null, 287 | 2 288 | ), 289 | }, 290 | ], 291 | }; 292 | ``` 293 | 294 | ## Configuration and Environment 295 | 296 | ### 1. Environment Variable Standards 297 | 298 | **Naming Convention**: `{SERVICE}__{SETTING}` (uppercase with double underscore) 299 | 300 | ```bash 301 | # Database configuration 302 | POSTGRES__HOST=localhost 303 | POSTGRES__PORT=5432 304 | POSTGRES__DATABASE=mydb 305 | POSTGRES__USER=user 306 | POSTGRES__PASSWORD=pass 307 | POSTGRES__SSL_MODE=require 308 | POSTGRES__MAX_CONNECTIONS=10 309 | 310 | # Service-specific settings 311 | KUBERNETES__KUBECONFIG=/path/to/config 312 | KUBERNETES__DEFAULT_NAMESPACE=local 313 | KUBERNETES__API_TIMEOUT=30000 314 | ``` 315 | 316 | ### 2. Configuration Validation 317 | 318 | ```typescript 319 | interface ServerConfig { 320 | host: string; 321 | port: number; 322 | database: string; 323 | user: string; 324 | password: string; 325 | sslMode?: boolean; 326 | maxConnections?: number; 327 | } 328 | 329 | function validateConfig(): ServerConfig { 330 | const config: ServerConfig = { 331 | host: process.env.POSTGRES__HOST || "localhost", 332 | port: parseInt(process.env.POSTGRES__PORT || "5432"), 333 | database: process.env.POSTGRES__DATABASE!, 334 | user: process.env.POSTGRES__USER!, 335 | password: process.env.POSTGRES__PASSWORD!, 336 | sslMode: process.env.POSTGRES__SSL_MODE === "require", 337 | maxConnections: parseInt(process.env.POSTGRES__MAX_CONNECTIONS || "10"), 338 | }; 339 | 340 | // Validate required fields 341 | const required = ["database", "user", "password"]; 342 | const missing = required.filter((key) => !config[key as keyof ServerConfig]); 343 | 344 | if (missing.length > 0) { 345 | throw new Error(`Missing required configuration: ${missing.join(", ")}`); 346 | } 347 | 348 | return config; 349 | } 350 | ``` 351 | 352 | ## Error Handling Standards 353 | 354 | ### 1. Error Response Pattern 355 | 356 | ```typescript 357 | async function handleToolCall<T>( 358 | operation: () => Promise<T>, 359 | operationName: string 360 | ): Promise<ToolResponse> { 361 | try { 362 | const result = await operation(); 363 | return { 364 | content: [ 365 | { type: "text", text: `${operationName} completed successfully` }, 366 | { type: "text", text: JSON.stringify(result, null, 2) }, 367 | ], 368 | }; 369 | } catch (error) { 370 | console.error(`Error in ${operationName}:`, error); 371 | return { 372 | content: [ 373 | { 374 | type: "text", 375 | text: `Error in ${operationName}: ${ 376 | error instanceof Error ? error.message : String(error) 377 | }`, 378 | }, 379 | ], 380 | isError: true, 381 | }; 382 | } 383 | } 384 | ``` 385 | 386 | ### 2. Service-Specific Error Handling 387 | 388 | ```typescript 389 | // Database connection errors 390 | try { 391 | const result = await pool.query(query, params); 392 | return formatSuccessResponse(result); 393 | } catch (error) { 394 | if (error.code === "23505") { 395 | return formatErrorResponse("Duplicate key violation"); 396 | } else if (error.code === "ECONNREFUSED") { 397 | return formatErrorResponse("Database connection refused"); 398 | } 399 | return formatErrorResponse(`Database error: ${error.message}`); 400 | } 401 | ``` 402 | 403 | ### 3. Graceful Degradation 404 | 405 | ```typescript 406 | // Fallback to demo mode when external service unavailable 407 | if (demoMode) { 408 | return { 409 | content: [ 410 | { type: "text", text: "Running in demo mode - showing sample data" }, 411 | { type: "text", text: JSON.stringify(mockData, null, 2) }, 412 | ], 413 | }; 414 | } 415 | ``` 416 | 417 | ## Documentation Requirements 418 | 419 | ### 1. Server README Structure 420 | 421 | Every server MUST have a README.md with: 422 | 423 | ````markdown 424 | # [Server Name] MCP Server 425 | 426 | Brief description of what the server does. 427 | 428 | ## Features 429 | 430 | - Feature 1: Description 431 | - Feature 2: Description 432 | 433 | ## Tools 434 | 435 | ### `tool_name` 436 | 437 | Description of what the tool does. 438 | 439 | **Parameters:** 440 | 441 | - `param1` (string): Description 442 | - `param2` (number, optional): Description with default 443 | 444 | **Returns:** 445 | 446 | - Success status 447 | - Data description 448 | - Any additional fields 449 | 450 | **Example Usage:** 451 | [Provide example of how the tool would be used] 452 | 453 | ## Configuration 454 | 455 | Environment variables required: 456 | 457 | - `SERVICE_HOST`: Description (default: localhost) 458 | - `SERVICE_PORT`: Description (default: 5432) 459 | 460 | ## Dependencies 461 | 462 | - dependency1: Purpose 463 | - dependency2: Purpose 464 | 465 | ## Running the Server 466 | 467 | ```bash 468 | # Development 469 | npm run dev:servername 470 | 471 | # Production 472 | npm run start:servername 473 | ``` 474 | ```` 475 | 476 | ```` 477 | 478 | ### 2. Tool Documentation Standards 479 | 480 | ```typescript 481 | // In-code documentation 482 | server.tool( 483 | "descriptive_tool_name", 484 | { 485 | // Parameter descriptions must be clear and include examples where helpful 486 | table_name: z.string().describe("Name of the database table to query (e.g., 'users', 'products')"), 487 | limit: z.number().optional().describe("Maximum number of rows to return (default: 100, max: 1000)"), 488 | filters: z.record(z.string()).optional().describe("Column filters as key-value pairs (e.g., {'status': 'active'})"), 489 | }, 490 | async ({ table_name, limit = 100, filters }) => { 491 | // Implementation with clear success/error paths 492 | } 493 | ); 494 | ```` 495 | 496 | ## Testing and Deployment 497 | 498 | ### 1. Package.json Integration 499 | 500 | **Required Scripts**: 501 | 502 | ```json 503 | { 504 | "scripts": { 505 | "dev:yourserver": "NODE_OPTIONS=\"--loader ts-node/esm\" node src/servers/your-server/your-server.ts", 506 | "start:yourserver": "node dist/src/servers/your-server/your-server.js" 507 | } 508 | } 509 | ``` 510 | 511 | ### 2. TypeScript Configuration 512 | 513 | Must work with the project's `tsconfig.json`: 514 | 515 | ```json 516 | { 517 | "compilerOptions": { 518 | "target": "ES2020", 519 | "module": "NodeNext", 520 | "moduleResolution": "NodeNext", 521 | "esModuleInterop": true, 522 | "strict": true, 523 | "outDir": "dist" 524 | } 525 | } 526 | ``` 527 | 528 | ### 3. Process Lifecycle Management 529 | 530 | **Required signal handlers**: 531 | 532 | ```typescript 533 | // Handle graceful shutdown 534 | process.on("SIGINT", async () => { 535 | console.log("Received SIGINT signal. Shutting down..."); 536 | // Clean up resources (close connections, etc.) 537 | await cleanup(); 538 | process.exit(0); 539 | }); 540 | 541 | process.on("SIGTERM", async () => { 542 | console.log("Received SIGTERM signal. Shutting down..."); 543 | await cleanup(); 544 | process.exit(0); 545 | }); 546 | ``` 547 | 548 | ## Integration and Automation 549 | 550 | ### 1. Server Registry Updates 551 | 552 | When adding a new server, update these files: 553 | 554 | **src/index.ts**: 555 | 556 | ```typescript 557 | const servers = [ 558 | // ... existing servers 559 | { 560 | name: "your-server", 561 | displayName: "Your Server Name", 562 | path: join(__dirname, "servers/your-server/your-server.ts"), 563 | }, 564 | ]; 565 | ``` 566 | 567 | **src/run-all.ts**: 568 | 569 | ```typescript 570 | const servers = [ 571 | // ... existing servers 572 | { 573 | name: "Your Server Name", 574 | path: join(__dirname, "servers/your-server/your-server.ts"), 575 | }, 576 | ]; 577 | ``` 578 | 579 | ### 2. Cursor IDE Integration 580 | 581 | The setup script automatically generates: 582 | 583 | - Shell scripts for each server (`cursor-{server}-server.sh`) 584 | - MCP configuration instructions 585 | - Absolute paths for Cursor IDE 586 | 587 | **Manual verification**: 588 | 589 | ```bash 590 | npm run setup 591 | # Verify your server appears in the generated scripts and instructions 592 | ``` 593 | 594 | ### 3. Development Workflow 595 | 596 | ```bash 597 | # 1. Create server structure 598 | mkdir -p src/servers/your-server 599 | 600 | # 2. Implement server files 601 | # - your-server.ts (main implementation) 602 | # - types.ts (TypeScript definitions) 603 | # - README.md (documentation) 604 | 605 | # 3. Register server 606 | # - Add to src/index.ts 607 | # - Add to src/run-all.ts 608 | # - Add npm scripts to package.json 609 | 610 | # 4. Test development mode 611 | npm run dev -- your-server 612 | 613 | # 5. Test production build 614 | npm run build 615 | npm run start:your-server 616 | 617 | # 6. Generate Cursor integration 618 | npm run setup 619 | 620 | # 7. Test in Cursor IDE 621 | # - Add server to MCP configuration 622 | # - Test tools in Cursor composer 623 | ``` 624 | 625 | ## Advanced Patterns 626 | 627 | ### 1. Dependency Injection 628 | 629 | ```typescript 630 | // For testable, modular servers 631 | export class YourServer { 632 | constructor( 633 | private config: ServerConfig, 634 | private apiClient: ExternalAPIClient, 635 | private logger: Logger = console 636 | ) {} 637 | 638 | async initialize(): Promise<McpServer> { 639 | const server = new McpServer({ 640 | name: this.config.name, 641 | version: this.config.version, 642 | }); 643 | 644 | this.registerTools(server); 645 | return server; 646 | } 647 | 648 | private registerTools(server: McpServer): void { 649 | server.tool("tool_name", schema, this.handleToolCall.bind(this)); 650 | } 651 | } 652 | ``` 653 | 654 | ### 2. Connection Pooling and Resource Management 655 | 656 | ```typescript 657 | // For servers that manage persistent connections 658 | export class ConnectionManager { 659 | private pool: ConnectionPool; 660 | 661 | constructor(config: PoolConfig) { 662 | this.pool = new ConnectionPool(config); 663 | this.setupCleanup(); 664 | } 665 | 666 | private setupCleanup(): void { 667 | const cleanup = async () => { 668 | await this.pool.end(); 669 | console.log("Connection pool closed"); 670 | }; 671 | 672 | process.on("SIGINT", cleanup); 673 | process.on("SIGTERM", cleanup); 674 | process.on("exit", cleanup); 675 | } 676 | } 677 | ``` 678 | 679 | ### 3. Tool Composition 680 | 681 | ```typescript 682 | // For servers with complex tool interactions 683 | export class CompositeToolHandler { 684 | async handleComplexOperation(params: ComplexParams) { 685 | // Break down complex operations into smaller, reusable pieces 686 | const step1Result = await this.executeStep1(params.step1Params); 687 | const step2Result = await this.executeStep2( 688 | step1Result, 689 | params.step2Params 690 | ); 691 | const finalResult = await this.combineResults(step1Result, step2Result); 692 | 693 | return this.formatResponse(finalResult); 694 | } 695 | } 696 | ``` 697 | 698 | ### 4. Validation and Sanitization 699 | 700 | ```typescript 701 | // Advanced parameter validation 702 | const paramSchema = z.object({ 703 | query: z 704 | .string() 705 | .min(1, "Query cannot be empty") 706 | .max(10000, "Query too long") 707 | .refine( 708 | (query) => !query.toLowerCase().includes("drop table"), 709 | "Destructive operations not allowed" 710 | ), 711 | params: z 712 | .array(z.union([z.string(), z.number(), z.boolean(), z.null()])) 713 | .max(100, "Too many parameters") 714 | .optional(), 715 | }); 716 | ``` 717 | 718 | ## Summary Checklist 719 | 720 | When building a new MCP server, ensure: 721 | 722 | **Core Requirements**: 723 | 724 | - [ ] Uses MCP SDK with proper server initialization 725 | - [ ] Implements StdioServerTransport for Cursor compatibility 726 | - [ ] Uses Zod for parameter validation 727 | - [ ] Returns properly formatted responses 728 | - [ ] Handles errors gracefully with isError flag 729 | 730 | **Project Integration**: 731 | 732 | - [ ] Follows directory structure conventions 733 | - [ ] Registered in src/index.ts and src/run-all.ts 734 | - [ ] Has appropriate npm scripts in package.json 735 | - [ ] Includes comprehensive README.md 736 | - [ ] Defines TypeScript types 737 | 738 | **Production Readiness**: 739 | 740 | - [ ] Handles process termination signals 741 | - [ ] Manages external resources properly 742 | - [ ] Supports configuration via environment variables 743 | - [ ] Includes demo/fallback mode for development 744 | - [ ] Provides clear error messages 745 | 746 | **Documentation**: 747 | 748 | - [ ] README with features, tools, and configuration 749 | - [ ] Clear parameter descriptions in tool definitions 750 | - [ ] Usage examples and configuration guide 751 | - [ ] Dependencies and setup instructions 752 | 753 | **Testing**: 754 | 755 | - [ ] Works in development mode (npm run dev) 756 | - [ ] Builds and runs in production mode 757 | - [ ] Integrates with Cursor IDE setup process 758 | - [ ] Validates all tools with expected parameters 759 | 760 | This guide ensures consistency, maintainability, and proper integration with the Cursor IDE ecosystem. 761 | ```