@cursor/july 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/dist/bin/agent-serve.js +26 -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.Ciosf24M.js → app.BcXUGca7.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.BIbvK8M1.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BcRDly7n.js → VPLocalSearchBox.CWur6XGZ.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CMpbjdfx.js → theme.BnvulOX2.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.D189RBCH.js → reference_cli.md.B74NrVzb.js} +5 -4
- package/dist/docs/assets/{reference_cli.md.D189RBCH.lean.js → reference_cli.md.B74NrVzb.lean.js} +1 -1
- package/dist/docs/assets/{reference_connections.md.zaEYCLHT.js → reference_connections.md.CsSr3rDz.js} +9 -4
- package/dist/docs/assets/{reference_connections.md.zaEYCLHT.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 +8 -7
- package/dist/docs/reference/connections.html +13 -8
- 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-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +7 -4
- 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/corp-ca.d.ts +9 -0
- package/dist/internal/corp-ca.d.ts.map +1 -0
- package/dist/internal/corp-ca.js +44 -0
- 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/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/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 +6 -0
- package/dist/internal/resolved-connections.d.ts.map +1 -1
- package/dist/internal/resolved-connections.js +10 -3
- 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 +24 -6
- package/dist/internal/session-engine.d.ts +30 -10
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +166 -74
- package/dist/internal/sessions-client.d.ts +16 -2
- package/dist/internal/sessions-client.d.ts.map +1 -1
- package/dist/internal/sessions-client.js +30 -13
- package/dist/internal/storage-coordinator.d.ts +11 -0
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +30 -0
- 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-C0-QEG4O.js → index-BIGLubwK.js} +46 -46
- package/dist/playground/assets/{index-z3JY9O2x.css → index-C17ZQKDG.css} +1 -1
- 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/storage-backends/cursor-hosted.d.ts +10 -0
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/storage-backends/cursor-hosted.js +14 -0
- package/dist/types.d.ts +93 -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 +30 -0
- package/docs/reference/connections.md +32 -2
- 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 +30 -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-ax.ts +10 -4
- 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/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 +20 -5
- package/src/internal/sdk-runner.ts +37 -1
- package/src/internal/server.ts +28 -6
- package/src/internal/session-engine.ts +192 -76
- package/src/internal/sessions-client.ts +51 -15
- package/src/internal/storage-coordinator.ts +36 -0
- package/src/internal/stream-progress.ts +8 -0
- package/src/internal/turn-governor.ts +145 -0
- package/src/prompt.ts +83 -0
- package/src/storage-backends/cursor-hosted.ts +21 -0
- package/src/types.ts +92 -6
- package/dist/docs/assets/chunks/@localSearchIndexroot.D154Detj.js +0 -1
|
@@ -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;
|
|
@@ -78,13 +83,23 @@ export function assertCloudCanHonorAccountServersFilters(args: {
|
|
|
78
83
|
/** Fail closed when anonymous HTTP admission would expose account connectors. */
|
|
79
84
|
export function assertAccountConnectionsNotAnonymous(args: {
|
|
80
85
|
allowAnonymous?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Opt in when every admitted caller is already trusted (e.g. behind an
|
|
88
|
+
* authenticating proxy / hosted alias token). Same trust model as
|
|
89
|
+
* `--allow-anonymous-cursor-github`.
|
|
90
|
+
*/
|
|
91
|
+
allowAnonymousCursorAccountMcp?: boolean;
|
|
81
92
|
hasAccountConnections: boolean;
|
|
82
93
|
}): void {
|
|
83
|
-
if (args.allowAnonymous
|
|
84
|
-
|
|
85
|
-
"[agentkit] --allow-anonymous cannot be used with Cursor account MCP connections (defineConnection({ cursorAccount: true })); use --bearer-token or remove the account MCP connections."
|
|
86
|
-
);
|
|
94
|
+
if (args.allowAnonymous !== true || !args.hasAccountConnections) {
|
|
95
|
+
return;
|
|
87
96
|
}
|
|
97
|
+
if (args.allowAnonymousCursorAccountMcp === true) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
throw new Error(
|
|
101
|
+
"[agentkit] --allow-anonymous cannot be used with Cursor account MCP connections (defineConnection({ cursorAccount: true })); use --bearer-token, or opt in with --allow-anonymous-cursor-account-mcp behind a trusted boundary."
|
|
102
|
+
);
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
export function resolveMountConnections(args: {
|
|
@@ -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 };
|
package/src/internal/server.ts
CHANGED
|
@@ -49,7 +49,6 @@ import {
|
|
|
49
49
|
} from "./cursor/account-mcp.js";
|
|
50
50
|
import { CursorBackendClient } from "./cursor/backend-client.js";
|
|
51
51
|
import {
|
|
52
|
-
cursorBackendUrl,
|
|
53
52
|
cursorExternalApiUrl,
|
|
54
53
|
type ResolvedApiKey,
|
|
55
54
|
resolveApiKey,
|
|
@@ -286,9 +285,10 @@ export async function startServer(
|
|
|
286
285
|
}
|
|
287
286
|
const runner =
|
|
288
287
|
options.runner ??
|
|
289
|
-
new CursorSdkRunner(
|
|
290
|
-
accountKey === undefined ? {} : { apiKey: accountKey.apiKey }
|
|
291
|
-
|
|
288
|
+
new CursorSdkRunner({
|
|
289
|
+
...(accountKey === undefined ? {} : { apiKey: accountKey.apiKey }),
|
|
290
|
+
logger,
|
|
291
|
+
});
|
|
292
292
|
|
|
293
293
|
const router = new Router();
|
|
294
294
|
const runtimes: MountedRuntime[] = [];
|
|
@@ -307,8 +307,18 @@ export async function startServer(
|
|
|
307
307
|
const accountRefsBySlug = collectCursorAccountConnections(mounts);
|
|
308
308
|
assertAccountConnectionsNotAnonymous({
|
|
309
309
|
allowAnonymous: options.allowAnonymous,
|
|
310
|
+
allowAnonymousCursorAccountMcp: options.allowAnonymousCursorAccountMcp,
|
|
310
311
|
hasAccountConnections: accountRefsBySlug.size > 0,
|
|
311
312
|
});
|
|
313
|
+
if (
|
|
314
|
+
options.allowAnonymous === true &&
|
|
315
|
+
options.allowAnonymousCursorAccountMcp === true &&
|
|
316
|
+
accountRefsBySlug.size > 0
|
|
317
|
+
) {
|
|
318
|
+
logger(
|
|
319
|
+
"[agentkit] warning: --allow-anonymous-cursor-account-mcp — every admitted HTTP caller can drive Cursor account MCP connectors; serve this only behind a trusted boundary (e.g. an SSO proxy or hosted alias token)"
|
|
320
|
+
);
|
|
321
|
+
}
|
|
312
322
|
let accountBridge: CursorAccountMcpBridge | undefined;
|
|
313
323
|
if (accountRefsBySlug.size > 0) {
|
|
314
324
|
if (accountKey === undefined) {
|
|
@@ -377,6 +387,12 @@ export async function startServer(
|
|
|
377
387
|
reminderRunner.cancelForContinuation(token, reason),
|
|
378
388
|
dispatch: (id) => reminderRunner.dispatch(id),
|
|
379
389
|
});
|
|
390
|
+
engine.setEvals({
|
|
391
|
+
list: () => evalRuns.list(),
|
|
392
|
+
get: (runId) => evalRuns.get(runId),
|
|
393
|
+
listRunsWithActive: () => evalRuns.listRunsWithActive(),
|
|
394
|
+
start: (input) => evalRuns.start(input),
|
|
395
|
+
});
|
|
380
396
|
const runtime: MountedRuntime = {
|
|
381
397
|
slug,
|
|
382
398
|
project,
|
|
@@ -914,8 +930,12 @@ function mountAgentRoutes(
|
|
|
914
930
|
(relaxSessionReadAuthBeyondLoopback ||
|
|
915
931
|
(typeof ctx.requestIp === "string" &&
|
|
916
932
|
isLoopbackAddress(ctx.requestIp)));
|
|
933
|
+
const listed = await engine.listSessionsWithSource(ctx.auth, {
|
|
934
|
+
includeAll,
|
|
935
|
+
});
|
|
917
936
|
return jsonResponse({
|
|
918
|
-
sessions:
|
|
937
|
+
sessions: listed.sessions,
|
|
938
|
+
source: listed.source,
|
|
919
939
|
});
|
|
920
940
|
},
|
|
921
941
|
});
|
|
@@ -1595,7 +1615,9 @@ async function startCursorSlackRelay(input: {
|
|
|
1595
1615
|
`This project has Slack channels using the Cursor account connection (slackChannel({ cursorAccount: true })) but the host is not signed in to Cursor. ${SIGN_IN_HINT}`
|
|
1596
1616
|
);
|
|
1597
1617
|
}
|
|
1598
|
-
|
|
1618
|
+
// Same host split as scm-events: `/v0/slack-events/*` is on api.cursor.com,
|
|
1619
|
+
// not api2 (accountKey.backendUrl 404s these routes).
|
|
1620
|
+
const baseUrl = cursorExternalApiUrl();
|
|
1599
1621
|
const baseStateRoot =
|
|
1600
1622
|
options.stateRoot ??
|
|
1601
1623
|
join(targets[0].runtime.project.rootDir, ".agent-serve");
|