#
tokens: 15959/50000 10/11 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
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
```
Page 1/2FirstPrevNextLast