diff --git a/packages/context/src/mcp/context-tools.ts b/packages/context/src/mcp/context-tools.ts index e49e99a0..1d5e7009 100644 --- a/packages/context/src/mcp/context-tools.ts +++ b/packages/context/src/mcp/context-tools.ts @@ -143,6 +143,25 @@ const scanArtifactReadSchema = z.object({ path: z.string().min(1), }); +const entityDetailsTableRefSchema = z.object({ + catalog: z.string().nullable(), + db: z.string().nullable(), + name: z.string().min(1), +}); + +const entityDetailsSchema = z.object({ + connectionId: connectionIdSchema, + entities: z + .array( + z.object({ + table: z.union([z.string().min(1), entityDetailsTableRefSchema]), + columns: z.array(z.string().min(1)).optional(), + }), + ) + .min(1) + .max(20), +}); + const sqlExecutionSchema = z.object({ connectionId: connectionIdSchema, sql: z.string().min(1), @@ -367,6 +386,21 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void ); } + if (ports.entityDetails) { + const entityDetails = ports.entityDetails; + registerParsedTool( + server, + 'entity_details', + { + title: 'Entity Details', + description: 'Read raw table and column metadata from the latest KTX live-database scan snapshot.', + inputSchema: entityDetailsSchema.shape, + }, + entityDetailsSchema, + async (input) => jsonToolResult(await entityDetails.read(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 c3f02a66..39e19726 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, + KtxEntityDetailsMcpPort, KtxIngestDiffSummary, KtxIngestMcpPort, KtxIngestStatusResponse, diff --git a/packages/context/src/mcp/server.test.ts b/packages/context/src/mcp/server.test.ts index f21c4595..fed90c0d 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 { + KtxEntityDetailsMcpPort, KtxIngestMcpPort, KtxKnowledgeMcpPort, KtxMcpContextPorts, @@ -123,6 +124,71 @@ describe('createKtxMcpServer', () => { }); }); + it('registers entity_details when the host provides an entity-details port', async () => { + const fake = makeFakeServer(); + const entityDetails: KtxEntityDetailsMcpPort = { + read: vi.fn().mockResolvedValue({ + results: [ + { + ok: true, + connectionId: 'warehouse', + tableRef: { catalog: null, db: 'public', name: 'orders' }, + display: 'public.orders', + kind: 'table', + comment: 'Customer orders', + estimatedRows: 12, + columns: [ + { + name: 'id', + nativeType: 'integer', + normalizedType: 'integer', + dimensionType: 'number', + nullable: false, + primaryKey: true, + comment: null, + }, + ], + foreignKeys: [], + snapshot: { + syncId: 'sync-1', + extractedAt: '2026-05-14T09:00:00.000Z', + scanRunId: 'scan-1', + }, + }, + ], + }), + }; + + createKtxMcpServer({ + server: fake.server, + userContext: { userId: 'local-user' }, + contextTools: { entityDetails }, + }); + + expect(fake.tools.map((tool) => tool.name)).toEqual(['entity_details']); + await expect( + getTool(fake.tools, 'entity_details').handler({ + connectionId: 'warehouse', + entities: [{ table: 'public.orders', columns: ['id'] }], + }), + ).resolves.toMatchObject({ + structuredContent: { + results: [ + { + ok: true, + connectionId: 'warehouse', + display: 'public.orders', + columns: [{ name: 'id' }], + }, + ], + }, + }); + expect(entityDetails.read).toHaveBeenCalledWith({ + connectionId: 'warehouse', + entities: [{ table: 'public.orders', columns: ['id'] }], + }); + }); + 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 df97e3ef..b7bb1d3f 100644 --- a/packages/context/src/mcp/types.ts +++ b/packages/context/src/mcp/types.ts @@ -1,5 +1,6 @@ import type { IngestReportSnapshot, MemoryFlowReplayInput, TableUsageOutput } from '../ingest/index.js'; 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 { SemanticLayerQueryInput, @@ -312,6 +313,10 @@ export interface KtxScanMcpPort { readArtifact?(input: { runId: string; path: string }): Promise; } +export interface KtxEntityDetailsMcpPort { + read(input: KtxEntityDetailsInput): Promise; +} + export interface KtxSqlExecutionResponse { headers: string[]; headerTypes?: string[]; @@ -327,6 +332,7 @@ export interface KtxMcpContextPorts { connections?: KtxConnectionsMcpPort; knowledge?: KtxKnowledgeMcpPort; semanticLayer?: KtxSemanticLayerMcpPort; + entityDetails?: KtxEntityDetailsMcpPort; sqlExecution?: KtxSqlExecutionMcpPort; ingest?: KtxIngestMcpPort; scan?: KtxScanMcpPort;