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