@cursor/july 0.1.62 → 0.1.73
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/AGENTS.md +4 -5
- package/README.md +23 -11
- package/dist/bin/agent-serve.js +11 -2
- package/dist/channels/github/api.d.ts +2 -0
- package/dist/channels/github/api.d.ts.map +1 -1
- package/dist/channels/github/api.js +2 -1
- package/dist/channels/github/binding.d.ts +7 -0
- package/dist/channels/github/binding.d.ts.map +1 -1
- package/dist/channels/github/binding.js +4 -0
- package/dist/channels/github/checks.d.ts +99 -0
- package/dist/channels/github/checks.d.ts.map +1 -0
- package/dist/channels/github/checks.js +108 -0
- package/dist/channels/github/cursor-account.d.ts +4 -4
- package/dist/channels/github/cursor-account.d.ts.map +1 -1
- package/dist/channels/github/cursor-account.js +7 -5
- package/dist/channels/github/index.d.ts +1 -0
- package/dist/channels/github/index.d.ts.map +1 -1
- package/dist/channels/github/index.js +1 -0
- package/dist/channels/github/types.d.ts +8 -0
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels/slack/channel-watch.d.ts +2 -1
- package/dist/channels/slack/channel-watch.d.ts.map +1 -1
- package/dist/channels/slack/channel-watch.js +7 -3
- package/dist/channels/slack/dispatch.d.ts +9 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +41 -21
- package/dist/channels/slack/manifest.d.ts +2 -2
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +14 -11
- package/dist/channels/slack/nudge-store.d.ts +16 -0
- package/dist/channels/slack/nudge-store.d.ts.map +1 -1
- package/dist/channels/slack/nudge-store.js +53 -0
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +4 -1
- package/dist/channels/slack/types.d.ts +24 -7
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/connections.d.ts +17 -5
- package/dist/connections.d.ts.map +1 -1
- package/dist/connections.js +17 -5
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.B-biogkN.js → app.CdbcFD8T.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DQad--_z.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.DZqKtFZS.js → VPLocalSearchBox.D_NKImM7.js} +1 -1
- package/dist/docs/assets/chunks/{theme.BwC5zP2r.js → theme.CMvz7cu9.js} +2 -2
- package/dist/docs/assets/{concepts.md.CqOsxbMU.js → concepts.md.BXAm6G-C.js} +1 -1
- package/dist/docs/assets/{guides_mcp-oauth.md.C7G7IykG.js → guides_mcp-oauth.md.CzEB6RaG.js} +1 -1
- package/dist/docs/assets/{reference_agent-config.md.DHkkvcEZ.js → reference_agent-config.md.BZnEKfEc.js} +3 -4
- package/dist/docs/assets/{reference_agent-config.md.DHkkvcEZ.lean.js → reference_agent-config.md.BZnEKfEc.lean.js} +1 -1
- package/dist/docs/assets/{reference_connections.md.CSQiNzre.js → reference_connections.md.Cu3N-S3Q.js} +26 -9
- package/dist/docs/assets/reference_connections.md.Cu3N-S3Q.lean.js +1 -0
- package/dist/docs/assets/{reference_sessions.md.DBVFi2Sx.js → reference_sessions.md.B0DdlM-K.js} +1 -1
- package/dist/docs/assets/{reference_tools.md.kJfJAP2m.js → reference_tools.md.DTg_kEsx.js} +2 -2
- package/dist/docs/assets/{reference_tools.md.kJfJAP2m.lean.js → reference_tools.md.DTg_kEsx.lean.js} +1 -1
- package/dist/docs/assets/troubleshooting.md.DQ-hYuZT.js +1 -0
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +5 -5
- 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 +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/convert-automation.html +3 -3
- 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 +5 -5
- package/dist/docs/guides/opentelemetry.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 +5 -6
- package/dist/docs/reference/artifacts.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 +28 -11
- 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 +5 -5
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +5 -5
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/templates/agentic-owners.html +3 -3
- package/dist/docs/templates/demo.html +3 -3
- package/dist/docs/templates/pr-autofixer.html +3 -3
- package/dist/docs/templates/security-reviewer.html +3 -3
- package/dist/docs/templates/triage.html +3 -3
- package/dist/docs/troubleshooting.html +5 -5
- package/dist/files-backends/agent-store-presigned-url.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/advertise-tools.d.ts +79 -0
- package/dist/internal/advertise-tools.d.ts.map +1 -0
- package/dist/internal/advertise-tools.js +185 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +7 -1
- package/dist/internal/cli-manifest.d.ts +19 -0
- package/dist/internal/cli-manifest.d.ts.map +1 -0
- package/dist/internal/cli-manifest.js +49 -0
- package/dist/internal/cli-slack.d.ts.map +1 -1
- package/dist/internal/cli-slack.js +9 -1
- package/dist/internal/convert-automation/convert-workflow.d.ts.map +1 -1
- package/dist/internal/convert-automation/convert-workflow.js +2 -10
- package/dist/internal/cursor/account-mcp.d.ts +29 -3
- package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
- package/dist/internal/cursor/account-mcp.js +147 -67
- package/dist/internal/cursor/backend-client.d.ts +11 -6
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +32 -10
- package/dist/internal/cursor-account-mcp-auth.d.ts +11 -0
- package/dist/internal/cursor-account-mcp-auth.d.ts.map +1 -1
- package/dist/internal/cursor-account-mcp-auth.js +69 -0
- package/dist/internal/cursor-agent-template.d.ts +9 -0
- package/dist/internal/cursor-agent-template.d.ts.map +1 -0
- package/dist/internal/cursor-agent-template.js +37 -0
- package/dist/internal/deploy-client.d.ts +1 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +18 -2
- package/dist/internal/deploy-manifest.d.ts +76 -0
- package/dist/internal/deploy-manifest.d.ts.map +1 -0
- package/dist/internal/deploy-manifest.js +117 -0
- package/dist/internal/deployment-scm-event-stream.d.ts +116 -0
- package/dist/internal/deployment-scm-event-stream.d.ts.map +1 -0
- package/dist/internal/deployment-scm-event-stream.js +474 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +111 -10
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +12 -123
- package/dist/internal/init-scaffold.d.ts +8 -0
- package/dist/internal/init-scaffold.d.ts.map +1 -0
- package/dist/internal/init-scaffold.js +168 -0
- package/dist/internal/inline-stdio-mcp.d.ts +2 -0
- package/dist/internal/inline-stdio-mcp.d.ts.map +1 -1
- package/dist/internal/inline-stdio-mcp.js +10 -8
- package/dist/internal/json-schema.d.ts +7 -0
- package/dist/internal/json-schema.d.ts.map +1 -1
- package/dist/internal/json-schema.js +18 -3
- package/dist/internal/mcp-endpoint.js +7 -7
- package/dist/internal/mcp-host.d.ts +18 -0
- package/dist/internal/mcp-host.d.ts.map +1 -1
- package/dist/internal/mcp-host.js +43 -5
- package/dist/internal/mcp-oauth.d.ts +22 -0
- package/dist/internal/mcp-oauth.d.ts.map +1 -1
- package/dist/internal/mcp-oauth.js +40 -1
- package/dist/internal/package-inline-stdio.d.ts.map +1 -1
- package/dist/internal/package-inline-stdio.js +5 -5
- package/dist/internal/review-comments.d.ts +97 -0
- package/dist/internal/review-comments.d.ts.map +1 -1
- package/dist/internal/review-comments.js +151 -0
- package/dist/internal/sanitize-mcp-tool-name.d.ts +20 -0
- package/dist/internal/sanitize-mcp-tool-name.d.ts.map +1 -0
- package/dist/internal/sanitize-mcp-tool-name.js +30 -0
- package/dist/internal/sdk-runner.d.ts +1 -1
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +36 -13
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +18 -0
- package/dist/internal/session-engine.d.ts +66 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +428 -110
- package/dist/internal/shallow-clone.d.ts +170 -0
- package/dist/internal/shallow-clone.d.ts.map +1 -0
- package/dist/internal/shallow-clone.js +412 -0
- package/dist/internal/slack-provision-client.d.ts +2 -0
- package/dist/internal/slack-provision-client.d.ts.map +1 -1
- package/dist/internal/slack-provision-client.js +1 -0
- package/dist/internal/terminal-style.d.ts +18 -0
- package/dist/internal/terminal-style.d.ts.map +1 -1
- package/dist/internal/terminal-style.js +46 -1
- package/dist/internal/turn-lease-store.d.ts +44 -0
- package/dist/internal/turn-lease-store.d.ts.map +1 -0
- package/dist/internal/turn-lease-store.js +139 -0
- package/dist/internal/workspace.d.ts +14 -0
- package/dist/internal/workspace.d.ts.map +1 -1
- package/dist/internal/workspace.js +33 -0
- package/dist/playground/assets/index-77uhu5Kr.css +1 -0
- package/dist/playground/assets/index-D4buox4m.js +85 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-protocol.d.ts +122 -0
- package/dist/storage-protocol.d.ts.map +1 -0
- package/dist/storage-protocol.js +173 -0
- package/dist/storage.d.ts +2 -72
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +2 -82
- package/dist/tools.d.ts +7 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/types.d.ts +141 -6
- package/dist/types.d.ts.map +1 -1
- package/docs/concepts.md +4 -4
- package/docs/guides/mcp-oauth.md +3 -1
- package/docs/reference/agent-config.md +11 -12
- package/docs/reference/connections.md +108 -10
- package/docs/reference/sessions.md +2 -2
- package/docs/reference/tools.md +14 -0
- package/docs/troubleshooting.md +3 -2
- package/package.json +7 -2
- package/skills/create-agent/SKILL.md +12 -9
- package/skills/debug/SKILL.md +2 -1
- package/skills/framework-map/SKILL.md +6 -6
- package/skills/mcp-auth/SKILL.md +8 -6
- package/src/bin/agent-serve.ts +12 -2
- package/src/channels/github/api.ts +6 -1
- package/src/channels/github/binding.ts +21 -0
- package/src/channels/github/checks.ts +179 -0
- package/src/channels/github/cursor-account.ts +9 -6
- package/src/channels/github/index.ts +1 -0
- package/src/channels/github/types.ts +8 -0
- package/src/channels/slack/channel-watch.ts +6 -3
- package/src/channels/slack/dispatch.ts +40 -3
- package/src/channels/slack/manifest.ts +9 -10
- package/src/channels/slack/nudge-store.ts +56 -0
- package/src/channels/slack/slack-channel.ts +3 -0
- package/src/channels/slack/types.ts +24 -7
- package/src/connections.ts +17 -5
- package/src/index.ts +5 -0
- package/src/internal/advertise-tools.ts +278 -0
- package/src/internal/cli-deploy.ts +11 -1
- package/src/internal/cli-manifest.ts +63 -0
- package/src/internal/cli-slack.ts +10 -1
- package/src/internal/convert-automation/convert-workflow.ts +2 -11
- package/src/internal/cursor/account-mcp.ts +208 -73
- package/src/internal/cursor/backend-client.ts +45 -11
- package/src/internal/cursor-account-mcp-auth.ts +83 -0
- package/src/internal/cursor-agent-template.ts +46 -0
- package/src/internal/deploy-client.ts +25 -6
- package/src/internal/deploy-manifest.ts +161 -0
- package/src/internal/deployment-scm-event-stream.ts +593 -0
- package/src/internal/discovery.ts +132 -3
- package/src/internal/init-project.ts +18 -153
- package/src/internal/init-scaffold.ts +176 -0
- package/src/internal/inline-stdio-mcp.ts +13 -0
- package/src/internal/json-schema.ts +24 -3
- package/src/internal/mcp-endpoint.ts +6 -0
- package/src/internal/mcp-host.ts +70 -9
- package/src/internal/mcp-oauth.ts +55 -1
- package/src/internal/package-inline-stdio.ts +5 -1
- package/src/internal/review-comments.ts +245 -0
- package/src/internal/sanitize-mcp-tool-name.ts +33 -0
- package/src/internal/sdk-runner.ts +50 -11
- package/src/internal/server.ts +19 -0
- package/src/internal/session-engine.ts +392 -19
- package/src/internal/shallow-clone.ts +595 -0
- package/src/internal/slack-provision-client.ts +3 -0
- package/src/internal/terminal-style.ts +67 -1
- package/src/internal/turn-lease-store.ts +147 -0
- package/src/internal/workspace.ts +33 -0
- package/src/storage-protocol.ts +247 -0
- package/src/storage.ts +16 -110
- package/src/tools.ts +7 -0
- package/src/types.ts +149 -7
- package/templates/agentic-owners/agent/agent.ts +3 -0
- package/templates/demo/agent/agent.ts +3 -0
- package/templates/pr-autofixer/agent/lib/drive-schemas.ts +4 -1
- package/templates/security-reviewer/agent/agent.ts +3 -0
- package/templates/triage/agent/agent.ts +3 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.5ozYNf74.js +0 -1
- package/dist/docs/assets/reference_connections.md.CSQiNzre.lean.js +0 -1
- package/dist/docs/assets/troubleshooting.md.CuVztCKs.js +0 -1
- package/dist/playground/assets/index-B4rcOgeP.css +0 -1
- package/dist/playground/assets/index-DXcY42Jv.js +0 -85
- /package/dist/docs/assets/{concepts.md.CqOsxbMU.lean.js → concepts.md.BXAm6G-C.lean.js} +0 -0
- /package/dist/docs/assets/{guides_mcp-oauth.md.C7G7IykG.lean.js → guides_mcp-oauth.md.CzEB6RaG.lean.js} +0 -0
- /package/dist/docs/assets/{reference_sessions.md.DBVFi2Sx.lean.js → reference_sessions.md.B0DdlM-K.lean.js} +0 -0
- /package/dist/docs/assets/{troubleshooting.md.CuVztCKs.lean.js → troubleshooting.md.DQ-hYuZT.lean.js} +0 -0
|
@@ -218,6 +218,9 @@ function buildToolMcpServer(
|
|
|
218
218
|
name: tool.name,
|
|
219
219
|
description: tool.description,
|
|
220
220
|
inputSchema: tool.inputSchema,
|
|
221
|
+
...(tool.outputSchema === undefined
|
|
222
|
+
? {}
|
|
223
|
+
: { outputSchema: tool.outputSchema }),
|
|
221
224
|
})),
|
|
222
225
|
})
|
|
223
226
|
);
|
|
@@ -298,6 +301,9 @@ function buildConnectionBridgeMcpServer(
|
|
|
298
301
|
? {}
|
|
299
302
|
: { description: tool.description }),
|
|
300
303
|
inputSchema: tool.inputSchema ?? { type: "object" },
|
|
304
|
+
...(tool.outputSchema === undefined
|
|
305
|
+
? {}
|
|
306
|
+
: { outputSchema: tool.outputSchema }),
|
|
301
307
|
})),
|
|
302
308
|
};
|
|
303
309
|
});
|
package/src/internal/mcp-host.ts
CHANGED
|
@@ -29,6 +29,18 @@ interface OpenedConnection {
|
|
|
29
29
|
close: () => Promise<void>;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* A non-caching view of one connection: every operation opens a fresh
|
|
34
|
+
* client and closes it afterwards. See {@link McpHost.oneOff}.
|
|
35
|
+
*/
|
|
36
|
+
export interface HostMcpOneOff {
|
|
37
|
+
listTools(): Promise<HostMcpToolInfo[]>;
|
|
38
|
+
callTool(
|
|
39
|
+
toolName: string,
|
|
40
|
+
args?: Record<string, unknown>
|
|
41
|
+
): Promise<HostMcpCallResult>;
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
export class McpHost implements HostMcpRegistry {
|
|
33
45
|
private readonly byName = new Map<string, DiscoveredConnection>();
|
|
34
46
|
private readonly clients = new Map<string, Promise<OpenedConnection>>();
|
|
@@ -54,18 +66,47 @@ export class McpHost implements HostMcpRegistry {
|
|
|
54
66
|
async listTools(connectionName: string): Promise<HostMcpToolInfo[]> {
|
|
55
67
|
return this.withClient(connectionName, async (client) => {
|
|
56
68
|
const listed = await client.listTools();
|
|
57
|
-
return listed.tools.map(
|
|
58
|
-
name: tool.name,
|
|
59
|
-
...(tool.description === undefined
|
|
60
|
-
? {}
|
|
61
|
-
: { description: tool.description }),
|
|
62
|
-
...(tool.inputSchema === undefined
|
|
63
|
-
? {}
|
|
64
|
-
: { inputSchema: tool.inputSchema as JsonObject }),
|
|
65
|
-
}));
|
|
69
|
+
return listed.tools.map(toToolInfo);
|
|
66
70
|
});
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
/**
|
|
74
|
+
* A one-off view of a connection that is not (or not exactly) in the
|
|
75
|
+
* registry — e.g. a registered connection with per-session auth headers
|
|
76
|
+
* substituted in. Every operation opens a fresh client and closes it
|
|
77
|
+
* afterwards: the registry's cached per-name client would leak one
|
|
78
|
+
* session's identity into another, and stateless streamable-HTTP
|
|
79
|
+
* servers answer a fresh initialize cheaply. Results ride the same
|
|
80
|
+
* mapping as registry calls.
|
|
81
|
+
*/
|
|
82
|
+
oneOff(connection: DiscoveredConnection): HostMcpOneOff {
|
|
83
|
+
const withFreshClient = async <T>(
|
|
84
|
+
run: (client: Client) => Promise<T>
|
|
85
|
+
): Promise<T> => {
|
|
86
|
+
const opened = await openConnection(connection);
|
|
87
|
+
try {
|
|
88
|
+
return await run(opened.client);
|
|
89
|
+
} finally {
|
|
90
|
+
await opened.close();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
listTools: () =>
|
|
95
|
+
withFreshClient(async (client) => {
|
|
96
|
+
const listed = await client.listTools();
|
|
97
|
+
return listed.tools.map(toToolInfo);
|
|
98
|
+
}),
|
|
99
|
+
callTool: (toolName, args = {}) =>
|
|
100
|
+
withFreshClient(async (client) => {
|
|
101
|
+
const result = await client.callTool({
|
|
102
|
+
name: toolName,
|
|
103
|
+
arguments: args,
|
|
104
|
+
});
|
|
105
|
+
return normalizeCallResult(result);
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
69
110
|
async callTool(
|
|
70
111
|
connectionName: string,
|
|
71
112
|
toolName: string,
|
|
@@ -172,6 +213,26 @@ export function isStaleMcpSessionError(error: unknown): boolean {
|
|
|
172
213
|
return code === 404;
|
|
173
214
|
}
|
|
174
215
|
|
|
216
|
+
function toToolInfo(tool: {
|
|
217
|
+
name: string;
|
|
218
|
+
description?: string;
|
|
219
|
+
inputSchema?: unknown;
|
|
220
|
+
outputSchema?: unknown;
|
|
221
|
+
}): HostMcpToolInfo {
|
|
222
|
+
return {
|
|
223
|
+
name: tool.name,
|
|
224
|
+
...(tool.description === undefined
|
|
225
|
+
? {}
|
|
226
|
+
: { description: tool.description }),
|
|
227
|
+
...(tool.inputSchema === undefined
|
|
228
|
+
? {}
|
|
229
|
+
: { inputSchema: tool.inputSchema as JsonObject }),
|
|
230
|
+
...(tool.outputSchema === undefined
|
|
231
|
+
? {}
|
|
232
|
+
: { outputSchema: tool.outputSchema as JsonObject }),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
175
236
|
async function openConnection(
|
|
176
237
|
connection: DiscoveredConnection
|
|
177
238
|
): Promise<OpenedConnection> {
|
|
@@ -259,8 +259,52 @@ type ProviderArgs = {
|
|
|
259
259
|
onRedirect?: (url: URL) => void;
|
|
260
260
|
tokens?: OAuthTokens;
|
|
261
261
|
clientInformation?: OAuthClientInformationFull;
|
|
262
|
+
/**
|
|
263
|
+
* True when authorize/token requests must NOT carry the RFC 8707
|
|
264
|
+
* `resource` indicator, read off the server's protected-resource
|
|
265
|
+
* metadata (`cursor_omit_resource_indicator`). Entra rejects the
|
|
266
|
+
* indicator with AADSTS9010010, so Microsoft 365 flows through Cursor's
|
|
267
|
+
* REST-as-MCP translator fail pre-auth without this.
|
|
268
|
+
*/
|
|
269
|
+
omitResourceIndicator?: boolean;
|
|
262
270
|
};
|
|
263
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Reads the Cursor REST-as-MCP translator's OAuth quirk off the resource's
|
|
274
|
+
* RFC 9728 protected-resource metadata. Deliberately self-contained
|
|
275
|
+
* (`@cursor/july` publishes to public npm with no private workspace
|
|
276
|
+
* dependencies — see `waitForMcpOAuthCallback`): matches the translator
|
|
277
|
+
* path shape on any origin so local translators work, and fails to the
|
|
278
|
+
* default behavior (indicator kept) when the PRM is unreachable.
|
|
279
|
+
*/
|
|
280
|
+
export async function fetchOmitResourceIndicatorFromPrm(
|
|
281
|
+
resourceUrl: string,
|
|
282
|
+
fetchImpl: typeof fetch = fetch
|
|
283
|
+
): Promise<boolean> {
|
|
284
|
+
let url: URL;
|
|
285
|
+
try {
|
|
286
|
+
url = new URL(resourceUrl);
|
|
287
|
+
} catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
if (!/^\/rest-mcp\/[^/]+\/mcp\/?$/.test(url.pathname)) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
const prmUrl = `${url.origin}/.well-known/oauth-protected-resource${url.pathname.replace(/\/+$/, "")}`;
|
|
294
|
+
try {
|
|
295
|
+
const response = await fetchImpl(prmUrl);
|
|
296
|
+
if (!response.ok) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
const document = (await response.json()) as {
|
|
300
|
+
cursor_omit_resource_indicator?: unknown;
|
|
301
|
+
};
|
|
302
|
+
return document.cursor_omit_resource_indicator === true;
|
|
303
|
+
} catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
264
308
|
export class HostMcpOAuthProvider implements OAuthClientProvider {
|
|
265
309
|
private tokensValue: OAuthTokens | undefined;
|
|
266
310
|
private clientInfo: OAuthClientInformationFull | undefined;
|
|
@@ -268,11 +312,19 @@ export class HostMcpOAuthProvider implements OAuthClientProvider {
|
|
|
268
312
|
private redirect: URL | undefined;
|
|
269
313
|
private readonly meta: OAuthClientMetadata;
|
|
270
314
|
private readonly args: ProviderArgs;
|
|
315
|
+
/**
|
|
316
|
+
* Assigned only when the PRM sets `cursor_omit_resource_indicator`; left
|
|
317
|
+
* absent otherwise so the SDK's default resource validation applies.
|
|
318
|
+
*/
|
|
319
|
+
readonly validateResourceURL: (() => Promise<URL | undefined>) | undefined;
|
|
271
320
|
|
|
272
321
|
constructor(args: ProviderArgs) {
|
|
273
322
|
this.args = args;
|
|
274
323
|
this.tokensValue = args.tokens;
|
|
275
324
|
this.clientInfo = args.clientInformation;
|
|
325
|
+
if (args.omitResourceIndicator === true) {
|
|
326
|
+
this.validateResourceURL = async () => undefined;
|
|
327
|
+
}
|
|
276
328
|
this.meta = {
|
|
277
329
|
redirect_uris: [args.redirectUrl ?? MCP_OAUTH_CALLBACK_URL],
|
|
278
330
|
token_endpoint_auth_method: "none",
|
|
@@ -366,9 +418,10 @@ export async function createHostMcpOAuthProvider(args: {
|
|
|
366
418
|
onRedirect?: (url: URL) => void;
|
|
367
419
|
}): Promise<HostMcpOAuthProvider> {
|
|
368
420
|
const store = args.store ?? new FileMcpOAuthStore();
|
|
369
|
-
const [fileTokens, fileClient] = await Promise.all([
|
|
421
|
+
const [fileTokens, fileClient, omitResourceIndicator] = await Promise.all([
|
|
370
422
|
store.loadTokens(args.identifier, args.resourceUrl),
|
|
371
423
|
store.loadClientInformation(args.identifier, args.resourceUrl),
|
|
424
|
+
fetchOmitResourceIndicatorFromPrm(args.resourceUrl),
|
|
372
425
|
]);
|
|
373
426
|
return new HostMcpOAuthProvider({
|
|
374
427
|
identifier: args.identifier,
|
|
@@ -377,6 +430,7 @@ export async function createHostMcpOAuthProvider(args: {
|
|
|
377
430
|
tokens: fileTokens ?? tokensFromEnv(args.identifier),
|
|
378
431
|
clientInformation: fileClient ?? clientIdFromEnv(args.identifier),
|
|
379
432
|
onRedirect: args.onRedirect,
|
|
433
|
+
omitResourceIndicator,
|
|
380
434
|
});
|
|
381
435
|
}
|
|
382
436
|
|
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
JsonSchemaObject,
|
|
15
15
|
} from "../types.js";
|
|
16
16
|
import type { InlineStdioCatalogEntry } from "./inline-stdio-mcp.js";
|
|
17
|
-
import { toolInputJsonSchema } from "./json-schema.js";
|
|
17
|
+
import { toolInputJsonSchema, toolOutputJsonSchema } from "./json-schema.js";
|
|
18
18
|
|
|
19
19
|
const require = createRequire(import.meta.url);
|
|
20
20
|
|
|
@@ -35,9 +35,13 @@ function jsonSchemaCatalog(
|
|
|
35
35
|
): Record<string, InlineStdioCatalogEntry> {
|
|
36
36
|
const catalog: Record<string, InlineStdioCatalogEntry> = {};
|
|
37
37
|
for (const [name, tool] of Object.entries(tools)) {
|
|
38
|
+
const outputSchema = toolOutputJsonSchema(tool.outputSchema);
|
|
38
39
|
catalog[name] = {
|
|
39
40
|
description: tool.description,
|
|
40
41
|
inputSchema: toolInputJsonSchema(tool.inputSchema) as JsonSchemaObject,
|
|
42
|
+
...(outputSchema === undefined
|
|
43
|
+
? {}
|
|
44
|
+
: { outputSchema: outputSchema as JsonSchemaObject }),
|
|
41
45
|
};
|
|
42
46
|
}
|
|
43
47
|
return catalog;
|
|
@@ -9,6 +9,21 @@ export type ReviewCommentPostResult = {
|
|
|
9
9
|
mode: "review_with_inline_comments" | "review_body_only" | "skipped";
|
|
10
10
|
inlineCommentCount: number;
|
|
11
11
|
detail: string;
|
|
12
|
+
/**
|
|
13
|
+
* Ids of the inline comments this post created, when the publisher could
|
|
14
|
+
* read them back. Callers that later re-read a PR's comments use these as
|
|
15
|
+
* the provenance allowlist: a comment id we did not create is not ours,
|
|
16
|
+
* no matter what its body looks like. Empty when the publisher cannot
|
|
17
|
+
* resolve them (it never guesses).
|
|
18
|
+
*/
|
|
19
|
+
postedComments?: PostedReviewComment[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** One inline comment this agent created, as the SCM assigned it. */
|
|
23
|
+
export type PostedReviewComment = {
|
|
24
|
+
id: number;
|
|
25
|
+
file: string;
|
|
26
|
+
line: number;
|
|
12
27
|
};
|
|
13
28
|
|
|
14
29
|
export type ReviewInlineComment = {
|
|
@@ -41,6 +56,175 @@ export type ReviewCommentPublisherImplementation = {
|
|
|
41
56
|
postReview: (args: ReviewCommentPostArgs) => Promise<ReviewCommentPostResult>;
|
|
42
57
|
};
|
|
43
58
|
|
|
59
|
+
/**
|
|
60
|
+
* SCMs this module can read review comments back from. Same growth seam as
|
|
61
|
+
* `shallow-clone.ts`'s {@link import("./shallow-clone.js").ScmProvider}:
|
|
62
|
+
* adding GitLab/Bitbucket/Origin means a resolver case plus a reader
|
|
63
|
+
* implementation, and nothing above this line changes.
|
|
64
|
+
*/
|
|
65
|
+
export type ScmReviewProvider = "github";
|
|
66
|
+
|
|
67
|
+
/** Which SCM's PR to read review comments from. */
|
|
68
|
+
export type ScmReviewTarget = {
|
|
69
|
+
provider: ScmReviewProvider;
|
|
70
|
+
/** `owner/name`. */
|
|
71
|
+
repo: string;
|
|
72
|
+
prNumber: number;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* One existing inline review comment, provider-neutral.
|
|
77
|
+
*
|
|
78
|
+
* Field names follow Bugbot's `ExistingInlineComment`
|
|
79
|
+
* (`backend/server/src/bugbot/review/applyBugbotPRReview.ts`), which is the
|
|
80
|
+
* shape its resolution judge consumes after normalizing each provider's
|
|
81
|
+
* `getPRComments` output — so the same review logic reads the same way here.
|
|
82
|
+
*/
|
|
83
|
+
export type ExistingReviewComment = {
|
|
84
|
+
id: number;
|
|
85
|
+
file: string;
|
|
86
|
+
startLine: number;
|
|
87
|
+
endLine: number;
|
|
88
|
+
body: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type GetReviewCommentsArgs = {
|
|
92
|
+
target: ScmReviewTarget;
|
|
93
|
+
/** Comment ids to fetch; ids the SCM no longer has are omitted. */
|
|
94
|
+
commentIds: readonly number[];
|
|
95
|
+
signal?: AbortSignal;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Read side of the review-comment seam, mirroring
|
|
100
|
+
* {@link ReviewCommentPublisherImplementation}. Bugbot's equivalent is
|
|
101
|
+
* `IGitProvider.getPRComments`.
|
|
102
|
+
*
|
|
103
|
+
* By id rather than by listing: callers only ever want comments they
|
|
104
|
+
* recorded creating, and a busy PR's comment list runs to thousands, where
|
|
105
|
+
* any page cap silently drops exactly the older comments a re-review needs.
|
|
106
|
+
*/
|
|
107
|
+
export type ReviewCommentReaderImplementation = {
|
|
108
|
+
getReviewComments: (
|
|
109
|
+
args: GetReviewCommentsArgs
|
|
110
|
+
) => Promise<ExistingReviewComment[]>;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** Parse a PR URL into the SCM target to read comments from. */
|
|
114
|
+
export function resolveScmReviewTarget(
|
|
115
|
+
prUrl: string
|
|
116
|
+
): ScmReviewTarget | undefined {
|
|
117
|
+
const parsed = parseGithubPrUrl(prUrl);
|
|
118
|
+
return parsed.kind === "valid"
|
|
119
|
+
? {
|
|
120
|
+
provider: "github",
|
|
121
|
+
repo: parsed.value.repo,
|
|
122
|
+
prNumber: parsed.value.prNumber,
|
|
123
|
+
}
|
|
124
|
+
: undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Reader for a resolved target, mirroring Bugbot's `createPrGitProvider`
|
|
129
|
+
* dispatch. Returns undefined when the host cannot serve that provider (no
|
|
130
|
+
* GitHub client bound), so callers degrade instead of throwing.
|
|
131
|
+
*/
|
|
132
|
+
export function createReviewCommentReader(args: {
|
|
133
|
+
target: ScmReviewTarget;
|
|
134
|
+
github?: HostGitHubClient;
|
|
135
|
+
}): ReviewCommentReaderImplementation | undefined {
|
|
136
|
+
switch (args.target.provider) {
|
|
137
|
+
case "github":
|
|
138
|
+
return args.github === undefined
|
|
139
|
+
? undefined
|
|
140
|
+
: createHostGithubReviewCommentReader(args.github);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Cap on pages pulled per PR; GitHub serves 100 comments per page. */
|
|
145
|
+
const REVIEW_COMMENTS_MAX_PAGES = 5;
|
|
146
|
+
const REVIEW_COMMENTS_PAGE_SIZE = 100;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Reads inline review comments through the host GitHub client (Cursor
|
|
150
|
+
* account lease, App installation token, or PAT).
|
|
151
|
+
*/
|
|
152
|
+
export function createHostGithubReviewCommentReader(
|
|
153
|
+
github: HostGitHubClient
|
|
154
|
+
): ReviewCommentReaderImplementation {
|
|
155
|
+
return {
|
|
156
|
+
async getReviewComments({ target, commentIds, signal }) {
|
|
157
|
+
const [owner, repo] = target.repo.split("/", 2);
|
|
158
|
+
if (owner === undefined || repo === undefined || repo === "") {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
const octokit = await github.getOctokit();
|
|
162
|
+
const comments: ExistingReviewComment[] = [];
|
|
163
|
+
for (const commentId of commentIds) {
|
|
164
|
+
signal?.throwIfAborted();
|
|
165
|
+
try {
|
|
166
|
+
const response = await octokit.rest.pulls.getReviewComment({
|
|
167
|
+
owner,
|
|
168
|
+
repo,
|
|
169
|
+
comment_id: commentId,
|
|
170
|
+
});
|
|
171
|
+
const normalized = normalizeGithubReviewComment(response.data);
|
|
172
|
+
if (normalized !== undefined) {
|
|
173
|
+
comments.push(normalized);
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// Deleted or otherwise unreadable: that finding simply has no
|
|
177
|
+
// prior text to judge, which is the same as it not being there.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return comments;
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
type GithubReviewCommentLike = {
|
|
186
|
+
id: number;
|
|
187
|
+
path: string;
|
|
188
|
+
body: string;
|
|
189
|
+
line?: number | null;
|
|
190
|
+
start_line?: number | null;
|
|
191
|
+
original_line?: number | null;
|
|
192
|
+
original_start_line?: number | null;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* GitHub review comment → {@link ExistingReviewComment}.
|
|
197
|
+
*
|
|
198
|
+
* Prefers the `original_*` fields — the path/lines/commit as of the review
|
|
199
|
+
* the comment belongs to. `line` drifts as the PR gains commits, so a
|
|
200
|
+
* caller correlating a comment with the review that produced it needs the
|
|
201
|
+
* original position, the same fields Bugbot carries on
|
|
202
|
+
* `PullRequestCommentOutput`.
|
|
203
|
+
*/
|
|
204
|
+
export function normalizeGithubReviewComment(
|
|
205
|
+
comment: GithubReviewCommentLike
|
|
206
|
+
): ExistingReviewComment | undefined {
|
|
207
|
+
// Take both lines from the same position set. Mixing them (original end
|
|
208
|
+
// line with the drifted current start line) would describe a range that
|
|
209
|
+
// never existed in either revision.
|
|
210
|
+
const original = comment.original_line ?? undefined;
|
|
211
|
+
const endLine = original ?? comment.line ?? undefined;
|
|
212
|
+
if (endLine === undefined || endLine === null) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
const startLine =
|
|
216
|
+
(original === undefined
|
|
217
|
+
? comment.start_line
|
|
218
|
+
: comment.original_start_line) ?? endLine;
|
|
219
|
+
return {
|
|
220
|
+
id: comment.id,
|
|
221
|
+
file: comment.path,
|
|
222
|
+
startLine,
|
|
223
|
+
endLine,
|
|
224
|
+
body: comment.body,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
44
228
|
type ParsedGithubPrUrl = {
|
|
45
229
|
repo: string;
|
|
46
230
|
prNumber: number;
|
|
@@ -148,6 +332,15 @@ async function postReviewViaHostGithub(
|
|
|
148
332
|
detail: htmlUrl
|
|
149
333
|
? `posted via host GitHub: ${htmlUrl}`
|
|
150
334
|
: `posted via host GitHub with ${inlineCommentCount} inline comment(s)`,
|
|
335
|
+
postedComments: await readPostedComments({
|
|
336
|
+
octokit,
|
|
337
|
+
owner,
|
|
338
|
+
repo,
|
|
339
|
+
prNumber: request.prNumber,
|
|
340
|
+
reviewId: response.data.id,
|
|
341
|
+
expected: inlineCommentCount,
|
|
342
|
+
signal: args.signal,
|
|
343
|
+
}),
|
|
151
344
|
};
|
|
152
345
|
} catch (error) {
|
|
153
346
|
if (args.signal?.aborted) {
|
|
@@ -159,6 +352,58 @@ async function postReviewViaHostGithub(
|
|
|
159
352
|
}
|
|
160
353
|
}
|
|
161
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Ids of the inline comments a just-created review owns.
|
|
357
|
+
*
|
|
358
|
+
* `pulls.createReview` returns the review, not its comments, so they come
|
|
359
|
+
* from the review-scoped listing. Scoping to the review matters: the
|
|
360
|
+
* PR-wide list is ordered oldest-first and runs to thousands on a busy PR,
|
|
361
|
+
* so the comments we just created are the ones a page cap would drop.
|
|
362
|
+
* Best-effort: any failure yields an empty list, which costs the next run
|
|
363
|
+
* its prior findings rather than risking a wrong id. Never throws — the
|
|
364
|
+
* review is already posted by this point.
|
|
365
|
+
*/
|
|
366
|
+
async function readPostedComments(args: {
|
|
367
|
+
octokit: Awaited<ReturnType<HostGitHubClient["getOctokit"]>>;
|
|
368
|
+
owner: string;
|
|
369
|
+
repo: string;
|
|
370
|
+
prNumber: number;
|
|
371
|
+
reviewId: number;
|
|
372
|
+
expected: number;
|
|
373
|
+
signal?: AbortSignal;
|
|
374
|
+
}): Promise<PostedReviewComment[]> {
|
|
375
|
+
if (args.expected === 0) {
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
const posted: PostedReviewComment[] = [];
|
|
380
|
+
for (let page = 1; page <= REVIEW_COMMENTS_MAX_PAGES; page++) {
|
|
381
|
+
args.signal?.throwIfAborted();
|
|
382
|
+
const response = await args.octokit.rest.pulls.listCommentsForReview({
|
|
383
|
+
owner: args.owner,
|
|
384
|
+
repo: args.repo,
|
|
385
|
+
pull_number: args.prNumber,
|
|
386
|
+
review_id: args.reviewId,
|
|
387
|
+
per_page: REVIEW_COMMENTS_PAGE_SIZE,
|
|
388
|
+
page,
|
|
389
|
+
});
|
|
390
|
+
for (const comment of response.data) {
|
|
391
|
+
const line = comment.original_line ?? comment.line;
|
|
392
|
+
if (line === null || line === undefined) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
posted.push({ id: comment.id, file: comment.path, line });
|
|
396
|
+
}
|
|
397
|
+
if (response.data.length < REVIEW_COMMENTS_PAGE_SIZE) {
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return posted;
|
|
402
|
+
} catch {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
162
407
|
async function postReviewViaLocalGh(
|
|
163
408
|
args: ReviewCommentPostArgs
|
|
164
409
|
): Promise<ReviewCommentPostResult> {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool-name sanitization shared by the Cursor-account bridge
|
|
3
|
+
* (`account-mcp.ts`) and advertised connections (`advertise-tools.ts`).
|
|
4
|
+
*
|
|
5
|
+
* Connector display names — and the aggregate `/v1/agent/mcp` names minted
|
|
6
|
+
* from them (`<serverIdentifier>-<tool>`) — can contain spaces, dots, and
|
|
7
|
+
* other characters that are illegal as the tool name a model sees.
|
|
8
|
+
*
|
|
9
|
+
* Character class matches `packages/agent` `normalizeMcpToolName`
|
|
10
|
+
* (`[^a-zA-Z0-9_-]` → `_`). Length cap and the empty fallback match the
|
|
11
|
+
* account bridge (MCP clients commonly enforce `^[a-zA-Z0-9_-]{1,128}$`).
|
|
12
|
+
* Callers that talk to the backend keep the original advertised name.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** MCP clients commonly enforce `^[a-zA-Z0-9_-]{1,128}$` for tool names. */
|
|
16
|
+
export const MAX_MCP_TOOL_NAME_LENGTH = 128;
|
|
17
|
+
|
|
18
|
+
/** Connector display names can contain anything (spaces, dots, emoji). */
|
|
19
|
+
export function sanitizeMcpToolName(name: string): string {
|
|
20
|
+
const cleaned = name
|
|
21
|
+
.replace(/[^a-zA-Z0-9_-]/g, "_")
|
|
22
|
+
.slice(0, MAX_MCP_TOOL_NAME_LENGTH);
|
|
23
|
+
return cleaned === "" ? "tool" : cleaned;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `base` for the first claimant, `base_2` / `base_3` … for colliders. */
|
|
27
|
+
export function suffixMcpToolName(base: string, index: number): string {
|
|
28
|
+
if (index === 0) {
|
|
29
|
+
return base;
|
|
30
|
+
}
|
|
31
|
+
const suffix = `_${index + 1}`;
|
|
32
|
+
return `${base.slice(0, MAX_MCP_TOOL_NAME_LENGTH - suffix.length)}${suffix}`;
|
|
33
|
+
}
|
|
@@ -55,6 +55,7 @@ export type RunnerPrewarmRequest = Pick<
|
|
|
55
55
|
| "sandbox"
|
|
56
56
|
| "tools"
|
|
57
57
|
| "mcpServers"
|
|
58
|
+
| "perSendMcpServers"
|
|
58
59
|
| "subagents"
|
|
59
60
|
| "model"
|
|
60
61
|
>;
|
|
@@ -159,6 +160,28 @@ export function isAuthErrorMessage(message: string | undefined): boolean {
|
|
|
159
160
|
return /authentication error|try logging out and back in/i.test(message);
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
/** Cloud `wait()` copy for `stream_expired` / a missing start-state blob. */
|
|
164
|
+
function isStreamExpiredErrorMessage(message: string | undefined): boolean {
|
|
165
|
+
if (message === undefined) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return /run stream is no longer available|\[stream_expired\]/i.test(message);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function retryReason(
|
|
172
|
+
message: string | undefined,
|
|
173
|
+
isFirstTurn: boolean
|
|
174
|
+
): string | undefined {
|
|
175
|
+
if (isStreamExpiredErrorMessage(message)) {
|
|
176
|
+
return "cloud run stream expired";
|
|
177
|
+
}
|
|
178
|
+
// First-turn auth is the credential; a fresh handle cannot fix it.
|
|
179
|
+
if (!isFirstTurn && isAuthErrorMessage(message)) {
|
|
180
|
+
return "authentication failure";
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
162
185
|
export class CursorSdkRunner implements AgentRunner {
|
|
163
186
|
private readonly agents = new Map<string, CachedAgentHandle>();
|
|
164
187
|
private readonly stores = new Map<string, Promise<LocalAgentStore>>();
|
|
@@ -187,26 +210,23 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
187
210
|
const outcome = await this.runTurnAttempt(tracked);
|
|
188
211
|
if (
|
|
189
212
|
outcome.status !== "error" ||
|
|
190
|
-
!isAuthErrorMessage(outcome.errorMessage) ||
|
|
191
213
|
sawUpdate ||
|
|
192
|
-
request.isFirstTurn ||
|
|
193
214
|
request.signal?.aborted === true
|
|
194
215
|
) {
|
|
195
216
|
return outcome;
|
|
196
217
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
//
|
|
218
|
+
const reason = retryReason(outcome.errorMessage, request.isFirstTurn);
|
|
219
|
+
if (reason === undefined) {
|
|
220
|
+
return outcome;
|
|
221
|
+
}
|
|
222
|
+
// Log the original error even when the retry succeeds so the expiry
|
|
223
|
+
// still shows up in Datadog.
|
|
203
224
|
this.log(
|
|
204
|
-
`[agent-sdk] session ${request.sessionId}: retrying the turn
|
|
225
|
+
`[agent-sdk] session ${request.sessionId}: ${reason}: ${outcome.errorMessage}; retrying the turn`
|
|
205
226
|
);
|
|
206
227
|
try {
|
|
207
228
|
return await this.runTurnAttempt(tracked);
|
|
208
229
|
} catch {
|
|
209
|
-
// The retry was best effort; report the original failure.
|
|
210
230
|
return outcome;
|
|
211
231
|
}
|
|
212
232
|
}
|
|
@@ -235,6 +255,12 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
235
255
|
// Stream mapping must never reject into the SDK transport.
|
|
236
256
|
}
|
|
237
257
|
},
|
|
258
|
+
// The SDK builds this send's MCP session from exactly this set, so
|
|
259
|
+
// a turn always runs on the headers the engine evaluated for it —
|
|
260
|
+
// never on the ones a previous turn opened the cached handle with.
|
|
261
|
+
...(request.runtime === "local" && request.perSendMcpServers === true
|
|
262
|
+
? { mcpServers: request.mcpServers as AgentOptions["mcpServers"] }
|
|
263
|
+
: {}),
|
|
238
264
|
...(request.runtime === "local"
|
|
239
265
|
? {
|
|
240
266
|
local: {
|
|
@@ -270,6 +296,14 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
270
296
|
if (request.onSdkAgentId !== undefined) {
|
|
271
297
|
await request.onSdkAgentId(agent.agentId);
|
|
272
298
|
}
|
|
299
|
+
// Readable on the handle as soon as send resolves, long before wait().
|
|
300
|
+
if (
|
|
301
|
+
request.onRequestId !== undefined &&
|
|
302
|
+
run.requestId !== undefined &&
|
|
303
|
+
run.requestId !== ""
|
|
304
|
+
) {
|
|
305
|
+
await request.onRequestId(run.requestId);
|
|
306
|
+
}
|
|
273
307
|
try {
|
|
274
308
|
const result = await run.wait();
|
|
275
309
|
let outcome = toTurnOutcome(result);
|
|
@@ -401,7 +435,12 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
401
435
|
return {
|
|
402
436
|
model: toModelSelection(request.model ?? DEFAULT_MODEL),
|
|
403
437
|
apiKey: this.requireApiKey(),
|
|
404
|
-
|
|
438
|
+
// Per-send servers ride each agent.send instead (fresh per-session
|
|
439
|
+
// auth headers every turn). Keeping them out of the create/resume
|
|
440
|
+
// options also keeps the local executor cache key — which hashes
|
|
441
|
+
// these options — stable while credentials rotate.
|
|
442
|
+
...(Object.keys(request.mcpServers).length > 0 &&
|
|
443
|
+
request.perSendMcpServers !== true
|
|
405
444
|
? { mcpServers: request.mcpServers as AgentOptions["mcpServers"] }
|
|
406
445
|
: {}),
|
|
407
446
|
...(subagents === undefined ? {} : { agents: subagents }),
|
package/src/internal/server.ts
CHANGED
|
@@ -70,12 +70,14 @@ import {
|
|
|
70
70
|
scmEventToGitHubPayload,
|
|
71
71
|
scmMetadataToOriginPayload,
|
|
72
72
|
} from "./cursor-event-relay.js";
|
|
73
|
+
import { AGENT_SERVE_GENERATION_ENV_VAR } from "./cursor-relay-core.js";
|
|
73
74
|
import {
|
|
74
75
|
type CursorSlackAgentRegistration,
|
|
75
76
|
CursorSlackRelay,
|
|
76
77
|
createCursorSlackApiCaller,
|
|
77
78
|
formatCursorSlackEventLog,
|
|
78
79
|
} from "./cursor-slack-relay.js";
|
|
80
|
+
import { buildAgentDeployManifest } from "./deploy-manifest.js";
|
|
79
81
|
import {
|
|
80
82
|
formatDiagnostics,
|
|
81
83
|
hasErrorDiagnostics,
|
|
@@ -1111,6 +1113,23 @@ function mountAgentRoutes(
|
|
|
1111
1113
|
handler: () => jsonResponse(projectInfo(project)),
|
|
1112
1114
|
});
|
|
1113
1115
|
|
|
1116
|
+
// Hosted-only: the control plane's provision flow reads the deploy manifest
|
|
1117
|
+
// from the running engine (same derivation as `july manifest`) right after
|
|
1118
|
+
// the health probe, so every deploy surface — CLI, UI, redeploy — gets its
|
|
1119
|
+
// declarations from the installed code without a client-side extraction
|
|
1120
|
+
// step. `auth: "none"` mirrors `/v1/health`: hosted engines are reachable
|
|
1121
|
+
// only through the anyrun port proxy (network token) or the alias route
|
|
1122
|
+
// (alias token), and the route is not registered outside hosted mode, so
|
|
1123
|
+
// self-hosted `july serve` never exposes the manifest unauthenticated.
|
|
1124
|
+
if (process.env[AGENT_SERVE_GENERATION_ENV_VAR] !== undefined) {
|
|
1125
|
+
router.add({
|
|
1126
|
+
method: "GET",
|
|
1127
|
+
pattern: `${base}/v1/manifest`,
|
|
1128
|
+
auth: "none",
|
|
1129
|
+
handler: () => jsonResponse(buildAgentDeployManifest(project)),
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1114
1133
|
// Playground Logs tab: recent ServeOptions.logger lines (channels, sessions,
|
|
1115
1134
|
// webhook capture). `after` is an exclusive index cursor for polling.
|
|
1116
1135
|
router.add({
|