mcp-server.ts•4 kB
// Minimal MCP server exposing read-only database helpers over stdio.
// Uses the @modelcontextprotocol/sdk server transport; if the SDK API changes,
// adjust the tool registration calls accordingly.
import { z } from "zod";
import { getConfig } from "./storage.js";
import { listTables, previewTable, runSelect } from "./db.js";
type McpToolHandler = (args: any) => Promise<{ content: Array<{ type: string; text: string }> }>;
async function createTool(
server: any,
name: string,
description: string,
inputSchemaZod: z.ZodTypeAny,
legacyJsonSchema: Record<string, unknown>,
handler: McpToolHandler,
) {
// Prefer modern registerTool API, fall back to legacy tool/addTool.
if (typeof server.registerTool === "function") {
server.registerTool(
name,
{ description, inputSchema: inputSchemaZod },
async (args: any) => handler(args),
);
return;
}
const register = server.tool ? server.tool.bind(server) : server.addTool.bind(server);
register(
{
name,
description,
inputSchema: legacyJsonSchema,
},
async (args: any) => handler(args),
);
}
async function main() {
// Lazy import keeps this file side-effect free for UI/server users.
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp");
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio");
const transport = new StdioServerTransport();
const server = new McpServer({ name: "db-mcp", version: "0.1.0" });
await createTool(
server,
"list_tables",
"List tables for a configured database name.",
z.object({
db: z.string().describe("Config name"),
}),
{
type: "object",
required: ["db"],
properties: {
db: { type: "string", description: "Config name" },
},
},
async ({ db }) => {
const config = getConfig(db);
if (!config) {
throw new Error(`Unknown db config: ${db}`);
}
const tables = await listTables(config);
return { content: [{ type: "text", text: JSON.stringify(tables, null, 2) }] };
},
);
await createTool(
server,
"preview_table",
"Preview up to 20 rows from a table in a configured database.",
z.object({
db: z.string().describe("Config name"),
table: z.string().describe("Table name"),
limit: z.number().optional().describe("Max rows (default 20)"),
}),
{
type: "object",
required: ["db", "table"],
properties: {
db: { type: "string", description: "Config name" },
table: { type: "string", description: "Table name" },
limit: { type: "number", description: "Max rows (default 20)" },
},
},
async ({ db, table, limit }) => {
const config = getConfig(db);
if (!config) {
throw new Error(`Unknown db config: ${db}`);
}
const preview = await previewTable(config, table, limit || 20);
return { content: [{ type: "text", text: JSON.stringify(preview, null, 2) }] };
},
);
await createTool(
server,
"run_select",
"Run a SELECT-only query against a configured database (all other statements are rejected).",
z.object({
db: z.string().describe("Config name"),
sql: z.string().describe("SELECT statement (must start with SELECT)"),
}),
{
type: "object",
required: ["db", "sql"],
properties: {
db: { type: "string", description: "Config name" },
sql: { type: "string", description: "SELECT statement" },
},
},
async ({ db, sql }) => {
const config = getConfig(db);
if (!config) {
throw new Error(`Unknown db config: ${db}`);
}
const rows = await runSelect(config, sql);
return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
},
);
console.error("db-mcp server ready on stdio");
await server.connect(transport);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});