@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
@@ -16,8 +16,19 @@
16
16
  * directly (no model turn). Registered only when server tools exist.
17
17
  */
18
18
 
19
+ import type {
20
+ SDKCustomToolContent,
21
+ SDKCustomToolResult,
22
+ SDKJsonValue,
23
+ } from "@cursor/sdk";
24
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
19
25
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
20
26
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
27
+ import {
28
+ CallToolRequestSchema,
29
+ type CallToolResult,
30
+ ListToolsRequestSchema,
31
+ } from "@modelcontextprotocol/sdk/types.js";
21
32
  import { z } from "zod";
22
33
  import type {
23
34
  AgentProject,
@@ -40,6 +51,8 @@ export const MCP_CHANNEL_ID = "mcp";
40
51
 
41
52
  /** Mount path of the MCP surface, relative to the agent base. */
42
53
  export const MCP_ENDPOINT_PATH = "/v1/mcp";
54
+ /** Direct authored server tools used by this agent's cloud turns. */
55
+ export const TOOL_MCP_ENDPOINT_PATH = "/v1/mcp/tools";
43
56
 
44
57
  /**
45
58
  * Bounded waits stay below the MCP client default request timeout (60s) so
@@ -147,6 +160,170 @@ export function createAgentMcpEndpoint(args: {
147
160
  };
148
161
  }
149
162
 
163
+ export function createToolMcpEndpoint(args: {
164
+ engine: SessionEngine;
165
+ logger?: (line: string) => void;
166
+ }): AgentMcpEndpoint {
167
+ const { engine, logger } = args;
168
+ return {
169
+ handle(request): Promise<Response> {
170
+ const url = new URL(request.url);
171
+ const sessionId = url.searchParams.get("agentkitSessionId");
172
+ // `connection=<name>` serves a host connection bridge instead of the
173
+ // authored server tools: same endpoint, same auth, same session
174
+ // binding, but tools/list and tools/call forward to `host.mcp` for
175
+ // that connection (see buildConnectionBridgeMcpServer).
176
+ const connection = url.searchParams.get("connection");
177
+ return handleStatelessMcpRequest({
178
+ request,
179
+ buildServer: () =>
180
+ connection === null
181
+ ? buildToolMcpServer(engine, sessionId)
182
+ : buildConnectionBridgeMcpServer(engine, sessionId, connection),
183
+ label:
184
+ connection === null
185
+ ? "tool mcp endpoint"
186
+ : `connection bridge mcp endpoint (${connection})`,
187
+ ...(logger === undefined ? {} : { logger }),
188
+ });
189
+ },
190
+ };
191
+ }
192
+
193
+ function buildToolMcpServer(
194
+ engine: SessionEngine,
195
+ sessionId: string | null
196
+ ): Server {
197
+ const server = new Server(
198
+ { name: `${engine.project.name}:tools`, version: "0.1.0" },
199
+ {
200
+ capabilities: { tools: {} },
201
+ instructions:
202
+ "Direct tools implemented by the AgentSDK deployment for its active cloud turn.",
203
+ }
204
+ );
205
+ const tools = engine.project.agent.tools.filter(
206
+ (tool) => tool.execution === "server"
207
+ );
208
+ server.setRequestHandler(ListToolsRequestSchema, () =>
209
+ Promise.resolve({
210
+ tools: tools.map((tool) => ({
211
+ name: tool.name,
212
+ description: tool.description,
213
+ inputSchema: tool.inputSchema,
214
+ })),
215
+ })
216
+ );
217
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
218
+ if (sessionId === null || sessionId === "") {
219
+ return errorResult("Missing AgentSDK session binding.");
220
+ }
221
+ const tool = tools.find(
222
+ (candidate) => candidate.name === request.params.name
223
+ );
224
+ if (tool === undefined) {
225
+ return errorResult(`Unknown server tool "${request.params.name}".`);
226
+ }
227
+ const result = await engine.callActiveServerTool(
228
+ sessionId,
229
+ tool.name,
230
+ request.params.arguments ?? {}
231
+ );
232
+ return sdkResultToMcpResult(result);
233
+ });
234
+ return server;
235
+ }
236
+
237
+ /**
238
+ * Host connection bridge: one authored MCP connection served to a cloud turn
239
+ * over the tool MCP endpoint. Listing and calling forward to the host-side
240
+ * client (`host.mcp`), where the connection's credential — account bridge
241
+ * secret or host OAuth token — actually lives, so nothing key-shaped ever
242
+ * travels in the turn's MCP config or lands on the VM.
243
+ *
244
+ * The engine gates access twice, mirroring the authored server-tool path:
245
+ * only bridgeable names (`isCloudBridgedConnection`) — `hostOnly`
246
+ * connections and unknown names stay unreachable from a VM even with a
247
+ * valid endpoint credential — and only while the bound session has a turn
248
+ * in flight (`hasActiveTurn`), so a credential that persists on a VM after
249
+ * its turn cannot keep calling host connectors. Listing forwards per
250
+ * request, so a server that adds tools mid-session (`tools/list_changed`,
251
+ * e.g. Datadog's skills-first mode) is picked up on the next list instead
252
+ * of never.
253
+ */
254
+ function buildConnectionBridgeMcpServer(
255
+ engine: SessionEngine,
256
+ sessionId: string | null,
257
+ connectionName: string
258
+ ): Server {
259
+ const server = new Server(
260
+ {
261
+ name: `${engine.project.name}:bridge:${connectionName}`,
262
+ version: "0.1.0",
263
+ },
264
+ {
265
+ capabilities: { tools: {} },
266
+ instructions: `Tools from the "${connectionName}" MCP connection, listed and called on the serve host where its credential lives.`,
267
+ }
268
+ );
269
+ const rejected = (): string | undefined => {
270
+ if (sessionId === null || sessionId === "") {
271
+ return "Missing AgentSDK session binding.";
272
+ }
273
+ if (!engine.isCloudBridgedConnection(connectionName)) {
274
+ return `MCP connection "${connectionName}" is not bridged to cloud turns.`;
275
+ }
276
+ if (!engine.hasActiveTurn(sessionId)) {
277
+ return `Session ${sessionId} has no active turn for connection "${connectionName}".`;
278
+ }
279
+ return undefined;
280
+ };
281
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
282
+ const reason = rejected();
283
+ if (reason !== undefined) {
284
+ throw new Error(reason);
285
+ }
286
+ const tools = await engine.host.mcp.listTools(connectionName);
287
+ return {
288
+ tools: tools.map((tool) => ({
289
+ name: tool.name,
290
+ ...(tool.description === undefined
291
+ ? {}
292
+ : { description: tool.description }),
293
+ inputSchema: tool.inputSchema ?? { type: "object" },
294
+ })),
295
+ };
296
+ });
297
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
298
+ const reason = rejected();
299
+ if (reason !== undefined) {
300
+ return errorResult(reason);
301
+ }
302
+ try {
303
+ const result = await engine.host.mcp.callTool(
304
+ connectionName,
305
+ request.params.name,
306
+ request.params.arguments ?? {}
307
+ );
308
+ return {
309
+ // The host client already speaks MCP, so the content array is valid
310
+ // CallToolResult content; HostMcpCallResult just types the tail of
311
+ // the union open-endedly.
312
+ content: result.content as CallToolResult["content"],
313
+ ...(result.isError === undefined ? {} : { isError: result.isError }),
314
+ ...(result.structuredContent === undefined
315
+ ? {}
316
+ : { structuredContent: result.structuredContent }),
317
+ };
318
+ } catch (error) {
319
+ return errorResult(
320
+ `MCP connection "${connectionName}" is not reachable from the serve host: ${describeError(error)}`
321
+ );
322
+ }
323
+ });
324
+ return server;
325
+ }
326
+
150
327
  // ============================================================================
