# 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: -------------------------------------------------------------------------------- ``` # Node.js dependencies node_modules/ npm-debug.log yarn-debug.log yarn-error.log package-lock.json yarn.lock # TypeScript compiled output dist/ build/ *.tsbuildinfo # Environment variables .env .env.local .env.development.local .env.test.local .env.production.local # Editor directories and files .idea/ .vscode/* !.vscode/extensions.json !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json *.suo *.ntvs* *.njsproj *.sln *.sw* # Logs logs/ *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # Operating System Files .DS_Store Thumbs.db ehthumbs.db Desktop.ini $RECYCLE.BIN/ ._* # Testing coverage/ .nyc_output/ # Temporary files *.tmp *.temp .cache/ tmp/ # unsupported formats *.pdf ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` # Jira API Configuration JIRA_API_URL=https://your-domain.atlassian.net [email protected] JIRA_API_TOKEN=your-jira-api-token # You can generate an API token at: https://id.atlassian.com/manage-profile/security/api-tokens # GitHub API Configuration GITHUB_TOKEN=your-github-personal-access-token # You can generate a personal access token at: https://github.com/settings/tokens # PostgreSQL Configuration POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_DB=your_database_name POSTGRES_USER=your_username POSTGRES_PASSWORD=your_password # POSTGRES_SSL_MODE=require # Uncomment if SSL is required # POSTGRES_MAX_CONNECTIONS=10 # Optional: limit connection pool size # Kubernetes Configuration DEFAULT_NAMESPACE=local # Default namespace to use when not specified ``` -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- ``` # MCP Servers Project Rules This project contains multiple Model Context Protocol (MCP) servers for Cursor IDE integration. ## Project Structure - `src/servers/`: Individual MCP server implementations - `src/template/`: Reusable template for creating new servers - `scripts/`: Automation scripts for setup and deployment - Shell scripts for Cursor IDE integration are auto-generated ## Core MCP Server Patterns ### 1. Server Architecture Every MCP server must: - Import `McpServer` from `@modelcontextprotocol/sdk/server/mcp.js` - Use `StdioServerTransport` for Cursor IDE communication - Export default server instance for testing/imports - Use `process.argv[1] === new URL(import.meta.url).pathname` for direct execution ### 2. Tool Definition Standards - Use Zod for parameter validation with descriptive messages - Parameter names in `snake_case` - Tool names follow patterns: `get_*`, `list_*`, `create_*`, `execute_*` - Optional namespace prefix: `mcp__` for core operations ### 3. Response Format (CRITICAL) ```typescript // Success response return { content: [ { type: "text", text: "Human-readable summary" }, { type: "text", text: JSON.stringify(data, null, 2) }, ], }; // Error response return { content: [{ type: "text", text: `Error: ${message}` }], isError: true, }; ``` Never use `type: "json"` or return raw objects. ### 4. Configuration Patterns - Use `dotenv` for environment variable loading - Support demo/fallback mode when external services unavailable - Environment variables: `{SERVICE}_{SETTING}` (uppercase) - Validate required configuration with helpful error messages ### 5. Error Handling - Wrap tool implementations in try-catch blocks - Log errors to console for debugging - Return user-friendly error messages - Use `isError: true` flag for error responses ### 6. Process Lifecycle Always include signal handlers: ```typescript process.on("SIGINT", async () => { console.log("Shutting down..."); await cleanup(); process.exit(0); }); ``` ## File Structure Requirements Each server must have: - `{server-name}.ts` - Main implementation - `types.ts` - TypeScript type definitions - `README.md` - Documentation with tools, parameters, examples - `{server-name}-api.ts` - External service logic (if applicable) ## Integration Requirements When adding new servers: 1. Add to `src/index.ts` servers array 2. Add to `src/run-all.ts` servers array 3. Add npm scripts to `package.json`: - `dev:{server}`: Development mode with ts-node - `start:{server}`: Production mode with compiled JS 4. Run `npm run setup` to generate Cursor integration scripts ## Documentation Standards ### README.md Structure - Features list - Tool documentation with parameters and returns - Configuration requirements - Usage examples - Dependencies ### Parameter Documentation - Clear descriptions with examples - Specify defaults for optional parameters - Include validation constraints - Use realistic examples in descriptions ## Development Workflow 1. Create server directory: `mkdir -p src/servers/new-server` 2. Implement core files using existing servers as reference 3. Register server in index files and package.json 4. Test with `npm run dev -- new-server` 5. Build and test production: `npm run build && npm run start:new-server` 6. Generate integration: `npm run setup` 7. Test in Cursor IDE with generated configuration ## Common Patterns by Server Type ### Database Servers (like postgres-server) - Connection pooling with cleanup - Demo mode with mock data - Parameterized queries for security - Transaction support where needed ### API Servers (like kubernetes-server) - Separate API layer in `*-api.ts` - Client configuration from environment - Resource management (connections, auth) - Namespace/scope parameter patterns ### Processing Servers (like pdf-server) - Input validation for file paths vs base64 - Multiple output formats (file vs base64) - Processing result metadata - Stream handling for large files ## TypeScript Configuration Project uses ES modules with: - `"type": "module"` in package.json - `"module": "NodeNext"` in tsconfig.json - `.js` extensions in imports for compiled output - `--loader ts-node/esm` for development ## Testing and Validation Before production: - [ ] Server starts without errors - [ ] All tools accept expected parameters - [ ] Error cases return proper error responses - [ ] Demo mode works when external services unavailable - [ ] Cursor IDE integration script generated correctly - [ ] Documentation includes all tools and parameters ## Anti-Patterns to Avoid - Don't use unsupported response content types - Don't return raw objects without text wrapper - Don't skip parameter validation with Zod - Don't forget error handling with isError flag - Don't hardcode paths or configuration - Don't skip process signal handlers - Don't forget to export default server instance ## Cursor IDE Integration Generated shell scripts handle: - Working directory setup - Build process execution - Server startup with proper stdio - Absolute path resolution for configuration This project prioritizes consistency, reliability, and seamless Cursor IDE integration. ``` -------------------------------------------------------------------------------- /src/servers/lease-pdf-server/README.md: -------------------------------------------------------------------------------- ```markdown # PDF MCP Server A Model Context Protocol (MCP) server that provides basic PDF reading and writing functionality. ## Features - **Read PDF**: Extract text content and form fields from PDF files - **Write PDF**: Create new PDFs or modify existing ones with new content ## Tools ### `read_pdf` Extracts content from PDF files. **Parameters:** - `input` (string): PDF file path or base64 encoded PDF content **Returns:** - Success status - Page count - Extracted text content - Form fields (if any) ### `write_pdf` Creates or modifies PDF files. **Parameters:** - `content` (object): - `text` (optional): Text content to add to PDF - `formFields` (optional): Form fields to update as key-value pairs - `templatePdf` (optional): Template PDF file path or base64 content to modify - `outputPath` (optional): Output file path (if not provided, returns base64) **Returns:** - Success status - Output file path (if specified) - Base64 encoded PDF (if no output path specified) ## Usage 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. ### Example Workflow for Data Anonymization: 1. **Read PDF**: Use `read_pdf` to extract content from a lease contract 2. **AI Processing**: The MCP client (Cursor) uses AI to: - Identify sensitive data (names, addresses, financial amounts) - Generate realistic fake replacements - Maintain document structure and relationships 3. **Write PDF**: Use `write_pdf` to create the anonymized version ## Dependencies - `pdf-lib`: PDF creation and modification - `pdf-parse`: PDF text extraction - `@modelcontextprotocol/sdk`: MCP framework ## Running the Server ```bash # Development mode npm run dev:pdf # Production mode npm run start:pdf ``` ## Input Formats The server accepts PDF input in multiple formats: - File path: `/path/to/document.pdf` - Base64 with data URL: `data:application/pdf;base64,JVBERi0xLjQ...` - Raw base64: `JVBERi0xLjQ...` ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # MCP Servers for Cursor IDE 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. ## Table of Contents - [How To Use](#how-to-use) - [What is MCP?](#what-is-mcp) - [Project Structure](#project-structure) - [Available Servers](#available-servers) - [Server Configuration](#server-configuration) - [Available Tools](#available-tools) - [Running the Servers](#running-the-servers) - [Quick Start](#quick-start) - [Running a Server Using the Helper Script](#running-a-server-using-the-helper-script) - [Running a Single Server Manually](#running-a-single-server-manually) - [Running All Servers](#running-all-servers) - [List Available Servers](#list-available-servers) - [Testing Your MCP Server](#testing-your-mcp-server) - [Adding a New MCP Server](#adding-a-new-mcp-server) - [Understanding MCP Server Development](#understanding-mcp-server-development) - [Building the Project](#building-the-project) ## Prerequisites - Node.js (v16 or newer) - npm or yarn ## How To Use 1. Clone this repository: ``` git clone https://github.com/yourusername/mcp-servers.git cd mcp-servers ``` 2. Install dependencies and set up everything: ``` npm run setup ``` This command will: - Install all dependencies - Build the TypeScript project - Generate the necessary scripts for Cursor IDE integration - Provide instructions for setting up each server in Cursor 3. Configure Cursor IDE: - Open Cursor IDE - Go to Cursor Settings > Features > Mcp Servers - Click "Add New Mcp Server" - Enter a name for the server (e.g., "postgres") - For "Connection Type", select "command" - For "command", paste the path provided by the prepare script - Click "Save" 4. Environment Variables: - Copy the `.env.example` file to `.env` - Update the variables with your own credentials for each service 5. Use Mcp In Cursor IDE: - Open the composer - make sure you are using agent mode (claude 3.7 sonnet thinking is recommended) - submit the message you want to cursor ## What is MCP? 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. ## Project Structure ``` mcp-servers/ ├── src/ │ ├── servers/ # Individual MCP servers │ │ ├── postgres-server/ # PostgreSQL integration server │ │ │ └── postgres-server.ts │ │ ├── kubernetes-server/ # Kubernetes integration server │ │ │ └── kubernetes-server.ts │ │ ├── lease-pdf-server/ # PDF processing server │ │ │ └── pdf-server.ts │ │ └── ... (more servers) │ ├── template/ # Reusable templates │ │ └── mcp-server-template.ts │ ├── index.ts # Server runner utility │ └── run-all.ts # Script to run all servers ├── package.json └── tsconfig.json ``` ## Available Servers Currently, the following MCP servers are available: 1. **PostgreSQL Server** - Provides access to PostgreSQL databases for executing queries and retrieving schema information 2. **Kubernetes Server** - Provides access to Kubernetes clusters for managing pods, executing commands, and retrieving logs 3. **PDF Server** - Provides PDF document processing capabilities including text extraction, form field reading/writing, and PDF generation ## Server Configuration 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: ```bash # PostgreSQL Configuration (for PostgreSQL server) POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_DB=your_database_name POSTGRES_USER=your_username POSTGRES_PASSWORD=your_password # POSTGRES_SSL_MODE=require # Uncomment if SSL is required # POSTGRES_MAX_CONNECTIONS=10 # Optional: limit connection pool size # Kubernetes Configuration (for Kubernetes server) KUBECONFIG=/path/to/your/kubeconfig # Alternative Kubernetes configuration: # KUBE_API_URL=https://your-kubernetes-api-server # KUBE_API_TOKEN=your-kubernetes-service-account-token # PDF Server requires no additional configuration ``` ## Available Tools ### PostgreSQL Server Tools - `mcp__get_database_info` - Retrieves information about the connected database - `mcp__list_tables` - Lists all tables in the current database schema - `mcp__get_table_structure` - Gets the column definitions for a specific table - `mcp__execute_query` - Executes a custom SQL query against the database ### Kubernetes Server Tools - `get_pods` - Retrieves pods from a specified namespace, with optional field and label selectors - `find_pods` - Finds pods matching a name pattern in a specified namespace - `kill_pod` - Deletes a pod in a specified namespace - `exec_in_pod` - Executes a command in a specified pod and container - `get_pod_logs` - Retrieves logs from a specified pod, with options for container, line count, and previous instance ### PDF Server Tools - `read_pdf` - Extracts text content and form field data from PDF documents - **Parameters:** `input` (string) - PDF file path or base64 encoded PDF content - **Returns:** JSON with success status, page count, extracted text, form fields, and metadata - `write_pdf` - Creates new PDFs or modifies existing ones with content and form field updates - **Parameters:** - `content` (object) - Content to write (text and/or form fields) - `templatePdf` (optional string) - Template PDF file path or base64 content - `outputPath` (optional string) - Output file path (returns base64 if not provided) - **Returns:** JSON with success status, output path, and base64 data (if applicable) ## Running the Servers ### Quick Start The setup command creates individual shell scripts for each server that can be used directly with Cursor IDE. After running `npm run setup`, you'll see instructions for each server configuration. ### Running a Server Using the Helper Script To run a specific server using the included helper script: ``` npm run server -- [server-name] ``` For example, to run the postgres server: ``` npm run server -- postgres ``` Or to run the PDF server: ``` npm run server -- pdf ``` This will automatically build the TypeScript code and start the server. ### Running a Single Server Manually To run a specific server manually: ``` npm run dev -- [server-name] ``` For example, to run the postgres server: ``` npm run dev -- postgres ``` Or to run the PDF server: ``` npm run dev -- pdf ``` ### Running All Servers To run all servers simultaneously: ``` npm run dev:all ``` ### List Available Servers To see a list of all available servers: ``` npm run dev -- --list ``` ## Testing Your MCP Server Before connecting to Cursor IDE, you can test your MCP server's functionality: 1. Build your TypeScript project: ``` npm run build ``` 2. Run the server: ``` npm run start:postgres ``` Or for the PDF server: ``` npm run start:pdf ``` 3. For convenience, this project includes a ready-to-use script for Cursor: ``` /path/to/mcp-servers/cursor-mcp-server.sh [server-name] ``` 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: ``` # Run postgres server (default) /path/to/mcp-servers/cursor-mcp-server.sh # Run PDF server /path/to/mcp-servers/cursor-mcp-server.sh pdf # Run kubernetes server /path/to/mcp-servers/cursor-mcp-server.sh kubernetes ``` ## Adding a New MCP Server To add a new MCP server to this project: 1. Create a new directory for your server under `src/servers/`: ``` mkdir -p src/servers/my-new-server ``` 2. Create your server implementation: ```typescript // src/servers/my-new-server/my-new-server.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; // Create an MCP server const server = new McpServer({ name: "My New Server", version: "1.0.0", }); // Add your tools server.tool( "my-tool", { param1: z.string().describe("Parameter description"), param2: z.number().describe("Parameter description"), }, async ({ param1, param2 }) => { // Tool implementation return { content: [{ type: "text", text: `Result: ${param1} ${param2}` }], }; } ); // Start the server async function startServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.log("My New Server started and ready to process requests"); } // Start the server if this file is run directly if (process.argv[1] === new URL(import.meta.url).pathname) { startServer(); } export default server; ``` 3. Add your server to the server list in `src/index.ts` and `src/run-all.ts`: ```typescript const servers = [ // ... existing servers { name: "my-new-server", displayName: "My New Server", path: join(__dirname, "servers/my-new-server/my-new-server.ts"), }, ]; ``` 4. Update the package.json scripts (optional): ```json "scripts": { // ... existing scripts "dev:my-new-server": "ts-node --esm src/servers/my-new-server/my-new-server.ts" } ``` ## Understanding MCP Server Development When developing an MCP server, keep in mind: 1. **Tools** are the primary way to expose functionality. Each tool should: - Have a unique name - Define parameters using Zod for validation - Return results in a standardized format 2. **Communication** happens via stdio for Cursor integration. 3. **Response Format** is critical - MCP servers must follow the exact format: - Tools should return content with `type: "text"` for text responses - Avoid using unsupported types like `type: "json"` directly - For structured data, convert to JSON string and use `type: "text"` - Example: ```typescript return { content: [ { type: "text", text: "Human-readable response" }, { type: "text", text: JSON.stringify(structuredData, null, 2) }, ], }; ``` ## Building the Project To build the project, run: ``` npm run build ``` ``` -------------------------------------------------------------------------------- /src/servers/lease-pdf-server/index.ts: -------------------------------------------------------------------------------- ```typescript export { default } from "./pdf-server.js"; ``` -------------------------------------------------------------------------------- /src/servers/kubernetes-server/index.ts: -------------------------------------------------------------------------------- ```typescript export { default } from "./kubernetes-server.js"; ``` -------------------------------------------------------------------------------- /src/servers/postgres-server/index.ts: -------------------------------------------------------------------------------- ```typescript import server from "./postgres-server.js"; // Export the server instance export default server; ``` -------------------------------------------------------------------------------- /cursor-pdf-server.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Script to run the pdf MCP server for Cursor IDE cd "$(dirname "$0")" # Ensure the TypeScript code is built npm run build # Run the server with node node dist/src/servers/lease-pdf-server/pdf-server.js ``` -------------------------------------------------------------------------------- /cursor-postgres-server.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Script to run the postgres MCP server for Cursor IDE cd "$(dirname "$0")" # Ensure the TypeScript code is built npm run build # Run the server with node node dist/src/servers/postgres-server/postgres-server.js ``` -------------------------------------------------------------------------------- /cursor-kubernetes-server.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Script to run the kubernetes MCP server for Cursor IDE cd "$(dirname "$0")" # Ensure the TypeScript code is built npm run build # Run the server with node node dist/src/servers/kubernetes-server/kubernetes-server.js ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "outDir": "dist", "rootDir": ".", "resolveJsonModule": true }, "include": [ "src/**/*" ], "exclude": [ "node_modules" ] } ``` -------------------------------------------------------------------------------- /src/servers/lease-pdf-server/types.ts: -------------------------------------------------------------------------------- ```typescript export interface PdfReadResult { success: boolean; content?: { text: string; formFields?: Record<string, string>; pageCount: number; metadata?: { numrender?: number; info?: any; textLength: number; }; }; error?: string; } export interface PdfWriteRequest { content: { text?: string; formFields?: Record<string, string>; }; templatePdf?: string; // Base64 or file path to use as template } export interface PdfWriteResult { success: boolean; outputPath?: string; base64?: string; error?: string; } ``` -------------------------------------------------------------------------------- /cursor-mcp-server.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # This script is specifically for Cursor IDE to run the MCP server # It ensures proper working directory and environment # Usage: ./cursor-mcp-server.sh [server-name] # Default server: postgres # Change to the project directory cd "$(dirname "$0")" # Get server name from argument or default to postgres SERVER_NAME=${1:-postgres} # Ensure the TypeScript code is built npm run build > /dev/null 2>&1 # Set the server path based on server name if [ "$SERVER_NAME" = "pdf" ]; then SERVER_PATH="dist/src/servers/lease-pdf-server/pdf-server.js" else SERVER_PATH="dist/src/servers/${SERVER_NAME}-server/${SERVER_NAME}-server.js" fi # Run the compiled JavaScript version of the server # No stdout/stderr redirection to ensure clean stdio communication node "$SERVER_PATH" ``` -------------------------------------------------------------------------------- /run-mcp-server.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Log file for debugging LOG_FILE="$HOME/mcp-server-cursor.log" # Get server name from argument or default to postgres SERVER_NAME=${1:-postgres} # Log start time and command echo "=== Starting MCP Server $(date) ===" > "$LOG_FILE" echo "Working directory: $(pwd)" >> "$LOG_FILE" echo "Command: $0 $@" >> "$LOG_FILE" echo "Server: $SERVER_NAME" >> "$LOG_FILE" echo "Environment:" >> "$LOG_FILE" env >> "$LOG_FILE" # Change to the project directory cd "$(dirname "$0")" echo "Changed to directory: $(pwd)" >> "$LOG_FILE" # Set the server path based on server name if [ "$SERVER_NAME" = "pdf" ]; then SERVER_PATH="src/servers/lease-pdf-server/pdf-server.ts" else SERVER_PATH="src/servers/${SERVER_NAME}-server/${SERVER_NAME}-server.ts" fi # Run the server with ts-node loader echo "Running server..." >> "$LOG_FILE" NODE_OPTIONS="--loader ts-node/esm" node "$SERVER_PATH" 2>> "$LOG_FILE" # Log exit code echo "Server exited with code: $?" >> "$LOG_FILE" ``` -------------------------------------------------------------------------------- /src/template/mcp-server-template.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; export interface McpServerConfig { name: string; version: string; } export class McpServerTemplate { protected server: McpServer; private transport: StdioServerTransport | null = null; private config: McpServerConfig; constructor(config: McpServerConfig) { this.config = config; this.server = new McpServer({ name: config.name, version: config.version, }); } /** * Initialize the server by defining tools and their handlers */ async initialize(): Promise<void> { // This method should be overridden by subclasses to define tools throw new Error( "Method not implemented: Subclasses should implement this method" ); } /** * Start the server by connecting to the stdio transport */ async start(): Promise<void> { try { // Register tools before starting await this.initialize(); // Set up stdio transport this.transport = new StdioServerTransport(); // Connect to the transport await this.server.connect(this.transport); console.log( `${this.config.name} v${this.config.version} MCP server started` ); } catch (error) { console.error("Error starting MCP server:", error); process.exit(1); } } /** * Stop the server */ async stop(): Promise<void> { if (this.transport) { await this.transport.close(); console.log(`${this.config.name} MCP server stopped`); } } } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "mcp-servers", "version": "1.0.0", "description": "Multiple MCP servers for Cursor IDE", "main": "index.js", "type": "module", "scripts": { "build": "tsc", "start": "node dist/src/index.js", "start:all": "node dist/src/run-all.js", "start:postgres": "node dist/src/servers/postgres-server/postgres-server.js", "start:kubernetes": "node dist/src/servers/kubernetes-server/kubernetes-server.js", "start:pdf": "node dist/src/servers/lease-pdf-server/pdf-server.js", "setup": "npm install && npm run build && find . -name \"*.sh\" -exec chmod +x {} \\; && node scripts/generate-cursor-commands.js", "server": "node scripts/run-server.js", "dev": "NODE_OPTIONS=\"--loader ts-node/esm\" node --experimental-specifier-resolution=node src/index.ts", "dev:all": "NODE_OPTIONS=\"--loader ts-node/esm\" node --experimental-specifier-resolution=node src/run-all.ts", "dev:postgres": "NODE_OPTIONS=\"--loader ts-node/esm\" node src/servers/postgres-server/postgres-server.ts", "dev:kubernetes": "NODE_OPTIONS=\"--loader ts-node/esm\" node src/servers/kubernetes-server/kubernetes-server.ts", "dev:pdf": "NODE_OPTIONS=\"--loader ts-node/esm\" node src/servers/lease-pdf-server/pdf-server.ts" }, "keywords": [ "mcp", "cursor", "ide", "server" ], "author": "", "license": "ISC", "dependencies": { "@kubernetes/client-node": "^0.20.0", "@modelcontextprotocol/sdk": "^1.7.0", "@types/node": "^22.13.11", "dotenv": "^16.4.7", "node-fetch": "^3.3.2", "pdf-lib": "^1.17.1", "pdf-parse": "^1.1.1", "pg": "^8.14.1", "typescript": "^5.8.2", "zod": "^3.24.2" }, "devDependencies": { "@types/pdf-parse": "^1.1.5", "@types/pg": "^8.11.11", "ts-node": "^10.9.2" } } ``` -------------------------------------------------------------------------------- /src/run-all.ts: -------------------------------------------------------------------------------- ```typescript import { spawn } from "child_process"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Define available servers const servers = [ { name: "PostgreSQL Server", path: join(__dirname, "servers/postgres-server/postgres-server.ts"), }, { name: "Kubernetes Server", path: join(__dirname, "servers/kubernetes-server/kubernetes-server.ts"), }, { name: "PDF Server", path: join(__dirname, "servers/lease-pdf-server/pdf-server.ts"), }, // Add more servers here as they are created ]; // Function to start a server function startServer(serverInfo: { name: string; path: string }) { console.log(`Starting ${serverInfo.name}...`); // Use ts-node to run the TypeScript file directly const serverProcess = spawn("npx", ["ts-node", "--esm", serverInfo.path], { stdio: "pipe", // Capture output detached: false, }); // Set up logging for the server serverProcess.stdout.on("data", (data) => { console.log(`[${serverInfo.name}] ${data.toString().trim()}`); }); serverProcess.stderr.on("data", (data) => { console.error(`[${serverInfo.name}] ERROR: ${data.toString().trim()}`); }); // Handle server exit serverProcess.on("exit", (code) => { console.log(`[${serverInfo.name}] exited with code ${code}`); }); return serverProcess; } // Function to start all servers function startAllServers() { console.log("Starting all MCP servers..."); const processes = servers.map(startServer); // Handle script termination process.on("SIGINT", () => { console.log("Shutting down all servers..."); processes.forEach((p) => { if (!p.killed) { p.kill("SIGINT"); } }); }); process.on("SIGTERM", () => { console.log("Shutting down all servers..."); processes.forEach((p) => { if (!p.killed) { p.kill("SIGTERM"); } }); }); console.log("All servers started. Press Ctrl+C to stop."); } // Start all servers if this script is run directly if (process.argv[1] === fileURLToPath(import.meta.url)) { startAllServers(); } ``` -------------------------------------------------------------------------------- /scripts/run-server.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node import { spawn } from "child_process"; import { fileURLToPath } from "url"; import path from "path"; import fs from "fs"; // Get project root const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const projectRoot = path.resolve(__dirname, ".."); // Function to get available servers from package.json function getAvailableServers() { const packageJsonPath = path.join(projectRoot, "package.json"); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); return Object.keys(packageJson.scripts) .filter((script) => script.startsWith("start:") && script !== "start:all") .map((script) => script.replace("start:", "")); } // Main function function main() { const args = process.argv.slice(2); const availableServers = getAvailableServers(); // Display help if no arguments or help is requested if (args.length === 0 || args[0] === "--help" || args[0] === "-h") { console.log("Usage: node scripts/run-server.js [server-name]"); console.log(""); console.log("Available servers:"); availableServers.forEach((server) => { console.log(` - ${server}`); }); console.log(""); console.log("Example: node scripts/run-server.js postgres"); return; } const serverName = args[0]; // Check if server exists if (!availableServers.includes(serverName)) { console.error(`Error: Server "${serverName}" not found.`); console.log("Available servers:"); availableServers.forEach((server) => { console.log(` - ${server}`); }); process.exit(1); } // Run the server console.log(`Starting ${serverName} server...`); // First build the TypeScript code const buildProcess = spawn("npm", ["run", "build"], { stdio: "inherit", cwd: projectRoot, }); buildProcess.on("close", (code) => { if (code !== 0) { console.error(`Error: Build failed with code ${code}`); process.exit(code); } // Then run the server const serverProcess = spawn("npm", ["run", `start:${serverName}`], { stdio: "inherit", cwd: projectRoot, }); serverProcess.on("close", (code) => { console.log(`Server exited with code ${code}`); process.exit(code); }); // Handle process termination process.on("SIGINT", () => { console.log("Received SIGINT. Shutting down server..."); serverProcess.kill("SIGINT"); }); process.on("SIGTERM", () => { console.log("Received SIGTERM. Shutting down server..."); serverProcess.kill("SIGTERM"); }); }); } // Run the main function main(); ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript import { spawn } from "child_process"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Define available servers const servers = [ { name: "postgres", displayName: "PostgreSQL Server", path: join(__dirname, "servers/postgres-server/postgres-server.ts"), }, { name: "kubernetes", displayName: "Kubernetes Server", path: join(__dirname, "servers/kubernetes-server/kubernetes-server.ts"), }, { name: "pdf", displayName: "PDF Server", path: join(__dirname, "servers/lease-pdf-server/pdf-server.ts"), }, // Add more servers here as they are created ]; // Function to start a server by name function startServerByName(serverName: string) { const server = servers.find((s) => s.name === serverName); if (!server) { console.error( `Server "${serverName}" not found. Available servers: ${servers .map((s) => s.name) .join(", ")}` ); process.exit(1); } console.log(`Starting ${server.displayName}...`); // Use ts-node to run the TypeScript file directly const serverProcess = spawn("npx", ["ts-node", "--esm", server.path], { stdio: "inherit", // Inherit stdio from parent process detached: false, }); // Handle server exit serverProcess.on("exit", (code) => { console.log(`${server.displayName} exited with code ${code}`); process.exit(code || 0); }); // Forward termination signals process.on("SIGINT", () => serverProcess.kill("SIGINT")); process.on("SIGTERM", () => serverProcess.kill("SIGTERM")); } // Function to list all available servers function listServers() { console.log("Available MCP servers:"); servers.forEach((server) => { console.log(`- ${server.name}: ${server.displayName}`); }); } // Main function to parse arguments and run the appropriate server function main() { const args = process.argv.slice(2); if (args.length === 0 || args[0] === "--help" || args[0] === "-h") { console.log("Usage: npm run dev -- [server-name]"); console.log(" npm run dev -- --list"); console.log("\nOptions:"); console.log(" --list, -l List all available servers"); console.log(" --help, -h Show this help message"); console.log("\nTo run all servers: npm run dev:all"); listServers(); return; } if (args[0] === "--list" || args[0] === "-l") { listServers(); return; } startServerByName(args[0]); } // Run the main function if this script is executed directly if (process.argv[1] === fileURLToPath(import.meta.url)) { main(); } ``` -------------------------------------------------------------------------------- /scripts/generate-cursor-commands.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const projectRoot = path.resolve(__dirname, ".."); // Create individual server script for each MCP server function createServerScripts() { // Read package.json to get the list of servers const packageJson = JSON.parse( fs.readFileSync(path.join(projectRoot, "package.json"), "utf8") ); // Extract start scripts for servers const serverScripts = Object.keys(packageJson.scripts) .filter((script) => script.startsWith("start:") && script !== "start:all") .map((script) => { const serverName = script.replace("start:", ""); const scriptPath = packageJson.scripts[script]; const jsPath = scriptPath.replace("node ", ""); return { serverName, scriptPath, jsPath, }; }); // Create script for each server serverScripts.forEach((server) => { const scriptName = `cursor-${server.serverName}-server.sh`; const scriptPath = path.join(projectRoot, scriptName); const scriptContent = `#!/bin/bash # Script to run the ${server.serverName} MCP server for Cursor IDE cd "$(dirname "$0")" # Ensure the TypeScript code is built npm run build # Run the server with node node ${server.jsPath} `; fs.writeFileSync(scriptPath, scriptContent); console.log(`Created ${scriptName}`); }); // Make all scripts executable serverScripts.forEach((server) => { const scriptName = `cursor-${server.serverName}-server.sh`; const scriptPath = path.join(projectRoot, scriptName); fs.chmodSync(scriptPath, "755"); }); // Generate workspace path (used for absolute paths in instructions) const workspacePath = process.cwd(); // Print instructions console.log("\n===== CURSOR IDE SETUP INSTRUCTIONS =====\n"); console.log("To use these MCP servers in Cursor IDE:"); console.log("1. Open Cursor IDE"); console.log("2. Go to Cursor Settings > Features > Model Context Protocol"); console.log("3. Edit the mcp.json file or paste this configuration:"); console.log("4. Add the following to your mcp.json file under mcpServers:\n"); console.log("```json"); console.log('"mcpServers": {'); serverScripts.forEach((server, index) => { const scriptName = `cursor-${server.serverName}-server.sh`; const absoluteScriptPath = path.join(workspacePath, scriptName); const isLast = index === serverScripts.length - 1; console.log(` "${server.serverName}": {`); console.log(` "command": "${absoluteScriptPath}",`); console.log(` "args": []`); console.log(` }${isLast ? "" : ","}`); }); console.log("}"); console.log("```\n"); console.log( "After adding these configurations, restart Cursor IDE and the MCP servers" ); console.log("will be available for use in the composer.\n"); } // Run the function createServerScripts(); ``` -------------------------------------------------------------------------------- /src/servers/lease-pdf-server/pdf-server.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { PdfProcessor } from "./pdf-processor.js"; // Create an MCP server const server = new McpServer({ name: "PDF Server", version: "1.0.0", }); const pdfProcessor = new PdfProcessor(); // Tool 1: Read PDF - extracts text and form field data server.tool( "read_pdf", { input: z.string().describe("PDF file path or base64 encoded PDF content"), }, async ({ input }) => { try { const result = await pdfProcessor.readPdf(input); if (!result.success) { return { content: [ { type: "text" as const, text: `Error reading PDF: ${result.error}`, }, ], isError: true, }; } const response = { success: true, pageCount: result.content?.pageCount || 0, text: result.content?.text || "", formFields: result.content?.formFields || {}, hasFormFields: !!result.content?.formFields && Object.keys(result.content.formFields).length > 0, }; return { content: [ { type: "text" as const, text: "PDF read successfully" }, { type: "text" as const, text: JSON.stringify(response, null, 2) }, ], }; } catch (error) { console.error("Error in read_pdf handler:", error); return { content: [ { type: "text" as const, text: `Error reading PDF: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } } ); // Tool 2: Write PDF - creates or modifies PDF with new content server.tool( "write_pdf", { content: z .object({ text: z.string().optional().describe("Text content to add to PDF"), formFields: z .record(z.string()) .optional() .describe("Form fields to update as key-value pairs"), }) .describe("Content to write to PDF"), templatePdf: z .string() .optional() .describe("Template PDF file path or base64 content to modify"), outputPath: z .string() .optional() .describe("Output file path (if not provided, returns base64)"), }, async ({ content, templatePdf, outputPath }) => { try { const writeRequest = { content: content, templatePdf: templatePdf, }; const result = await pdfProcessor.writePdf(writeRequest, outputPath); if (!result.success) { return { content: [ { type: "text" as const, text: `Error writing PDF: ${result.error}`, }, ], isError: true, }; } const response = { success: true, outputPath: result.outputPath, hasBase64: !!result.base64, base64Length: result.base64 ? result.base64.length : 0, }; const responseContent = [ { type: "text" as const, text: "PDF written successfully" }, { type: "text" as const, text: JSON.stringify(response, null, 2) }, ]; // Include base64 data if no output path was specified if (result.base64 && !outputPath) { responseContent.push({ type: "text" as const, text: `Base64 PDF Data:\ndata:application/pdf;base64,${result.base64}`, }); } return { content: responseContent, }; } catch (error) { console.error("Error in write_pdf handler:", error); return { content: [ { type: "text" as const, text: `Error writing PDF: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } } ); // Handle termination signals process.on("SIGINT", () => { console.log("Received SIGINT signal. Shutting down..."); process.exit(0); }); process.on("SIGTERM", () => { console.log("Received SIGTERM signal. Shutting down..."); process.exit(0); }); // Start the server async function startServer() { try { const transport = new StdioServerTransport(); await server.connect(transport); console.log("PDF Server started and ready to process requests"); } catch (error) { console.error("Failed to start server:", error); process.exit(1); } } // Start the server if this file is run directly if (process.argv[1] === new URL(import.meta.url).pathname) { startServer(); } export default server; ``` -------------------------------------------------------------------------------- /src/servers/kubernetes-server/kubernetes-api.ts: -------------------------------------------------------------------------------- ```typescript import * as k8s from "@kubernetes/client-node"; import { spawn } from "child_process"; import { config } from "dotenv"; // Load environment variables config(); // Set up Kubernetes client const kc = new k8s.KubeConfig(); kc.loadFromDefault(); // This will load from ~/.kube/config by default // Get the API clients const k8sApi = kc.makeApiClient(k8s.CoreV1Api); const exec = new k8s.Exec(kc); // Type definitions export interface PodList { kind: string | undefined; apiVersion: string | undefined; metadata: any; items: any[]; // Changed from Pod[] to any[] to handle V1Pod[] } export interface Pod { metadata: { name: string; namespace: string; [key: string]: any; }; spec: any; status: { phase: string; conditions: any[]; containerStatuses: any[]; [key: string]: any; }; [key: string]: any; } export interface ExecResult { stdout: string; stderr: string; exitCode: number | null; } /** * Get pods in a namespace with optional label and field selectors * @param namespace Kubernetes namespace * @param labelSelector Label selector to filter pods * @param fieldSelector Field selector to filter pods */ export async function getPods( namespace: string = "local", labelSelector?: string, fieldSelector?: string ): Promise<PodList> { try { const response = await k8sApi.listNamespacedPod( namespace, undefined, // pretty undefined, // allowWatchBookmarks undefined, // _continue undefined, // fieldSelector fieldSelector, undefined, // includeUninitialized labelSelector, undefined, // limit undefined, // resourceVersion undefined, // resourceVersionMatch undefined, // timeoutSeconds undefined // watch ); return { kind: response.body.kind, apiVersion: response.body.apiVersion, metadata: response.body.metadata, items: response.body.items, }; } catch (error) { console.error(`Error getting pods in namespace ${namespace}:`, error); throw error; } } /** * Find pods by name pattern * @param namePattern Pod name pattern * @param namespace Kubernetes namespace */ export async function findPodsByName( namePattern: string, namespace: string = "local" ): Promise<PodList> { try { // Get all pods in the namespace const allPods = await getPods(namespace); // Filter pods by name pattern // Convert * wildcard to JavaScript RegExp const regexPattern = new RegExp( "^" + namePattern.replace(/\*/g, ".*") + "$" ); const filteredItems = allPods.items.filter((pod) => regexPattern.test(pod.metadata?.name || "") ); return { kind: allPods.kind, apiVersion: allPods.apiVersion, metadata: allPods.metadata, items: filteredItems, }; } catch (error) { console.error( `Error finding pods by name pattern ${namePattern} in namespace ${namespace}:`, error ); throw error; } } /** * Delete a pod * @param podName Pod name * @param namespace Kubernetes namespace * @param gracePeriodSeconds Grace period in seconds before force deletion */ export async function deletePod( podName: string, namespace: string = "local", gracePeriodSeconds?: number ): Promise<any> { try { // Create delete options const deleteOptions = new k8s.V1DeleteOptions(); if (gracePeriodSeconds !== undefined) { deleteOptions.gracePeriodSeconds = gracePeriodSeconds; } const response = await k8sApi.deleteNamespacedPod( podName, namespace, undefined, // pretty undefined, // dryRun gracePeriodSeconds, // gracePeriodSeconds undefined, // orphanDependents undefined, // propagationPolicy deleteOptions ); return response.body; } catch (error) { console.error( `Error deleting pod ${podName} in namespace ${namespace}:`, error ); throw error; } } /** * Execute a command in a pod * @param podName Pod name * @param command Command to execute (will be split by space) * @param namespace Kubernetes namespace * @param containerName Container name (if pod has multiple containers) */ export async function execCommandInPod( podName: string, command: string, namespace: string = "local", containerName?: string ): Promise<ExecResult> { try { // First, get the pod to find container name if not provided if (!containerName) { const pod = await k8sApi.readNamespacedPod(podName, namespace); // Use first container as default if it exists if ( pod.body.spec && pod.body.spec.containers && pod.body.spec.containers.length > 0 ) { containerName = pod.body.spec.containers[0].name; } else { throw new Error(`No containers found in pod ${podName}`); } } // We'll use the kubectl exec command for reliability // Parse the command into array of arguments const cmd = command.split(/\s+/); return new Promise((resolve, reject) => { // Execute kubectl command const kubectl = spawn("kubectl", [ "exec", "-n", namespace, podName, ...(containerName ? ["-c", containerName] : []), "--", ...cmd, ]); let stdout = ""; let stderr = ""; kubectl.stdout.on("data", (data) => { stdout += data.toString(); }); kubectl.stderr.on("data", (data) => { stderr += data.toString(); }); kubectl.on("close", (code) => { resolve({ stdout, stderr, exitCode: code, }); }); kubectl.on("error", (err) => { reject(err); }); }); } catch (error) { console.error( `Error executing command in pod ${podName} in namespace ${namespace}:`, error ); throw error; } } /** * Get logs from a pod * @param podName Pod name * @param namespace Kubernetes namespace * @param containerName Container name (if pod has multiple containers) * @param tailLines Number of lines to fetch from the end * @param previous Get logs from previous terminated container instance */ export async function getPodLogs( podName: string, namespace: string = "local", containerName?: string, tailLines?: number, previous?: boolean ): Promise<string> { try { // First, get the pod to find container name if not provided if (!containerName) { const pod = await k8sApi.readNamespacedPod(podName, namespace); // Use first container as default if it exists if ( pod.body.spec && pod.body.spec.containers && pod.body.spec.containers.length > 0 ) { containerName = pod.body.spec.containers[0].name; } else { throw new Error(`No containers found in pod ${podName}`); } } const response = await k8sApi.readNamespacedPodLog( podName, namespace, containerName, undefined, // follow undefined, // insecureSkipTLSVerifyBackend undefined, // pretty previous ? "true" : undefined, // previous - convert boolean to string undefined, // sinceSeconds undefined, // sinceTime tailLines, // tailLines undefined // timestamps ); return response.body; } catch (error) { console.error( `Error getting logs from pod ${podName} in namespace ${namespace}:`, error ); throw error; } } ``` -------------------------------------------------------------------------------- /src/servers/kubernetes-server/kubernetes-server.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { config } from "dotenv"; import * as K8sAPI from "./kubernetes-api.js"; // Load environment variables config(); // Create an MCP server const server = new McpServer({ name: "Kubernetes Server", version: "1.0.0", }); // Get pods tool server.tool( "get_pods", { namespace: z .string() .optional() .describe("Kubernetes namespace (default: local)"), label_selector: z .string() .optional() .describe("Label selector to filter pods (e.g. 'app=myapp')"), field_selector: z .string() .optional() .describe("Field selector to filter pods (e.g. 'status.phase=Running')"), }, async ({ namespace = "local", label_selector, field_selector }) => { try { const pods = await K8sAPI.getPods( namespace, label_selector, field_selector ); return { content: [ { type: "text", text: `Found ${pods.items.length} pods in namespace '${namespace}'.`, }, { type: "text", text: JSON.stringify(pods, null, 2), }, ], }; } catch (error) { console.error("Error in get_pods handler:", error); return { content: [ { type: "text", text: `Error fetching pods: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } } ); // Find pods tool server.tool( "find_pods", { name_pattern: z .string() .describe( "Pod name pattern to search for (supports wildcards, e.g. 'nginx*')" ), namespace: z .string() .optional() .describe("Kubernetes namespace (default: local)"), }, async ({ name_pattern, namespace = "local" }) => { try { const pods = await K8sAPI.findPodsByName(name_pattern, namespace); return { content: [ { type: "text", text: `Found ${pods.items.length} pods matching '${name_pattern}' in namespace '${namespace}'.`, }, { type: "text", text: JSON.stringify(pods, null, 2), }, ], }; } catch (error) { console.error("Error in find_pods handler:", error); return { content: [ { type: "text", text: `Error finding pods: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } } ); // Kill pod tool server.tool( "kill_pod", { pod_name: z.string().describe("Name of the pod to delete"), namespace: z .string() .optional() .describe("Kubernetes namespace (default: local)"), grace_period_seconds: z .number() .optional() .describe("Grace period in seconds before force deletion"), }, async ({ pod_name, namespace = "local", grace_period_seconds }) => { try { const result = await K8sAPI.deletePod( pod_name, namespace, grace_period_seconds ); return { content: [ { type: "text", text: `Successfully deleted pod '${pod_name}' in namespace '${namespace}'.`, }, { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { console.error("Error in kill_pod handler:", error); return { content: [ { type: "text", text: `Error deleting pod: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } } ); // Execute command in pod tool server.tool( "exec_in_pod", { pod_name: z.string().describe("Name of the pod"), command: z.string().describe("Command to execute (e.g. 'ls -la')"), container_name: z .string() .optional() .describe("Container name (if pod has multiple containers)"), namespace: z .string() .optional() .describe("Kubernetes namespace (default: local)"), }, async ({ pod_name, command, container_name, namespace = "local" }) => { try { const result = await K8sAPI.execCommandInPod( pod_name, command, namespace, container_name ); return { content: [ { type: "text", text: `Command execution results from pod '${pod_name}' in namespace '${namespace}':`, }, { type: "text", text: result.stdout, }, { type: "text", text: result.stderr ? `Error output: ${result.stderr}` : "", }, ], }; } catch (error) { console.error("Error in exec_in_pod handler:", error); return { content: [ { type: "text", text: `Error executing command in pod: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } } ); // Get pod logs tool server.tool( "get_pod_logs", { pod_name: z.string().describe("Name of the pod"), container_name: z .string() .optional() .describe("Container name (if pod has multiple containers)"), namespace: z .string() .optional() .describe("Kubernetes namespace (default: local)"), tail_lines: z .number() .optional() .describe("Number of lines to fetch from the end"), previous: z .boolean() .optional() .describe("Get logs from previous terminated container instance"), }, async ({ pod_name, container_name, namespace = "local", tail_lines, previous, }) => { try { const logs = await K8sAPI.getPodLogs( pod_name, namespace, container_name, tail_lines, previous ); return { content: [ { type: "text", text: `Logs from pod '${pod_name}' in namespace '${namespace}':`, }, { type: "text", text: logs, }, ], }; } catch (error) { console.error("Error in get_pod_logs handler:", error); return { content: [ { type: "text", text: `Error getting pod logs: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } } ); // Handle termination signals process.on("SIGINT", () => { console.log("Received SIGINT signal. Shutting down..."); process.exit(0); }); process.on("SIGTERM", () => { console.log("Received SIGTERM signal. Shutting down..."); process.exit(0); }); // Start the server async function startServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.log("Kubernetes Server started and ready to process requests"); } // Start the server if this file is run directly if (import.meta.url === new URL(import.meta.url).href) { startServer(); } export default server; ``` -------------------------------------------------------------------------------- /src/servers/lease-pdf-server/pdf-processor.ts: -------------------------------------------------------------------------------- ```typescript import { PDFDocument, PDFForm, PDFTextField, StandardFonts } from "pdf-lib"; import * as fs from "fs"; import * as path from "path"; // Fix for pdf-parse ENOENT error - import directly from lib to avoid debug code // @ts-ignore - pdf-parse lib doesn't have proper types import * as pdfParse from "pdf-parse/lib/pdf-parse.js"; import { PdfReadResult, PdfWriteRequest, PdfWriteResult } from "./types.js"; export class PdfProcessor { /** * Custom page render function for better text extraction */ private renderPage(pageData: any) { // Enhanced render options for better text extraction const renderOptions = { // Normalize whitespace to standard spaces normalizeWhitespace: true, // Don't combine text items to preserve structure disableCombineTextItems: true, }; return pageData.getTextContent(renderOptions).then((textContent: any) => { let lastY: number | undefined; let text = ""; // Process each text item for (const item of textContent.items) { // Add line breaks when Y position changes significantly if (lastY !== undefined && Math.abs(lastY - item.transform[5]) > 1) { text += "\n"; } // Add the text content text += item.str; // Add space if this item doesn't end the line and next item is on same line if (item.hasEOL === false) { text += " "; } lastY = item.transform[5]; } return text; }); } /** * Read PDF content - extracts text and form fields with enhanced options */ async readPdf(input: string): Promise<PdfReadResult> { try { let pdfBuffer: Buffer; // Handle input as file path or base64 if (input.startsWith("data:application/pdf;base64,")) { const base64Data = input.split(",")[1]; pdfBuffer = Buffer.from(base64Data, "base64"); } else if (input.length > 500) { // Assume it's base64 without data URL prefix pdfBuffer = Buffer.from(input, "base64"); } else { // Assume it's a file path if (!fs.existsSync(input)) { return { success: false, error: `File not found: ${input}` }; } pdfBuffer = fs.readFileSync(input); } // Enhanced parsing options for better text extraction const options = { // Parse all pages (0 = all pages) max: 0, // Use our custom page render function pagerender: this.renderPage, // Use a valid PDF.js version for better compatibility version: "v1.10.100" as const, }; console.log(`Starting PDF parsing with enhanced options...`); // Extract text content with enhanced options - use the fixed import const pdfData = await (pdfParse as any).default(pdfBuffer, options); console.log(`PDF parsing completed: - Total pages: ${pdfData.numpages} - Pages rendered: ${pdfData.numrender || "N/A"} - Text length: ${pdfData.text.length} characters - First 200 chars: ${pdfData.text.substring(0, 200)}...`); // Extract form fields using pdf-lib const pdfDoc = await PDFDocument.load(pdfBuffer); const form = pdfDoc.getForm(); const formFields: Record<string, string> = {}; try { const fields = form.getFields(); console.log(`Found ${fields.length} form fields`); fields.forEach((field: any) => { const fieldName = field.getName(); if (field instanceof PDFTextField) { formFields[fieldName] = field.getText() || ""; } else { // Handle other field types as needed formFields[fieldName] = field.toString(); } }); } catch (error) { // PDF might not have form fields, that's okay console.log("No form fields found or error reading form fields"); } return { success: true, content: { text: pdfData.text, formFields: Object.keys(formFields).length > 0 ? formFields : undefined, pageCount: pdfData.numpages, // Add additional metadata for debugging metadata: { numrender: pdfData.numrender, info: pdfData.info, textLength: pdfData.text.length, }, }, }; } catch (error) { console.error("PDF parsing error:", error); return { success: false, error: `Error reading PDF: ${ error instanceof Error ? error.message : String(error) }`, }; } } /** * Write/Create PDF - creates new PDF or modifies existing one */ async writePdf( request: PdfWriteRequest, outputPath?: string ): Promise<PdfWriteResult> { try { let pdfDoc: PDFDocument; // If template PDF provided, load it; otherwise create new document if (request.templatePdf) { let templateBuffer: Buffer; if (request.templatePdf.startsWith("data:application/pdf;base64,")) { const base64Data = request.templatePdf.split(",")[1]; templateBuffer = Buffer.from(base64Data, "base64"); } else if (request.templatePdf.length > 500) { templateBuffer = Buffer.from(request.templatePdf, "base64"); } else { if (!fs.existsSync(request.templatePdf)) { return { success: false, error: `Template file not found: ${request.templatePdf}`, }; } templateBuffer = fs.readFileSync(request.templatePdf); } pdfDoc = await PDFDocument.load(templateBuffer); } else { pdfDoc = await PDFDocument.create(); } // Update form fields if provided if (request.content.formFields) { try { const form = pdfDoc.getForm(); Object.entries(request.content.formFields).forEach( ([fieldName, value]) => { try { const field = form.getTextField(fieldName); field.setText(value); } catch (error) { console.log(`Could not update field ${fieldName}: ${error}`); } } ); } catch (error) { console.log("Error updating form fields:", error); } } // Add text content if provided and no template (multi-page text PDF) if (request.content.text && !request.templatePdf) { await this.addMultiPageText(pdfDoc, request.content.text); } // Generate PDF bytes const pdfBytes = await pdfDoc.save(); // Save to file if output path provided if (outputPath) { const dir = path.dirname(outputPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(outputPath, pdfBytes); return { success: true, outputPath: outputPath, }; } else { // Return as base64 const base64 = Buffer.from(pdfBytes).toString("base64"); return { success: true, base64: base64, }; } } catch (error) { return { success: false, error: `Error writing PDF: ${ error instanceof Error ? error.message : String(error) }`, }; } } /** * Add multi-page text to a PDF document with proper pagination */ private async addMultiPageText( pdfDoc: PDFDocument, text: string ): Promise<void> { const font = await pdfDoc.embedFont(StandardFonts.Helvetica); const fontSize = 12; const lineHeight = fontSize * 1.5; // 1.5 line spacing for readability const pageMargin = 50; // Standard US Letter page dimensions const pageWidth = 612; const pageHeight = 792; const textWidth = pageWidth - pageMargin * 2; const textHeight = pageHeight - pageMargin * 2; const linesPerPage = Math.floor(textHeight / lineHeight); console.log(`Multi-page text setup: - Font size: ${fontSize} - Line height: ${lineHeight} - Lines per page: ${linesPerPage} - Text width: ${textWidth} - Text height: ${textHeight} - Page margins: ${pageMargin}`); // Split text into lines, preserving existing line breaks const lines = text.split("\n"); console.log(`Total lines to process: ${lines.length}`); let currentPage = pdfDoc.addPage([pageWidth, pageHeight]); let currentLineOnPage = 0; let pageCount = 1; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Check if we need a new page BEFORE processing the line if (currentLineOnPage >= linesPerPage) { console.log( `Creating new page at line ${i}, current line on page: ${currentLineOnPage}` ); currentPage = pdfDoc.addPage([pageWidth, pageHeight]); currentLineOnPage = 0; pageCount++; console.log(`Created page ${pageCount}`); } // Calculate Y position (top to bottom) const yPosition = pageHeight - pageMargin - currentLineOnPage * lineHeight; // Draw the line (simplified - no word wrapping for now) const textToDraw = line || " "; // Handle empty lines console.log( `Drawing line ${i + 1} on page ${pageCount}, line ${ currentLineOnPage + 1 }: "${textToDraw.substring(0, 50)}..."` ); currentPage.drawText(textToDraw, { x: pageMargin, y: yPosition, font: font, size: fontSize, maxWidth: textWidth, }); currentLineOnPage++; } console.log( `Multi-page text complete: ${pageCount} pages created, ${lines.length} lines processed` ); } } ``` -------------------------------------------------------------------------------- /examples/sdk-readme.md: -------------------------------------------------------------------------------- ```markdown # MCP TypeScript SDK   ## Table of Contents - [Overview](#overview) - [Installation](#installation) - [Quickstart](#quickstart) - [What is MCP?](#what-is-mcp) - [Core Concepts](#core-concepts) - [Server](#server) - [Resources](#resources) - [Tools](#tools) - [Prompts](#prompts) - [Running Your Server](#running-your-server) - [stdio](#stdio) - [HTTP with SSE](#http-with-sse) - [Testing and Debugging](#testing-and-debugging) - [Examples](#examples) - [Echo Server](#echo-server) - [SQLite Explorer](#sqlite-explorer) - [Advanced Usage](#advanced-usage) - [Low-Level Server](#low-level-server) - [Writing MCP Clients](#writing-mcp-clients) - [Server Capabilities](#server-capabilities) ## Overview 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: - Build MCP clients that can connect to any MCP server - Create MCP servers that expose resources, prompts and tools - Use standard transports like stdio and SSE - Handle all MCP protocol messages and lifecycle events ## Installation ```bash npm install @modelcontextprotocol/sdk ``` ## Quick Start Let's create a simple MCP server that exposes a calculator tool and some data: ```typescript import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; // Create an MCP server const server = new McpServer({ name: "Demo", version: "1.0.0", }); // Add a multiplication tool server.tool("multiply", { a: z.number(), b: z.number() }, async ({ a, b }) => ({ content: [{ type: "text", text: String(a * b) }], })); // Add a dynamic greeting resource server.resource( "greeting", new ResourceTemplate("greeting://{name}", { list: undefined }), async (uri, { name }) => ({ contents: [ { uri: uri.href, text: `Hello, ${name}!`, }, ], }) ); // Start receiving messages on stdin and sending messages on stdout const transport = new StdioServerTransport(); await server.connect(transport); ``` ## What is MCP? 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: - Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context) - Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect) - Define interaction patterns through **Prompts** (reusable templates for LLM interactions) - And more! ## Core Concepts ### Server The McpServer is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing: ```typescript const server = new McpServer({ name: "My App", version: "1.0.0", }); ``` ### Resources 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: ```typescript // Static resource server.resource("config", "config://app", async (uri) => ({ contents: [ { uri: uri.href, text: "App configuration here", }, ], })); // Dynamic resource with parameters server.resource( "user-profile", new ResourceTemplate("users://{userId}/profile", { list: undefined }), async (uri, { userId }) => ({ contents: [ { uri: uri.href, text: `Profile data for user ${userId}`, }, ], }) ); ``` ### Tools Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects: ```typescript // Simple tool with parameters server.tool( "calculate-bmi", { weightKg: z.number(), heightM: z.number(), }, async ({ weightKg, heightM }) => ({ content: [ { type: "text", text: String(weightKg / (heightM * heightM)), }, ], }) ); // Async tool with external API call server.tool("fetch-weather", { city: z.string() }, async ({ city }) => { const response = await fetch(`https://api.weather.com/${city}`); const data = await response.text(); return { content: [{ type: "text", text: data }], }; }); ``` ### Prompts Prompts are reusable templates that help LLMs interact with your server effectively: ```typescript server.prompt("review-code", { code: z.string() }, ({ code }) => ({ messages: [ { role: "user", content: { type: "text", text: `Please review this code:\n\n${code}`, }, }, ], })); ``` ## Running Your Server 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: ### stdio For command-line tools and direct integrations: ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; const server = new McpServer({ name: "example-server", version: "1.0.0", }); // ... set up server resources, tools, and prompts ... const transport = new StdioServerTransport(); await server.connect(transport); ``` ### HTTP with SSE 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: ```typescript import express from "express"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; const server = new McpServer({ name: "example-server", version: "1.0.0", }); // ... set up server resources, tools, and prompts ... const app = express(); app.get("/sse", async (req, res) => { const transport = new SSEServerTransport("/messages", res); await server.connect(transport); }); app.post("/messages", async (req, res) => { // Note: to support multiple simultaneous connections, these messages will // need to be routed to a specific matching transport. (This logic isn't // implemented here, for simplicity.) await transport.handlePostMessage(req, res); }); app.listen(3001); ``` ### Testing and Debugging To test your server, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). See its README for more information. ## Examples ### Echo Server A simple server demonstrating resources, tools, and prompts: ```typescript import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; const server = new McpServer({ name: "Echo", version: "1.0.0", }); server.resource( "echo", new ResourceTemplate("echo://{message}", { list: undefined }), async (uri, { message }) => ({ contents: [ { uri: uri.href, text: `Resource echo: ${message}`, }, ], }) ); server.tool("echo", { message: z.string() }, async ({ message }) => ({ content: [{ type: "text", text: `Tool echo: ${message}` }], })); server.prompt("echo", { message: z.string() }, ({ message }) => ({ messages: [ { role: "user", content: { type: "text", text: `Please process this message: ${message}`, }, }, ], })); ``` ### SQLite Explorer A more complex example showing database integration: ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import sqlite3 from "sqlite3"; import { promisify } from "util"; import { z } from "zod"; const server = new McpServer({ name: "SQLite Explorer", version: "1.0.0", }); // Helper to create DB connection const getDb = () => { const db = new sqlite3.Database("database.db"); return { all: promisify<string, any[]>(db.all.bind(db)), close: promisify(db.close.bind(db)), }; }; server.resource("schema", "schema://main", async (uri) => { const db = getDb(); try { const tables = await db.all( "SELECT sql FROM sqlite_master WHERE type='table'" ); return { contents: [ { uri: uri.href, text: tables.map((t: { sql: string }) => t.sql).join("\n"), }, ], }; } finally { await db.close(); } }); server.tool("query", { sql: z.string() }, async ({ sql }) => { const db = getDb(); try { const results = await db.all(sql); return { content: [ { type: "text", text: JSON.stringify(results, null, 2), }, ], }; } catch (err: unknown) { const error = err as Error; return { content: [ { type: "text", text: `Error: ${error.message}`, }, ], isError: true, }; } finally { await db.close(); } }); ``` ## Advanced Usage ### Low-Level Server For more control, you can use the low-level Server class directly: ```typescript import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; const server = new Server( { name: "example-server", version: "1.0.0", }, { capabilities: { prompts: {}, }, } ); server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: [ { name: "example-prompt", description: "An example prompt template", arguments: [ { name: "arg1", description: "Example argument", required: true, }, ], }, ], }; }); server.setRequestHandler(GetPromptRequestSchema, async (request) => { if (request.params.name !== "example-prompt") { throw new Error("Unknown prompt"); } return { description: "Example prompt", messages: [ { role: "user", content: { type: "text", text: "Example prompt text", }, }, ], }; }); const transport = new StdioServerTransport(); await server.connect(transport); ``` ### Writing MCP Clients The SDK provides a high-level client interface: ```typescript import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; const transport = new StdioClientTransport({ command: "node", args: ["server.js"], }); const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: { prompts: {}, resources: {}, tools: {}, }, } ); await client.connect(transport); // List prompts const prompts = await client.listPrompts(); // Get a prompt const prompt = await client.getPrompt("example-prompt", { arg1: "value", }); // List resources const resources = await client.listResources(); // Read a resource const resource = await client.readResource("file:///example.txt"); // Call a tool const result = await client.callTool({ name: "example-tool", arguments: { arg1: "value", }, }); ``` ## Documentation - [Model Context Protocol documentation](https://modelcontextprotocol.io) - [MCP Specification](https://spec.modelcontextprotocol.io) - [Example Servers](https://github.com/modelcontextprotocol/servers) ## Contributing Issues and pull requests are welcome on GitHub at https://github.com/modelcontextprotocol/typescript-sdk. ## License This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details. ``` -------------------------------------------------------------------------------- /src/servers/postgres-server/postgres-server.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { config } from "dotenv"; import pkg from "pg"; const { Pool } = pkg; // Load environment variables config(); // Flag to track if we're in demo mode (no real DB connection) let demoMode = false; // Define interfaces for our demo data interface DemoUser { id: number; username: string; email: string; created_at: string; } interface DemoProduct { id: number; name: string; price: number; created_at: string; } interface DemoResult { rows: any[]; rowCount: number; } // Create the PostgreSQL pool with a connection timeout const pool = new Pool({ host: process.env.POSTGRES_HOST || "localhost", port: parseInt(process.env.POSTGRES_PORT || "5432"), database: process.env.POSTGRES_DB || "postgres", user: process.env.POSTGRES_USER || "postgres", password: process.env.POSTGRES_PASSWORD || "", ssl: process.env.POSTGRES_SSL_MODE === "require" ? { rejectUnauthorized: false } : undefined, max: parseInt(process.env.POSTGRES_MAX_CONNECTIONS || "10"), idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000, // 5 second timeout }); // Add error handler pool.on("error", (err) => { console.error("Unexpected error on idle PostgreSQL client", err); }); // Create an MCP server const server = new McpServer({ name: "PostgreSQL Server", version: "1.0.0", }); // Get database info tool server.tool("mcp__get_database_info", {}, async () => { if (demoMode) { return { content: [ { type: "text", text: "Running in demo mode - no actual PostgreSQL connection.", }, { type: "text", text: JSON.stringify( { database_name: "demo_database", current_user: "demo_user", postgresql_version: "PostgreSQL 14.0 (Demo Version)", }, null, 2 ), }, ], }; } try { const result = await pool.query(` SELECT current_database() as database_name, current_user as current_user, version() as postgresql_version `); return { content: [ { type: "text", text: `Connected to database: ${result.rows[0].database_name} as ${result.rows[0].current_user}`, }, { type: "text", text: JSON.stringify(result.rows[0], null, 2), }, ], }; } catch (error) { console.error("Error getting database info:", error); return { content: [ { type: "text", text: `Error getting database info: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } }); // List tables tool server.tool("mcp__list_tables", {}, async () => { if (demoMode) { return { content: [ { type: "text", text: "Running in demo mode - showing sample tables.", }, { type: "text", text: JSON.stringify( [ { table_name: "users", table_schema: "public", table_type: "BASE TABLE", }, { table_name: "products", table_schema: "public", table_type: "BASE TABLE", }, { table_name: "orders", table_schema: "public", table_type: "BASE TABLE", }, ], null, 2 ), }, ], }; } try { const result = await pool.query(` SELECT table_name, table_schema, table_type FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog', 'information_schema') ORDER BY table_schema, table_name `); return { content: [ { type: "text", text: `Found ${result.rows.length} tables.`, }, { type: "text", text: JSON.stringify(result.rows, null, 2), }, ], }; } catch (error) { console.error("Error listing tables:", error); return { content: [ { type: "text", text: `Error listing tables: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } }); // Get table structure tool server.tool( "mcp__get_table_structure", { table_name: z.string().describe("The name of the table to examine"), }, async ({ table_name }) => { if (demoMode) { // Return different demo data based on the requested table let columns = []; if (table_name === "users") { columns = [ { column_name: "id", data_type: "integer", is_nullable: "NO", column_default: "nextval('users_id_seq'::regclass)", }, { column_name: "username", data_type: "character varying", is_nullable: "NO", column_default: null, }, { column_name: "email", data_type: "character varying", is_nullable: "NO", column_default: null, }, { column_name: "created_at", data_type: "timestamp", is_nullable: "NO", column_default: "CURRENT_TIMESTAMP", }, ]; } else if (table_name === "products") { columns = [ { column_name: "id", data_type: "integer", is_nullable: "NO", column_default: "nextval('products_id_seq'::regclass)", }, { column_name: "name", data_type: "character varying", is_nullable: "NO", column_default: null, }, { column_name: "price", data_type: "numeric", is_nullable: "NO", column_default: null, }, { column_name: "created_at", data_type: "timestamp", is_nullable: "NO", column_default: "CURRENT_TIMESTAMP", }, ]; } else { columns = [ { column_name: "id", data_type: "integer", is_nullable: "NO", column_default: "nextval('table_id_seq'::regclass)", }, { column_name: "name", data_type: "character varying", is_nullable: "NO", column_default: null, }, { column_name: "created_at", data_type: "timestamp", is_nullable: "NO", column_default: "CURRENT_TIMESTAMP", }, ]; } return { content: [ { type: "text", text: `Demo structure for table ${table_name}: ${columns.length} columns found.`, }, { type: "text", text: JSON.stringify(columns, null, 2), }, ], }; } try { const result = await pool.query( ` SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = $1 ORDER BY ordinal_position `, [table_name] ); return { content: [ { type: "text", text: `Structure for table ${table_name}: ${result.rows.length} columns found.`, }, { type: "text", text: JSON.stringify(result.rows, null, 2), }, ], }; } catch (error) { console.error(`Error getting structure for table ${table_name}:`, error); return { content: [ { type: "text", text: `Error getting table structure: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } } ); // Execute query tool server.tool( "mcp__execute_query", { query: z.string().describe("The SQL query to execute"), params: z .array(z.union([z.string(), z.number(), z.boolean(), z.null()])) .optional() .describe("Optional parameters for the query"), }, async ({ query, params }) => { if (demoMode) { // In demo mode, generate some fake results based on the query const lowerQuery = query.toLowerCase(); let demoResult: DemoResult = { rows: [], rowCount: 0 }; if (lowerQuery.includes("select") && lowerQuery.includes("users")) { const users: DemoUser[] = [ { id: 1, username: "john_doe", email: "[email protected]", created_at: "2023-01-01T00:00:00Z", }, { id: 2, username: "jane_smith", email: "[email protected]", created_at: "2023-01-02T00:00:00Z", }, ]; demoResult.rows = users; demoResult.rowCount = 2; } else if ( lowerQuery.includes("select") && lowerQuery.includes("products") ) { const products: DemoProduct[] = [ { id: 1, name: "Product A", price: 19.99, created_at: "2023-01-01T00:00:00Z", }, { id: 2, name: "Product B", price: 29.99, created_at: "2023-01-02T00:00:00Z", }, { id: 3, name: "Product C", price: 39.99, created_at: "2023-01-03T00:00:00Z", }, ]; demoResult.rows = products; demoResult.rowCount = 3; } else if ( lowerQuery.includes("insert") || lowerQuery.includes("update") || lowerQuery.includes("delete") ) { demoResult.rowCount = 1; } return { content: [ { type: "text", text: `Demo query executed successfully. Rows affected: ${demoResult.rowCount}`, }, { type: "text", text: JSON.stringify( { rows: demoResult.rows, rowCount: demoResult.rowCount, fields: demoResult.rows.length > 0 ? Object.keys(demoResult.rows[0]).map((name) => ({ name, dataTypeID: 0, })) : [], }, null, 2 ), }, ], }; } try { const result = await pool.query(query, params); const resultData = { rows: result.rows, rowCount: result.rowCount, fields: result.fields ? result.fields.map((f) => ({ name: f.name, dataTypeID: f.dataTypeID, })) : [], }; return { content: [ { type: "text", text: `Query executed successfully. Rows affected: ${result.rowCount}`, }, { type: "text", text: JSON.stringify(resultData, null, 2), }, ], }; } catch (error) { console.error("Error executing query:", error); return { content: [ { type: "text", text: `Error executing query: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } } ); // Handle termination signals process.on("SIGINT", async () => { console.log("Received SIGINT signal. Shutting down..."); if (!demoMode) { await pool.end(); console.log("PostgreSQL connection pool closed"); } process.exit(0); }); process.on("SIGTERM", async () => { console.log("Received SIGTERM signal. Shutting down..."); if (!demoMode) { await pool.end(); console.log("PostgreSQL connection pool closed"); } process.exit(0); }); // Start the server async function startServer() { try { // Validate environment variables, but don't error out if missing const requiredEnvVars = [ "POSTGRES_DB", "POSTGRES_USER", "POSTGRES_PASSWORD", ]; const missingEnvVars = requiredEnvVars.filter( (envVar) => !process.env[envVar] ); if (missingEnvVars.length > 0) { console.warn( `Warning: Missing environment variables: ${missingEnvVars.join(", ")}` ); console.warn( "The server will run in demo mode without connecting to a real database." ); demoMode = true; } else { // Test the connection try { const client = await pool.connect(); console.log("Successfully connected to PostgreSQL"); client.release(); } catch (error) { console.warn("Failed to connect to PostgreSQL:", error); console.warn( "The server will run in demo mode without a real database connection." ); demoMode = true; } } // Start receiving messages on stdin and sending messages on stdout const transport = new StdioServerTransport(); await server.connect(transport); console.log( `PostgreSQL Server started in ${ demoMode ? "DEMO" : "PRODUCTION" } mode and ready to process requests` ); } catch (error) { console.error("Failed to start server:", error); process.exit(1); } } // Start the server if this file is run directly if (process.argv[1] === new URL(import.meta.url).pathname) { startServer(); } export default server; ``` -------------------------------------------------------------------------------- /MCP_SERVER_DEVELOPMENT_GUIDE.md: -------------------------------------------------------------------------------- ```markdown # MCP Server Development Guide This guide provides comprehensive rules and best practices for building Model Context Protocol (MCP) servers, based on analysis of existing implementations in this project. ## Table of Contents 1. [Core Architecture Patterns](#core-architecture-patterns) 2. [Project Structure Requirements](#project-structure-requirements) 3. [Implementation Patterns](#implementation-patterns) 4. [Tool Development Guidelines](#tool-development-guidelines) 5. [Configuration and Environment](#configuration-and-environment) 6. [Error Handling Standards](#error-handling-standards) 7. [Documentation Requirements](#documentation-requirements) 8. [Testing and Deployment](#testing-and-deployment) 9. [Integration and Automation](#integration-and-automation) 10. [Advanced Patterns](#advanced-patterns) ## Core Architecture Patterns ### 1. MCP Server Foundation Every MCP server MUST follow this core structure: ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; // Create server instance const server = new McpServer({ name: "Your Server Name", version: "1.0.0", }); // Define tools using zod for validation server.tool( "tool_name", { param1: z.string().describe("Parameter description"), param2: z.number().optional().describe("Optional parameter"), }, async ({ param1, param2 }) => { // Tool implementation return { content: [ { type: "text", text: "Human-readable response" }, { type: "text", text: JSON.stringify(data, null, 2) }, ], }; } ); // Server lifecycle management async function startServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.log("Server started and ready to process requests"); } // Export for testing and import export default server; // Start if run directly if (process.argv[1] === new URL(import.meta.url).pathname) { startServer(); } ``` ### 2. Response Format Standards **CRITICAL**: MCP servers must return responses in exact format: ```typescript // Standard successful response return { content: [ { type: "text", text: "Human-readable summary" }, { type: "text", text: JSON.stringify(structuredData, null, 2) }, ], }; // Error response return { content: [{ type: "text", text: `Error: ${errorMessage}` }], isError: true, }; ``` **DO NOT**: - Use unsupported content types like `type: "json"` - Return raw objects without text wrapper - Mix different response formats ## Project Structure Requirements ### 1. Directory Structure ``` src/servers/your-server/ ├── index.ts # Server entry point ├── your-server.ts # Main server implementation ├── your-server-api.ts # External API/logic layer (if needed) ├── types.ts # TypeScript type definitions ├── README.md # Server-specific documentation └── test/ # Server-specific tests ``` ### 2. Naming Conventions - **Server directory**: `kebab-case` (e.g., `postgres-server`, `pdf-server`) - **Main file**: `{server-name}.ts` (e.g., `postgres-server.ts`) - **Tool names**: `snake_case` with optional prefix (e.g., `mcp__get_data`, `execute_command`) - **Parameters**: `snake_case` (e.g., `table_name`, `namespace`) ### 3. File Requirements **Every server MUST have**: - Main server file with export default - TypeScript types file - README.md with tool documentation - Entry in `src/index.ts` and `src/run-all.ts` ## Implementation Patterns ### 1. Parameter Validation with Zod ```typescript // Required parameter param_name: z.string().describe("Clear description of what this parameter does"), // Optional parameter with default namespace: z.string().optional().describe("Kubernetes namespace (default: local)"), // Complex object parameter content: z.object({ text: z.string().optional(), formFields: z.record(z.string()).optional(), }).describe("Content structure"), // Array parameter params: z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])) .optional() .describe("Query parameters"), ``` ### 2. Configuration Management **Environment Variables Pattern**: ```typescript import { config } from "dotenv"; config(); // Load .env file // Define configuration with defaults const config = { host: process.env.SERVICE_HOST || "localhost", port: parseInt(process.env.SERVICE_PORT || "5432"), database: process.env.SERVICE_DB || "default_db", maxConnections: parseInt(process.env.SERVICE_MAX_CONNECTIONS || "10"), sslMode: process.env.SERVICE_SSL_MODE === "require", }; ``` **Demo Mode Pattern** (for servers that require external services): ```typescript let demoMode = false; // Check for required environment variables const requiredEnvVars = ["SERVICE_DB", "SERVICE_USER", "SERVICE_PASSWORD"]; const missingEnvVars = requiredEnvVars.filter((envVar) => !process.env[envVar]); if (missingEnvVars.length > 0) { console.warn(`Missing environment variables: ${missingEnvVars.join(", ")}`); console.warn("Running in demo mode with mock data."); demoMode = true; } ``` ### 3. External Service Integration **Separate API Layer Pattern**: ```typescript // your-server-api.ts export class YourServiceAPI { async getData(params): Promise<ResultType> { // External service logic } } // your-server.ts import * as YourAPI from "./your-server-api.js"; server.tool("get_data", schema, async (params) => { try { const result = await YourAPI.getData(params); return formatResponse(result); } catch (error) { return handleError(error); } }); ``` ## Tool Development Guidelines ### 1. Tool Naming Strategy **Categories of tools**: - **Read operations**: `get_*`, `list_*`, `find_*` - **Write operations**: `create_*`, `update_*`, `delete_*`, `execute_*` - **Utility operations**: `convert_*`, `parse_*`, `validate_*` **Examples**: - `mcp__get_database_info` (namespaced with mcp\_\_) - `get_pods` (generic name) - `execute_query` (action-oriented) ### 2. Parameter Design **Best Practices**: - Use descriptive parameter names - Provide clear descriptions for all parameters - Use optional parameters with sensible defaults - Group related parameters into objects - Validate parameter combinations in the handler ```typescript // Good parameter design server.tool( "execute_command", { pod_name: z .string() .describe("Name of the pod where command will be executed"), command: z.string().describe("Shell command to execute (e.g., 'ls -la')"), container_name: z .string() .optional() .describe("Container name (required for multi-container pods)"), namespace: z .string() .optional() .describe("Kubernetes namespace (default: 'local')"), timeout_seconds: z .number() .optional() .describe("Command timeout in seconds (default: 30)"), }, async ({ pod_name, command, container_name, namespace = "local", timeout_seconds = 30, }) => { // Implementation } ); ``` ### 3. Response Design **Multi-part responses for complex data**: ```typescript return { content: [ { type: "text", text: `Successfully processed ${items.length} items from ${source}`, }, { type: "text", text: JSON.stringify( { summary: { total: items.length, processed: results.length }, results: results, metadata: { timestamp: new Date().toISOString() }, }, null, 2 ), }, ], }; ``` ## Configuration and Environment ### 1. Environment Variable Standards **Naming Convention**: `{SERVICE}__{SETTING}` (uppercase with double underscore) ```bash # Database configuration POSTGRES__HOST=localhost POSTGRES__PORT=5432 POSTGRES__DATABASE=mydb POSTGRES__USER=user POSTGRES__PASSWORD=pass POSTGRES__SSL_MODE=require POSTGRES__MAX_CONNECTIONS=10 # Service-specific settings KUBERNETES__KUBECONFIG=/path/to/config KUBERNETES__DEFAULT_NAMESPACE=local KUBERNETES__API_TIMEOUT=30000 ``` ### 2. Configuration Validation ```typescript interface ServerConfig { host: string; port: number; database: string; user: string; password: string; sslMode?: boolean; maxConnections?: number; } function validateConfig(): ServerConfig { const config: ServerConfig = { host: process.env.POSTGRES__HOST || "localhost", port: parseInt(process.env.POSTGRES__PORT || "5432"), database: process.env.POSTGRES__DATABASE!, user: process.env.POSTGRES__USER!, password: process.env.POSTGRES__PASSWORD!, sslMode: process.env.POSTGRES__SSL_MODE === "require", maxConnections: parseInt(process.env.POSTGRES__MAX_CONNECTIONS || "10"), }; // Validate required fields const required = ["database", "user", "password"]; const missing = required.filter((key) => !config[key as keyof ServerConfig]); if (missing.length > 0) { throw new Error(`Missing required configuration: ${missing.join(", ")}`); } return config; } ``` ## Error Handling Standards ### 1. Error Response Pattern ```typescript async function handleToolCall<T>( operation: () => Promise<T>, operationName: string ): Promise<ToolResponse> { try { const result = await operation(); return { content: [ { type: "text", text: `${operationName} completed successfully` }, { type: "text", text: JSON.stringify(result, null, 2) }, ], }; } catch (error) { console.error(`Error in ${operationName}:`, error); return { content: [ { type: "text", text: `Error in ${operationName}: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } } ``` ### 2. Service-Specific Error Handling ```typescript // Database connection errors try { const result = await pool.query(query, params); return formatSuccessResponse(result); } catch (error) { if (error.code === "23505") { return formatErrorResponse("Duplicate key violation"); } else if (error.code === "ECONNREFUSED") { return formatErrorResponse("Database connection refused"); } return formatErrorResponse(`Database error: ${error.message}`); } ``` ### 3. Graceful Degradation ```typescript // Fallback to demo mode when external service unavailable if (demoMode) { return { content: [ { type: "text", text: "Running in demo mode - showing sample data" }, { type: "text", text: JSON.stringify(mockData, null, 2) }, ], }; } ``` ## Documentation Requirements ### 1. Server README Structure Every server MUST have a README.md with: ````markdown # [Server Name] MCP Server Brief description of what the server does. ## Features - Feature 1: Description - Feature 2: Description ## Tools ### `tool_name` Description of what the tool does. **Parameters:** - `param1` (string): Description - `param2` (number, optional): Description with default **Returns:** - Success status - Data description - Any additional fields **Example Usage:** [Provide example of how the tool would be used] ## Configuration Environment variables required: - `SERVICE_HOST`: Description (default: localhost) - `SERVICE_PORT`: Description (default: 5432) ## Dependencies - dependency1: Purpose - dependency2: Purpose ## Running the Server ```bash # Development npm run dev:servername # Production npm run start:servername ``` ```` ```` ### 2. Tool Documentation Standards ```typescript // In-code documentation server.tool( "descriptive_tool_name", { // Parameter descriptions must be clear and include examples where helpful table_name: z.string().describe("Name of the database table to query (e.g., 'users', 'products')"), limit: z.number().optional().describe("Maximum number of rows to return (default: 100, max: 1000)"), filters: z.record(z.string()).optional().describe("Column filters as key-value pairs (e.g., {'status': 'active'})"), }, async ({ table_name, limit = 100, filters }) => { // Implementation with clear success/error paths } ); ```` ## Testing and Deployment ### 1. Package.json Integration **Required Scripts**: ```json { "scripts": { "dev:yourserver": "NODE_OPTIONS=\"--loader ts-node/esm\" node src/servers/your-server/your-server.ts", "start:yourserver": "node dist/src/servers/your-server/your-server.js" } } ``` ### 2. TypeScript Configuration Must work with the project's `tsconfig.json`: ```json { "compilerOptions": { "target": "ES2020", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "strict": true, "outDir": "dist" } } ``` ### 3. Process Lifecycle Management **Required signal handlers**: ```typescript // Handle graceful shutdown process.on("SIGINT", async () => { console.log("Received SIGINT signal. Shutting down..."); // Clean up resources (close connections, etc.) await cleanup(); process.exit(0); }); process.on("SIGTERM", async () => { console.log("Received SIGTERM signal. Shutting down..."); await cleanup(); process.exit(0); }); ``` ## Integration and Automation ### 1. Server Registry Updates When adding a new server, update these files: **src/index.ts**: ```typescript const servers = [ // ... existing servers { name: "your-server", displayName: "Your Server Name", path: join(__dirname, "servers/your-server/your-server.ts"), }, ]; ``` **src/run-all.ts**: ```typescript const servers = [ // ... existing servers { name: "Your Server Name", path: join(__dirname, "servers/your-server/your-server.ts"), }, ]; ``` ### 2. Cursor IDE Integration The setup script automatically generates: - Shell scripts for each server (`cursor-{server}-server.sh`) - MCP configuration instructions - Absolute paths for Cursor IDE **Manual verification**: ```bash npm run setup # Verify your server appears in the generated scripts and instructions ``` ### 3. Development Workflow ```bash # 1. Create server structure mkdir -p src/servers/your-server # 2. Implement server files # - your-server.ts (main implementation) # - types.ts (TypeScript definitions) # - README.md (documentation) # 3. Register server # - Add to src/index.ts # - Add to src/run-all.ts # - Add npm scripts to package.json # 4. Test development mode npm run dev -- your-server # 5. Test production build npm run build npm run start:your-server # 6. Generate Cursor integration npm run setup # 7. Test in Cursor IDE # - Add server to MCP configuration # - Test tools in Cursor composer ``` ## Advanced Patterns ### 1. Dependency Injection ```typescript // For testable, modular servers export class YourServer { constructor( private config: ServerConfig, private apiClient: ExternalAPIClient, private logger: Logger = console ) {} async initialize(): Promise<McpServer> { const server = new McpServer({ name: this.config.name, version: this.config.version, }); this.registerTools(server); return server; } private registerTools(server: McpServer): void { server.tool("tool_name", schema, this.handleToolCall.bind(this)); } } ``` ### 2. Connection Pooling and Resource Management ```typescript // For servers that manage persistent connections export class ConnectionManager { private pool: ConnectionPool; constructor(config: PoolConfig) { this.pool = new ConnectionPool(config); this.setupCleanup(); } private setupCleanup(): void { const cleanup = async () => { await this.pool.end(); console.log("Connection pool closed"); }; process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); process.on("exit", cleanup); } } ``` ### 3. Tool Composition ```typescript // For servers with complex tool interactions export class CompositeToolHandler { async handleComplexOperation(params: ComplexParams) { // Break down complex operations into smaller, reusable pieces const step1Result = await this.executeStep1(params.step1Params); const step2Result = await this.executeStep2( step1Result, params.step2Params ); const finalResult = await this.combineResults(step1Result, step2Result); return this.formatResponse(finalResult); } } ``` ### 4. Validation and Sanitization ```typescript // Advanced parameter validation const paramSchema = z.object({ query: z .string() .min(1, "Query cannot be empty") .max(10000, "Query too long") .refine( (query) => !query.toLowerCase().includes("drop table"), "Destructive operations not allowed" ), params: z .array(z.union([z.string(), z.number(), z.boolean(), z.null()])) .max(100, "Too many parameters") .optional(), }); ``` ## Summary Checklist When building a new MCP server, ensure: **Core Requirements**: - [ ] Uses MCP SDK with proper server initialization - [ ] Implements StdioServerTransport for Cursor compatibility - [ ] Uses Zod for parameter validation - [ ] Returns properly formatted responses - [ ] Handles errors gracefully with isError flag **Project Integration**: - [ ] Follows directory structure conventions - [ ] Registered in src/index.ts and src/run-all.ts - [ ] Has appropriate npm scripts in package.json - [ ] Includes comprehensive README.md - [ ] Defines TypeScript types **Production Readiness**: - [ ] Handles process termination signals - [ ] Manages external resources properly - [ ] Supports configuration via environment variables - [ ] Includes demo/fallback mode for development - [ ] Provides clear error messages **Documentation**: - [ ] README with features, tools, and configuration - [ ] Clear parameter descriptions in tool definitions - [ ] Usage examples and configuration guide - [ ] Dependencies and setup instructions **Testing**: - [ ] Works in development mode (npm run dev) - [ ] Builds and runs in production mode - [ ] Integrates with Cursor IDE setup process - [ ] Validates all tools with expected parameters This guide ensures consistency, maintainability, and proper integration with the Cursor IDE ecosystem. ```