This is page 1 of 2. Use http://codebase.md/stefanraath3/mcp-supabase?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitignore ├── examples │ └── claude-example.md ├── LICENSE ├── mcp-docs.txt ├── mcp-typescript-readme.txt ├── package-lock.json ├── package.json ├── README.md └── src ├── db.js ├── http-server.js ├── index.js ├── public │ └── index.html └── server.js ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Environment variables 2 | .env 3 | .env.local 4 | .env.* 5 | 6 | # Node.js 7 | node_modules/ 8 | npm-debug.log 9 | yarn-debug.log 10 | yarn-error.log 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Build directories 23 | dist/ 24 | build/ 25 | 26 | # Dependency directories 27 | jspm_packages/ 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional eslint cache 33 | .eslintcache 34 | 35 | # Mac-specific files 36 | .DS_Store 37 | 38 | # Editor directories and files 39 | .idea/ 40 | .vscode/ 41 | *.swp 42 | *.swo 43 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Supabase MCP Server 2 | 3 | An MCP server that connects to a Supabase PostgreSQL database, exposing table schemas as resources and providing tools for data analysis. 4 | 5 | ## Features 6 | 7 | - Connection to Supabase PostgreSQL database 8 | - Table schemas exposed as resources 9 | - Read-only SQL query tools 10 | - Prompts for common data analysis tasks 11 | 12 | ## Setup 13 | 14 | 1. Clone this repository 15 | 2. Install dependencies: 16 | ``` 17 | npm install 18 | ``` 19 | 3. Copy `.env.example` to `.env` and update with your Supabase credentials: 20 | ``` 21 | cp .env.example .env 22 | ``` 23 | 4. Edit the `.env` file with your actual Supabase connection details 24 | 25 | ## Running the Server 26 | 27 | ### Using stdio (command line integration) 28 | 29 | ``` 30 | npm start 31 | ``` 32 | 33 | ### Using HTTP with SSE (for web integration) 34 | 35 | ``` 36 | npm run start:http 37 | ``` 38 | 39 | ## Using with MCP Clients 40 | 41 | This server can be used with any MCP-compatible client, including Claude.app and the MCP Inspector for testing. 42 | 43 | ### Available Resources 44 | 45 | - `schema://tables` - Lists all tables in the database 46 | - `schema://table/{tableName}` - Shows schema for a specific table 47 | 48 | ### Available Tools 49 | 50 | - `query` - Runs a read-only SQL query against the database 51 | - `analyze-table` - Gets basic statistics about a table 52 | - `find-related-tables` - Discovers tables related to a given table 53 | 54 | ### Available Prompts 55 | 56 | - `table-exploration` - Guides exploration of a specific table 57 | - `data-summary` - Creates a summary of data in a table 58 | - `relationship-analysis` - Analyzes relationships between tables 59 | ``` -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- ```javascript 1 | const server = require("./server"); 2 | const { 3 | StdioServerTransport, 4 | } = require("@modelcontextprotocol/sdk/server/stdio.js"); 5 | 6 | async function main() { 7 | try { 8 | console.error("Starting Supabase MCP Server with stdio transport..."); 9 | 10 | // Create stdio transport 11 | const transport = new StdioServerTransport(); 12 | 13 | // Connect server to transport 14 | await server.connect(transport); 15 | 16 | console.error("Server connected and ready. Awaiting messages..."); 17 | } catch (error) { 18 | console.error("Error starting server:", error); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "supabase-mcp-server", 3 | "version": "1.0.0", 4 | "description": "MCP server that connects to Supabase PostgreSQL database", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "node src/index.js", 8 | "start:http": "node src/http-server.js", 9 | "dev": "nodemon src/index.js", 10 | "dev:http": "nodemon src/http-server.js", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [ 14 | "mcp", 15 | "supabase", 16 | "postgresql", 17 | "llm", 18 | "ai" 19 | ], 20 | "author": "", 21 | "license": "ISC", 22 | "dependencies": { 23 | "@modelcontextprotocol/sdk": "^1.6.1", 24 | "body-parser": "^1.20.2", 25 | "cors": "^2.8.5", 26 | "dotenv": "^16.3.1", 27 | "express": "^4.18.2", 28 | "pg": "^8.11.3", 29 | "zod": "^3.22.4" 30 | }, 31 | "devDependencies": { 32 | "nodemon": "^3.0.2" 33 | } 34 | } 35 | ``` -------------------------------------------------------------------------------- /examples/claude-example.md: -------------------------------------------------------------------------------- ```markdown 1 | # Using the Supabase MCP Server with Claude 2 | 3 | This guide shows how to use your Supabase MCP Server with Claude through Claude.app. 4 | 5 | ## Prerequisites 6 | 7 | 1. The Supabase MCP Server running 8 | 2. A Claude.app account with API access 9 | 3. Your database configuration properly set up 10 | 11 | ## Steps to Connect 12 | 13 | 1. Start your MCP server using one of the following methods: 14 | 15 | **For command-line integration (stdio):** 16 | 17 | ```bash 18 | npm start 19 | ``` 20 | 21 | **For HTTP/SSE (recommended for Claude.app):** 22 | 23 | ```bash 24 | npm run start:http 25 | ``` 26 | 27 | 2. Once your server is running with HTTP/SSE on port 3000, you can access it at: 28 | 29 | ``` 30 | http://localhost:3000 31 | ``` 32 | 33 | 3. In Claude.app, use the "Connect to MCP Server" feature (when available) to connect to your local server. 34 | 35 | ## Example Prompts for Claude 36 | 37 | Once connected to your MCP server, you can use prompts like these with Claude: 38 | 39 | ### Exploring Database Structure 40 | 41 | ``` 42 | Please use the connected MCP server to: 43 | 1. List all tables in the database 44 | 2. Show me the schema for the "users" table (or any table you identify in step 1) 45 | 3. Identify any relationships between tables 46 | ``` 47 | 48 | ### Running SQL Queries 49 | 50 | ``` 51 | Using the MCP server, please: 52 | 1. Show me the schema for the "products" table 53 | 2. Run a query to get the top 5 most expensive products 54 | 3. Find any tables related to the products table 55 | 4. Explain the relationships you found 56 | ``` 57 | 58 | ### Data Analysis 59 | 60 | ``` 61 | I'd like to analyze the "orders" table in our database. Using the MCP server: 62 | 1. Get basic statistics about the table 63 | 2. Find the most recent orders 64 | 3. Calculate the average order value 65 | 4. Identify any trends or patterns in the data 66 | ``` 67 | 68 | ### Using Prompts 69 | 70 | ``` 71 | Please use the "data-summary" prompt with the table name "customers" to provide a comprehensive overview of our customer data. 72 | ``` 73 | 74 | ## Tips for Effective Use 75 | 76 | 1. **Start with exploration**: First ask Claude to list tables and explore schemas before running specific queries. 77 | 78 | 2. **Use read-only queries**: Remember that the MCP server only allows read-only queries for security reasons. 79 | 80 | 3. **Leverage relationships**: Ask Claude to find and explain relationships between tables to better understand the database structure. 81 | 82 | 4. **Use the built-in prompts**: The MCP server includes specialized prompts for table exploration, data summaries, and relationship analysis. 83 | 84 | 5. **Combine with Claude's capabilities**: Claude can not only retrieve data but also analyze it, visualize it (through descriptions), and make recommendations based on the findings. 85 | 86 | ## Troubleshooting 87 | 88 | If you encounter issues: 89 | 90 | 1. Check that your server is running and accessible 91 | 2. Verify your database connection credentials in the `.env` file 92 | 3. Ensure you have the correct permissions for your Supabase database 93 | 4. Check the server logs for any error messages 94 | 95 | ## Next Steps 96 | 97 | - Customize the server by adding more specialized tools or prompts 98 | - Deploy the server to a public endpoint for remote access 99 | - Integrate with other MCP clients besides Claude 100 | ``` -------------------------------------------------------------------------------- /src/http-server.js: -------------------------------------------------------------------------------- ```javascript 1 | // Load environment variables from .env file 2 | require("dotenv").config(); 3 | 4 | const express = require("express"); 5 | const cors = require("cors"); 6 | const bodyParser = require("body-parser"); 7 | const server = require("./server"); 8 | const { 9 | SSEServerTransport, 10 | } = require("@modelcontextprotocol/sdk/server/sse.js"); 11 | 12 | // Use a simple implementation that avoids the complex SSE logic 13 | async function startHttpServer(port = 3000) { 14 | // Test database connection before starting server 15 | try { 16 | console.log("Initializing database connection..."); 17 | const db = require("./db"); 18 | const result = await db.query("SELECT NOW()"); 19 | console.log("Database connection successful at", result.rows[0].now); 20 | } catch (error) { 21 | console.error("Failed to connect to database:", error); 22 | console.log( 23 | "Starting server anyway, but database functionality will be limited" 24 | ); 25 | } 26 | 27 | // Create Express app 28 | const app = express(); 29 | 30 | // Enable CORS 31 | app.use(cors()); 32 | 33 | // Use JSON parser for all non-webhook routes 34 | app.use(bodyParser.json()); 35 | 36 | // Serve static files from the public directory 37 | app.use(express.static("public")); 38 | 39 | // Simple health check endpoint 40 | app.get("/health", (req, res) => { 41 | res.json({ status: "ok", message: "MCP Server is running" }); 42 | }); 43 | 44 | // Alternative approach to test basic database connectivity 45 | app.get("/test-db", async (req, res) => { 46 | try { 47 | // Include connection parameters in response (without password) 48 | const connectionParams = { 49 | host: process.env.PGHOST, 50 | port: process.env.PGPORT, 51 | database: process.env.PGDATABASE, 52 | user: process.env.PGUSER, 53 | }; 54 | 55 | console.log("Testing database connection with params:", connectionParams); 56 | 57 | const db = require("./db"); 58 | const result = await db.query("SELECT NOW()"); 59 | const tables = await db.listTables(); 60 | 61 | res.json({ 62 | status: "ok", 63 | message: "Database connection successful", 64 | timestamp: result.rows[0].now, 65 | connectionParams, 66 | tables, 67 | }); 68 | } catch (error) { 69 | console.error("Database test failed:", error); 70 | 71 | res.status(500).json({ 72 | status: "error", 73 | message: "Database connection failed", 74 | connectionParams: { 75 | host: process.env.PGHOST, 76 | port: process.env.PGPORT, 77 | database: process.env.PGDATABASE, 78 | user: process.env.PGUSER, 79 | }, 80 | error: error.message, 81 | stack: error.stack, 82 | }); 83 | } 84 | }); 85 | 86 | // Start the server 87 | const httpServer = app.listen(port, () => { 88 | console.log(`HTTP server started on port ${port}`); 89 | console.log(`Health check available at http://localhost:${port}/health`); 90 | console.log(`Database test available at http://localhost:${port}/test-db`); 91 | }); 92 | 93 | return httpServer; 94 | } 95 | 96 | // Start the HTTP server if this file is run directly 97 | if (require.main === module) { 98 | const port = process.env.PORT || 3000; 99 | startHttpServer(port).catch((error) => { 100 | console.error("Error starting HTTP server:", error); 101 | process.exit(1); 102 | }); 103 | } 104 | 105 | module.exports = { startHttpServer }; 106 | ``` -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- ```javascript 1 | require("dotenv").config(); 2 | const { 3 | McpServer, 4 | ResourceTemplate, 5 | } = require("@modelcontextprotocol/sdk/server/mcp.js"); 6 | const { z } = require("zod"); 7 | const db = require("./db"); 8 | 9 | // Create MCP server instance 10 | const server = new McpServer({ 11 | name: process.env.SERVER_NAME || "Supabase MCP Server", 12 | version: process.env.SERVER_VERSION || "1.0.0", 13 | }); 14 | 15 | // RESOURCES 16 | 17 | // Resource to list all tables in the database 18 | server.resource("tables-list", "schema://tables", async (uri) => { 19 | try { 20 | const tables = await db.listTables(); 21 | return { 22 | contents: [ 23 | { 24 | uri: uri.href, 25 | text: `Database Tables:\n\n${tables 26 | .map((table) => `- ${table}`) 27 | .join("\n")}`, 28 | }, 29 | ], 30 | }; 31 | } catch (error) { 32 | return { 33 | contents: [ 34 | { 35 | uri: uri.href, 36 | text: `Error retrieving tables: ${error.message}`, 37 | }, 38 | ], 39 | isError: true, 40 | }; 41 | } 42 | }); 43 | 44 | // Resource to get schema for a specific table 45 | server.resource( 46 | "table-schema", 47 | new ResourceTemplate("schema://table/{tableName}", { 48 | list: "schema://tables", 49 | }), 50 | async (uri, { tableName }) => { 51 | try { 52 | const schema = await db.getTableSchema(tableName); 53 | 54 | // Format the schema information in a readable way 55 | const columnsText = schema.columns 56 | .map( 57 | (col) => 58 | `- ${col.column_name} (${col.data_type})${ 59 | col.is_nullable === "YES" ? " NULL" : " NOT NULL" 60 | }${ 61 | schema.primaryKeys.includes(col.column_name) ? " PRIMARY KEY" : "" 62 | }` 63 | ) 64 | .join("\n"); 65 | 66 | const fkText = 67 | schema.foreignKeys.length > 0 68 | ? `\nForeign Keys:\n${schema.foreignKeys 69 | .map( 70 | (fk) => 71 | `- ${fk.column_name} -> ${fk.foreign_table_name}.${fk.foreign_column_name}` 72 | ) 73 | .join("\n")}` 74 | : "\nNo Foreign Keys"; 75 | 76 | const indicesText = 77 | schema.indices.length > 0 78 | ? `\nIndices:\n${schema.indices 79 | .map((idx) => `- ${idx.indexname}: ${idx.indexdef}`) 80 | .join("\n")}` 81 | : "\nNo Indices"; 82 | 83 | return { 84 | contents: [ 85 | { 86 | uri: uri.href, 87 | text: `Table: ${tableName}\n\nColumns:\n${columnsText}${fkText}${indicesText}`, 88 | }, 89 | ], 90 | }; 91 | } catch (error) { 92 | return { 93 | contents: [ 94 | { 95 | uri: uri.href, 96 | text: `Error retrieving schema for table ${tableName}: ${error.message}`, 97 | }, 98 | ], 99 | isError: true, 100 | }; 101 | } 102 | } 103 | ); 104 | 105 | // TOOLS 106 | 107 | // Tool to run a read-only SQL query 108 | server.tool("query", { sql: z.string() }, async ({ sql }) => { 109 | try { 110 | const result = await db.executeReadOnlyQuery(sql); 111 | 112 | // Format the result as a table 113 | let formattedResult = ""; 114 | 115 | if (result.rows.length > 0) { 116 | // Get column names from the first row 117 | const columns = Object.keys(result.rows[0]); 118 | 119 | // Create header row with column names 120 | formattedResult += columns.join(" | ") + "\n"; 121 | formattedResult += columns.map(() => "---").join(" | ") + "\n"; 122 | 123 | // Add data rows 124 | result.rows.forEach((row) => { 125 | formattedResult += 126 | columns 127 | .map((col) => { 128 | const value = row[col]; 129 | return value === null ? "NULL" : String(value); 130 | }) 131 | .join(" | ") + "\n"; 132 | }); 133 | 134 | formattedResult += `\n${result.rows.length} rows returned`; 135 | } else { 136 | formattedResult = "Query returned no results"; 137 | } 138 | 139 | return { 140 | content: [ 141 | { 142 | type: "text", 143 | text: formattedResult, 144 | }, 145 | ], 146 | }; 147 | } catch (error) { 148 | return { 149 | content: [ 150 | { 151 | type: "text", 152 | text: `Error executing query: ${error.message}`, 153 | }, 154 | ], 155 | isError: true, 156 | }; 157 | } 158 | }); 159 | 160 | // Tool to analyze a table and get basic statistics 161 | server.tool( 162 | "analyze-table", 163 | { tableName: z.string() }, 164 | async ({ tableName }) => { 165 | try { 166 | const stats = await db.analyzeTable(tableName); 167 | 168 | // Format the statistics as text 169 | let formattedStats = `Table: ${tableName}\n\n`; 170 | formattedStats += `Row Count: ${stats.rowCount}\n\n`; 171 | formattedStats += "Column Statistics:\n"; 172 | 173 | // Header 174 | formattedStats += "Column | Type | Null Count | Null %\n"; 175 | formattedStats += "--- | --- | --- | ---\n"; 176 | 177 | // Rows 178 | stats.columnStats.forEach((col) => { 179 | formattedStats += `${col.column_name} | ${col.data_type} | ${col.null_count} | ${col.null_percentage}\n`; 180 | }); 181 | 182 | return { 183 | content: [ 184 | { 185 | type: "text", 186 | text: formattedStats, 187 | }, 188 | ], 189 | }; 190 | } catch (error) { 191 | return { 192 | content: [ 193 | { 194 | type: "text", 195 | text: `Error analyzing table ${tableName}: ${error.message}`, 196 | }, 197 | ], 198 | isError: true, 199 | }; 200 | } 201 | } 202 | ); 203 | 204 | // Tool to find related tables 205 | server.tool( 206 | "find-related-tables", 207 | { tableName: z.string() }, 208 | async ({ tableName }) => { 209 | try { 210 | const relatedTables = await db.findRelatedTables(tableName); 211 | 212 | let formattedRelationships = `Relationships for Table: ${tableName}\n\n`; 213 | 214 | if (relatedTables.relationships.length === 0) { 215 | formattedRelationships += "No relationships found"; 216 | } else { 217 | // Group by relationship type for better organization 218 | const outgoing = relatedTables.relationships.filter( 219 | (r) => r.relationship_type === "outgoing" 220 | ); 221 | const incoming = relatedTables.relationships.filter( 222 | (r) => r.relationship_type === "incoming" 223 | ); 224 | 225 | if (outgoing.length > 0) { 226 | formattedRelationships += 227 | "Outgoing Relationships (Tables this table references):\n"; 228 | outgoing.forEach((rel) => { 229 | formattedRelationships += `- ${tableName}.${rel.from_column} -> ${rel.related_table}.${rel.to_column}\n`; 230 | }); 231 | formattedRelationships += "\n"; 232 | } 233 | 234 | if (incoming.length > 0) { 235 | formattedRelationships += 236 | "Incoming Relationships (Tables that reference this table):\n"; 237 | incoming.forEach((rel) => { 238 | formattedRelationships += `- ${rel.related_table}.${rel.from_column} -> ${tableName}.${rel.to_column}\n`; 239 | }); 240 | } 241 | } 242 | 243 | return { 244 | content: [ 245 | { 246 | type: "text", 247 | text: formattedRelationships, 248 | }, 249 | ], 250 | }; 251 | } catch (error) { 252 | return { 253 | content: [ 254 | { 255 | type: "text", 256 | text: `Error finding related tables for ${tableName}: ${error.message}`, 257 | }, 258 | ], 259 | isError: true, 260 | }; 261 | } 262 | } 263 | ); 264 | 265 | // PROMPTS 266 | 267 | // Prompt for table exploration 268 | server.prompt( 269 | "table-exploration", 270 | { tableName: z.string() }, 271 | ({ tableName }) => ({ 272 | messages: [ 273 | { 274 | role: "user", 275 | content: [ 276 | { 277 | type: "text", 278 | text: `I want to explore the "${tableName}" table in our database. Please help me understand: 279 | 280 | 1. What is the schema of this table (column names, types, constraints)? 281 | 2. What relationships does this table have with other tables? 282 | 3. Can you provide some basic statistics about the data in this table? 283 | 4. What are some useful queries I could run to explore this table further? 284 | 285 | Please use the available tools to gather this information and present it in a well-organized way.`, 286 | }, 287 | ], 288 | }, 289 | ], 290 | }) 291 | ); 292 | 293 | // Prompt for data summary 294 | server.prompt( 295 | "data-summary", 296 | { 297 | tableName: z.string(), 298 | limit: z.number().optional(), 299 | }, 300 | ({ tableName, limit = 10 }) => ({ 301 | messages: [ 302 | { 303 | role: "user", 304 | content: [ 305 | { 306 | type: "text", 307 | text: `I need a summary of the data in the "${tableName}" table. Please: 308 | 309 | 1. Get the table schema to understand what we're working with 310 | 2. Analyze the table for basic stats (row count, null values) 311 | 3. Run appropriate queries to show me a sample of ${limit} records 312 | 4. Identify any potential data quality issues 313 | 5. Suggest any insights or patterns that might be useful for further analysis 314 | 315 | Please organize your findings in a clear and concise way.`, 316 | }, 317 | ], 318 | }, 319 | ], 320 | }) 321 | ); 322 | 323 | // Prompt for relationship analysis 324 | server.prompt( 325 | "relationship-analysis", 326 | { tableName: z.string() }, 327 | ({ tableName }) => ({ 328 | messages: [ 329 | { 330 | role: "user", 331 | content: [ 332 | { 333 | type: "text", 334 | text: `I need to understand how the "${tableName}" table relates to other tables in our database. Please: 335 | 336 | 1. Identify all tables that have foreign key relationships with this table 337 | 2. Show both incoming and outgoing relationships 338 | 3. Explain what these relationships mean in a business context 339 | 4. Provide example join queries to demonstrate how to use these relationships 340 | 5. Suggest how these relationships could be used for data analysis 341 | 342 | Please use the appropriate tools to gather this information and present it in a clear, organized way.`, 343 | }, 344 | ], 345 | }, 346 | ], 347 | }) 348 | ); 349 | 350 | module.exports = server; 351 | ``` -------------------------------------------------------------------------------- /src/db.js: -------------------------------------------------------------------------------- ```javascript 1 | // Load environment variables from .env file 2 | require("dotenv").config(); 3 | 4 | const { Pool } = require("pg"); 5 | 6 | // Use the direct connection string from environment variables 7 | const connectionString = 8 | process.env.DB_CONNECTION_STRING || 9 | `postgresql://${process.env.PGUSER}:${process.env.PGPASSWORD}@${process.env.PGHOST}:${process.env.PGPORT}/${process.env.PGDATABASE}`; 10 | 11 | // Log connection attempt (without the password) 12 | const sanitizedConnectionString = connectionString.replace(/:[^:@]+@/, ":***@"); 13 | console.log( 14 | `Attempting to connect to PostgreSQL with: ${sanitizedConnectionString}` 15 | ); 16 | 17 | // Create a connection pool using the connection string 18 | const pool = new Pool({ 19 | connectionString, 20 | ssl: { 21 | rejectUnauthorized: false, // Required for Supabase's self-signed certificates 22 | }, 23 | // Transaction pooler doesn't support prepared statements 24 | // Important: disable prepared statements for Supabase pooler 25 | statement_timeout: 10000, // 10 second statement timeout 26 | connectionTimeoutMillis: 15000, // 15 second connection timeout 27 | query_timeout: 15000, // 15 second query timeout 28 | max: 5, // Maximum number of clients in the pool 29 | }); 30 | 31 | // Add error handler to the pool 32 | pool.on("error", (err) => { 33 | console.error("Unexpected error on idle client", err); 34 | }); 35 | 36 | // Test the connection when the module is loaded 37 | pool.query("SELECT NOW()", (err, res) => { 38 | if (err) { 39 | console.error("Database connection failed:", err.stack); 40 | } else { 41 | console.log("Database connected:", res.rows[0].now); 42 | } 43 | }); 44 | 45 | // Helper function to execute queries with proper error handling 46 | async function query(text, params = [], retries = 3) { 47 | let lastError; 48 | 49 | for (let attempt = 1; attempt <= retries; attempt++) { 50 | try { 51 | // For transaction pooler, we need to use a specific query format 52 | const queryConfig = { 53 | text: text, 54 | values: params, 55 | // Disable prepared statements (critical for transaction pooler compatibility) 56 | name: "", 57 | }; 58 | 59 | const result = await pool.query(queryConfig); 60 | return result; 61 | } catch (error) { 62 | lastError = error; 63 | 64 | // Only retry on connection errors, not on query syntax errors 65 | const isConnectionError = 66 | error.code === "ECONNREFUSED" || 67 | error.code === "ETIMEDOUT" || 68 | error.code === "57P01" || // database admin shutdown 69 | error.code === "57P02" || // crash shutdown 70 | error.code === "57P03" || // cannot connect now 71 | error.message.includes("Connection terminated") || 72 | error.message.includes("connection to server was lost"); 73 | 74 | if (!isConnectionError || attempt === retries) { 75 | console.error( 76 | `Query error (attempt ${attempt}/${retries}):`, 77 | error.message 78 | ); 79 | throw error; 80 | } 81 | 82 | // Wait before retrying (exponential backoff) 83 | const delay = Math.min(100 * Math.pow(2, attempt - 1), 1000); 84 | console.log( 85 | `Connection issue, retrying in ${delay}ms (attempt ${attempt}/${retries})...` 86 | ); 87 | await new Promise((resolve) => setTimeout(resolve, delay)); 88 | } 89 | } 90 | 91 | throw lastError; 92 | } 93 | 94 | // Function to list all tables in the database 95 | async function listTables() { 96 | const sql = ` 97 | SELECT tablename 98 | FROM pg_catalog.pg_tables 99 | WHERE schemaname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') 100 | ORDER BY tablename; 101 | `; 102 | 103 | const result = await query(sql); 104 | return result.rows.map((row) => row.tablename); 105 | } 106 | 107 | // Function to get schema details for a specific table 108 | async function getTableSchema(tableName) { 109 | // Get column information 110 | const columnsSql = ` 111 | SELECT 112 | column_name, 113 | data_type, 114 | is_nullable, 115 | column_default 116 | FROM 117 | information_schema.columns 118 | WHERE 119 | table_name = $1 120 | ORDER BY 121 | ordinal_position; 122 | `; 123 | 124 | // Get primary key information 125 | const pkSql = ` 126 | SELECT 127 | kcu.column_name 128 | FROM 129 | information_schema.table_constraints tc 130 | JOIN information_schema.key_column_usage kcu 131 | ON tc.constraint_name = kcu.constraint_name 132 | WHERE 133 | tc.constraint_type = 'PRIMARY KEY' 134 | AND tc.table_name = $1; 135 | `; 136 | 137 | // Get foreign key information 138 | const fkSql = ` 139 | SELECT 140 | kcu.column_name, 141 | ccu.table_name AS foreign_table_name, 142 | ccu.column_name AS foreign_column_name 143 | FROM 144 | information_schema.table_constraints tc 145 | JOIN information_schema.key_column_usage kcu 146 | ON tc.constraint_name = kcu.constraint_name 147 | JOIN information_schema.constraint_column_usage ccu 148 | ON tc.constraint_name = ccu.constraint_name 149 | WHERE 150 | tc.constraint_type = 'FOREIGN KEY' 151 | AND tc.table_name = $1; 152 | `; 153 | 154 | // Get indices information 155 | const indicesSql = ` 156 | SELECT 157 | indexname, 158 | indexdef 159 | FROM 160 | pg_indexes 161 | WHERE 162 | tablename = $1; 163 | `; 164 | 165 | try { 166 | const [columnsResult, pkResult, fkResult, indicesResult] = 167 | await Promise.all([ 168 | query(columnsSql, [tableName]), 169 | query(pkSql, [tableName]), 170 | query(fkSql, [tableName]), 171 | query(indicesSql, [tableName]), 172 | ]); 173 | 174 | return { 175 | tableName, 176 | columns: columnsResult.rows, 177 | primaryKeys: pkResult.rows.map((row) => row.column_name), 178 | foreignKeys: fkResult.rows, 179 | indices: indicesResult.rows, 180 | }; 181 | } catch (error) { 182 | console.error( 183 | `Error fetching schema for table ${tableName}:`, 184 | error.message 185 | ); 186 | throw error; 187 | } 188 | } 189 | 190 | // Function to get basic statistics about a table 191 | async function analyzeTable(tableName) { 192 | // Get row count 193 | const countSql = `SELECT COUNT(*) as count FROM "${tableName}";`; 194 | 195 | // Get column statistics 196 | const statsSql = ` 197 | SELECT 198 | column_name, 199 | data_type, 200 | ( 201 | SELECT COUNT(*) 202 | FROM "${tableName}" 203 | WHERE "${column_name}" IS NULL 204 | ) AS null_count 205 | FROM 206 | information_schema.columns 207 | WHERE 208 | table_name = $1; 209 | `; 210 | 211 | try { 212 | const [countResult, statsResult] = await Promise.all([ 213 | query(countSql), 214 | query(statsSql, [tableName]), 215 | ]); 216 | 217 | const totalRows = parseInt(countResult.rows[0].count); 218 | 219 | // Add null percentage to each column stat 220 | const columnStats = statsResult.rows.map((col) => ({ 221 | ...col, 222 | null_count: parseInt(col.null_count), 223 | null_percentage: 224 | totalRows > 0 225 | ? ((parseInt(col.null_count) / totalRows) * 100).toFixed(2) + "%" 226 | : "0%", 227 | })); 228 | 229 | return { 230 | tableName, 231 | rowCount: totalRows, 232 | columnStats, 233 | }; 234 | } catch (error) { 235 | console.error(`Error analyzing table ${tableName}:`, error.message); 236 | throw error; 237 | } 238 | } 239 | 240 | // Function to find tables related to a given table (through foreign keys) 241 | async function findRelatedTables(tableName) { 242 | // Tables that this table references (outgoing foreign keys) 243 | const referencedTablesSql = ` 244 | SELECT 245 | ccu.table_name AS related_table, 246 | kcu.column_name AS from_column, 247 | ccu.column_name AS to_column, 248 | 'outgoing' AS relationship_type 249 | FROM 250 | information_schema.table_constraints tc 251 | JOIN information_schema.key_column_usage kcu 252 | ON tc.constraint_name = kcu.constraint_name 253 | JOIN information_schema.constraint_column_usage ccu 254 | ON tc.constraint_name = ccu.constraint_name 255 | WHERE 256 | tc.constraint_type = 'FOREIGN KEY' 257 | AND tc.table_name = $1; 258 | `; 259 | 260 | // Tables that reference this table (incoming foreign keys) 261 | const referencingTablesSql = ` 262 | SELECT 263 | kcu.table_name AS related_table, 264 | kcu.column_name AS from_column, 265 | ccu.column_name AS to_column, 266 | 'incoming' AS relationship_type 267 | FROM 268 | information_schema.table_constraints tc 269 | JOIN information_schema.key_column_usage kcu 270 | ON tc.constraint_name = kcu.constraint_name 271 | JOIN information_schema.constraint_column_usage ccu 272 | ON tc.constraint_name = ccu.constraint_name 273 | WHERE 274 | tc.constraint_type = 'FOREIGN KEY' 275 | AND ccu.table_name = $1; 276 | `; 277 | 278 | try { 279 | const [referencedResult, referencingResult] = await Promise.all([ 280 | query(referencedTablesSql, [tableName]), 281 | query(referencingTablesSql, [tableName]), 282 | ]); 283 | 284 | return { 285 | tableName, 286 | relationships: [...referencedResult.rows, ...referencingResult.rows], 287 | }; 288 | } catch (error) { 289 | console.error( 290 | `Error finding related tables for ${tableName}:`, 291 | error.message 292 | ); 293 | throw error; 294 | } 295 | } 296 | 297 | // Function to execute a read-only query safely 298 | async function executeReadOnlyQuery(sql) { 299 | // Check if the query is attempting to modify data 300 | const normalizedSql = sql.trim().toLowerCase(); 301 | 302 | if ( 303 | normalizedSql.startsWith("insert") || 304 | normalizedSql.startsWith("update") || 305 | normalizedSql.startsWith("delete") || 306 | normalizedSql.startsWith("drop") || 307 | normalizedSql.startsWith("alter") || 308 | normalizedSql.startsWith("create") || 309 | normalizedSql.includes("set schema") 310 | ) { 311 | throw new Error("Only read-only queries are allowed"); 312 | } 313 | 314 | try { 315 | // Execute with a read-only transaction to ensure safety 316 | await query("BEGIN TRANSACTION READ ONLY"); 317 | const result = await query(sql); 318 | await query("COMMIT"); 319 | return result; 320 | } catch (error) { 321 | await query("ROLLBACK"); 322 | throw error; 323 | } 324 | } 325 | 326 | module.exports = { 327 | query, 328 | listTables, 329 | getTableSchema, 330 | analyzeTable, 331 | findRelatedTables, 332 | executeReadOnlyQuery, 333 | }; 334 | ``` -------------------------------------------------------------------------------- /src/public/index.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 | <title>Supabase MCP Server Tester</title> 7 | <style> 8 | body { 9 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 10 | Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 11 | max-width: 1000px; 12 | margin: 0 auto; 13 | padding: 20px; 14 | line-height: 1.6; 15 | } 16 | 17 | h1, 18 | h2, 19 | h3 { 20 | margin-top: 1.5em; 21 | } 22 | 23 | pre { 24 | background-color: #f5f5f5; 25 | padding: 10px; 26 | border-radius: 5px; 27 | overflow-x: auto; 28 | } 29 | 30 | button { 31 | background-color: #4caf50; 32 | border: none; 33 | color: white; 34 | padding: 8px 16px; 35 | text-align: center; 36 | text-decoration: none; 37 | display: inline-block; 38 | font-size: 14px; 39 | margin: 4px 2px; 40 | cursor: pointer; 41 | border-radius: 4px; 42 | } 43 | 44 | input, 45 | select { 46 | padding: 8px; 47 | margin: 5px 0; 48 | border: 1px solid #ddd; 49 | border-radius: 4px; 50 | } 51 | 52 | .response { 53 | margin-top: 20px; 54 | border: 1px solid #ddd; 55 | padding: 10px; 56 | border-radius: 5px; 57 | height: 300px; 58 | overflow-y: auto; 59 | } 60 | 61 | .status { 62 | font-size: 14px; 63 | margin-top: 10px; 64 | font-style: italic; 65 | } 66 | 67 | .connected { 68 | color: green; 69 | } 70 | 71 | .disconnected { 72 | color: red; 73 | } 74 | </style> 75 | </head> 76 | <body> 77 | <h1>Supabase MCP Server Tester</h1> 78 | 79 | <div class="status disconnected" id="status">Disconnected</div> 80 | 81 | <h2>Connection</h2> 82 | <button id="connect">Connect to Server</button> 83 | <button id="disconnect" disabled>Disconnect</button> 84 | 85 | <h2>Resources</h2> 86 | <button id="list-tables">List Tables</button> 87 | <div> 88 | <input type="text" id="table-name" placeholder="Table name" /> 89 | <button id="get-schema">Get Schema</button> 90 | </div> 91 | 92 | <h2>Tools</h2> 93 | <div> 94 | <textarea 95 | id="sql-query" 96 | rows="4" 97 | style="width: 100%" 98 | placeholder="SELECT * FROM your_table LIMIT 10" 99 | ></textarea> 100 | <button id="run-query">Run Query</button> 101 | </div> 102 | 103 | <div> 104 | <input type="text" id="analyze-table-name" placeholder="Table name" /> 105 | <button id="analyze-table">Analyze Table</button> 106 | </div> 107 | 108 | <div> 109 | <input type="text" id="related-table-name" placeholder="Table name" /> 110 | <button id="find-related">Find Related Tables</button> 111 | </div> 112 | 113 | <h2>Prompts</h2> 114 | <div> 115 | <select id="prompt-type"> 116 | <option value="table-exploration">Table Exploration</option> 117 | <option value="data-summary">Data Summary</option> 118 | <option value="relationship-analysis">Relationship Analysis</option> 119 | </select> 120 | <input type="text" id="prompt-table-name" placeholder="Table name" /> 121 | <input 122 | type="number" 123 | id="prompt-limit" 124 | placeholder="Limit (for data summary)" 125 | min="1" 126 | value="10" 127 | /> 128 | <button id="use-prompt">Use Prompt</button> 129 | </div> 130 | 131 | <h2>Response</h2> 132 | <pre class="response" id="response"></pre> 133 | 134 | <script> 135 | let connectionId = null; 136 | let eventSource = null; 137 | let requestId = 1; 138 | 139 | const statusEl = document.getElementById("status"); 140 | const responseEl = document.getElementById("response"); 141 | const connectBtn = document.getElementById("connect"); 142 | const disconnectBtn = document.getElementById("disconnect"); 143 | 144 | // Connect to the server 145 | connectBtn.addEventListener("click", () => { 146 | if (eventSource) { 147 | eventSource.close(); 148 | } 149 | 150 | responseEl.textContent = "Connecting to server..."; 151 | 152 | eventSource = new EventSource("/sse"); 153 | 154 | eventSource.onmessage = (event) => { 155 | const data = JSON.parse(event.data); 156 | 157 | if (data.type === "connection") { 158 | connectionId = data.id; 159 | statusEl.textContent = `Connected (ID: ${connectionId})`; 160 | statusEl.classList.remove("disconnected"); 161 | statusEl.classList.add("connected"); 162 | 163 | connectBtn.disabled = true; 164 | disconnectBtn.disabled = false; 165 | 166 | responseEl.textContent = "Connected successfully!"; 167 | } else { 168 | // Handle MCP protocol messages 169 | responseEl.textContent = JSON.stringify(data, null, 2); 170 | } 171 | }; 172 | 173 | eventSource.onerror = (error) => { 174 | console.error("SSE error:", error); 175 | responseEl.textContent = "Connection error: " + JSON.stringify(error); 176 | disconnect(); 177 | }; 178 | }); 179 | 180 | // Disconnect from the server 181 | disconnectBtn.addEventListener("click", disconnect); 182 | 183 | function disconnect() { 184 | if (eventSource) { 185 | eventSource.close(); 186 | eventSource = null; 187 | } 188 | 189 | connectionId = null; 190 | statusEl.textContent = "Disconnected"; 191 | statusEl.classList.remove("connected"); 192 | statusEl.classList.add("disconnected"); 193 | 194 | connectBtn.disabled = false; 195 | disconnectBtn.disabled = true; 196 | 197 | responseEl.textContent = "Disconnected from server."; 198 | } 199 | 200 | // Helper to send a request to the server 201 | async function sendRequest(request) { 202 | if (!connectionId) { 203 | responseEl.textContent = 204 | "Not connected to server. Click Connect first."; 205 | return; 206 | } 207 | 208 | const reqId = String(requestId++); 209 | request.id = reqId; 210 | 211 | try { 212 | const response = await fetch(`/messages/${connectionId}`, { 213 | method: "POST", 214 | headers: { 215 | "Content-Type": "application/json", 216 | }, 217 | body: JSON.stringify(request), 218 | }); 219 | 220 | if (!response.ok) { 221 | throw new Error(`HTTP error ${response.status}`); 222 | } 223 | 224 | responseEl.textContent = "Request sent. Waiting for response..."; 225 | } catch (error) { 226 | console.error("Request error:", error); 227 | responseEl.textContent = "Error sending request: " + error.message; 228 | } 229 | } 230 | 231 | // List Tables 232 | document.getElementById("list-tables").addEventListener("click", () => { 233 | sendRequest({ 234 | jsonrpc: "2.0", 235 | method: "mcp.readResource", 236 | params: { 237 | uri: "schema://tables", 238 | }, 239 | }); 240 | }); 241 | 242 | // Get Schema 243 | document.getElementById("get-schema").addEventListener("click", () => { 244 | const tableName = document.getElementById("table-name").value.trim(); 245 | 246 | if (!tableName) { 247 | responseEl.textContent = "Please enter a table name."; 248 | return; 249 | } 250 | 251 | sendRequest({ 252 | jsonrpc: "2.0", 253 | method: "mcp.readResource", 254 | params: { 255 | uri: `schema://table/${tableName}`, 256 | }, 257 | }); 258 | }); 259 | 260 | // Run Query 261 | document.getElementById("run-query").addEventListener("click", () => { 262 | const sql = document.getElementById("sql-query").value.trim(); 263 | 264 | if (!sql) { 265 | responseEl.textContent = "Please enter an SQL query."; 266 | return; 267 | } 268 | 269 | sendRequest({ 270 | jsonrpc: "2.0", 271 | method: "mcp.callTool", 272 | params: { 273 | name: "query", 274 | arguments: { 275 | sql, 276 | }, 277 | }, 278 | }); 279 | }); 280 | 281 | // Analyze Table 282 | document.getElementById("analyze-table").addEventListener("click", () => { 283 | const tableName = document 284 | .getElementById("analyze-table-name") 285 | .value.trim(); 286 | 287 | if (!tableName) { 288 | responseEl.textContent = "Please enter a table name."; 289 | return; 290 | } 291 | 292 | sendRequest({ 293 | jsonrpc: "2.0", 294 | method: "mcp.callTool", 295 | params: { 296 | name: "analyze-table", 297 | arguments: { 298 | tableName, 299 | }, 300 | }, 301 | }); 302 | }); 303 | 304 | // Find Related Tables 305 | document.getElementById("find-related").addEventListener("click", () => { 306 | const tableName = document 307 | .getElementById("related-table-name") 308 | .value.trim(); 309 | 310 | if (!tableName) { 311 | responseEl.textContent = "Please enter a table name."; 312 | return; 313 | } 314 | 315 | sendRequest({ 316 | jsonrpc: "2.0", 317 | method: "mcp.callTool", 318 | params: { 319 | name: "find-related-tables", 320 | arguments: { 321 | tableName, 322 | }, 323 | }, 324 | }); 325 | }); 326 | 327 | // Use Prompt 328 | document.getElementById("use-prompt").addEventListener("click", () => { 329 | const promptType = document.getElementById("prompt-type").value; 330 | const tableName = document 331 | .getElementById("prompt-table-name") 332 | .value.trim(); 333 | 334 | if (!tableName) { 335 | responseEl.textContent = "Please enter a table name."; 336 | return; 337 | } 338 | 339 | const args = { tableName }; 340 | 341 | // Add limit for data summary 342 | if (promptType === "data-summary") { 343 | args.limit = 344 | parseInt(document.getElementById("prompt-limit").value) || 10; 345 | } 346 | 347 | sendRequest({ 348 | jsonrpc: "2.0", 349 | method: "mcp.getPrompt", 350 | params: { 351 | name: promptType, 352 | arguments: args, 353 | }, 354 | }); 355 | }); 356 | </script> 357 | </body> 358 | </html> 359 | ``` -------------------------------------------------------------------------------- /mcp-typescript-readme.txt: -------------------------------------------------------------------------------- ``` 1 | MCP TypeScript SDK NPM Version MIT licensed 2 | Table of Contents 3 | Overview 4 | Installation 5 | Quickstart 6 | What is MCP? 7 | Core Concepts 8 | Server 9 | Resources 10 | Tools 11 | Prompts 12 | Running Your Server 13 | stdio 14 | HTTP with SSE 15 | Testing and Debugging 16 | Examples 17 | Echo Server 18 | SQLite Explorer 19 | Advanced Usage 20 | Low-Level Server 21 | Writing MCP Clients 22 | Server Capabilities 23 | Overview 24 | 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: 25 | 26 | Build MCP clients that can connect to any MCP server 27 | Create MCP servers that expose resources, prompts and tools 28 | Use standard transports like stdio and SSE 29 | Handle all MCP protocol messages and lifecycle events 30 | Installation 31 | npm install @modelcontextprotocol/sdk 32 | Quick Start 33 | Let's create a simple MCP server that exposes a calculator tool and some data: 34 | 35 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; 36 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 37 | import { z } from "zod"; 38 | 39 | // Create an MCP server 40 | const server = new McpServer({ 41 | name: "Demo", 42 | version: "1.0.0" 43 | }); 44 | 45 | // Add an addition tool 46 | server.tool("add", 47 | { a: z.number(), b: z.number() }, 48 | async ({ a, b }) => ({ 49 | content: [{ type: "text", text: String(a + b) }] 50 | }) 51 | ); 52 | 53 | // Add a dynamic greeting resource 54 | server.resource( 55 | "greeting", 56 | new ResourceTemplate("greeting://{name}", { list: undefined }), 57 | async (uri, { name }) => ({ 58 | contents: [{ 59 | uri: uri.href, 60 | text: `Hello, ${name}!` 61 | }] 62 | }) 63 | ); 64 | 65 | // Start receiving messages on stdin and sending messages on stdout 66 | const transport = new StdioServerTransport(); 67 | await server.connect(transport); 68 | What is MCP? 69 | The Model Context Protocol (MCP) 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: 70 | 71 | Expose data through Resources (think of these sort of like GET endpoints; they are used to load information into the LLM's context) 72 | Provide functionality through Tools (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect) 73 | Define interaction patterns through Prompts (reusable templates for LLM interactions) 74 | And more! 75 | Core Concepts 76 | Server 77 | The McpServer is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing: 78 | 79 | const server = new McpServer({ 80 | name: "My App", 81 | version: "1.0.0" 82 | }); 83 | Resources 84 | 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: 85 | 86 | // Static resource 87 | server.resource( 88 | "config", 89 | "config://app", 90 | async (uri) => ({ 91 | contents: [{ 92 | uri: uri.href, 93 | text: "App configuration here" 94 | }] 95 | }) 96 | ); 97 | 98 | // Dynamic resource with parameters 99 | server.resource( 100 | "user-profile", 101 | new ResourceTemplate("users://{userId}/profile", { list: undefined }), 102 | async (uri, { userId }) => ({ 103 | contents: [{ 104 | uri: uri.href, 105 | text: `Profile data for user ${userId}` 106 | }] 107 | }) 108 | ); 109 | Tools 110 | Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects: 111 | 112 | // Simple tool with parameters 113 | server.tool( 114 | "calculate-bmi", 115 | { 116 | weightKg: z.number(), 117 | heightM: z.number() 118 | }, 119 | async ({ weightKg, heightM }) => ({ 120 | content: [{ 121 | type: "text", 122 | text: String(weightKg / (heightM * heightM)) 123 | }] 124 | }) 125 | ); 126 | 127 | // Async tool with external API call 128 | server.tool( 129 | "fetch-weather", 130 | { city: z.string() }, 131 | async ({ city }) => { 132 | const response = await fetch(`https://api.weather.com/${city}`); 133 | const data = await response.text(); 134 | return { 135 | content: [{ type: "text", text: data }] 136 | }; 137 | } 138 | ); 139 | Prompts 140 | Prompts are reusable templates that help LLMs interact with your server effectively: 141 | 142 | server.prompt( 143 | "review-code", 144 | { code: z.string() }, 145 | ({ code }) => ({ 146 | messages: [{ 147 | role: "user", 148 | content: { 149 | type: "text", 150 | text: `Please review this code:\n\n${code}` 151 | } 152 | }] 153 | }) 154 | ); 155 | Running Your Server 156 | 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: 157 | 158 | stdio 159 | For command-line tools and direct integrations: 160 | 161 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 162 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 163 | 164 | const server = new McpServer({ 165 | name: "example-server", 166 | version: "1.0.0" 167 | }); 168 | 169 | // ... set up server resources, tools, and prompts ... 170 | 171 | const transport = new StdioServerTransport(); 172 | await server.connect(transport); 173 | HTTP with SSE 174 | 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: 175 | 176 | import express from "express"; 177 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 178 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; 179 | 180 | const server = new McpServer({ 181 | name: "example-server", 182 | version: "1.0.0" 183 | }); 184 | 185 | // ... set up server resources, tools, and prompts ... 186 | 187 | const app = express(); 188 | 189 | app.get("/sse", async (req, res) => { 190 | const transport = new SSEServerTransport("/messages", res); 191 | await server.connect(transport); 192 | }); 193 | 194 | app.post("/messages", async (req, res) => { 195 | // Note: to support multiple simultaneous connections, these messages will 196 | // need to be routed to a specific matching transport. (This logic isn't 197 | // implemented here, for simplicity.) 198 | await transport.handlePostMessage(req, res); 199 | }); 200 | 201 | app.listen(3001); 202 | Testing and Debugging 203 | To test your server, you can use the MCP Inspector. See its README for more information. 204 | 205 | Examples 206 | Echo Server 207 | A simple server demonstrating resources, tools, and prompts: 208 | 209 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; 210 | import { z } from "zod"; 211 | 212 | const server = new McpServer({ 213 | name: "Echo", 214 | version: "1.0.0" 215 | }); 216 | 217 | server.resource( 218 | "echo", 219 | new ResourceTemplate("echo://{message}", { list: undefined }), 220 | async (uri, { message }) => ({ 221 | contents: [{ 222 | uri: uri.href, 223 | text: `Resource echo: ${message}` 224 | }] 225 | }) 226 | ); 227 | 228 | server.tool( 229 | "echo", 230 | { message: z.string() }, 231 | async ({ message }) => ({ 232 | content: [{ type: "text", text: `Tool echo: ${message}` }] 233 | }) 234 | ); 235 | 236 | server.prompt( 237 | "echo", 238 | { message: z.string() }, 239 | ({ message }) => ({ 240 | messages: [{ 241 | role: "user", 242 | content: { 243 | type: "text", 244 | text: `Please process this message: ${message}` 245 | } 246 | }] 247 | }) 248 | ); 249 | SQLite Explorer 250 | A more complex example showing database integration: 251 | 252 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 253 | import sqlite3 from "sqlite3"; 254 | import { promisify } from "util"; 255 | import { z } from "zod"; 256 | 257 | const server = new McpServer({ 258 | name: "SQLite Explorer", 259 | version: "1.0.0" 260 | }); 261 | 262 | // Helper to create DB connection 263 | const getDb = () => { 264 | const db = new sqlite3.Database("database.db"); 265 | return { 266 | all: promisify<string, any[]>(db.all.bind(db)), 267 | close: promisify(db.close.bind(db)) 268 | }; 269 | }; 270 | 271 | server.resource( 272 | "schema", 273 | "schema://main", 274 | async (uri) => { 275 | const db = getDb(); 276 | try { 277 | const tables = await db.all( 278 | "SELECT sql FROM sqlite_master WHERE type='table'" 279 | ); 280 | return { 281 | contents: [{ 282 | uri: uri.href, 283 | text: tables.map((t: {sql: string}) => t.sql).join("\n") 284 | }] 285 | }; 286 | } finally { 287 | await db.close(); 288 | } 289 | } 290 | ); 291 | 292 | server.tool( 293 | "query", 294 | { sql: z.string() }, 295 | async ({ sql }) => { 296 | const db = getDb(); 297 | try { 298 | const results = await db.all(sql); 299 | return { 300 | content: [{ 301 | type: "text", 302 | text: JSON.stringify(results, null, 2) 303 | }] 304 | }; 305 | } catch (err: unknown) { 306 | const error = err as Error; 307 | return { 308 | content: [{ 309 | type: "text", 310 | text: `Error: ${error.message}` 311 | }], 312 | isError: true 313 | }; 314 | } finally { 315 | await db.close(); 316 | } 317 | } 318 | ); 319 | Advanced Usage 320 | Low-Level Server 321 | For more control, you can use the low-level Server class directly: 322 | 323 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 324 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 325 | import { 326 | ListPromptsRequestSchema, 327 | GetPromptRequestSchema 328 | } from "@modelcontextprotocol/sdk/types.js"; 329 | 330 | const server = new Server( 331 | { 332 | name: "example-server", 333 | version: "1.0.0" 334 | }, 335 | { 336 | capabilities: { 337 | prompts: {} 338 | } 339 | } 340 | ); 341 | 342 | server.setRequestHandler(ListPromptsRequestSchema, async () => { 343 | return { 344 | prompts: [{ 345 | name: "example-prompt", 346 | description: "An example prompt template", 347 | arguments: [{ 348 | name: "arg1", 349 | description: "Example argument", 350 | required: true 351 | }] 352 | }] 353 | }; 354 | }); 355 | 356 | server.setRequestHandler(GetPromptRequestSchema, async (request) => { 357 | if (request.params.name !== "example-prompt") { 358 | throw new Error("Unknown prompt"); 359 | } 360 | return { 361 | description: "Example prompt", 362 | messages: [{ 363 | role: "user", 364 | content: { 365 | type: "text", 366 | text: "Example prompt text" 367 | } 368 | }] 369 | }; 370 | }); 371 | 372 | const transport = new StdioServerTransport(); 373 | await server.connect(transport); 374 | Writing MCP Clients 375 | The SDK provides a high-level client interface: 376 | 377 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 378 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 379 | 380 | const transport = new StdioClientTransport({ 381 | command: "node", 382 | args: ["server.js"] 383 | }); 384 | 385 | const client = new Client( 386 | { 387 | name: "example-client", 388 | version: "1.0.0" 389 | }, 390 | { 391 | capabilities: { 392 | prompts: {}, 393 | resources: {}, 394 | tools: {} 395 | } 396 | } 397 | ); 398 | 399 | await client.connect(transport); 400 | 401 | // List prompts 402 | const prompts = await client.listPrompts(); 403 | 404 | // Get a prompt 405 | const prompt = await client.getPrompt("example-prompt", { 406 | arg1: "value" 407 | }); 408 | 409 | // List resources 410 | const resources = await client.listResources(); 411 | 412 | // Read a resource 413 | const resource = await client.readResource("file:///example.txt"); 414 | 415 | // Call a tool 416 | const result = await client.callTool({ 417 | name: "example-tool", 418 | arguments: { 419 | arg1: "value" 420 | } 421 | }); 422 | Documentation 423 | Model Context Protocol documentation 424 | MCP Specification 425 | Example Servers 426 | Contributing ```