151
328
  // Server construction (per request; tools close over the caller principal)
152
329
  // ============================================================================
@@ -489,6 +666,72 @@ function errorResult(message: string): McpToolResult {
489
666
  };
490
667
  }
491
668
 
669
+ function sdkResultToMcpResult(result: SDKCustomToolResult) {
670
+ if (isSdkToolEnvelope(result)) {
671
+ return {
672
+ content: result.content.map(toMcpContent),
673
+ ...(result.isError === true ? { isError: true } : {}),
674
+ ...(result.structuredContent === undefined
675
+ ? {}
676
+ : { structuredContent: result.structuredContent }),
677
+ };
678
+ }
679
+ if (typeof result === "string") {
680
+ return { content: [{ type: "text" as const, text: result }] };
681
+ }
682
+ return {
683
+ content: [{ type: "text" as const, text: JSON.stringify(result) }],
684
+ ...(isJsonObject(result) ? { structuredContent: result } : {}),
685
+ };
686
+ }
687
+
688
+ function toMcpContent(content: SDKCustomToolContent) {
689
+ if (content.type === "text") {
690
+ return content;
691
+ }
692
+ return {
693
+ type: "image" as const,
694
+ data: content.data,
695
+ mimeType: content.mimeType ?? "application/octet-stream",
696
+ };
697
+ }
698
+
699
+ function isSdkToolEnvelope(
700
+ result: SDKCustomToolResult
701
+ ): result is Extract<SDKCustomToolResult, { content: unknown }> {
702
+ return (
703
+ typeof result === "object" &&
704
+ result !== null &&
705
+ !Array.isArray(result) &&
706
+ "content" in result &&
707
+ Array.isArray(result.content) &&
708
+ result.content.every(isSdkToolContent)
709
+ );
710
+ }
711
+
712
+ function isSdkToolContent(value: unknown): value is SDKCustomToolContent {
713
+ if (typeof value !== "object" || value === null || !("type" in value)) {
714
+ return false;
715
+ }
716
+ if (value.type === "text") {
717
+ return "text" in value && typeof value.text === "string";
718
+ }
719
+ return (
720
+ value.type === "image" &&
721
+ "data" in value &&
722
+ typeof value.data === "string" &&
723
+ (!("mimeType" in value) ||
724
+ value.mimeType === undefined ||
725
+ typeof value.mimeType === "string")
726
+ );
727
+ }
728
+
729
+ function isJsonObject(
730
+ value: SDKJsonValue
731
+ ): value is Record<string, SDKJsonValue> {
732
+ return typeof value === "object" && value !== null && !Array.isArray(value);
733
+ }
734
+
492
735
  /** Resolve to the promise value, or undefined once `ms` elapses first. */
