[Phase 3] MCP Server — TypeScript npm package
Parent epic: #9
Status: Design
Priority: High
Background
The MCP Server is the unified entry point for AI coding agents (Claude Code, Cursor, Windsurf, OpenCode, Codex). It runs as a stdio-based MCP server, discovers dockit and sqlkit backends via port files, and proxies tool calls to the correct backend over HTTP.
It is implemented in TypeScript (not Rust) because it is purely a routing layer — no database drivers, no CPU-intensive work. This eliminates platform-specific binary builds and allows npx-based distribution with zero installation friction.
Architecture
Agent (Claude Code / Cursor / OpenCode)
│
│ MCP stdio protocol
▼
@geekfun/data-studio-mcp (npx)
│
│ @modelcontextprotocol/sdk v2
│ node:http
│
├── POST 127.0.0.1:9120 (dockit tools)
│ ├── /tools
│ └── /invoke
│
└── POST 127.0.0.1:9121 (sqlkit tools)
├── /tools
└── /invoke
Package structure
packages/data-studio-mcp/
├── package.json ← @geekfun/data-studio-mcp
├── tsconfig.json
├── server.json ← MCP registry metadata (smithery.ai etc.)
├── README.md
└── src/
├── index.ts ← Entry: create server, discover backends, start
├── discovery.ts ← Find dockit/sqlkit via port files or env vars
├── backends.ts ← HTTP client for bridge calls
└── tools.ts ← Tool definitions with unified naming
Key dependencies
{
"dependencies": {
"@modelcontextprotocol/sdk": "^2.0.0"
},
"bin": {
"data-studio-mcp": "./bin/data-studio-mcp.js"
}
}
Single runtime dependency: the official MCP SDK. The SDK handles protocol negotiation, tool registration, and stdio transport.
Module design
src/discovery.ts — Backend discovery
Reads port files written by dockit and sqlkit at startup.
export interface BackendInfo {
name: 'dockit' | 'sqlkit';
port: number;
baseUrl: string;
}
export function discoverBackends(): BackendInfo[] {
const backends: BackendInfo[] = [];
// 1. Try port files
const configDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
for (const app of ['dockit', 'sqlkit'] as const) {
const portFile = path.join(configDir, app, 'mcp-port');
try {
const port = parseInt(fs.readFileSync(portFile, 'utf-8').trim(), 10);
if (!isNaN(port)) {
backends.push({ name: app, port, baseUrl: `http://127.0.0.1:${port}` });
}
} catch { /* app not running */ }
}
// 2. Fallback: environment variables (for Docker/CI scenarios)
if (process.env.DOCKIT_WEB_URL) {
backends.push({ name: 'dockit', port: 0, baseUrl: process.env.DOCKIT_WEB_URL });
}
if (process.env.SQLKIT_WEB_URL) {
backends.push({ name: 'sqlkit', port: 0, baseUrl: process.env.SQLKIT_WEB_URL });
}
return backends;
}
src/backends.ts — HTTP proxy
Thin wrapper around fetch() for bridge communication.
import { BackendInfo } from './discovery.js';
export interface ToolDef {
name: string;
description: string;
inputSchema: object;
metadata: { riskLevel: string; requiredPermission: string };
}
export class BackendClient {
constructor(private info: BackendInfo) {}
async listTools(): Promise<ToolDef[]> {
const res = await fetch(`${this.info.baseUrl}/tools`, { method: 'POST' });
if (!res.ok) throw new Error(`Backend ${this.info.name} unavailable`);
const data = await res.json();
return data.tools;
}
async invokeTool(name: string, args: any, connectionId?: number): Promise<any> {
const res = await fetch(`${this.info.baseUrl}/invoke`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, args, connection_id: connectionId }),
});
return res.json();
}
}
src/tools.ts — Tool naming and routing
Defines the unified tool naming convention and maps MCP tool calls to backend capability names.
export const ROUTING_RULES: Array<{
prefix: string;
backend: 'dockit' | 'sqlkit';
transform: (name: string) => string;
}> = [
// sqlkit tools
{ prefix: 'data_studio__sql_execute', backend: 'sqlkit', transform: () => 'sqlkit__execute_query' },
{ prefix: 'data_studio__sql_list_databases', backend: 'sqlkit', transform: () => 'sqlkit__list_databases' },
{ prefix: 'data_studio__sql_list_schemas', backend: 'sqlkit', transform: () => 'sqlkit__list_schemas' },
{ prefix: 'data_studio__sql_list_tables', backend: 'sqlkit', transform: () => 'sqlkit__list_tables' },
{ prefix: 'data_studio__sql_describe_table', backend: 'sqlkit', transform: () => 'sqlkit__describe_table' },
{ prefix: 'data_studio__sql_get_schema', backend: 'sqlkit', transform: () => 'sqlkit__get_schema' },
{ prefix: 'data_studio__sql_explain', backend: 'sqlkit', transform: () => 'sqlkit__explain_query' },
// dockit ES tools
{ prefix: 'data_studio__es_search', backend: 'dockit', transform: () => 'es__search' },
{ prefix: 'data_studio__es_get_document', backend: 'dockit', transform: () => 'es__get_document' },
// ... (all 16 ES tools)
// dockit MongoDB tools
{ prefix: 'data_studio__mongo_find', backend: 'dockit', transform: () => 'mongo__find' },
// ... (all ~15 Mongo tools)
// dockit DynamoDB tools
{ prefix: 'data_studio__dynamo_query', backend: 'dockit', transform: () => 'dynamo__execute_query' },
// ... (all ~24 DynamoDB tools)
// metadata tools (query both backends, merge)
{ prefix: 'data_studio__list_connections', backend: 'both', transform: () => '' },
];
export function route(toolName: string): { backend: string; capabilityName: string } {
for (const rule of ROUTING_RULES) {
if (toolName.startsWith(rule.prefix)) {
return { backend: rule.backend, capabilityName: rule.transform(toolName) };
}
}
throw new Error(`Unknown tool: ${toolName}`);
}
Tool descriptions for the MCP registry include routing metadata:
const ES_SEARCH_TOOL = {
name: 'data_studio__es_search',
description: 'Execute an Elasticsearch search query using Query DSL. Requires an Elasticsearch connection configured in dockit.',
inputSchema: {
type: 'object',
properties: {
connection_id: { type: 'number', description: 'Connection ID from data_studio__list_connections' },
index: { type: 'string', description: 'Target index name or pattern' },
body: { type: 'object', description: 'Query DSL body' },
},
required: ['connection_id', 'body'],
},
};
src/index.ts — Entry point
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { discoverBackends } from './discovery.js';
import { BackendClient } from './backends.js';
async function main() {
const backends = discoverBackends();
if (backends.length === 0) {
console.error('No backends found. Start dockit or sqlkit first.');
process.exit(1);
}
const clients = Object.fromEntries(
backends.map(b => [b.name, new BackendClient(b)])
);
// Fetch tools from all backends
const allTools = [];
for (const [name, client] of Object.entries(clients)) {
const tools = await client.listTools();
allTools.push(...tools.map(t => transformTool(name, t)));
}
const server = new Server(
{ name: 'data-studio-mcp', version: '0.1.0' },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: allTools }));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const { backend, capabilityName } = route(name);
const connId = args?.connection_id;
try {
const result = await clients[backend].invokeTool(capabilityName, args, connId);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
} catch (e) {
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);
Error handling strategy
| Scenario |
Behavior |
| No backends found |
Exit with clear message: "Start dockit or sqlkit first" |
| One backend unavailable |
Still start, expose only available backend's tools |
| Backend goes down mid-session |
Return isError: true for tools routed to that backend |
| Unknown tool name |
Return isError: true with "Unknown tool" message |
| Bridge HTTP error |
Return isError: true with the bridge's error message |
| Invalid arguments |
Return isError: true with validation details |
npm distribution
package.json
{
"name": "@geekfun/data-studio-mcp",
"version": "0.1.0",
"type": "module",
"bin": {
"data-studio-mcp": "./bin/data-studio-mcp.js"
},
"files": ["bin", "src", "server.json"],
"dependencies": {
"@modelcontextprotocol/sdk": "^2.0.0"
},
"engines": { "node": ">=18.0.0" }
}
The bin/data-studio-mcp.js launcher is a thin shim that compiles TypeScript via tsx:
#!/usr/bin/env node
import './src/index.js';
Or simpler: compile to JS and ship directly:
{
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
}
}
server.json (MCP registry metadata)
{
"name": "@geekfun/data-studio-mcp",
"description": "Unified database MCP server for SQL (PostgreSQL, MySQL, SQL Server, SQLite) and NoSQL (Elasticsearch, MongoDB, DynamoDB) — powered by dockit and sqlkit.",
"repository": { "url": "https://github.com/geek-fun/data-studio-agent", "source": "github" },
"packages": [
{
"registryType": "npm",
"identifier": "@geekfun/data-studio-mcp",
"transport": { "type": "stdio" }
}
]
}
GitHub Actions publish workflow
# .github/workflows/release-mcp.yml
name: Release MCP Server
on:
push:
tags: ['mcp-v*']
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, registry-url: 'https://registry.npmjs.org' }
- run: npm ci
working-directory: packages/data-studio-mcp
- run: npm run build
working-directory: packages/data-studio-mcp
- run: npm publish --access public
working-directory: packages/data-studio-mcp
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Verification
Future considerations
- Phase 4: Add permission filtering based on backend's
McpPolicy
- Phase 4: Handle
listChanged notification when tools change
- Streamable HTTP transport as an alternative to stdio (follow-up)
server.json submission to smithery.ai MCP directory
[Phase 3] MCP Server — TypeScript npm package
Parent epic: #9
Status: Design
Priority: High
Background
The MCP Server is the unified entry point for AI coding agents (Claude Code, Cursor, Windsurf, OpenCode, Codex). It runs as a stdio-based MCP server, discovers dockit and sqlkit backends via port files, and proxies tool calls to the correct backend over HTTP.
It is implemented in TypeScript (not Rust) because it is purely a routing layer — no database drivers, no CPU-intensive work. This eliminates platform-specific binary builds and allows
npx-based distribution with zero installation friction.Architecture
Package structure
Key dependencies
{ "dependencies": { "@modelcontextprotocol/sdk": "^2.0.0" }, "bin": { "data-studio-mcp": "./bin/data-studio-mcp.js" } }Single runtime dependency: the official MCP SDK. The SDK handles protocol negotiation, tool registration, and stdio transport.
Module design
src/discovery.ts— Backend discoveryReads port files written by dockit and sqlkit at startup.
src/backends.ts— HTTP proxyThin wrapper around
fetch()for bridge communication.src/tools.ts— Tool naming and routingDefines the unified tool naming convention and maps MCP tool calls to backend capability names.
Tool descriptions for the MCP registry include routing metadata:
src/index.ts— Entry pointError handling strategy
isError: truefor tools routed to that backendisError: truewith "Unknown tool" messageisError: truewith the bridge's error messageisError: truewith validation detailsnpm distribution
package.json{ "name": "@geekfun/data-studio-mcp", "version": "0.1.0", "type": "module", "bin": { "data-studio-mcp": "./bin/data-studio-mcp.js" }, "files": ["bin", "src", "server.json"], "dependencies": { "@modelcontextprotocol/sdk": "^2.0.0" }, "engines": { "node": ">=18.0.0" } }The
bin/data-studio-mcp.jslauncher is a thin shim that compiles TypeScript viatsx:Or simpler: compile to JS and ship directly:
{ "scripts": { "build": "tsc", "prepublishOnly": "npm run build" } }server.json(MCP registry metadata){ "name": "@geekfun/data-studio-mcp", "description": "Unified database MCP server for SQL (PostgreSQL, MySQL, SQL Server, SQLite) and NoSQL (Elasticsearch, MongoDB, DynamoDB) — powered by dockit and sqlkit.", "repository": { "url": "https://github.com/geek-fun/data-studio-agent", "source": "github" }, "packages": [ { "registryType": "npm", "identifier": "@geekfun/data-studio-mcp", "transport": { "type": "stdio" } } ] }GitHub Actions publish workflow
Verification
npx -y @geekfun/data-studio-mcpstarts without errors (with dockit running)npx -y @geekfun/data-studio-mcpstarts without errors (with sqlkit running)npx -y @geekfun/data-studio-mcpstarts without errors (with both running)data_studio__es_list_indicesdata_studio__sql_list_databasesdata_studio__list_connectionsreturns connections from both backendsisError: truewith clear messageFuture considerations
McpPolicylistChangednotification when tools changeserver.jsonsubmission to smithery.ai MCP directory