From 6850bf44eb1b18e4a7b769abe6c2ddbef5614a9e Mon Sep 17 00:00:00 2001 From: blankll Date: Sun, 2 Aug 2026 17:58:27 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20merged=20data=5Fstudio=5F=5Flist=5F?= =?UTF-8?q?connections=20tool=20=E2=80=94=20query=20both=20backends?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/data-studio-mcp/src/backends.ts | 23 ++++- packages/data-studio-mcp/src/connections.ts | 24 +++++ packages/data-studio-mcp/src/index.ts | 33 +++++-- packages/data-studio-mcp/src/tools.ts | 2 +- .../data-studio-mcp/tests/backends.test.ts | 11 ++- .../data-studio-mcp/tests/connections.test.ts | 94 +++++++++++++++++++ 6 files changed, 168 insertions(+), 19 deletions(-) create mode 100644 packages/data-studio-mcp/src/connections.ts create mode 100644 packages/data-studio-mcp/tests/connections.test.ts diff --git a/packages/data-studio-mcp/src/backends.ts b/packages/data-studio-mcp/src/backends.ts index eefe0cb..78cf785 100644 --- a/packages/data-studio-mcp/src/backends.ts +++ b/packages/data-studio-mcp/src/backends.ts @@ -16,10 +16,21 @@ export interface InvokeResult { message?: string; } +export interface BridgeConnection { + id: unknown; + name: string; + type: string; +} + +export interface BridgeCatalog { + tools: BridgeToolDef[]; + connections: BridgeConnection[]; +} + export interface BackendClient { readonly name: string; readonly baseUrl: string; - listTools(): Promise; + listTools(): Promise; invokeTool(name: string, args: Record): Promise; } @@ -33,7 +44,7 @@ export function createBackendClient(info: BackendInfo): BackendClient { return info.baseUrl; }, - async listTools(): Promise { + async listTools(): Promise { const res = await fetch(`${info.baseUrl}/tools`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -44,8 +55,12 @@ export function createBackendClient(info: BackendInfo): BackendClient { throw new Error(`Backend ${info.name} returned HTTP ${res.status}: ${res.statusText}`); } - const body = (await res.json()) as { tools: BridgeToolDef[]; connections: unknown[] }; - return body.tools ?? []; + const body = (await res.json()) as { + tools: BridgeToolDef[]; + connections?: BridgeConnection[]; + }; + + return { tools: body.tools ?? [], connections: body.connections ?? [] }; }, async invokeTool(name: string, args: Record): Promise { diff --git a/packages/data-studio-mcp/src/connections.ts b/packages/data-studio-mcp/src/connections.ts new file mode 100644 index 0000000..ac5fa3a --- /dev/null +++ b/packages/data-studio-mcp/src/connections.ts @@ -0,0 +1,24 @@ +import type { BackendClient } from './backends.js'; + +export interface MergedConnection { + backend: string; + id: unknown; + name: string; + type: string; +} + +export async function listConnections( + clients: Map, +): Promise { + const results = await Promise.all( + [...clients.entries()].map(async ([backend, client]) => { + try { + const { connections } = await client.listTools(); + return connections.map(conn => ({ backend, ...conn })); + } catch { + return []; + } + }), + ); + return results.flat(); +} diff --git a/packages/data-studio-mcp/src/index.ts b/packages/data-studio-mcp/src/index.ts index 31ce447..9245cab 100644 --- a/packages/data-studio-mcp/src/index.ts +++ b/packages/data-studio-mcp/src/index.ts @@ -11,6 +11,9 @@ import { import { discoverBackends } from './discovery.js'; import { createBackendClient } from './backends.js'; import { buildToolCatalog } from './tools.js'; +import { listConnections } from './connections.js'; + +const LIST_CONNECTIONS_TOOL = 'data_studio__list_connections'; const errorResult = (text: string): CallToolResult => ({ content: [{ type: 'text', text }], @@ -50,22 +53,34 @@ const main = async (): Promise => { ); server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: tools.map(t => ({ - name: t.name, - description: t.description, - inputSchema: { - type: 'object' as const, - properties: (t.inputSchema as Record)?.properties as - Record | undefined, - required: (t.inputSchema as Record)?.required as string[] | undefined, + tools: [ + { + name: LIST_CONNECTIONS_TOOL, + description: + 'List all database connections from both dockit and sqlkit. Each entry has { backend, id, name, type } — use id as connection_id when calling database tools.', + inputSchema: { type: 'object' as const, properties: {}, required: [] }, }, - })), + ...tools.map(t => ({ + name: t.name, + description: t.description, + inputSchema: { + type: 'object' as const, + properties: (t.inputSchema as Record)?.properties as + Record | undefined, + required: (t.inputSchema as Record)?.required as string[] | undefined, + }, + })), + ], })); server.setRequestHandler(CallToolRequestSchema, async request => { const toolName = request.params.name; const args = (request.params.arguments ?? {}) as Record; + if (toolName === LIST_CONNECTIONS_TOOL) { + return okResult(await listConnections(clients)); + } + const route = routeMap.get(toolName); if (!route) return errorResult(`Unknown tool: ${toolName}`); diff --git a/packages/data-studio-mcp/src/tools.ts b/packages/data-studio-mcp/src/tools.ts index 58fc625..b8f49c3 100644 --- a/packages/data-studio-mcp/src/tools.ts +++ b/packages/data-studio-mcp/src/tools.ts @@ -18,7 +18,7 @@ async function fetchBackendCatalog(backend: BackendInfo): Promise { metadata: { riskLevel: 'safe' }, }, ], - connections: [], + connections: [{ id: 1, name: 'prod-es', type: 'ELASTICSEARCH' }], }), ); } else if (req.method === 'POST' && req.url === '/invoke') { @@ -53,7 +53,7 @@ afterAll(() => { }); describe('BackendClient', () => { - it('listTools returns tools from the bridge', async () => { + it('listTools returns tools and connections from the bridge', async () => { mockPort = await startMock(); const client = createBackendClient({ name: 'dockit', @@ -61,9 +61,10 @@ describe('BackendClient', () => { baseUrl: `http://127.0.0.1:${mockPort}`, }); - const tools = await client.listTools(); - expect(tools).toHaveLength(1); - expect(tools[0].name).toBe('es__search'); + const catalog = await client.listTools(); + expect(catalog.tools).toHaveLength(1); + expect(catalog.tools[0].name).toBe('es__search'); + expect(catalog.connections).toEqual([{ id: 1, name: 'prod-es', type: 'ELASTICSEARCH' }]); }); it('listTools throws when bridge returns error', async () => { diff --git a/packages/data-studio-mcp/tests/connections.test.ts b/packages/data-studio-mcp/tests/connections.test.ts new file mode 100644 index 0000000..b57cfb5 --- /dev/null +++ b/packages/data-studio-mcp/tests/connections.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect, afterAll } from 'vitest'; +import { createServer, type Server } from 'node:http'; +import { createBackendClient } from '../src/backends.js'; +import { listConnections } from '../src/connections.js'; + +const servers: Server[] = []; + +async function startBackend( + connections: Array<{ id: number; name: string; type: string }>, +): Promise { + return new Promise(resolve => { + const server = createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ tools: [], connections })); + }); + servers.push(server); + server.listen(0, '127.0.0.1', () => { + const addr = server.address(); + resolve(typeof addr === 'object' ? addr!.port : 0); + }); + }); +} + +afterAll(() => { + servers.forEach(s => s.close()); +}); + +describe('listConnections', () => { + it('merges connections from both backends with backend tag', async () => { + const dockitPort = await startBackend([{ id: 1, name: 'prod-es', type: 'ELASTICSEARCH' }]); + const sqlkitPort = await startBackend([{ id: 2, name: 'analytics-pg', type: 'POSTGRESQL' }]); + + const clients = new Map([ + [ + 'dockit', + createBackendClient({ + name: 'dockit', + port: dockitPort, + baseUrl: `http://127.0.0.1:${dockitPort}`, + }), + ], + [ + 'sqlkit', + createBackendClient({ + name: 'sqlkit', + port: sqlkitPort, + baseUrl: `http://127.0.0.1:${sqlkitPort}`, + }), + ], + ]); + + const result = await listConnections(clients); + expect(result).toEqual([ + { backend: 'dockit', id: 1, name: 'prod-es', type: 'ELASTICSEARCH' }, + { backend: 'sqlkit', id: 2, name: 'analytics-pg', type: 'POSTGRESQL' }, + ]); + }); + + it('skips unreachable backends', async () => { + const dockitPort = await startBackend([{ id: 1, name: 'prod-es', type: 'ELASTICSEARCH' }]); + + const clients = new Map([ + [ + 'dockit', + createBackendClient({ + name: 'dockit', + port: dockitPort, + baseUrl: `http://127.0.0.1:${dockitPort}`, + }), + ], + ['sqlkit', createBackendClient({ name: 'sqlkit', port: 1, baseUrl: 'http://127.0.0.1:1' })], + ]); + + const result = await listConnections(clients); + expect(result).toEqual([{ backend: 'dockit', id: 1, name: 'prod-es', type: 'ELASTICSEARCH' }]); + }); + + it('returns empty list when no backends have connections', async () => { + const dockitPort = await startBackend([]); + const clients = new Map([ + [ + 'dockit', + createBackendClient({ + name: 'dockit', + port: dockitPort, + baseUrl: `http://127.0.0.1:${dockitPort}`, + }), + ], + ]); + + const result = await listConnections(clients); + expect(result).toEqual([]); + }); +}); From cc834679b5ddc1e2acc08bb1e6d4b630389cce8f Mon Sep 17 00:00:00 2001 From: blankll Date: Sun, 2 Aug 2026 18:00:34 +0800 Subject: [PATCH 2/3] chore: bump data-studio-mcp to 0.1.3 --- packages/data-studio-mcp/package-lock.json | 4 ++-- packages/data-studio-mcp/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/data-studio-mcp/package-lock.json b/packages/data-studio-mcp/package-lock.json index a244487..ca4ee9e 100644 --- a/packages/data-studio-mcp/package-lock.json +++ b/packages/data-studio-mcp/package-lock.json @@ -1,12 +1,12 @@ { "name": "@geek-fun/data-studio-mcp", - "version": "0.1.2", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@geek-fun/data-studio-mcp", - "version": "0.1.2", + "version": "0.1.3", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.30.0" diff --git a/packages/data-studio-mcp/package.json b/packages/data-studio-mcp/package.json index 7c91389..82101ba 100644 --- a/packages/data-studio-mcp/package.json +++ b/packages/data-studio-mcp/package.json @@ -1,6 +1,6 @@ { "name": "@geek-fun/data-studio-mcp", - "version": "0.1.2", + "version": "0.1.3", "description": "Unified MCP server for SQL (PostgreSQL, MySQL, SQL Server, SQLite) and NoSQL (Elasticsearch, MongoDB, DynamoDB) — powered by dockit and sqlkit desktop apps", "keywords": [ "mcp", From b399ec5cda3142ce4c92f2fbe6a19a19ea4dfe68 Mon Sep 17 00:00:00 2001 From: blankll Date: Sun, 2 Aug 2026 18:04:10 +0800 Subject: [PATCH 3/3] =?UTF-8?q?style:=20align=20TS=20with=20AGENTS.md=20?= =?UTF-8?q?=E2=80=94=20const=20fn=20over=20function,=20type=20over=20inter?= =?UTF-8?q?face?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/data-studio-mcp/src/backends.ts | 24 +++++++++---------- packages/data-studio-mcp/src/connections.ts | 10 ++++---- packages/data-studio-mcp/src/discovery.ts | 16 ++++++------- packages/data-studio-mcp/src/tools.ts | 20 +++++++++------- .../data-studio-mcp/tests/backends.test.ts | 4 ++-- .../data-studio-mcp/tests/connections.test.ts | 6 ++--- .../data-studio-mcp/tests/discovery.test.ts | 4 ++-- 7 files changed, 43 insertions(+), 41 deletions(-) diff --git a/packages/data-studio-mcp/src/backends.ts b/packages/data-studio-mcp/src/backends.ts index 78cf785..d6c3489 100644 --- a/packages/data-studio-mcp/src/backends.ts +++ b/packages/data-studio-mcp/src/backends.ts @@ -1,6 +1,6 @@ import type { BackendInfo } from './discovery.js'; -export interface BridgeToolDef { +export type BridgeToolDef = { name: string; description: string; inputSchema: object; @@ -8,33 +8,33 @@ export interface BridgeToolDef { riskLevel?: string; requiredPermission?: string; }; -} +}; -export interface InvokeResult { +export type InvokeResult = { status: number; data?: unknown; message?: string; -} +}; -export interface BridgeConnection { +export type BridgeConnection = { id: unknown; name: string; type: string; -} +}; -export interface BridgeCatalog { +export type BridgeCatalog = { tools: BridgeToolDef[]; connections: BridgeConnection[]; -} +}; -export interface BackendClient { +export type BackendClient = { readonly name: string; readonly baseUrl: string; listTools(): Promise; invokeTool(name: string, args: Record): Promise; -} +}; -export function createBackendClient(info: BackendInfo): BackendClient { +export const createBackendClient = (info: BackendInfo): BackendClient => { return { get name() { return info.name; @@ -74,4 +74,4 @@ export function createBackendClient(info: BackendInfo): BackendClient { return (await res.json()) as InvokeResult; }, }; -} +}; diff --git a/packages/data-studio-mcp/src/connections.ts b/packages/data-studio-mcp/src/connections.ts index ac5fa3a..0b7f6d4 100644 --- a/packages/data-studio-mcp/src/connections.ts +++ b/packages/data-studio-mcp/src/connections.ts @@ -1,15 +1,15 @@ import type { BackendClient } from './backends.js'; -export interface MergedConnection { +export type MergedConnection = { backend: string; id: unknown; name: string; type: string; -} +}; -export async function listConnections( +export const listConnections = async ( clients: Map, -): Promise { +): Promise => { const results = await Promise.all( [...clients.entries()].map(async ([backend, client]) => { try { @@ -21,4 +21,4 @@ export async function listConnections( }), ); return results.flat(); -} +}; diff --git a/packages/data-studio-mcp/src/discovery.ts b/packages/data-studio-mcp/src/discovery.ts index 1ca814d..e94d049 100644 --- a/packages/data-studio-mcp/src/discovery.ts +++ b/packages/data-studio-mcp/src/discovery.ts @@ -2,11 +2,11 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -export interface BackendInfo { +export type BackendInfo = { name: 'dockit' | 'sqlkit'; port: number; baseUrl: string; -} +}; const BACKENDS: ReadonlyArray<{ name: 'dockit' | 'sqlkit'; @@ -18,7 +18,7 @@ const BACKENDS: ReadonlyArray<{ { name: 'sqlkit', bundleId: 'club.geekfun.sqlkit', defaultPort: 9121, envVar: 'SQLKIT_MCP_PORT' }, ]; -export function appDataDir(bundleId: string): string { +export const appDataDir = (bundleId: string): string => { const home = os.homedir(); const platform = os.platform(); @@ -26,9 +26,9 @@ export function appDataDir(bundleId: string): string { if (platform === 'linux') return path.join(home, '.local', 'share', bundleId); if (platform === 'win32') return path.join(home, 'AppData', 'Roaming', bundleId); return path.join(home, '.local', 'share', bundleId); -} +}; -function discoverBackend(cfg: (typeof BACKENDS)[number]): BackendInfo | null { +const discoverBackend = (cfg: (typeof BACKENDS)[number]): BackendInfo | null => { const envPort = process.env[cfg.envVar]; if (envPort) { const port = parseInt(envPort, 10); @@ -49,8 +49,8 @@ function discoverBackend(cfg: (typeof BACKENDS)[number]): BackendInfo | null { } return null; -} +}; -export function discoverBackends(): BackendInfo[] { +export const discoverBackends = (): BackendInfo[] => { return BACKENDS.map(discoverBackend).filter((b): b is BackendInfo => b !== null); -} +}; diff --git a/packages/data-studio-mcp/src/tools.ts b/packages/data-studio-mcp/src/tools.ts index b8f49c3..545b2b9 100644 --- a/packages/data-studio-mcp/src/tools.ts +++ b/packages/data-studio-mcp/src/tools.ts @@ -1,20 +1,20 @@ import type { BackendInfo } from './discovery.js'; import { createBackendClient, type BridgeToolDef } from './backends.js'; -export interface McpToolDef { +export type McpToolDef = { name: string; description: string; inputSchema: object; backendName: string; internalName: string; -} +}; -interface CatalogEntry { +type CatalogEntry = { tool: McpToolDef; route: { backendName: 'dockit' | 'sqlkit'; internalName: string }; -} +}; -async function fetchBackendCatalog(backend: BackendInfo): Promise { +const fetchBackendCatalog = async (backend: BackendInfo): Promise => { const client = createBackendClient(backend); let bridgeTools: BridgeToolDef[]; try { @@ -37,15 +37,17 @@ async function fetchBackendCatalog(backend: BackendInfo): Promise; -}> { +}> => { const entries = (await Promise.all(backends.map(fetchBackendCatalog))).flat(); return { tools: entries.map(e => e.tool), routeMap: new Map(entries.map(e => [e.tool.name, e.route])), }; -} +}; diff --git a/packages/data-studio-mcp/tests/backends.test.ts b/packages/data-studio-mcp/tests/backends.test.ts index def8acf..0b1e126 100644 --- a/packages/data-studio-mcp/tests/backends.test.ts +++ b/packages/data-studio-mcp/tests/backends.test.ts @@ -5,7 +5,7 @@ import { createBackendClient } from '../src/backends.js'; let mockServer: Server | null = null; let mockPort = 0; -function startMock(failTools = false): Promise { +const startMock = (failTools = false): Promise => { return new Promise(resolve => { mockServer = createServer((req, res) => { if (req.method === 'POST' && req.url === '/tools') { @@ -46,7 +46,7 @@ function startMock(failTools = false): Promise { resolve(typeof addr === 'object' ? addr!.port : 0); }); }); -} +}; afterAll(() => { mockServer?.close(); diff --git a/packages/data-studio-mcp/tests/connections.test.ts b/packages/data-studio-mcp/tests/connections.test.ts index b57cfb5..d73905c 100644 --- a/packages/data-studio-mcp/tests/connections.test.ts +++ b/packages/data-studio-mcp/tests/connections.test.ts @@ -5,9 +5,9 @@ import { listConnections } from '../src/connections.js'; const servers: Server[] = []; -async function startBackend( +const startBackend = async ( connections: Array<{ id: number; name: string; type: string }>, -): Promise { +): Promise => { return new Promise(resolve => { const server = createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); @@ -19,7 +19,7 @@ async function startBackend( resolve(typeof addr === 'object' ? addr!.port : 0); }); }); -} +}; afterAll(() => { servers.forEach(s => s.close()); diff --git a/packages/data-studio-mcp/tests/discovery.test.ts b/packages/data-studio-mcp/tests/discovery.test.ts index 91224c1..d9b5608 100644 --- a/packages/data-studio-mcp/tests/discovery.test.ts +++ b/packages/data-studio-mcp/tests/discovery.test.ts @@ -23,11 +23,11 @@ afterEach(() => { }); // Need to dynamically import after setting up mock dir -async function getDiscovery() { +const getDiscovery = async () => { // Recreate test dir fs.mkdirSync(testDir, { recursive: true }); return await import('../src/discovery.js'); -} +}; describe('discoverBackends', () => { it('returns empty array when no backends found', async () => {