@cursor/july 0.1.9 → 0.1.11

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 (200) hide show
  1. package/AGENTS.md +1 -0
  2. package/dist/bin/agent-serve.js +26 -3
  3. package/dist/channels/github/github-channel.d.ts.map +1 -1
  4. package/dist/channels/github/github-channel.js +2 -1
  5. package/dist/channels/github/types.d.ts +4 -0
  6. package/dist/channels/github/types.d.ts.map +1 -1
  7. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  8. package/dist/channels/slack/cursor-account.js +1 -1
  9. package/dist/channels/slack/dispatch.d.ts +13 -0
  10. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  11. package/dist/channels/slack/dispatch.js +36 -11
  12. package/dist/channels/slack/eval-directive.d.ts +63 -0
  13. package/dist/channels/slack/eval-directive.d.ts.map +1 -0
  14. package/dist/channels/slack/eval-directive.js +283 -0
  15. package/dist/channels/slack/index.d.ts +1 -0
  16. package/dist/channels/slack/index.d.ts.map +1 -1
  17. package/dist/channels/slack/index.js +1 -0
  18. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  19. package/dist/channels/slack/slack-channel.js +4 -2
  20. package/dist/channels/slack/types.d.ts +7 -0
  21. package/dist/channels/slack/types.d.ts.map +1 -1
  22. package/dist/connections.d.ts +10 -5
  23. package/dist/connections.d.ts.map +1 -1
  24. package/dist/connections.js +10 -5
  25. package/dist/docs/404.html +2 -2
  26. package/dist/docs/ab.html +4 -4
  27. package/dist/docs/assets/{app.Ciosf24M.js → app.BcXUGca7.js} +1 -1
  28. package/dist/docs/assets/chunks/@localSearchIndexroot.BIbvK8M1.js +1 -0
  29. package/dist/docs/assets/chunks/{VPLocalSearchBox.BcRDly7n.js → VPLocalSearchBox.CWur6XGZ.js} +1 -1
  30. package/dist/docs/assets/chunks/{theme.CMpbjdfx.js → theme.BnvulOX2.js} +2 -2
  31. package/dist/docs/assets/{deployment.md.DC5-ImcM.js → deployment.md.TecHo0_2.js} +3 -3
  32. package/dist/docs/assets/{deployment.md.DC5-ImcM.lean.js → deployment.md.TecHo0_2.lean.js} +1 -1
  33. package/dist/docs/assets/guides_mcp-oauth.md.DqmEOQY1.js +29 -0
  34. package/dist/docs/assets/guides_mcp-oauth.md.DqmEOQY1.lean.js +1 -0
  35. package/dist/docs/assets/{index.md.COiu-1jL.js → index.md.t0TM2Qzz.js} +3 -3
  36. package/dist/docs/assets/{index.md.COiu-1jL.lean.js → index.md.t0TM2Qzz.lean.js} +1 -1
  37. package/dist/docs/assets/{reference_cli.md.D189RBCH.js → reference_cli.md.B74NrVzb.js} +5 -4
  38. package/dist/docs/assets/{reference_cli.md.D189RBCH.lean.js → reference_cli.md.B74NrVzb.lean.js} +1 -1
  39. package/dist/docs/assets/{reference_connections.md.zaEYCLHT.js → reference_connections.md.CsSr3rDz.js} +9 -4
  40. package/dist/docs/assets/{reference_connections.md.zaEYCLHT.lean.js → reference_connections.md.CsSr3rDz.lean.js} +1 -1
  41. package/dist/docs/assets/reference_prompt.md.BaiweQxE.js +1 -0
  42. package/dist/docs/assets/reference_prompt.md.BaiweQxE.lean.js +1 -0
  43. package/dist/docs/assets/{reference_tools.md.CqgJroI0.js → reference_tools.md.BRSDnTbN.js} +16 -3
  44. package/dist/docs/assets/{reference_tools.md.CqgJroI0.lean.js → reference_tools.md.BRSDnTbN.lean.js} +1 -1
  45. package/dist/docs/assets/{troubleshooting.md.DkovjS7h.js → troubleshooting.md.CmQkmnzC.js} +1 -1
  46. package/dist/docs/assets/{troubleshooting.md.DkovjS7h.lean.js → troubleshooting.md.CmQkmnzC.lean.js} +1 -1
  47. package/dist/docs/building-with-agents.html +4 -4
  48. package/dist/docs/concepts.html +4 -4
  49. package/dist/docs/deployment.html +7 -7
  50. package/dist/docs/evals.html +4 -4
  51. package/dist/docs/example-agents/approval-buddy.html +4 -4
  52. package/dist/docs/example-agents/benny.html +4 -4
  53. package/dist/docs/example-agents/bugbot.html +4 -4
  54. package/dist/docs/example-agents/codebase-wiki.html +4 -4
  55. package/dist/docs/example-agents/codeowners-review.html +4 -4
  56. package/dist/docs/example-agents/concierge.html +4 -4
  57. package/dist/docs/example-agents/fsd.html +4 -4
  58. package/dist/docs/example-agents/index.html +4 -4
  59. package/dist/docs/example-agents/knowledge-base.html +4 -4
  60. package/dist/docs/example-agents/oncall.html +4 -4
  61. package/dist/docs/example-agents/security-reviewer.html +4 -4
  62. package/dist/docs/example-agents/slack-agent.html +4 -4
  63. package/dist/docs/example-agents/weather-agent.html +4 -4
  64. package/dist/docs/guides/agent-to-agent.html +4 -4
  65. package/dist/docs/guides/cloud-runtime.html +4 -4
  66. package/dist/docs/guides/github.html +4 -4
  67. package/dist/docs/guides/human-in-the-loop.html +4 -4
  68. package/dist/docs/guides/mcp-oauth.html +53 -0
  69. package/dist/docs/guides/slack.html +4 -4
  70. package/dist/docs/guides/webhooks.html +4 -4
  71. package/dist/docs/hashmap.json +1 -1
  72. package/dist/docs/hillclimbing.html +4 -4
  73. package/dist/docs/index.html +6 -6
  74. package/dist/docs/quickstart.html +4 -4
  75. package/dist/docs/reference/agent-config.html +4 -4
  76. package/dist/docs/reference/channels.html +4 -4
  77. package/dist/docs/reference/cli.html +8 -7
  78. package/dist/docs/reference/connections.html +13 -8
  79. package/dist/docs/reference/hooks.html +4 -4
  80. package/dist/docs/reference/http-api.html +4 -4
  81. package/dist/docs/reference/instructions.html +4 -4
  82. package/dist/docs/reference/playground.html +4 -4
  83. package/dist/docs/reference/project-layout.html +4 -4
  84. package/dist/docs/reference/prompt.html +33 -0
  85. package/dist/docs/reference/schedules.html +4 -4
  86. package/dist/docs/reference/sessions.html +4 -4
  87. package/dist/docs/reference/skills.html +5 -5
  88. package/dist/docs/reference/subagents.html +4 -4
  89. package/dist/docs/reference/tools.html +20 -7
  90. package/dist/docs/scaffolding-agents.html +4 -4
  91. package/dist/docs/storage.html +4 -4
  92. package/dist/docs/troubleshooting.html +5 -5
  93. package/dist/evals.d.ts +39 -0
  94. package/dist/evals.d.ts.map +1 -1
  95. package/dist/index.d.ts +3 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +2 -1
  98. package/dist/internal/cli-ax.d.ts.map +1 -1
  99. package/dist/internal/cli-ax.js +7 -4
  100. package/dist/internal/cli-mcp-oauth.d.ts +16 -0
  101. package/dist/internal/cli-mcp-oauth.d.ts.map +1 -0
  102. package/dist/internal/cli-mcp-oauth.js +194 -0
  103. package/dist/internal/cli-mcp.d.ts +4 -1
  104. package/dist/internal/cli-mcp.d.ts.map +1 -1
  105. package/dist/internal/cli-mcp.js +19 -1
  106. package/dist/internal/corp-ca.d.ts +9 -0
  107. package/dist/internal/corp-ca.d.ts.map +1 -0
  108. package/dist/internal/corp-ca.js +44 -0
  109. package/dist/internal/cursor/account-mcp.d.ts +17 -4
  110. package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
  111. package/dist/internal/cursor/account-mcp.js +105 -17
  112. package/dist/internal/discovery.d.ts.map +1 -1
  113. package/dist/internal/discovery.js +64 -10
  114. package/dist/internal/distribution.d.ts.map +1 -1
  115. package/dist/internal/distribution.js +1 -0
  116. package/dist/internal/logs-client.js +1 -1
  117. package/dist/internal/mcp-host.d.ts.map +1 -1
  118. package/dist/internal/mcp-host.js +49 -26
  119. package/dist/internal/mcp-oauth.d.ts +88 -0
  120. package/dist/internal/mcp-oauth.d.ts.map +1 -0
  121. package/dist/internal/mcp-oauth.js +385 -0
  122. package/dist/internal/resolved-connections.d.ts +6 -0
  123. package/dist/internal/resolved-connections.d.ts.map +1 -1
  124. package/dist/internal/resolved-connections.js +10 -3
  125. package/dist/internal/sdk-runner.d.ts +9 -0
  126. package/dist/internal/sdk-runner.d.ts.map +1 -1
  127. package/dist/internal/sdk-runner.js +28 -2
  128. package/dist/internal/server.d.ts.map +1 -1
  129. package/dist/internal/server.js +24 -6
  130. package/dist/internal/session-engine.d.ts +30 -10
  131. package/dist/internal/session-engine.d.ts.map +1 -1
  132. package/dist/internal/session-engine.js +166 -74
  133. package/dist/internal/sessions-client.d.ts +16 -2
  134. package/dist/internal/sessions-client.d.ts.map +1 -1
  135. package/dist/internal/sessions-client.js +30 -13
  136. package/dist/internal/storage-coordinator.d.ts +11 -0
  137. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  138. package/dist/internal/storage-coordinator.js +30 -0
  139. package/dist/internal/stream-progress.d.ts.map +1 -1
  140. package/dist/internal/stream-progress.js +8 -4
  141. package/dist/internal/turn-governor.d.ts +54 -0
  142. package/dist/internal/turn-governor.d.ts.map +1 -0
  143. package/dist/internal/turn-governor.js +111 -0
  144. package/dist/playground/assets/{index-C0-QEG4O.js → index-BIGLubwK.js} +46 -46
  145. package/dist/playground/assets/{index-z3JY9O2x.css → index-C17ZQKDG.css} +1 -1
  146. package/dist/playground/index.html +2 -2
  147. package/dist/prompt.d.ts +39 -0
  148. package/dist/prompt.d.ts.map +1 -0
  149. package/dist/prompt.js +70 -0
  150. package/dist/storage-backends/cursor-hosted.d.ts +10 -0
  151. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
  152. package/dist/storage-backends/cursor-hosted.js +14 -0
  153. package/dist/types.d.ts +93 -5
  154. package/dist/types.d.ts.map +1 -1
  155. package/dist/types.js +6 -0
  156. package/docs/README.md +4 -0
  157. package/docs/deployment.md +6 -1
  158. package/docs/guides/mcp-oauth.md +157 -0
  159. package/docs/reference/cli.md +30 -0
  160. package/docs/reference/connections.md +32 -2
  161. package/docs/reference/prompt.md +47 -0
  162. package/docs/reference/tools.md +21 -0
  163. package/docs/troubleshooting.md +11 -0
  164. package/package.json +9 -2
  165. package/skills/create-agent/SKILL.md +7 -3
  166. package/skills/framework-map/SKILL.md +1 -0
  167. package/skills/mcp-auth/SKILL.md +138 -0
  168. package/src/bin/agent-serve.ts +30 -3
  169. package/src/channels/github/github-channel.ts +2 -0
  170. package/src/channels/github/types.ts +4 -0
  171. package/src/channels/slack/cursor-account.ts +2 -0
  172. package/src/channels/slack/dispatch.ts +49 -0
  173. package/src/channels/slack/eval-directive.ts +348 -0
  174. package/src/channels/slack/index.ts +1 -0
  175. package/src/channels/slack/slack-channel.ts +3 -0
  176. package/src/channels/slack/types.ts +7 -0
  177. package/src/connections.ts +10 -5
  178. package/src/evals.ts +43 -0
  179. package/src/index.ts +3 -0
  180. package/src/internal/cli-ax.ts +10 -4
  181. package/src/internal/cli-mcp-oauth.ts +254 -0
  182. package/src/internal/cli-mcp.ts +25 -2
  183. package/src/internal/cursor/account-mcp.ts +153 -41
  184. package/src/internal/discovery.ts +70 -8
  185. package/src/internal/distribution.ts +1 -0
  186. package/src/internal/logs-client.ts +1 -1
  187. package/src/internal/mcp-host.ts +33 -9
  188. package/src/internal/mcp-oauth.ts +496 -0
  189. package/src/internal/resolved-connections.ts +20 -5
  190. package/src/internal/sdk-runner.ts +37 -1
  191. package/src/internal/server.ts +28 -6
  192. package/src/internal/session-engine.ts +192 -76
  193. package/src/internal/sessions-client.ts +51 -15
  194. package/src/internal/storage-coordinator.ts +36 -0
  195. package/src/internal/stream-progress.ts +8 -0
  196. package/src/internal/turn-governor.ts +145 -0
  197. package/src/prompt.ts +83 -0
  198. package/src/storage-backends/cursor-hosted.ts +21 -0
  199. package/src/types.ts +92 -6
  200. package/dist/docs/assets/chunks/@localSearchIndexroot.D154Detj.js +0 -1
