@namzu/cli 0.8.0 → 1.0.1
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 +1 -1
- package/dist/__tests__/headless-trust-gate.test.d.ts +23 -0
- package/dist/__tests__/headless-trust-gate.test.d.ts.map +1 -0
- package/dist/__tests__/headless-trust-gate.test.js +227 -0
- package/dist/__tests__/headless-trust-gate.test.js.map +1 -0
- package/dist/__tests__/mcp-servers-reach-the-session.test.d.ts +17 -0
- package/dist/__tests__/mcp-servers-reach-the-session.test.d.ts.map +1 -0
- package/dist/__tests__/mcp-servers-reach-the-session.test.js +171 -0
- package/dist/__tests__/mcp-servers-reach-the-session.test.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/__tests__/run-exit-code.test.js +11 -0
- package/dist/commands/__tests__/run-exit-code.test.js.map +1 -1
- package/dist/commands/__tests__/run-flags.test.js +12 -0
- package/dist/commands/__tests__/run-flags.test.js.map +1 -1
- package/dist/commands/__tests__/run-stream-flags.test.js +16 -0
- package/dist/commands/__tests__/run-stream-flags.test.js.map +1 -1
- package/dist/commands/run-flags.d.ts +11 -0
- package/dist/commands/run-flags.d.ts.map +1 -1
- package/dist/commands/run-flags.js +5 -0
- package/dist/commands/run-flags.js.map +1 -1
- package/dist/commands/run-stream.d.ts +7 -0
- package/dist/commands/run-stream.d.ts.map +1 -1
- package/dist/commands/run-stream.js +49 -1
- package/dist/commands/run-stream.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +43 -2
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/stubs.d.ts +1 -1
- package/dist/commands/stubs.js +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +5 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +9 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/doctor/registry.test.d.ts +1 -1
- package/dist/doctor/registry.test.js +1 -1
- package/dist/exit-codes.d.ts +11 -1
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +10 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/integrations/mcp/__tests__/connect.test.d.ts +11 -0
- package/dist/integrations/mcp/__tests__/connect.test.d.ts.map +1 -0
- package/dist/integrations/mcp/__tests__/connect.test.js +231 -0
- package/dist/integrations/mcp/__tests__/connect.test.js.map +1 -0
- package/dist/integrations/mcp/servers.d.ts +101 -0
- package/dist/integrations/mcp/servers.d.ts.map +1 -0
- package/dist/integrations/mcp/servers.js +160 -0
- package/dist/integrations/mcp/servers.js.map +1 -0
- package/dist/integrations/sessions/__tests__/archived-workspace.test.d.ts +21 -0
- package/dist/integrations/sessions/__tests__/archived-workspace.test.d.ts.map +1 -0
- package/dist/integrations/sessions/__tests__/archived-workspace.test.js +62 -0
- package/dist/integrations/sessions/__tests__/archived-workspace.test.js.map +1 -0
- package/dist/integrations/sessions/store.d.ts +15 -1
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +17 -2
- package/dist/integrations/sessions/store.js.map +1 -1
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +13 -0
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/permissions/headless-trust.d.ts +60 -0
- package/dist/permissions/headless-trust.d.ts.map +1 -0
- package/dist/permissions/headless-trust.js +72 -0
- package/dist/permissions/headless-trust.js.map +1 -0
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +21 -0
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/agent.d.ts +24 -0
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +15 -0
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/types.d.ts +7 -0
- package/dist/tui/types.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/exit-codes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exit-codes.d.ts","sourceRoot":"","sources":["../src/exit-codes.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"exit-codes.d.ts","sourceRoot":"","sources":["../src/exit-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,OAAO,IAAI,CAAA;AACxB,eAAO,MAAM,SAAS,IAAI,CAAA;AAC1B,eAAO,MAAM,cAAc,IAAI,CAAA;AAC/B,eAAO,MAAM,UAAU,KAAK,CAAA;AAC5B,eAAO,MAAM,mBAAmB,KAAK,CAAA;AACrC,eAAO,MAAM,cAAc,KAAK,CAAA;AAEhC,MAAM,MAAM,WAAW,GACpB,OAAO,OAAO,GACd,OAAO,SAAS,GAChB,OAAO,cAAc,GACrB,OAAO,UAAU,GACjB,OAAO,mBAAmB,GAC1B,OAAO,cAAc,CAAA"}
|
package/dist/exit-codes.js
CHANGED
|
@@ -12,10 +12,20 @@
|
|
|
12
12
|
* the invocation is, and reporting one as the other
|
|
13
13
|
* sends the reader looking in the wrong place.
|
|
14
14
|
* 70 EXIT_INTERNAL_ERROR — sysexits EX_SOFTWARE; internal CLI error
|
|
15
|
+
* 77 EXIT_UNTRUSTED — sysexits EX_NOPERM; the working directory has
|
|
16
|
+
* not been trusted, so a headless run refused to
|
|
17
|
+
* start. Its own code because a caller has to be
|
|
18
|
+
* able to tell it from 64 (your arguments are
|
|
19
|
+
* wrong) and from 1 (the run failed): this one is
|
|
20
|
+
* fixed by a human decision about a folder, and
|
|
21
|
+
* nothing else is. A caller who cannot tell them
|
|
22
|
+
* apart matches on the message string, and then
|
|
23
|
+
* the message can never be reworded.
|
|
15
24
|
*/
|
|
16
25
|
export const EXIT_OK = 0;
|
|
17
26
|
export const EXIT_FAIL = 1;
|
|
18
27
|
export const EXIT_NO_CONFIG = 2;
|
|
19
28
|
export const EXIT_USAGE = 64;
|
|
20
29
|
export const EXIT_INTERNAL_ERROR = 70;
|
|
30
|
+
export const EXIT_UNTRUSTED = 77;
|
|
21
31
|
//# sourceMappingURL=exit-codes.js.map
|
package/dist/exit-codes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exit-codes.js","sourceRoot":"","sources":["../src/exit-codes.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"exit-codes.js","sourceRoot":"","sources":["../src/exit-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAA;AACxB,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAA;AAC1B,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAA;AAC/B,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,CAAA;AAC5B,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAA;AACrC,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An external tool server declared in the config is actually reachable.
|
|
3
|
+
*
|
|
4
|
+
* The server here is a REAL child process speaking real JSON-RPC over stdio,
|
|
5
|
+
* written to a temp file and spawned by the shipped transport. A stubbed
|
|
6
|
+
* transport would prove the adapter agrees with itself and would say nothing
|
|
7
|
+
* about the thing that actually breaks: a process that spawns, or does not, and
|
|
8
|
+
* a handshake that answers, or does not.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=connect.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.test.d.ts","sourceRoot":"","sources":["../../../../src/integrations/mcp/__tests__/connect.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An external tool server declared in the config is actually reachable.
|
|
3
|
+
*
|
|
4
|
+
* The server here is a REAL child process speaking real JSON-RPC over stdio,
|
|
5
|
+
* written to a temp file and spawned by the shipped transport. A stubbed
|
|
6
|
+
* transport would prove the adapter agrees with itself and would say nothing
|
|
7
|
+
* about the thing that actually breaks: a process that spawns, or does not, and
|
|
8
|
+
* a handshake that answers, or does not.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
14
|
+
import { connectMcpServers, transportFor } from '../servers.js';
|
|
15
|
+
let dir;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
dir = mkdtempSync(join(tmpdir(), 'namzu-mcp-'));
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
// A child's working directory is the temp directory, and `close()` sends
|
|
21
|
+
// SIGTERM without waiting for the exit — so on Windows the directory is
|
|
22
|
+
// still held for a moment after the last assertion. Retried rather than
|
|
23
|
+
// ignored: a directory that never frees would mean a process that never
|
|
24
|
+
// died, which is the failure `close()` exists to prevent.
|
|
25
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
26
|
+
try {
|
|
27
|
+
rmSync(dir, { recursive: true, force: true });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
rmSync(dir, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* Resolves once the process is gone, or throws after the deadline.
|
|
38
|
+
*
|
|
39
|
+
* This asserts the SYMPTOM — the pid no longer exists — and not the mechanism,
|
|
40
|
+
* and that distinction is worth stating because the same shape can be sound
|
|
41
|
+
* about the wrong thing: a child that would have exited on its own the moment
|
|
42
|
+
* its stdin pipe closed would satisfy this whether or not anything killed it,
|
|
43
|
+
* and the test would report that `close()` works while `close()` did nothing.
|
|
44
|
+
*
|
|
45
|
+
* MEASURED, not reasoned about. With `close()` replaced by an early `return`,
|
|
46
|
+
* this test fails with "process N was still alive after 5000ms" — so the child
|
|
47
|
+
* does not go away by itself here, and the pid disappearing is caused by the
|
|
48
|
+
* shutdown path under test. The symptom is the right claim for this package:
|
|
49
|
+
* whether the runtime also REAPS the child is the transport's business, and
|
|
50
|
+
* "no process left behind" is what a user is owed.
|
|
51
|
+
*/
|
|
52
|
+
async function waitForExit(pid, deadlineMs = 5_000) {
|
|
53
|
+
const until = Date.now() + deadlineMs;
|
|
54
|
+
for (;;) {
|
|
55
|
+
try {
|
|
56
|
+
// Signal 0 tests for existence without delivering anything.
|
|
57
|
+
process.kill(pid, 0);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (Date.now() > until)
|
|
63
|
+
throw new Error(`process ${pid} was still alive after ${deadlineMs}ms`);
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The smallest server that satisfies the protocol: initialize, tools/list,
|
|
69
|
+
* tools/call. Newline-delimited JSON-RPC on stdio, which is the wire the
|
|
70
|
+
* shipped `StdioTransport` speaks.
|
|
71
|
+
*/
|
|
72
|
+
function writeServer(name, body) {
|
|
73
|
+
const path = join(dir, name);
|
|
74
|
+
writeFileSync(path, body);
|
|
75
|
+
return path;
|
|
76
|
+
}
|
|
77
|
+
const WORKING_SERVER = `
|
|
78
|
+
if (process.argv[2]) require('node:fs').writeFileSync(process.argv[2], String(process.pid))
|
|
79
|
+
let buf = ''
|
|
80
|
+
process.stdin.on('data', (chunk) => {
|
|
81
|
+
buf += chunk
|
|
82
|
+
let i
|
|
83
|
+
while ((i = buf.indexOf('\\n')) >= 0) {
|
|
84
|
+
const line = buf.slice(0, i)
|
|
85
|
+
buf = buf.slice(i + 1)
|
|
86
|
+
if (!line.trim()) continue
|
|
87
|
+
const msg = JSON.parse(line)
|
|
88
|
+
if (msg.method === 'initialize') {
|
|
89
|
+
send({ jsonrpc: '2.0', id: msg.id, result: {
|
|
90
|
+
protocolVersion: msg.params.protocolVersion,
|
|
91
|
+
serverInfo: { name: 'tickets', version: '1' },
|
|
92
|
+
capabilities: { tools: {} },
|
|
93
|
+
}})
|
|
94
|
+
} else if (msg.method === 'tools/list') {
|
|
95
|
+
send({ jsonrpc: '2.0', id: msg.id, result: { tools: [
|
|
96
|
+
{ name: 'create', description: 'Open a ticket', inputSchema: {
|
|
97
|
+
type: 'object', properties: { title: { type: 'string' } }, required: ['title'] } },
|
|
98
|
+
{ name: 'close', description: 'Close a ticket', inputSchema: {
|
|
99
|
+
type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
|
|
100
|
+
]}})
|
|
101
|
+
} else if (msg.method === 'tools/call') {
|
|
102
|
+
send({ jsonrpc: '2.0', id: msg.id, result: {
|
|
103
|
+
content: [{ type: 'text', text: 'opened ' + msg.params.arguments.title }],
|
|
104
|
+
}})
|
|
105
|
+
} else if (msg.id !== undefined) {
|
|
106
|
+
send({ jsonrpc: '2.0', id: msg.id, result: {} })
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
function send(o) { process.stdout.write(JSON.stringify(o) + '\\n') }
|
|
111
|
+
`;
|
|
112
|
+
/** Starts, and never answers. The case a request timeout cannot cover. */
|
|
113
|
+
const SILENT_SERVER = 'setInterval(() => {}, 1000)\n';
|
|
114
|
+
describe('a declared server', () => {
|
|
115
|
+
it('brings its tools, named after it', async () => {
|
|
116
|
+
const server = writeServer('tickets.js', WORKING_SERVER);
|
|
117
|
+
const mcp = await connectMcpServers({ tickets: { command: process.execPath, args: [server] } }, { cwd: dir });
|
|
118
|
+
try {
|
|
119
|
+
expect(mcp.failed).toEqual([]);
|
|
120
|
+
expect(mcp.connected).toEqual([{ name: 'tickets', toolCount: 2 }]);
|
|
121
|
+
// Prefixed with the server name so two servers offering `create` do
|
|
122
|
+
// not collide and the transcript says where a call went.
|
|
123
|
+
expect(mcp.tools.map((t) => t.name)).toEqual(['mcp_tickets_create', 'mcp_tickets_close']);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await mcp.close();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
it('produces tools that actually call the server', async () => {
|
|
130
|
+
// The tool objects existing is not the feature. Registering something the
|
|
131
|
+
// model can see and cannot successfully invoke is worse than having
|
|
132
|
+
// nothing, because the failure arrives mid-task.
|
|
133
|
+
const server = writeServer('tickets.js', WORKING_SERVER);
|
|
134
|
+
const mcp = await connectMcpServers({ tickets: { command: process.execPath, args: [server] } }, { cwd: dir });
|
|
135
|
+
try {
|
|
136
|
+
const create = mcp.tools.find((t) => t.name === 'mcp_tickets_create');
|
|
137
|
+
const result = await create?.execute({ title: 'the build is red' }, {});
|
|
138
|
+
expect(result?.success).toBe(true);
|
|
139
|
+
expect(JSON.stringify(result?.output)).toContain('opened the build is red');
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
await mcp.close();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
it('leaves no child process behind when the session closes', async () => {
|
|
146
|
+
// The reason `close()` exists. A stdio server is a child process and
|
|
147
|
+
// nothing else in this package owns one, so before this there was no
|
|
148
|
+
// shutdown path at all: every TUI session and every one-shot left its
|
|
149
|
+
// servers running.
|
|
150
|
+
const server = writeServer('tickets.js', WORKING_SERVER);
|
|
151
|
+
const pidFile = join(dir, 'server.pid');
|
|
152
|
+
const mcp = await connectMcpServers({ tickets: { command: process.execPath, args: [server, pidFile] } }, { cwd: dir });
|
|
153
|
+
const pid = Number(readFileSync(pidFile, 'utf8'));
|
|
154
|
+
expect(Number.isInteger(pid), 'the server must have actually started').toBe(true);
|
|
155
|
+
expect(() => process.kill(pid, 0), 'and must be alive before the close').not.toThrow();
|
|
156
|
+
await mcp.close();
|
|
157
|
+
await waitForExit(pid);
|
|
158
|
+
}, 20_000);
|
|
159
|
+
it('is absent from the roster when nothing is configured', async () => {
|
|
160
|
+
const mcp = await connectMcpServers(undefined, { cwd: dir });
|
|
161
|
+
expect(mcp.tools).toEqual([]);
|
|
162
|
+
expect(mcp.connected).toEqual([]);
|
|
163
|
+
expect(mcp.failed).toEqual([]);
|
|
164
|
+
await mcp.close();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe('a server that does not work is named, never merely absent', () => {
|
|
168
|
+
it('reports a command that cannot be spawned', async () => {
|
|
169
|
+
const mcp = await connectMcpServers({ tickets: { command: join(dir, 'no-such-executable') } }, { cwd: dir });
|
|
170
|
+
expect(mcp.connected).toEqual([]);
|
|
171
|
+
expect(mcp.failed).toHaveLength(1);
|
|
172
|
+
expect(mcp.failed[0]?.name).toBe('tickets');
|
|
173
|
+
expect(mcp.failed[0]?.reason.length).toBeGreaterThan(0);
|
|
174
|
+
await mcp.close();
|
|
175
|
+
});
|
|
176
|
+
it('reports a server that starts and never answers, rather than hanging', async () => {
|
|
177
|
+
// The client's per-request timeout cannot cover this: the process is up,
|
|
178
|
+
// the pipe is open, and nothing is coming. Without the connect deadline a
|
|
179
|
+
// single wedged server holds the whole session open before the first turn
|
|
180
|
+
// — no error, no failure, just a namzu that does not start.
|
|
181
|
+
const server = writeServer('silent.js', SILENT_SERVER);
|
|
182
|
+
const started = Date.now();
|
|
183
|
+
const mcp = await connectMcpServers({ quiet: { command: process.execPath, args: [server] } }, { cwd: dir });
|
|
184
|
+
expect(mcp.failed[0]?.name).toBe('quiet');
|
|
185
|
+
expect(mcp.failed[0]?.reason).toContain('did not answer');
|
|
186
|
+
expect(Date.now() - started, 'the deadline has to actually bound it').toBeLessThan(30_000);
|
|
187
|
+
await mcp.close();
|
|
188
|
+
}, 40_000);
|
|
189
|
+
it('does not let one broken server take the working ones with it', async () => {
|
|
190
|
+
const good = writeServer('tickets.js', WORKING_SERVER);
|
|
191
|
+
const mcp = await connectMcpServers({
|
|
192
|
+
broken: { command: join(dir, 'no-such-executable') },
|
|
193
|
+
tickets: { command: process.execPath, args: [good] },
|
|
194
|
+
}, { cwd: dir });
|
|
195
|
+
try {
|
|
196
|
+
expect(mcp.failed.map((f) => f.name)).toEqual(['broken']);
|
|
197
|
+
expect(mcp.connected.map((c) => c.name)).toEqual(['tickets']);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
await mcp.close();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
describe('a spec that is not a server', () => {
|
|
205
|
+
it('refuses one that names neither a command nor a url', () => {
|
|
206
|
+
expect(transportFor({}, dir)).toContain('neither a command nor a url');
|
|
207
|
+
});
|
|
208
|
+
it('refuses one that names both, rather than picking', () => {
|
|
209
|
+
// An operator who edited a url into an entry that already had a command
|
|
210
|
+
// meant one of them. Choosing for them runs something they did not ask
|
|
211
|
+
// to run.
|
|
212
|
+
expect(transportFor({ command: 'x', url: 'https://y' }, dir)).toContain('both');
|
|
213
|
+
});
|
|
214
|
+
it('reports a bad spec by name, alongside the servers that worked', async () => {
|
|
215
|
+
const good = writeServer('tickets.js', WORKING_SERVER);
|
|
216
|
+
const mcp = await connectMcpServers({ nonsense: {}, tickets: { command: process.execPath, args: [good] } }, { cwd: dir });
|
|
217
|
+
try {
|
|
218
|
+
expect(mcp.failed[0]?.name).toBe('nonsense');
|
|
219
|
+
expect(mcp.connected.map((c) => c.name)).toEqual(['tickets']);
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
await mcp.close();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
it('defaults a stdio server to the agent working directory', () => {
|
|
226
|
+
const transport = transportFor({ command: 'node' }, dir);
|
|
227
|
+
expect(typeof transport).not.toBe('string');
|
|
228
|
+
expect(transport.cwd).toBe(dir);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
//# sourceMappingURL=connect.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.test.js","sourceRoot":"","sources":["../../../../src/integrations/mcp/__tests__/connect.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External tool servers an operator declares in their config.
|
|
3
|
+
*
|
|
4
|
+
* The kernel has spoken this protocol for a long time — `MCPClient`,
|
|
5
|
+
* `StdioTransport`, `StreamableHttpTransport` and the tool adapter are all
|
|
6
|
+
* exported from `@namzu/sdk`. `packages/cli` imported none of them, so the
|
|
7
|
+
* capability existed and was unreachable from the product: a namzu user could
|
|
8
|
+
* not connect an external tool server at all, whatever the kernel could do.
|
|
9
|
+
*
|
|
10
|
+
* A server is declared by name under `mcpServers` in `namzu.config.json`:
|
|
11
|
+
*
|
|
12
|
+
* "mcpServers": {
|
|
13
|
+
* "tickets": { "command": "node", "args": ["./tickets-server.js"] },
|
|
14
|
+
* "search": { "url": "https://tools.example.internal/mcp" }
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* Its tools arrive prefixed with the server's name (`mcp_tickets_create`), so
|
|
18
|
+
* two servers offering `search` do not collide and the transcript says where a
|
|
19
|
+
* call went.
|
|
20
|
+
*
|
|
21
|
+
* ## Every failure is named
|
|
22
|
+
*
|
|
23
|
+
* A server that does not start, a spec that names neither a command nor a URL,
|
|
24
|
+
* a handshake that never answers — each becomes an entry in `failed` with a
|
|
25
|
+
* reason, never an absence. The whole hazard of this feature is the operator
|
|
26
|
+
* who configures a server, watches the agent run without its tools, and
|
|
27
|
+
* concludes the model is bad at the task.
|
|
28
|
+
*
|
|
29
|
+
* What to DO about a failure is not decided here, because the answer differs
|
|
30
|
+
* by surface: a person watching the TUI can read the line and fix their config,
|
|
31
|
+
* and a headless run has nobody to read anything, so it refuses. This module
|
|
32
|
+
* reports; the callers decide.
|
|
33
|
+
*/
|
|
34
|
+
import { type MCPTransportUnion, type ToolDefinition } from '@namzu/sdk';
|
|
35
|
+
/**
|
|
36
|
+
* How long a single server gets to connect, hand shake and list its tools.
|
|
37
|
+
*
|
|
38
|
+
* The client's own `requestTimeoutMs` bounds one round trip; it cannot bound a
|
|
39
|
+
* process that spawns and never speaks. Without this, one wedged server holds
|
|
40
|
+
* the whole session open before the first turn — no error, no failure, just a
|
|
41
|
+
* namzu that does not start.
|
|
42
|
+
*/
|
|
43
|
+
export declare const CONNECT_TIMEOUT_MS = 10000;
|
|
44
|
+
/**
|
|
45
|
+
* How long shutting one server down may take before it is given up on.
|
|
46
|
+
*
|
|
47
|
+
* Shorter than the connect bound because nothing waits on the answer: a
|
|
48
|
+
* one-shot is exiting and a TUI is replacing the session. What this prevents
|
|
49
|
+
* is the opposite of a leak — a `close()` that never resolves, holding the
|
|
50
|
+
* command open past the work it was asked to do.
|
|
51
|
+
*/
|
|
52
|
+
export declare const CLOSE_TIMEOUT_MS = 2000;
|
|
53
|
+
/** A single entry under `mcpServers`. Either a command or a URL, never both. */
|
|
54
|
+
export interface McpServerSpec {
|
|
55
|
+
/** Stdio: the executable to run. */
|
|
56
|
+
readonly command?: string;
|
|
57
|
+
readonly args?: readonly string[];
|
|
58
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
59
|
+
/** Working directory for the child. Defaults to the agent's. */
|
|
60
|
+
readonly cwd?: string;
|
|
61
|
+
/** HTTP: the server's endpoint. */
|
|
62
|
+
readonly url?: string;
|
|
63
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
64
|
+
}
|
|
65
|
+
export type McpServersConfig = Readonly<Record<string, McpServerSpec>>;
|
|
66
|
+
export interface ConnectedMcpServer {
|
|
67
|
+
readonly name: string;
|
|
68
|
+
readonly toolCount: number;
|
|
69
|
+
}
|
|
70
|
+
export interface FailedMcpServer {
|
|
71
|
+
readonly name: string;
|
|
72
|
+
/** Phrased for one line in front of a person who has to fix it. */
|
|
73
|
+
readonly reason: string;
|
|
74
|
+
}
|
|
75
|
+
export interface McpConnection {
|
|
76
|
+
/** Adapted tools from every server that connected, ready to register. */
|
|
77
|
+
readonly tools: readonly ToolDefinition[];
|
|
78
|
+
readonly connected: readonly ConnectedMcpServer[];
|
|
79
|
+
readonly failed: readonly FailedMcpServer[];
|
|
80
|
+
/**
|
|
81
|
+
* Shut every connected server down.
|
|
82
|
+
*
|
|
83
|
+
* A stdio server is a CHILD PROCESS. Nothing else in this package owns one,
|
|
84
|
+
* which is why the session had no shutdown path before: without this, a TUI
|
|
85
|
+
* session that ends leaves the child running, and a long-lived host that
|
|
86
|
+
* opens sessions leaks one per session.
|
|
87
|
+
*/
|
|
88
|
+
close(): Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Turn one spec into a transport, or say why it is not one.
|
|
92
|
+
*
|
|
93
|
+
* Refused rather than guessed. A spec with both a command and a URL is an
|
|
94
|
+
* operator who edited one into a file that already had the other, and picking
|
|
95
|
+
* either would run something they did not mean to run.
|
|
96
|
+
*/
|
|
97
|
+
export declare function transportFor(spec: McpServerSpec, defaultCwd: string): MCPTransportUnion | string;
|
|
98
|
+
export declare function connectMcpServers(config: McpServersConfig | undefined, options: {
|
|
99
|
+
readonly cwd: string;
|
|
100
|
+
}): Promise<McpConnection>;
|
|
101
|
+
//# sourceMappingURL=servers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"servers.d.ts","sourceRoot":"","sources":["../../../src/integrations/mcp/servers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAEN,KAAK,iBAAiB,EACtB,KAAK,cAAc,EAEnB,MAAM,YAAY,CAAA;AAEnB;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,QAAS,CAAA;AAExC;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,OAAQ,CAAA;AAErC,gFAAgF;AAChF,MAAM,WAAW,aAAa;IAC7B,oCAAoC;IACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC/C,gEAAgE;IAChE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,mCAAmC;IACnC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACnD;AAED,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAA;AAEtE,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,aAAa;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAA;IACzC,QAAQ,CAAC,SAAS,EAAE,SAAS,kBAAkB,EAAE,CAAA;IACjD,QAAQ,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAA;IAC3C;;;;;;;OAOG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,iBAAiB,GAAG,MAAM,CAuBhG;AAkBD,wBAAsB,iBAAiB,CACtC,MAAM,EAAE,gBAAgB,GAAG,SAAS,EACpC,OAAO,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC/B,OAAO,CAAC,aAAa,CAAC,CAmExB"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External tool servers an operator declares in their config.
|
|
3
|
+
*
|
|
4
|
+
* The kernel has spoken this protocol for a long time — `MCPClient`,
|
|
5
|
+
* `StdioTransport`, `StreamableHttpTransport` and the tool adapter are all
|
|
6
|
+
* exported from `@namzu/sdk`. `packages/cli` imported none of them, so the
|
|
7
|
+
* capability existed and was unreachable from the product: a namzu user could
|
|
8
|
+
* not connect an external tool server at all, whatever the kernel could do.
|
|
9
|
+
*
|
|
10
|
+
* A server is declared by name under `mcpServers` in `namzu.config.json`:
|
|
11
|
+
*
|
|
12
|
+
* "mcpServers": {
|
|
13
|
+
* "tickets": { "command": "node", "args": ["./tickets-server.js"] },
|
|
14
|
+
* "search": { "url": "https://tools.example.internal/mcp" }
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* Its tools arrive prefixed with the server's name (`mcp_tickets_create`), so
|
|
18
|
+
* two servers offering `search` do not collide and the transcript says where a
|
|
19
|
+
* call went.
|
|
20
|
+
*
|
|
21
|
+
* ## Every failure is named
|
|
22
|
+
*
|
|
23
|
+
* A server that does not start, a spec that names neither a command nor a URL,
|
|
24
|
+
* a handshake that never answers — each becomes an entry in `failed` with a
|
|
25
|
+
* reason, never an absence. The whole hazard of this feature is the operator
|
|
26
|
+
* who configures a server, watches the agent run without its tools, and
|
|
27
|
+
* concludes the model is bad at the task.
|
|
28
|
+
*
|
|
29
|
+
* What to DO about a failure is not decided here, because the answer differs
|
|
30
|
+
* by surface: a person watching the TUI can read the line and fix their config,
|
|
31
|
+
* and a headless run has nobody to read anything, so it refuses. This module
|
|
32
|
+
* reports; the callers decide.
|
|
33
|
+
*/
|
|
34
|
+
import { MCPClient, mcpToolToToolDefinition, } from '@namzu/sdk';
|
|
35
|
+
/**
|
|
36
|
+
* How long a single server gets to connect, hand shake and list its tools.
|
|
37
|
+
*
|
|
38
|
+
* The client's own `requestTimeoutMs` bounds one round trip; it cannot bound a
|
|
39
|
+
* process that spawns and never speaks. Without this, one wedged server holds
|
|
40
|
+
* the whole session open before the first turn — no error, no failure, just a
|
|
41
|
+
* namzu that does not start.
|
|
42
|
+
*/
|
|
43
|
+
export const CONNECT_TIMEOUT_MS = 10_000;
|
|
44
|
+
/**
|
|
45
|
+
* How long shutting one server down may take before it is given up on.
|
|
46
|
+
*
|
|
47
|
+
* Shorter than the connect bound because nothing waits on the answer: a
|
|
48
|
+
* one-shot is exiting and a TUI is replacing the session. What this prevents
|
|
49
|
+
* is the opposite of a leak — a `close()` that never resolves, holding the
|
|
50
|
+
* command open past the work it was asked to do.
|
|
51
|
+
*/
|
|
52
|
+
export const CLOSE_TIMEOUT_MS = 2_000;
|
|
53
|
+
/**
|
|
54
|
+
* Turn one spec into a transport, or say why it is not one.
|
|
55
|
+
*
|
|
56
|
+
* Refused rather than guessed. A spec with both a command and a URL is an
|
|
57
|
+
* operator who edited one into a file that already had the other, and picking
|
|
58
|
+
* either would run something they did not mean to run.
|
|
59
|
+
*/
|
|
60
|
+
export function transportFor(spec, defaultCwd) {
|
|
61
|
+
const hasCommand = typeof spec.command === 'string' && spec.command.trim().length > 0;
|
|
62
|
+
const hasUrl = typeof spec.url === 'string' && spec.url.trim().length > 0;
|
|
63
|
+
if (hasCommand && hasUrl) {
|
|
64
|
+
return 'it declares both a command and a url — pick one';
|
|
65
|
+
}
|
|
66
|
+
if (hasCommand) {
|
|
67
|
+
return {
|
|
68
|
+
type: 'stdio',
|
|
69
|
+
command: spec.command,
|
|
70
|
+
...(spec.args ? { args: [...spec.args] } : {}),
|
|
71
|
+
...(spec.env ? { env: { ...spec.env } } : {}),
|
|
72
|
+
cwd: spec.cwd ?? defaultCwd,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (hasUrl) {
|
|
76
|
+
return {
|
|
77
|
+
type: 'streamable-http',
|
|
78
|
+
url: spec.url,
|
|
79
|
+
...(spec.headers ? { headers: { ...spec.headers } } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return 'it declares neither a command nor a url';
|
|
83
|
+
}
|
|
84
|
+
async function withDeadline(work, ms, what) {
|
|
85
|
+
let timer;
|
|
86
|
+
try {
|
|
87
|
+
return await Promise.race([
|
|
88
|
+
work,
|
|
89
|
+
new Promise((_, reject) => {
|
|
90
|
+
timer = setTimeout(() => reject(new Error(`${what} did not answer within ${ms}ms`)), ms);
|
|
91
|
+
}),
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
if (timer)
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const reasonOf = (err) => (err instanceof Error ? err.message : String(err));
|
|
100
|
+
export async function connectMcpServers(config, options) {
|
|
101
|
+
const entries = Object.entries(config ?? {});
|
|
102
|
+
const tools = [];
|
|
103
|
+
const connected = [];
|
|
104
|
+
const failed = [];
|
|
105
|
+
const clients = [];
|
|
106
|
+
// Sequential, not parallel. Each server may spawn a process and each is
|
|
107
|
+
// bounded separately; a parallel fan-out would make the worst case the sum
|
|
108
|
+
// of nothing and the failure output arrive interleaved, for a saving that
|
|
109
|
+
// matters only to someone running many servers, who has other problems.
|
|
110
|
+
for (const [name, spec] of entries) {
|
|
111
|
+
const transport = transportFor(spec, options.cwd);
|
|
112
|
+
if (typeof transport === 'string') {
|
|
113
|
+
failed.push({ name, reason: transport });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const client = new MCPClient({ serverName: name, transport });
|
|
117
|
+
try {
|
|
118
|
+
await withDeadline(client.connect(), CONNECT_TIMEOUT_MS, `server "${name}"`);
|
|
119
|
+
const listed = await withDeadline(client.listTools(), CONNECT_TIMEOUT_MS, `server "${name}" listing its tools`);
|
|
120
|
+
for (const tool of listed) {
|
|
121
|
+
tools.push(mcpToolToToolDefinition(tool, client, name));
|
|
122
|
+
}
|
|
123
|
+
connected.push({ name, toolCount: listed.length });
|
|
124
|
+
clients.push(client);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
failed.push({ name, reason: reasonOf(err) });
|
|
128
|
+
// A half-connected client still owns a child process. Tearing it down
|
|
129
|
+
// here is the difference between a failed server and a leaked one.
|
|
130
|
+
//
|
|
131
|
+
// Bounded, and deliberately not dependent on what a transport does
|
|
132
|
+
// when it is closed having never connected — the shipped transports
|
|
133
|
+
// disagree about that (one returns early, one notifies), the
|
|
134
|
+
// divergence is a known one, and a shutdown path that only works
|
|
135
|
+
// under one of the two answers is a shutdown path waiting to hang.
|
|
136
|
+
// Nothing here reads the client's state afterwards either.
|
|
137
|
+
try {
|
|
138
|
+
await withDeadline(client.disconnect(), CLOSE_TIMEOUT_MS, `closing "${name}"`);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Already gone, never started, or refusing to answer. The failure is
|
|
142
|
+
// already recorded and there is nothing further to do about it.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
tools,
|
|
148
|
+
connected,
|
|
149
|
+
failed,
|
|
150
|
+
close: async () => {
|
|
151
|
+
await Promise.all(clients.map((c) => withDeadline(c.disconnect(), CLOSE_TIMEOUT_MS, 'closing a tool server').catch(() => {
|
|
152
|
+
// Shutting down is best effort by definition: the process may
|
|
153
|
+
// already be gone, and nothing useful follows from saying so.
|
|
154
|
+
// Bounded so a server that will not close cannot hold the
|
|
155
|
+
// command open past the work it was asked to do.
|
|
156
|
+
})));
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=servers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"servers.js","sourceRoot":"","sources":["../../../src/integrations/mcp/servers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A closed workspace takes no new conversation from the CLI either.
|
|
3
|
+
*
|
|
4
|
+
* The kernel gained a workspace-closed gate and it was applied to the SDK's own
|
|
5
|
+
* ingress paths. `startConversation` calls `createSession` on the store
|
|
6
|
+
* DIRECTLY, and a store deliberately holds no view of workspace status, so the
|
|
7
|
+
* invariant did not reach here.
|
|
8
|
+
*
|
|
9
|
+
* Whether that mattered turned on one question a grep cannot answer: does the
|
|
10
|
+
* CLI ever reach a project it did not just create? It does. `openSessions`
|
|
11
|
+
* reads the project id back out of `.namzu/cli.json` and creates a new project
|
|
12
|
+
* only when the pointer is missing or stale — so every run after the first
|
|
13
|
+
* attaches to a project that already existed and may since have been closed.
|
|
14
|
+
*
|
|
15
|
+
* That is why this test archives a project created by an EARLIER `openSessions`
|
|
16
|
+
* and then reopens the same directory. A test that archived a project it made
|
|
17
|
+
* itself, in one call, would pass against the first-run case the gate can never
|
|
18
|
+
* fire on.
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=archived-workspace.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archived-workspace.test.d.ts","sourceRoot":"","sources":["../../../../src/integrations/sessions/__tests__/archived-workspace.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG"}
|