@cursor/july 0.1.32 → 0.1.34

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 (186) hide show
  1. package/README.md +1 -1
  2. package/dist/artifacts.d.ts.map +1 -1
  3. package/dist/artifacts.js +0 -5
  4. package/dist/bin/agent-serve.d.ts +1 -0
  5. package/dist/bin/agent-serve.d.ts.map +1 -1
  6. package/dist/bin/agent-serve.js +32 -2
  7. package/dist/channels/github/types.d.ts +10 -7
  8. package/dist/channels/github/types.d.ts.map +1 -1
  9. package/dist/docs/404.html +2 -2
  10. package/dist/docs/ab.html +3 -3
  11. package/dist/docs/assets/{app.C2aVczmo.js → app.FPupl4SP.js} +1 -1
  12. package/dist/docs/assets/chunks/@localSearchIndexroot.WoYunhnT.js +1 -0
  13. package/dist/docs/assets/chunks/{VPLocalSearchBox.DLEzetbD.js → VPLocalSearchBox.Cd182Cu0.js} +1 -1
  14. package/dist/docs/assets/chunks/{theme.BkOH1oMp.js → theme.BEM3Okcd.js} +2 -2
  15. package/dist/docs/assets/{example-agents_weather-agent.md.Dmrcphhl.js → example-agents_weather-agent.md.BADkPqxQ.js} +6 -4
  16. package/dist/docs/assets/example-agents_weather-agent.md.BADkPqxQ.lean.js +1 -0
  17. package/dist/docs/assets/guides_cloud-runtime.md.CDJGvVC4.js +9 -0
  18. package/dist/docs/assets/guides_cloud-runtime.md.CDJGvVC4.lean.js +1 -0
  19. package/dist/docs/building-with-agents.html +3 -3
  20. package/dist/docs/concepts.html +3 -3
  21. package/dist/docs/deployment.html +3 -3
  22. package/dist/docs/evals.html +3 -3
  23. package/dist/docs/example-agents/approval-buddy.html +3 -3
  24. package/dist/docs/example-agents/benny.html +3 -3
  25. package/dist/docs/example-agents/bugbot.html +3 -3
  26. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  27. package/dist/docs/example-agents/codeowners-review.html +3 -3
  28. package/dist/docs/example-agents/concierge.html +3 -3
  29. package/dist/docs/example-agents/fsd.html +3 -3
  30. package/dist/docs/example-agents/index.html +3 -3
  31. package/dist/docs/example-agents/knowledge-base.html +3 -3
  32. package/dist/docs/example-agents/oncall.html +3 -3
  33. package/dist/docs/example-agents/security-reviewer.html +3 -3
  34. package/dist/docs/example-agents/slack-agent.html +3 -3
  35. package/dist/docs/example-agents/weather-agent.html +10 -8
  36. package/dist/docs/guides/agent-to-agent.html +3 -3
  37. package/dist/docs/guides/cloud-runtime.html +5 -5
  38. package/dist/docs/guides/github.html +3 -3
  39. package/dist/docs/guides/human-in-the-loop.html +3 -3
  40. package/dist/docs/guides/mcp-oauth.html +3 -3
  41. package/dist/docs/guides/slack.html +3 -3
  42. package/dist/docs/guides/webhooks.html +3 -3
  43. package/dist/docs/hashmap.json +1 -1
  44. package/dist/docs/hillclimbing.html +3 -3
  45. package/dist/docs/index.html +3 -3
  46. package/dist/docs/quickstart.html +3 -3
  47. package/dist/docs/reference/agent-config.html +3 -3
  48. package/dist/docs/reference/channels.html +3 -3
  49. package/dist/docs/reference/cli.html +3 -3
  50. package/dist/docs/reference/connections.html +3 -3
  51. package/dist/docs/reference/hooks.html +3 -3
  52. package/dist/docs/reference/http-api.html +3 -3
  53. package/dist/docs/reference/instructions.html +3 -3
  54. package/dist/docs/reference/playground.html +3 -3
  55. package/dist/docs/reference/project-layout.html +3 -3
  56. package/dist/docs/reference/prompt.html +3 -3
  57. package/dist/docs/reference/schedules.html +3 -3
  58. package/dist/docs/reference/sessions.html +3 -3
  59. package/dist/docs/reference/skills.html +3 -3
  60. package/dist/docs/reference/subagents.html +3 -3
  61. package/dist/docs/reference/tools.html +3 -3
  62. package/dist/docs/scaffolding-agents.html +3 -3
  63. package/dist/docs/storage.html +3 -3
  64. package/dist/docs/troubleshooting.html +3 -3
  65. package/dist/files-backends/agent-store-presigned-url.d.ts.map +1 -1
  66. package/dist/files-backends/agent-store-presigned-url.js +19 -810
  67. package/dist/files.d.ts +20 -0
  68. package/dist/files.d.ts.map +1 -1
  69. package/dist/files.js +20 -0
  70. package/dist/index.d.ts +2 -2
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +2 -2
  73. package/dist/internal/artifacts-store.d.ts +14 -6
  74. package/dist/internal/artifacts-store.d.ts.map +1 -1
  75. package/dist/internal/artifacts-store.js +160 -16
  76. package/dist/internal/cli-convert-automation.d.ts +39 -0
  77. package/dist/internal/cli-convert-automation.d.ts.map +1 -0
  78. package/dist/internal/cli-convert-automation.js +280 -0
  79. package/dist/internal/cli-deploy.d.ts +2 -0
  80. package/dist/internal/cli-deploy.d.ts.map +1 -1
  81. package/dist/internal/cli-deploy.js +1 -1
  82. package/dist/internal/cli-mcp-oauth.d.ts +23 -0
  83. package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
  84. package/dist/internal/cli-mcp-oauth.js +57 -33
  85. package/dist/internal/convert-automation/convert-workflow.d.ts +10 -0
  86. package/dist/internal/convert-automation/convert-workflow.d.ts.map +1 -0
  87. package/dist/internal/convert-automation/convert-workflow.js +998 -0
  88. package/dist/internal/convert-automation/index.d.ts +3 -0
  89. package/dist/internal/convert-automation/index.d.ts.map +1 -0
  90. package/dist/internal/convert-automation/index.js +1 -0
  91. package/dist/internal/convert-automation/slug.d.ts +11 -0
  92. package/dist/internal/convert-automation/slug.d.ts.map +1 -0
  93. package/dist/internal/convert-automation/slug.js +36 -0
  94. package/dist/internal/convert-automation/types.d.ts +191 -0
  95. package/dist/internal/convert-automation/types.d.ts.map +1 -0
  96. package/dist/internal/convert-automation/types.js +13 -0
  97. package/dist/internal/cursor-relay-core.d.ts +12 -0
  98. package/dist/internal/cursor-relay-core.d.ts.map +1 -1
  99. package/dist/internal/cursor-relay-core.js +42 -8
  100. package/dist/internal/deploy-client.d.ts +31 -0
  101. package/dist/internal/deploy-client.d.ts.map +1 -1
  102. package/dist/internal/deploy-client.js +48 -0
  103. package/dist/internal/discovery.d.ts.map +1 -1
  104. package/dist/internal/discovery.js +3 -32
  105. package/dist/internal/init-project.d.ts +1 -0
  106. package/dist/internal/init-project.d.ts.map +1 -1
  107. package/dist/internal/init-project.js +1 -1
  108. package/dist/internal/mcp-endpoint.d.ts +6 -0
  109. package/dist/internal/mcp-endpoint.d.ts.map +1 -1
  110. package/dist/internal/mcp-endpoint.js +168 -0
  111. package/dist/internal/resolved-connections.d.ts +15 -0
  112. package/dist/internal/resolved-connections.d.ts.map +1 -1
  113. package/dist/internal/resolved-connections.js +12 -5
  114. package/dist/internal/review-comments.d.ts +9 -1
  115. package/dist/internal/review-comments.d.ts.map +1 -1
  116. package/dist/internal/review-comments.js +76 -0
  117. package/dist/internal/server.d.ts.map +1 -1
  118. package/dist/internal/server.js +83 -8
  119. package/dist/internal/session-engine.d.ts +56 -3
  120. package/dist/internal/session-engine.d.ts.map +1 -1
  121. package/dist/internal/session-engine.js +252 -83
  122. package/dist/internal/storage-coordinator.d.ts +0 -15
  123. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  124. package/dist/internal/storage-coordinator.js +0 -52
  125. package/dist/internal/workspace.d.ts +10 -5
  126. package/dist/internal/workspace.d.ts.map +1 -1
  127. package/dist/internal/workspace.js +13 -0
  128. package/dist/memory.d.ts +22 -6
  129. package/dist/memory.d.ts.map +1 -1
  130. package/dist/memory.js +100 -8
  131. package/dist/playground/assets/index-CDDWw0YX.js +85 -0
  132. package/dist/playground/assets/index-MVuNTd8v.css +1 -0
  133. package/dist/playground/index.html +2 -2
  134. package/dist/storage.d.ts +0 -4
  135. package/dist/storage.d.ts.map +1 -1
  136. package/dist/storage.js +0 -4
  137. package/dist/types.d.ts +18 -0
  138. package/dist/types.d.ts.map +1 -1
  139. package/docs/example-agents/weather-agent.md +29 -11
  140. package/docs/guides/cloud-runtime.md +6 -4
  141. package/package.json +3 -22
  142. package/src/artifacts.ts +0 -6
  143. package/src/bin/agent-serve.ts +36 -2
  144. package/src/channels/github/types.ts +10 -7
  145. package/src/files-backends/agent-store-presigned-url.ts +7 -5
  146. package/src/files.ts +27 -0
  147. package/src/index.ts +6 -1
  148. package/src/internal/artifacts-store.ts +185 -22
  149. package/src/internal/cli-convert-automation.ts +352 -0
  150. package/src/internal/cli-deploy.ts +1 -1
  151. package/src/internal/cli-mcp-oauth.ts +69 -31
  152. package/src/internal/convert-automation/convert-workflow.ts +1422 -0
  153. package/src/internal/convert-automation/index.ts +10 -0
  154. package/src/internal/convert-automation/slug.ts +38 -0
  155. package/src/internal/convert-automation/types.ts +199 -0
  156. package/src/internal/cursor-relay-core.ts +62 -3
  157. package/src/internal/deploy-client.ts +78 -0
  158. package/src/internal/discovery.ts +3 -37
  159. package/src/internal/init-project.ts +1 -1
  160. package/src/internal/mcp-endpoint.ts +243 -0
  161. package/src/internal/resolved-connections.ts +32 -6
  162. package/src/internal/review-comments.ts +102 -2
  163. package/src/internal/server.ts +108 -2
  164. package/src/internal/session-engine.ts +329 -99
  165. package/src/internal/storage-coordinator.ts +0 -62
  166. package/src/internal/workspace.ts +19 -5
  167. package/src/memory.ts +126 -10
  168. package/src/storage.ts +0 -6
  169. package/src/types.ts +15 -0
  170. package/dist/channels/slack/post-update-delivery.d.ts +0 -85
  171. package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
  172. package/dist/docs/assets/chunks/@localSearchIndexroot.xgMnlUSO.js +0 -1
  173. package/dist/docs/assets/example-agents_weather-agent.md.Dmrcphhl.lean.js +0 -1
  174. package/dist/docs/assets/guides_cloud-runtime.md.BZ2GA7Es.js +0 -9
  175. package/dist/docs/assets/guides_cloud-runtime.md.BZ2GA7Es.lean.js +0 -1
  176. package/dist/internal/json-dir-store.d.ts +0 -32
  177. package/dist/internal/json-dir-store.d.ts.map +0 -1
  178. package/dist/internal/persistence-coordinator.d.ts +0 -127
  179. package/dist/internal/persistence-coordinator.d.ts.map +0 -1
  180. package/dist/multi-tenant.d.ts +0 -80
  181. package/dist/multi-tenant.d.ts.map +0 -1
  182. package/dist/multi-tenant.js +0 -69
  183. package/dist/persistence.d.ts +0 -184
  184. package/dist/persistence.d.ts.map +0 -1
  185. package/dist/playground/assets/index-50PKeJlG.css +0 -1
  186. package/dist/playground/assets/index-C0f2Wl1q.js +0 -85
