@kaelio/ktx 0.1.0-rc.1 → 0.1.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-program.js +3 -0
- package/dist/cli-runtime.d.ts +1 -0
- package/dist/commands/mcp-commands.js +14 -1
- package/dist/commands/mcp-commands.test.js +35 -0
- package/dist/commands/setup-commands.js +9 -0
- package/dist/context-build-view.js +1 -0
- package/dist/context-build-view.test.js +18 -0
- package/dist/index.test.js +23 -0
- package/dist/ingest.d.ts +1 -0
- package/dist/ingest.js +1 -1
- package/dist/ingest.test.js +3 -1
- package/dist/managed-mcp-daemon.d.ts +1 -1
- package/dist/managed-mcp-daemon.js +12 -1
- package/dist/managed-mcp-daemon.test.js +58 -0
- package/dist/mcp-http-server.js +2 -54
- package/dist/mcp-server-factory.d.ts +9 -0
- package/dist/mcp-server-factory.js +51 -0
- package/dist/mcp-stdio-server.d.ts +14 -0
- package/dist/mcp-stdio-server.js +48 -0
- package/dist/memory-flow-tui.test.js +1 -1
- package/dist/public-ingest.d.ts +1 -0
- package/dist/public-ingest.js +17 -7
- package/dist/public-ingest.test.js +10 -3
- package/dist/scan.d.ts +1 -0
- package/dist/scan.js +1 -1
- package/dist/scan.test.js +3 -2
- package/dist/setup-agents.d.ts +4 -3
- package/dist/setup-agents.js +346 -43
- package/dist/setup-agents.test.js +385 -41
- package/dist/setup-demo-tour.js +1 -1
- package/dist/setup-demo-tour.test.js +1 -1
- package/dist/setup-models.d.ts +1 -0
- package/dist/setup-models.js +69 -12
- package/dist/setup-models.test.js +25 -2
- package/dist/setup-prompts.d.ts +4 -1
- package/dist/setup-prompts.js +5 -2
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +24 -17
- package/dist/setup.test.js +44 -9
- package/dist/skills/analytics/SKILL.md +62 -0
- package/dist/text-ingest.d.ts +5 -5
- package/dist/text-ingest.js +10 -10
- package/dist/text-ingest.test.js +32 -32
- package/node_modules/@ktx/context/dist/connections/connection-type.d.ts +4 -4
- package/node_modules/@ktx/context/dist/core/git-env.js +1 -1
- package/node_modules/@ktx/context/dist/core/git.service.js +5 -0
- package/node_modules/@ktx/context/dist/core/git.service.test.js +33 -1
- package/node_modules/@ktx/context/dist/daemon/semantic-layer-compute.d.ts +11 -3
- package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/types.d.ts +5 -5
- package/node_modules/@ktx/context/dist/ingest/adapters/looker/types.d.ts +4 -4
- package/node_modules/@ktx/context/dist/ingest/git-env.js +2 -1
- package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.d.ts +3 -3
- package/node_modules/@ktx/context/dist/llm/claude-code-runtime.js +11 -1
- package/node_modules/@ktx/context/dist/llm/claude-code-runtime.test.js +11 -0
- package/node_modules/@ktx/context/dist/llm/local-config.js +13 -1
- package/node_modules/@ktx/context/dist/llm/local-config.test.js +24 -0
- package/node_modules/@ktx/context/dist/mcp/context-tools.d.ts +2 -2
- package/node_modules/@ktx/context/dist/mcp/context-tools.js +447 -292
- package/node_modules/@ktx/context/dist/mcp/index.d.ts +1 -1
- package/node_modules/@ktx/context/dist/mcp/local-project-ports.d.ts +0 -2
- package/node_modules/@ktx/context/dist/mcp/local-project-ports.js +15 -442
- package/node_modules/@ktx/context/dist/mcp/local-project-ports.test.js +108 -811
- package/node_modules/@ktx/context/dist/mcp/server.js +1 -46
- package/node_modules/@ktx/context/dist/mcp/server.test.js +376 -509
- package/node_modules/@ktx/context/dist/mcp/types.d.ts +38 -227
- package/node_modules/@ktx/context/dist/memory/index.d.ts +2 -2
- package/node_modules/@ktx/context/dist/memory/index.js +2 -2
- package/node_modules/@ktx/context/dist/memory/local-memory.d.ts +3 -3
- package/node_modules/@ktx/context/dist/memory/local-memory.js +5 -5
- package/node_modules/@ktx/context/dist/memory/local-memory.test.js +10 -10
- package/node_modules/@ktx/context/dist/memory/memory-runs.d.ts +8 -8
- package/node_modules/@ktx/context/dist/memory/memory-runs.js +5 -5
- package/node_modules/@ktx/context/dist/memory/memory-runs.test.js +25 -25
- package/node_modules/@ktx/context/dist/project/config.d.ts +2 -2
- package/node_modules/@ktx/context/dist/sl/local-query.d.ts +2 -0
- package/node_modules/@ktx/context/dist/sl/local-query.js +10 -3
- package/node_modules/@ktx/context/dist/sl/local-query.test.js +65 -0
- package/node_modules/@ktx/context/dist/tools/context-candidate-write.tool.d.ts +2 -2
- package/package.json +2 -1
- package/dist/skills/research/SKILL.md +0 -49
package/dist/cli-program.js
CHANGED
|
@@ -116,6 +116,9 @@ function shouldSuppressProjectDirLine(path, options) {
|
|
|
116
116
|
if (commandPathKey === 'ktx setup') {
|
|
117
117
|
return true;
|
|
118
118
|
}
|
|
119
|
+
if (commandPathKey === 'ktx mcp stdio') {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
119
122
|
if (commandPathKey === 'ktx status' &&
|
|
120
123
|
typeof options.projectDir !== 'string' &&
|
|
121
124
|
process.env.KTX_PROJECT_DIR === undefined &&
|
package/dist/cli-runtime.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface KtxCliDeps {
|
|
|
35
35
|
stopDaemon?: typeof import('./managed-mcp-daemon.js').stopKtxMcpDaemon;
|
|
36
36
|
readStatus?: typeof import('./managed-mcp-daemon.js').readKtxMcpDaemonStatus;
|
|
37
37
|
runServer?: typeof import('./mcp-http-server.js').runKtxMcpHttpServer;
|
|
38
|
+
runStdioServer?: typeof import('./mcp-stdio-server.js').runKtxMcpStdioServer;
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
export declare function getKtxCliPackageInfo(): KtxCliPackageInfo;
|
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { collectOption, parsePositiveIntegerOption, resolveCommandProjectDir, } from '../cli-program.js';
|
|
5
5
|
import { mcpDaemonLayout, readKtxMcpDaemonStatus, startKtxMcpDaemon, stopKtxMcpDaemon, } from '../managed-mcp-daemon.js';
|
|
6
6
|
import { buildMcpSecurityConfig, runKtxMcpHttpServer } from '../mcp-http-server.js';
|
|
7
|
+
import { runKtxMcpStdioServer } from '../mcp-stdio-server.js';
|
|
7
8
|
function tokenFromOption(value) {
|
|
8
9
|
return value ?? process.env.KTX_MCP_TOKEN;
|
|
9
10
|
}
|
|
@@ -12,6 +13,16 @@ function binPath() {
|
|
|
12
13
|
}
|
|
13
14
|
export function registerMcpCommands(program, context) {
|
|
14
15
|
const mcp = program.command('mcp').description('Run the KTX MCP HTTP server');
|
|
16
|
+
mcp
|
|
17
|
+
.command('stdio')
|
|
18
|
+
.description('Run the KTX MCP server over stdio')
|
|
19
|
+
.action(async (_options, command) => {
|
|
20
|
+
await (context.deps.mcp?.runStdioServer ?? runKtxMcpStdioServer)({
|
|
21
|
+
projectDir: resolveCommandProjectDir(command),
|
|
22
|
+
cliVersion: context.packageInfo.version,
|
|
23
|
+
io: context.io,
|
|
24
|
+
});
|
|
25
|
+
});
|
|
15
26
|
mcp
|
|
16
27
|
.command('start')
|
|
17
28
|
.description('Start the KTX MCP HTTP server')
|
|
@@ -55,7 +66,9 @@ export function registerMcpCommands(program, context) {
|
|
|
55
66
|
allowedOrigins: options.allowedOrigin,
|
|
56
67
|
binPath: binPath(),
|
|
57
68
|
});
|
|
58
|
-
context.io.stdout.write(
|
|
69
|
+
context.io.stdout.write(result.status === 'started'
|
|
70
|
+
? `KTX MCP daemon started: ${result.url}\n`
|
|
71
|
+
: `KTX MCP daemon already running: ${result.url}\n`);
|
|
59
72
|
});
|
|
60
73
|
mcp
|
|
61
74
|
.command('stop')
|
|
@@ -31,6 +31,7 @@ describe('registerMcpCommands', () => {
|
|
|
31
31
|
'serve-internal',
|
|
32
32
|
'start',
|
|
33
33
|
'status',
|
|
34
|
+
'stdio',
|
|
34
35
|
'stop',
|
|
35
36
|
]);
|
|
36
37
|
expect(mcp?.commands.find((command) => command.name() === 'serve-internal')
|
|
@@ -44,4 +45,38 @@ describe('registerMcpCommands', () => {
|
|
|
44
45
|
await expect(program.parseAsync(['mcp', 'start', '--host', '0.0.0.0'], { from: 'user' })).rejects.toThrow('Binding KTX MCP to 0.0.0.0 requires --token or KTX_MCP_TOKEN');
|
|
45
46
|
expect(startDaemon).not.toHaveBeenCalled();
|
|
46
47
|
});
|
|
48
|
+
it('prints "already running" when startDaemon reports already-running', async () => {
|
|
49
|
+
const program = new Command().exitOverride().option('--project-dir <path>');
|
|
50
|
+
const startDaemon = vi.fn().mockResolvedValue({
|
|
51
|
+
status: 'already-running',
|
|
52
|
+
url: 'http://127.0.0.1:7878/mcp',
|
|
53
|
+
state: {
|
|
54
|
+
schemaVersion: 1,
|
|
55
|
+
pid: 4242,
|
|
56
|
+
host: '127.0.0.1',
|
|
57
|
+
port: 7878,
|
|
58
|
+
tokenAuth: false,
|
|
59
|
+
projectDir: '/tmp/ktx-already',
|
|
60
|
+
startedAt: '2026-05-14T00:00:00.000Z',
|
|
61
|
+
logPath: '/tmp/ktx-already/.ktx/logs/mcp.log',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const context = makeContext({ deps: { mcp: { startDaemon } } });
|
|
65
|
+
registerMcpCommands(program, context);
|
|
66
|
+
await program.parseAsync(['--project-dir', '/tmp/ktx-already', 'mcp', 'start'], { from: 'user' });
|
|
67
|
+
expect(startDaemon).toHaveBeenCalledTimes(1);
|
|
68
|
+
expect(context.io.stdout.write).toHaveBeenCalledWith('KTX MCP daemon already running: http://127.0.0.1:7878/mcp\n');
|
|
69
|
+
});
|
|
70
|
+
it('runs the stdio server with the resolved project directory', async () => {
|
|
71
|
+
const program = new Command().exitOverride().option('--project-dir <path>');
|
|
72
|
+
const runStdioServer = vi.fn().mockResolvedValue(undefined);
|
|
73
|
+
const context = makeContext({ deps: { mcp: { runStdioServer } } });
|
|
74
|
+
registerMcpCommands(program, context);
|
|
75
|
+
await expect(program.parseAsync(['--project-dir', '/tmp/ktx6', 'mcp', 'stdio'], { from: 'user' })).resolves.toBe(program);
|
|
76
|
+
expect(runStdioServer).toHaveBeenCalledWith({
|
|
77
|
+
projectDir: '/tmp/ktx6',
|
|
78
|
+
cliVersion: '0.0.0-test',
|
|
79
|
+
io: context.io,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
47
82
|
});
|
|
@@ -91,6 +91,7 @@ function shouldShowSetupEntryMenu(options, command) {
|
|
|
91
91
|
'llmBackend',
|
|
92
92
|
'anthropicApiKeyEnv',
|
|
93
93
|
'anthropicApiKeyFile',
|
|
94
|
+
'llmModel',
|
|
94
95
|
'anthropicModel',
|
|
95
96
|
'vertexProject',
|
|
96
97
|
'vertexLocation',
|
|
@@ -136,6 +137,7 @@ export function registerSetupCommands(program, context) {
|
|
|
136
137
|
.option('--agents', 'Install agent integration only', false)
|
|
137
138
|
.addOption(new Option('--target <target>', 'Agent target').choices([
|
|
138
139
|
'claude-code',
|
|
140
|
+
'claude-desktop',
|
|
139
141
|
'codex',
|
|
140
142
|
'cursor',
|
|
141
143
|
'opencode',
|
|
@@ -149,6 +151,7 @@ export function registerSetupCommands(program, context) {
|
|
|
149
151
|
.addOption(new Option('--llm-backend <backend>', 'LLM backend').argParser(llmBackend).hideHelp())
|
|
150
152
|
.addOption(new Option('--anthropic-api-key-env <name>', 'Environment variable containing the Anthropic API key').hideHelp())
|
|
151
153
|
.addOption(new Option('--anthropic-api-key-file <path>', 'File containing the Anthropic API key').hideHelp())
|
|
154
|
+
.addOption(new Option('--llm-model <model>', 'LLM model ID or backend model alias').hideHelp())
|
|
152
155
|
.addOption(new Option('--anthropic-model <model>', 'Anthropic model ID to validate and save').hideHelp())
|
|
153
156
|
.addOption(new Option('--vertex-project <project>', 'Google Vertex AI project ID, env:NAME, or file:/path').hideHelp())
|
|
154
157
|
.addOption(new Option('--vertex-location <location>', 'Google Vertex AI location, env:NAME, or file:/path').hideHelp())
|
|
@@ -235,6 +238,11 @@ export function registerSetupCommands(program, context) {
|
|
|
235
238
|
context.setExitCode(1);
|
|
236
239
|
return;
|
|
237
240
|
}
|
|
241
|
+
if (options.llmModel && options.anthropicModel) {
|
|
242
|
+
context.io.stderr.write('Choose only one LLM model flag: --llm-model or --anthropic-model.\n');
|
|
243
|
+
context.setExitCode(1);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
238
246
|
if (options.llmBackend &&
|
|
239
247
|
options.llmBackend !== 'anthropic' &&
|
|
240
248
|
(options.anthropicApiKeyEnv || options.anthropicApiKeyFile)) {
|
|
@@ -293,6 +301,7 @@ export function registerSetupCommands(program, context) {
|
|
|
293
301
|
...(options.llmBackend ? { llmBackend: options.llmBackend } : {}),
|
|
294
302
|
...(options.anthropicApiKeyEnv ? { anthropicApiKeyEnv: options.anthropicApiKeyEnv } : {}),
|
|
295
303
|
...(options.anthropicApiKeyFile ? { anthropicApiKeyFile: options.anthropicApiKeyFile } : {}),
|
|
304
|
+
...(options.llmModel ? { llmModel: options.llmModel } : {}),
|
|
296
305
|
...(options.anthropicModel ? { anthropicModel: options.anthropicModel } : {}),
|
|
297
306
|
...(options.vertexProject ? { vertexProject: options.vertexProject } : {}),
|
|
298
307
|
...(options.vertexLocation ? { vertexLocation: options.vertexLocation } : {}),
|
|
@@ -801,6 +801,7 @@ export async function runContextBuild(project, args, io, deps = {}) {
|
|
|
801
801
|
const progressDeps = {
|
|
802
802
|
scanProgress: createContextBuildProgressPort(updateSchemaPhase),
|
|
803
803
|
ingestProgress: updateIngestPhase,
|
|
804
|
+
runtimeIo: io,
|
|
804
805
|
onPhaseStart,
|
|
805
806
|
onPhaseEnd,
|
|
806
807
|
};
|
|
@@ -764,6 +764,24 @@ describe('runContextBuild', () => {
|
|
|
764
764
|
ingestProgress: expect.any(Function),
|
|
765
765
|
}));
|
|
766
766
|
});
|
|
767
|
+
it('threads the original runtime IO into captured target execution', async () => {
|
|
768
|
+
const io = makeIo({ isTTY: true });
|
|
769
|
+
const project = projectWithConnections({
|
|
770
|
+
warehouse: { driver: 'postgres', context: { queryHistory: { enabled: true } } },
|
|
771
|
+
});
|
|
772
|
+
const executeTarget = vi.fn(async (target) => successResult(target.connectionId, target.driver, target.operation));
|
|
773
|
+
await runContextBuild(project, {
|
|
774
|
+
projectDir: '/tmp/project',
|
|
775
|
+
inputMode: 'auto',
|
|
776
|
+
cliVersion: '0.2.0',
|
|
777
|
+
runtimeInstallPolicy: 'auto',
|
|
778
|
+
}, io.io, { executeTarget, now: () => 1000 });
|
|
779
|
+
expect(executeTarget).toHaveBeenCalledWith(expect.objectContaining({ connectionId: 'warehouse' }), expect.objectContaining({ runtimeInstallPolicy: 'auto' }), expect.objectContaining({
|
|
780
|
+
stdout: expect.objectContaining({ isTTY: false }),
|
|
781
|
+
}), expect.objectContaining({
|
|
782
|
+
runtimeIo: io.io,
|
|
783
|
+
}));
|
|
784
|
+
});
|
|
767
785
|
it('calls onSourceProgress when sources start and finish', async () => {
|
|
768
786
|
const io = makeIo();
|
|
769
787
|
const project = projectWithConnections({
|
package/dist/index.test.js
CHANGED
|
@@ -785,6 +785,29 @@ describe('runKtxCli', () => {
|
|
|
785
785
|
skipLlm: false,
|
|
786
786
|
}), setupIo.io);
|
|
787
787
|
});
|
|
788
|
+
it('dispatches the provider-neutral LLM model setup flag to the setup runner', async () => {
|
|
789
|
+
const setup = vi.fn(async () => 0);
|
|
790
|
+
const setupIo = makeIo();
|
|
791
|
+
await expect(runKtxCli([
|
|
792
|
+
'--project-dir',
|
|
793
|
+
tempDir,
|
|
794
|
+
'setup',
|
|
795
|
+
'--no-input',
|
|
796
|
+
'--llm-backend',
|
|
797
|
+
'claude-code',
|
|
798
|
+
'--llm-model',
|
|
799
|
+
'opus',
|
|
800
|
+
], setupIo.io, { setup })).resolves.toBe(0);
|
|
801
|
+
expect(setup).toHaveBeenCalledWith(expect.objectContaining({
|
|
802
|
+
command: 'run',
|
|
803
|
+
projectDir: tempDir,
|
|
804
|
+
inputMode: 'disabled',
|
|
805
|
+
cliVersion: '0.0.0-private',
|
|
806
|
+
llmBackend: 'claude-code',
|
|
807
|
+
llmModel: 'opus',
|
|
808
|
+
skipLlm: false,
|
|
809
|
+
}), setupIo.io);
|
|
810
|
+
});
|
|
788
811
|
it('rejects conflicting Anthropic credential setup flags', async () => {
|
|
789
812
|
const setup = vi.fn(async () => 0);
|
|
790
813
|
const setupIo = makeIo();
|
package/dist/ingest.d.ts
CHANGED
|
@@ -59,6 +59,7 @@ export interface KtxIngestDeps {
|
|
|
59
59
|
env?: NodeJS.ProcessEnv;
|
|
60
60
|
localIngestOptions?: Pick<RunLocalIngestOptions, 'agentRunner' | 'llmRuntime' | 'memoryModel' | 'semanticLayerCompute' | 'queryExecutor' | 'logger' | 'pullConfigOptions'>;
|
|
61
61
|
progress?: (update: KtxIngestProgressUpdate) => void;
|
|
62
|
+
runtimeIo?: KtxIngestIo;
|
|
62
63
|
}
|
|
63
64
|
export declare function runKtxIngest(args: KtxIngestArgs, io?: KtxIngestIo, deps?: KtxIngestDeps): Promise<number>;
|
|
64
65
|
export {};
|
package/dist/ingest.js
CHANGED
|
@@ -412,7 +412,7 @@ export async function runKtxIngest(args, io = process, deps = {}) {
|
|
|
412
412
|
(deps.runLocalIngest || deps.runLocalMetabaseIngest ? () => [] : createKtxCliLocalIngestAdapters);
|
|
413
413
|
const executeLocalIngest = deps.runLocalIngest ?? runLocalIngest;
|
|
414
414
|
const localIngestOptions = deps.localIngestOptions ?? {};
|
|
415
|
-
const managedDaemon = managedDaemonOptionsForIngestRun(args, io);
|
|
415
|
+
const managedDaemon = managedDaemonOptionsForIngestRun(args, deps.runtimeIo ?? io);
|
|
416
416
|
const operationalLogger = createCliOperationalLogger(io, args.outputMode);
|
|
417
417
|
const adapterOptions = {
|
|
418
418
|
...(localIngestOptions.pullConfigOptions ?? {}),
|
package/dist/ingest.test.js
CHANGED
|
@@ -852,6 +852,7 @@ describe('runKtxIngest', () => {
|
|
|
852
852
|
const createAdapters = vi.fn(() => createdAdapters);
|
|
853
853
|
const runLocal = vi.fn(async (input) => completedLocalBundleRun(input, input.jobId ?? 'local-job-1'));
|
|
854
854
|
const io = makeIo();
|
|
855
|
+
const runtimeIo = makeIo({ isTTY: true });
|
|
855
856
|
await expect(runKtxIngest({
|
|
856
857
|
command: 'run',
|
|
857
858
|
projectDir,
|
|
@@ -864,12 +865,13 @@ describe('runKtxIngest', () => {
|
|
|
864
865
|
createAdapters,
|
|
865
866
|
runLocalIngest: runLocal,
|
|
866
867
|
jobIdFactory: () => 'local-job-1',
|
|
868
|
+
runtimeIo: runtimeIo.io,
|
|
867
869
|
})).resolves.toBe(0);
|
|
868
870
|
const expectedManagedDaemon = {
|
|
869
871
|
cliVersion: '0.2.0',
|
|
870
872
|
projectDir,
|
|
871
873
|
installPolicy: 'auto',
|
|
872
|
-
io:
|
|
874
|
+
io: runtimeIo.io,
|
|
873
875
|
};
|
|
874
876
|
expect(createAdapters).toHaveBeenCalledWith(expect.objectContaining({ projectDir }), expect.objectContaining({
|
|
875
877
|
managedDaemon: expectedManagedDaemon,
|
|
@@ -81,7 +81,18 @@ export async function startKtxMcpDaemon(options) {
|
|
|
81
81
|
const existing = await readState(options.projectDir).catch(() => undefined);
|
|
82
82
|
const processAlive = options.processAlive ?? defaultProcessAlive;
|
|
83
83
|
if (existing && processAlive(existing.pid)) {
|
|
84
|
-
|
|
84
|
+
const sameConfig = existing.host === options.host &&
|
|
85
|
+
existing.port === options.port &&
|
|
86
|
+
existing.tokenAuth === Boolean(options.token);
|
|
87
|
+
if (sameConfig) {
|
|
88
|
+
return {
|
|
89
|
+
status: 'already-running',
|
|
90
|
+
state: existing,
|
|
91
|
+
url: `http://${existing.host}:${existing.port}/mcp`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`KTX MCP daemon is already running at http://${existing.host}:${existing.port}/mcp ` +
|
|
95
|
+
'with a different configuration. Run `ktx mcp stop` first, then start again.');
|
|
85
96
|
}
|
|
86
97
|
const portAvailable = options.portAvailable ?? defaultPortAvailable;
|
|
87
98
|
if (!(await portAvailable(options.host, options.port))) {
|
|
@@ -72,6 +72,64 @@ describe('managed MCP daemon lifecycle', () => {
|
|
|
72
72
|
}));
|
|
73
73
|
expect(JSON.stringify(JSON.parse(await readFile(join(projectDir, '.ktx/mcp.json'), 'utf8')))).not.toContain('secret-token');
|
|
74
74
|
});
|
|
75
|
+
it('returns already-running without spawning when the daemon is alive at the same host/port', async () => {
|
|
76
|
+
await mkdir(join(projectDir, '.ktx'), { recursive: true });
|
|
77
|
+
await writeFile(join(projectDir, '.ktx/mcp.json'), `${JSON.stringify(state(projectDir), null, 2)}\n`);
|
|
78
|
+
const spawnDaemon = vi.fn(() => child(9999));
|
|
79
|
+
const result = await startKtxMcpDaemon({
|
|
80
|
+
projectDir,
|
|
81
|
+
cliVersion: '0.0.0-test',
|
|
82
|
+
host: '127.0.0.1',
|
|
83
|
+
port: 7878,
|
|
84
|
+
allowedHosts: [],
|
|
85
|
+
allowedOrigins: [],
|
|
86
|
+
binPath: '/repo/packages/cli/dist/bin.js',
|
|
87
|
+
spawnDaemon,
|
|
88
|
+
processAlive: vi.fn(() => true),
|
|
89
|
+
portAvailable: vi.fn(async () => true),
|
|
90
|
+
});
|
|
91
|
+
expect(result.status).toBe('already-running');
|
|
92
|
+
expect(result.url).toBe('http://127.0.0.1:7878/mcp');
|
|
93
|
+
expect(result.state.pid).toBe(4242);
|
|
94
|
+
expect(spawnDaemon).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
it('throws when the recorded daemon uses a different host or port', async () => {
|
|
97
|
+
await mkdir(join(projectDir, '.ktx'), { recursive: true });
|
|
98
|
+
await writeFile(join(projectDir, '.ktx/mcp.json'), `${JSON.stringify(state(projectDir), null, 2)}\n`);
|
|
99
|
+
const spawnDaemon = vi.fn(() => child(9999));
|
|
100
|
+
await expect(startKtxMcpDaemon({
|
|
101
|
+
projectDir,
|
|
102
|
+
cliVersion: '0.0.0-test',
|
|
103
|
+
host: '127.0.0.1',
|
|
104
|
+
port: 9000,
|
|
105
|
+
allowedHosts: [],
|
|
106
|
+
allowedOrigins: [],
|
|
107
|
+
binPath: '/repo/packages/cli/dist/bin.js',
|
|
108
|
+
spawnDaemon,
|
|
109
|
+
processAlive: vi.fn(() => true),
|
|
110
|
+
portAvailable: vi.fn(async () => true),
|
|
111
|
+
})).rejects.toThrow(/different configuration[\s\S]*ktx mcp stop/);
|
|
112
|
+
expect(spawnDaemon).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
it('throws when token-auth presence differs from the recorded daemon', async () => {
|
|
115
|
+
await mkdir(join(projectDir, '.ktx'), { recursive: true });
|
|
116
|
+
await writeFile(join(projectDir, '.ktx/mcp.json'), `${JSON.stringify(state(projectDir, { tokenAuth: false }), null, 2)}\n`);
|
|
117
|
+
const spawnDaemon = vi.fn(() => child(9999));
|
|
118
|
+
await expect(startKtxMcpDaemon({
|
|
119
|
+
projectDir,
|
|
120
|
+
cliVersion: '0.0.0-test',
|
|
121
|
+
host: '127.0.0.1',
|
|
122
|
+
port: 7878,
|
|
123
|
+
token: 'secret-token',
|
|
124
|
+
allowedHosts: [],
|
|
125
|
+
allowedOrigins: [],
|
|
126
|
+
binPath: '/repo/packages/cli/dist/bin.js',
|
|
127
|
+
spawnDaemon,
|
|
128
|
+
processAlive: vi.fn(() => true),
|
|
129
|
+
portAvailable: vi.fn(async () => true),
|
|
130
|
+
})).rejects.toThrow(/different configuration/);
|
|
131
|
+
expect(spawnDaemon).not.toHaveBeenCalled();
|
|
132
|
+
});
|
|
75
133
|
it('reports running when the process is alive and health passes', async () => {
|
|
76
134
|
await mkdir(join(projectDir, '.ktx'), { recursive: true });
|
|
77
135
|
await writeFile(join(projectDir, '.ktx/mcp.json'), `${JSON.stringify(state(projectDir), null, 2)}\n`);
|
package/dist/mcp-http-server.js
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
|
-
import { createDefaultKtxMcpServer, createLocalProjectMcpContextPorts } from '@ktx/context/mcp';
|
|
4
|
-
import { createLocalProjectMemoryCapture } from '@ktx/context/memory';
|
|
5
3
|
import { loadKtxProject } from '@ktx/context/project';
|
|
6
4
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
7
5
|
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
-
import {
|
|
9
|
-
import { createKtxCliScanConnector } from './local-scan-connectors.js';
|
|
10
|
-
import { createManagedPythonSemanticLayerComputePort } from './managed-python-command.js';
|
|
11
|
-
import { createManagedDaemonSqlAnalysisPort } from './managed-python-http.js';
|
|
6
|
+
import { createKtxMcpServerFactory } from './mcp-server-factory.js';
|
|
12
7
|
const DEFAULT_ALLOWED_HOSTS = ['localhost', '127.0.0.1', '::1'];
|
|
13
8
|
function isLoopbackHost(host) {
|
|
14
9
|
const normalized = normalizeHostHeader(host);
|
|
@@ -79,12 +74,6 @@ export function isMcpRequestAuthorized(request, config) {
|
|
|
79
74
|
}
|
|
80
75
|
return { ok: true };
|
|
81
76
|
}
|
|
82
|
-
function noopIo() {
|
|
83
|
-
return {
|
|
84
|
-
stdout: { write() { } },
|
|
85
|
-
stderr: { write() { } },
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
77
|
function writeJson(res, status, body) {
|
|
89
78
|
const payload = `${JSON.stringify(body)}\n`;
|
|
90
79
|
res.writeHead(status, {
|
|
@@ -109,47 +98,6 @@ async function readJsonBody(req) {
|
|
|
109
98
|
const raw = Buffer.concat(chunks).toString('utf8');
|
|
110
99
|
return raw.trim().length === 0 ? undefined : JSON.parse(raw);
|
|
111
100
|
}
|
|
112
|
-
async function defaultMcpServerFactory(input) {
|
|
113
|
-
const io = input.io ?? noopIo();
|
|
114
|
-
const queryExecutor = createKtxCliIngestQueryExecutor(input.project);
|
|
115
|
-
const semanticLayerCompute = await createManagedPythonSemanticLayerComputePort({
|
|
116
|
-
cliVersion: input.cliVersion,
|
|
117
|
-
installPolicy: 'auto',
|
|
118
|
-
io,
|
|
119
|
-
});
|
|
120
|
-
const sqlAnalysis = createManagedDaemonSqlAnalysisPort({
|
|
121
|
-
cliVersion: input.cliVersion,
|
|
122
|
-
projectDir: input.projectDir,
|
|
123
|
-
installPolicy: 'auto',
|
|
124
|
-
io,
|
|
125
|
-
});
|
|
126
|
-
const contextTools = createLocalProjectMcpContextPorts(input.project, {
|
|
127
|
-
semanticLayerCompute,
|
|
128
|
-
queryExecutor,
|
|
129
|
-
sqlAnalysis,
|
|
130
|
-
localScan: {
|
|
131
|
-
createConnector: async (connectionId) => createKtxCliScanConnector(input.project, connectionId),
|
|
132
|
-
},
|
|
133
|
-
localIngest: {
|
|
134
|
-
semanticLayerCompute,
|
|
135
|
-
queryExecutor,
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
let memoryCapture;
|
|
139
|
-
try {
|
|
140
|
-
memoryCapture = createLocalProjectMemoryCapture(input.project, { semanticLayerCompute, queryExecutor });
|
|
141
|
-
}
|
|
142
|
-
catch (error) {
|
|
143
|
-
input.io?.stderr.write(`KTX MCP memory_capture disabled: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
144
|
-
}
|
|
145
|
-
return () => createDefaultKtxMcpServer({
|
|
146
|
-
name: 'ktx',
|
|
147
|
-
version: input.cliVersion,
|
|
148
|
-
userContext: { userId: 'local' },
|
|
149
|
-
contextTools,
|
|
150
|
-
memoryCapture,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
101
|
function listenerPort(server, fallback) {
|
|
154
102
|
const address = server.address();
|
|
155
103
|
return typeof address === 'object' && address ? address.port : fallback;
|
|
@@ -171,7 +119,7 @@ export async function runKtxMcpHttpServer(options) {
|
|
|
171
119
|
? await (options.loadProject ?? loadKtxProject)({ projectDir: options.projectDir })
|
|
172
120
|
: undefined;
|
|
173
121
|
const createMcpServer = options.createMcpServer ??
|
|
174
|
-
(await
|
|
122
|
+
(await createKtxMcpServerFactory({
|
|
175
123
|
project: project,
|
|
176
124
|
projectDir: options.projectDir,
|
|
177
125
|
cliVersion: options.cliVersion ?? '0.0.0-private',
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { KtxLocalProject } from '@ktx/context/project';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import type { KtxCliIo } from './cli-runtime.js';
|
|
4
|
+
export declare function createKtxMcpServerFactory(input: {
|
|
5
|
+
project: KtxLocalProject;
|
|
6
|
+
projectDir: string;
|
|
7
|
+
cliVersion: string;
|
|
8
|
+
io?: KtxCliIo;
|
|
9
|
+
}): Promise<() => McpServer>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createDefaultKtxMcpServer, createLocalProjectMcpContextPorts } from '@ktx/context/mcp';
|
|
2
|
+
import { createLocalProjectMemoryIngest } from '@ktx/context/memory';
|
|
3
|
+
import { createKtxCliIngestQueryExecutor } from './ingest-query-executor.js';
|
|
4
|
+
import { createKtxCliScanConnector } from './local-scan-connectors.js';
|
|
5
|
+
import { createManagedPythonSemanticLayerComputePort } from './managed-python-command.js';
|
|
6
|
+
import { createManagedDaemonSqlAnalysisPort } from './managed-python-http.js';
|
|
7
|
+
function noopMcpIo() {
|
|
8
|
+
return {
|
|
9
|
+
stdout: { write() { } },
|
|
10
|
+
stderr: { write() { } },
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export async function createKtxMcpServerFactory(input) {
|
|
14
|
+
const io = input.io ?? noopMcpIo();
|
|
15
|
+
const queryExecutor = createKtxCliIngestQueryExecutor(input.project);
|
|
16
|
+
const semanticLayerCompute = await createManagedPythonSemanticLayerComputePort({
|
|
17
|
+
cliVersion: input.cliVersion,
|
|
18
|
+
installPolicy: 'auto',
|
|
19
|
+
io,
|
|
20
|
+
});
|
|
21
|
+
const sqlAnalysis = createManagedDaemonSqlAnalysisPort({
|
|
22
|
+
cliVersion: input.cliVersion,
|
|
23
|
+
projectDir: input.projectDir,
|
|
24
|
+
installPolicy: 'auto',
|
|
25
|
+
io,
|
|
26
|
+
});
|
|
27
|
+
const contextTools = createLocalProjectMcpContextPorts(input.project, {
|
|
28
|
+
semanticLayerCompute,
|
|
29
|
+
queryExecutor,
|
|
30
|
+
sqlAnalysis,
|
|
31
|
+
localScan: {
|
|
32
|
+
createConnector: async (connectionId) => createKtxCliScanConnector(input.project, connectionId),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
let memoryIngest;
|
|
36
|
+
try {
|
|
37
|
+
memoryIngest = createLocalProjectMemoryIngest(input.project, { semanticLayerCompute, queryExecutor });
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
input.io?.stderr.write(`KTX MCP memory_ingest disabled: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
41
|
+
}
|
|
42
|
+
return () => createDefaultKtxMcpServer({
|
|
43
|
+
name: 'ktx',
|
|
44
|
+
version: input.cliVersion,
|
|
45
|
+
userContext: { userId: 'local' },
|
|
46
|
+
contextTools: {
|
|
47
|
+
...contextTools,
|
|
48
|
+
...(memoryIngest ? { memoryIngest } : {}),
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Readable, Writable } from 'node:stream';
|
|
2
|
+
import { loadKtxProject } from '@ktx/context/project';
|
|
3
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import type { KtxCliIo } from './cli-runtime.js';
|
|
5
|
+
export interface RunKtxMcpStdioServerOptions {
|
|
6
|
+
projectDir: string;
|
|
7
|
+
cliVersion?: string;
|
|
8
|
+
io?: KtxCliIo;
|
|
9
|
+
createMcpServer?: () => McpServer;
|
|
10
|
+
loadProject?: typeof loadKtxProject;
|
|
11
|
+
stdin?: Readable;
|
|
12
|
+
stdout?: Writable;
|
|
13
|
+
}
|
|
14
|
+
export declare function runKtxMcpStdioServer(options: RunKtxMcpStdioServerOptions): Promise<void>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { loadKtxProject } from '@ktx/context/project';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { createKtxMcpServerFactory } from './mcp-server-factory.js';
|
|
5
|
+
export async function runKtxMcpStdioServer(options) {
|
|
6
|
+
const project = options.createMcpServer === undefined
|
|
7
|
+
? await (options.loadProject ?? loadKtxProject)({ projectDir: options.projectDir })
|
|
8
|
+
: undefined;
|
|
9
|
+
const protocolIo = {
|
|
10
|
+
stdout: { write() { } },
|
|
11
|
+
stderr: options.io?.stderr ?? { write() { } },
|
|
12
|
+
};
|
|
13
|
+
const createMcpServer = options.createMcpServer ??
|
|
14
|
+
(await createKtxMcpServerFactory({
|
|
15
|
+
project: project,
|
|
16
|
+
projectDir: options.projectDir,
|
|
17
|
+
cliVersion: options.cliVersion ?? '0.0.0-private',
|
|
18
|
+
io: protocolIo,
|
|
19
|
+
}));
|
|
20
|
+
const stdin = options.stdin ?? process.stdin;
|
|
21
|
+
const transport = new StdioServerTransport(stdin, options.stdout);
|
|
22
|
+
await new Promise((resolve, reject) => {
|
|
23
|
+
let settled = false;
|
|
24
|
+
const settle = (callback) => {
|
|
25
|
+
if (settled)
|
|
26
|
+
return;
|
|
27
|
+
settled = true;
|
|
28
|
+
stdin.off('end', closeTransport);
|
|
29
|
+
stdin.off('close', closeTransport);
|
|
30
|
+
callback();
|
|
31
|
+
};
|
|
32
|
+
const closeTransport = () => {
|
|
33
|
+
transport.close().catch((error) => {
|
|
34
|
+
settle(() => reject(error instanceof Error ? error : new Error(String(error))));
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
transport.onclose = () => settle(resolve);
|
|
38
|
+
transport.onerror = (error) => {
|
|
39
|
+
options.io?.stderr.write(`KTX MCP stdio transport error: ${error.message}\n`);
|
|
40
|
+
settle(() => reject(error));
|
|
41
|
+
};
|
|
42
|
+
stdin.once('end', closeTransport);
|
|
43
|
+
stdin.once('close', closeTransport);
|
|
44
|
+
createMcpServer().connect(transport).catch((error) => {
|
|
45
|
+
settle(() => reject(error instanceof Error ? error : new Error(String(error))));
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -17,7 +17,7 @@ function replayInput() {
|
|
|
17
17
|
{ unitKey: 'customers', target: 'sl', action: 'updated', key: 'orbit_demo.customers', summary: 'customer metrics', rawFiles: ['customers'], status: 'success' },
|
|
18
18
|
],
|
|
19
19
|
provenance: [{ rawPath: 'orders', artifactKind: 'wiki', artifactKey: 'wiki/orders.md', actionType: 'wiki_written' }],
|
|
20
|
-
transcripts: [{ unitKey: 'orders', path: '/tmp/t.jsonl', toolCallCount: 2, errorCount: 0, toolNames: ['read_raw_span', '
|
|
20
|
+
transcripts: [{ unitKey: 'orders', path: '/tmp/t.jsonl', toolCallCount: 2, errorCount: 0, toolNames: ['read_raw_span', 'memory_ingest'] }],
|
|
21
21
|
},
|
|
22
22
|
events: [
|
|
23
23
|
{ type: 'source_acquired', adapter: 'live-database', trigger: 'manual_resync', fileCount: 2 },
|
package/dist/public-ingest.d.ts
CHANGED
|
@@ -75,6 +75,7 @@ export interface KtxPublicIngestDeps {
|
|
|
75
75
|
}>;
|
|
76
76
|
scanProgress?: KtxProgressPort;
|
|
77
77
|
ingestProgress?: (update: KtxIngestProgressUpdate) => void;
|
|
78
|
+
runtimeIo?: KtxCliIo;
|
|
78
79
|
onPhaseStart?: (phaseKey: KtxPublicIngestPhaseKey) => void;
|
|
79
80
|
onPhaseEnd?: (phaseKey: KtxPublicIngestPhaseKey, status: 'done' | 'failed' | 'skipped', summary?: string) => void;
|
|
80
81
|
}
|
package/dist/public-ingest.js
CHANGED
|
@@ -434,10 +434,12 @@ export async function executePublicIngestTarget(target, args, io, deps) {
|
|
|
434
434
|
const runScan = deps.runScan ?? runKtxScan;
|
|
435
435
|
const capturedScanIo = deps.scanProgress ? null : createCapturedPublicIngestIo();
|
|
436
436
|
const scanIo = capturedScanIo ?? io;
|
|
437
|
+
const scanDeps = {
|
|
438
|
+
...(deps.scanProgress ? { progress: deps.scanProgress } : {}),
|
|
439
|
+
...(deps.runtimeIo ? { runtimeIo: deps.runtimeIo } : {}),
|
|
440
|
+
};
|
|
437
441
|
deps.onPhaseStart?.('database-schema');
|
|
438
|
-
const scanExitCode =
|
|
439
|
-
? await runScan(scanArgs, scanIo, { progress: deps.scanProgress })
|
|
440
|
-
: await runScan(scanArgs, scanIo);
|
|
442
|
+
const scanExitCode = Object.keys(scanDeps).length > 0 ? await runScan(scanArgs, scanIo, scanDeps) : await runScan(scanArgs, scanIo);
|
|
441
443
|
if (scanExitCode !== 0) {
|
|
442
444
|
deps.onPhaseEnd?.('database-schema', 'failed');
|
|
443
445
|
if (target.queryHistory?.enabled === true) {
|
|
@@ -466,9 +468,13 @@ export async function executePublicIngestTarget(target, args, io, deps) {
|
|
|
466
468
|
};
|
|
467
469
|
const capturedIngestIo = deps.ingestProgress ? null : createCapturedPublicIngestIo();
|
|
468
470
|
const ingestIo = capturedIngestIo ?? io;
|
|
471
|
+
const ingestDeps = {
|
|
472
|
+
...(deps.ingestProgress ? { progress: deps.ingestProgress } : {}),
|
|
473
|
+
...(deps.runtimeIo ? { runtimeIo: deps.runtimeIo } : {}),
|
|
474
|
+
};
|
|
469
475
|
deps.onPhaseStart?.('query-history');
|
|
470
|
-
const qhExitCode =
|
|
471
|
-
? await runIngest(ingestArgs, ingestIo,
|
|
476
|
+
const qhExitCode = Object.keys(ingestDeps).length > 0
|
|
477
|
+
? await runIngest(ingestArgs, ingestIo, ingestDeps)
|
|
472
478
|
: await runIngest(ingestArgs, ingestIo);
|
|
473
479
|
if (qhExitCode !== 0) {
|
|
474
480
|
deps.onPhaseEnd?.('query-history', 'failed');
|
|
@@ -494,9 +500,13 @@ export async function executePublicIngestTarget(target, args, io, deps) {
|
|
|
494
500
|
const runIngest = deps.runIngest ?? runKtxIngest;
|
|
495
501
|
const capturedIngestIo = deps.ingestProgress ? null : createCapturedPublicIngestIo();
|
|
496
502
|
const ingestIo = capturedIngestIo ?? io;
|
|
503
|
+
const ingestDeps = {
|
|
504
|
+
...(deps.ingestProgress ? { progress: deps.ingestProgress } : {}),
|
|
505
|
+
...(deps.runtimeIo ? { runtimeIo: deps.runtimeIo } : {}),
|
|
506
|
+
};
|
|
497
507
|
deps.onPhaseStart?.('source-ingest');
|
|
498
|
-
const exitCode =
|
|
499
|
-
? await runIngest(ingestArgs, ingestIo,
|
|
508
|
+
const exitCode = Object.keys(ingestDeps).length > 0
|
|
509
|
+
? await runIngest(ingestArgs, ingestIo, ingestDeps)
|
|
500
510
|
: await runIngest(ingestArgs, ingestIo);
|
|
501
511
|
deps.onPhaseEnd?.('source-ingest', exitCode === 0 ? 'done' : 'failed');
|
|
502
512
|
return markTargetResult(target, args, exitCode === 0 ? 'done' : 'failed', 'source-ingest', capturedIngestIo ? firstCapturedFailureLine(capturedIngestIo.capturedOutput()) : undefined);
|