@@ -0,0 +1,496 @@
1
+ /**
2
+ * Host-side MCP OAuth for `defineConnection({ url, oauth: true })`.
3
+ * Local: mcp-auth.json. Prod secrets: `mcp oauth <name> --store`.
4
+ */
5
+
6
+ import { execFile } from "node:child_process";
7
+ import { randomUUID } from "node:crypto";
8
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
9
+ import { createServer, type Server } from "node:http";
10
+ import { dirname, join } from "node:path";
11
+ import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
12
+ import type {
13
+ OAuthClientInformationFull,
14
+ OAuthClientMetadata,
15
+ OAuthTokens,
16
+ } from "@modelcontextprotocol/sdk/shared/auth.js";
17
+ import { configDirPath } from "./cursor/credentials.js";
18
+
19
+ export const MCP_OAUTH_CALLBACK_PORT = 8787;
20
+ export const MCP_OAUTH_CALLBACK_PATH = "/callback";
21
+ export const MCP_OAUTH_CALLBACK_URL: string = `http://localhost:${MCP_OAUTH_CALLBACK_PORT}${MCP_OAUTH_CALLBACK_PATH}`;
22
+
23
+ /** `anytool` → `MCP_OAUTH_ANYTOOL`. */
24
+ export function mcpOauthEnvPrefix(connection: string): string {
25
+ const key = connection
26
+ .trim()
27
+ .toUpperCase()
28
+ .replace(/[^A-Z0-9]+/g, "_")
29
+ .replace(/^_+|_+$/g, "");
30
+ return `MCP_OAUTH_${key.length > 0 ? key : "CONNECTION"}`;
31
+ }
32
+
33
+ export type McpOauthSecretNames = {
34
+ accessToken: string;
35
+ refreshToken: string;
36
+ clientId: string;
37
+ };
38
+
39
+ export function mcpOauthSecretNames(connection: string): McpOauthSecretNames {
40
+ const prefix = mcpOauthEnvPrefix(connection);
41
+ return {
42
+ accessToken: `${prefix}_ACCESS_TOKEN`,
43
+ refreshToken: `${prefix}_REFRESH_TOKEN`,
44
+ clientId: `${prefix}_CLIENT_ID`,
45
+ };
46
+ }
47
+
48
+ export function tokensFromEnv(
49
+ connection: string,
50
+ env: NodeJS.ProcessEnv = process.env
51
+ ): OAuthTokens | undefined {
52
+ const names = mcpOauthSecretNames(connection);
53
+ const access = env[names.accessToken]?.trim();
54
+ if (access === undefined || access === "") {
55
+ return undefined;
56
+ }
57
+ const refresh = env[names.refreshToken]?.trim();
58
+ return {
59
+ access_token: access,
60
+ refresh_token:
61
+ refresh === undefined || refresh === "" ? undefined : refresh,
62
+ token_type: "Bearer",
63
+ };
64
+ }
65
+
66
+ export function clientIdFromEnv(
67
+ connection: string,
68
+ env: NodeJS.ProcessEnv = process.env
69
+ ): OAuthClientInformationFull | undefined {
70
+ const clientId = env[mcpOauthSecretNames(connection).clientId]?.trim();
71
+ if (clientId === undefined || clientId === "") {
72
+ return undefined;
73
+ }
74
+ return {
75
+ client_id: clientId,
76
+ redirect_uris: [MCP_OAUTH_CALLBACK_URL],
77
+ };
78
+ }
79
+
80
+ interface AuthEntry {
81
+ /** MCP resource URL this entry was issued for (origin + path, no query). */
82
+ resourceUrl?: string;
83
+ tokens?: OAuthTokens;
84
+ clientInfo?: OAuthClientInformationFull;
85
+ }
86
+
87
+ type AuthFile = Record<string, AuthEntry>;
88
+
89
+ const locks = new Map<string, Promise<void>>();
90
+
91
+ /** Sibling of `credentials.json` under `~/.config/agent-serve/`. */
92
+ export function defaultMcpAuthPath(): string {
93
+ return join(configDirPath(), "mcp-auth.json");
94
+ }
95
+
96
+ /** Canonical MCP resource key for binding stored tokens to a server URL. */
97
+ export function normalizeMcpResourceUrl(url: string): string {
98
+ const parsed = new URL(url);
99
+ parsed.hash = "";
100
+ parsed.search = "";
101
+ // Drop default ports so http://h:80 and http://h match.
102
+ if (
103
+ (parsed.protocol === "https:" && parsed.port === "443") ||
104
+ (parsed.protocol === "http:" && parsed.port === "80")
105
+ ) {
106
+ parsed.port = "";
107
+ }
108
+ let path = parsed.pathname;
109
+ if (path.length > 1 && path.endsWith("/")) {
110
+ path = path.slice(0, -1);
111
+ }
112
+ return `${parsed.origin}${path === "/" ? "" : path}`;
113
+ }
114
+
115
+ export class FileMcpOAuthStore {
116
+ private cache: AuthFile | undefined;
117
+ private loading: Promise<AuthFile> | undefined;
118
+
119
+ constructor(readonly path: string = defaultMcpAuthPath()) {}
120
+
121
+ async loadTokens(
122
+ id: string,
123
+ resourceUrl: string
124
+ ): Promise<OAuthTokens | undefined> {
125
+ return (await this.entryFor(id, resourceUrl))?.tokens;
126
+ }
127
+
128
+ async loadClientInformation(
129
+ id: string,
130
+ resourceUrl: string
131
+ ): Promise<OAuthClientInformationFull | undefined> {
132
+ return (await this.entryFor(id, resourceUrl))?.clientInfo;
133
+ }
134
+
135
+ async saveTokens(
136
+ id: string,
137
+ resourceUrl: string,
138
+ tokens: OAuthTokens
139
+ ): Promise<void> {
140
+ await this.update(id, resourceUrl, (entry) => {
141
+ entry.tokens = tokens;
142
+ });
143
+ }
144
+
145
+ async saveClientInformation(
146
+ id: string,
147
+ resourceUrl: string,
148
+ clientInfo: OAuthClientInformationFull
149
+ ): Promise<void> {
150
+ await this.update(id, resourceUrl, (entry) => {
151
+ entry.clientInfo = clientInfo;
152
+ });
153
+ }
154
+
155
+ async deleteCredentials(id: string): Promise<void> {
156
+ await this.withLock(async () => {
157
+ const data = await this.read();
158
+ delete data[id];
159
+ await this.write(data);
160
+ });
161
+ }
162
+
163
+ private async entryFor(
164
+ id: string,
165
+ resourceUrl: string
166
+ ): Promise<AuthEntry | undefined> {
167
+ const entry = (await this.read())[id];
168
+ if (entry === undefined) {
169
+ return undefined;
170
+ }
171
+ const bound = entry.resourceUrl;
172
+ if (bound === undefined || bound !== normalizeMcpResourceUrl(resourceUrl)) {
173
+ return undefined;
174
+ }
175
+ return entry;
176
+ }
177
+
178
+ private async update(
179
+ id: string,
180
+ resourceUrl: string,
181
+ mutate: (entry: AuthEntry) => void
182
+ ): Promise<void> {
183
+ const normalized = normalizeMcpResourceUrl(resourceUrl);
184
+ await this.withLock(async () => {
185
+ const data = await this.read();
186
+ const existing = data[id];
187
+ const entry: AuthEntry =
188
+ existing?.resourceUrl === normalized ? { ...existing } : {};
189
+ entry.resourceUrl = normalized;
190
+ mutate(entry);
191
+ data[id] = entry;
192
+ await this.write(data);
193
+ });
194
+ }
195
+
196
+ private async read(): Promise<AuthFile> {
197
+ if (this.cache !== undefined) {
198
+ return this.cache;
199
+ }
200
+ this.loading ??= this.readDisk().then((data) => {
201
+ this.cache = data;
202
+ return data;
203
+ });
204
+ return this.loading;
205
+ }
206
+
207
+ private async readDisk(): Promise<AuthFile> {
208
+ try {
209
+ const parsed: unknown = JSON.parse(await readFile(this.path, "utf8"));
210
+ if (typeof parsed !== "object" || parsed === null) {
211
+ return {};
212
+ }
213
+ return parsed as AuthFile;
214
+ } catch (error: unknown) {
215
+ if (
216
+ typeof error === "object" &&
217
+ error !== null &&
218
+ "code" in error &&
219
+ (error as { code: unknown }).code === "ENOENT"
220
+ ) {
221
+ return {};
222
+ }
223
+ throw error;
224
+ }
225
+ }
226
+
227
+ private async write(data: AuthFile): Promise<void> {
228
+ this.cache = data;
229
+ await mkdir(dirname(this.path), { recursive: true, mode: 0o700 });
230
+ await writeFile(this.path, `${JSON.stringify(data, null, 2)}\n`, {
231
+ mode: 0o600,
232
+ });
233
+ await chmod(this.path, 0o600);
234
+ }
235
+
236
+ private async withLock<T>(fn: () => Promise<T>): Promise<T> {
237
+ const prev = locks.get(this.path) ?? Promise.resolve();
238
+ let release!: () => void;
239
+ locks.set(
240
+ this.path,
241
+ new Promise<void>((resolve) => {
242
+ release = resolve;
243
+ })
244
+ );
245
+ try {
246
+ await prev;
247
+ return await fn();
248
+ } finally {
249
+ release();
250
+ }
251
+ }
252
+ }
253
+
254
+ type ProviderArgs = {
255
+ identifier: string;
256
+ resourceUrl: string;
257
+ store: FileMcpOAuthStore;
258
+ redirectUrl?: string;
259
+ onRedirect?: (url: URL) => void;
260
+ tokens?: OAuthTokens;
261
+ clientInformation?: OAuthClientInformationFull;
262
+ };
263
+
264
+ export class HostMcpOAuthProvider implements OAuthClientProvider {
265
+ private tokensValue: OAuthTokens | undefined;
266
+ private clientInfo: OAuthClientInformationFull | undefined;
267
+ private verifier: string | undefined;
268
+ private redirect: URL | undefined;
269
+ private readonly meta: OAuthClientMetadata;
270
+ private readonly args: ProviderArgs;
271
+
272
+ constructor(args: ProviderArgs) {
273
+ this.args = args;
274
+ this.tokensValue = args.tokens;
275
+ this.clientInfo = args.clientInformation;
276
+ this.meta = {
277
+ redirect_uris: [args.redirectUrl ?? MCP_OAUTH_CALLBACK_URL],
278
+ token_endpoint_auth_method: "none",
279
+ grant_types: ["authorization_code", "refresh_token"],
280
+ response_types: ["code"],
281
+ client_name: "agentkit",
282
+ };
283
+ }
284
+
285
+ get redirectUrl(): string {
286
+ return this.args.redirectUrl ?? MCP_OAUTH_CALLBACK_URL;
287
+ }
288
+
289
+ get clientMetadata(): OAuthClientMetadata {
290
+ return this.meta;
291
+ }
292
+
293
+ get lastRedirectUrl(): URL | undefined {
294
+ return this.redirect;
295
+ }
296
+
297
+ state(): string {
298
+ return randomUUID();
299
+ }
300
+
301
+ clientInformation(): OAuthClientInformationFull | undefined {
302
+ return this.clientInfo;
303
+ }
304
+
305
+ async saveClientInformation(info: OAuthClientInformationFull): Promise<void> {
306
+ this.clientInfo = info;
307
+ await this.args.store.saveClientInformation(
308
+ this.args.identifier,
309
+ this.args.resourceUrl,
310
+ info
311
+ );
312
+ }
313
+
314
+ tokens(): OAuthTokens | undefined {
315
+ return this.tokensValue;
316
+ }
317
+
318
+ async saveTokens(tokens: OAuthTokens): Promise<void> {
319
+ this.tokensValue = tokens;
320
+ await this.args.store.saveTokens(
321
+ this.args.identifier,
322
+ this.args.resourceUrl,
323
+ tokens
324
+ );
325
+ }
326
+
327
+ redirectToAuthorization(url: URL): void {
328
+ this.redirect = url;
329
+ this.args.onRedirect?.(url);
330
+ }
331
+
332
+ async saveCodeVerifier(codeVerifier: string): Promise<void> {
333
+ this.verifier = codeVerifier;
334
+ }
335
+
336
+ codeVerifier(): string {
337
+ if (this.verifier === undefined) {
338
+ throw new Error("No PKCE code verifier saved for this OAuth attempt.");
339
+ }
340
+ return this.verifier;
341
+ }
342
+
343
+ async invalidateCredentials(
344
+ scope: "all" | "client" | "tokens" | "verifier"
345
+ ): Promise<void> {
346
+ if (scope === "tokens" || scope === "all") {
347
+ this.tokensValue = undefined;
348
+ }
349
+ if (scope === "client" || scope === "all") {
350
+ this.clientInfo = undefined;
351
+ }
352
+ if (scope === "verifier" || scope === "all") {
353
+ this.verifier = undefined;
354
+ this.redirect = undefined;
355
+ }
356
+ if (scope === "all") {
357
+ await this.args.store.deleteCredentials(this.args.identifier);
358
+ }
359
+ }
360
+ }
361
+
362
+ export async function createHostMcpOAuthProvider(args: {
363
+ identifier: string;
364
+ resourceUrl: string;
365
+ store?: FileMcpOAuthStore;
366
+ onRedirect?: (url: URL) => void;
367
+ }): Promise<HostMcpOAuthProvider> {
368
+ const store = args.store ?? new FileMcpOAuthStore();
369
+ const [fileTokens, fileClient] = await Promise.all([
370
+ store.loadTokens(args.identifier, args.resourceUrl),
371
+ store.loadClientInformation(args.identifier, args.resourceUrl),
372
+ ]);
373
+ return new HostMcpOAuthProvider({
374
+ identifier: args.identifier,
375
+ resourceUrl: args.resourceUrl,
376
+ store,
377
+ tokens: fileTokens ?? tokensFromEnv(args.identifier),
378
+ clientInformation: fileClient ?? clientIdFromEnv(args.identifier),
379
+ onRedirect: args.onRedirect,
380
+ });
381
+ }
382
+
383
+ export type OAuthCallbackResult = {
384
+ code: string;
385
+ state?: string;
386
+ };
387
+
388
+ export async function waitForMcpOAuthCallback(args?: {
389
+ port?: number;
390
+ path?: string;
391
+ timeoutMs?: number;
392
+ }): Promise<OAuthCallbackResult> {
393
+ const port = args?.port ?? MCP_OAUTH_CALLBACK_PORT;
394
+ const path = args?.path ?? MCP_OAUTH_CALLBACK_PATH;
395
+ const timeoutMs = args?.timeoutMs ?? 5 * 60 * 1000;
396
+
397
+ return new Promise((resolve, reject) => {
398
+ let done = false;
399
+ const finish = (
400
+ result: OAuthCallbackResult | undefined,
401
+ error?: Error
402
+ ): void => {
403
+ if (done) {
404
+ return;
405
+ }
406
+ done = true;
407
+ clearTimeout(timer);
408
+ server.close();
409
+ if (error !== undefined) {
410
+ reject(error);
411
+ return;
412
+ }
413
+ if (result !== undefined) {
414
+ resolve(result);
415
+ }
416
+ };
417
+
418
+ const server: Server = createServer((req, res) => {
419
+ if (req.url === "/favicon.ico") {
420
+ res.writeHead(404).end();
421
+ return;
422
+ }
423
+ const url = new URL(req.url ?? "", `http://localhost:${port}`);
424
+ if (url.pathname !== path) {
425
+ res.writeHead(404).end("Not found");
426
+ return;
427
+ }
428
+ const code = url.searchParams.get("code");
429
+ const oauthError = url.searchParams.get("error");
430
+ const state = url.searchParams.get("state") ?? undefined;
431
+ if (code) {
432
+ res
433
+ .writeHead(200, { "content-type": "text/html; charset=utf-8" })
434
+ .end(
435
+ "<!doctype html><html><body><h1>Authorized</h1><p>Return to agentkit.</p></body></html>"
436
+ );
437
+ finish({ code, state });
438
+ return;
439
+ }
440
+ res
441
+ .writeHead(400, { "content-type": "text/html; charset=utf-8" })
442
+ .end(
443
+ `<!doctype html><html><body><h1>Authorization failed</h1><p>${oauthError ?? "missing code"}</p></body></html>`
444
+ );
445
+ finish(
446
+ undefined,
447
+ new Error(`OAuth authorization failed: ${oauthError ?? "missing code"}`)
448
+ );
449
+ });
450
+
451
+ const timer = setTimeout(() => {
452
+ finish(
453
+ undefined,
454
+ new Error(
455
+ `Timed out waiting for OAuth callback on http://localhost:${port}${path}`
456
+ )
457
+ );
458
+ }, timeoutMs);
459
+
460
+ server.on("error", (error) => {
461
+ finish(
462
+ undefined,
463
+ new Error(
464
+ `OAuth callback port ${port} failed: ${
465
+ error instanceof Error ? error.message : String(error)
466
+ }`
467
+ )
468
+ );
469
+ });
470
+ server.listen(port, "127.0.0.1");
471
+ });
472
+ }
473
+
474
+ export function openBrowser(url: string): void {
475
+ const parsed = new URL(url);
476
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
477
+ throw new Error(
478
+ `Refusing to open non-http(s) authorization URL: ${parsed.protocol}`
479
+ );
480
+ }
481
+ const href = parsed.href;
482
+ if (process.platform === "darwin") {
483
+ execFile("open", [href], () => undefined);
484
+ return;
485
+ }
486
+ if (process.platform === "win32") {
487
+ // Avoid `cmd /c start` — authorize URLs with `&` are shell-metachar risks.
488
+ execFile(
489
+ "rundll32",
490
+ ["url.dll,FileProtocolHandler", href],
491
+ () => undefined
492
+ );
493
+ return;
494
+ }
495
+ execFile("xdg-open", [href], () => undefined);
496
+ }
@@ -35,8 +35,13 @@ export function validateMountConnectionsConfig(args: {
35
35
  return;
36
36
  }
37
37
  const label = args.slug === "" ? args.project.name : args.slug;
38
+ const hostOnlyNames = new Set(
39
+ agent.connections
40
+ .filter((connection) => connection.hostOnly === true)
41
+ .map((connection) => connection.name)
42
+ );
38
43
  const filteredRefs = args.accountRefs.filter(
39
- (ref) => ref.servers !== undefined
44
+ (ref) => ref.servers !== undefined && !hostOnlyNames.has(ref.name)
40
45
  );
41
46
  if (filteredRefs.length === 0) {
42
47
  return;
@@ -78,13 +83,23 @@ export function assertCloudCanHonorAccountServersFilters(args: {
78
83
  /** Fail closed when anonymous HTTP admission would expose account connectors. */
79
84
  export function assertAccountConnectionsNotAnonymous(args: {
80
85
  allowAnonymous?: boolean;
86
+ /**
87
+ * Opt in when every admitted caller is already trusted (e.g. behind an
88
+ * authenticating proxy / hosted alias token). Same trust model as
89
+ * `--allow-anonymous-cursor-github`.
90
+ */
91
+ allowAnonymousCursorAccountMcp?: boolean;
81
92
  hasAccountConnections: boolean;
82
93
  }): void {
83
- if (args.allowAnonymous === true && args.hasAccountConnections) {
84
- throw new Error(
85
- "[agentkit] --allow-anonymous cannot be used with Cursor account MCP connections (defineConnection({ cursorAccount: true })); use --bearer-token or remove the account MCP connections."
86
- );
94
+ if (args.allowAnonymous !== true || !args.hasAccountConnections) {
95
+ return;
87
96
  }
97
+ if (args.allowAnonymousCursorAccountMcp === true) {
98
+ return;
99
+ }
100
+ throw new Error(
101
+ "[agentkit] --allow-anonymous cannot be used with Cursor account MCP connections (defineConnection({ cursorAccount: true })); use --bearer-token, or opt in with --allow-anonymous-cursor-account-mcp behind a trusted boundary."
102
+ );
88
103
  }
89
104
 
90
105
  export function resolveMountConnections(args: {
@@ -42,6 +42,8 @@ export interface CursorSdkRunnerOptions {
42
42
  * `LocalAgentStore` for heavy multi-session concurrency.
43
43
  */
44
44
  store?: LocalAgentStore;
45
+ /** Diagnostics for degraded paths (resume-miss fallback). */
46
+ logger?: (line: string) => void;
45
47
  }
46
48
 
47
49
  export class CursorSdkRunner implements AgentRunner {
@@ -180,7 +182,24 @@ export class CursorSdkRunner implements AgentRunner {
180
182
  agentId: request.sdkAgentId ?? request.sessionId,
181
183
  });
182
184
  }
183
- return Agent.resume(request.sdkAgentId ?? request.sessionId, options);
185
+
186
+ const agentId = request.sdkAgentId ?? request.sessionId;
187
+ try {
188
+ return await Agent.resume(agentId, options);
189
+ } catch (error) {
190
+ if (!isLocalAgentNotFoundError(error, agentId)) {
191
+ throw error;
192
+ }
193
+ // Host replacement: the session record survived in durable storage,
194
+ // but the conversation store died with the old pod. A fresh
195
+ // conversation under the same id beats failing this thread's turns
196
+ // forever ("Agent … not found" on every follow-up). The channel's
197
+ // warm-turn prompt context re-hydrates the thread history.
198
+ (this.options.logger ?? (() => {}))(
199
+ `[agentkit] no stored conversation for ${agentId} on this host; starting a fresh one`
200
+ );
201
+ return Agent.create({ ...options, agentId });
202
+ }
184
203
  }
185
204
 
186
205
  private async openCloudAgent(
@@ -237,6 +256,23 @@ export function isServerMintedCloudAgentId(id: string): boolean {
237
256
  return id.startsWith("bc-");
238
257
  }
239
258
 
259
+ /**
260
+ * `Agent.resume` on a store with no row for the agent ("Agent <id> not
261
+ * found", surfaced by every built-in {@link LocalAgentStore} backend).
262
+ * Deliberately narrow: transient store/transport failures must propagate
263
+ * rather than silently fork the thread onto a fresh conversation.
264
+ */
265
+ export function isLocalAgentNotFoundError(
266
+ error: unknown,
267
+ agentId: string
268
+ ): boolean {
269
+ return (
270
+ error instanceof Error &&
271
+ error.message.includes(agentId) &&
272
+ /\bnot found\b/i.test(error.message)
273
+ );
274
+ }
275
+
240
276
  export function toModelSelection(model: ModelSetting): ModelSelection {
241
277
  if (typeof model === "string") {
242
278
  return { id: model };
@@ -49,7 +49,6 @@ import {
49
49
  } from "./cursor/account-mcp.js";
50
50
  import { CursorBackendClient } from "./cursor/backend-client.js";
51
51
  import {
52
- cursorBackendUrl,
53
52
  cursorExternalApiUrl,
54
53
  type ResolvedApiKey,
55
54
  resolveApiKey,
@@ -286,9 +285,10 @@ export async function startServer(
286
285
  }
287
286
  const runner =
288
287
  options.runner ??
289
- new CursorSdkRunner(
290
- accountKey === undefined ? {} : { apiKey: accountKey.apiKey }
291
- );
288
+ new CursorSdkRunner({
289
+ ...(accountKey === undefined ? {} : { apiKey: accountKey.apiKey }),
290
+ logger,
291
+ });
292
292
 
293
293
  const router = new Router();
294
294
  const runtimes: MountedRuntime[] = [];
@@ -307,8 +307,18 @@ export async function startServer(
307
307
  const accountRefsBySlug = collectCursorAccountConnections(mounts);
308
308
  assertAccountConnectionsNotAnonymous({
309
309
  allowAnonymous: options.allowAnonymous,
310
+ allowAnonymousCursorAccountMcp: options.allowAnonymousCursorAccountMcp,
310
311
  hasAccountConnections: accountRefsBySlug.size > 0,
311
312
  });
313
+ if (
314
+ options.allowAnonymous === true &&
315
+ options.allowAnonymousCursorAccountMcp === true &&
316
+ accountRefsBySlug.size > 0
317
+ ) {
318
+ logger(
319
+ "[agentkit] warning: --allow-anonymous-cursor-account-mcp — every admitted HTTP caller can drive Cursor account MCP connectors; serve this only behind a trusted boundary (e.g. an SSO proxy or hosted alias token)"
320
+ );
321
+ }
312
322
  let accountBridge: CursorAccountMcpBridge | undefined;
313
323
  if (accountRefsBySlug.size > 0) {
314
324
  if (accountKey === undefined) {
@@ -377,6 +387,12 @@ export async function startServer(
377
387
  reminderRunner.cancelForContinuation(token, reason),
378
388
  dispatch: (id) => reminderRunner.dispatch(id),
379
389
  });
390
+ engine.setEvals({
391
+ list: () => evalRuns.list(),
392
+ get: (runId) => evalRuns.get(runId),
393
+ listRunsWithActive: () => evalRuns.listRunsWithActive(),
394
+ start: (input) => evalRuns.start(input),
395
+ });
380
396
  const runtime: MountedRuntime = {
381
397
  slug,
382
398
  project,
@@ -914,8 +930,12 @@ function mountAgentRoutes(
914
930
  (relaxSessionReadAuthBeyondLoopback ||
915
931
  (typeof ctx.requestIp === "string" &&
916
932
  isLoopbackAddress(ctx.requestIp)));
933
+ const listed = await engine.listSessionsWithSource(ctx.auth, {
934
+ includeAll,
935
+ });
917
936
  return jsonResponse({
918
- sessions: await engine.listSessions(ctx.auth, { includeAll }),
937
+ sessions: listed.sessions,
938
+ source: listed.source,
919
939
  });
920
940
  },
921
941
  });
@@ -1595,7 +1615,9 @@ async function startCursorSlackRelay(input: {
1595
1615
  `This project has Slack channels using the Cursor account connection (slackChannel({ cursorAccount: true })) but the host is not signed in to Cursor. ${SIGN_IN_HINT}`
1596
1616
  );
1597
1617
  }
1598
- const baseUrl = accountKey.backendUrl ?? cursorBackendUrl();
1618
+ // Same host split as scm-events: `/v0/slack-events/*` is on api.cursor.com,
1619
+ // not api2 (accountKey.backendUrl 404s these routes).
1620
+ const baseUrl = cursorExternalApiUrl();
1599
1621
  const baseStateRoot =
1600
1622
  options.stateRoot ??
1601
1623
  join(targets[0].runtime.project.rootDir, ".agent-serve");