keystone-cli 0.1.1 → 0.3.0
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/README.md +69 -16
- package/package.json +14 -3
- package/src/cli.ts +183 -84
- package/src/db/workflow-db.ts +0 -7
- package/src/expression/evaluator.test.ts +46 -0
- package/src/expression/evaluator.ts +36 -0
- package/src/parser/agent-parser.test.ts +10 -0
- package/src/parser/agent-parser.ts +13 -5
- package/src/parser/config-schema.ts +24 -5
- package/src/parser/schema.ts +1 -1
- package/src/parser/workflow-parser.ts +5 -9
- package/src/runner/llm-adapter.test.ts +0 -8
- package/src/runner/llm-adapter.ts +33 -10
- package/src/runner/llm-executor.test.ts +230 -96
- package/src/runner/llm-executor.ts +9 -4
- package/src/runner/mcp-client.test.ts +204 -88
- package/src/runner/mcp-client.ts +349 -22
- package/src/runner/mcp-manager.test.ts +73 -15
- package/src/runner/mcp-manager.ts +84 -18
- package/src/runner/mcp-server.test.ts +4 -1
- package/src/runner/mcp-server.ts +25 -11
- package/src/runner/shell-executor.ts +3 -3
- package/src/runner/step-executor.test.ts +2 -2
- package/src/runner/step-executor.ts +31 -16
- package/src/runner/tool-integration.test.ts +21 -14
- package/src/runner/workflow-runner.ts +34 -7
- package/src/templates/agents/explore.md +54 -0
- package/src/templates/agents/general.md +8 -0
- package/src/templates/agents/keystone-architect.md +54 -0
- package/src/templates/agents/my-agent.md +3 -0
- package/src/templates/agents/summarizer.md +28 -0
- package/src/templates/agents/test-agent.md +10 -0
- package/src/templates/approval-process.yaml +36 -0
- package/src/templates/basic-inputs.yaml +19 -0
- package/src/templates/basic-shell.yaml +20 -0
- package/src/templates/batch-processor.yaml +43 -0
- package/src/templates/cleanup-finally.yaml +22 -0
- package/src/templates/composition-child.yaml +13 -0
- package/src/templates/composition-parent.yaml +14 -0
- package/src/templates/data-pipeline.yaml +38 -0
- package/src/templates/full-feature-demo.yaml +64 -0
- package/src/templates/human-interaction.yaml +12 -0
- package/src/templates/invalid.yaml +5 -0
- package/src/templates/llm-agent.yaml +8 -0
- package/src/templates/loop-parallel.yaml +37 -0
- package/src/templates/retry-policy.yaml +36 -0
- package/src/templates/scaffold-feature.yaml +48 -0
- package/src/templates/state.db +0 -0
- package/src/templates/state.db-shm +0 -0
- package/src/templates/state.db-wal +0 -0
- package/src/templates/stop-watch.yaml +17 -0
- package/src/templates/workflow.db +0 -0
- package/src/utils/auth-manager.test.ts +86 -0
- package/src/utils/auth-manager.ts +89 -0
- package/src/utils/config-loader.test.ts +32 -2
- package/src/utils/config-loader.ts +11 -1
- package/src/utils/mermaid.test.ts +27 -3
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
|
|
2
|
-
import { MCPClient } from './mcp-client';
|
|
3
2
|
import * as child_process from 'node:child_process';
|
|
4
3
|
import { EventEmitter } from 'node:events';
|
|
5
4
|
import { Readable, Writable } from 'node:stream';
|
|
5
|
+
import { MCPClient } from './mcp-client';
|
|
6
6
|
|
|
7
7
|
interface MockProcess extends EventEmitter {
|
|
8
8
|
stdout: Readable;
|
|
@@ -11,112 +11,228 @@ interface MockProcess extends EventEmitter {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
describe('MCPClient', () => {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
describe('Local Transport', () => {
|
|
15
|
+
let mockProcess: MockProcess;
|
|
16
|
+
let spawnSpy: ReturnType<typeof spyOn>;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
const emitter = new EventEmitter();
|
|
20
|
+
const stdout = new Readable({
|
|
21
|
+
read() {},
|
|
22
|
+
});
|
|
23
|
+
const stdin = new Writable({
|
|
24
|
+
write(_chunk, _encoding, callback) {
|
|
25
|
+
callback();
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const kill = mock(() => {});
|
|
16
29
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
mockProcess = Object.assign(emitter, { stdout, stdin, kill }) as unknown as MockProcess;
|
|
31
|
+
|
|
32
|
+
spawnSpy = spyOn(child_process, 'spawn').mockReturnValue(
|
|
33
|
+
mockProcess as unknown as child_process.ChildProcess
|
|
34
|
+
);
|
|
21
35
|
});
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
},
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
spawnSpy.mockRestore();
|
|
26
39
|
});
|
|
27
|
-
const kill = mock(() => {});
|
|
28
40
|
|
|
29
|
-
|
|
41
|
+
it('should initialize correctly', async () => {
|
|
42
|
+
const client = await MCPClient.createLocal('node', ['server.js']);
|
|
43
|
+
const initPromise = client.initialize();
|
|
44
|
+
|
|
45
|
+
// Simulate server response
|
|
46
|
+
mockProcess.stdout.push(
|
|
47
|
+
`${JSON.stringify({
|
|
48
|
+
jsonrpc: '2.0',
|
|
49
|
+
id: 0,
|
|
50
|
+
result: { protocolVersion: '2024-11-05' },
|
|
51
|
+
})}\n`
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const response = await initPromise;
|
|
55
|
+
expect(response.result?.protocolVersion).toBe('2024-11-05');
|
|
56
|
+
expect(spawnSpy).toHaveBeenCalledWith('node', ['server.js'], expect.any(Object));
|
|
57
|
+
});
|
|
30
58
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
it('should list tools', async () => {
|
|
60
|
+
const client = await MCPClient.createLocal('node');
|
|
61
|
+
const listPromise = client.listTools();
|
|
62
|
+
|
|
63
|
+
mockProcess.stdout.push(
|
|
64
|
+
`${JSON.stringify({
|
|
65
|
+
jsonrpc: '2.0',
|
|
66
|
+
id: 0,
|
|
67
|
+
result: {
|
|
68
|
+
tools: [{ name: 'test-tool', inputSchema: {} }],
|
|
69
|
+
},
|
|
70
|
+
})}\n`
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const tools = await listPromise;
|
|
74
|
+
expect(tools).toHaveLength(1);
|
|
75
|
+
expect(tools[0].name).toBe('test-tool');
|
|
76
|
+
});
|
|
35
77
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
78
|
+
it('should call tool', async () => {
|
|
79
|
+
const client = await MCPClient.createLocal('node');
|
|
80
|
+
const callPromise = client.callTool('my-tool', { arg: 1 });
|
|
39
81
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
jsonrpc: '2.0',
|
|
48
|
-
id: 0,
|
|
49
|
-
result: { protocolVersion: '2024-11-05' },
|
|
50
|
-
})}\n`
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
const response = await initPromise;
|
|
54
|
-
expect(response.result?.protocolVersion).toBe('2024-11-05');
|
|
55
|
-
expect(spawnSpy).toHaveBeenCalledWith('node', ['server.js'], expect.any(Object));
|
|
56
|
-
});
|
|
82
|
+
mockProcess.stdout.push(
|
|
83
|
+
`${JSON.stringify({
|
|
84
|
+
jsonrpc: '2.0',
|
|
85
|
+
id: 0,
|
|
86
|
+
result: { content: [{ type: 'text', text: 'success' }] },
|
|
87
|
+
})}\n`
|
|
88
|
+
);
|
|
57
89
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
90
|
+
const result = await callPromise;
|
|
91
|
+
expect((result as { content: Array<{ text: string }> }).content[0].text).toBe('success');
|
|
92
|
+
});
|
|
61
93
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
id: 0,
|
|
66
|
-
result: {
|
|
67
|
-
tools: [{ name: 'test-tool', inputSchema: {} }],
|
|
68
|
-
},
|
|
69
|
-
})}\n`
|
|
70
|
-
);
|
|
94
|
+
it('should handle tool call error', async () => {
|
|
95
|
+
const client = await MCPClient.createLocal('node');
|
|
96
|
+
const callPromise = client.callTool('my-tool', {});
|
|
71
97
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
98
|
+
mockProcess.stdout.push(
|
|
99
|
+
`${JSON.stringify({
|
|
100
|
+
jsonrpc: '2.0',
|
|
101
|
+
id: 0,
|
|
102
|
+
error: { code: -32000, message: 'Tool error' },
|
|
103
|
+
})}\n`
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
await expect(callPromise).rejects.toThrow(/MCP tool call failed/);
|
|
107
|
+
});
|
|
76
108
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
109
|
+
it('should timeout on request', async () => {
|
|
110
|
+
// Set a very short timeout for testing
|
|
111
|
+
const client = await MCPClient.createLocal('node', [], {}, 10);
|
|
112
|
+
const requestPromise = client.initialize();
|
|
80
113
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
id: 0,
|
|
85
|
-
result: { content: [{ type: 'text', text: 'success' }] },
|
|
86
|
-
})}\n`
|
|
87
|
-
);
|
|
114
|
+
// Don't push any response to stdout
|
|
115
|
+
await expect(requestPromise).rejects.toThrow(/MCP request timeout/);
|
|
116
|
+
});
|
|
88
117
|
|
|
89
|
-
|
|
90
|
-
|
|
118
|
+
it('should stop the process', async () => {
|
|
119
|
+
const client = await MCPClient.createLocal('node');
|
|
120
|
+
client.stop();
|
|
121
|
+
expect(mockProcess.kill).toHaveBeenCalled();
|
|
122
|
+
});
|
|
91
123
|
});
|
|
92
124
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
125
|
+
describe('SSE Transport', () => {
|
|
126
|
+
it('should connect and receive endpoint', async () => {
|
|
127
|
+
let controller: ReadableStreamDefaultController;
|
|
128
|
+
const stream = new ReadableStream({
|
|
129
|
+
start(c) {
|
|
130
|
+
controller = c;
|
|
131
|
+
controller.enqueue(new TextEncoder().encode('event: endpoint\ndata: /endpoint\n\n'));
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const fetchMock = spyOn(global, 'fetch').mockImplementation((url) => {
|
|
136
|
+
if (url === 'http://localhost:8080/sse') {
|
|
137
|
+
return Promise.resolve(new Response(stream));
|
|
138
|
+
}
|
|
139
|
+
return Promise.resolve(new Response(JSON.stringify({ ok: true })));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const clientPromise = MCPClient.createRemote('http://localhost:8080/sse');
|
|
143
|
+
|
|
144
|
+
const client = await clientPromise;
|
|
145
|
+
expect(client).toBeDefined();
|
|
146
|
+
|
|
147
|
+
const initPromise = client.initialize();
|
|
148
|
+
|
|
149
|
+
// Simulate message event (response from server)
|
|
150
|
+
if (controller) {
|
|
151
|
+
controller.enqueue(
|
|
152
|
+
new TextEncoder().encode(
|
|
153
|
+
`data: ${JSON.stringify({
|
|
154
|
+
jsonrpc: '2.0',
|
|
155
|
+
id: 0,
|
|
156
|
+
result: { protocolVersion: '2024-11-05' },
|
|
157
|
+
})}\n\n`
|
|
158
|
+
)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const response = await initPromise;
|
|
163
|
+
expect(response.result?.protocolVersion).toBe('2024-11-05');
|
|
164
|
+
expect(fetchMock).toHaveBeenCalledWith('http://localhost:8080/endpoint', expect.any(Object));
|
|
165
|
+
|
|
166
|
+
client.stop();
|
|
167
|
+
fetchMock.mockRestore();
|
|
168
|
+
});
|
|
96
169
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
170
|
+
it('should handle SSE with multiple events and chunked data', async () => {
|
|
171
|
+
let controller: ReadableStreamDefaultController;
|
|
172
|
+
const stream = new ReadableStream({
|
|
173
|
+
start(c) {
|
|
174
|
+
controller = c;
|
|
175
|
+
// Send endpoint event
|
|
176
|
+
controller.enqueue(new TextEncoder().encode('event: endpoint\n'));
|
|
177
|
+
controller.enqueue(new TextEncoder().encode('data: /endpoint\n\n'));
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const fetchMock = spyOn(global, 'fetch').mockImplementation((url) => {
|
|
182
|
+
if (url === 'http://localhost:8080/sse') {
|
|
183
|
+
return Promise.resolve(new Response(stream));
|
|
184
|
+
}
|
|
185
|
+
return Promise.resolve(new Response(JSON.stringify({ ok: true })));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const client = await MCPClient.createRemote('http://localhost:8080/sse');
|
|
189
|
+
|
|
190
|
+
// We can't easily hook into onMessage without reaching into internals
|
|
191
|
+
// Instead, we'll test that initialize resolves correctly when the response arrives
|
|
192
|
+
const initPromise = client.initialize();
|
|
193
|
+
|
|
194
|
+
// Enqueue data in chunks
|
|
195
|
+
if (controller) {
|
|
196
|
+
controller.enqueue(new TextEncoder().encode('data: {"jsonrpc":"2.0","id":0,'));
|
|
197
|
+
controller.enqueue(
|
|
198
|
+
new TextEncoder().encode('"result":{"protocolVersion":"2024-11-05"}}\n\n')
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Send another event (just to test dispatching doesn't crash)
|
|
202
|
+
controller.enqueue(
|
|
203
|
+
new TextEncoder().encode(
|
|
204
|
+
'event: message\ndata: {"jsonrpc":"2.0","id":99,"result":"ignored"}\n\n'
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Send empty line
|
|
209
|
+
controller.enqueue(new TextEncoder().encode('\n'));
|
|
210
|
+
|
|
211
|
+
controller.close();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const response = await initPromise;
|
|
215
|
+
expect(response.result?.protocolVersion).toBe('2024-11-05');
|
|
216
|
+
|
|
217
|
+
client.stop();
|
|
218
|
+
fetchMock.mockRestore();
|
|
219
|
+
});
|
|
104
220
|
|
|
105
|
-
|
|
106
|
-
|
|
221
|
+
it('should handle SSE connection failure', async () => {
|
|
222
|
+
const fetchMock = spyOn(global, 'fetch').mockImplementation(() =>
|
|
223
|
+
Promise.resolve(
|
|
224
|
+
new Response(null, {
|
|
225
|
+
status: 500,
|
|
226
|
+
statusText: 'Internal Server Error',
|
|
227
|
+
})
|
|
228
|
+
)
|
|
229
|
+
);
|
|
107
230
|
|
|
108
|
-
|
|
109
|
-
// Set a very short timeout for testing
|
|
110
|
-
const client = new MCPClient('node', [], {}, 10);
|
|
111
|
-
const requestPromise = client.initialize();
|
|
231
|
+
const clientPromise = MCPClient.createRemote('http://localhost:8080/sse');
|
|
112
232
|
|
|
113
|
-
|
|
114
|
-
await expect(requestPromise).rejects.toThrow(/MCP request timeout/);
|
|
115
|
-
});
|
|
233
|
+
await expect(clientPromise).rejects.toThrow(/SSE connection failed: 500/);
|
|
116
234
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
client.stop();
|
|
120
|
-
expect(mockProcess.kill).toHaveBeenCalled();
|
|
235
|
+
fetchMock.mockRestore();
|
|
236
|
+
});
|
|
121
237
|
});
|
|
122
238
|
});
|