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