@cursor/july 0.1.23 → 0.1.24

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.
Files changed (174) hide show
  1. package/dist/bin/agent-serve.js +0 -0
  2. package/dist/docs/404.html +1 -1
  3. package/dist/docs/ab.html +2 -2
  4. package/dist/docs/assets/{app.DYcC9FY-.js → app.CvSGaAxk.js} +1 -1
  5. package/dist/docs/assets/chunks/@localSearchIndexroot.CL2Y0Zmh.js +1 -0
  6. package/dist/docs/assets/chunks/{VPLocalSearchBox.o4N_knTV.js → VPLocalSearchBox.J1jJbCvs.js} +1 -1
  7. package/dist/docs/assets/chunks/{theme.DQ-njyo0.js → theme.C7vfzr7h.js} +2 -2
  8. package/dist/docs/building-with-agents.html +2 -2
  9. package/dist/docs/concepts.html +2 -2
  10. package/dist/docs/deployment.html +2 -2
  11. package/dist/docs/evals.html +2 -2
  12. package/dist/docs/example-agents/approval-buddy.html +2 -2
  13. package/dist/docs/example-agents/benny.html +2 -2
  14. package/dist/docs/example-agents/bugbot.html +2 -2
  15. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  16. package/dist/docs/example-agents/codeowners-review.html +2 -2
  17. package/dist/docs/example-agents/concierge.html +2 -2
  18. package/dist/docs/example-agents/fsd.html +2 -2
  19. package/dist/docs/example-agents/index.html +2 -2
  20. package/dist/docs/example-agents/knowledge-base.html +2 -2
  21. package/dist/docs/example-agents/oncall.html +2 -2
  22. package/dist/docs/example-agents/security-reviewer.html +2 -2
  23. package/dist/docs/example-agents/slack-agent.html +2 -2
  24. package/dist/docs/example-agents/weather-agent.html +2 -2
  25. package/dist/docs/guides/agent-to-agent.html +2 -2
  26. package/dist/docs/guides/cloud-runtime.html +2 -2
  27. package/dist/docs/guides/github.html +2 -2
  28. package/dist/docs/guides/human-in-the-loop.html +2 -2
  29. package/dist/docs/guides/mcp-oauth.html +2 -2
  30. package/dist/docs/guides/slack.html +2 -2
  31. package/dist/docs/guides/webhooks.html +2 -2
  32. package/dist/docs/hillclimbing.html +2 -2
  33. package/dist/docs/index.html +2 -2
  34. package/dist/docs/quickstart.html +2 -2
  35. package/dist/docs/reference/agent-config.html +2 -2
  36. package/dist/docs/reference/channels.html +2 -2
  37. package/dist/docs/reference/cli.html +2 -2
  38. package/dist/docs/reference/connections.html +2 -2
  39. package/dist/docs/reference/hooks.html +2 -2
  40. package/dist/docs/reference/http-api.html +2 -2
  41. package/dist/docs/reference/instructions.html +2 -2
  42. package/dist/docs/reference/playground.html +2 -2
  43. package/dist/docs/reference/project-layout.html +2 -2
  44. package/dist/docs/reference/prompt.html +2 -2
  45. package/dist/docs/reference/schedules.html +2 -2
  46. package/dist/docs/reference/sessions.html +2 -2
  47. package/dist/docs/reference/skills.html +2 -2
  48. package/dist/docs/reference/subagents.html +2 -2
  49. package/dist/docs/reference/tools.html +2 -2
  50. package/dist/docs/scaffolding-agents.html +2 -2
  51. package/dist/docs/storage.html +2 -2
  52. package/dist/docs/troubleshooting.html +2 -2
  53. package/dist/playground/assets/index-CidizGZv.css +1 -0
  54. package/dist/playground/assets/{index-DqXdAFGa.js → index-DTG9OsPV.js} +41 -41
  55. package/dist/playground/index.html +2 -2
  56. package/package.json +24 -24
  57. package/dist/channels/github/instrument.d.ts +0 -20
  58. package/dist/channels/github/instrument.d.ts.map +0 -1
  59. package/dist/docs/assets/chunks/@localSearchIndexroot.BQTzJjR_.js +0 -1
  60. package/dist/internal/json-dir-store.d.ts +0 -32
  61. package/dist/internal/json-dir-store.d.ts.map +0 -1
  62. package/dist/internal/json-dir-store.js +0 -100
  63. package/dist/playground/assets/index-CjOQ4hN9.css +0 -1
  64. package/src/bin/agent-serve.version.test.ts +0 -64
  65. package/src/channels/github/api.test.ts +0 -64
  66. package/src/channels/github/auth.test.ts +0 -105
  67. package/src/channels/github/cursor-account.test.ts +0 -204
  68. package/src/channels/github/forward.test.ts +0 -457
  69. package/src/channels/github/github.test.ts +0 -937
  70. package/src/channels/github/replay.test.ts +0 -179
  71. package/src/channels/slack/api.post-message.test.ts +0 -148
  72. package/src/channels/slack/approvals.test.ts +0 -328
  73. package/src/channels/slack/block-actions.test.ts +0 -452
  74. package/src/channels/slack/bot-mentions.test.ts +0 -267
  75. package/src/channels/slack/channel-watch.test.ts +0 -363
  76. package/src/channels/slack/cursor-account.test.ts +0 -253
  77. package/src/channels/slack/defaults.final-post.test.ts +0 -182
  78. package/src/channels/slack/dispatch.test.ts +0 -795
  79. package/src/channels/slack/eval-directive.test.ts +0 -273
  80. package/src/channels/slack/message-body.test.ts +0 -54
  81. package/src/channels/slack/nudge-store.test.ts +0 -143
  82. package/src/channels/slack/slack.test.ts +0 -391
  83. package/src/channels/slack/stop.test.ts +0 -23
  84. package/src/channels/slack/thread-context.test.ts +0 -202
  85. package/src/evals/assertions.test.ts +0 -580
  86. package/src/evals/expect.test.ts +0 -144
  87. package/src/evals/judge.test.ts +0 -181
  88. package/src/evals/loaders.test.ts +0 -132
  89. package/src/evals/matchers.test.ts +0 -95
  90. package/src/evals/reporters.test.ts +0 -303
  91. package/src/evals/run-facts.test.ts +0 -259
  92. package/src/internal/ab-snapshot.test.ts +0 -325
  93. package/src/internal/approval-gate.test.ts +0 -49
  94. package/src/internal/approvals.integration.test.ts +0 -383
  95. package/src/internal/authored-loaders.test.ts +0 -31
  96. package/src/internal/builtin-tools/reminders.test.ts +0 -201
  97. package/src/internal/channel-route-schema.test.ts +0 -294
  98. package/src/internal/chat-attach.test.ts +0 -262
  99. package/src/internal/cli-deploy.test.ts +0 -1991
  100. package/src/internal/cli-docs.test.ts +0 -161
  101. package/src/internal/cli-mcp.test.ts +0 -789
  102. package/src/internal/cli-skills.test.ts +0 -133
  103. package/src/internal/cli-slack.test.ts +0 -1647
  104. package/src/internal/cloud-merge.test.ts +0 -74
  105. package/src/internal/cron.test.ts +0 -22
  106. package/src/internal/cursor/account-mcp.test.ts +0 -807
  107. package/src/internal/cursor/backend-client.test.ts +0 -591
  108. package/src/internal/cursor/credentials.test.ts +0 -351
  109. package/src/internal/cursor/github-credentials.test.ts +0 -136
  110. package/src/internal/cursor-account-mcp-auth.test.ts +0 -310
  111. package/src/internal/cursor-account.integration.test.ts +0 -441
  112. package/src/internal/cursor-event-relay.test.ts +0 -746
  113. package/src/internal/cursor-github-credentials.integration.test.ts +0 -271
  114. package/src/internal/cursor-slack-relay.test.ts +0 -525
  115. package/src/internal/deploy-source.test.ts +0 -111
  116. package/src/internal/discovery.builtin-tools.test.ts +0 -94
  117. package/src/internal/discovery.concurrency.test.ts +0 -60
  118. package/src/internal/discovery.cursor-account.test.ts +0 -133
  119. package/src/internal/discovery.cwd.test.ts +0 -83
  120. package/src/internal/discovery.hosting.test.ts +0 -80
  121. package/src/internal/discovery.identity.test.ts +0 -44
  122. package/src/internal/docs-site.test.ts +0 -66
  123. package/src/internal/duration.test.ts +0 -29
  124. package/src/internal/eval-judge-model.test.ts +0 -187
  125. package/src/internal/eval-run-store.cancel.test.ts +0 -142
  126. package/src/internal/eval-run-store.storage.test.ts +0 -211
  127. package/src/internal/eval-runner.http.test.ts +0 -403
  128. package/src/internal/eval-runner.run.test.ts +0 -928
  129. package/src/internal/evals-client.test.ts +0 -307
  130. package/src/internal/event-mapper.test.ts +0 -243
  131. package/src/internal/github-fanout.test.ts +0 -213
  132. package/src/internal/handleAgentServeTrigger.test.ts +0 -179
  133. package/src/internal/host-kv.test.ts +0 -82
  134. package/src/internal/host-platforms.test.ts +0 -126
  135. package/src/internal/http-channel.test.ts +0 -402
  136. package/src/internal/init-project.test.ts +0 -270
  137. package/src/internal/install-cursor-skills.test.ts +0 -262
  138. package/src/internal/local-env.test.ts +0 -120
  139. package/src/internal/log-ring.test.ts +0 -31
  140. package/src/internal/logs-client.test.ts +0 -350
  141. package/src/internal/mcp-endpoint.test.ts +0 -436
  142. package/src/internal/mcp-host.test.ts +0 -298
  143. package/src/internal/mcp-oauth.test.ts +0 -148
  144. package/src/internal/net.test.ts +0 -17
  145. package/src/internal/peer-connections.test.ts +0 -128
  146. package/src/internal/peer-mcp.integration.test.ts +0 -289
  147. package/src/internal/playground/toolchain.test.ts +0 -53
  148. package/src/internal/playground-cli.test.ts +0 -187
  149. package/src/internal/playground-proxy.test.ts +0 -376
  150. package/src/internal/prompt-context.integration.test.ts +0 -232
  151. package/src/internal/prompt-context.test.ts +0 -127
  152. package/src/internal/reminder-runner.test.ts +0 -390
  153. package/src/internal/reminder-store.test.ts +0 -53
  154. package/src/internal/request-headers.test.ts +0 -27
  155. package/src/internal/resolve-prod-target.test.ts +0 -787
  156. package/src/internal/resolved-connections.test.ts +0 -295
  157. package/src/internal/router.test.ts +0 -57
  158. package/src/internal/sdk-runner.test.ts +0 -290
  159. package/src/internal/session-engine.coalesce.test.ts +0 -169
  160. package/src/internal/session-engine.concurrency.test.ts +0 -250
  161. package/src/internal/session-engine.host-oauth-mcp.test.ts +0 -110
  162. package/src/internal/session-engine.interrupt.test.ts +0 -577
  163. package/src/internal/session-engine.storage.test.ts +0 -547
  164. package/src/internal/session-urls.test.ts +0 -28
  165. package/src/internal/sessions-client.test.ts +0 -518
  166. package/src/internal/storage-coordinator.test.ts +0 -517
  167. package/src/internal/tool-call.test.ts +0 -458
  168. package/src/internal/tool-result.test.ts +0 -52
  169. package/src/internal/trajectory.approvals.test.ts +0 -83
  170. package/src/internal/trajectory.subagents.test.ts +0 -198
  171. package/src/internal/turn-governor.test.ts +0 -137
  172. package/src/internal/update-check.test.ts +0 -485
  173. package/src/internal/workspace.test.ts +0 -207
  174. package/src/storage-backends/cursor-hosted.test.ts +0 -121
