diff --git a/packages/context/src/mcp/context-tools.ts b/packages/context/src/mcp/context-tools.ts index a6ba2258..773155bf 100644 --- a/packages/context/src/mcp/context-tools.ts +++ b/packages/context/src/mcp/context-tools.ts @@ -167,6 +167,15 @@ const dictionarySearchSchema = z.object({ connectionId: connectionIdSchema.optional(), }); +const discoverDataKindSchema = z.enum(['wiki', 'sl_source', 'sl_measure', 'sl_dimension', 'table', 'column']); + +const discoverDataSchema = z.object({ + query: z.string().min(1), + connectionId: connectionIdSchema.optional(), + kinds: z.array(discoverDataKindSchema).optional(), + limit: z.number().int().min(1).max(50).default(15).optional(), +}); + const sqlExecutionSchema = z.object({ connectionId: connectionIdSchema, sql: z.string().min(1), @@ -422,6 +431,22 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void ); } + if (ports.discover) { + const discover = ports.discover; + registerParsedTool( + server, + 'discover_data', + { + title: 'Discover Data', + description: + 'Search across KTX wiki pages, semantic-layer sources/measures/dimensions, and raw warehouse schema refs.', + inputSchema: discoverDataSchema.shape, + }, + discoverDataSchema, + async (input) => jsonToolResult(await discover.search(input)), + ); + } + if (ports.sqlExecution) { const sqlExecution = ports.sqlExecution; registerParsedTool( diff --git a/packages/context/src/mcp/index.ts b/packages/context/src/mcp/index.ts index a78d74ba..df1bc6c5 100644 --- a/packages/context/src/mcp/index.ts +++ b/packages/context/src/mcp/index.ts @@ -5,6 +5,7 @@ export { createDefaultKtxMcpServer, createKtxMcpServer } from './server.js'; export type { KtxConnectionSummary, KtxConnectionsMcpPort, + KtxDiscoverDataMcpPort, KtxDictionarySearchMcpPort, KtxEntityDetailsMcpPort, KtxIngestDiffSummary, diff --git a/packages/context/src/mcp/server.test.ts b/packages/context/src/mcp/server.test.ts index b65b9747..0e3f93dc 100644 --- a/packages/context/src/mcp/server.test.ts +++ b/packages/context/src/mcp/server.test.ts @@ -6,6 +6,7 @@ import { createLocalProjectMemoryCapture } from '../memory/index.js'; import { initKtxProject } from '../project/index.js'; import { createKtxMcpServer } from './server.js'; import type { + KtxDiscoverDataMcpPort, KtxDictionarySearchMcpPort, KtxEntityDetailsMcpPort, KtxIngestMcpPort, @@ -255,6 +256,55 @@ describe('createKtxMcpServer', () => { }); }); + it('registers discover_data when the host provides a discover port', async () => { + const fake = makeFakeServer(); + const discover: KtxDiscoverDataMcpPort = { + search: vi.fn().mockResolvedValue([ + { + kind: 'table', + id: 'public.orders', + score: 1, + summary: 'Orders table', + snippet: 'id, status', + matchedOn: 'name', + connectionId: 'warehouse', + tableRef: { catalog: null, db: 'public', name: 'orders' }, + }, + ]), + }; + + createKtxMcpServer({ + server: fake.server, + userContext: { userId: 'local-user' }, + contextTools: { discover }, + }); + + expect(fake.tools.map((tool) => tool.name)).toEqual(['discover_data']); + await expect( + getTool(fake.tools, 'discover_data').handler({ + query: 'orders', + connectionId: 'warehouse', + kinds: ['table'], + limit: 5, + }), + ).resolves.toMatchObject({ + structuredContent: [ + { + kind: 'table', + id: 'public.orders', + connectionId: 'warehouse', + tableRef: { catalog: null, db: 'public', name: 'orders' }, + }, + ], + }); + expect(discover.search).toHaveBeenCalledWith({ + query: 'orders', + connectionId: 'warehouse', + kinds: ['table'], + limit: 5, + }); + }); + it('registers memory capture tools without host app dependencies', async () => { const fake = makeFakeServer(); const capture: MemoryCapturePort = { diff --git a/packages/context/src/mcp/types.ts b/packages/context/src/mcp/types.ts index 0171319f..ab53f56e 100644 --- a/packages/context/src/mcp/types.ts +++ b/packages/context/src/mcp/types.ts @@ -2,6 +2,7 @@ import type { IngestReportSnapshot, MemoryFlowReplayInput, TableUsageOutput } fr import type { MemoryCaptureService } from '../memory/index.js'; import type { KtxEntityDetailsInput, KtxEntityDetailsResponse } from '../scan/entity-details.js'; import type { KtxScanMode, KtxScanReport } from '../scan/index.js'; +import type { KtxDiscoverDataInput, KtxDiscoverDataResponse } from '../search/index.js'; import type { KtxDictionarySearchInput, KtxDictionarySearchResponse, @@ -323,6 +324,10 @@ export interface KtxDictionarySearchMcpPort { search(input: KtxDictionarySearchInput): Promise; } +export interface KtxDiscoverDataMcpPort { + search(input: KtxDiscoverDataInput): Promise; +} + export interface KtxSqlExecutionResponse { headers: string[]; headerTypes?: string[]; @@ -340,6 +345,7 @@ export interface KtxMcpContextPorts { semanticLayer?: KtxSemanticLayerMcpPort; entityDetails?: KtxEntityDetailsMcpPort; dictionarySearch?: KtxDictionarySearchMcpPort; + discover?: KtxDiscoverDataMcpPort; sqlExecution?: KtxSqlExecutionMcpPort; ingest?: KtxIngestMcpPort; scan?: KtxScanMcpPort;