@cursor/july 0.1.20 → 0.1.21

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 (212) hide show
  1. package/dist/bin/agent-serve.js +0 -0
  2. package/dist/channels/github/instrument.d.ts +20 -0
  3. package/dist/channels/github/instrument.d.ts.map +1 -0
  4. package/dist/channels/slack/api.d.ts +2 -0
  5. package/dist/channels/slack/api.d.ts.map +1 -1
  6. package/dist/channels/slack/api.js +3 -0
  7. package/dist/channels/slack/bot-mentions.d.ts +106 -0
  8. package/dist/channels/slack/bot-mentions.d.ts.map +1 -0
  9. package/dist/channels/slack/bot-mentions.js +243 -0
  10. package/dist/channels/slack/dispatch.d.ts +6 -0
  11. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  12. package/dist/channels/slack/dispatch.js +24 -3
  13. package/dist/channels/slack/inbound.d.ts +10 -1
  14. package/dist/channels/slack/inbound.d.ts.map +1 -1
  15. package/dist/channels/slack/inbound.js +12 -4
  16. package/dist/channels/slack/index.d.ts +1 -0
  17. package/dist/channels/slack/index.d.ts.map +1 -1
  18. package/dist/channels/slack/index.js +1 -0
  19. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  20. package/dist/channels/slack/slack-channel.js +61 -22
  21. package/dist/channels/slack/types.d.ts +58 -0
  22. package/dist/channels/slack/types.d.ts.map +1 -1
  23. package/dist/docs/404.html +1 -1
  24. package/dist/docs/ab.html +2 -2
  25. package/dist/docs/assets/{app.T-5ImnM3.js → app.jDxLzWv4.js} +1 -1
  26. package/dist/docs/assets/chunks/@localSearchIndexroot.DoJHJjqF.js +1 -0
  27. package/dist/docs/assets/chunks/{VPLocalSearchBox.OMGMWH6H.js → VPLocalSearchBox.Y6bDR1-a.js} +1 -1
  28. package/dist/docs/assets/chunks/{theme.DLg4yB4D.js → theme.CLazCWlJ.js} +2 -2
  29. package/dist/docs/building-with-agents.html +2 -2
  30. package/dist/docs/concepts.html +2 -2
  31. package/dist/docs/deployment.html +2 -2
  32. package/dist/docs/evals.html +2 -2
  33. package/dist/docs/example-agents/approval-buddy.html +2 -2
  34. package/dist/docs/example-agents/benny.html +2 -2
  35. package/dist/docs/example-agents/bugbot.html +2 -2
  36. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  37. package/dist/docs/example-agents/codeowners-review.html +2 -2
  38. package/dist/docs/example-agents/concierge.html +2 -2
  39. package/dist/docs/example-agents/fsd.html +2 -2
  40. package/dist/docs/example-agents/index.html +2 -2
  41. package/dist/docs/example-agents/knowledge-base.html +2 -2
  42. package/dist/docs/example-agents/oncall.html +2 -2
  43. package/dist/docs/example-agents/security-reviewer.html +2 -2
  44. package/dist/docs/example-agents/slack-agent.html +2 -2
  45. package/dist/docs/example-agents/weather-agent.html +2 -2
  46. package/dist/docs/guides/agent-to-agent.html +2 -2
  47. package/dist/docs/guides/cloud-runtime.html +2 -2
  48. package/dist/docs/guides/github.html +2 -2
  49. package/dist/docs/guides/human-in-the-loop.html +2 -2
  50. package/dist/docs/guides/mcp-oauth.html +2 -2
  51. package/dist/docs/guides/slack.html +2 -2
  52. package/dist/docs/guides/webhooks.html +2 -2
  53. package/dist/docs/hillclimbing.html +2 -2
  54. package/dist/docs/index.html +2 -2
  55. package/dist/docs/quickstart.html +2 -2
  56. package/dist/docs/reference/agent-config.html +2 -2
  57. package/dist/docs/reference/channels.html +2 -2
  58. package/dist/docs/reference/cli.html +2 -2
  59. package/dist/docs/reference/connections.html +2 -2
  60. package/dist/docs/reference/hooks.html +2 -2
  61. package/dist/docs/reference/http-api.html +2 -2
  62. package/dist/docs/reference/instructions.html +2 -2
  63. package/dist/docs/reference/playground.html +2 -2
  64. package/dist/docs/reference/project-layout.html +2 -2
  65. package/dist/docs/reference/prompt.html +2 -2
  66. package/dist/docs/reference/schedules.html +2 -2
  67. package/dist/docs/reference/sessions.html +2 -2
  68. package/dist/docs/reference/skills.html +2 -2
  69. package/dist/docs/reference/subagents.html +2 -2
  70. package/dist/docs/reference/tools.html +2 -2
  71. package/dist/docs/scaffolding-agents.html +2 -2
  72. package/dist/docs/storage.html +2 -2
  73. package/dist/docs/troubleshooting.html +2 -2
  74. package/dist/internal/json-dir-store.d.ts +32 -0
  75. package/dist/internal/json-dir-store.d.ts.map +1 -0
  76. package/dist/internal/json-dir-store.js +100 -0
  77. package/dist/internal/resolved-connections.d.ts +1 -1
  78. package/dist/internal/resolved-connections.d.ts.map +1 -1
  79. package/dist/internal/resolved-connections.js +10 -0
  80. package/dist/internal/session-engine.d.ts +4 -1
  81. package/dist/internal/session-engine.d.ts.map +1 -1
  82. package/dist/internal/session-engine.js +13 -2
  83. package/dist/playground/assets/index-CjOQ4hN9.css +1 -0
  84. package/dist/playground/assets/index-dshZQJCp.js +85 -0
  85. package/dist/playground/index.html +2 -2
  86. package/dist/types.d.ts +7 -0
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/types.js +9 -0
  89. package/package.json +24 -24
  90. package/src/bin/agent-serve.version.test.ts +62 -0
  91. package/src/channels/github/api.test.ts +64 -0
  92. package/src/channels/github/auth.test.ts +105 -0
  93. package/src/channels/github/cursor-account.test.ts +204 -0
  94. package/src/channels/github/forward.test.ts +457 -0
  95. package/src/channels/github/github.test.ts +937 -0
  96. package/src/channels/github/replay.test.ts +179 -0
  97. package/src/channels/slack/api.post-message.test.ts +148 -0
  98. package/src/channels/slack/api.ts +5 -0
  99. package/src/channels/slack/approvals.test.ts +328 -0
  100. package/src/channels/slack/block-actions.test.ts +452 -0
  101. package/src/channels/slack/bot-mentions.test.ts +217 -0
  102. package/src/channels/slack/bot-mentions.ts +315 -0
  103. package/src/channels/slack/channel-watch.test.ts +363 -0
  104. package/src/channels/slack/cursor-account.test.ts +253 -0
  105. package/src/channels/slack/defaults.final-post.test.ts +182 -0
  106. package/src/channels/slack/dispatch.test.ts +795 -0
  107. package/src/channels/slack/dispatch.ts +27 -1
  108. package/src/channels/slack/eval-directive.test.ts +273 -0
  109. package/src/channels/slack/inbound.ts +25 -4
  110. package/src/channels/slack/index.ts +1 -0
  111. package/src/channels/slack/message-body.test.ts +54 -0
  112. package/src/channels/slack/nudge-store.test.ts +143 -0
  113. package/src/channels/slack/slack-channel.ts +66 -8
  114. package/src/channels/slack/slack.test.ts +391 -0
  115. package/src/channels/slack/stop.test.ts +23 -0
  116. package/src/channels/slack/thread-context.test.ts +202 -0
  117. package/src/channels/slack/types.ts +60 -0
  118. package/src/evals/assertions.test.ts +580 -0
  119. package/src/evals/expect.test.ts +144 -0
  120. package/src/evals/judge.test.ts +181 -0
  121. package/src/evals/loaders.test.ts +132 -0
  122. package/src/evals/matchers.test.ts +95 -0
  123. package/src/evals/reporters.test.ts +303 -0
  124. package/src/evals/run-facts.test.ts +259 -0
  125. package/src/internal/ab-snapshot.test.ts +325 -0
  126. package/src/internal/approval-gate.test.ts +49 -0
  127. package/src/internal/approvals.integration.test.ts +383 -0
  128. package/src/internal/authored-loaders.test.ts +31 -0
  129. package/src/internal/builtin-tools/reminders.test.ts +201 -0
  130. package/src/internal/channel-route-schema.test.ts +294 -0
  131. package/src/internal/chat-attach.test.ts +262 -0
  132. package/src/internal/cli-deploy.test.ts +1991 -0
  133. package/src/internal/cli-mcp.test.ts +789 -0
  134. package/src/internal/cli-skills.test.ts +133 -0
  135. package/src/internal/cli-slack.test.ts +1647 -0
  136. package/src/internal/cloud-merge.test.ts +74 -0
  137. package/src/internal/cron.test.ts +22 -0
  138. package/src/internal/cursor/account-mcp.test.ts +807 -0
  139. package/src/internal/cursor/backend-client.test.ts +591 -0
  140. package/src/internal/cursor/credentials.test.ts +351 -0
  141. package/src/internal/cursor/github-credentials.test.ts +136 -0
  142. package/src/internal/cursor-account-mcp-auth.test.ts +310 -0
  143. package/src/internal/cursor-account.integration.test.ts +441 -0
  144. package/src/internal/cursor-event-relay.test.ts +746 -0
  145. package/src/internal/cursor-github-credentials.integration.test.ts +271 -0
  146. package/src/internal/cursor-slack-relay.test.ts +525 -0
  147. package/src/internal/deploy-source.test.ts +111 -0
  148. package/src/internal/discovery.builtin-tools.test.ts +94 -0
  149. package/src/internal/discovery.concurrency.test.ts +60 -0
  150. package/src/internal/discovery.cursor-account.test.ts +133 -0
  151. package/src/internal/discovery.cwd.test.ts +83 -0
  152. package/src/internal/discovery.hosting.test.ts +80 -0
  153. package/src/internal/discovery.identity.test.ts +44 -0
  154. package/src/internal/docs-site.test.ts +66 -0
  155. package/src/internal/duration.test.ts +29 -0
  156. package/src/internal/eval-judge-model.test.ts +187 -0
  157. package/src/internal/eval-run-store.cancel.test.ts +142 -0
  158. package/src/internal/eval-run-store.storage.test.ts +211 -0
  159. package/src/internal/eval-runner.http.test.ts +403 -0
  160. package/src/internal/eval-runner.run.test.ts +928 -0
  161. package/src/internal/evals-client.test.ts +307 -0
  162. package/src/internal/event-mapper.test.ts +243 -0
  163. package/src/internal/github-fanout.test.ts +213 -0
  164. package/src/internal/handleAgentServeTrigger.test.ts +179 -0
  165. package/src/internal/host-kv.test.ts +82 -0
  166. package/src/internal/host-platforms.test.ts +126 -0
  167. package/src/internal/http-channel.test.ts +402 -0
  168. package/src/internal/init-project.test.ts +269 -0
  169. package/src/internal/install-cursor-skills.test.ts +262 -0
  170. package/src/internal/local-env.test.ts +100 -0
  171. package/src/internal/log-ring.test.ts +31 -0
  172. package/src/internal/logs-client.test.ts +350 -0
  173. package/src/internal/mcp-endpoint.test.ts +436 -0
  174. package/src/internal/mcp-host.test.ts +298 -0
  175. package/src/internal/mcp-oauth.test.ts +148 -0
  176. package/src/internal/net.test.ts +17 -0
  177. package/src/internal/peer-connections.test.ts +128 -0
  178. package/src/internal/peer-mcp.integration.test.ts +289 -0
  179. package/src/internal/playground/toolchain.test.ts +53 -0
  180. package/src/internal/playground-cli.test.ts +187 -0
  181. package/src/internal/playground-proxy.test.ts +376 -0
  182. package/src/internal/prompt-context.integration.test.ts +232 -0
  183. package/src/internal/prompt-context.test.ts +127 -0
  184. package/src/internal/reminder-runner.test.ts +390 -0
  185. package/src/internal/reminder-store.test.ts +53 -0
  186. package/src/internal/request-headers.test.ts +27 -0
  187. package/src/internal/resolve-prod-target.test.ts +787 -0
  188. package/src/internal/resolved-connections.test.ts +295 -0
  189. package/src/internal/resolved-connections.ts +18 -1
  190. package/src/internal/router.test.ts +57 -0
  191. package/src/internal/sdk-runner.test.ts +290 -0
  192. package/src/internal/session-engine.coalesce.test.ts +169 -0
  193. package/src/internal/session-engine.concurrency.test.ts +250 -0
  194. package/src/internal/session-engine.host-oauth-mcp.test.ts +110 -0
  195. package/src/internal/session-engine.interrupt.test.ts +577 -0
  196. package/src/internal/session-engine.storage.test.ts +547 -0
  197. package/src/internal/session-engine.ts +15 -1
  198. package/src/internal/session-urls.test.ts +28 -0
  199. package/src/internal/sessions-client.test.ts +518 -0
  200. package/src/internal/storage-coordinator.test.ts +517 -0
  201. package/src/internal/tool-call.test.ts +458 -0
  202. package/src/internal/tool-result.test.ts +52 -0
  203. package/src/internal/trajectory.approvals.test.ts +83 -0
  204. package/src/internal/trajectory.subagents.test.ts +198 -0
  205. package/src/internal/turn-governor.test.ts +137 -0
  206. package/src/internal/update-check.test.ts +485 -0
  207. package/src/internal/workspace.test.ts +81 -0
  208. package/src/storage-backends/cursor-hosted.test.ts +121 -0
  209. package/src/types.ts +12 -0
  210. package/dist/docs/assets/chunks/@localSearchIndexroot.CvejISXF.js +0 -1
  211. package/dist/playground/assets/index-CidizGZv.css +0 -1
  212. package/dist/playground/assets/index-CuiAKZaP.js +0 -85
@@ -0,0 +1,789 @@
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
+ });