@@ -1,789 +0,0 @@
1
- /**
2
- * `agentkit mcp` — the stdio ↔ streamable-HTTP proxy and the
3
- * `mcp install` config writer.
4
- *
5
- * Serve-mode tests drive the real wire path: raw newline-delimited JSON-RPC
6
- * into the proxy, which POSTs to a real `startServer` MCP endpoint (mock
7
- * runner) or a stub engine (prod credential refresh).
8
- */
9
-
10
- import { once } from "node:events";
11
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
12
- import { createServer, type Server } from "node:http";
13
- import type { AddressInfo } from "node:net";
14
- import { tmpdir } from "node:os";
15
- import { join } from "node:path";
16
- import { PassThrough } from "node:stream";
17
- import { afterEach, describe, expect, it, vi } from "vitest";
18
- import { httpChannel } from "../channels.js";
19
- import type {
20
- AgentProject,
21
- AgentServeHandle,
22
- RunnerTurnRequest,
23
- } from "../types.js";
24
- import type { resolveDeployAuth } from "./cli-deploy.js";
25
- import {
26
- cmdMcp,
27
- cmdMcpInstall,
28
- cmdMcpServe,
29
- cursorMcpJsonPath,
30
- joinMcpPath,
31
- } from "./cli-mcp.js";
32
- import { AgentServeDeployClient } from "./deploy-client.js";
33
- import type { resolveProdTarget } from "./resolve-prod-target.js";
34
- import type { AgentRunner } from "./sdk-runner.js";
35
- import { startServer } from "./server.js";
36
-
37
- type ResolveProdTargetImpl = typeof resolveProdTarget;
38
-
39
- function makeProject(name: string): AgentProject {
40
- return {
41
- rootDir: `/tmp/mcp-cli-fixture-${name}`,
42
- agentDir: `/tmp/mcp-cli-fixture-${name}/agent`,
43
- name,
44
- agent: {
45
- name,
46
- description: `The ${name} fixture agent.`,
47
- runtime: "local",
48
- tools: [],
49
- skills: [],
50
- connections: [],
51
- subagents: [],
52
- seedFiles: [],
53
- },
54
- httpChannel: httpChannel(),
55
- channels: [],
56
- schedules: [],
57
- hooks: [],
58
- abs: [],
59
- diagnostics: [],
60
- };
61
- }
62
-
63
- interface WireCapture {
64
- sink: { write(chunk: string): unknown };
65
- next: () => Promise<string>;
66
- drained: () => string[];
67
- }
68
-
69
- function createWireCapture(): WireCapture {
70
- const lines: string[] = [];
71
- const waiters: Array<(line: string) => void> = [];
72
- let buffer = "";
73
- return {
74
- sink: {
75
- write(chunk: string): boolean {
76
- buffer += chunk;
77
- let index = buffer.indexOf("\n");
78
- while (index !== -1) {
79
- const line = buffer.slice(0, index);
80
- buffer = buffer.slice(index + 1);
81
- index = buffer.indexOf("\n");
82
- const waiter = waiters.shift();
83
- if (waiter !== undefined) {
84
- waiter(line);
85
- } else {
86
- lines.push(line);
87
- }
88
- }
89
- return true;
90
- },
91
- },
92
- next: () => {
93
- const line = lines.shift();
94
- if (line !== undefined) {
95
- return Promise.resolve(line);
96
- }
97
- return new Promise<string>((resolve, reject) => {
98
- const timer = setTimeout(() => {
99
- reject(new Error("timed out waiting for a wire line"));
100
- }, 15_000);
101
- waiters.push((value) => {
102
- clearTimeout(timer);
103
- resolve(value);
104
- });
105
- });
106
- },
107
- drained: () => [...lines],
108
- };
109
- }
110
-
111
- interface ProxySession {
112
- send: (message: Record<string, unknown>) => void;
113
- next: () => Promise<Record<string, unknown>>;
114
- stdoutLines: () => string[];
115
- stderrText: () => string;
116
- finish: () => Promise<number>;
117
- }
118
-
119
- function startProxySession(
120
- options: Omit<Parameters<typeof cmdMcpServe>[0], "stdin" | "stdout" | "err">
121
- ): ProxySession {
122
- const stdin = new PassThrough();
123
- const capture = createWireCapture();
124
- const stderr: string[] = [];
125
- const exit = cmdMcpServe({
126
- ...options,
127
- stdin,
128
- stdout: capture.sink,
129
- err: (text) => {
130
- stderr.push(text);
131
- },
132
- });
133
- return {
134
- send: (message) => {
135
- stdin.write(`${JSON.stringify(message)}\n`);
136
- },
137
- next: async () =>
138
- JSON.parse(await capture.next()) as Record<string, unknown>,
139
- stdoutLines: () => capture.drained(),
140
- stderrText: () => stderr.join(""),
141
- finish: async () => {
142
- stdin.end();
143
- return exit;
144
- },
145
- };
146
- }
147
-
148
- const INITIALIZE = {
149
- jsonrpc: "2.0",
150
- id: 0,
151
- method: "initialize",
152
- params: {
153
- protocolVersion: "2025-06-18",
154
- capabilities: {},
155
- clientInfo: { name: "cli-mcp-test", version: "0.0.0" },
156
- },
157
- };
158
-
159
- describe("joinMcpPath", () => {
160
- it("appends /v1/mcp to an agent base URL, idempotently", () => {
161
- expect(joinMcpPath("http://127.0.0.1:3000/weather")).toBe(
162
- "http://127.0.0.1:3000/weather/v1/mcp"
163
- );
164
- expect(joinMcpPath("http://127.0.0.1:3000/weather/")).toBe(
165
- "http://127.0.0.1:3000/weather/v1/mcp"
166
- );
167
- expect(joinMcpPath("http://127.0.0.1:3000/weather/v1/mcp")).toBe(
168
- "http://127.0.0.1:3000/weather/v1/mcp"
169
- );
170
- expect(joinMcpPath("http://127.0.0.1:3000/weather?token=abc")).toBe(
171
- "http://127.0.0.1:3000/weather/v1/mcp?token=abc"
172
- );
173
- });
174
- });
175
-
176
- describe("cmdMcpServe stdin framing", () => {
177
- it("processes a final message when stdin ends without a trailing newline", async () => {
178
- const stdin = new PassThrough();
179
- const capture = createWireCapture();
180
- const fetchImpl = vi.fn(async () =>
181
- Response.json({ jsonrpc: "2.0", id: 3, result: { tools: [] } })
182
- );
183
- const exit = cmdMcpServe({
184
- dir: ".",
185
- url: "http://127.0.0.1:3000/helper",
186
- stdin,
187
- stdout: capture.sink,
188
- fetchImpl,
189
- sleep: async () => {},
190
- });
191
- stdin.write(
192
- JSON.stringify({ jsonrpc: "2.0", id: 3, method: "tools/list" })
193
- );
194
- stdin.end();
195
- expect(await exit).toBe(0);
196
- expect(fetchImpl).toHaveBeenCalledTimes(1);
197
- expect(fetchImpl).toHaveBeenCalledWith(
198
- "http://127.0.0.1:3000/helper/v1/mcp",
199
- expect.anything()
200
- );
201
- const response = JSON.parse(await capture.next()) as Record<
202
- string,
203
- unknown
204
- >;
205
- expect(response.id).toBe(3);
206
- });
207
- });
208
-
209
- describe("cmdMcpServe --url against a served agent", () => {
210
- let handle: AgentServeHandle | undefined;
211
- let stateRoot: string | undefined;
212
-
213
- afterEach(async () => {
214
- await handle?.close();
215
- handle = undefined;
216
- if (stateRoot !== undefined) {
217
- await rm(stateRoot, { recursive: true, force: true });
218
- stateRoot = undefined;
219
- }
220
- });
221
-
222
- it("proxies initialize, notifications, tools/list, and ask end-to-end", async () => {
223
- stateRoot = await mkdtemp(join(tmpdir(), "agentkit-mcp-cli-"));
224
- const runner: AgentRunner = {
225
- async runTurn(request: RunnerTurnRequest) {
226
- void request;
227
- return { status: "finished", result: "the answer is 4" };
228
- },
229
- };
230
- handle = await startServer(
231
- [{ slug: "helper", project: makeProject("helper") }],
232
- { port: 0, stateRoot, runner, playground: false, docs: false }
233
- );
234
-
235
- const session = startProxySession({
236
- dir: ".",
237
- url: `${handle.url}/helper`,
238
- });
239
-
240
- session.send(INITIALIZE);
241
- const initResponse = await session.next();
242
- expect(initResponse.id).toBe(0);
243
- expect(initResponse).toHaveProperty("result.serverInfo.name", "helper");
244
-
245
- // Notifications relay upstream (202) and produce no wire output.
246
- session.send({ jsonrpc: "2.0", method: "notifications/initialized" });
247
-
248
- session.send({ jsonrpc: "2.0", id: 1, method: "tools/list" });
249
- const toolsResponse = await session.next();
250
- expect(toolsResponse.id).toBe(1);
251
- const tools = (
252
- toolsResponse.result as { tools: Array<{ name: string }> }
253
- ).tools.map((tool) => tool.name);
254
- expect(tools).toContain("ask");
255
- expect(tools).toContain("check");
256
-
257
- session.send({
258
- jsonrpc: "2.0",
259
- id: 2,
260
- method: "tools/call",
261
- params: { name: "ask", arguments: { message: "What is 2+2?" } },
262
- });
263
- const askResponse = await session.next();
264
- expect(askResponse.id).toBe(2);
265
- const content = (askResponse.result as { content: Array<{ text: string }> })
266
- .content;
267
- const payload = JSON.parse(content[0]?.text ?? "{}") as {
268
- status: string;
269
- reply?: string;
270
- };
271
- expect(payload.status).toBe("completed");
272
- expect(payload.reply).toBe("the answer is 4");
273
-
274
- expect(await session.finish()).toBe(0);
275
- expect(session.stdoutLines()).toEqual([]);
276
- expect(session.stderrText()).toContain("proxying stdio <->");
277
- }, 30_000);
278
- });
279
-
280
- interface StubMcpEngine {
281
- url: string;
282
- close: () => Promise<void>;
283
- seenTokens: string[];
284
- respondFor: (token: string) => void;
285
- }
286
-
287
- /**
288
- * Engine stub that 401s unless the request carries the currently accepted
289
- * port token, echoing a canned JSON-RPC response otherwise.
290
- */
291
- async function startStubMcpEngine(): Promise<StubMcpEngine> {
292
- const seenTokens: string[] = [];
293
- let acceptedToken = "";
294
- const server: Server = createServer((req, res) => {
295
- let raw = "";
296
- req.on("data", (chunk: Buffer) => {
297
- raw += chunk.toString("utf8");
298
- });
299
- req.on("end", () => {
300
- const token = String(req.headers["x-anyrun-port-token"] ?? "");
301
- seenTokens.push(token);
302
- if (token !== acceptedToken) {
303
- res.writeHead(401, { "content-type": "application/json" });
304
- res.end(JSON.stringify({ error: "expired engine access" }));
305
- return;
306
- }
307
- const inbound = JSON.parse(raw) as { id?: unknown };
308
- res.writeHead(200, { "content-type": "application/json" });
309
- res.end(
310
- JSON.stringify({
311
- jsonrpc: "2.0",
312
- id: inbound.id ?? null,
313
- result: { ok: true, token },
314
- })
315
- );
316
- });
317
- });
318
- server.listen(0, "127.0.0.1");
319
- await once(server, "listening");
320
- const { port } = server.address() as AddressInfo;
321
- return {
322
- url: `http://127.0.0.1:${port}`,
323
- seenTokens,
324
- respondFor: (token) => {
325
- acceptedToken = token;
326
- },
327
- close: () =>
328
- new Promise<void>((resolvePromise, reject) =>
329
- server.close((error) => (error ? reject(error) : resolvePromise()))
330
- ),
331
- };
332
- }
333
-
334
- describe("cmdMcpServe --prod credential lifecycle", () => {
335
- let engine: StubMcpEngine | undefined;
336
-
337
- afterEach(async () => {
338
- await engine?.close();
339
- engine = undefined;
340
- });
341
-
342
- it("re-mints engineAccess on 401 and retries the message", async () => {
343
- engine = await startStubMcpEngine();
344
- engine.respondFor("token-2");
345
-
346
- let minted = 0;
347
- const resolveImpl = vi.fn(async () => {
348
- minted += 1;
349
- return {
350
- agentUrl: engine!.url,
351
- headers: { "x-anyrun-port-token": `token-${minted}` },
352
- slug: "weather",
353
- expiresAt: "2099-01-01T00:00:00.000Z",
354
- };
355
- }) satisfies ResolveProdTargetImpl;
356
-
357
- const session = startProxySession({
358
- dir: ".",
359
- prod: true,
360
- slug: "weather",
361
- resolveProdTargetImpl: resolveImpl,
362
- sleep: async () => {},
363
- });
364
-
365
- session.send({ jsonrpc: "2.0", id: 5, method: "tools/list" });
366
- const response = await session.next();
367
- expect(response.id).toBe(5);
368
- expect(response).toHaveProperty("result.token", "token-2");
369
- // token-1 was rejected (401), invalidated, token-2 accepted.
370
- expect(engine.seenTokens).toEqual(["token-1", "token-2"]);
371
- expect(resolveImpl).toHaveBeenCalledTimes(2);
372
-
373
- expect(await session.finish()).toBe(0);
374
- });
375
-
376
- it("proactively re-resolves near expiresAt", async () => {
377
- engine = await startStubMcpEngine();
378
- engine.respondFor("token-1");
379
-
380
- let nowMs = Date.parse("2026-07-21T00:00:00.000Z");
381
- const expiresAt = new Date(nowMs + 10 * 60 * 1000).toISOString();
382
- let minted = 0;
383
- const resolveImpl = vi.fn(async () => {
384
- minted += 1;
385
- if (minted === 2) {
386
- engine!.respondFor("token-2");
387
- }
388
- return {
389
- agentUrl: engine!.url,
390
- headers: { "x-anyrun-port-token": `token-${minted}` },
391
- slug: "weather",
392
- expiresAt,
393
- };
394
- }) satisfies ResolveProdTargetImpl;
395
-
396
- const session = startProxySession({
397
- dir: ".",
398
- prod: true,
399
- slug: "weather",
400
- resolveProdTargetImpl: resolveImpl,
401
- now: () => nowMs,
402
- sleep: async () => {},
403
- });
404
-
405
- session.send({ jsonrpc: "2.0", id: 1, method: "tools/list" });
406
- expect((await session.next()).id).toBe(1);
407
- expect(resolveImpl).toHaveBeenCalledTimes(1);
408
-
409
- // Within the 5-minute leeway of expiry: the next message re-resolves.
410
- nowMs += 6 * 60 * 1000;
411
- session.send({ jsonrpc: "2.0", id: 2, method: "tools/list" });
412
- const second = await session.next();
413
- expect(second.id).toBe(2);
414
- expect(second).toHaveProperty("result.token", "token-2");
415
- expect(resolveImpl).toHaveBeenCalledTimes(2);
416
-
417
- expect(await session.finish()).toBe(0);
418
- });
419
- });
420
-
421
- describe("cmdMcpServe failure shaping", () => {
422
- it("answers requests with a JSON-RPC error when the target is unreachable", async () => {
423
- const session = startProxySession({
424
- dir: ".",
425
- // Port 9 (discard) refuses connections on loopback.
426
- url: "http://127.0.0.1:9/helper",
427
- sleep: async () => {},
428
- });
429
-
430
- session.send({ jsonrpc: "2.0", id: 7, method: "tools/list" });
431
- const response = await session.next();
432
- expect(response.id).toBe(7);
433
- expect(response).toHaveProperty("error.code", -32000);
434
- expect(String((response.error as { message: string }).message)).toContain(
435
- "request failed"
436
- );
437
-
438
- // Notification failures log to stderr but never emit wire output.
439
- session.send({ jsonrpc: "2.0", method: "notifications/initialized" });
440
- expect(await session.finish()).toBe(0);
441
- expect(session.stdoutLines()).toEqual([]);
442
- expect(session.stderrText()).toContain("request failed");
443
- });
444
-
445
- it("rejects --prod with --url and requires a target", async () => {
446
- const errText: string[] = [];
447
- const both = await cmdMcpServe({
448
- dir: ".",
449
- prod: true,
450
- url: "http://127.0.0.1:3000",
451
- err: (text) => {
452
- errText.push(text);
453
- },
454
- });
455
- expect(both).toBe(1);
456
- expect(errText.join("")).toContain("mutually exclusive");
457
-
458
- const neither = await cmdMcpServe({
459
- dir: ".",
460
- err: (text) => {
461
- errText.push(text);
462
- },
463
- });
464
- expect(neither).toBe(1);
465
- expect(errText.join("")).toContain("requires --prod");
466
- });
467
-
468
- it("rejects unknown subcommands", async () => {
469
- const errText: string[] = [];
470
- const code = await cmdMcp("uninstall", {
471
- dir: ".",
472
- err: (text) => {
473
- errText.push(text);
474
- },
475
- });
476
- expect(code).toBe(1);
477
- expect(errText.join("")).toContain('Unknown mcp subcommand "uninstall"');
478
- });
479
- });
480
-
481
- describe("cmdMcpInstall", () => {
482
- let home: string | undefined;
483
- const savedHome = process.env.HOME;
484
-
485
- afterEach(async () => {
486
- if (savedHome === undefined) {
487
- delete process.env.HOME;
488
- } else {
489
- process.env.HOME = savedHome;
490
- }
491
- if (home !== undefined) {
492
- await rm(home, { recursive: true, force: true });
493
- home = undefined;
494
- }
495
- });
496
-
497
- async function withTempHome(): Promise<string> {
498
- home = await mkdtemp(join(tmpdir(), "agentkit-mcp-home-"));
499
- process.env.HOME = home;
500
- return home;
501
- }
502
-
503
- it("writes a --url entry named after the mount slug", async () => {
504
- const dir = await withTempHome();
505
- const out = createWireCapture();
506
- const code = await cmdMcpInstall({
507
- dir: ".",
508
- url: "http://127.0.0.1:3000/weather-agent/",
509
- stdout: out.sink,
510
- err: () => {},
511
- });
512
- expect(code).toBe(0);
513
-
514
- const raw = await readFile(join(dir, ".cursor", "mcp.json"), "utf8");
515
- expect(JSON.parse(raw)).toEqual({
516
- mcpServers: {
517
- "weather-agent": {
518
- command: "agentkit",
519
- args: ["mcp", "--url", "http://127.0.0.1:3000/weather-agent"],
520
- },
521
- },
522
- });
523
- });
524
-
525
- it("requires --name when the URL has no path segment", async () => {
526
- await withTempHome();
527
- const errText: string[] = [];
528
- const code = await cmdMcpInstall({
529
- dir: ".",
530
- url: "http://127.0.0.1:3000",
531
- err: (text) => {
532
- errText.push(text);
533
- },
534
- });
535
- expect(code).toBe(1);
536
- expect(errText.join("")).toContain("pass --name");
537
- });
538
-
539
- it("verifies the hosted deployment and pins slug + team in the args", async () => {
540
- const dir = await withTempHome();
541
- const resolveImpl = vi.fn(async () => ({
542
- agentUrl: "http://127.0.0.1:4000",
543
- headers: {},
544
- slug: "weather",
545
- expiresAt: "2099-01-01T00:00:00.000Z",
546
- })) satisfies ResolveProdTargetImpl;
547
-
548
- const code = await cmdMcpInstall({
549
- dir: ".",
550
- prod: true,
551
- slug: "weather",
552
- team: "42",
553
- resolveProdTargetImpl: resolveImpl,
554
- stdout: createWireCapture().sink,
555
- err: () => {},
556
- });
557
- expect(code).toBe(0);
558
- expect(resolveImpl).toHaveBeenCalledTimes(1);
559
-
560
- const raw = await readFile(join(dir, ".cursor", "mcp.json"), "utf8");
561
- expect(JSON.parse(raw)).toEqual({
562
- mcpServers: {
563
- weather: {
564
- command: "agentkit",
565
- args: ["mcp", "--prod", "--slug", "weather", "--team", "42"],
566
- },
567
- },
568
- });
569
- });
570
-
571
- it("--remote writes a stable gateway URL with the API-key bearer", async () => {
572
- const dir = await withTempHome();
573
- const client = new AgentServeDeployClient({
574
- backendUrl: "https://api.cursor.com",
575
- apiKey: "crsr_test_key",
576
- fetchImpl: (async () =>
577
- new Response(
578
- JSON.stringify({
579
- id: 1,
580
- slug: "jenny",
581
- teamId: 42,
582
- deploymentKind: "repo",
583
- status: "running",
584
- generation: 1,
585
- engineAlias: "/internal/agent-serve/agents/asd_x/engine",
586
- }),
587
- { status: 200, headers: { "content-type": "application/json" } }
588
- )) as unknown as typeof fetch,
589
- });
590
- const resolveDeployAuthImpl: typeof resolveDeployAuth = async () => ({
591
- client,
592
- teamId: 42,
593
- apiKey: "crsr_test_key",
594
- backendUrl: "https://api.cursor.com",
595
- });
596
-
597
- const code = await cmdMcpInstall({
598
- dir: ".",
599
- prod: true,
600
- remote: true,
601
- slug: "jenny",
602
- resolveDeployAuthImpl,
603
- stdout: createWireCapture().sink,
604
- err: () => {},
605
- });
606
- expect(code).toBe(0);
607
-
608
- const raw = await readFile(join(dir, ".cursor", "mcp.json"), "utf8");
609
- expect(JSON.parse(raw)).toEqual({
610
- mcpServers: {
611
- jenny: {
612
- url: "https://api.cursor.com/internal/agent-serve/mcp/42/jenny",
613
- headers: { Authorization: "Bearer crsr_test_key" },
614
- },
615
- },
616
- });
617
- });
618
-
619
- it("--remote still installs when the caller cannot read deployments", async () => {
620
- // Reading deployments is admin-tier; calling the gateway only needs team
621
- // membership, so a 403 preflight must not lock a member out.
622
- const dir = await withTempHome();
623
- const client = new AgentServeDeployClient({
624
- backendUrl: "https://api.cursor.com",
625
- apiKey: "crsr_member_key",
626
- fetchImpl: (async () =>
627
- new Response(JSON.stringify({ error: "Not authorized" }), {
628
- status: 403,
629
- headers: { "content-type": "application/json" },
630
- })) as unknown as typeof fetch,
631
- });
632
- const resolveDeployAuthImpl: typeof resolveDeployAuth = async () => ({
633
- client,
634
- teamId: 42,
635
- apiKey: "crsr_member_key",
636
- backendUrl: "https://api.cursor.com",
637
- });
638
-
639
- const errText: string[] = [];
640
- const code = await cmdMcpInstall({
641
- dir: ".",
642
- prod: true,
643
- remote: true,
644
- slug: "jenny",
645
- resolveDeployAuthImpl,
646
- stdout: createWireCapture().sink,
647
- err: (text) => {
648
- errText.push(text);
649
- },
650
- });
651
- expect(code).toBe(0);
652
- expect(errText.join("")).toContain("Could not verify");
653
-
654
- const raw = await readFile(join(dir, ".cursor", "mcp.json"), "utf8");
655
- expect(JSON.parse(raw).mcpServers.jenny.url).toBe(
656
- "https://api.cursor.com/internal/agent-serve/mcp/42/jenny"
657
- );
658
- });
659
-
660
- it("--remote requires --prod", async () => {
661
- await withTempHome();
662
- const errText: string[] = [];
663
- const code = await cmdMcpInstall({
664
- dir: ".",
665
- remote: true,
666
- slug: "jenny",
667
- err: (text) => {
668
- errText.push(text);
669
- },
670
- });
671
- expect(code).toBe(1);
672
- expect(errText.join("")).toContain("--remote requires --prod");
673
- });
674
-
675
- it("fails without writing when the deployment cannot be resolved", async () => {
676
- const dir = await withTempHome();
677
- const resolveImpl = vi.fn(async () => {
678
- throw new Error('Deployment "weather" (team 42): Deployment not found');
679
- }) satisfies ResolveProdTargetImpl;
680
-
681
- const errText: string[] = [];
682
- const code = await cmdMcpInstall({
683
- dir: ".",
684
- prod: true,
685
- slug: "weather",
686
- resolveProdTargetImpl: resolveImpl,
687
- err: (text) => {
688
- errText.push(text);
689
- },
690
- });
691
- expect(code).toBe(1);
692
- expect(errText.join("")).toContain("Deployment not found");
693
- await expect(
694
- readFile(join(dir, ".cursor", "mcp.json"), "utf8")
695
- ).rejects.toThrow();
696
- });
697
-
698
- it("merges into an existing mcp.json without clobbering other servers", async () => {
699
- const dir = await withTempHome();
700
- const path = cursorMcpJsonPath();
701
- await mkdir(join(dir, ".cursor"), { recursive: true });
702
- await writeFile(
703
- path,
704
- `${JSON.stringify(
705
- {
706
- mcpServers: {
707
- linear: { url: "https://mcp.linear.app/mcp" },
708
- },
709
- otherTopLevelKey: true,
710
- },
711
- null,
712
- 2
713
- )}\n`,
714
- "utf8"
715
- );
716
-
717
- const code = await cmdMcpInstall({
718
- dir: ".",
719
- url: "http://127.0.0.1:3000/helper",
720
- stdout: createWireCapture().sink,
721
- err: () => {},
722
- });
723
- expect(code).toBe(0);
724
-
725
- const raw = JSON.parse(await readFile(path, "utf8")) as {
726
- mcpServers: Record<string, unknown>;
727
- otherTopLevelKey: boolean;
728
- };
729
- expect(raw.otherTopLevelKey).toBe(true);
730
- expect(raw.mcpServers.linear).toEqual({
731
- url: "https://mcp.linear.app/mcp",
732
- });
733
- expect(raw.mcpServers.helper).toEqual({
734
- command: "agentkit",
735
- args: ["mcp", "--url", "http://127.0.0.1:3000/helper"],
736
- });
737
- });
738
-
739
- it("refuses to clobber an unparseable mcp.json", async () => {
740
- const tempHome = await withTempHome();
741
- await mkdir(join(tempHome, ".cursor"), { recursive: true });
742
- const path = cursorMcpJsonPath();
743
- await writeFile(path, "{ not json", "utf8");
744
-
745
- const errText: string[] = [];
746
- const code = await cmdMcpInstall({
747
- dir: ".",
748
- url: "http://127.0.0.1:3000/helper",
749
- err: (text) => {
750
- errText.push(text);
751
- },
752
- });
753
- expect(code).toBe(1);
754
- expect(errText.join("")).toContain("not valid JSON");
755
- expect(await readFile(path, "utf8")).toBe("{ not json");
756
- });
757
-
758
- it("--print emits the entry without touching the filesystem", async () => {
759
- const dir = await withTempHome();
760
- const out = createWireCapture();
761
- const lines: string[] = [];
762
- const sink = {
763
- write(chunk: string): boolean {
764
- lines.push(chunk);
765
- out.sink.write(chunk);
766
- return true;
767
- },
768
- };
769
- const code = await cmdMcpInstall({
770
- dir: ".",
771
- url: "http://127.0.0.1:3000/helper",
772
- print: true,
773
- stdout: sink,
774
- err: () => {},
775
- });
776
- expect(code).toBe(0);
777
- expect(JSON.parse(lines.join(""))).toEqual({
778
- mcpServers: {
779
- helper: {
780
- command: "agentkit",
781
- args: ["mcp", "--url", "http://127.0.0.1:3000/helper"],
782
- },
783
- },
784
- });
785
- await expect(
786
- readFile(join(dir, ".cursor", "mcp.json"), "utf8")
787
- ).rejects.toThrow();
788
- });
789
- });