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
```