From da6f8873d4192a8a6dbb9c1956e9f52a265ef945 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov <7889985+andreybavt@users.noreply.github.com> Date: Thu, 14 May 2026 18:11:27 +0200 Subject: [PATCH] feat(context): expose local MCP entity details --- .../src/mcp/local-project-ports.test.ts | 138 ++++++++++++++++++ .../context/src/mcp/local-project-ports.ts | 6 + 2 files changed, 144 insertions(+) diff --git a/packages/context/src/mcp/local-project-ports.test.ts b/packages/context/src/mcp/local-project-ports.test.ts index 190e926c..cb629094 100644 --- a/packages/context/src/mcp/local-project-ports.test.ts +++ b/packages/context/src/mcp/local-project-ports.test.ts @@ -76,6 +76,93 @@ describe('createLocalProjectMcpContextPorts', () => { }; } + async function seedScanReport(projectDir: string, syncId = 'sync-1'): Promise { + const root = `raw-sources/warehouse/live-database/${syncId}`; + await mkdir(join(projectDir, root, 'tables'), { recursive: true }); + await writeFile( + join(projectDir, root, 'connection.json'), + JSON.stringify( + { + connectionId: 'warehouse', + driver: 'postgres', + extractedAt: '2026-05-14T09:00:00.000Z', + scope: { schemas: ['public'] }, + }, + null, + 2, + ), + 'utf-8', + ); + await writeFile( + join(projectDir, root, 'tables', 'orders.json'), + JSON.stringify( + { + catalog: null, + db: 'public', + name: 'orders', + kind: 'table', + comment: 'Customer orders', + estimatedRows: 12, + columns: [ + { + name: 'id', + nativeType: 'integer', + normalizedType: 'integer', + dimensionType: 'number', + nullable: false, + primaryKey: true, + comment: null, + }, + ], + foreignKeys: [], + }, + null, + 2, + ), + 'utf-8', + ); + await writeFile( + join(projectDir, root, 'scan-report.json'), + JSON.stringify( + { + connectionId: 'warehouse', + driver: 'postgres', + syncId, + runId: 'scan-1', + trigger: 'mcp', + mode: 'structural', + dryRun: false, + artifactPaths: { + rawSourcesDir: root, + reportPath: `${root}/scan-report.json`, + manifestShards: [], + enrichmentArtifacts: [], + }, + diffSummary: { added: 0, modified: 0, deleted: 0, unchanged: 1 }, + manifestShardsWritten: 0, + structuralSyncStats: { tablesWritten: 1, tablesDeleted: 0, foreignKeysWritten: 0 }, + enrichment: { + dataDictionary: 'skipped', + tableDescriptions: 'skipped', + columnDescriptions: 'skipped', + embeddings: 'skipped', + deterministicRelationships: 'skipped', + llmRelationshipValidation: 'skipped', + statisticalValidation: 'skipped', + }, + capabilityGaps: [], + warnings: [], + relationships: { accepted: 0, review: 0, rejected: 0, skipped: 0 }, + enrichmentState: { resumedStages: [], completedStages: [], failedStages: [] }, + createdAt: '2026-05-14T09:00:00.000Z', + }, + null, + 2, + ), + 'utf-8', + ); + } + it('lists local project connections from ktx.yaml', async () => { const project = await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' }); project.config.connections.warehouse = { @@ -213,6 +300,57 @@ describe('createLocalProjectMcpContextPorts', () => { expect(connector.executeReadOnly).not.toHaveBeenCalled(); }); + it('exposes local scan entity details through MCP ports', async () => { + const project = await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' }); + project.config.connections.warehouse = { + driver: 'postgres', + url: 'env:DATABASE_URL', + }; + await seedScanReport(project.projectDir); + const ports = createLocalProjectMcpContextPorts(project); + + await expect( + ports.entityDetails?.read({ + connectionId: 'warehouse', + entities: [{ table: 'public.orders', columns: ['id'] }], + }), + ).resolves.toMatchObject({ + results: [ + { + ok: true, + connectionId: 'warehouse', + display: 'public.orders', + columns: [{ name: 'id', nativeType: 'integer' }], + snapshot: { syncId: 'sync-1', scanRunId: 'scan-1' }, + }, + ], + }); + }); + + it('returns a structured local entity details error when no scan exists', async () => { + const project = await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' }); + project.config.connections.warehouse = { + driver: 'postgres', + url: 'env:DATABASE_URL', + }; + const ports = createLocalProjectMcpContextPorts(project); + + await expect( + ports.entityDetails?.read({ + connectionId: 'warehouse', + entities: [{ table: 'public.orders' }], + }), + ).resolves.toMatchObject({ + results: [ + { + ok: false, + connectionId: 'warehouse', + error: { code: 'scan_missing' }, + }, + ], + }); + }); + it('triggers canonical bundle ingest and reads status, report, and replay through MCP ports', async () => { const project = await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' }); project.config.connections.warehouse = { diff --git a/packages/context/src/mcp/local-project-ports.ts b/packages/context/src/mcp/local-project-ports.ts index a3d42f34..936c151b 100644 --- a/packages/context/src/mcp/local-project-ports.ts +++ b/packages/context/src/mcp/local-project-ports.ts @@ -18,6 +18,7 @@ import { import { createLocalKtxEmbeddingProviderFromConfig, KtxIngestEmbeddingPortAdapter } from '../llm/index.js'; import type { KtxLocalProject } from '../project/index.js'; import { + createKtxEntityDetailsService, getLocalScanReport, getLocalScanStatus, type KtxConnectionDriver, @@ -629,6 +630,11 @@ export function createLocalProjectMcpContextPorts( }); }, }, + entityDetails: { + async read(input) { + return createKtxEntityDetailsService(project).read(input); + }, + }, }; if (options.sqlAnalysis && options.localScan?.createConnector) {