@@ -0,0 +1,352 @@
1
+ /**
2
+ * `agent-serve convert-automation <url>` — export a Cursor Automation into
3
+ * an agentkit project.
4
+ *
5
+ * Fetches the automation's workflow from the backend export endpoint
6
+ * (Bearer user API key, same credential chain as deploy), runs the pure
7
+ * converter, writes the converted files (wx — existing files are kept),
8
+ * fills in the init scaffold around them, and installs npm dependencies
9
+ * (like `init`). MCP servers convert to Cursor-account connections resolved
10
+ * at runtime (no URLs or credentials in the project; authorization is
11
+ * managed in Cursor, dashboard → MCP). The generated project is NEVER
12
+ * imported/executed here — it embeds server-derived data, so checking it is
13
+ * left to an explicit `validate` next step the user runs after review.
14
+ * Exit 0 even with warnings; nonzero only for bad args, auth/fetch
15
+ * failures, or filesystem write failures.
16
+ */
17
+
18
+ import { mkdir, rm, writeFile } from "node:fs/promises";
19
+ import { dirname, join, resolve } from "node:path";
20
+ import {
21
+ type ConvertedMcpConnection,
22
+ type ConvertWorkflowResult,
23
+ convertWorkflow,
24
+ type WorkflowJson,
25
+ } from "./convert-automation/index.js";
26
+ import { slugify } from "./convert-automation/slug.js";
27
+ import {
28
+ cursorExternalApiUrl,
29
+ resolveApiKey,
30
+ SIGN_IN_HINT,
31
+ } from "./cursor/credentials.js";
32
+ import {
33
+ AgentServeDeployClient,
34
+ type AutomationExport,
35
+ DeployApiError,
36
+ } from "./deploy-client.js";
37
+ import { CLI_COMMAND_NAME as CLI } from "./distribution.js";
38
+ import {
39
+ formatCdCommand,
40
+ type InitFileEntry,
41
+ initProject,
42
+ installScaffoldDependencies,
43
+ isEexist,
44
+ } from "./init-project.js";
45
+ import { stdoutPalette } from "./terminal-style.js";
46
+
47
+ export interface ConvertAutomationCliOptions {
48
+ /** Positional: automation URL or bare uuid. */
49
+ url?: string;
50
+ /** Target directory; defaults to ./<automation-name-slug>. */
51
+ dir?: string;
52
+ json?: boolean;
53
+ apiKey?: string;
54
+ /** Test seams. */
55
+ fetchImpl?: typeof fetch;
56
+ out?: (text: string) => void;
57
+ err?: (text: string) => void;
58
+ installImpl?: typeof installScaffoldDependencies;
59
+ cwd?: string;
60
+ }
61
+
62
+ const UUID_PATTERN =
63
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
64
+
65
+ /**
66
+ * Extract the automation uuid from a dashboard URL
67
+ * (`/automations/<uuid>` or `/custom-agents/<uuid>`, any origin — dev
68
+ * hosts included) or a bare uuid. Undefined when nothing matches.
69
+ */
70
+ export function parseAutomationRef(input: string): string | undefined {
71
+ const trimmed = input.trim();
72
+ if (UUID_PATTERN.test(trimmed)) {
73
+ return trimmed.toLowerCase();
74
+ }
75
+ let parsed: URL;
76
+ try {
77
+ parsed = new URL(trimmed);
78
+ } catch {
79
+ return undefined;
80
+ }
81
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
82
+ return undefined;
83
+ }
84
+ const segments = parsed.pathname.split("/").filter((s) => s !== "");
85
+ if (segments.length !== 2) {
86
+ return undefined;
87
+ }
88
+ const [kind, uuid] = segments;
89
+ if (kind !== "automations" && kind !== "custom-agents") {
90
+ return undefined;
91
+ }
92
+ if (uuid === undefined || !UUID_PATTERN.test(uuid)) {
93
+ return undefined;
94
+ }
95
+ return uuid.toLowerCase();
96
+ }
97
+
98
+ export async function cmdConvertAutomation(
99
+ options: ConvertAutomationCliOptions
100
+ ): Promise<number> {
101
+ const out =
102
+ options.out ?? ((text: string) => void process.stdout.write(text));
103
+ const err =
104
+ options.err ?? ((text: string) => void process.stderr.write(text));
105
+ const json = options.json === true;
106
+ // Failures stay machine-readable under --json (stderr keeps the prose).
107
+ const fail = (message: string): 1 => {
108
+ if (json) {
109
+ out(`${JSON.stringify({ ok: false, error: message })}\n`);
110
+ }
111
+ err(`${message}\n`);
112
+ return 1;
113
+ };
114
+
115
+ if (options.url === undefined || options.url.trim() === "") {
116
+ return fail(
117
+ `Usage: ${CLI} convert-automation <automation-url-or-uuid> [--dir <path>] [--json]`
118
+ );
119
+ }
120
+ const uuid = parseAutomationRef(options.url);
121
+ if (uuid === undefined) {
122
+ return fail(
123
+ `Could not parse ${JSON.stringify(options.url)} — expected an automation URL (https://cursor.com/automations/<uuid> or .../custom-agents/<uuid>) or a bare uuid.`
124
+ );
125
+ }
126
+
127
+ const resolved = await resolveApiKey({ apiKey: options.apiKey });
128
+ if (resolved === undefined) {
129
+ return fail(`Not signed in to Cursor. ${SIGN_IN_HINT}`);
130
+ }
131
+ const client = new AgentServeDeployClient({
132
+ backendUrl: cursorExternalApiUrl(),
133
+ apiKey: resolved.apiKey,
134
+ fetchImpl: options.fetchImpl,
135
+ });
136
+
137
+ let payload: AutomationExport;
138
+ try {
139
+ payload = await client.exportAutomation({ uuid });
140
+ } catch (error) {
141
+ if (error instanceof DeployApiError) {
142
+ return fail(error.message);
143
+ }
144
+ throw error;
145
+ }
146
+
147
+ const result = convertWorkflow({
148
+ automation: payload.automation,
149
+ workflow: payload.workflow as WorkflowJson,
150
+ mcpServers: payload.mcpServers,
151
+ exportedAt: new Date().toISOString(),
152
+ });
153
+
154
+ const displayDir =
155
+ options.dir ?? `./${slugify(payload.automation.name, "automation")}`;
156
+ const rootDir = resolve(options.cwd ?? process.cwd(), displayDir);
157
+
158
+ let files: InitFileEntry[];
159
+ try {
160
+ files = await writeProjectFiles(rootDir, result);
161
+ } catch (error) {
162
+ return fail(
163
+ `Could not write project files under ${rootDir}: ${describe(error)}`
164
+ );
165
+ }
166
+
167
+ // Same npm install as `init`, so the printed `validate` / `dev` next steps
168
+ // work immediately. Failure (npm exit or spawn error) is loud but never
169
+ // fails the conversion.
170
+ const install = options.installImpl ?? installScaffoldDependencies;
171
+ if (!json) {
172
+ out("\nInstalling npm packages\n");
173
+ }
174
+ let installed: Awaited<ReturnType<typeof installScaffoldDependencies>>;
175
+ try {
176
+ installed = await install(rootDir, {
177
+ stdio: json ? "pipe" : "inherit",
178
+ });
179
+ } catch (error) {
180
+ installed = { ok: false, exitCode: -1, stderr: describe(error) };
181
+ }
182
+ if (!installed.ok && !json) {
183
+ const reason = installed.stderr.trim() || `exit ${installed.exitCode}`;
184
+ err(
185
+ `npm install failed (${reason}); run \`npm install\` in ${displayDir} before \`${CLI} dev\`.\n`
186
+ );
187
+ }
188
+
189
+ if (json) {
190
+ out(
191
+ `${JSON.stringify(
192
+ {
193
+ ok: true,
194
+ directory: rootDir,
195
+ files: files.map(({ path, action }) => ({ path, action })),
196
+ warnings: result.warnings,
197
+ setupSteps: result.setupSteps,
198
+ installed: installed.ok,
199
+ installError: installed.ok
200
+ ? null
201
+ : installed.stderr.trim() ||
202
+ `npm install exited ${installed.exitCode}`,
203
+ mcpConnections: result.mcpConnections.map((connection) => ({
204
+ name: connection.name,
205
+ kind: connection.kind,
206
+ })),
207
+ },
208
+ null,
209
+ 2
210
+ )}\n`
211
+ );
212
+ return 0;
213
+ }
214
+
215
+ out(
216
+ renderSummary({
217
+ automationName: payload.automation.name,
218
+ displayDir,
219
+ rootDir,
220
+ cwd: options.cwd ?? process.cwd(),
221
+ files,
222
+ result,
223
+ })
224
+ );
225
+ return 0;
226
+ }
227
+
228
+ /**
229
+ * Converter files first (wx — existing files are kept), then the init
230
+ * scaffold fills in around them with the same skip-existing semantics.
231
+ */
232
+ async function writeProjectFiles(
233
+ rootDir: string,
234
+ result: ConvertWorkflowResult
235
+ ): Promise<InitFileEntry[]> {
236
+ const entries: InitFileEntry[] = [];
237
+ const converterPaths = new Set<string>();
238
+ await mkdir(rootDir, { recursive: true });
239
+ for (const file of result.files) {
240
+ converterPaths.add(file.path);
241
+ const abs = join(rootDir, file.path);
242
+ await mkdir(dirname(abs), { recursive: true });
243
+ try {
244
+ await writeFile(abs, file.contents, { flag: "wx" });
245
+ entries.push({ path: file.path, action: "create" });
246
+ } catch (error) {
247
+ if (!isEexist(error)) {
248
+ throw error;
249
+ }
250
+ entries.push({ path: file.path, action: "exist" });
251
+ }
252
+ }
253
+
254
+ const init = await initProject({ dir: rootDir });
255
+
256
+ // Scaffold files that make sense in a fresh `init` but not in an exported
257
+ // automation: the default memory hook (only when the automation had memory
258
+ // enabled — the converter emits its own copy then) and the echo demo tool.
259
+ const dropPaths = new Set<string>();
260
+ for (const scaffoldPath of ["agent/hooks/memory.ts", "agent/tools/echo.ts"]) {
261
+ if (
262
+ !converterPaths.has(scaffoldPath) &&
263
+ init.created.includes(scaffoldPath)
264
+ ) {
265
+ dropPaths.add(scaffoldPath);
266
+ await rm(join(rootDir, scaffoldPath));
267
+ }
268
+ }
269
+ for (const entry of init.files) {
270
+ if (converterPaths.has(entry.path) || dropPaths.has(entry.path)) {
271
+ continue;
272
+ }
273
+ entries.push(entry);
274
+ }
275
+ return entries;
276
+ }
277
+
278
+ function renderSummary(args: {
279
+ automationName: string;
280
+ displayDir: string;
281
+ rootDir: string;
282
+ cwd: string;
283
+ files: InitFileEntry[];
284
+ result: ConvertWorkflowResult;
285
+ }): string {
286
+ const c = stdoutPalette();
287
+ const lines: string[] = [];
288
+ lines.push("");
289
+ lines.push(`Converted "${args.automationName}" → ${args.displayDir}`);
290
+ lines.push("");
291
+ let anyExisting = false;
292
+ for (const file of args.files) {
293
+ if (file.path.endsWith(".gitkeep")) {
294
+ continue;
295
+ }
296
+ anyExisting ||= file.action === "exist";
297
+ lines.push(c.dim(`${file.action.padStart(12)} ${file.path}`));
298
+ }
299
+ if (anyExisting) {
300
+ lines.push(c.dim(`\n "exist" files already existed and were kept as-is.`));
301
+ }
302
+
303
+ const warnings = args.result.warnings;
304
+ if (warnings.length > 0) {
305
+ lines.push("");
306
+ lines.push("Warnings:");
307
+ for (const warning of warnings) {
308
+ lines.push(` - ${warning}`);
309
+ }
310
+ }
311
+
312
+ if (args.result.setupSteps.length > 0) {
313
+ lines.push("");
314
+ lines.push("Setup steps:");
315
+ for (const [index, step] of args.result.setupSteps.entries()) {
316
+ lines.push(` ${index + 1}. ${step}`);
317
+ }
318
+ }
319
+
320
+ if (args.result.mcpConnections.length > 0) {
321
+ lines.push("");
322
+ lines.push("MCP connections:");
323
+ for (const connection of args.result.mcpConnections) {
324
+ lines.push(` - ${connection.name}: ${connectionStatus(connection)}`);
325
+ }
326
+ }
327
+
328
+ lines.push("");
329
+ lines.push("Next steps:");
330
+ const cd = formatCdCommand(args.cwd, args.rootDir);
331
+ const steps = [
332
+ ...(cd === undefined ? [] : [cd]),
333
+ `${CLI} validate`,
334
+ `${CLI} dev`,
335
+ ];
336
+ for (const [index, step] of steps.entries()) {
337
+ lines.push(` ${index + 1}. ${step}`);
338
+ }
339
+ lines.push("");
340
+ return `${lines.join("\n")}\n`;
341
+ }
342
+
343
+ function connectionStatus(connection: ConvertedMcpConnection): string {
344
+ if (connection.kind === "cursorAccount") {
345
+ return "resolved from your Cursor account at runtime — manage authorization in Cursor (dashboard → MCP)";
346
+ }
347
+ return "needs manual setup — see the stub file and warnings above";
348
+ }
349
+
350
+ function describe(error: unknown): string {
351
+ return error instanceof Error ? error.message : String(error);
352
+ }
@@ -1345,7 +1345,7 @@ function promptHidden(
1345
1345
  }
