diff --git a/packages/context/src/mcp/local-project-ports.test.ts b/packages/context/src/mcp/local-project-ports.test.ts index 845055d6..1f275bd6 100644 --- a/packages/context/src/mcp/local-project-ports.test.ts +++ b/packages/context/src/mcp/local-project-ports.test.ts @@ -366,6 +366,88 @@ describe('createLocalProjectMcpContextPorts', () => { }); }); + it('exposes local dictionary search through MCP ports', async () => { + const project = await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' }); + project.config.connections.warehouse = { + driver: 'postgres', + url: 'env:DATABASE_URL', + }; + await project.fileStore.writeFile( + 'raw-sources/warehouse/live-database/sync-1/enrichment/relationship-profile.json', + `${JSON.stringify( + { + connectionId: 'warehouse', + driver: 'postgres', + sqlAvailable: true, + queryCount: 4, + tables: [], + columns: { + 'orders.status': { + table: { catalog: null, db: 'public', name: 'orders' }, + column: 'status', + nativeType: 'text', + normalizedType: 'string', + distinctCount: 2, + sampleValues: ['paid', 'refunded'], + }, + }, + warnings: [], + }, + null, + 2, + )}\n`, + 'ktx', + 'ktx@example.com', + 'Seed dictionary profile', + ); + + const ports = createLocalProjectMcpContextPorts(project); + + await expect(ports.dictionarySearch?.search({ values: ['paid'] })).resolves.toMatchObject({ + searched: [{ connectionId: 'warehouse', status: 'ready' }], + results: [ + { + value: 'paid', + matches: [{ connectionId: 'warehouse', sourceName: 'orders', columnName: 'status', matchedValue: 'paid' }], + misses: [], + }, + ], + }); + }); + + it('reports missing local dictionary profiles through MCP ports', 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.dictionarySearch?.search({ values: ['paid'] })).resolves.toEqual({ + searched: [ + { + connectionId: 'warehouse', + coverage: { + sampledRows: null, + valuesPerColumn: null, + profiledColumns: 0, + syncId: null, + profiledAt: null, + }, + status: 'no_profile_artifact', + }, + ], + results: [ + { + value: 'paid', + matches: [], + misses: [{ connectionId: 'warehouse', reason: 'no_profile_artifact' }], + }, + ], + }); + }); + 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 936c151b..6a8fac93 100644 --- a/packages/context/src/mcp/local-project-ports.ts +++ b/packages/context/src/mcp/local-project-ports.ts @@ -30,6 +30,7 @@ import { import type { SqlAnalysisDialect, SqlAnalysisPort } from '../sql-analysis/index.js'; import { compileLocalSlQuery, + createKtxDictionarySearchService, type LocalSlSourceSearchResult, type LocalSlSourceSummary, listLocalSlSources, @@ -635,6 +636,11 @@ export function createLocalProjectMcpContextPorts( return createKtxEntityDetailsService(project).read(input); }, }, + dictionarySearch: { + async search(input) { + return createKtxDictionarySearchService(project).search(input); + }, + }, }; if (options.sqlAnalysis && options.localScan?.createConnector) {