@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,298 +0,0 @@
1
- import { createServer, type IncomingMessage, type Server } from "node:http";
2
- import { fileURLToPath } from "node:url";
3
- import { afterEach, describe, expect, it } from "vitest";
4
- import { isStaleMcpSessionError, McpHost } from "./mcp-host.js";
5
-
6
- const unitsServer = fileURLToPath(
7
- new URL("../../examples/weather-agent/mcp/units-server.ts", import.meta.url)
8
- );
9
-
10
- describe("isStaleMcpSessionError", () => {
11
- it("detects anytool session_terminated payloads and HTTP 404 codes", () => {
12
- expect(
13
- isStaleMcpSessionError(
14
- new Error(
15
- 'Streamable HTTP error: Error POSTing to endpoint: {"error":"session_terminated"}'
16
- )
17
- )
18
- ).toBe(true);
19
-
20
- const http404 = new Error(
21
- "Streamable HTTP error: Error POSTing to endpoint: gone"
22
- );
23
- (http404 as { code?: number }).code = 404;
24
- expect(isStaleMcpSessionError(http404)).toBe(true);
25
-
26
- expect(isStaleMcpSessionError(new Error("unauthorized"))).toBe(false);
27
- expect(isStaleMcpSessionError("session_terminated")).toBe(false);
28
- });
29
- });
30
-
31
- describe("McpHost", () => {
32
- const servers: Server[] = [];
33
- afterEach(async () => {
34
- await Promise.all(
35
- servers.splice(0).map(
36
- (server) =>
37
- new Promise<void>((resolve, reject) => {
38
- server.close((error) => (error ? reject(error) : resolve()));
39
- })
40
- )
41
- );
42
- });
43
-
44
- it("lists and calls tools on a stdio connection", async () => {
45
- const host = new McpHost([
46
- {
47
- name: "units",
48
- transport: {
49
- type: "stdio",
50
- command: "npx",
51
- args: ["-y", "tsx", unitsServer],
52
- },
53
- },
54
- ]);
55
- try {
56
- expect(host.names()).toEqual(["units"]);
57
- const tools = await host.listTools("units");
58
- expect(tools.map((tool) => tool.name).sort()).toEqual([
59
- "celsius_to_fahrenheit",
60
- "fahrenheit_to_celsius",
61
- ]);
62
-
63
- const result = await host.callTool("units", "celsius_to_fahrenheit", {
64
- value: 0,
65
- });
66
- expect(result.isError).toBeFalsy();
67
- const text = result.content.find(
68
- (part): part is { type: "text"; text: string } => part.type === "text"
69
- )?.text;
70
- expect(text).toBeDefined();
71
- expect(JSON.parse(text as string)).toEqual({
72
- celsius: 0,
73
- fahrenheit: 32,
74
- });
75
- } finally {
76
- await host.close();
77
- }
78
- }, 30_000);
79
-
80
- it("rejects unknown MCP connection names", async () => {
81
- const host = new McpHost([]);
82
- await expect(host.callTool("missing", "noop", {})).rejects.toThrow(
83
- /Unknown MCP connection "missing"/
84
- );
85
- await host.close();
86
- });
87
-
88
- it("reconnects after the remote Streamable HTTP session is terminated", async () => {
89
- const mock = await startTerminatingMcpMock();
90
- servers.push(mock.server);
91
-
92
- const host = new McpHost([
93
- {
94
- name: "anytool",
95
- transport: {
96
- type: "http",
97
- url: mock.url,
98
- },
99
- },
100
- ]);
101
- try {
102
- const first = await host.callTool("anytool", "ping", {});
103
- expect(first.isError).toBeFalsy();
104
- expect(mock.initializeCount).toBe(1);
105
-
106
- mock.terminateAllSessions();
107
-
108
- const second = await host.callTool("anytool", "ping", {});
109
- expect(second.isError).toBeFalsy();
110
- expect(mock.initializeCount).toBe(2);
111
- } finally {
112
- await host.close();
113
- }
114
- }, 30_000);
115
- });
116
-
117
- async function startTerminatingMcpMock(): Promise<{
118
- server: Server;
119
- url: string;
120
- initializeCount: number;
121
- terminateAllSessions: () => void;
122
- }> {
123
- const sessions = new Set<string>();
124
- const state = { initializeCount: 0 };
125
-
126
- const server = createServer((req, res) => {
127
- void handleMcpRequest(req, res, sessions, state);
128
- });
129
-
130
- await new Promise<void>((resolve, reject) => {
131
- server.once("error", reject);
132
- server.listen(0, "127.0.0.1", () => resolve());
133
- });
134
- const address = server.address();
135
- if (address === null || typeof address === "string") {
136
- throw new Error("expected TCP listen address");
137
- }
138
-
139
- return {
140
- server,
141
- url: `http://127.0.0.1:${address.port}/mcp`,
142
- get initializeCount() {
143
- return state.initializeCount;
144
- },
145
- terminateAllSessions() {
146
- sessions.clear();
147
- },
148
- };
149
- }
150
-
151
- async function handleMcpRequest(
152
- req: IncomingMessage,
153
- res: import("node:http").ServerResponse,
154
- sessions: Set<string>,
155
- state: { initializeCount: number }
156
- ): Promise<void> {
157
- if (req.method === "GET") {
158
- // Optional SSE stream; idle keep-alive is enough for the SDK.
159
- const sessionId = headerValue(req.headers["mcp-session-id"]);
160
- if (sessionId === undefined || !sessions.has(sessionId)) {
161
- res.writeHead(404, { "Content-Type": "application/json" });
162
- res.end(JSON.stringify({ error: "session_terminated" }));
163
- return;
164
- }
165
- res.writeHead(200, {
166
- "Content-Type": "text/event-stream",
167
- "Cache-Control": "no-cache",
168
- Connection: "keep-alive",
169
- "mcp-session-id": sessionId,
170
- });
171
- res.write(": connected\n\n");
172
- return;
173
- }
174
-
175
- if (req.method === "DELETE") {
176
- const sessionId = headerValue(req.headers["mcp-session-id"]);
177
- if (sessionId !== undefined) {
178
- sessions.delete(sessionId);
179
- }
180
- res.writeHead(200).end();
181
- return;
182
- }
183
-
184
- if (req.method !== "POST") {
185
- res.writeHead(405).end();
186
- return;
187
- }
188
-
189
- const body = await readJsonBody(req);
190
- const sessionId = headerValue(req.headers["mcp-session-id"]);
191
- const method =
192
- typeof body === "object" &&
193
- body !== null &&
194
- "method" in body &&
195
- typeof body.method === "string"
196
- ? body.method
197
- : undefined;
198
- const id =
199
- typeof body === "object" && body !== null && "id" in body
200
- ? body.id
201
- : undefined;
202
-
203
- if (method === "initialize") {
204
- state.initializeCount += 1;
205
- const nextSessionId = `session-${state.initializeCount}`;
206
- sessions.add(nextSessionId);
207
- writeSseJson(res, nextSessionId, {
208
- jsonrpc: "2.0",
209
- id,
210
- result: {
211
- protocolVersion: "2025-03-26",
212
- capabilities: { tools: {} },
213
- serverInfo: { name: "mock-anytool", version: "0.0.0" },
214
- },
215
- });
216
- return;
217
- }
218
-
219
- if (sessionId === undefined || !sessions.has(sessionId)) {
220
- res.writeHead(404, { "Content-Type": "application/json" });
221
- res.end(JSON.stringify({ error: "session_terminated" }));
222
- return;
223
- }
224
-
225
- if (method === undefined || id === undefined) {
226
- // notifications/initialized and similar
227
- res.writeHead(202).end();
228
- return;
229
- }
230
-
231
- if (method === "tools/list") {
232
- writeSseJson(res, sessionId, {
233
- jsonrpc: "2.0",
234
- id,
235
- result: {
236
- tools: [
237
- {
238
- name: "ping",
239
- description: "ping",
240
- inputSchema: { type: "object", properties: {} },
241
- },
242
- ],
243
- },
244
- });
245
- return;
246
- }
247
-
248
- if (method === "tools/call") {
249
- writeSseJson(res, sessionId, {
250
- jsonrpc: "2.0",
251
- id,
252
- result: {
253
- content: [{ type: "text", text: "pong" }],
254
- },
255
- });
256
- return;
257
- }
258
-
259
- writeSseJson(res, sessionId, {
260
- jsonrpc: "2.0",
261
- id,
262
- error: { code: -32601, message: `Method not found: ${method}` },
263
- });
264
- }
265
-
266
- function writeSseJson(
267
- res: import("node:http").ServerResponse,
268
- sessionId: string,
269
- payload: unknown
270
- ): void {
271
- res.writeHead(200, {
272
- "Content-Type": "text/event-stream",
273
- "Cache-Control": "no-cache",
274
- "mcp-session-id": sessionId,
275
- });
276
- res.end(`event: message\ndata: ${JSON.stringify(payload)}\n\n`);
277
- }
278
-
279
- function headerValue(value: string | string[] | undefined): string | undefined {
280
- if (typeof value === "string") {
281
- return value;
282
- }
283
- if (Array.isArray(value) && value.length > 0) {
284
- return value[0];
285
- }
286
- return undefined;
287
- }
288
-
289
- async function readJsonBody(req: IncomingMessage): Promise<unknown> {
290
- const chunks: Buffer[] = [];
291
- for await (const chunk of req) {
292
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
293
- }
294
- if (chunks.length === 0) {
295
- return undefined;
296
- }
297
- return JSON.parse(Buffer.concat(chunks).toString("utf8")) as unknown;
298
- }
@@ -1,148 +0,0 @@
1
- import { mkdtemp, readFile, writeFile } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { describe, expect, it } from "vitest";
5
- import {
6
- createHostMcpOAuthProvider,
7
- FileMcpOAuthStore,
8
- HostMcpOAuthProvider,
9
- mcpOauthSecretNames,
10
- normalizeMcpResourceUrl,
11
- openBrowser,
12
- tokensFromEnv,
13
- } from "./mcp-oauth.js";
14
-
15
- const ANYTOOL_URL = "https://anytool.example/mcp";
16
-
17
- describe("FileMcpOAuthStore", () => {
18
- it("persists tokens and client info bound to resource URL", async () => {
19
- const dir = await mkdtemp(join(tmpdir(), "agentkit-mcp-oauth-"));
20
- const path = join(dir, "mcp-auth.json");
21
- const store = new FileMcpOAuthStore(path);
22
-
23
- await store.saveTokens("anytool", ANYTOOL_URL, {
24
- access_token: "access-1",
25
- refresh_token: "refresh-1",
26
- token_type: "Bearer",
27
- });
28
- await store.saveClientInformation("anytool", ANYTOOL_URL, {
29
- client_id: "client-1",
30
- redirect_uris: ["http://localhost:8787/callback"],
31
- });
32
-
33
- const reloaded = new FileMcpOAuthStore(path);
34
- expect(await reloaded.loadTokens("anytool", ANYTOOL_URL)).toMatchObject({
35
- access_token: "access-1",
36
- refresh_token: "refresh-1",
37
- });
38
- expect(
39
- await reloaded.loadClientInformation("anytool", ANYTOOL_URL)
40
- ).toMatchObject({
41
- client_id: "client-1",
42
- });
43
-
44
- const raw = JSON.parse(await readFile(path, "utf8")) as {
45
- anytool: { resourceUrl: string; tokens: { refresh_token: string } };
46
- };
47
- expect(raw.anytool.resourceUrl).toBe(normalizeMcpResourceUrl(ANYTOOL_URL));
48
- expect(raw.anytool.tokens.refresh_token).toBe("refresh-1");
49
- });
50
-
51
- it("ignores tokens when connection URL changes", async () => {
52
- const dir = await mkdtemp(join(tmpdir(), "agentkit-mcp-oauth-"));
53
- const store = new FileMcpOAuthStore(join(dir, "mcp-auth.json"));
54
- await store.saveTokens("anytool", ANYTOOL_URL, {
55
- access_token: "access-1",
56
- token_type: "Bearer",
57
- });
58
- expect(
59
- await store.loadTokens("anytool", "https://evil.example/mcp")
60
- ).toBeUndefined();
61
- expect(await store.loadTokens("anytool", ANYTOOL_URL)).toMatchObject({
62
- access_token: "access-1",
63
- });
64
- });
65
-
66
- it("ignores legacy entries without resourceUrl", async () => {
67
- const dir = await mkdtemp(join(tmpdir(), "agentkit-mcp-oauth-"));
68
- const path = join(dir, "mcp-auth.json");
69
- await writeFile(
70
- path,
71
- JSON.stringify({
72
- anytool: {
73
- tokens: { access_token: "legacy", token_type: "Bearer" },
74
- },
75
- })
76
- );
77
- const store = new FileMcpOAuthStore(path);
78
- expect(await store.loadTokens("anytool", ANYTOOL_URL)).toBeUndefined();
79
- });
80
- });
81
-
82
- describe("HostMcpOAuthProvider", () => {
83
- it("writes token updates back through the store", async () => {
84
- const dir = await mkdtemp(join(tmpdir(), "agentkit-mcp-oauth-"));
85
- const store = new FileMcpOAuthStore(join(dir, "mcp-auth.json"));
86
- const provider = await createHostMcpOAuthProvider({
87
- identifier: "anytool",
88
- resourceUrl: ANYTOOL_URL,
89
- store,
90
- });
91
-
92
- await provider.saveTokens({
93
- access_token: "access-2",
94
- refresh_token: "refresh-2",
95
- token_type: "Bearer",
96
- });
97
- expect(provider.tokens()?.refresh_token).toBe("refresh-2");
98
- expect(
99
- (await store.loadTokens("anytool", ANYTOOL_URL))?.refresh_token
100
- ).toBe("refresh-2");
101
- });
102
-
103
- it("records redirect URLs for the login CLI", () => {
104
- const redirects: string[] = [];
105
- const provider = new HostMcpOAuthProvider({
106
- identifier: "anytool",
107
- resourceUrl: ANYTOOL_URL,
108
- store: new FileMcpOAuthStore(join(tmpdir(), "unused.json")),
109
- onRedirect: (url) => redirects.push(url.toString()),
110
- });
111
- provider.redirectToAuthorization(
112
- new URL("https://anytool.example/oauth/authorize?x=1")
113
- );
114
- expect(provider.lastRedirectUrl?.toString()).toContain(
115
- "anytool.example/oauth/authorize"
116
- );
117
- expect(redirects).toHaveLength(1);
118
- });
119
- });
120
-
121
- describe("openBrowser", () => {
122
- it("refuses non-http(s) URLs", () => {
123
- expect(() => openBrowser("file:///etc/passwd")).toThrow(/non-http/);
124
- });
125
- });
126
-
127
- describe("mcp oauth deployment secrets", () => {
128
- it("names secrets from the connection id", () => {
129
- expect(mcpOauthSecretNames("anytool")).toEqual({
130
- accessToken: "MCP_OAUTH_ANYTOOL_ACCESS_TOKEN",
131
- refreshToken: "MCP_OAUTH_ANYTOOL_REFRESH_TOKEN",
132
- clientId: "MCP_OAUTH_ANYTOOL_CLIENT_ID",
133
- });
134
- });
135
-
136
- it("reads bearer tokens from env when local file is empty", () => {
137
- const names = mcpOauthSecretNames("anytool");
138
- expect(
139
- tokensFromEnv("anytool", {
140
- [names.accessToken]: "access-env",
141
- [names.refreshToken]: "refresh-env",
142
- })
143
- ).toMatchObject({
144
- access_token: "access-env",
145
- refresh_token: "refresh-env",
146
- });
147
- });
148
- });
@@ -1,17 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { isLoopbackHostname } from "./net.js";
3
-
4
- describe("isLoopbackHostname", () => {
5
- it("accepts localhost and loopback literals", () => {
6
- expect(isLoopbackHostname("localhost")).toBe(true);
7
- expect(isLoopbackHostname("127.0.0.1")).toBe(true);
8
- expect(isLoopbackHostname("::1")).toBe(true);
9
- });
10
-
11
- it("rejects remote hostnames used in DNS rebinding", () => {
12
- expect(isLoopbackHostname("attacker.example")).toBe(false);
13
- expect(isLoopbackHostname("localhost.evil.example")).toBe(false);
14
- expect(isLoopbackHostname("127.0.0.1.nip.io")).toBe(false);
15
- expect(isLoopbackHostname("127.0.0.1.attacker.example")).toBe(false);
16
- });
17
- });
@@ -1,128 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { httpChannel } from "../channels.js";
3
- import type { AgentProject, ConnectionTransport } from "../types.js";
4
- import {
5
- collectPeerConnections,
6
- resolvePeerConnections,
7
- } from "./peer-connections.js";
8
-
9
- function makeProject(
10
- name: string,
11
- connections: Array<{ name: string; transport: ConnectionTransport }> = []
12
- ): AgentProject {
13
- return {
14
- rootDir: `/tmp/${name}`,
15
- agentDir: `/tmp/${name}/agent`,
16
- name,
17
- agent: {
18
- name,
19
- runtime: "local",
20
- tools: [],
21
- skills: [],
22
- connections,
23
- subagents: [],
24
- seedFiles: [],
25
- },
26
- httpChannel: httpChannel(),
27
- channels: [],
28
- schedules: [],
29
- hooks: [],
30
- abs: [],
31
- diagnostics: [],
32
- };
33
- }
34
-
35
- describe("collectPeerConnections", () => {
36
- it("groups validated refs by the referencing mount slug", () => {
37
- const mounts = [
38
- {
39
- slug: "concierge",
40
- project: makeProject("concierge", [
41
- { name: "weather", transport: { agent: "weather-agent" } },
42
- { name: "linear", transport: { url: "https://mcp.linear.app/mcp" } },
43
- ]),
44
- },
45
- { slug: "weather-agent", project: makeProject("weather-agent") },
46
- ];
47
- const refs = collectPeerConnections(mounts);
48
- expect([...refs.keys()]).toEqual(["concierge"]);
49
- expect(refs.get("concierge")).toEqual([
50
- { name: "weather", targetSlug: "weather-agent" },
51
- ]);
52
- });
53
-
54
- it("throws on unknown peer slugs, listing the mounted agents", () => {
55
- const mounts = [
56
- {
57
- slug: "concierge",
58
- project: makeProject("concierge", [
59
- { name: "weather", transport: { agent: "nope" } },
60
- ]),
61
- },
62
- { slug: "weather-agent", project: makeProject("weather-agent") },
63
- ];
64
- expect(() => collectPeerConnections(mounts)).toThrow(
65
- /unknown peer agent "nope".*weather-agent/
66
- );
67
- });
68
-
69
- it("throws on self-references", () => {
70
- const mounts = [
71
- {
72
- slug: "concierge",
73
- project: makeProject("concierge", [
74
- { name: "me", transport: { agent: "concierge" } },
75
- ]),
76
- },
77
- ];
78
- expect(() => collectPeerConnections(mounts)).toThrow(/references itself/);
79
- });
80
-
81
- it("rejects peers in single-agent mode (empty slug set)", () => {
82
- const mounts = [
83
- {
84
- slug: "",
85
- project: makeProject("solo", [
86
- { name: "weather", transport: { agent: "weather-agent" } },
87
- ]),
88
- },
89
- ];
90
- expect(() => collectPeerConnections(mounts)).toThrow(
91
- /Mounted agents: \(none\)/
92
- );
93
- });
94
- });
95
-
96
- describe("resolvePeerConnections", () => {
97
- const refs = [{ name: "weather", targetSlug: "weather-agent" }];
98
-
99
- it("resolves loopback URLs and omits cloudUrl without a publicUrl", () => {
100
- const peers = resolvePeerConnections({
101
- refs,
102
- serverUrl: "http://127.0.0.1:4123",
103
- });
104
- expect(peers).toEqual([
105
- {
106
- name: "weather",
107
- localUrl: "http://127.0.0.1:4123/weather-agent/v1/mcp",
108
- },
109
- ]);
110
- });
111
-
112
- it("resolves cloudUrl from publicUrl and attaches the bearer token", () => {
113
- const peers = resolvePeerConnections({
114
- refs,
115
- serverUrl: "http://127.0.0.1:4123/",
116
- publicUrl: "https://agents.example.com/",
117
- authToken: "sekret",
118
- });
119
- expect(peers).toEqual([
120
- {
121
- name: "weather",
122
- localUrl: "http://127.0.0.1:4123/weather-agent/v1/mcp",
123
- cloudUrl: "https://agents.example.com/weather-agent/v1/mcp",
124
- headers: { authorization: "Bearer sekret" },
125
- },
126
- ]);
127
- });
128
- });