From 337c083f050d526d831278d906db3b66903ba921 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Sat, 16 May 2026 02:11:01 +0200 Subject: [PATCH] feat(mcp): slim research tool surface --- packages/context/src/mcp/context-tools.ts | 347 ++---------- packages/context/src/mcp/server.test.ts | 632 ++++------------------ packages/context/src/mcp/server.ts | 68 +-- packages/context/src/mcp/types.ts | 236 +------- 4 files changed, 148 insertions(+), 1135 deletions(-) diff --git a/packages/context/src/mcp/context-tools.ts b/packages/context/src/mcp/context-tools.ts index d8a567a1..f3b7e97b 100644 --- a/packages/context/src/mcp/context-tools.ts +++ b/packages/context/src/mcp/context-tools.ts @@ -1,4 +1,6 @@ +import { randomUUID } from 'node:crypto'; import { z } from 'zod'; +import type { MemoryAgentInput } from '../memory/index.js'; import type { KtxMcpContextPorts, KtxMcpServerLike, KtxMcpToolResult, KtxMcpUserContext } from './types.js'; export interface RegisterKtxContextToolsDeps { @@ -11,10 +13,6 @@ const connectionIdSchema = z.string().min(1); const connectionListSchema = z.object({}); -const connectionTestSchema = z.object({ - connectionId: connectionIdSchema, -}); - const knowledgeSearchSchema = z.object({ query: z.string().min(1), limit: z.number().int().min(1).max(50).default(10), @@ -24,55 +22,11 @@ const knowledgeReadSchema = z.object({ key: z.string().min(1), }); -const historicSqlUsageFrontmatterSchema = z.object({ - executions: z.number().int().nonnegative(), - distinct_users: z.number().int().nonnegative(), - first_seen: z.string().min(1), - last_seen: z.string().min(1), - p50_runtime_ms: z.number().nonnegative().nullable(), - p95_runtime_ms: z.number().nonnegative().nullable(), - error_rate: z.number().min(0).max(1), - rows_produced: z.number().int().nonnegative().optional(), -}); - -const knowledgeWriteSchema = z.object({ - key: z.string().min(1).max(120), - summary: z.string().min(1).max(200), - content: z.string().min(1), - tags: z.array(z.string()).optional(), - refs: z.array(z.string()).optional(), - sl_refs: z.array(z.string()).optional(), - source: z.string().optional(), - intent: z.string().optional(), - tables: z.array(z.string()).optional(), - representative_sql: z.string().optional(), - usage: historicSqlUsageFrontmatterSchema.optional(), - fingerprints: z.array(z.string()).optional(), -}); - -const slListSourcesSchema = z.object({ - connectionId: connectionIdSchema.optional(), - query: z.string().min(1).optional(), -}); - const slReadSourceSchema = z.object({ connectionId: connectionIdSchema, sourceName: z.string().min(1), }); -const slWriteSourceSchema = z.object({ - connectionId: connectionIdSchema, - sourceName: z.string().regex(/^[a-z0-9][a-z0-9_]*$/, 'Source name must be snake_case'), - yaml: z.string().min(1).optional(), - source: z.record(z.string(), z.unknown()).optional(), - delete: z.boolean().optional(), -}); - -const slValidateSchema = z.object({ - connectionId: connectionIdSchema, - names: z.array(z.string().min(1)).optional(), -}); - const slQueryMeasureSchema = z.union([ z.string(), z.object({ @@ -131,41 +85,6 @@ const slQuerySchema = z.object({ include_empty: z.boolean().default(true), }); -const ingestTriggerSchema = z.object({ - adapter: z.string().min(1), - connectionId: connectionIdSchema, - config: z.unknown().optional(), - trigger: z.enum(['upload', 'scheduled_pull', 'manual_resync']).default('manual_resync'), -}); - -const ingestStatusSchema = z.object({ - runId: z.string().min(1), -}); - -const ingestReportSchema = z.object({ - runId: z.string().min(1), -}); - -const ingestReplaySchema = z.object({ - runId: z.string().min(1), -}); - -const scanTriggerSchema = z.object({ - connectionId: connectionIdSchema, - mode: z.enum(['structural', 'relationships', 'enriched']).default('structural'), - detectRelationships: z.boolean().default(false), - dryRun: z.boolean().default(false), -}); - -const scanStatusSchema = z.object({ - runId: z.string().min(1), -}); - -const scanArtifactReadSchema = z.object({ - runId: z.string().min(1), - path: z.string().min(1), -}); - const entityDetailsTableRefSchema = z.object({ catalog: z.string().nullable(), db: z.string().nullable(), @@ -205,6 +124,24 @@ const sqlExecutionSchema = z.object({ maxRows: z.number().int().min(1).max(10_000).default(1000).optional(), }); +const memoryIngestSchema = z.object({ + content: z + .string() + .min(1) + .describe( + 'Free-form markdown to ingest. Include the knowledge itself plus any context (source, the user question, why this came up) that the memory agent should consider when triaging into wiki/SL.', + ), + connectionId: connectionIdSchema + .optional() + .describe( + 'Scope this memory to a specific connection. Required when the knowledge is warehouse-specific, including measure definitions, schema gotchas, or anything tied to a particular warehouse. Omit only for global wiki knowledge.', + ), +}); + +const memoryIngestStatusSchema = z.object({ + runId: z.string().min(1).describe('The memory ingest run id returned by memory_ingest.'), +}); + export function jsonToolResult(structuredContent: T): KtxMcpToolResult { return { content: [{ type: 'text', text: JSON.stringify(structuredContent, null, 2) }], @@ -245,25 +182,6 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void connectionListSchema, async () => jsonToolResult({ connections: await connections.list() }), ); - - if (connections.test) { - registerParsedTool( - server, - 'connection_test', - { - title: 'Connection Test', - description: 'Test a configured standalone KTX connection through the host-provided scan connector.', - inputSchema: connectionTestSchema.shape, - }, - connectionTestSchema, - async (input) => { - const result = await connections.test?.({ connectionId: input.connectionId }); - return result - ? jsonToolResult(result) - : jsonErrorToolResult(`Connection "${input.connectionId}" was not found.`); - }, - ); - } } if (ports.knowledge) { @@ -301,51 +219,10 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void return page ? jsonToolResult(page) : jsonErrorToolResult(`Wiki page "${input.key}" was not found.`); }, ); - - registerParsedTool( - server, - 'wiki_write', - { - title: 'Wiki Write', - description: 'Create or replace a KTX wiki page and its SL references.', - inputSchema: knowledgeWriteSchema.shape, - }, - knowledgeWriteSchema, - async (input) => - jsonToolResult( - await knowledge.write({ - userId: userContext.userId, - key: input.key, - summary: input.summary, - content: input.content, - tags: input.tags, - refs: input.refs, - slRefs: input.sl_refs, - source: input.source, - intent: input.intent, - tables: input.tables, - representativeSql: input.representative_sql, - usage: input.usage, - fingerprints: input.fingerprints, - }), - ), - ); } if (ports.semanticLayer) { const semanticLayer = ports.semanticLayer; - registerParsedTool( - server, - 'sl_list_sources', - { - title: 'Semantic Layer List Sources', - description: 'List semantic-layer sources, optionally filtered by connection or search query.', - inputSchema: slListSourcesSchema.shape, - }, - slListSourcesSchema, - async (input) => jsonToolResult(await semanticLayer.listSources(input)), - ); - registerParsedTool( server, 'sl_read_source', @@ -363,39 +240,6 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void }, ); - registerParsedTool( - server, - 'sl_write_source', - { - title: 'Semantic Layer Write Source', - description: 'Create, replace, or delete a semantic-layer source.', - inputSchema: slWriteSourceSchema.shape, - }, - slWriteSourceSchema, - async (input) => - jsonToolResult( - await semanticLayer.writeSource({ - connectionId: input.connectionId, - sourceName: input.sourceName, - yaml: input.yaml, - source: input.source, - delete: input.delete, - }), - ), - ); - - registerParsedTool( - server, - 'sl_validate', - { - title: 'Semantic Layer Validate', - description: 'Validate semantic-layer sources for a connection.', - inputSchema: slValidateSchema.shape, - }, - slValidateSchema, - async (input) => jsonToolResult(await semanticLayer.validate(input)), - ); - registerParsedTool( server, 'sl_query', @@ -501,149 +345,44 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void ); } - if (ports.ingest) { - const ingest = ports.ingest; + if (ports.memoryIngest) { + const memoryIngest = ports.memoryIngest; registerParsedTool( server, - 'ingest_trigger', + 'memory_ingest', { - title: 'Ingest Trigger', - description: 'Trigger a KTX ingest run for an adapter and connection.', - inputSchema: ingestTriggerSchema.shape, - }, - ingestTriggerSchema, - async (input) => jsonToolResult(await ingest.trigger(input)), - ); - - registerParsedTool( - server, - 'ingest_status', - { - title: 'Ingest Status', + title: 'Memory Ingest', description: - 'Read the current or final status for an ingest run, including local diff and work-unit summaries when available.', - inputSchema: ingestStatusSchema.shape, + 'Ingest free-form markdown knowledge into KTX durable memory. Use this for business rules, metric definitions, schema gotchas, recurring findings, or explicit user requests to remember something.', + inputSchema: memoryIngestSchema.shape, }, - ingestStatusSchema, + memoryIngestSchema, async (input) => { - const status = await ingest.status(input); - return status ? jsonToolResult(status) : jsonErrorToolResult(`Ingest run "${input.runId}" was not found.`); - }, - ); - - if (ingest.report) { - registerParsedTool( - server, - 'ingest_report', - { - title: 'Ingest Report', - description: 'Read the stored canonical KTX ingest report for a local run id, job id, or report id.', - inputSchema: ingestReportSchema.shape, - }, - ingestReportSchema, - async (input) => { - const report = await ingest.report?.(input); - return report ? jsonToolResult(report) : jsonErrorToolResult(`Ingest report "${input.runId}" was not found.`); - }, - ); - } - - if (ingest.replay) { - registerParsedTool( - server, - 'ingest_replay', - { - title: 'Ingest Replay', - description: 'Read the memory-flow replay snapshot for a stored canonical KTX ingest run.', - inputSchema: ingestReplaySchema.shape, - }, - ingestReplaySchema, - async (input) => { - const replay = await ingest.replay?.(input); - return replay ? jsonToolResult(replay) : jsonErrorToolResult(`Ingest replay "${input.runId}" was not found.`); - }, - ); - } - } - - if (ports.scan) { - const scan = ports.scan; - registerParsedTool( - server, - 'scan_trigger', - { - title: 'Scan Trigger', - description: 'Run a standalone KTX structural connection scan and return its report summary.', - inputSchema: scanTriggerSchema.shape, - }, - scanTriggerSchema, - async (input) => jsonToolResult(await scan.trigger(input)), - ); - - registerParsedTool( - server, - 'scan_status', - { - title: 'Scan Status', - description: 'Read the current or final status for a standalone KTX scan run.', - inputSchema: scanStatusSchema.shape, - }, - scanStatusSchema, - async (input) => { - const status = await scan.status(input); - return status ? jsonToolResult(status) : jsonErrorToolResult(`Scan run "${input.runId}" was not found.`); + const ingestInput: MemoryAgentInput = { + userId: userContext.userId, + chatId: `mcp-${randomUUID()}`, + userMessage: 'Ingest external knowledge into KTX memory.', + assistantMessage: input.content, + connectionId: input.connectionId, + sourceType: 'external_ingest', + }; + return jsonToolResult(await memoryIngest.ingest(ingestInput)); }, ); registerParsedTool( server, - 'scan_report', + 'memory_ingest_status', { - title: 'Scan Report', - description: 'Read a standalone KTX scan report by run id.', - inputSchema: scanStatusSchema.shape, + title: 'Memory Ingest Status', + description: 'Read the current or final status for a memory ingest run.', + inputSchema: memoryIngestStatusSchema.shape, }, - scanStatusSchema, + memoryIngestStatusSchema, async (input) => { - const report = await scan.report(input); - return report ? jsonToolResult(report) : jsonErrorToolResult(`Scan report "${input.runId}" was not found.`); + const status = await memoryIngest.status(input.runId); + return status ? jsonToolResult(status) : jsonErrorToolResult(`Memory ingest run "${input.runId}" was not found.`); }, ); - - if (scan.listArtifacts) { - registerParsedTool( - server, - 'scan_list_artifacts', - { - title: 'Scan List Artifacts', - description: 'List report, raw-source, manifest, and enrichment artifact paths for a standalone KTX scan run.', - inputSchema: scanStatusSchema.shape, - }, - scanStatusSchema, - async (input) => { - const result = await scan.listArtifacts?.({ runId: input.runId }); - return result ? jsonToolResult(result) : jsonErrorToolResult(`Scan run "${input.runId}" was not found.`); - }, - ); - } - - if (scan.readArtifact) { - registerParsedTool( - server, - 'scan_read_artifact', - { - title: 'Scan Read Artifact', - description: 'Read one artifact that belongs to a standalone KTX scan run.', - inputSchema: scanArtifactReadSchema.shape, - }, - scanArtifactReadSchema, - async (input) => { - const result = await scan.readArtifact?.({ runId: input.runId, path: input.path }); - return result - ? jsonToolResult(result) - : jsonErrorToolResult(`Scan artifact "${input.path}" was not found for run "${input.runId}".`); - }, - ); - } } } diff --git a/packages/context/src/mcp/server.test.ts b/packages/context/src/mcp/server.test.ts index bd582755..608b637b 100644 --- a/packages/context/src/mcp/server.test.ts +++ b/packages/context/src/mcp/server.test.ts @@ -2,21 +2,23 @@ import { access, mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; -import { createLocalProjectMemoryCapture } from '../memory/index.js'; +import { + createLocalProjectMemoryIngest, + detectCaptureSignals, + type MemoryAgentInput, +} from '../memory/index.js'; import { initKtxProject } from '../project/index.js'; import { createKtxMcpServer } from './server.js'; import type { KtxDiscoverDataMcpPort, KtxDictionarySearchMcpPort, KtxEntityDetailsMcpPort, - KtxIngestMcpPort, KtxKnowledgeMcpPort, KtxMcpContextPorts, - KtxScanMcpPort, KtxSemanticLayerMcpPort, KtxSqlExecutionMcpPort, KtxSqlExecutionResponse, - MemoryCapturePort, + MemoryIngestPort, } from './types.js'; type RegisteredTool = { @@ -259,10 +261,7 @@ describe('createKtxMcpServer', () => { it('sl_query normalizes order_by from cube-style {id, desc} and bare strings to {field, direction}', async () => { const fake = makeFakeServer(); const semanticLayer: KtxSemanticLayerMcpPort = { - listSources: vi.fn(), readSource: vi.fn(), - writeSource: vi.fn(), - validate: vi.fn(), query: vi.fn().mockResolvedValue({ sql: '', headers: [], @@ -352,11 +351,15 @@ describe('createKtxMcpServer', () => { }); }); - it('registers memory capture tools without host app dependencies', async () => { + it('registers memory ingest tools through the context tool surface', async () => { const fake = makeFakeServer(); - const capture: MemoryCapturePort = { - capture: vi.fn().mockResolvedValue({ runId: 'run-1' }), - status: vi.fn().mockResolvedValue({ + let receivedInput: MemoryAgentInput | undefined; + const ingest: MemoryIngestPort = { + ingest: vi.fn().mockImplementation(async (input) => { + receivedInput = input; + return { runId: 'run-1' }; + }), + status: vi.fn().mockResolvedValue({ runId: 'run-1', status: 'done', stage: 'done', @@ -371,33 +374,51 @@ describe('createKtxMcpServer', () => { createKtxMcpServer({ server: fake.server, - memoryCapture: capture, userContext: { userId: 'mcp-user' }, + contextTools: { memoryIngest: ingest }, }); - expect(fake.tools.map((tool) => tool.name).sort()).toEqual(['memory_capture', 'memory_capture_status']); + expect(fake.tools.map((tool) => tool.name).sort()).toEqual(['memory_ingest', 'memory_ingest_status']); - const memoryCapture = getTool(fake.tools, 'memory_capture'); + const content = [ + 'view: orders {', + ' sql_table_name: public.orders ;;', + ' measure: gross_revenue {', + ' type: sum', + ' sql: ${TABLE}.gross_revenue_cents ;;', + ' }', + '}', + ].join('\n'); + const memoryIngest = getTool(fake.tools, 'memory_ingest'); await expect( - memoryCapture.handler({ - userMessage: 'Revenue means paid order value.', - assistantMessage: 'Captured.', + memoryIngest.handler({ + content, connectionId: '00000000-0000-4000-8000-000000000001', }), ).resolves.toEqual({ content: [{ type: 'text', text: JSON.stringify({ runId: 'run-1' }, null, 2) }], structuredContent: { runId: 'run-1' }, }); - expect(capture.capture).toHaveBeenCalledWith({ + expect(ingest.ingest).toHaveBeenCalledWith({ userId: 'mcp-user', chatId: expect.stringMatching(/^mcp-/), - userMessage: 'Revenue means paid order value.', - assistantMessage: 'Captured.', + userMessage: 'Ingest external knowledge into KTX memory.', + assistantMessage: content, connectionId: '00000000-0000-4000-8000-000000000001', sourceType: 'external_ingest', }); - const memoryStatus = getTool(fake.tools, 'memory_capture_status'); + const cliEquivalentInput: MemoryAgentInput = { + userId: 'mcp-user', + chatId: 'cli-text-ingest-test-1', + userMessage: 'Ingest external text artifact "orders lookml" into KTX memory.', + assistantMessage: content, + connectionId: '00000000-0000-4000-8000-000000000001', + sourceType: 'external_ingest', + }; + expect(detectCaptureSignals(receivedInput!)).toEqual(detectCaptureSignals(cliEquivalentInput)); + + const memoryStatus = getTool(fake.tools, 'memory_ingest_status'); await expect(memoryStatus.handler({ runId: 'run-1' })).resolves.toEqual({ content: [ { @@ -433,36 +454,40 @@ describe('createKtxMcpServer', () => { }); }); - it('returns an MCP error payload for missing run ids', async () => { + it('returns an in-band error when a memory ingest run is missing', async () => { const fake = makeFakeServer(); - const capture: MemoryCapturePort = { - capture: vi.fn(), - status: vi.fn().mockResolvedValue(null), + const ingest: MemoryIngestPort = { + ingest: vi.fn(), + status: vi.fn().mockResolvedValue(null), }; createKtxMcpServer({ server: fake.server, - memoryCapture: capture, userContext: { userId: 'mcp-user' }, + contextTools: { memoryIngest: ingest }, }); - const memoryStatus = getTool(fake.tools, 'memory_capture_status'); - await expect(memoryStatus.handler({ runId: 'missing' })).resolves.toEqual({ - content: [{ type: 'text', text: 'Memory capture run "missing" was not found.' }], + const memoryStatus = getTool(fake.tools, 'memory_ingest_status'); + await expect(memoryStatus.handler({ runId: 'missing-run' })).resolves.toEqual({ + content: [{ type: 'text', text: 'Memory ingest run "missing-run" was not found.' }], isError: true, }); }); - it('runs MCP memory_capture against a local project memory port', async () => { + it('runs MCP memory_ingest against a local project memory port', async () => { const tempDir = await mkdtemp(join(tmpdir(), 'ktx-mcp-local-memory-')); try { const project = await initKtxProject({ projectDir: tempDir }); + let receivedInput: MemoryAgentInput | undefined; const agentRunner = { runLoop: async ({ + input, toolSet, }: { + input: MemoryAgentInput; toolSet: Record Promise }>; }) => { + receivedInput = input; await toolSet.load_skill.execute({ name: 'wiki_capture' }); await toolSet.wiki_write.execute( { @@ -475,7 +500,7 @@ describe('createKtxMcpServer', () => { return { stopReason: 'natural' as const }; }, }; - const memoryCapture = createLocalProjectMemoryCapture(project, { + const memoryIngest = createLocalProjectMemoryIngest(project, { agentRunner: agentRunner as never, runIdFactory: () => 'memory-run-mcp', }); @@ -483,21 +508,29 @@ describe('createKtxMcpServer', () => { createKtxMcpServer({ server: fake.server, - memoryCapture, - userContext: { userId: 'mcp-user' }, + userContext: { userId: 'local' }, + contextTools: { memoryIngest }, }); - const capture = await getTool(fake.tools, 'memory_capture').handler({ - userMessage: 'define ARR as annual recurring revenue', - assistantMessage: 'Captured.', + const capture = await getTool(fake.tools, 'memory_ingest').handler({ + content: 'Revenue means paid order value.', + connectionId: 'warehouse', }); expect(capture).toMatchObject({ structuredContent: { runId: 'memory-run-mcp' }, }); - await memoryCapture.waitForRun('memory-run-mcp'); + await memoryIngest.waitForRun('memory-run-mcp'); + expect(receivedInput).toMatchObject({ + userId: 'local', + chatId: expect.stringMatching(/^mcp-/), + userMessage: 'Ingest external knowledge into KTX memory.', + assistantMessage: 'Revenue means paid order value.', + connectionId: 'warehouse', + sourceType: 'external_ingest', + }); await expect( - getTool(fake.tools, 'memory_capture_status').handler({ runId: 'memory-run-mcp' }), + getTool(fake.tools, 'memory_ingest_status').handler({ runId: 'memory-run-mcp' }), ).resolves.toMatchObject({ structuredContent: { runId: 'memory-run-mcp', @@ -518,10 +551,6 @@ describe('createKtxMcpServer', () => { it('registers KTX context MCP tools when context ports are supplied', async () => { const fake = makeFakeServer(); - const capture: MemoryCapturePort = { - capture: vi.fn().mockResolvedValue({ runId: 'run-1' }), - status: vi.fn().mockResolvedValue(null), - }; const contextTools: KtxMcpContextPorts = { connections: { list: vi.fn().mockResolvedValue([ @@ -531,14 +560,6 @@ describe('createKtxMcpServer', () => { connectionType: 'POSTGRES', }, ]), - test: vi.fn().mockResolvedValue({ - id: 'warehouse', - connectionType: 'postgres', - ok: true, - tableCount: 2, - message: 'Connection test passed.', - warnings: [], - }), }, knowledge: { search: vi.fn().mockResolvedValue({ @@ -563,42 +584,12 @@ describe('createKtxMcpServer', () => { refs: [], slRefs: ['orders'], }), - write: vi.fn().mockResolvedValue({ - success: true, - key: 'revenue', - action: 'updated', - }), }, semanticLayer: { - listSources: vi.fn().mockResolvedValue({ - sources: [ - { - connectionId: '00000000-0000-4000-8000-000000000001', - connectionName: 'Warehouse', - name: 'orders', - description: 'Order facts', - columnCount: 2, - measureCount: 1, - joinCount: 0, - }, - ], - totalSources: 1, - }), readSource: vi.fn().mockResolvedValue({ sourceName: 'orders', yaml: 'name: orders\n', }), - writeSource: vi.fn().mockResolvedValue({ - success: true, - sourceName: 'orders', - yaml: 'name: orders\n', - commitHash: 'abc123', - }), - validate: vi.fn().mockResolvedValue({ - success: true, - errors: [], - warnings: [], - }), query: vi.fn().mockResolvedValue({ sql: 'select 1', headers: ['count'], @@ -607,221 +598,50 @@ describe('createKtxMcpServer', () => { plan: { sources: ['orders'] }, }), }, - ingest: { - trigger: vi.fn().mockResolvedValue({ - runId: 'run-42', - jobId: 'job-42', - reportId: 'report-42', - }), - status: vi.fn().mockResolvedValue({ - runId: 'run-42', - jobId: 'job-42', - reportId: 'report-42', - status: 'done', - stage: 'done', - progress: 1, - done: true, - adapter: 'fake', - connectionId: 'warehouse', - sourceDir: '/tmp/upload', - syncId: '2026-04-27-120000-run-42', - startedAt: '2026-04-27T12:00:00.000Z', - completedAt: '2026-04-27T12:00:01.000Z', - previousRunId: 'run-41', - diffSummary: { - added: 0, - modified: 1, - deleted: 0, - unchanged: 3, - }, - rawFileCount: 4, - workUnitCount: 1, - workUnits: [ - { - unitKey: 'fake-orders', - rawFiles: ['orders/orders.json'], - peerFileIndex: [], - dependencyPaths: [], - }, - ], - evictionDeletedRawPaths: [], - errors: [], - }), - report: vi.fn>().mockResolvedValue({ - id: 'report-42', - runId: 'run-42', - jobId: 'job-42', - connectionId: 'warehouse', - sourceKey: 'fake', - createdAt: '2026-04-27T12:00:01.000Z', - body: { - syncId: '2026-04-27-120000-run-42', - diffSummary: { added: 0, modified: 1, deleted: 0, unchanged: 3 }, - commitSha: null, - workUnits: [], - failedWorkUnits: [], - reconciliationSkipped: false, - conflictsResolved: [], - evictionsApplied: [], - unmappedFallbacks: [], - evictionInputs: [], - unresolvedCards: [], - supersededBy: null, - overrideOf: null, - provenanceRows: [], - toolTranscripts: [], - }, - }), - replay: vi.fn>().mockResolvedValue({ - runId: 'run-42', - reportId: 'report-42', - reportPath: 'report-42', - connectionId: 'warehouse', - adapter: 'fake', - status: 'done', - sourceDir: null, - syncId: '2026-04-27-120000-run-42', - errors: [], - events: [{ type: 'report_created', runId: 'run-42', reportPath: 'report-42' }], - plannedWorkUnits: [], - details: { actions: [], provenance: [], transcripts: [] }, + entityDetails: { + read: vi.fn().mockResolvedValue({ results: [] }), + }, + dictionarySearch: { + search: vi.fn().mockResolvedValue({ + searched: [], + results: [], }), }, - scan: { - trigger: vi.fn().mockResolvedValue({ - runId: 'scan-run-1', - status: 'done', - done: true, - connectionId: 'warehouse', - mode: 'structural', - dryRun: false, - syncId: 'sync-1', - report: { - connectionId: 'warehouse', - driver: 'postgres', - syncId: 'sync-1', - runId: 'scan-run-1', - trigger: 'mcp', - mode: 'structural', - dryRun: false, - artifactPaths: { - rawSourcesDir: 'raw-sources/warehouse/live-database/sync-1', - reportPath: 'raw-sources/warehouse/live-database/sync-1/scan-report.json', - manifestShards: [], - enrichmentArtifacts: [], - }, - diffSummary: { - tablesAdded: 1, - tablesModified: 0, - tablesDeleted: 0, - tablesUnchanged: 0, - columnsAdded: 0, - columnsModified: 0, - columnsDeleted: 0, - }, - manifestShardsWritten: 0, - structuralSyncStats: { - tablesCreated: 0, - tablesUpdated: 0, - tablesDeleted: 0, - columnsCreated: 0, - columnsUpdated: 0, - columnsDeleted: 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-04-29T09:00:00.000Z', - }, - }), - status: vi.fn().mockResolvedValue({ - runId: 'scan-run-1', - status: 'done', - done: true, - connectionId: 'warehouse', - mode: 'structural', - dryRun: false, - syncId: 'sync-1', - progress: 1, - startedAt: '2026-04-29T09:00:00.000Z', - completedAt: '2026-04-29T09:00:01.000Z', - reportPath: 'raw-sources/warehouse/live-database/sync-1/scan-report.json', - warnings: [], - }), - report: vi.fn().mockResolvedValue(null), - listArtifacts: vi.fn>().mockResolvedValue({ - runId: 'scan-run-1', - artifacts: [ - { - path: 'raw-sources/warehouse/live-database/sync-1/scan-report.json', - type: 'report', - size: 128, - }, - { - path: 'raw-sources/warehouse/live-database/sync-1/tables/orders.json', - type: 'raw_source', - size: 64, - }, - ], - }), - readArtifact: vi.fn>().mockImplementation(async (input) => { - if (input.path !== 'raw-sources/warehouse/live-database/sync-1/tables/orders.json') { - return null; - } - return { - runId: input.runId, - path: input.path, - type: 'raw_source', - size: 64, - content: '{"name":"orders"}\n', - }; + discover: { + search: vi.fn().mockResolvedValue([]), + }, + sqlExecution: { + execute: vi.fn().mockResolvedValue({ + headers: ['count'], + headerTypes: ['integer'], + rows: [[1]], + rowCount: 1, }), }, + memoryIngest: { + ingest: vi.fn().mockResolvedValue({ runId: 'run-1' }), + status: vi.fn().mockResolvedValue(null), + }, }; createKtxMcpServer({ server: fake.server, - memoryCapture: capture, userContext: { userId: 'mcp-user' }, contextTools, }); expect(fake.tools.map((tool) => tool.name).sort()).toEqual([ 'connection_list', - 'connection_test', - 'ingest_replay', - 'ingest_report', - 'ingest_status', - 'ingest_trigger', - 'memory_capture', - 'memory_capture_status', - 'scan_list_artifacts', - 'scan_read_artifact', - 'scan_report', - 'scan_status', - 'scan_trigger', - 'sl_list_sources', + 'dictionary_search', + 'discover_data', + 'entity_details', + 'memory_ingest', + 'memory_ingest_status', 'sl_query', 'sl_read_source', - 'sl_validate', - 'sl_write_source', + 'sql_execution', 'wiki_read', 'wiki_search', - 'wiki_write', ]); await expect(getTool(fake.tools, 'connection_list').handler({})).resolves.toEqual({ @@ -854,35 +674,6 @@ describe('createKtxMcpServer', () => { }, }); - await expect(getTool(fake.tools, 'connection_test').handler({ connectionId: 'warehouse' })).resolves.toEqual({ - content: [ - { - type: 'text', - text: JSON.stringify( - { - id: 'warehouse', - connectionType: 'postgres', - ok: true, - tableCount: 2, - message: 'Connection test passed.', - warnings: [], - }, - null, - 2, - ), - }, - ], - structuredContent: { - id: 'warehouse', - connectionType: 'postgres', - ok: true, - tableCount: 2, - message: 'Connection test passed.', - warnings: [], - }, - }); - expect(contextTools.connections?.test).toHaveBeenCalledWith({ connectionId: 'warehouse' }); - await getTool(fake.tools, 'wiki_search').handler({ query: 'revenue', limit: 5 }); expect(contextTools.knowledge?.search).toHaveBeenCalledWith({ userId: 'mcp-user', @@ -896,33 +687,6 @@ describe('createKtxMcpServer', () => { key: 'revenue', }); - await getTool(fake.tools, 'wiki_write').handler({ - key: 'revenue', - summary: 'Paid order value', - content: '# Revenue', - tags: ['finance'], - refs: ['gross-margin'], - sl_refs: ['orders'], - }); - expect(contextTools.knowledge?.write).toHaveBeenCalledWith({ - userId: 'mcp-user', - key: 'revenue', - summary: 'Paid order value', - content: '# Revenue', - tags: ['finance'], - refs: ['gross-margin'], - slRefs: ['orders'], - }); - - await getTool(fake.tools, 'sl_list_sources').handler({ - connectionId: '00000000-0000-4000-8000-000000000001', - query: 'orders', - }); - expect(contextTools.semanticLayer?.listSources).toHaveBeenCalledWith({ - connectionId: '00000000-0000-4000-8000-000000000001', - query: 'orders', - }); - await getTool(fake.tools, 'sl_read_source').handler({ connectionId: 'warehouse', sourceName: 'orders', @@ -932,28 +696,6 @@ describe('createKtxMcpServer', () => { sourceName: 'orders', }); - await getTool(fake.tools, 'sl_write_source').handler({ - connectionId: '00000000-0000-4000-8000-000000000001', - sourceName: 'orders', - source: { name: 'orders', table: 'public.orders', grain: ['id'], columns: [], joins: [], measures: [] }, - }); - expect(contextTools.semanticLayer?.writeSource).toHaveBeenCalledWith({ - connectionId: '00000000-0000-4000-8000-000000000001', - sourceName: 'orders', - source: { name: 'orders', table: 'public.orders', grain: ['id'], columns: [], joins: [], measures: [] }, - yaml: undefined, - delete: undefined, - }); - - await getTool(fake.tools, 'sl_validate').handler({ - connectionId: '00000000-0000-4000-8000-000000000001', - names: ['orders'], - }); - expect(contextTools.semanticLayer?.validate).toHaveBeenCalledWith({ - connectionId: '00000000-0000-4000-8000-000000000001', - names: ['orders'], - }); - await getTool(fake.tools, 'sl_query').handler({ connectionId: '00000000-0000-4000-8000-000000000001', measures: ['orders.count'], @@ -973,185 +715,5 @@ describe('createKtxMcpServer', () => { include_empty: true, }, }); - - await getTool(fake.tools, 'ingest_trigger').handler({ - adapter: 'lookml', - connectionId: '00000000-0000-4000-8000-000000000001', - trigger: 'scheduled_pull', - config: { repoUrl: 'https://github.com/acme/looker.git' }, - }); - expect(contextTools.ingest?.trigger).toHaveBeenCalledWith({ - adapter: 'lookml', - connectionId: '00000000-0000-4000-8000-000000000001', - trigger: 'scheduled_pull', - config: { repoUrl: 'https://github.com/acme/looker.git' }, - }); - - expect(getTool(fake.tools, 'ingest_status').config.description).toBe( - 'Read the current or final status for an ingest run, including local diff and work-unit summaries when available.', - ); - - await expect(getTool(fake.tools, 'ingest_status').handler({ runId: 'run-42' })).resolves.toMatchObject({ - structuredContent: { - runId: 'run-42', - status: 'done', - stage: 'done', - progress: 1, - done: true, - adapter: 'fake', - connectionId: 'warehouse', - sourceDir: '/tmp/upload', - syncId: '2026-04-27-120000-run-42', - previousRunId: 'run-41', - diffSummary: { - added: 0, - modified: 1, - deleted: 0, - unchanged: 3, - }, - rawFileCount: 4, - workUnitCount: 1, - workUnits: [ - { - unitKey: 'fake-orders', - rawFiles: ['orders/orders.json'], - peerFileIndex: [], - dependencyPaths: [], - }, - ], - evictionDeletedRawPaths: [], - errors: [], - }, - }); - expect(contextTools.ingest?.status).toHaveBeenCalledWith({ runId: 'run-42' }); - - await expect(getTool(fake.tools, 'ingest_report').handler({ runId: 'report-42' })).resolves.toMatchObject({ - structuredContent: { - id: 'report-42', - runId: 'run-42', - jobId: 'job-42', - sourceKey: 'fake', - }, - }); - expect(contextTools.ingest?.report).toHaveBeenCalledWith({ runId: 'report-42' }); - - await expect(getTool(fake.tools, 'ingest_replay').handler({ runId: 'run-42' })).resolves.toMatchObject({ - structuredContent: { - runId: 'run-42', - reportId: 'report-42', - status: 'done', - adapter: 'fake', - }, - }); - expect(contextTools.ingest?.replay).toHaveBeenCalledWith({ runId: 'run-42' }); - - await getTool(fake.tools, 'scan_trigger').handler({ - connectionId: 'warehouse', - mode: 'structural', - dryRun: true, - }); - expect(contextTools.scan?.trigger).toHaveBeenCalledWith({ - connectionId: 'warehouse', - mode: 'structural', - detectRelationships: false, - dryRun: true, - }); - - await getTool(fake.tools, 'scan_trigger').handler({ - connectionId: 'warehouse', - mode: 'relationships', - detectRelationships: true, - dryRun: false, - }); - expect(contextTools.scan?.trigger).toHaveBeenCalledWith({ - connectionId: 'warehouse', - mode: 'relationships', - detectRelationships: true, - dryRun: false, - }); - - await expect(getTool(fake.tools, 'scan_status').handler({ runId: 'scan-run-1' })).resolves.toMatchObject({ - structuredContent: { - runId: 'scan-run-1', - status: 'done', - connectionId: 'warehouse', - }, - }); - - await expect(getTool(fake.tools, 'scan_report').handler({ runId: 'missing' })).resolves.toEqual({ - content: [{ type: 'text', text: 'Scan report "missing" was not found.' }], - isError: true, - }); - - await expect(getTool(fake.tools, 'scan_list_artifacts').handler({ runId: 'scan-run-1' })).resolves.toEqual({ - content: [ - { - type: 'text', - text: JSON.stringify( - { - runId: 'scan-run-1', - artifacts: [ - { - path: 'raw-sources/warehouse/live-database/sync-1/scan-report.json', - type: 'report', - size: 128, - }, - { - path: 'raw-sources/warehouse/live-database/sync-1/tables/orders.json', - type: 'raw_source', - size: 64, - }, - ], - }, - null, - 2, - ), - }, - ], - structuredContent: { - runId: 'scan-run-1', - artifacts: [ - { - path: 'raw-sources/warehouse/live-database/sync-1/scan-report.json', - type: 'report', - size: 128, - }, - { - path: 'raw-sources/warehouse/live-database/sync-1/tables/orders.json', - type: 'raw_source', - size: 64, - }, - ], - }, - }); - expect(contextTools.scan?.listArtifacts).toHaveBeenCalledWith({ runId: 'scan-run-1' }); - - await expect( - getTool(fake.tools, 'scan_read_artifact').handler({ - runId: 'scan-run-1', - path: 'raw-sources/warehouse/live-database/sync-1/tables/orders.json', - }), - ).resolves.toMatchObject({ - structuredContent: { - runId: 'scan-run-1', - path: 'raw-sources/warehouse/live-database/sync-1/tables/orders.json', - type: 'raw_source', - content: '{"name":"orders"}\n', - }, - }); - expect(contextTools.scan?.readArtifact).toHaveBeenCalledWith({ - runId: 'scan-run-1', - path: 'raw-sources/warehouse/live-database/sync-1/tables/orders.json', - }); - - await expect( - getTool(fake.tools, 'scan_read_artifact').handler({ - runId: 'scan-run-1', - path: 'ktx.yaml', - }), - ).resolves.toEqual({ - content: [{ type: 'text', text: 'Scan artifact "ktx.yaml" was not found for run "scan-run-1".' }], - isError: true, - }); }); }); diff --git a/packages/context/src/mcp/server.ts b/packages/context/src/mcp/server.ts index ba11c086..8931650f 100644 --- a/packages/context/src/mcp/server.ts +++ b/packages/context/src/mcp/server.ts @@ -1,71 +1,8 @@ -import { randomUUID } from 'node:crypto'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { z } from 'zod'; -import type { MemoryAgentInput } from '../memory/index.js'; -import { jsonErrorToolResult, jsonToolResult, registerKtxContextTools } from './context-tools.js'; -import type { KtxMcpServerDeps, KtxMcpServerLike, MemoryCapturePort } from './types.js'; - -const memoryCaptureInputSchema = { - userMessage: z.string().min(1).describe('The user message that may contain durable knowledge.'), - assistantMessage: z.string().optional().describe('The assistant response that concluded the exchange.'), - connectionId: z.string().min(1).optional().describe('Optional connection id for semantic-layer capture.'), -}; - -const memoryCaptureStatusInputSchema = { - runId: z.string().min(1).describe('The memory capture run id returned by memory_capture.'), -}; - -function registerMemoryCaptureTools(deps: { - server: KtxMcpServerLike; - memoryCapture: MemoryCapturePort; - userContext: KtxMcpServerDeps['userContext']; -}): void { - deps.server.registerTool( - 'memory_capture', - { - title: 'Memory Capture', - description: - 'Capture durable knowledge and semantic-layer updates from the final user/assistant exchange. Returns a run id for polling.', - inputSchema: memoryCaptureInputSchema, - }, - async (input) => { - const captureInput: MemoryAgentInput = { - userId: deps.userContext.userId, - chatId: `mcp-${randomUUID()}`, - userMessage: String(input.userMessage), - assistantMessage: typeof input.assistantMessage === 'string' ? input.assistantMessage : undefined, - connectionId: typeof input.connectionId === 'string' ? input.connectionId : undefined, - sourceType: 'external_ingest', - }; - const result = await deps.memoryCapture.capture(captureInput); - return jsonToolResult(result); - }, - ); - - deps.server.registerTool( - 'memory_capture_status', - { - title: 'Memory Capture Status', - description: 'Read the current or final status for a memory capture run.', - inputSchema: memoryCaptureStatusInputSchema, - }, - async (input) => { - const runId = String(input.runId); - const status = await deps.memoryCapture.status(runId); - return status ? jsonToolResult(status) : jsonErrorToolResult(`Memory capture run "${runId}" was not found.`); - }, - ); -} +import { registerKtxContextTools } from './context-tools.js'; +import type { KtxMcpServerDeps, KtxMcpServerLike } from './types.js'; export function createKtxMcpServer(deps: KtxMcpServerDeps): KtxMcpServerDeps['server'] { - if (deps.memoryCapture) { - registerMemoryCaptureTools({ - server: deps.server, - memoryCapture: deps.memoryCapture, - userContext: deps.userContext, - }); - } - if (deps.contextTools) { registerKtxContextTools({ server: deps.server, @@ -86,7 +23,6 @@ export function createDefaultKtxMcpServer( }); createKtxMcpServer({ server: server as KtxMcpServerLike, - memoryCapture: deps.memoryCapture, userContext: deps.userContext, contextTools: deps.contextTools, }); diff --git a/packages/context/src/mcp/types.ts b/packages/context/src/mcp/types.ts index ab53f56e..1c934996 100644 --- a/packages/context/src/mcp/types.ts +++ b/packages/context/src/mcp/types.ts @@ -1,16 +1,7 @@ -import type { IngestReportSnapshot, MemoryFlowReplayInput, TableUsageOutput } from '../ingest/index.js'; -import type { MemoryCaptureService } from '../memory/index.js'; +import type { MemoryIngestService } 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, - SemanticLayerQueryInput, - SlDictionaryMatch, - SlSearchLaneSummary, - SlSearchMatchReason, -} from '../sl/index.js'; +import type { KtxDictionarySearchInput, KtxDictionarySearchResponse, SemanticLayerQueryInput } from '../sl/index.js'; import type { WikiSearchLaneSummary, WikiSearchMatchReason } from '../wiki/index.js'; export interface KtxMcpTextContent { @@ -24,9 +15,9 @@ export interface KtxMcpToolResult { isError?: true; } -export interface MemoryCapturePort { - capture: MemoryCaptureService['capture']; - status: MemoryCaptureService['status']; +export interface MemoryIngestPort { + ingest: MemoryIngestService['ingest']; + status: MemoryIngestService['status']; } export interface KtxMcpUserContext { @@ -51,18 +42,8 @@ export interface KtxConnectionSummary { connectionType: string; } -export interface KtxConnectionTestResponse { - id: string; - connectionType: string; - ok: boolean; - tableCount: number | null; - message: string; - warnings: string[]; -} - export interface KtxConnectionsMcpPort { list(): Promise; - test?(input: { connectionId: string }): Promise; } export interface KtxKnowledgeSearchResult { @@ -90,62 +71,9 @@ export interface KtxKnowledgePage { slRefs?: string[]; } -interface KtxHistoricSqlKnowledgeUsage { - executions: number; - distinct_users: number; - first_seen: string; - last_seen: string; - p50_runtime_ms: number | null; - p95_runtime_ms: number | null; - error_rate: number; - rows_produced?: number; -} - -export interface KtxKnowledgeWriteResponse { - success: boolean; - key: string; - action: 'created' | 'updated'; -} - export interface KtxKnowledgeMcpPort { search(input: { userId: string; query: string; limit: number }): Promise; read(input: { userId: string; key: string }): Promise; - write(input: { - userId: string; - key: string; - summary: string; - content: string; - tags?: string[]; - refs?: string[]; - slRefs?: string[]; - source?: string; - intent?: string; - tables?: string[]; - representativeSql?: string; - usage?: KtxHistoricSqlKnowledgeUsage; - fingerprints?: string[]; - }): Promise; -} - -export interface KtxSemanticLayerSourceSummary { - connectionId: string; - connectionName: string; - name: string; - description?: string; - columnCount: number; - measureCount: number; - joinCount: number; - frequencyTier?: TableUsageOutput['frequencyTier']; - snippet?: string; - score?: number; - matchReasons?: SlSearchMatchReason[]; - dictionaryMatches?: SlDictionaryMatch[]; - lanes?: SlSearchLaneSummary[]; -} - -export interface KtxSemanticLayerListResponse { - sources: KtxSemanticLayerSourceSummary[]; - totalSources: number; } export interface KtxSemanticLayerReadResponse { @@ -153,21 +81,6 @@ export interface KtxSemanticLayerReadResponse { yaml: string; } -export interface KtxSemanticLayerWriteResponse { - success: boolean; - sourceName: string; - yaml?: string; - errors?: string[]; - warnings?: string[]; - commitHash?: string; -} - -export interface KtxSemanticLayerValidationResponse { - success: boolean; - errors: string[]; - warnings: string[]; -} - export interface KtxSemanticLayerQueryResponse { sql: string; headers: string[]; @@ -177,145 +90,10 @@ export interface KtxSemanticLayerQueryResponse { } export interface KtxSemanticLayerMcpPort { - listSources(input: { connectionId?: string; query?: string }): Promise; readSource(input: { connectionId: string; sourceName: string }): Promise; - writeSource(input: { - connectionId: string; - sourceName: string; - yaml?: string; - source?: Record; - delete?: boolean; - }): Promise; - validate(input: { connectionId: string; names?: string[] }): Promise; query(input: { connectionId?: string; query: SemanticLayerQueryInput }): Promise; } -export type KtxIngestTriggerKind = 'upload' | 'scheduled_pull' | 'manual_resync'; - -interface KtxIngestTriggerFanoutChild { - runId: string; - jobId: string; - reportId: string; - targetConnectionId: string; - metabaseDatabaseId: number; -} - -export interface KtxIngestTriggerResponse { - runId: string; - jobId?: string; - reportId?: string; - fanout?: { - status: 'all_succeeded' | 'partial_failure' | 'all_failed'; - children: KtxIngestTriggerFanoutChild[]; - }; -} - -export interface KtxIngestDiffSummary { - added: number; - modified: number; - deleted: number; - unchanged: number; -} - -export interface KtxIngestWorkUnitSummary { - unitKey: string; - rawFiles: string[]; - peerFileIndex: string[]; - dependencyPaths: string[]; -} - -export interface KtxIngestStatusResponse { - runId: string; - jobId?: string; - reportId?: string; - status: string; - stage?: string; - progress?: number; - errors?: string[]; - done: boolean; - adapter?: string; - connectionId?: string; - sourceDir?: string | null; - syncId?: string; - startedAt?: string; - completedAt?: string; - previousRunId?: string | null; - diffSummary?: KtxIngestDiffSummary; - workUnitCount?: number; - rawFileCount?: number; - workUnits?: KtxIngestWorkUnitSummary[]; - evictionDeletedRawPaths?: string[]; -} - -export interface KtxIngestMcpPort { - trigger(input: { - adapter: string; - connectionId: string; - config?: unknown; - trigger: KtxIngestTriggerKind; - }): Promise; - status(input: { runId: string }): Promise; - report?(input: { runId: string }): Promise; - replay?(input: { runId: string }): Promise; -} - -interface KtxScanTriggerResponse { - runId: string; - status: 'done'; - done: true; - connectionId: string; - mode: KtxScanMode; - dryRun: boolean; - syncId: string; - report: KtxScanReport; -} - -interface KtxScanStatusResponse { - runId: string; - status: string; - done: boolean; - connectionId: string; - mode: KtxScanMode; - dryRun: boolean; - syncId: string; - progress: number; - startedAt: string; - completedAt: string; - reportPath: string | null; - warnings: KtxScanReport['warnings']; -} - -export type KtxScanArtifactType = 'report' | 'raw_source' | 'manifest_shard' | 'enrichment_artifact'; - -export interface KtxScanArtifactSummary { - path: string; - type: KtxScanArtifactType; - size?: number; -} - -export interface KtxScanArtifactListResponse { - runId: string; - artifacts: KtxScanArtifactSummary[]; -} - -export interface KtxScanArtifactReadResponse extends KtxScanArtifactSummary { - runId: string; - content: string; -} - -export interface KtxScanMcpPort { - trigger(input: { - connectionId: string; - mode?: KtxScanMode; - detectRelationships: boolean; - dryRun: boolean; - }): Promise; - status(input: { runId: string }): Promise; - report(input: { runId: string }): Promise; - listArtifacts?(input: { runId: string }): Promise; - readArtifact?(input: { runId: string; path: string }): Promise; -} - export interface KtxEntityDetailsMcpPort { read(input: KtxEntityDetailsInput): Promise; } @@ -347,13 +125,11 @@ export interface KtxMcpContextPorts { dictionarySearch?: KtxDictionarySearchMcpPort; discover?: KtxDiscoverDataMcpPort; sqlExecution?: KtxSqlExecutionMcpPort; - ingest?: KtxIngestMcpPort; - scan?: KtxScanMcpPort; + memoryIngest?: MemoryIngestPort; } export interface KtxMcpServerDeps { server: KtxMcpServerLike; - memoryCapture?: MemoryCapturePort; userContext: KtxMcpUserContext; contextTools?: KtxMcpContextPorts; }