1346
1346
 
1347
1347
  /** Buffered line reader over a piped stream; undefined once it ends. */
1348
- function createLineReader(
1348
+ export function createLineReader(
1349
1349
  stream: NodeJS.ReadableStream
1350
1350
  ): () => Promise<string | undefined> {
1351
1351
  let buffer = "";
@@ -96,30 +96,77 @@ export async function cmdMcpOauth(
96
96
  return 1;
97
97
  }
98
98
 
99
- out(
100
- `${LOG_PREFIX} Authorizing "${options.connection}" (${transport.url})…\n`
101
- );
99
+ const flow = await authorizeHostMcpOauth({
100
+ connection: options.connection,
101
+ url: transport.url,
102
+ type: transport.type === "sse" ? "sse" : "http",
103
+ headers: transport.headers,
104
+ out,
105
+ err,
106
+ });
107
+ if (!flow.ok) {
108
+ return 1;
109
+ }
110
+
111
+ if (options.store !== true) {
112
+ return 0;
113
+ }
114
+ return storeOauthSecrets({
115
+ connection: options.connection,
116
+ dir: options.dir,
117
+ slug: options.slug,
118
+ team: options.team,
119
+ apiKey: options.apiKey,
120
+ tokens: flow.tokens,
121
+ clientId: flow.clientId,
122
+ err,
123
+ out,
124
+ });
125
+ }
126
+
127
+ export interface HostMcpOauthArgs {
128
+ /** Connection name — the local token-store identifier. */
129
+ connection: string;
130
+ url: string;
131
+ type?: "http" | "sse";
132
+ headers?: Record<string, string>;
133
+ out: (text: string) => void;
134
+ err: (text: string) => void;
135
+ }
136
+
137
+ export type HostMcpOauthResult =
138
+ | { ok: true; tokens: OAuthTokens | undefined; clientId: string | undefined }
139
+ | { ok: false };
140
+
141
+ /**
142
+ * The interactive host-OAuth browser flow against an MCP server URL. Takes
143
+ * the transport metadata directly so callers that already hold it never have
144
+ * to load the authored project to authorize.
145
+ */
146
+ export async function authorizeHostMcpOauth(
147
+ args: HostMcpOauthArgs
148
+ ): Promise<HostMcpOauthResult> {
149
+ const { connection, out, err } = args;
150
+ out(`${LOG_PREFIX} Authorizing "${connection}" (${args.url})…\n`);
102
151
 
103
152
  let authorizationUrl: URL | undefined;
104
153
  const provider = await createHostMcpOAuthProvider({
105
- identifier: options.connection,
106
- resourceUrl: transport.url,
154
+ identifier: connection,
155
+ resourceUrl: args.url,
107
156
  onRedirect: (url) => {
108
157
  authorizationUrl = url;
109
158
  },
110
159
  });
111
160
 
112
161
  const client = new Client({ name: "agent-sdk-mcp-oauth", version: "0.1.0" });
113
- const url = new URL(transport.url);
162
+ const url = new URL(args.url);
114
163
  const requestInit =
115
- transport.headers === undefined
116
- ? undefined
117
- : { headers: transport.headers };
164
+ args.headers === undefined ? undefined : { headers: args.headers };
118
165
 
119
166
  const makeTransport = ():
120
167
  | StreamableHTTPClientTransport
121
168
  | SSEClientTransport => {
122
- if (transport.type === "sse") {
169
+ if (args.type === "sse") {
123
170
  return new SSEClientTransport(url, {
124
171
  authProvider: provider,
125
172
  requestInit,
@@ -135,15 +182,15 @@ export async function cmdMcpOauth(
135
182
  let authorized = false;
136
183
  try {
137
184
  await client.connect(mcpTransport);
138
- out(`${LOG_PREFIX} "${options.connection}" already authorized.\n`);
185
+ out(`${LOG_PREFIX} "${connection}" already authorized.\n`);
139
186
  await client.close().catch(() => undefined);
140
187
  authorized = true;
141
188
  } catch (error) {
142
189
  if (!(error instanceof UnauthorizedError)) {
143
190
  err(
144
- `${LOG_PREFIX} Failed to connect to "${options.connection}": ${describe(error)}\n`
191
+ `${LOG_PREFIX} Failed to connect to "${connection}": ${describe(error)}\n`
145
192
  );
146
- return 1;
193
+ return { ok: false };
147
194
  }
148
195
  }
149
196
 
@@ -153,7 +200,7 @@ export async function cmdMcpOauth(
153
200
  err(
154
201
  `${LOG_PREFIX} Auth required but no authorization URL was provided.\n`
155
202
  );
156
- return 1;
203
+ return { ok: false };
157
204
  }
158
205
 
159
206
  out(`${LOG_PREFIX} Open if needed:\n ${redirect.toString()}\n`);
@@ -165,14 +212,14 @@ export async function cmdMcpOauth(
165
212
  callback = await waitForMcpOAuthCallback();
166
213
  } catch (error) {
167
214
  err(`${LOG_PREFIX} OAuth callback failed: ${describe(error)}\n`);
168
- return 1;
215
+ return { ok: false };
169
216
  }
170
217
 
171
218
  try {
172
219
  await mcpTransport.finishAuth(callback.code);
173
220
  } catch (error) {
174
221
  err(`${LOG_PREFIX} Token exchange failed: ${describe(error)}\n`);
175
- return 1;
222
+ return { ok: false };
176
223
  }
177
224
 
178
225
  mcpTransport = makeTransport();
@@ -180,31 +227,22 @@ export async function cmdMcpOauth(
180
227
  await client.connect(mcpTransport);
181
228
  const tools = await client.listTools();
182
229
  out(
183
- `${LOG_PREFIX} Authorized "${options.connection}" (${tools.tools.length} tools). Local tokens in the agent-serve config dir.\n`
230
+ `${LOG_PREFIX} Authorized "${connection}" (${tools.tools.length} tools). Local tokens in the agent-serve config dir.\n`
184
231
  );
185
232
  await client.close().catch(() => undefined);
186
233
  } catch (error) {
187
234
  err(
188
- `${LOG_PREFIX} Reconnect failed: ${describe(error)}. Re-run \`${CLI} mcp oauth ${options.connection}\`.\n`
235
+ `${LOG_PREFIX} Reconnect failed: ${describe(error)}. Re-run \`${CLI} mcp oauth ${connection}\`.\n`
189
236
  );
190
- return 1;
237
+ return { ok: false };
191
238
  }
192
239
  }
193
240
 
194
- if (options.store !== true) {
195
- return 0;
196
- }
197
- return storeOauthSecrets({
198
- connection: options.connection,
199
- dir: options.dir,
200
- slug: options.slug,
201
- team: options.team,
202
- apiKey: options.apiKey,
241
+ return {
242
+ ok: true,
203
243
  tokens: provider.tokens(),
204
244
  clientId: provider.clientInformation()?.client_id,
205
- err,
206
- out,
207
- });
245
+ };
208
246
  }
209
247
 
210
248
  /**