Skip to content

Phase 3: MCP Server — TypeScript npm package #13

Description

@Blankll

[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

  • npx -y @geekfun/data-studio-mcp starts without errors (with dockit running)
  • npx -y @geekfun/data-studio-mcp starts without errors (with sqlkit running)
  • npx -y @geekfun/data-studio-mcp starts without errors (with both running)
  • Tools from both backends are merged and correctly name-transformed
  • Tool call to dockit backend succeeds: data_studio__es_list_indices
  • Tool call to sqlkit backend succeeds: data_studio__sql_list_databases
  • data_studio__list_connections returns connections from both backends
  • Error when backend is down returns isError: true with clear message

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions