@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.
- package/README.md +1 -1
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +0 -5
- package/dist/bin/agent-serve.d.ts +1 -0
- package/dist/bin/agent-serve.d.ts.map +1 -1
- package/dist/bin/agent-serve.js +32 -2
- package/dist/channels/github/types.d.ts +10 -7
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.C2aVczmo.js → app.FPupl4SP.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.WoYunhnT.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.DLEzetbD.js → VPLocalSearchBox.Cd182Cu0.js} +1 -1
- package/dist/docs/assets/chunks/{theme.BkOH1oMp.js → theme.BEM3Okcd.js} +2 -2
- package/dist/docs/assets/{example-agents_weather-agent.md.Dmrcphhl.js → example-agents_weather-agent.md.BADkPqxQ.js} +6 -4
- package/dist/docs/assets/example-agents_weather-agent.md.BADkPqxQ.lean.js +1 -0
- package/dist/docs/assets/guides_cloud-runtime.md.CDJGvVC4.js +9 -0
- package/dist/docs/assets/guides_cloud-runtime.md.CDJGvVC4.lean.js +1 -0
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +10 -8
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +5 -5
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/files-backends/agent-store-presigned-url.d.ts.map +1 -1
- package/dist/files-backends/agent-store-presigned-url.js +19 -810
- package/dist/files.d.ts +20 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +20 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/internal/artifacts-store.d.ts +14 -6
- package/dist/internal/artifacts-store.d.ts.map +1 -1
- package/dist/internal/artifacts-store.js +160 -16
- package/dist/internal/cli-convert-automation.d.ts +39 -0
- package/dist/internal/cli-convert-automation.d.ts.map +1 -0
- package/dist/internal/cli-convert-automation.js +280 -0
- package/dist/internal/cli-deploy.d.ts +2 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +1 -1
- package/dist/internal/cli-mcp-oauth.d.ts +23 -0
- package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
- package/dist/internal/cli-mcp-oauth.js +57 -33
- package/dist/internal/convert-automation/convert-workflow.d.ts +10 -0
- package/dist/internal/convert-automation/convert-workflow.d.ts.map +1 -0
- package/dist/internal/convert-automation/convert-workflow.js +998 -0
- package/dist/internal/convert-automation/index.d.ts +3 -0
- package/dist/internal/convert-automation/index.d.ts.map +1 -0
- package/dist/internal/convert-automation/index.js +1 -0
- package/dist/internal/convert-automation/slug.d.ts +11 -0
- package/dist/internal/convert-automation/slug.d.ts.map +1 -0
- package/dist/internal/convert-automation/slug.js +36 -0
- package/dist/internal/convert-automation/types.d.ts +191 -0
- package/dist/internal/convert-automation/types.d.ts.map +1 -0
- package/dist/internal/convert-automation/types.js +13 -0
- package/dist/internal/cursor-relay-core.d.ts +12 -0
- package/dist/internal/cursor-relay-core.d.ts.map +1 -1
- package/dist/internal/cursor-relay-core.js +42 -8
- package/dist/internal/deploy-client.d.ts +31 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +48 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +3 -32
- package/dist/internal/init-project.d.ts +1 -0
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +1 -1
- package/dist/internal/mcp-endpoint.d.ts +6 -0
- package/dist/internal/mcp-endpoint.d.ts.map +1 -1
- package/dist/internal/mcp-endpoint.js +168 -0
- package/dist/internal/resolved-connections.d.ts +15 -0
- package/dist/internal/resolved-connections.d.ts.map +1 -1
- package/dist/internal/resolved-connections.js +12 -5
- package/dist/internal/review-comments.d.ts +9 -1
- package/dist/internal/review-comments.d.ts.map +1 -1
- package/dist/internal/review-comments.js +76 -0
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +83 -8
- package/dist/internal/session-engine.d.ts +56 -3
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +252 -83
- package/dist/internal/storage-coordinator.d.ts +0 -15
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +0 -52
- package/dist/internal/workspace.d.ts +10 -5
- package/dist/internal/workspace.d.ts.map +1 -1
- package/dist/internal/workspace.js +13 -0
- package/dist/memory.d.ts +22 -6
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +100 -8
- package/dist/playground/assets/index-CDDWw0YX.js +85 -0
- package/dist/playground/assets/index-MVuNTd8v.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage.d.ts +0 -4
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +0 -4
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/example-agents/weather-agent.md +29 -11
- package/docs/guides/cloud-runtime.md +6 -4
- package/package.json +3 -22
- package/src/artifacts.ts +0 -6
- package/src/bin/agent-serve.ts +36 -2
- package/src/channels/github/types.ts +10 -7
- package/src/files-backends/agent-store-presigned-url.ts +7 -5
- package/src/files.ts +27 -0
- package/src/index.ts +6 -1
- package/src/internal/artifacts-store.ts +185 -22
- package/src/internal/cli-convert-automation.ts +352 -0
- package/src/internal/cli-deploy.ts +1 -1
- package/src/internal/cli-mcp-oauth.ts +69 -31
- package/src/internal/convert-automation/convert-workflow.ts +1422 -0
- package/src/internal/convert-automation/index.ts +10 -0
- package/src/internal/convert-automation/slug.ts +38 -0
- package/src/internal/convert-automation/types.ts +199 -0
- package/src/internal/cursor-relay-core.ts +62 -3
- package/src/internal/deploy-client.ts +78 -0
- package/src/internal/discovery.ts +3 -37
- package/src/internal/init-project.ts +1 -1
- package/src/internal/mcp-endpoint.ts +243 -0
- package/src/internal/resolved-connections.ts +32 -6
- package/src/internal/review-comments.ts +102 -2
- package/src/internal/server.ts +108 -2
- package/src/internal/session-engine.ts +329 -99
- package/src/internal/storage-coordinator.ts +0 -62
- package/src/internal/workspace.ts +19 -5
- package/src/memory.ts +126 -10
- package/src/storage.ts +0 -6
- package/src/types.ts +15 -0
- package/dist/channels/slack/post-update-delivery.d.ts +0 -85
- package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.xgMnlUSO.js +0 -1
- package/dist/docs/assets/example-agents_weather-agent.md.Dmrcphhl.lean.js +0 -1
- package/dist/docs/assets/guides_cloud-runtime.md.BZ2GA7Es.js +0 -9
- package/dist/docs/assets/guides_cloud-runtime.md.BZ2GA7Es.lean.js +0 -1
- package/dist/internal/json-dir-store.d.ts +0 -32
- package/dist/internal/json-dir-store.d.ts.map +0 -1
- package/dist/internal/persistence-coordinator.d.ts +0 -127
- package/dist/internal/persistence-coordinator.d.ts.map +0 -1
- package/dist/multi-tenant.d.ts +0 -80
- package/dist/multi-tenant.d.ts.map +0 -1
- package/dist/multi-tenant.js +0 -69
- package/dist/persistence.d.ts +0 -184
- package/dist/persistence.d.ts.map +0 -1
- package/dist/playground/assets/index-50PKeJlG.css +0 -1
- 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 (
|
|
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
|
-
|
|
132
|
-
"
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
):
|
package/src/internal/server.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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({
|