493
736
  function raceTimeout<T>(
494
737
  promise: Promise<T>,
@@ -25,13 +25,24 @@ export function validateMountConnectionsConfig(args: {
25
25
  project: AgentProject;
26
26
  accountRefs?: CursorAccountConnectionRef[];
27
27
  publicUrl?: string;
28
+ /**
29
+ * True when serve has a cloud tools URL (`--cloud-tools-url`, always set on
30
+ * managed hosting): cloud turns then reach filtered account connections
31
+ * through the host connection bridge on the tool MCP endpoint, so the
32
+ * filter is honored host-side and no `--public-url` is needed.
33
+ */
34
+ cloudToolsBridge?: boolean;
28
35
  }): void {
29
36
  if (args.accountRefs === undefined || args.accountRefs.length === 0) {
30
37
  return;
31
38
  }
32
39
  const agent = args.project.agent;
33
40
  const cloudCapable = agent.runtime === "cloud" || agent.cloud !== undefined;
34
- if (!cloudCapable || args.publicUrl !== undefined) {
41
+ if (
42
+ !cloudCapable ||
43
+ args.publicUrl !== undefined ||
44
+ args.cloudToolsBridge === true
45
+ ) {
35
46
  return;
36
47
  }
37
48
  const label = args.slug === "" ? args.project.name : args.slug;
@@ -59,6 +70,12 @@ export function validateMountConnectionsConfig(args: {
59
70
  export function assertCloudCanHonorAccountServersFilters(args: {
60
71
  runtime: "local" | "cloud";
61
72
  resolvedConnections: readonly EngineResolvedConnection[];
73
+ /**
74
+ * Connection names this cloud turn reaches through the host connection
75
+ * bridge (tool MCP endpoint). The filter is honored on the serve host
76
+ * there, so a missing `cloudUrl` is not a fail-open.
77
+ */
78
+ bridgedNames?: ReadonlySet<string>;
62
79
  }): void {
63
80
  if (args.runtime !== "cloud") {
64
81
  return;
@@ -66,7 +83,8 @@ export function assertCloudCanHonorAccountServersFilters(args: {
66
83
  const blocked = args.resolvedConnections.filter(
67
84
  (connection) =>
68
85
  connection.accountServersFilter !== undefined &&
69
- connection.cloudUrl === undefined
86
+ connection.cloudUrl === undefined &&
87
+ args.bridgedNames?.has(connection.name) !== true
70
88
  );
71
89
  if (blocked.length === 0) {
72
90
  return;
@@ -111,6 +129,8 @@ export function resolveMountConnections(args: {
111
129
  accountBridge?: CursorAccountMcpBridge;
112
130
  publicUrl?: string;
113
131
  authToken?: string;
132
+ /** See {@link validateMountConnectionsConfig}. */
133
+ cloudToolsBridge?: boolean;
114
134
  logger: (line: string) => void;
115
135
  }): EngineResolvedConnection[] {
116
136
  const agent = args.project.agent;
@@ -128,9 +148,13 @@ export function resolveMountConnections(args: {
128
148
  .map((connection) => connection.name);
129
149
  if (hostOauthNames.length > 0) {
130
150
  args.logger(
131
- `[agent-sdk] warning: agent "${label}" has host-OAuth MCP connections (${hostOauthNames.join(
132
- ", "
133
- )}) whose tokens live on this serve host; cloud-runtime turns omit them. Mark them \`hostOnly: true\` and call them from host tools, or drop \`oauth\` and authorize the server another way.`
151
+ args.cloudToolsBridge === true
152
+ ? `[agent-sdk] agent "${label}": host-OAuth MCP connections (${hostOauthNames.join(
153
+ ", "
154
+ )}) reach cloud turns through the host connection bridge; their tokens stay on this serve host.`
155
+ : `[agent-sdk] warning: agent "${label}" has host-OAuth MCP connections (${hostOauthNames.join(
156
+ ", "
157
+ )}) whose tokens live on this serve host; cloud-runtime turns omit them. Mark them \`hostOnly: true\` and call them from host tools, or drop \`oauth\` and authorize the server another way.`
134
158
  );
135
159
  }
136
160
  }
@@ -168,7 +192,9 @@ export function resolveMountConnections(args: {
168
192
  args.accountBridge.logPreflight(args.accountRefs);
169
193
  if (cloudCapable && args.publicUrl === undefined) {
170
194
  args.logger(
171
- `[agent-sdk] agent "${label}": cloud-runtime turns omit the account bridge without --public-url (cloud agents fall back to the account's native connectors).`
195
+ args.cloudToolsBridge === true
196
+ ? `[agent-sdk] agent "${label}": cloud-runtime turns reach filtered account connections through the host connection bridge (unfiltered ones fall back to the account's native connectors).`
197
+ : `[agent-sdk] agent "${label}": cloud-runtime turns omit the account bridge without --public-url (cloud agents fall back to the account's native connectors).`
172
198
  );
173
199
  }
174
200
  }
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
+ import type { HostGitHubClient } from "../types.js";
2
3
 
3
- export type ReviewCommentPublisher = "local_gh";
4
+ export type ReviewCommentPublisher = "local_gh" | "host_github";
4
5
 
5
6
  export type ReviewCommentPostResult = {
6
7
  posted: boolean;
@@ -84,6 +85,80 @@ export function createLocalGhReviewPublisher(): ReviewCommentPublisherImplementa
84
85
  };
85
86
  }
86
87
 
88
+ /**
89
+ * Posts a PR review through the host GitHub client (Cursor account lease,
90
+ * App installation token, or PAT). Prefer this on Cursor-deployed agents —
91
+ * no separate GitHub write secret is required when `githubChannel({
92
+ * cursorAccount })` is bound.
93
+ */
94
+ export function createHostGithubReviewPublisher(
95
+ github: HostGitHubClient
96
+ ): ReviewCommentPublisherImplementation {
97
+ return {
98
+ async postReview(args) {
99
+ return postReviewViaHostGithub(github, args);
100
+ },
101
+ };
102
+ }
103
+
104
+ async function postReviewViaHostGithub(
105
+ github: HostGitHubClient,
106
+ args: ReviewCommentPostArgs
107
+ ): Promise<ReviewCommentPostResult> {
108
+ const prepared = prepareReviewCommentsRequest(args);
109
+ if (prepared.kind === "skipped") {
110
+ return {
111
+ ...prepared.result,
112
+ publisher: "host_github",
113
+ };
114
+ }
115
+
116
+ const { request } = prepared;
117
+ const [owner, repo] = request.repo.split("/", 2);
118
+ if (owner === undefined || repo === undefined || repo === "") {
119
+ return skippedHost("unparseable owner/repo from PR URL");
120
+ }
121
+
122
+ try {
123
+ args.signal?.throwIfAborted();
124
+ const octokit = await github.getOctokit();
125
+ args.signal?.throwIfAborted();
126
+ const response = await octokit.rest.pulls.createReview({
127
+ owner,
128
+ repo,
129
+ pull_number: request.prNumber,
130
+ commit_id: request.headSha,
131
+ body: request.message,
132
+ event: "COMMENT",
133
+ comments: request.comments.map(toGithubPullRequestReviewComment),
134
+ });
135
+ const htmlUrl =
136
+ typeof response.data.html_url === "string"
137
+ ? response.data.html_url
138
+ : undefined;
139
+ const inlineCommentCount = request.comments.length;
140
+ return {
141
+ posted: true,
142
+ publisher: "host_github",
143
+ mode:
144
+ inlineCommentCount > 0
145
+ ? "review_with_inline_comments"
146
+ : "review_body_only",
147
+ inlineCommentCount,
148
+ detail: htmlUrl
149
+ ? `posted via host GitHub: ${htmlUrl}`
150
+ : `posted via host GitHub with ${inlineCommentCount} inline comment(s)`,
151
+ };
152
+ } catch (error) {
153
+ if (args.signal?.aborted) {
154
+ throw error;
155
+ }
156
+ return skippedHost(
157
+ `host GitHub review post failed: ${error instanceof Error ? error.message : String(error)}`
158
+ );
159
+ }
160
+ }
161
+
87
162
  async function postReviewViaLocalGh(
88
163
  args: ReviewCommentPostArgs
89
164
  ): Promise<ReviewCommentPostResult> {
@@ -129,10 +204,25 @@ async function postReviewViaLocalGh(
129
204
  }
130
205
  }
131
206
 
207
+ type GithubPullRequestReviewComment = {
208
+ path: string;
209
+ body: string;
210
+ line: number;
211
+ side: "RIGHT";
212
+ start_line?: number;
213
+ start_side?: "RIGHT";
214
+ };
215
+
132
216
  function toGithubReviewComment(
133
217
  comment: ReviewInlineComment
134
218
  ): Record<string, string | number> {
135
- const body: Record<string, string | number> = {
219
+ return toGithubPullRequestReviewComment(comment);
220
+ }
221
+
222
+ function toGithubPullRequestReviewComment(
223
+ comment: ReviewInlineComment
224
+ ): GithubPullRequestReviewComment {
225
+ const body: GithubPullRequestReviewComment = {
136
226
  path: comment.file,
137
227
  body: comment.body,
138
228
  line: comment.line,
@@ -159,6 +249,16 @@ function skipped(detail: string): ReviewCommentPostResult {
159
249
  };
160
250
  }
161
251
 
252
+ function skippedHost(detail: string): ReviewCommentPostResult {
253
+ return {
254
+ posted: false,
255
+ publisher: "host_github",
256
+ mode: "skipped",
257
+ inlineCommentCount: 0,
258
+ detail,
259
+ };
260
+ }
261
+
162
262
  function parseGithubPrUrl(
163
263
  prUrl: string
164
264
  ):
@@ -103,7 +103,12 @@ import {
103
103
  type LocalControlPlane,
104
104
  } from "./local-control-plane.js";
105
105
  import { createLogRing, type LogRing } from "./log-ring.js";
106
- import { createAgentMcpEndpoint, MCP_ENDPOINT_PATH } from "./mcp-endpoint.js";
106
+ import {
107
+ createAgentMcpEndpoint,
108
+ createToolMcpEndpoint,
109
+ MCP_ENDPOINT_PATH,
110
+ TOOL_MCP_ENDPOINT_PATH,
111
+ } from "./mcp-endpoint.js";
107
112
  import { isLoopbackAddress } from "./net.js";
108
113
  import { collectPeerConnections } from "./peer-connections.js";
109
114
  import {
@@ -681,6 +686,7 @@ export async function startServer(
681
686
  ...(options.publicUrl === undefined
682
687
  ? {}
683
688
  : { publicUrl: options.publicUrl }),
689
+ cloudToolsBridge: options.cloudToolsUrl !== undefined,
684
690
  });
685
691
  }
686
692
 
@@ -751,11 +757,55 @@ export async function startServer(
751
757
  ...(options.authToken === undefined
752
758
  ? {}
753
759
  : { authToken: options.authToken }),
760
+ cloudToolsBridge: options.cloudToolsUrl !== undefined,
754
761
  logger,
755
762
  });
756
763
  if (resolved.length > 0) {
757
764
  runtime.engine.setResolvedConnections(resolved);
758
765
  }
766
+ const hasServerTools = runtime.project.agent.tools.some(
767
+ (tool) => tool.execution === "server"
768
+ );
769
+ // Host-bridged connections ride the same endpoint as server tools, so
770
+ // the cloud tools connection is set whenever either needs it.
771
+ const bridgeableNames = runtime.engine.cloudBridgeableConnectionNames();
772
+ const cloudCapable =
773
+ runtime.project.agent.runtime === "cloud" ||
774
+ runtime.project.agent.cloud !== undefined;
775
+ if ((hasServerTools || bridgeableNames.length > 0) && cloudCapable) {
776
+ const basePath = runtime.slug === "" ? "" : `/${runtime.slug}`;
777
+ const publicToolsUrl =
778
+ options.cloudToolsUrl ??
779
+ (options.publicUrl === undefined
780
+ ? undefined
781
+ : `${options.publicUrl.replace(/\/$/, "")}${basePath}${TOOL_MCP_ENDPOINT_PATH}`);
782
+ if (publicToolsUrl === undefined) {
783
+ if (hasServerTools) {
784
+ logger(
785
+ `[agent-sdk] warning: agent "${runtime.project.name}" has server tools but no --public-url or --cloud-tools-url; cloud turns omit those tools.`
786
+ );
787
+ }
788
+ } else {
789
+ const headers =
790
+ options.cloudToolsUrl !== undefined
791
+ ? accountKey === undefined
792
+ ? undefined
793
+ : { authorization: `Bearer ${accountKey.apiKey}` }
794
+ : options.authToken === undefined
795
+ ? undefined
796
+ : { authorization: `Bearer ${options.authToken}` };
797
+ if (options.cloudToolsUrl !== undefined && headers === undefined) {
798
+ throw new Error(
799
+ `Agent "${runtime.project.name}" needs a Cursor API key to authenticate --cloud-tools-url. ${SIGN_IN_HINT}`
800
+ );
801
+ }
802
+ runtime.engine.setCloudServerToolsConnection({
803
+ type: "http",
804
+ url: publicToolsUrl,
805
+ ...(headers === undefined ? {} : { headers }),
806
+ });
807
+ }
808
+ }
759
809
  }
760
810
 
761
811
  const agents: MountedAgentInfo[] = runtimes.map(({ slug, project }) => ({
@@ -959,7 +1009,15 @@ function mountAgentRoutes(
959
1009
  method: "GET",
960
1010
  pattern: `${base}/v1/health`,
961
1011
  auth: "none",
962
- handler: () => jsonResponse({ ok: true, name: project.name }),
1012
+ // Temporal consumes this response before promoting a newly deployed pod.
1013
+ // Include the framework build there so deploy observability gets the
1014
+ // version from the process that will serve turns, with no second request.
1015
+ handler: () =>
1016
+ jsonResponse({
1017
+ ok: true,
1018
+ name: project.name,
1019
+ version: packageVersion(),
1020
+ }),
963
1021
  });
964
1022
 
965
1023
  // Bootstrap the SPA can't derive from its own URL (agent name, dev mode, and
@@ -1091,6 +1149,44 @@ function mountAgentRoutes(
1091
1149
  },
1092
1150
  });
1093
1151
 
1152
+ // File/blob payload of one artifact (`tag({ contents })`).
1153
+ router.add({
1154
+ method: "GET",
1155
+ pattern: `${base}/v1/artifacts/:id/content`,
1156
+ auth: httpAuth,
1157
+ handler: async (_request, ctx) => {
1158
+ const includeAll =
1159
+ relaxSessionReadAuth &&
1160
+ (relaxSessionReadAuthBeyondLoopback ||
1161
+ (typeof ctx.requestIp === "string" &&
1162
+ isLoopbackAddress(ctx.requestIp)));
1163
+ const found = await engine.getArtifactContent(
1164
+ ctx.auth,
1165
+ ctx.params.id ?? "",
1166
+ { includeAll }
1167
+ );
1168
+ if (found === undefined) {
1169
+ return errorResponse(
1170
+ 404,
1171
+ "not_found",
1172
+ "No such artifact, or it carries no content."
1173
+ );
1174
+ }
1175
+ // A view over bytes is a valid fetch body; the DOM lib types are
1176
+ // narrower than the runtime.
1177
+ // contentType is author/model-controlled and the playground shares
1178
+ // this origin: force download, never render or sniff.
1179
+ return new Response(found.body as unknown as BodyInit, {
1180
+ headers: {
1181
+ "content-type":
1182
+ found.record.content?.contentType ?? "application/octet-stream",
1183
+ "content-disposition": "attachment",
1184
+ "x-content-type-options": "nosniff",
1185
+ },
1186
+ });
1187
+ },
1188
+ });
1189
+
1094
1190
  // The agent's MCP surface: peers and external MCP clients delegate to
1095
1191
  // this agent (ask / check / call_tool) over streamable HTTP. Stateless —
1096
1192
  // POST only; GET/DELETE get a spec-compliant 405 from the endpoint.
@@ -1104,6 +1200,16 @@ function mountAgentRoutes(
1104
1200
  });
1105
1201
  }
1106
1202
 
1203
+ const toolMcpEndpoint = createToolMcpEndpoint({ engine, logger });
1204
+ for (const method of ["POST", "GET", "DELETE"] as const) {
1205
+ router.add({
1206
+ method,
1207
+ pattern: `${base}${TOOL_MCP_ENDPOINT_PATH}`,
1208
+ auth: defaultAuth,
1209
+ handler: (request, ctx) => toolMcpEndpoint.handle(request, ctx.auth),
1210
+ });
1211
+ }
1212
+
1107
1213
  // Deterministic tool invocation: run an authored server tool directly,
1108
1214
  // without a model turn deciding to call it.
1109
1215
  router.add({