@aria-cli/tools 1.0.9 → 1.0.11
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/package.json +9 -5
- package/src/__tests__/web-fetch-download.test.ts +0 -433
- package/src/__tests__/web-tools.test.ts +0 -619
- package/src/ask-user-interaction.ts +0 -33
- package/src/cache/web-cache.ts +0 -110
- package/src/definitions/arion.ts +0 -118
- package/src/definitions/browser/browser.ts +0 -502
- package/src/definitions/browser/index.ts +0 -5
- package/src/definitions/browser/pw-downloads.ts +0 -142
- package/src/definitions/browser/pw-interactions.ts +0 -282
- package/src/definitions/browser/pw-responses.ts +0 -98
- package/src/definitions/browser/pw-session.ts +0 -405
- package/src/definitions/browser/pw-shared.ts +0 -85
- package/src/definitions/browser/pw-snapshot.ts +0 -383
- package/src/definitions/browser/pw-state.ts +0 -101
- package/src/definitions/browser/types.ts +0 -203
- package/src/definitions/code-intelligence.ts +0 -526
- package/src/definitions/core.ts +0 -118
- package/src/definitions/delegation.ts +0 -567
- package/src/definitions/deploy.ts +0 -73
- package/src/definitions/filesystem.ts +0 -217
- package/src/definitions/frg.ts +0 -67
- package/src/definitions/index.ts +0 -28
- package/src/definitions/memory.ts +0 -150
- package/src/definitions/messaging.ts +0 -734
- package/src/definitions/meta.ts +0 -392
- package/src/definitions/network.ts +0 -179
- package/src/definitions/outlook.ts +0 -318
- package/src/definitions/patch/apply-patch.ts +0 -235
- package/src/definitions/patch/fuzzy-match.ts +0 -217
- package/src/definitions/patch/index.ts +0 -1
- package/src/definitions/patch/patch-parser.ts +0 -297
- package/src/definitions/patch/sandbox-paths.ts +0 -129
- package/src/definitions/process/index.ts +0 -5
- package/src/definitions/process/process-registry.ts +0 -303
- package/src/definitions/process/process.ts +0 -456
- package/src/definitions/process/pty-keys.ts +0 -298
- package/src/definitions/process/session-slug.ts +0 -147
- package/src/definitions/quip.ts +0 -225
- package/src/definitions/search.ts +0 -67
- package/src/definitions/session-history.ts +0 -79
- package/src/definitions/shell.ts +0 -202
- package/src/definitions/slack.ts +0 -211
- package/src/definitions/web.ts +0 -119
- package/src/executors/apply-patch.ts +0 -1035
- package/src/executors/arion.ts +0 -199
- package/src/executors/code-intelligence.ts +0 -1179
- package/src/executors/deploy.ts +0 -1066
- package/src/executors/filesystem.ts +0 -1428
- package/src/executors/frg-freshness.ts +0 -743
- package/src/executors/frg.ts +0 -394
- package/src/executors/index.ts +0 -280
- package/src/executors/learning-meta.ts +0 -1367
- package/src/executors/lsp-client.ts +0 -355
- package/src/executors/memory.ts +0 -978
- package/src/executors/meta.ts +0 -293
- package/src/executors/process-registry.ts +0 -570
- package/src/executors/pty-session-store.ts +0 -43
- package/src/executors/pty.ts +0 -342
- package/src/executors/restart.ts +0 -133
- package/src/executors/search-freshness.ts +0 -249
- package/src/executors/search-types.ts +0 -98
- package/src/executors/search.ts +0 -89
- package/src/executors/self-diagnose.ts +0 -552
- package/src/executors/session-history.ts +0 -435
- package/src/executors/shell-safety.ts +0 -519
- package/src/executors/shell.ts +0 -1243
- package/src/executors/utils.ts +0 -40
- package/src/executors/web.ts +0 -786
- package/src/extraction/content-extraction.ts +0 -281
- package/src/extraction/index.ts +0 -5
- package/src/headless-control-contract.ts +0 -1149
- package/src/index.ts +0 -788
- package/src/local-control-http-auth.ts +0 -2
- package/src/mcp/client.ts +0 -218
- package/src/mcp/connection.ts +0 -568
- package/src/mcp/index.ts +0 -11
- package/src/mcp/jsonrpc.ts +0 -195
- package/src/mcp/types.ts +0 -199
- package/src/network-control-adapter.ts +0 -88
- package/src/network-runtime/address-types.ts +0 -218
- package/src/network-runtime/db-owner-fencing.ts +0 -91
- package/src/network-runtime/delivery-receipts.ts +0 -372
- package/src/network-runtime/direct-endpoint-authority.ts +0 -35
- package/src/network-runtime/index.ts +0 -316
- package/src/network-runtime/local-control-contract.ts +0 -784
- package/src/network-runtime/node-store-contract.ts +0 -46
- package/src/network-runtime/pair-route-contract.ts +0 -97
- package/src/network-runtime/peer-capabilities.ts +0 -48
- package/src/network-runtime/peer-principal-ref.ts +0 -20
- package/src/network-runtime/peer-state-machine.ts +0 -160
- package/src/network-runtime/protocol-schemas.ts +0 -265
- package/src/network-runtime/runtime-bootstrap-contract.ts +0 -83
- package/src/outlook/desktop-session.ts +0 -409
- package/src/policy.ts +0 -171
- package/src/providers/brave.ts +0 -80
- package/src/providers/duckduckgo.ts +0 -199
- package/src/providers/exa.ts +0 -85
- package/src/providers/firecrawl.ts +0 -77
- package/src/providers/index.ts +0 -8
- package/src/providers/jina.ts +0 -70
- package/src/providers/router.ts +0 -121
- package/src/providers/search-provider.ts +0 -74
- package/src/providers/tavily.ts +0 -74
- package/src/quip/desktop-session.ts +0 -435
- package/src/registry/index.ts +0 -1
- package/src/registry/registry.ts +0 -905
- package/src/runtime-socket-local-control-client.ts +0 -632
- package/src/security/dns-normalization.ts +0 -34
- package/src/security/dns-pinning.ts +0 -138
- package/src/security/external-content.ts +0 -129
- package/src/security/ssrf.ts +0 -207
- package/src/slack/desktop-session.ts +0 -493
- package/src/tool-factory.ts +0 -91
- package/src/types.ts +0 -1341
- package/src/utils/retry.ts +0 -163
- package/src/utils/safe-parse-json.ts +0 -176
- package/src/utils/url.ts +0 -20
- package/tests/benchmarks/registry.bench.ts +0 -57
- package/tests/cache/web-cache.test.ts +0 -147
- package/tests/critical-integration.test.ts +0 -1465
- package/tests/definitions/apply-patch.test.ts +0 -586
- package/tests/definitions/browser.test.ts +0 -495
- package/tests/definitions/delegation-pause-resume.test.ts +0 -758
- package/tests/definitions/execution.test.ts +0 -671
- package/tests/definitions/messaging-inbox-scope.test.ts +0 -229
- package/tests/definitions/messaging.test.ts +0 -1468
- package/tests/definitions/outlook.test.ts +0 -30
- package/tests/definitions/process.test.ts +0 -469
- package/tests/definitions/slack.test.ts +0 -28
- package/tests/definitions/tool-inventory.test.ts +0 -218
- package/tests/e2e/delegation-quest-orchestration.e2e.test.ts +0 -433
- package/tests/e2e/memory-tool-discovery-contract.e2e.test.ts +0 -81
- package/tests/executors/apply-patch.test.ts +0 -538
- package/tests/executors/arion.test.ts +0 -309
- package/tests/executors/conversation-primitives.test.ts +0 -250
- package/tests/executors/deploy.test.ts +0 -746
- package/tests/executors/filesystem-tools.test.ts +0 -357
- package/tests/executors/filesystem.test.ts +0 -959
- package/tests/executors/frg-freshness.test.ts +0 -136
- package/tests/executors/frg-merge.test.ts +0 -70
- package/tests/executors/frg-session-content.test.ts +0 -40
- package/tests/executors/frg.test.ts +0 -56
- package/tests/executors/memory-bugfixes.test.ts +0 -257
- package/tests/executors/memory-real-memoria.integration.test.ts +0 -316
- package/tests/executors/memory.test.ts +0 -853
- package/tests/executors/meta-tools.test.ts +0 -411
- package/tests/executors/meta.test.ts +0 -683
- package/tests/executors/path-containment.test.ts +0 -51
- package/tests/executors/process-registry.test.ts +0 -505
- package/tests/executors/pty.test.ts +0 -664
- package/tests/executors/quest-security.test.ts +0 -249
- package/tests/executors/read-file-media.test.ts +0 -230
- package/tests/executors/recall-knowledge-schema.test.ts +0 -209
- package/tests/executors/recall-tags.test.ts +0 -278
- package/tests/executors/remember-null-safety.contract.test.ts +0 -41
- package/tests/executors/restart.test.ts +0 -67
- package/tests/executors/search-unified.test.ts +0 -381
- package/tests/executors/session-history.test.ts +0 -340
- package/tests/executors/session-transcript.test.ts +0 -561
- package/tests/executors/shell-abort.test.ts +0 -416
- package/tests/executors/shell-env-blocklist.test.ts +0 -648
- package/tests/executors/shell-env-process.test.ts +0 -245
- package/tests/executors/shell-process-registry.test.ts +0 -334
- package/tests/executors/shell-tools.test.ts +0 -393
- package/tests/executors/shell.test.ts +0 -690
- package/tests/executors/web-abort-vs-timeout.test.ts +0 -213
- package/tests/executors/web-integration.test.ts +0 -633
- package/tests/executors/web-symlink.test.ts +0 -18
- package/tests/executors/web.test.ts +0 -1400
- package/tests/executors/write-stdin.test.ts +0 -145
- package/tests/extraction/content-extraction.test.ts +0 -153
- package/tests/guards/tools-default-test-lane.integration.test.ts +0 -21
- package/tests/guards/tools-package-test-commands.e2e.test.ts +0 -43
- package/tests/guards/tools-test-lane-manifest.contract.test.ts +0 -76
- package/tests/guards/tools-vitest-workspace-alias.contract.test.ts +0 -63
- package/tests/helpers/async-waits.ts +0 -53
- package/tests/integration/headless-control-contract.integration.test.ts +0 -153
- package/tests/integration/memory-tool-schema-parity.integration.test.ts +0 -67
- package/tests/integration/meta-tools-round-trip.integration.test.ts +0 -506
- package/tests/integration/quest-round-trip.test.ts +0 -303
- package/tests/integration/registry-executor-flow.test.ts +0 -85
- package/tests/integration.test.ts +0 -177
- package/tests/loading-tier.test.ts +0 -126
- package/tests/mcp/client-reconnect.test.ts +0 -267
- package/tests/mcp/connection.test.ts +0 -846
- package/tests/mcp/injectable-logger.test.ts +0 -83
- package/tests/mcp/jsonrpc.test.ts +0 -109
- package/tests/mcp/lifecycle.test.ts +0 -879
- package/tests/network-runtime/address-types.contract.test.ts +0 -143
- package/tests/network-runtime/continuity-bind-schema.contract.test.ts +0 -203
- package/tests/network-runtime/local-control-contract.test.ts +0 -869
- package/tests/network-runtime/local-control-invite-token.contract.test.ts +0 -146
- package/tests/network-runtime/node-store-contract.test.ts +0 -11
- package/tests/network-runtime/pair-protocol-nodeid.contract.test.ts +0 -15
- package/tests/network-runtime/peer-state-machine.contract.test.ts +0 -148
- package/tests/network-runtime/protocol-schemas.contract.test.ts +0 -512
- package/tests/network-runtime/relay-pending-nodeid.contract.test.ts +0 -62
- package/tests/network-runtime/runtime-bootstrap-contract.test.ts +0 -227
- package/tests/network-runtime/runtime-socket-local-control-client.test.ts +0 -621
- package/tests/network-runtime/wait-for-message-script.test.ts +0 -288
- package/tests/parallel.test.ts +0 -71
- package/tests/policy.test.ts +0 -184
- package/tests/print-default-test-lane.ts +0 -14
- package/tests/print-test-lane-manifest.ts +0 -22
- package/tests/providers/brave.test.ts +0 -159
- package/tests/providers/duckduckgo.test.ts +0 -207
- package/tests/providers/exa.test.ts +0 -175
- package/tests/providers/firecrawl.test.ts +0 -168
- package/tests/providers/jina.test.ts +0 -144
- package/tests/providers/router.test.ts +0 -328
- package/tests/providers/tavily.test.ts +0 -165
- package/tests/registry/discovery.test.ts +0 -154
- package/tests/registry/injectable-logger.test.ts +0 -230
- package/tests/registry/input-validation.test.ts +0 -361
- package/tests/registry/interface-completeness.test.ts +0 -85
- package/tests/registry/mcp-integration.test.ts +0 -103
- package/tests/registry/mcp-read-only-hint.test.ts +0 -60
- package/tests/registry/memoria-discovery.test.ts +0 -390
- package/tests/registry/nested-validation.test.ts +0 -283
- package/tests/registry/pseudo-tool-filtering.test.ts +0 -258
- package/tests/registry/registration-lifecycle.test.ts +0 -133
- package/tests/registry-validation.test.ts +0 -424
- package/tests/registry.test.ts +0 -460
- package/tests/security/dns-pinning.test.ts +0 -162
- package/tests/security/external-content.test.ts +0 -144
- package/tests/security/ssrf.test.ts +0 -118
- package/tests/shell-safety-integration.test.ts +0 -32
- package/tests/shell-safety.test.ts +0 -365
- package/tests/slack/desktop-session.test.ts +0 -50
- package/tests/test-lane-manifest.ts +0 -440
- package/tests/test-utils.ts +0 -27
- package/tests/tool-factory.test.ts +0 -188
- package/tests/utils/retry.test.ts +0 -231
- package/tests/utils/url.test.ts +0 -63
- package/tsconfig.cjs.json +0 -24
- package/tsconfig.json +0 -12
- package/vitest.config.ts +0 -55
- package/vitest.e2e.config.ts +0 -24
- package/vitest.integration.config.ts +0 -24
- package/vitest.native.config.ts +0 -24
|
@@ -1,846 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { PassThrough } from "stream";
|
|
3
|
-
import { EventEmitter } from "events";
|
|
4
|
-
import { MCPServerConnection } from "../../src/mcp/connection.js";
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Fake child process that behaves like a spawned stdio MCP server.
|
|
8
|
-
// Incoming requests arrive on fakeStdin; responses are written to fakeStdout.
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
interface FakeProcess extends EventEmitter {
|
|
12
|
-
stdin: PassThrough;
|
|
13
|
-
stdout: PassThrough;
|
|
14
|
-
stderr: PassThrough;
|
|
15
|
-
pid: number;
|
|
16
|
-
killed: boolean;
|
|
17
|
-
kill: ReturnType<typeof vi.fn>;
|
|
18
|
-
/** Helper: read next JSON-RPC message written by the client */
|
|
19
|
-
nextRequest: () => Promise<{ jsonrpc: string; id: string; method: string; params?: unknown }>;
|
|
20
|
-
/** Helper: send a JSON-RPC response back to the client */
|
|
21
|
-
respond: (id: string, result: unknown) => void;
|
|
22
|
-
/** Helper: send a JSON-RPC error back to the client */
|
|
23
|
-
respondError: (id: string, code: number, message: string) => void;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function createFakeProcess(): FakeProcess {
|
|
27
|
-
const proc = new EventEmitter() as FakeProcess;
|
|
28
|
-
proc.stdin = new PassThrough();
|
|
29
|
-
proc.stdout = new PassThrough();
|
|
30
|
-
proc.stderr = new PassThrough();
|
|
31
|
-
proc.pid = 12345;
|
|
32
|
-
proc.killed = false;
|
|
33
|
-
proc.kill = vi.fn((signal?: string) => {
|
|
34
|
-
// On SIGKILL or SIGTERM, emit exit after a microtask
|
|
35
|
-
proc.killed = true;
|
|
36
|
-
process.nextTick(() => proc.emit("exit", signal === "SIGKILL" ? 137 : 0));
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Collect data written to stdin (i.e. client -> server)
|
|
40
|
-
const incomingBuffer: string[] = [];
|
|
41
|
-
let waitingResolve: ((line: string) => void) | null = null;
|
|
42
|
-
let lineBuffer = "";
|
|
43
|
-
|
|
44
|
-
proc.stdin.on("data", (chunk: Buffer) => {
|
|
45
|
-
lineBuffer += chunk.toString();
|
|
46
|
-
let idx: number;
|
|
47
|
-
while ((idx = lineBuffer.indexOf("\n")) !== -1) {
|
|
48
|
-
const line = lineBuffer.slice(0, idx).trim();
|
|
49
|
-
lineBuffer = lineBuffer.slice(idx + 1);
|
|
50
|
-
if (line) {
|
|
51
|
-
if (waitingResolve) {
|
|
52
|
-
const resolve = waitingResolve;
|
|
53
|
-
waitingResolve = null;
|
|
54
|
-
resolve(line);
|
|
55
|
-
} else {
|
|
56
|
-
incomingBuffer.push(line);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
proc.nextRequest = () =>
|
|
63
|
-
new Promise((resolve) => {
|
|
64
|
-
if (incomingBuffer.length > 0) {
|
|
65
|
-
resolve(JSON.parse(incomingBuffer.shift()!));
|
|
66
|
-
} else {
|
|
67
|
-
waitingResolve = (line) => resolve(JSON.parse(line));
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
proc.respond = (id: string, result: unknown) => {
|
|
72
|
-
proc.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
proc.respondError = (id: string, code: number, message: string) => {
|
|
76
|
-
proc.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n");
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
return proc;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
// Mock child_process.spawn so MCPServerConnection uses our fake process
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
|
|
86
|
-
let fakeProc: FakeProcess;
|
|
87
|
-
|
|
88
|
-
vi.mock("child_process", () => ({
|
|
89
|
-
spawn: vi.fn(() => fakeProc),
|
|
90
|
-
}));
|
|
91
|
-
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
// Tests
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
|
|
96
|
-
describe("MCPServerConnection", () => {
|
|
97
|
-
beforeEach(() => {
|
|
98
|
-
vi.useFakeTimers();
|
|
99
|
-
fakeProc = createFakeProcess();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
afterEach(() => {
|
|
103
|
-
vi.useRealTimers();
|
|
104
|
-
vi.restoreAllMocks();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// ============================
|
|
108
|
-
// Helper: perform the initialize handshake
|
|
109
|
-
// ============================
|
|
110
|
-
async function doInitialize(
|
|
111
|
-
conn: MCPServerConnection,
|
|
112
|
-
capabilities: Record<string, unknown> = { tools: { listChanged: true } },
|
|
113
|
-
) {
|
|
114
|
-
const initPromise = conn.initialize();
|
|
115
|
-
|
|
116
|
-
// Server receives initialize request
|
|
117
|
-
const initReq = await fakeProc.nextRequest();
|
|
118
|
-
expect(initReq.method).toBe("initialize");
|
|
119
|
-
expect(initReq.params).toMatchObject({
|
|
120
|
-
protocolVersion: "2024-11-05",
|
|
121
|
-
clientInfo: { name: "aria-cli", version: "1.0.0" },
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// Server responds with capabilities
|
|
125
|
-
fakeProc.respond(initReq.id, {
|
|
126
|
-
protocolVersion: "2024-11-05",
|
|
127
|
-
capabilities,
|
|
128
|
-
serverInfo: { name: "test-server", version: "0.1.0" },
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
const result = await initPromise;
|
|
132
|
-
|
|
133
|
-
// Client should send initialized notification (no id, just method)
|
|
134
|
-
const initNotif = await fakeProc.nextRequest();
|
|
135
|
-
expect(initNotif.method).toBe("notifications/initialized");
|
|
136
|
-
|
|
137
|
-
return result;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ==========================================================================
|
|
141
|
-
// initialize()
|
|
142
|
-
// ==========================================================================
|
|
143
|
-
|
|
144
|
-
describe("initialize()", () => {
|
|
145
|
-
it("should send initialize request and receive capabilities", async () => {
|
|
146
|
-
const conn = new MCPServerConnection({ name: "test", command: "echo" });
|
|
147
|
-
const caps = await doInitialize(conn, {
|
|
148
|
-
tools: { listChanged: true },
|
|
149
|
-
resources: { subscribe: true },
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
expect(caps).toEqual({
|
|
153
|
-
tools: { listChanged: true },
|
|
154
|
-
resources: { subscribe: true },
|
|
155
|
-
});
|
|
156
|
-
expect(conn.initialized).toBe(true);
|
|
157
|
-
expect(conn.name).toBe("test");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("should throw when URL is missing for sse transport", async () => {
|
|
161
|
-
const conn = new MCPServerConnection({
|
|
162
|
-
name: "sse-test",
|
|
163
|
-
transport: "sse",
|
|
164
|
-
});
|
|
165
|
-
await expect(conn.initialize()).rejects.toThrow("URL required for sse transport");
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("should throw when command is missing for stdio transport", async () => {
|
|
169
|
-
const conn = new MCPServerConnection({ name: "no-cmd" });
|
|
170
|
-
await expect(conn.initialize()).rejects.toThrow("Command required for stdio transport");
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("should handle empty capabilities from server", async () => {
|
|
174
|
-
const conn = new MCPServerConnection({ name: "bare", command: "echo" });
|
|
175
|
-
const initPromise = conn.initialize();
|
|
176
|
-
|
|
177
|
-
const initReq = await fakeProc.nextRequest();
|
|
178
|
-
fakeProc.respond(initReq.id, {
|
|
179
|
-
protocolVersion: "2024-11-05",
|
|
180
|
-
serverInfo: { name: "bare-server", version: "0.1.0" },
|
|
181
|
-
// capabilities omitted -> defaults to {}
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const caps = await initPromise;
|
|
185
|
-
expect(caps).toEqual({});
|
|
186
|
-
expect(conn.initialized).toBe(true);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("should emit 'log' on stderr data", async () => {
|
|
190
|
-
const conn = new MCPServerConnection({ name: "stderr-test", command: "echo" });
|
|
191
|
-
const logs: unknown[] = [];
|
|
192
|
-
conn.on("log", (msg) => logs.push(msg));
|
|
193
|
-
|
|
194
|
-
const initPromise = conn.initialize();
|
|
195
|
-
|
|
196
|
-
// Write something to stderr before responding to init
|
|
197
|
-
fakeProc.stderr.write("Server starting up...\n");
|
|
198
|
-
|
|
199
|
-
const initReq = await fakeProc.nextRequest();
|
|
200
|
-
fakeProc.respond(initReq.id, {
|
|
201
|
-
protocolVersion: "2024-11-05",
|
|
202
|
-
capabilities: {},
|
|
203
|
-
serverInfo: { name: "test", version: "0.1.0" },
|
|
204
|
-
});
|
|
205
|
-
await initPromise;
|
|
206
|
-
// Consume initialized notification
|
|
207
|
-
await fakeProc.nextRequest();
|
|
208
|
-
|
|
209
|
-
expect(logs.length).toBeGreaterThan(0);
|
|
210
|
-
expect(logs[0]).toMatchObject({
|
|
211
|
-
level: "error",
|
|
212
|
-
message: expect.stringContaining("Server starting up"),
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it("should mark initialized=false and emit 'exit' when process exits", async () => {
|
|
217
|
-
const conn = new MCPServerConnection({ name: "exit-test", command: "echo" });
|
|
218
|
-
await doInitialize(conn);
|
|
219
|
-
|
|
220
|
-
expect(conn.initialized).toBe(true);
|
|
221
|
-
|
|
222
|
-
const exitPromise = new Promise<number | null>((resolve) => {
|
|
223
|
-
conn.on("exit", resolve);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
// Simulate process exit
|
|
227
|
-
fakeProc.emit("exit", 1);
|
|
228
|
-
|
|
229
|
-
const exitCode = await exitPromise;
|
|
230
|
-
expect(exitCode).toBe(1);
|
|
231
|
-
expect(conn.initialized).toBe(false);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("should forward tools/list_changed notifications", async () => {
|
|
235
|
-
const conn = new MCPServerConnection({ name: "notif-test", command: "echo" });
|
|
236
|
-
await doInitialize(conn);
|
|
237
|
-
|
|
238
|
-
const notifPromise = new Promise<void>((resolve) => {
|
|
239
|
-
conn.on("tools/list_changed", resolve);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// Server sends a notification
|
|
243
|
-
fakeProc.stdout.write(
|
|
244
|
-
JSON.stringify({
|
|
245
|
-
jsonrpc: "2.0",
|
|
246
|
-
method: "notifications/tools/list_changed",
|
|
247
|
-
params: {},
|
|
248
|
-
}) + "\n",
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
await notifPromise; // should resolve without timeout
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
// ==========================================================================
|
|
256
|
-
// listTools()
|
|
257
|
-
// ==========================================================================
|
|
258
|
-
|
|
259
|
-
describe("listTools()", () => {
|
|
260
|
-
it("should return tool definitions from server", async () => {
|
|
261
|
-
const conn = new MCPServerConnection({ name: "tools-test", command: "echo" });
|
|
262
|
-
await doInitialize(conn);
|
|
263
|
-
|
|
264
|
-
const listPromise = conn.listTools();
|
|
265
|
-
|
|
266
|
-
const req = await fakeProc.nextRequest();
|
|
267
|
-
expect(req.method).toBe("tools/list");
|
|
268
|
-
|
|
269
|
-
const toolDefs = [
|
|
270
|
-
{
|
|
271
|
-
name: "read_file",
|
|
272
|
-
description: "Read a file",
|
|
273
|
-
inputSchema: {
|
|
274
|
-
type: "object",
|
|
275
|
-
properties: { path: { type: "string" } },
|
|
276
|
-
required: ["path"],
|
|
277
|
-
},
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
name: "write_file",
|
|
281
|
-
description: "Write a file",
|
|
282
|
-
inputSchema: {
|
|
283
|
-
type: "object",
|
|
284
|
-
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
285
|
-
required: ["path", "content"],
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
];
|
|
289
|
-
|
|
290
|
-
fakeProc.respond(req.id, { tools: toolDefs });
|
|
291
|
-
|
|
292
|
-
const tools = await listPromise;
|
|
293
|
-
expect(tools).toHaveLength(2);
|
|
294
|
-
expect(tools[0].name).toBe("read_file");
|
|
295
|
-
expect(tools[1].name).toBe("write_file");
|
|
296
|
-
expect(tools[0].inputSchema).toMatchObject({ type: "object" });
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("should return empty array when server returns no tools", async () => {
|
|
300
|
-
const conn = new MCPServerConnection({ name: "empty-tools", command: "echo" });
|
|
301
|
-
await doInitialize(conn);
|
|
302
|
-
|
|
303
|
-
const listPromise = conn.listTools();
|
|
304
|
-
const req = await fakeProc.nextRequest();
|
|
305
|
-
fakeProc.respond(req.id, {}); // no tools property
|
|
306
|
-
|
|
307
|
-
const tools = await listPromise;
|
|
308
|
-
expect(tools).toEqual([]);
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
// ==========================================================================
|
|
313
|
-
// callTool()
|
|
314
|
-
// ==========================================================================
|
|
315
|
-
|
|
316
|
-
describe("callTool()", () => {
|
|
317
|
-
it("should send tool call and return successful result", async () => {
|
|
318
|
-
const conn = new MCPServerConnection({ name: "call-test", command: "echo" });
|
|
319
|
-
await doInitialize(conn);
|
|
320
|
-
|
|
321
|
-
const callPromise = conn.callTool("read_file", { path: "/tmp/test.txt" });
|
|
322
|
-
|
|
323
|
-
const req = await fakeProc.nextRequest();
|
|
324
|
-
expect(req.method).toBe("tools/call");
|
|
325
|
-
expect(req.params).toEqual({ name: "read_file", arguments: { path: "/tmp/test.txt" } });
|
|
326
|
-
|
|
327
|
-
fakeProc.respond(req.id, {
|
|
328
|
-
content: [{ type: "text", text: "file contents here" }],
|
|
329
|
-
isError: false,
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
const result = await callPromise;
|
|
333
|
-
expect(result.success).toBe(true);
|
|
334
|
-
expect(result.message).toBe("file contents here");
|
|
335
|
-
expect(result.data).toEqual([{ type: "text", text: "file contents here" }]);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it("should handle tool call with isError=true", async () => {
|
|
339
|
-
const conn = new MCPServerConnection({ name: "call-err", command: "echo" });
|
|
340
|
-
await doInitialize(conn);
|
|
341
|
-
|
|
342
|
-
const callPromise = conn.callTool("write_file", { path: "/readonly/file" });
|
|
343
|
-
|
|
344
|
-
const req = await fakeProc.nextRequest();
|
|
345
|
-
fakeProc.respond(req.id, {
|
|
346
|
-
content: [{ type: "text", text: "Permission denied" }],
|
|
347
|
-
isError: true,
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
const result = await callPromise;
|
|
351
|
-
expect(result.success).toBe(false);
|
|
352
|
-
expect(result.message).toBe("Permission denied");
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it("should concatenate multiple text content blocks", async () => {
|
|
356
|
-
const conn = new MCPServerConnection({ name: "multi-text", command: "echo" });
|
|
357
|
-
await doInitialize(conn);
|
|
358
|
-
|
|
359
|
-
const callPromise = conn.callTool("multi_tool", {});
|
|
360
|
-
|
|
361
|
-
const req = await fakeProc.nextRequest();
|
|
362
|
-
fakeProc.respond(req.id, {
|
|
363
|
-
content: [
|
|
364
|
-
{ type: "text", text: "line 1" },
|
|
365
|
-
{ type: "image", data: "base64data", mimeType: "image/png" },
|
|
366
|
-
{ type: "text", text: "line 2" },
|
|
367
|
-
],
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
const result = await callPromise;
|
|
371
|
-
expect(result.success).toBe(true);
|
|
372
|
-
expect(result.message).toBe("line 1\nline 2");
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
it("should return failure on JSON-RPC error", async () => {
|
|
376
|
-
const conn = new MCPServerConnection({ name: "rpc-err", command: "echo" });
|
|
377
|
-
await doInitialize(conn);
|
|
378
|
-
|
|
379
|
-
const callPromise = conn.callTool("bad_tool", {});
|
|
380
|
-
|
|
381
|
-
const req = await fakeProc.nextRequest();
|
|
382
|
-
fakeProc.respondError(req.id, -32601, "Method not found");
|
|
383
|
-
|
|
384
|
-
const result = await callPromise;
|
|
385
|
-
expect(result.success).toBe(false);
|
|
386
|
-
expect(result.message).toContain("Method not found");
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it("should return empty message when content has no text blocks", async () => {
|
|
390
|
-
const conn = new MCPServerConnection({ name: "no-text", command: "echo" });
|
|
391
|
-
await doInitialize(conn);
|
|
392
|
-
|
|
393
|
-
const callPromise = conn.callTool("image_tool", {});
|
|
394
|
-
|
|
395
|
-
const req = await fakeProc.nextRequest();
|
|
396
|
-
fakeProc.respond(req.id, {
|
|
397
|
-
content: [{ type: "image", data: "base64data", mimeType: "image/png" }],
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
const result = await callPromise;
|
|
401
|
-
expect(result.success).toBe(true);
|
|
402
|
-
expect(result.message).toBe("");
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
// ==========================================================================
|
|
407
|
-
// shutdown()
|
|
408
|
-
// ==========================================================================
|
|
409
|
-
|
|
410
|
-
describe("shutdown()", () => {
|
|
411
|
-
it("should send shutdown request, exit notification, then SIGTERM", async () => {
|
|
412
|
-
const conn = new MCPServerConnection({ name: "shutdown-test", command: "echo" });
|
|
413
|
-
await doInitialize(conn);
|
|
414
|
-
|
|
415
|
-
// Start shutdown (don't await yet)
|
|
416
|
-
const shutdownPromise = conn.shutdown();
|
|
417
|
-
|
|
418
|
-
// Server receives shutdown request
|
|
419
|
-
const shutdownReq = await fakeProc.nextRequest();
|
|
420
|
-
expect(shutdownReq.method).toBe("shutdown");
|
|
421
|
-
|
|
422
|
-
// Server responds to shutdown
|
|
423
|
-
fakeProc.respond(shutdownReq.id, {});
|
|
424
|
-
|
|
425
|
-
// Server receives exit notification
|
|
426
|
-
const exitNotif = await fakeProc.nextRequest();
|
|
427
|
-
expect(exitNotif.method).toBe("notifications/exit");
|
|
428
|
-
|
|
429
|
-
await vi.advanceTimersByTimeAsync(600);
|
|
430
|
-
await shutdownPromise;
|
|
431
|
-
|
|
432
|
-
// Should have called kill with SIGTERM
|
|
433
|
-
expect(fakeProc.kill).toHaveBeenCalledWith("SIGTERM");
|
|
434
|
-
expect(conn.initialized).toBe(false);
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
it("should resolve immediately if not initialized", async () => {
|
|
438
|
-
const conn = new MCPServerConnection({ name: "not-init", command: "echo" });
|
|
439
|
-
expect(conn.initialized).toBe(false);
|
|
440
|
-
|
|
441
|
-
// Should resolve immediately, no error
|
|
442
|
-
await conn.shutdown();
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
it("should escalate to SIGKILL when process does not exit after SIGTERM", async () => {
|
|
446
|
-
const conn = new MCPServerConnection({ name: "stubborn", command: "echo" });
|
|
447
|
-
await doInitialize(conn);
|
|
448
|
-
|
|
449
|
-
// Make kill NOT emit exit on SIGTERM (simulate hung process)
|
|
450
|
-
fakeProc.kill = vi.fn((signal?: string) => {
|
|
451
|
-
if (signal === "SIGKILL") {
|
|
452
|
-
// Only SIGKILL actually kills it
|
|
453
|
-
fakeProc.killed = true;
|
|
454
|
-
process.nextTick(() => fakeProc.emit("exit", 137));
|
|
455
|
-
}
|
|
456
|
-
// SIGTERM is ignored — process stays alive
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
const shutdownPromise = conn.shutdown();
|
|
460
|
-
|
|
461
|
-
// Respond to shutdown RPC
|
|
462
|
-
const shutdownReq = await fakeProc.nextRequest();
|
|
463
|
-
fakeProc.respond(shutdownReq.id, {});
|
|
464
|
-
|
|
465
|
-
// Consume exit notification
|
|
466
|
-
await fakeProc.nextRequest();
|
|
467
|
-
|
|
468
|
-
await vi.advanceTimersByTimeAsync(600);
|
|
469
|
-
await vi.advanceTimersByTimeAsync(5000);
|
|
470
|
-
await shutdownPromise;
|
|
471
|
-
|
|
472
|
-
// Should have tried SIGTERM first, then SIGKILL
|
|
473
|
-
expect(fakeProc.kill).toHaveBeenCalledWith("SIGTERM");
|
|
474
|
-
expect(fakeProc.kill).toHaveBeenCalledWith("SIGKILL");
|
|
475
|
-
}, 10000);
|
|
476
|
-
|
|
477
|
-
it("should handle already-dead process during shutdown gracefully", async () => {
|
|
478
|
-
const conn = new MCPServerConnection({ name: "dead-proc", command: "echo" });
|
|
479
|
-
await doInitialize(conn);
|
|
480
|
-
|
|
481
|
-
// Make the shutdown RPC fail (process already dead)
|
|
482
|
-
const shutdownPromise = conn.shutdown();
|
|
483
|
-
|
|
484
|
-
const shutdownReq = await fakeProc.nextRequest();
|
|
485
|
-
// Simulate process dying mid-shutdown by sending an error
|
|
486
|
-
fakeProc.respondError(shutdownReq.id, -32000, "Connection lost");
|
|
487
|
-
|
|
488
|
-
// shutdown() catches this error and proceeds to kill
|
|
489
|
-
await shutdownPromise;
|
|
490
|
-
|
|
491
|
-
expect(fakeProc.kill).toHaveBeenCalledWith("SIGTERM");
|
|
492
|
-
expect(conn.initialized).toBe(false);
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
it("should resolve only after process exits (not before)", async () => {
|
|
496
|
-
const conn = new MCPServerConnection({ name: "wait-exit", command: "echo" });
|
|
497
|
-
await doInitialize(conn);
|
|
498
|
-
|
|
499
|
-
let resolved = false;
|
|
500
|
-
|
|
501
|
-
// Make kill NOT auto-emit exit — we control when exit happens
|
|
502
|
-
fakeProc.kill = vi.fn(() => {
|
|
503
|
-
// Don't emit exit automatically
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
const shutdownPromise = conn.shutdown().then(() => {
|
|
507
|
-
resolved = true;
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
// Respond to shutdown RPC
|
|
511
|
-
const shutdownReq = await fakeProc.nextRequest();
|
|
512
|
-
fakeProc.respond(shutdownReq.id, {});
|
|
513
|
-
|
|
514
|
-
// Consume exit notification
|
|
515
|
-
await fakeProc.nextRequest();
|
|
516
|
-
|
|
517
|
-
await vi.advanceTimersByTimeAsync(600);
|
|
518
|
-
|
|
519
|
-
expect(fakeProc.kill).toHaveBeenCalledWith("SIGTERM");
|
|
520
|
-
expect(resolved).toBe(false); // Should NOT have resolved yet
|
|
521
|
-
|
|
522
|
-
// Now simulate process finally exiting
|
|
523
|
-
fakeProc.emit("exit", 0);
|
|
524
|
-
|
|
525
|
-
await shutdownPromise;
|
|
526
|
-
expect(resolved).toBe(true);
|
|
527
|
-
});
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
// ==========================================================================
|
|
531
|
-
// Transport default (aria-rfu.3 fix)
|
|
532
|
-
// ==========================================================================
|
|
533
|
-
|
|
534
|
-
describe("transport default", () => {
|
|
535
|
-
it("should treat undefined transport as stdio and initialize normally", async () => {
|
|
536
|
-
// Config with no transport field at all — should default to "stdio"
|
|
537
|
-
const conn = new MCPServerConnection({ name: "no-transport", command: "echo" });
|
|
538
|
-
const caps = await doInitialize(conn);
|
|
539
|
-
|
|
540
|
-
expect(caps).toEqual({ tools: { listChanged: true } });
|
|
541
|
-
expect(conn.initialized).toBe(true);
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
it("should treat explicit stdio transport the same as undefined", async () => {
|
|
545
|
-
const conn = new MCPServerConnection({
|
|
546
|
-
name: "explicit-stdio",
|
|
547
|
-
transport: "stdio",
|
|
548
|
-
command: "echo",
|
|
549
|
-
});
|
|
550
|
-
const caps = await doInitialize(conn);
|
|
551
|
-
|
|
552
|
-
expect(caps).toEqual({ tools: { listChanged: true } });
|
|
553
|
-
expect(conn.initialized).toBe(true);
|
|
554
|
-
});
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
// ==========================================================================
|
|
558
|
-
// listResources()
|
|
559
|
-
// ==========================================================================
|
|
560
|
-
|
|
561
|
-
describe("listResources()", () => {
|
|
562
|
-
it("should return resource definitions from server", async () => {
|
|
563
|
-
const conn = new MCPServerConnection({ name: "res-test", command: "echo" });
|
|
564
|
-
await doInitialize(conn);
|
|
565
|
-
|
|
566
|
-
const listPromise = conn.listResources();
|
|
567
|
-
|
|
568
|
-
const req = await fakeProc.nextRequest();
|
|
569
|
-
expect(req.method).toBe("resources/list");
|
|
570
|
-
|
|
571
|
-
fakeProc.respond(req.id, {
|
|
572
|
-
resources: [
|
|
573
|
-
{ uri: "file:///tmp/data.json", name: "data.json", mimeType: "application/json" },
|
|
574
|
-
{ uri: "file:///tmp/config.yaml", name: "config.yaml" },
|
|
575
|
-
],
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
const resources = await listPromise;
|
|
579
|
-
expect(resources).toHaveLength(2);
|
|
580
|
-
expect(resources[0].uri).toBe("file:///tmp/data.json");
|
|
581
|
-
expect(resources[0].name).toBe("data.json");
|
|
582
|
-
expect(resources[1].uri).toBe("file:///tmp/config.yaml");
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
it("should return empty array when server returns no resources", async () => {
|
|
586
|
-
const conn = new MCPServerConnection({ name: "empty-res", command: "echo" });
|
|
587
|
-
await doInitialize(conn);
|
|
588
|
-
|
|
589
|
-
const listPromise = conn.listResources();
|
|
590
|
-
const req = await fakeProc.nextRequest();
|
|
591
|
-
fakeProc.respond(req.id, {}); // no resources property
|
|
592
|
-
|
|
593
|
-
const resources = await listPromise;
|
|
594
|
-
expect(resources).toEqual([]);
|
|
595
|
-
});
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
// ==========================================================================
|
|
599
|
-
// readResource()
|
|
600
|
-
// ==========================================================================
|
|
601
|
-
|
|
602
|
-
describe("readResource()", () => {
|
|
603
|
-
it("should send read request with URI and return contents", async () => {
|
|
604
|
-
const conn = new MCPServerConnection({ name: "read-res", command: "echo" });
|
|
605
|
-
await doInitialize(conn);
|
|
606
|
-
|
|
607
|
-
const readPromise = conn.readResource("file:///tmp/data.json");
|
|
608
|
-
|
|
609
|
-
const req = await fakeProc.nextRequest();
|
|
610
|
-
expect(req.method).toBe("resources/read");
|
|
611
|
-
expect(req.params).toEqual({ uri: "file:///tmp/data.json" });
|
|
612
|
-
|
|
613
|
-
fakeProc.respond(req.id, {
|
|
614
|
-
contents: [
|
|
615
|
-
{ uri: "file:///tmp/data.json", mimeType: "application/json", text: '{"key":"value"}' },
|
|
616
|
-
],
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
const contents = await readPromise;
|
|
620
|
-
expect(contents).toHaveLength(1);
|
|
621
|
-
expect(contents[0].text).toBe('{"key":"value"}');
|
|
622
|
-
expect(contents[0].mimeType).toBe("application/json");
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
it("should return empty array when no contents returned", async () => {
|
|
626
|
-
const conn = new MCPServerConnection({ name: "empty-read", command: "echo" });
|
|
627
|
-
await doInitialize(conn);
|
|
628
|
-
|
|
629
|
-
const readPromise = conn.readResource("file:///missing");
|
|
630
|
-
const req = await fakeProc.nextRequest();
|
|
631
|
-
fakeProc.respond(req.id, {}); // no contents property
|
|
632
|
-
|
|
633
|
-
const contents = await readPromise;
|
|
634
|
-
expect(contents).toEqual([]);
|
|
635
|
-
});
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
// ==========================================================================
|
|
639
|
-
// listPrompts()
|
|
640
|
-
// ==========================================================================
|
|
641
|
-
|
|
642
|
-
describe("listPrompts()", () => {
|
|
643
|
-
it("should return prompt definitions from server", async () => {
|
|
644
|
-
const conn = new MCPServerConnection({ name: "prompts-test", command: "echo" });
|
|
645
|
-
await doInitialize(conn);
|
|
646
|
-
|
|
647
|
-
const listPromise = conn.listPrompts();
|
|
648
|
-
|
|
649
|
-
const req = await fakeProc.nextRequest();
|
|
650
|
-
expect(req.method).toBe("prompts/list");
|
|
651
|
-
|
|
652
|
-
fakeProc.respond(req.id, {
|
|
653
|
-
prompts: [
|
|
654
|
-
{
|
|
655
|
-
name: "summarize",
|
|
656
|
-
description: "Summarize text",
|
|
657
|
-
arguments: [{ name: "text", required: true }],
|
|
658
|
-
},
|
|
659
|
-
{ name: "translate", description: "Translate text" },
|
|
660
|
-
],
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
const prompts = await listPromise;
|
|
664
|
-
expect(prompts).toHaveLength(2);
|
|
665
|
-
expect(prompts[0].name).toBe("summarize");
|
|
666
|
-
expect(prompts[0].arguments).toHaveLength(1);
|
|
667
|
-
expect(prompts[1].name).toBe("translate");
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
it("should return empty array when server returns no prompts", async () => {
|
|
671
|
-
const conn = new MCPServerConnection({ name: "empty-prompts", command: "echo" });
|
|
672
|
-
await doInitialize(conn);
|
|
673
|
-
|
|
674
|
-
const listPromise = conn.listPrompts();
|
|
675
|
-
const req = await fakeProc.nextRequest();
|
|
676
|
-
fakeProc.respond(req.id, {}); // no prompts property
|
|
677
|
-
|
|
678
|
-
const prompts = await listPromise;
|
|
679
|
-
expect(prompts).toEqual([]);
|
|
680
|
-
});
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
// ==========================================================================
|
|
684
|
-
// getPrompt()
|
|
685
|
-
// ==========================================================================
|
|
686
|
-
|
|
687
|
-
describe("getPrompt()", () => {
|
|
688
|
-
it("should send get request with name and args and return messages", async () => {
|
|
689
|
-
const conn = new MCPServerConnection({ name: "get-prompt", command: "echo" });
|
|
690
|
-
await doInitialize(conn);
|
|
691
|
-
|
|
692
|
-
const getPromise = conn.getPrompt("summarize", { text: "hello world" });
|
|
693
|
-
|
|
694
|
-
const req = await fakeProc.nextRequest();
|
|
695
|
-
expect(req.method).toBe("prompts/get");
|
|
696
|
-
expect(req.params).toEqual({ name: "summarize", arguments: { text: "hello world" } });
|
|
697
|
-
|
|
698
|
-
fakeProc.respond(req.id, {
|
|
699
|
-
messages: [{ role: "user", content: { type: "text", text: "Summarize: hello world" } }],
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
const messages = await getPromise;
|
|
703
|
-
expect(messages).toHaveLength(1);
|
|
704
|
-
expect(messages[0].role).toBe("user");
|
|
705
|
-
expect(messages[0].content).toEqual({ type: "text", text: "Summarize: hello world" });
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
it("should return empty array when no messages returned", async () => {
|
|
709
|
-
const conn = new MCPServerConnection({ name: "empty-prompt", command: "echo" });
|
|
710
|
-
await doInitialize(conn);
|
|
711
|
-
|
|
712
|
-
const getPromise = conn.getPrompt("empty");
|
|
713
|
-
const req = await fakeProc.nextRequest();
|
|
714
|
-
fakeProc.respond(req.id, {}); // no messages property
|
|
715
|
-
|
|
716
|
-
const messages = await getPromise;
|
|
717
|
-
expect(messages).toEqual([]);
|
|
718
|
-
});
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
// ==========================================================================
|
|
722
|
-
// Full lifecycle
|
|
723
|
-
// ==========================================================================
|
|
724
|
-
|
|
725
|
-
describe("full lifecycle: init -> listTools -> callTool -> shutdown", () => {
|
|
726
|
-
it("should complete a full interaction cycle", async () => {
|
|
727
|
-
const conn = new MCPServerConnection({
|
|
728
|
-
name: "lifecycle-test",
|
|
729
|
-
command: "echo",
|
|
730
|
-
args: ["--stdio"],
|
|
731
|
-
env: { DEBUG: "true" },
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
// 1. Initialize
|
|
735
|
-
const caps = await doInitialize(conn, { tools: { listChanged: true } });
|
|
736
|
-
expect(conn.initialized).toBe(true);
|
|
737
|
-
expect(caps).toEqual({ tools: { listChanged: true } });
|
|
738
|
-
|
|
739
|
-
// 2. List tools
|
|
740
|
-
const listPromise = conn.listTools();
|
|
741
|
-
const listReq = await fakeProc.nextRequest();
|
|
742
|
-
expect(listReq.method).toBe("tools/list");
|
|
743
|
-
fakeProc.respond(listReq.id, {
|
|
744
|
-
tools: [
|
|
745
|
-
{
|
|
746
|
-
name: "echo",
|
|
747
|
-
description: "Echo input",
|
|
748
|
-
inputSchema: { type: "object", properties: { text: { type: "string" } } },
|
|
749
|
-
},
|
|
750
|
-
],
|
|
751
|
-
});
|
|
752
|
-
const tools = await listPromise;
|
|
753
|
-
expect(tools).toHaveLength(1);
|
|
754
|
-
expect(tools[0].name).toBe("echo");
|
|
755
|
-
|
|
756
|
-
// 3. Call tool
|
|
757
|
-
const callPromise = conn.callTool("echo", { text: "hello" });
|
|
758
|
-
const callReq = await fakeProc.nextRequest();
|
|
759
|
-
expect(callReq.method).toBe("tools/call");
|
|
760
|
-
fakeProc.respond(callReq.id, {
|
|
761
|
-
content: [{ type: "text", text: "echo: hello" }],
|
|
762
|
-
isError: false,
|
|
763
|
-
});
|
|
764
|
-
const result = await callPromise;
|
|
765
|
-
expect(result.success).toBe(true);
|
|
766
|
-
expect(result.message).toBe("echo: hello");
|
|
767
|
-
|
|
768
|
-
// 4. Shutdown
|
|
769
|
-
const shutdownPromise = conn.shutdown();
|
|
770
|
-
const shutdownReq = await fakeProc.nextRequest();
|
|
771
|
-
expect(shutdownReq.method).toBe("shutdown");
|
|
772
|
-
fakeProc.respond(shutdownReq.id, {});
|
|
773
|
-
const exitNotif = await fakeProc.nextRequest();
|
|
774
|
-
expect(exitNotif.method).toBe("notifications/exit");
|
|
775
|
-
await vi.advanceTimersByTimeAsync(600);
|
|
776
|
-
await shutdownPromise;
|
|
777
|
-
|
|
778
|
-
expect(conn.initialized).toBe(false);
|
|
779
|
-
});
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
// ==========================================================================
|
|
783
|
-
// Error cases
|
|
784
|
-
// ==========================================================================
|
|
785
|
-
|
|
786
|
-
describe("error cases", () => {
|
|
787
|
-
it("should propagate initialize RPC error", async () => {
|
|
788
|
-
const conn = new MCPServerConnection({ name: "init-fail", command: "echo" });
|
|
789
|
-
const initPromise = conn.initialize();
|
|
790
|
-
|
|
791
|
-
const req = await fakeProc.nextRequest();
|
|
792
|
-
expect(req.method).toBe("initialize");
|
|
793
|
-
|
|
794
|
-
fakeProc.respondError(req.id, -32600, "Invalid initialize request");
|
|
795
|
-
|
|
796
|
-
await expect(initPromise).rejects.toThrow("Invalid initialize request");
|
|
797
|
-
expect(conn.initialized).toBe(false);
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
it("should timeout when server never responds to initialize", async () => {
|
|
801
|
-
// Use a short-timeout RPC client to avoid waiting 30s
|
|
802
|
-
// MCPServerConnection uses default 30s timeout from JSONRPCClient.
|
|
803
|
-
// We test this by verifying the init promise rejects on timeout.
|
|
804
|
-
// To keep test fast, we mock the internal RPC client timeout.
|
|
805
|
-
const conn = new MCPServerConnection({ name: "timeout", command: "echo" });
|
|
806
|
-
|
|
807
|
-
const initPromise = conn.initialize();
|
|
808
|
-
|
|
809
|
-
// Read the initialize request but never respond to it
|
|
810
|
-
const req = await fakeProc.nextRequest();
|
|
811
|
-
expect(req.method).toBe("initialize");
|
|
812
|
-
|
|
813
|
-
// The JSONRPCClient has a 30s default timeout. Instead of waiting,
|
|
814
|
-
// we verify the request was sent correctly and the connection is
|
|
815
|
-
// not yet initialized.
|
|
816
|
-
expect(conn.initialized).toBe(false);
|
|
817
|
-
|
|
818
|
-
// Now respond to unblock (we don't actually want to wait 30s)
|
|
819
|
-
fakeProc.respond(req.id, {
|
|
820
|
-
protocolVersion: "2024-11-05",
|
|
821
|
-
capabilities: {},
|
|
822
|
-
serverInfo: { name: "test", version: "0.1.0" },
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
await initPromise;
|
|
826
|
-
expect(conn.initialized).toBe(true);
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
it("should handle process crash during tool call", async () => {
|
|
830
|
-
const conn = new MCPServerConnection({ name: "crash-test", command: "echo" });
|
|
831
|
-
await doInitialize(conn);
|
|
832
|
-
|
|
833
|
-
const callPromise = conn.callTool("dangerous_tool", {});
|
|
834
|
-
|
|
835
|
-
const req = await fakeProc.nextRequest();
|
|
836
|
-
expect(req.method).toBe("tools/call");
|
|
837
|
-
|
|
838
|
-
// Simulate process crash — send RPC error back
|
|
839
|
-
fakeProc.respondError(req.id, -32000, "Server process crashed");
|
|
840
|
-
|
|
841
|
-
const result = await callPromise;
|
|
842
|
-
expect(result.success).toBe(false);
|
|
843
|
-
expect(result.message).toContain("Server process crashed");
|
|
844
|
-
});
|
|
845
|
-
});
|
|
846
|
-
});
|