mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-25 12:01:03 +02:00
feat(mcp): build mongoQuery port over the shared connector factory
This commit is contained in:
parent
90e78171f1
commit
9853995930
2 changed files with 112 additions and 1 deletions
|
|
@ -15,6 +15,7 @@ import { KtxDaemonComputeError, type KtxSemanticLayerComputePort } from '../../c
|
|||
import type { KtxLocalProject } from '../../context/project/project.js';
|
||||
import { createKtxEntityDetailsService } from '../../context/scan/entity-details.js';
|
||||
import type { LocalScanMcpOptions } from '../../context/scan/local-scan.js';
|
||||
import type { KtxScanConnector } from '../../context/scan/types.js';
|
||||
import { createKtxDiscoverDataService } from '../../context/search/discover.js';
|
||||
import { sqlAnalysisDialectForDriver } from '../../context/sql-analysis/dialect.js';
|
||||
import type { SqlAnalysisPort } from '../../context/sql-analysis/ports.js';
|
||||
|
|
@ -24,7 +25,8 @@ 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 { KtxMcpContextPorts, KtxMcpProgressCallback, KtxSqlExecutionResponse } from './types.js';
|
||||
import type { KtxMongoDbScanConnector } from '../../connectors/mongodb/connector.js';
|
||||
import type { KtxMcpContextPorts, KtxMcpProgressCallback, KtxMongoQueryResponse, KtxSqlExecutionResponse } from './types.js';
|
||||
|
||||
interface CreateLocalProjectMcpContextPortsOptions {
|
||||
semanticLayerCompute?: KtxSemanticLayerComputePort;
|
||||
|
|
@ -54,6 +56,35 @@ function throwClassifiedQueryError(error: unknown): never {
|
|||
throw new KtxQueryError(error instanceof Error ? error.message : String(error), { cause: error });
|
||||
}
|
||||
|
||||
function projectHasMongoConnection(project: KtxLocalProject): boolean {
|
||||
return Object.values(project.config.connections).some(
|
||||
(connection) => normalizeConnectionDriver(connection) === 'mongodb',
|
||||
);
|
||||
}
|
||||
|
||||
async function executeMongoQuery(
|
||||
createConnector: (connectionId: string) => Promise<KtxScanConnector> | KtxScanConnector,
|
||||
input: { connectionId: string; collection: string; database?: string; pipeline: Record<string, unknown>[]; limit: number },
|
||||
): Promise<KtxMongoQueryResponse> {
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
const result = await (connector as unknown as KtxMongoDbScanConnector).executeQuery(
|
||||
{ connectionId, collection: input.collection, database: input.database, pipeline: input.pipeline, limit: input.limit },
|
||||
{ runId: 'mcp-mongo-query' },
|
||||
);
|
||||
return { headers: result.headers, rows: result.rows, rowCount: result.rowCount };
|
||||
} finally {
|
||||
await connector?.cleanup?.();
|
||||
}
|
||||
}
|
||||
|
||||
async function executeValidatedReadOnlySql(
|
||||
project: KtxLocalProject,
|
||||
options: CreateLocalProjectMcpContextPortsOptions,
|
||||
|
|
@ -239,5 +270,18 @@ export function createLocalProjectMcpContextPorts(
|
|||
};
|
||||
}
|
||||
|
||||
const mongoCreateConnector = options.localScan?.createConnector;
|
||||
if (mongoCreateConnector && projectHasMongoConnection(project)) {
|
||||
ports.mongoQuery = {
|
||||
async execute(input) {
|
||||
try {
|
||||
return await executeMongoQuery(mongoCreateConnector, input);
|
||||
} catch (error) {
|
||||
throwClassifiedQueryError(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return ports;
|
||||
}
|
||||
|
|
|
|||
67
packages/cli/test/mcp/mongo-query-tool.test.ts
Normal file
67
packages/cli/test/mcp/mongo-query-tool.test.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { KtxQueryError } from '../../src/errors.js';
|
||||
import { KtxMongoDbScanConnector } from '../../src/connectors/mongodb/connector.js';
|
||||
import { createLocalProjectMcpContextPorts } from '../../src/context/mcp/local-project-ports.js';
|
||||
import type { KtxLocalProject } from '../../src/context/project/project.js';
|
||||
|
||||
const MONGO_DOCS = [
|
||||
{ _id: 'a1', city: 'Indianapolis' },
|
||||
{ _id: 'a2', city: 'Indianapolis' },
|
||||
];
|
||||
|
||||
function mongoConnector(): KtxMongoDbScanConnector {
|
||||
return new KtxMongoDbScanConnector({
|
||||
connectionId: 'mongo',
|
||||
connection: { driver: 'mongodb', url: 'mongodb://localhost:27017/app', databases: ['app'] },
|
||||
clientFactory: {
|
||||
create: () => ({
|
||||
listCollections: vi.fn(async () => []),
|
||||
estimatedDocumentCount: vi.fn(async () => 0),
|
||||
find: vi.fn(async () => []),
|
||||
aggregate: vi.fn(async () => MONGO_DOCS),
|
||||
ping: vi.fn(async () => undefined),
|
||||
close: vi.fn(async () => undefined),
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function project(): KtxLocalProject {
|
||||
return {
|
||||
projectDir: '/tmp/ktx',
|
||||
config: {
|
||||
connections: {
|
||||
mongo: { driver: 'mongodb', url: 'mongodb://localhost:27017/app', databases: ['app'] },
|
||||
warehouse: { driver: 'postgres' },
|
||||
},
|
||||
},
|
||||
} as unknown as KtxLocalProject;
|
||||
}
|
||||
|
||||
function portsFor(createConnector: (id: string) => KtxMongoDbScanConnector) {
|
||||
return createLocalProjectMcpContextPorts(project(), {
|
||||
embeddingService: null,
|
||||
localScan: { createConnector } as never,
|
||||
});
|
||||
}
|
||||
|
||||
describe('mongoQuery MCP port', () => {
|
||||
it('fetches real rows from a mongodb connection', async () => {
|
||||
const ports = portsFor(() => mongoConnector());
|
||||
const result = await ports.mongoQuery!.execute({
|
||||
connectionId: 'mongo',
|
||||
collection: 'business',
|
||||
pipeline: [{ $match: { city: 'Indianapolis' } }],
|
||||
limit: 100,
|
||||
});
|
||||
expect(result.headers).toEqual(['_id', 'city']);
|
||||
expect(result.rowCount).toBe(2);
|
||||
});
|
||||
|
||||
it('rejects a non-mongodb connection id as an expected query error', async () => {
|
||||
const ports = portsFor(() => mongoConnector());
|
||||
await expect(
|
||||
ports.mongoQuery!.execute({ connectionId: 'warehouse', collection: 'x', pipeline: [], limit: 10 }),
|
||||
).rejects.toBeInstanceOf(KtxQueryError);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue