@cursor/july 0.1.10 → 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 (194) hide show
  1. package/AGENTS.md +1 -0
  2. package/dist/bin/agent-serve.js +17 -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/post-update-delivery.js +318 -0
  19. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  20. package/dist/channels/slack/slack-channel.js +4 -2
  21. package/dist/channels/slack/types.d.ts +7 -0
  22. package/dist/channels/slack/types.d.ts.map +1 -1
  23. package/dist/connections.d.ts +10 -5
  24. package/dist/connections.d.ts.map +1 -1
  25. package/dist/connections.js +10 -5
  26. package/dist/docs/404.html +2 -2
  27. package/dist/docs/ab.html +4 -4
  28. package/dist/docs/assets/{app.CY2buaEE.js → app.BcXUGca7.js} +1 -1
  29. package/dist/docs/assets/chunks/@localSearchIndexroot.BIbvK8M1.js +1 -0
  30. package/dist/docs/assets/chunks/{VPLocalSearchBox.Ix954SFJ.js → VPLocalSearchBox.CWur6XGZ.js} +1 -1
  31. package/dist/docs/assets/chunks/{theme.PDwuJbyi.js → theme.BnvulOX2.js} +2 -2
  32. package/dist/docs/assets/{deployment.md.DC5-ImcM.js → deployment.md.TecHo0_2.js} +3 -3
  33. package/dist/docs/assets/{deployment.md.DC5-ImcM.lean.js → deployment.md.TecHo0_2.lean.js} +1 -1
  34. package/dist/docs/assets/guides_mcp-oauth.md.DqmEOQY1.js +29 -0
  35. package/dist/docs/assets/guides_mcp-oauth.md.DqmEOQY1.lean.js +1 -0
  36. package/dist/docs/assets/{index.md.COiu-1jL.js → index.md.t0TM2Qzz.js} +3 -3
  37. package/dist/docs/assets/{index.md.COiu-1jL.lean.js → index.md.t0TM2Qzz.lean.js} +1 -1
  38. package/dist/docs/assets/{reference_cli.md.CjfiV2Q-.js → reference_cli.md.B74NrVzb.js} +3 -3
  39. package/dist/docs/assets/{reference_cli.md.CjfiV2Q-.lean.js → reference_cli.md.B74NrVzb.lean.js} +1 -1
  40. package/dist/docs/assets/{reference_connections.md.FbWupyzW.js → reference_connections.md.CsSr3rDz.js} +8 -3
  41. package/dist/docs/assets/{reference_connections.md.FbWupyzW.lean.js → reference_connections.md.CsSr3rDz.lean.js} +1 -1
  42. package/dist/docs/assets/reference_prompt.md.BaiweQxE.js +1 -0
  43. package/dist/docs/assets/reference_prompt.md.BaiweQxE.lean.js +1 -0
  44. package/dist/docs/assets/{reference_tools.md.CqgJroI0.js → reference_tools.md.BRSDnTbN.js} +16 -3
  45. package/dist/docs/assets/{reference_tools.md.CqgJroI0.lean.js → reference_tools.md.BRSDnTbN.lean.js} +1 -1
  46. package/dist/docs/assets/{troubleshooting.md.DkovjS7h.js → troubleshooting.md.CmQkmnzC.js} +1 -1
  47. package/dist/docs/assets/{troubleshooting.md.DkovjS7h.lean.js → troubleshooting.md.CmQkmnzC.lean.js} +1 -1
  48. package/dist/docs/building-with-agents.html +4 -4
  49. package/dist/docs/concepts.html +4 -4
  50. package/dist/docs/deployment.html +7 -7
  51. package/dist/docs/evals.html +4 -4
  52. package/dist/docs/example-agents/approval-buddy.html +4 -4
  53. package/dist/docs/example-agents/benny.html +4 -4
  54. package/dist/docs/example-agents/bugbot.html +4 -4
  55. package/dist/docs/example-agents/codebase-wiki.html +4 -4
  56. package/dist/docs/example-agents/codeowners-review.html +4 -4
  57. package/dist/docs/example-agents/concierge.html +4 -4
  58. package/dist/docs/example-agents/fsd.html +4 -4
  59. package/dist/docs/example-agents/index.html +4 -4
  60. package/dist/docs/example-agents/knowledge-base.html +4 -4
  61. package/dist/docs/example-agents/oncall.html +4 -4
  62. package/dist/docs/example-agents/security-reviewer.html +4 -4
  63. package/dist/docs/example-agents/slack-agent.html +4 -4
  64. package/dist/docs/example-agents/weather-agent.html +4 -4
  65. package/dist/docs/guides/agent-to-agent.html +4 -4
  66. package/dist/docs/guides/cloud-runtime.html +4 -4
  67. package/dist/docs/guides/github.html +4 -4
  68. package/dist/docs/guides/human-in-the-loop.html +4 -4
  69. package/dist/docs/guides/mcp-oauth.html +53 -0
  70. package/dist/docs/guides/slack.html +4 -4
  71. package/dist/docs/guides/webhooks.html +4 -4
  72. package/dist/docs/hashmap.json +1 -1
  73. package/dist/docs/hillclimbing.html +4 -4
  74. package/dist/docs/index.html +6 -6
  75. package/dist/docs/quickstart.html +4 -4
  76. package/dist/docs/reference/agent-config.html +4 -4
  77. package/dist/docs/reference/channels.html +4 -4
  78. package/dist/docs/reference/cli.html +6 -6
  79. package/dist/docs/reference/connections.html +12 -7
  80. package/dist/docs/reference/hooks.html +4 -4
  81. package/dist/docs/reference/http-api.html +4 -4
  82. package/dist/docs/reference/instructions.html +4 -4
  83. package/dist/docs/reference/playground.html +4 -4
  84. package/dist/docs/reference/project-layout.html +4 -4
  85. package/dist/docs/reference/prompt.html +33 -0
  86. package/dist/docs/reference/schedules.html +4 -4
  87. package/dist/docs/reference/sessions.html +4 -4
  88. package/dist/docs/reference/skills.html +5 -5
  89. package/dist/docs/reference/subagents.html +4 -4
  90. package/dist/docs/reference/tools.html +20 -7
  91. package/dist/docs/scaffolding-agents.html +4 -4
  92. package/dist/docs/storage.html +4 -4
  93. package/dist/docs/troubleshooting.html +5 -5
  94. package/dist/evals.d.ts +39 -0
  95. package/dist/evals.d.ts.map +1 -1
  96. package/dist/index.d.ts +3 -1
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +2 -1
  99. package/dist/internal/cli-mcp-oauth.d.ts +16 -0
  100. package/dist/internal/cli-mcp-oauth.d.ts.map +1 -0
  101. package/dist/internal/cli-mcp-oauth.js +194 -0
  102. package/dist/internal/cli-mcp.d.ts +4 -1
  103. package/dist/internal/cli-mcp.d.ts.map +1 -1
  104. package/dist/internal/cli-mcp.js +19 -1
  105. package/dist/internal/corp-ca.d.ts +9 -0
  106. package/dist/internal/corp-ca.d.ts.map +1 -0
  107. package/dist/internal/corp-ca.js +44 -0
  108. package/dist/internal/cursor/account-mcp.d.ts +17 -4
  109. package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
  110. package/dist/internal/cursor/account-mcp.js +105 -17
  111. package/dist/internal/discovery.d.ts.map +1 -1
  112. package/dist/internal/discovery.js +64 -10
  113. package/dist/internal/distribution.d.ts.map +1 -1
  114. package/dist/internal/distribution.js +1 -0
  115. package/dist/internal/json-dir-store.js +100 -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.map +1 -1
  123. package/dist/internal/resolved-connections.js +4 -1
  124. package/dist/internal/sdk-runner.d.ts +9 -0
  125. package/dist/internal/sdk-runner.d.ts.map +1 -1
  126. package/dist/internal/sdk-runner.js +28 -2
  127. package/dist/internal/server.d.ts.map +1 -1
  128. package/dist/internal/server.js +13 -5
  129. package/dist/internal/session-engine.d.ts +7 -9
  130. package/dist/internal/session-engine.d.ts.map +1 -1
  131. package/dist/internal/session-engine.js +96 -64
  132. package/dist/internal/sessions-client.d.ts.map +1 -1
  133. package/dist/internal/sessions-client.js +2 -1
  134. package/dist/internal/stream-progress.d.ts.map +1 -1
  135. package/dist/internal/stream-progress.js +8 -4
  136. package/dist/internal/turn-governor.d.ts +54 -0
  137. package/dist/internal/turn-governor.d.ts.map +1 -0
  138. package/dist/internal/turn-governor.js +111 -0
  139. package/dist/playground/assets/{index-C0-QEG4O.js → index-BIGLubwK.js} +46 -46
  140. package/dist/playground/assets/{index-z3JY9O2x.css → index-C17ZQKDG.css} +1 -1
  141. package/dist/playground/index.html +2 -2
  142. package/dist/prompt.d.ts +39 -0
  143. package/dist/prompt.d.ts.map +1 -0
  144. package/dist/prompt.js +70 -0
  145. package/dist/types.d.ts +78 -5
  146. package/dist/types.d.ts.map +1 -1
  147. package/dist/types.js +6 -0
  148. package/docs/.vitepress/cache/deps/package.json +3 -0
  149. package/docs/README.md +4 -0
  150. package/docs/deployment.md +6 -1
  151. package/docs/guides/mcp-oauth.md +157 -0
  152. package/docs/reference/cli.md +28 -0
  153. package/docs/reference/connections.md +28 -0
  154. package/docs/reference/prompt.md +47 -0
  155. package/docs/reference/tools.md +21 -0
  156. package/docs/troubleshooting.md +11 -0
  157. package/package.json +9 -2
  158. package/skills/create-agent/SKILL.md +7 -3
  159. package/skills/framework-map/SKILL.md +1 -0
  160. package/skills/mcp-auth/SKILL.md +138 -0
  161. package/src/bin/agent-serve.ts +20 -3
  162. package/src/channels/github/github-channel.ts +2 -0
  163. package/src/channels/github/types.ts +4 -0
  164. package/src/channels/slack/cursor-account.ts +2 -0
  165. package/src/channels/slack/dispatch.ts +49 -0
  166. package/src/channels/slack/eval-directive.ts +348 -0
  167. package/src/channels/slack/index.ts +1 -0
  168. package/src/channels/slack/slack-channel.ts +3 -0
  169. package/src/channels/slack/types.ts +7 -0
  170. package/src/connections.ts +10 -5
  171. package/src/evals.ts +43 -0
  172. package/src/index.ts +3 -0
  173. package/src/internal/cli-mcp-oauth.ts +254 -0
  174. package/src/internal/cli-mcp.ts +25 -2
  175. package/src/internal/cursor/account-mcp.ts +153 -41
  176. package/src/internal/discovery.ts +70 -8
  177. package/src/internal/distribution.ts +1 -0
  178. package/src/internal/logs-client.ts +1 -1
  179. package/src/internal/mcp-host.ts +33 -9
  180. package/src/internal/mcp-oauth.ts +496 -0
  181. package/src/internal/resolved-connections.ts +6 -1
  182. package/src/internal/sdk-runner.ts +37 -1
  183. package/src/internal/server.ts +13 -5
  184. package/src/internal/session-engine.ts +115 -74
  185. package/src/internal/sessions-client.ts +2 -1
  186. package/src/internal/stream-progress.ts +8 -0
  187. package/src/internal/turn-governor.ts +145 -0
  188. package/src/prompt.ts +83 -0
  189. package/src/types.ts +76 -6
  190. package/dist/docs/assets/chunks/@localSearchIndexroot.OfDZ5CXg.js +0 -1
  191. package/dist/internal/persistence-coordinator.d.ts +0 -127
  192. package/dist/internal/persistence-coordinator.d.ts.map +0 -1
  193. package/dist/persistence.d.ts +0 -184
  194. package/dist/persistence.d.ts.map +0 -1
@@ -9,6 +9,7 @@
9
9
  * {@link HostMcpRegistry} on {@link ChannelHandlerArgs.host}.mcp
10
10
  */
11
11
 
12
+ import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
12
13
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
13
14
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
14
15
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -20,6 +21,8 @@ import type {
20
21
  HostMcpToolInfo,
21
22
  JsonObject,
22
23
  } from "../types.js";
24
+ import { CLI_COMMAND_NAME as CLI } from "./distribution.js";
25
+ import { createHostMcpOAuthProvider } from "./mcp-oauth.js";
23
26
 
24
27
  interface OpenedConnection {
25
28
  client: Client;
@@ -118,8 +121,21 @@ async function openConnection(
118
121
  name: "agent-serve",
119
122
  version: "0.1.0",
120
123
  });
121
- const transport = createTransport(connection);
122
- await client.connect(transport);
124
+ const transport = await createTransport(connection);
125
+ try {
126
+ await client.connect(transport);
127
+ } catch (error) {
128
+ if (
129
+ error instanceof UnauthorizedError &&
130
+ isOauthHttpTransport(connection.transport)
131
+ ) {
132
+ throw new Error(
133
+ `MCP connection "${connection.name}" needs authorization. Run \`${CLI} mcp oauth ${connection.name}\`.`,
134
+ { cause: error }
135
+ );
136
+ }
137
+ throw error;
138
+ }
123
139
 
124
140
  return {
125
141
  client,
@@ -138,7 +154,7 @@ async function openConnection(
138
154
  };
139
155
  }
140
156
 
141
- function createTransport(connection: DiscoveredConnection) {
157
+ async function createTransport(connection: DiscoveredConnection) {
142
158
  const transport = connection.transport;
143
159
  if ("agent" in transport) {
144
160
  // Peers are resolved to concrete URLs by the serve host before any call
@@ -176,16 +192,24 @@ function createTransport(connection: DiscoveredConnection) {
176
192
  transport.headers === undefined
177
193
  ? undefined
178
194
  : { headers: transport.headers };
195
+ const authProvider =
196
+ transport.oauth === true
197
+ ? await createHostMcpOAuthProvider({
198
+ identifier: connection.name,
199
+ resourceUrl: transport.url,
200
+ })
201
+ : undefined;
179
202
 
180
203
  if (transport.type === "sse") {
181
- return new SSEClientTransport(url, {
182
- ...(requestInit === undefined ? {} : { requestInit }),
183
- });
204
+ return new SSEClientTransport(url, { requestInit, authProvider });
184
205
  }
206
+ return new StreamableHTTPClientTransport(url, { requestInit, authProvider });
207
+ }
185
208
 
186
- return new StreamableHTTPClientTransport(url, {
187
- ...(requestInit === undefined ? {} : { requestInit }),
188
- });
209
+ function isOauthHttpTransport(
210
+ transport: DiscoveredConnection["transport"]
211
+ ): boolean {
212
+ return "url" in transport && transport.oauth === true;
189
213
  }
190
214
 
191
215
  function normalizeCallResult(result: unknown): HostMcpCallResult {
@@ -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;
@@ -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[] = [];
@@ -387,6 +387,12 @@ export async function startServer(
387
387
  reminderRunner.cancelForContinuation(token, reason),
388
388
  dispatch: (id) => reminderRunner.dispatch(id),
389
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
+ });
390
396
  const runtime: MountedRuntime = {
391
397
  slug,
392
398
  project,
@@ -1609,7 +1615,9 @@ async function startCursorSlackRelay(input: {
1609
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}`
1610
1616
  );
1611
1617
  }
1612
- 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();
1613
1621
  const baseStateRoot =
1614
1622
  options.stateRoot ??
1615
1623
  join(targets[0].runtime.project.rootDir, ".agent-serve");