diff --git a/packages/cli/src/context/mcp/local-project-ports.ts b/packages/cli/src/context/mcp/local-project-ports.ts index c366a658..43901685 100644 --- a/packages/cli/src/context/mcp/local-project-ports.ts +++ b/packages/cli/src/context/mcp/local-project-ports.ts @@ -25,7 +25,7 @@ import { readLocalSlSource } from '../../context/sl/local-sl.js'; import { assertSafeConnectionId } from '../../context/sl/source-files.js'; import { assertConfiguredConnectionId } from '../../context/connections/configured-connections.js'; import { readLocalKnowledgePage, searchLocalKnowledgePages } from '../wiki/local-knowledge.js'; -import type { KtxMongoDbScanConnector } from '../../connectors/mongodb/connector.js'; +import { runMongoQuery } from './mongo-query-runner.js'; import type { KtxMcpContextPorts, KtxMcpProgressCallback, KtxMongoQueryResponse, KtxSqlExecutionResponse } from './types.js'; interface CreateLocalProjectMcpContextPortsOptions { @@ -66,22 +66,7 @@ async function executeMongoQuery( createConnector: (connectionId: string) => Promise | KtxScanConnector, input: { connectionId: string; collection: string; database?: string; pipeline: Record[]; limit: number }, ): Promise { - const connectionId = assertSafeConnectionId(input.connectionId); - let connector: KtxScanConnector | null = null; - try { - connector = await createConnector(connectionId); - if (connector.driver !== 'mongodb') { - throw new KtxExpectedError( - `Connection "${connectionId}" driver "${connector.driver}" is not a MongoDB connection; mongo_query serves mongodb connections only.`, - ); - } - return (connector as unknown as KtxMongoDbScanConnector).executeQuery( - { connectionId, collection: input.collection, database: input.database, pipeline: input.pipeline, limit: input.limit }, - { runId: 'mcp-mongo-query' }, - ); - } finally { - await connector?.cleanup?.(); - } + return runMongoQuery(createConnector, input); } async function executeValidatedReadOnlySql( diff --git a/packages/cli/src/context/mcp/mongo-query-runner.ts b/packages/cli/src/context/mcp/mongo-query-runner.ts new file mode 100644 index 00000000..fdd0b082 --- /dev/null +++ b/packages/cli/src/context/mcp/mongo-query-runner.ts @@ -0,0 +1,35 @@ +import type { KtxMongoDbScanConnector, KtxMongoQueryResult } from '../../connectors/mongodb/connector.js'; +import { KtxExpectedError } from '../../errors.js'; +import { assertSafeConnectionId } from '../sl/source-files.js'; +import type { KtxScanConnector } from '../scan/types.js'; + +export interface KtxMongoQueryRequest { + connectionId: string; + collection: string; + database?: string; + pipeline: Record[]; + limit: number; +} + +/** Single Mongo-read execution seam: resolve the connector, guard the driver, run the pipeline. */ +export async function runMongoQuery( + createConnector: (connectionId: string) => Promise | KtxScanConnector, + input: KtxMongoQueryRequest, +): Promise { + const connectionId = assertSafeConnectionId(input.connectionId); + let connector: KtxScanConnector | null = null; + try { + connector = await createConnector(connectionId); + if (connector.driver !== 'mongodb') { + throw new KtxExpectedError( + `Connection "${connectionId}" driver "${connector.driver}" is not a MongoDB connection; mongo_query serves mongodb connections only.`, + ); + } + return await (connector as unknown as KtxMongoDbScanConnector).executeQuery( + { connectionId, collection: input.collection, database: input.database, pipeline: input.pipeline, limit: input.limit }, + { runId: 'mongo-query' }, + ); + } finally { + await connector?.cleanup?.(); + } +} diff --git a/packages/cli/test/mcp/mongo-query-tool.test.ts b/packages/cli/test/mcp/mongo-query-tool.test.ts index ee9f788a..e0af6023 100644 --- a/packages/cli/test/mcp/mongo-query-tool.test.ts +++ b/packages/cli/test/mcp/mongo-query-tool.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest'; import { KtxExpectedError } from '../../src/errors.js'; import { KtxMongoDbScanConnector } from '../../src/connectors/mongodb/connector.js'; import { createLocalProjectMcpContextPorts } from '../../src/context/mcp/local-project-ports.js'; +import { runMongoQuery } from '../../src/context/mcp/mongo-query-runner.js'; import type { KtxLocalProject } from '../../src/context/project/project.js'; import type { KtxScanConnector } from '../../src/context/scan/types.js'; @@ -80,3 +81,25 @@ describe('mongoQuery MCP port', () => { ).rejects.toBeInstanceOf(KtxExpectedError); }); }); + +describe('runMongoQuery shared helper', () => { + it('fetches rows from a mongodb connector', async () => { + const result = await runMongoQuery( + (id) => (id === 'mongo' ? mongoConnector() : warehouseConnector()), + { connectionId: 'mongo', collection: 'business', pipeline: [], limit: 100 }, + ); + expect(result.rowCount).toBe(2); + expect(result.headers).toEqual(['_id', 'city']); + }); + + it('throws an expected error naming the wrong driver for a non-mongodb connector', async () => { + await expect( + runMongoQuery((id) => (id === 'mongo' ? mongoConnector() : warehouseConnector()), { + connectionId: 'warehouse', + collection: 'x', + pipeline: [], + limit: 10, + }), + ).rejects.toThrow(/is not a MongoDB connection/); + }); +});