@cursor/july 0.1.20 → 0.1.21
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/dist/bin/agent-serve.js +0 -0
- package/dist/channels/github/instrument.d.ts +20 -0
- package/dist/channels/github/instrument.d.ts.map +1 -0
- package/dist/channels/slack/api.d.ts +2 -0
- package/dist/channels/slack/api.d.ts.map +1 -1
- package/dist/channels/slack/api.js +3 -0
- package/dist/channels/slack/bot-mentions.d.ts +106 -0
- package/dist/channels/slack/bot-mentions.d.ts.map +1 -0
- package/dist/channels/slack/bot-mentions.js +243 -0
- package/dist/channels/slack/dispatch.d.ts +6 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +24 -3
- package/dist/channels/slack/inbound.d.ts +10 -1
- package/dist/channels/slack/inbound.d.ts.map +1 -1
- package/dist/channels/slack/inbound.js +12 -4
- 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 +61 -22
- package/dist/channels/slack/types.d.ts +58 -0
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +1 -1
- package/dist/docs/ab.html +2 -2
- package/dist/docs/assets/{app.T-5ImnM3.js → app.jDxLzWv4.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DoJHJjqF.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.OMGMWH6H.js → VPLocalSearchBox.Y6bDR1-a.js} +1 -1
- package/dist/docs/assets/chunks/{theme.DLg4yB4D.js → theme.CLazCWlJ.js} +2 -2
- package/dist/docs/building-with-agents.html +2 -2
- package/dist/docs/concepts.html +2 -2
- package/dist/docs/deployment.html +2 -2
- package/dist/docs/evals.html +2 -2
- package/dist/docs/example-agents/approval-buddy.html +2 -2
- package/dist/docs/example-agents/benny.html +2 -2
- package/dist/docs/example-agents/bugbot.html +2 -2
- package/dist/docs/example-agents/codebase-wiki.html +2 -2
- package/dist/docs/example-agents/codeowners-review.html +2 -2
- package/dist/docs/example-agents/concierge.html +2 -2
- package/dist/docs/example-agents/fsd.html +2 -2
- package/dist/docs/example-agents/index.html +2 -2
- package/dist/docs/example-agents/knowledge-base.html +2 -2
- package/dist/docs/example-agents/oncall.html +2 -2
- package/dist/docs/example-agents/security-reviewer.html +2 -2
- package/dist/docs/example-agents/slack-agent.html +2 -2
- package/dist/docs/example-agents/weather-agent.html +2 -2
- package/dist/docs/guides/agent-to-agent.html +2 -2
- package/dist/docs/guides/cloud-runtime.html +2 -2
- package/dist/docs/guides/github.html +2 -2
- package/dist/docs/guides/human-in-the-loop.html +2 -2
- package/dist/docs/guides/mcp-oauth.html +2 -2
- package/dist/docs/guides/slack.html +2 -2
- package/dist/docs/guides/webhooks.html +2 -2
- package/dist/docs/hillclimbing.html +2 -2
- package/dist/docs/index.html +2 -2
- package/dist/docs/quickstart.html +2 -2
- package/dist/docs/reference/agent-config.html +2 -2
- package/dist/docs/reference/channels.html +2 -2
- package/dist/docs/reference/cli.html +2 -2
- package/dist/docs/reference/connections.html +2 -2
- package/dist/docs/reference/hooks.html +2 -2
- package/dist/docs/reference/http-api.html +2 -2
- package/dist/docs/reference/instructions.html +2 -2
- package/dist/docs/reference/playground.html +2 -2
- package/dist/docs/reference/project-layout.html +2 -2
- package/dist/docs/reference/prompt.html +2 -2
- package/dist/docs/reference/schedules.html +2 -2
- package/dist/docs/reference/sessions.html +2 -2
- package/dist/docs/reference/skills.html +2 -2
- package/dist/docs/reference/subagents.html +2 -2
- package/dist/docs/reference/tools.html +2 -2
- package/dist/docs/scaffolding-agents.html +2 -2
- package/dist/docs/storage.html +2 -2
- package/dist/docs/troubleshooting.html +2 -2
- package/dist/internal/json-dir-store.d.ts +32 -0
- package/dist/internal/json-dir-store.d.ts.map +1 -0
- package/dist/internal/json-dir-store.js +100 -0
- package/dist/internal/resolved-connections.d.ts +1 -1
- package/dist/internal/resolved-connections.d.ts.map +1 -1
- package/dist/internal/resolved-connections.js +10 -0
- package/dist/internal/session-engine.d.ts +4 -1
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +13 -2
- package/dist/playground/assets/index-CjOQ4hN9.css +1 -0
- package/dist/playground/assets/index-dshZQJCp.js +85 -0
- package/dist/playground/index.html +2 -2
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -0
- package/package.json +24 -24
- package/src/bin/agent-serve.version.test.ts +62 -0
- package/src/channels/github/api.test.ts +64 -0
- package/src/channels/github/auth.test.ts +105 -0
- package/src/channels/github/cursor-account.test.ts +204 -0
- package/src/channels/github/forward.test.ts +457 -0
- package/src/channels/github/github.test.ts +937 -0
- package/src/channels/github/replay.test.ts +179 -0
- package/src/channels/slack/api.post-message.test.ts +148 -0
- package/src/channels/slack/api.ts +5 -0
- package/src/channels/slack/approvals.test.ts +328 -0
- package/src/channels/slack/block-actions.test.ts +452 -0
- package/src/channels/slack/bot-mentions.test.ts +217 -0
- package/src/channels/slack/bot-mentions.ts +315 -0
- package/src/channels/slack/channel-watch.test.ts +363 -0
- package/src/channels/slack/cursor-account.test.ts +253 -0
- package/src/channels/slack/defaults.final-post.test.ts +182 -0
- package/src/channels/slack/dispatch.test.ts +795 -0
- package/src/channels/slack/dispatch.ts +27 -1
- package/src/channels/slack/eval-directive.test.ts +273 -0
- package/src/channels/slack/inbound.ts +25 -4
- package/src/channels/slack/index.ts +1 -0
- package/src/channels/slack/message-body.test.ts +54 -0
- package/src/channels/slack/nudge-store.test.ts +143 -0
- package/src/channels/slack/slack-channel.ts +66 -8
- package/src/channels/slack/slack.test.ts +391 -0
- package/src/channels/slack/stop.test.ts +23 -0
- package/src/channels/slack/thread-context.test.ts +202 -0
- package/src/channels/slack/types.ts +60 -0
- package/src/evals/assertions.test.ts +580 -0
- package/src/evals/expect.test.ts +144 -0
- package/src/evals/judge.test.ts +181 -0
- package/src/evals/loaders.test.ts +132 -0
- package/src/evals/matchers.test.ts +95 -0
- package/src/evals/reporters.test.ts +303 -0
- package/src/evals/run-facts.test.ts +259 -0
- package/src/internal/ab-snapshot.test.ts +325 -0
- package/src/internal/approval-gate.test.ts +49 -0
- package/src/internal/approvals.integration.test.ts +383 -0
- package/src/internal/authored-loaders.test.ts +31 -0
- package/src/internal/builtin-tools/reminders.test.ts +201 -0
- package/src/internal/channel-route-schema.test.ts +294 -0
- package/src/internal/chat-attach.test.ts +262 -0
- package/src/internal/cli-deploy.test.ts +1991 -0
- package/src/internal/cli-mcp.test.ts +789 -0
- package/src/internal/cli-skills.test.ts +133 -0
- package/src/internal/cli-slack.test.ts +1647 -0
- package/src/internal/cloud-merge.test.ts +74 -0
- package/src/internal/cron.test.ts +22 -0
- package/src/internal/cursor/account-mcp.test.ts +807 -0
- package/src/internal/cursor/backend-client.test.ts +591 -0
- package/src/internal/cursor/credentials.test.ts +351 -0
- package/src/internal/cursor/github-credentials.test.ts +136 -0
- package/src/internal/cursor-account-mcp-auth.test.ts +310 -0
- package/src/internal/cursor-account.integration.test.ts +441 -0
- package/src/internal/cursor-event-relay.test.ts +746 -0
- package/src/internal/cursor-github-credentials.integration.test.ts +271 -0
- package/src/internal/cursor-slack-relay.test.ts +525 -0
- package/src/internal/deploy-source.test.ts +111 -0
- package/src/internal/discovery.builtin-tools.test.ts +94 -0
- package/src/internal/discovery.concurrency.test.ts +60 -0
- package/src/internal/discovery.cursor-account.test.ts +133 -0
- package/src/internal/discovery.cwd.test.ts +83 -0
- package/src/internal/discovery.hosting.test.ts +80 -0
- package/src/internal/discovery.identity.test.ts +44 -0
- package/src/internal/docs-site.test.ts +66 -0
- package/src/internal/duration.test.ts +29 -0
- package/src/internal/eval-judge-model.test.ts +187 -0
- package/src/internal/eval-run-store.cancel.test.ts +142 -0
- package/src/internal/eval-run-store.storage.test.ts +211 -0
- package/src/internal/eval-runner.http.test.ts +403 -0
- package/src/internal/eval-runner.run.test.ts +928 -0
- package/src/internal/evals-client.test.ts +307 -0
- package/src/internal/event-mapper.test.ts +243 -0
- package/src/internal/github-fanout.test.ts +213 -0
- package/src/internal/handleAgentServeTrigger.test.ts +179 -0
- package/src/internal/host-kv.test.ts +82 -0
- package/src/internal/host-platforms.test.ts +126 -0
- package/src/internal/http-channel.test.ts +402 -0
- package/src/internal/init-project.test.ts +269 -0
- package/src/internal/install-cursor-skills.test.ts +262 -0
- package/src/internal/local-env.test.ts +100 -0
- package/src/internal/log-ring.test.ts +31 -0
- package/src/internal/logs-client.test.ts +350 -0
- package/src/internal/mcp-endpoint.test.ts +436 -0
- package/src/internal/mcp-host.test.ts +298 -0
- package/src/internal/mcp-oauth.test.ts +148 -0
- package/src/internal/net.test.ts +17 -0
- package/src/internal/peer-connections.test.ts +128 -0
- package/src/internal/peer-mcp.integration.test.ts +289 -0
- package/src/internal/playground/toolchain.test.ts +53 -0
- package/src/internal/playground-cli.test.ts +187 -0
- package/src/internal/playground-proxy.test.ts +376 -0
- package/src/internal/prompt-context.integration.test.ts +232 -0
- package/src/internal/prompt-context.test.ts +127 -0
- package/src/internal/reminder-runner.test.ts +390 -0
- package/src/internal/reminder-store.test.ts +53 -0
- package/src/internal/request-headers.test.ts +27 -0
- package/src/internal/resolve-prod-target.test.ts +787 -0
- package/src/internal/resolved-connections.test.ts +295 -0
- package/src/internal/resolved-connections.ts +18 -1
- package/src/internal/router.test.ts +57 -0
- package/src/internal/sdk-runner.test.ts +290 -0
- package/src/internal/session-engine.coalesce.test.ts +169 -0
- package/src/internal/session-engine.concurrency.test.ts +250 -0
- package/src/internal/session-engine.host-oauth-mcp.test.ts +110 -0
- package/src/internal/session-engine.interrupt.test.ts +577 -0
- package/src/internal/session-engine.storage.test.ts +547 -0
- package/src/internal/session-engine.ts +15 -1
- package/src/internal/session-urls.test.ts +28 -0
- package/src/internal/sessions-client.test.ts +518 -0
- package/src/internal/storage-coordinator.test.ts +517 -0
- package/src/internal/tool-call.test.ts +458 -0
- package/src/internal/tool-result.test.ts +52 -0
- package/src/internal/trajectory.approvals.test.ts +83 -0
- package/src/internal/trajectory.subagents.test.ts +198 -0
- package/src/internal/turn-governor.test.ts +137 -0
- package/src/internal/update-check.test.ts +485 -0
- package/src/internal/workspace.test.ts +81 -0
- package/src/storage-backends/cursor-hosted.test.ts +121 -0
- package/src/types.ts +12 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.CvejISXF.js +0 -1
- package/dist/playground/assets/index-CidizGZv.css +0 -1
- package/dist/playground/assets/index-CuiAKZaP.js +0 -85
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { httpChannel } from "../../channels.js";
|
|
3
|
+
import type { AgentProject, ConnectionTransport } from "../../types.js";
|
|
4
|
+
import {
|
|
5
|
+
CursorAccountMcpBridge,
|
|
6
|
+
collectCursorAccountConnections,
|
|
7
|
+
cursorAccountMcpPath,
|
|
8
|
+
diagnoseConnectorError,
|
|
9
|
+
formatServerStatusSummary,
|
|
10
|
+
resolveCursorAccountConnections,
|
|
11
|
+
} from "./account-mcp.js";
|
|
12
|
+
import { CursorBackendClient } from "./backend-client.js";
|
|
13
|
+
|
|
14
|
+
function makeProject(
|
|
15
|
+
name: string,
|
|
16
|
+
connections: Array<{ name: string; transport: ConnectionTransport }> = []
|
|
17
|
+
): AgentProject {
|
|
18
|
+
return {
|
|
19
|
+
rootDir: `/tmp/account-fixture-${name}`,
|
|
20
|
+
agentDir: `/tmp/account-fixture-${name}/agent`,
|
|
21
|
+
name,
|
|
22
|
+
agent: {
|
|
23
|
+
name,
|
|
24
|
+
runtime: "local",
|
|
25
|
+
tools: [],
|
|
26
|
+
skills: [],
|
|
27
|
+
connections,
|
|
28
|
+
subagents: [],
|
|
29
|
+
seedFiles: [],
|
|
30
|
+
},
|
|
31
|
+
httpChannel: httpChannel(),
|
|
32
|
+
channels: [],
|
|
33
|
+
schedules: [],
|
|
34
|
+
hooks: [],
|
|
35
|
+
abs: [],
|
|
36
|
+
diagnostics: [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function fakeJwt(): string {
|
|
41
|
+
const claims = Buffer.from(
|
|
42
|
+
JSON.stringify({ exp: Math.floor(Date.now() / 1000) + 3600 })
|
|
43
|
+
).toString("base64url");
|
|
44
|
+
return `header.${claims}.signature`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A stubbed backend client whose listings/executions come from `handlers`. */
|
|
48
|
+
function stubClient(handlers: {
|
|
49
|
+
list?: (serverIdentifiers: readonly string[]) => unknown;
|
|
50
|
+
execute?: (body: {
|
|
51
|
+
serverIdentifier: string;
|
|
52
|
+
toolName: string;
|
|
53
|
+
args: Record<string, unknown>;
|
|
54
|
+
}) => unknown;
|
|
55
|
+
onList?: () => void;
|
|
56
|
+
}): CursorBackendClient {
|
|
57
|
+
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
58
|
+
const url = String(input);
|
|
59
|
+
if (url.endsWith("/auth/exchange_user_api_key")) {
|
|
60
|
+
return Response.json({ accessToken: fakeJwt(), refreshToken: "r" });
|
|
61
|
+
}
|
|
62
|
+
const body = JSON.parse(String(init?.body ?? "{}")) as Record<
|
|
63
|
+
string,
|
|
64
|
+
unknown
|
|
65
|
+
>;
|
|
66
|
+
if (url.endsWith("/ListSandMcpTools")) {
|
|
67
|
+
handlers.onList?.();
|
|
68
|
+
return Response.json(
|
|
69
|
+
handlers.list?.((body.serverIdentifiers as string[]) ?? []) ?? {
|
|
70
|
+
servers: [],
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (url.endsWith("/ExecuteSandMcpTool")) {
|
|
75
|
+
return Response.json(
|
|
76
|
+
handlers.execute?.(
|
|
77
|
+
body as {
|
|
78
|
+
serverIdentifier: string;
|
|
79
|
+
toolName: string;
|
|
80
|
+
args: Record<string, unknown>;
|
|
81
|
+
}
|
|
82
|
+
) ?? { result: { error: { error: "no execute stub" } } }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return new Response("{}", { status: 404 });
|
|
86
|
+
}) as typeof fetch;
|
|
87
|
+
return new CursorBackendClient({
|
|
88
|
+
backendUrl: "https://backend.example",
|
|
89
|
+
apiKey: "crsr_key",
|
|
90
|
+
fetchImpl,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const CONNECTED_LINEAR = {
|
|
95
|
+
servers: [
|
|
96
|
+
{
|
|
97
|
+
serverIdentifier: "Linear",
|
|
98
|
+
status: "connected",
|
|
99
|
+
tools: [
|
|
100
|
+
{
|
|
101
|
+
name: "Linear-list_issues",
|
|
102
|
+
description: "List issues.",
|
|
103
|
+
inputSchema: { type: "object" },
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
function rpcRequest(secret: string, body: unknown): Request {
|
|
111
|
+
return new Request("http://127.0.0.1:3000/v1/cursor-account/cursor/mcp", {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: {
|
|
114
|
+
"content-type": "application/json",
|
|
115
|
+
accept: "application/json, text/event-stream",
|
|
116
|
+
authorization: `Bearer ${secret}`,
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify(body),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
describe("collectCursorAccountConnections", () => {
|
|
123
|
+
it("groups account MCP connections by mount slug and keeps filters", () => {
|
|
124
|
+
const mounts = [
|
|
125
|
+
{
|
|
126
|
+
slug: "bugbot",
|
|
127
|
+
project: makeProject("bugbot", [
|
|
128
|
+
{
|
|
129
|
+
name: "cursor",
|
|
130
|
+
transport: { cursorAccount: true, servers: ["Linear"] },
|
|
131
|
+
},
|
|
132
|
+
{ name: "linear", transport: { url: "https://mcp.example/mcp" } },
|
|
133
|
+
]),
|
|
134
|
+
},
|
|
135
|
+
{ slug: "plain", project: makeProject("plain") },
|
|
136
|
+
];
|
|
137
|
+
const collected = collectCursorAccountConnections(mounts);
|
|
138
|
+
expect([...collected.keys()]).toEqual(["bugbot"]);
|
|
139
|
+
expect(collected.get("bugbot")).toEqual([
|
|
140
|
+
{ name: "cursor", servers: ["Linear"] },
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("resolveCursorAccountConnections", () => {
|
|
146
|
+
it("resolves loopback bridge URLs with the boot secret and no cloudUrl", () => {
|
|
147
|
+
const resolved = resolveCursorAccountConnections({
|
|
148
|
+
refs: [{ name: "cursor" }],
|
|
149
|
+
serverUrl: "http://127.0.0.1:4100",
|
|
150
|
+
basePath: "/bugbot",
|
|
151
|
+
bridgeSecret: "s3cret",
|
|
152
|
+
});
|
|
153
|
+
expect(resolved).toEqual([
|
|
154
|
+
{
|
|
155
|
+
name: "cursor",
|
|
156
|
+
localUrl: `http://127.0.0.1:4100/bugbot${cursorAccountMcpPath("cursor")}`,
|
|
157
|
+
headers: { authorization: "Bearer s3cret" },
|
|
158
|
+
},
|
|
159
|
+
]);
|
|
160
|
+
expect(resolved[0]).not.toHaveProperty("cloudUrl");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("resolves cloudUrl from publicUrl so cloud turns use the filtered bridge", () => {
|
|
164
|
+
const resolved = resolveCursorAccountConnections({
|
|
165
|
+
refs: [{ name: "cursor", servers: ["Linear"] }],
|
|
166
|
+
serverUrl: "http://127.0.0.1:4100",
|
|
167
|
+
basePath: "/bugbot",
|
|
168
|
+
bridgeSecret: "s3cret",
|
|
169
|
+
publicUrl: "https://agents.example.com",
|
|
170
|
+
});
|
|
171
|
+
expect(resolved[0]?.cloudUrl).toBe(
|
|
172
|
+
`https://agents.example.com/bugbot${cursorAccountMcpPath("cursor")}`
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("uses the serve bind host for localUrl when not loopback", () => {
|
|
177
|
+
const resolved = resolveCursorAccountConnections({
|
|
178
|
+
refs: [{ name: "cursor" }],
|
|
179
|
+
serverUrl: "http://192.168.1.50:4100",
|
|
180
|
+
basePath: "",
|
|
181
|
+
bridgeSecret: "s3cret",
|
|
182
|
+
});
|
|
183
|
+
expect(resolved[0]?.localUrl).toBe(
|
|
184
|
+
`http://192.168.1.50:4100${cursorAccountMcpPath("cursor")}`
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("CursorAccountMcpBridge auth", () => {
|
|
190
|
+
const request = (secret?: string): Request =>
|
|
191
|
+
new Request("http://127.0.0.1:3000/v1/cursor-account/cursor/mcp", {
|
|
192
|
+
method: "POST",
|
|
193
|
+
...(secret === undefined
|
|
194
|
+
? {}
|
|
195
|
+
: { headers: { authorization: `Bearer ${secret}` } }),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("admits only loopback callers presenting the boot secret by default", async () => {
|
|
199
|
+
const bridge = new CursorAccountMcpBridge({ client: stubClient({}) });
|
|
200
|
+
const policy = bridge.authPolicy();
|
|
201
|
+
|
|
202
|
+
expect(
|
|
203
|
+
await policy(request(bridge.secret), { requestIp: "127.0.0.1" })
|
|
204
|
+
).toMatchObject({ principalType: "runtime" });
|
|
205
|
+
// Without a publicUrl the bridge is never remotely reachable — even
|
|
206
|
+
// with the secret.
|
|
207
|
+
expect(
|
|
208
|
+
await policy(request(bridge.secret), { requestIp: "203.0.113.9" })
|
|
209
|
+
).toBeNull();
|
|
210
|
+
expect(
|
|
211
|
+
await policy(request("wrong-secret"), { requestIp: "127.0.0.1" })
|
|
212
|
+
).toBeNull();
|
|
213
|
+
expect(await policy(request(), { requestIp: "127.0.0.1" })).toBeNull();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("admits remote callers with the secret when publicUrl exposure is on", async () => {
|
|
217
|
+
const bridge = new CursorAccountMcpBridge({
|
|
218
|
+
client: stubClient({}),
|
|
219
|
+
allowRemoteCallers: true,
|
|
220
|
+
});
|
|
221
|
+
const policy = bridge.authPolicy();
|
|
222
|
+
|
|
223
|
+
expect(
|
|
224
|
+
await policy(request(bridge.secret), { requestIp: "203.0.113.9" })
|
|
225
|
+
).toMatchObject({ principalType: "runtime" });
|
|
226
|
+
expect(
|
|
227
|
+
await policy(request("wrong-secret"), { requestIp: "203.0.113.9" })
|
|
228
|
+
).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("CursorAccountMcpBridge MCP surface", () => {
|
|
233
|
+
it("forwards every account connector when servers is omitted", async () => {
|
|
234
|
+
const requested: string[][] = [];
|
|
235
|
+
const bridge = new CursorAccountMcpBridge({
|
|
236
|
+
client: stubClient({
|
|
237
|
+
list: (serverIdentifiers) => {
|
|
238
|
+
requested.push([...serverIdentifiers]);
|
|
239
|
+
return {
|
|
240
|
+
servers: [
|
|
241
|
+
{
|
|
242
|
+
serverIdentifier: "Linear",
|
|
243
|
+
status: "connected",
|
|
244
|
+
tools: [
|
|
245
|
+
{
|
|
246
|
+
name: "Linear-list_issues",
|
|
247
|
+
description: "List issues.",
|
|
248
|
+
inputSchema: { type: "object" },
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
serverIdentifier: "Notion",
|
|
254
|
+
status: "connected",
|
|
255
|
+
tools: [
|
|
256
|
+
{
|
|
257
|
+
name: "Notion-search",
|
|
258
|
+
description: "Search pages.",
|
|
259
|
+
inputSchema: { type: "object" },
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
});
|
|
268
|
+
const ref = { name: "cursor" };
|
|
269
|
+
|
|
270
|
+
const listResponse = await bridge.handle(
|
|
271
|
+
rpcRequest(bridge.secret, {
|
|
272
|
+
jsonrpc: "2.0",
|
|
273
|
+
id: 1,
|
|
274
|
+
method: "tools/list",
|
|
275
|
+
params: {},
|
|
276
|
+
}),
|
|
277
|
+
ref
|
|
278
|
+
);
|
|
279
|
+
expect(listResponse.status).toBe(200);
|
|
280
|
+
expect(requested).toEqual([[]]);
|
|
281
|
+
const listBody = (await listResponse.json()) as {
|
|
282
|
+
result: { tools: Array<{ name: string }> };
|
|
283
|
+
};
|
|
284
|
+
expect(listBody.result.tools.map((tool) => tool.name).sort()).toEqual([
|
|
285
|
+
"Linear-list_issues",
|
|
286
|
+
"Notion-search",
|
|
287
|
+
]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("lists connector tools and executes them on the backend", async () => {
|
|
291
|
+
const executions: Array<{ serverIdentifier: string; toolName: string }> =
|
|
292
|
+
[];
|
|
293
|
+
const bridge = new CursorAccountMcpBridge({
|
|
294
|
+
client: stubClient({
|
|
295
|
+
list: (serverIdentifiers) => {
|
|
296
|
+
expect(serverIdentifiers).toEqual(["Linear"]);
|
|
297
|
+
return CONNECTED_LINEAR;
|
|
298
|
+
},
|
|
299
|
+
execute: (body) => {
|
|
300
|
+
executions.push(body);
|
|
301
|
+
return {
|
|
302
|
+
result: {
|
|
303
|
+
success: {
|
|
304
|
+
content: [{ text: { text: "2 open issues" } }],
|
|
305
|
+
isError: false,
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
}),
|
|
311
|
+
});
|
|
312
|
+
const ref = { name: "cursor", servers: ["Linear"] };
|
|
313
|
+
|
|
314
|
+
const listResponse = await bridge.handle(
|
|
315
|
+
rpcRequest(bridge.secret, {
|
|
316
|
+
jsonrpc: "2.0",
|
|
317
|
+
id: 1,
|
|
318
|
+
method: "tools/list",
|
|
319
|
+
params: {},
|
|
320
|
+
}),
|
|
321
|
+
ref
|
|
322
|
+
);
|
|
323
|
+
expect(listResponse.status).toBe(200);
|
|
324
|
+
const listBody = (await listResponse.json()) as {
|
|
325
|
+
result: { tools: Array<{ name: string; inputSchema: unknown }> };
|
|
326
|
+
};
|
|
327
|
+
expect(listBody.result.tools).toEqual([
|
|
328
|
+
{
|
|
329
|
+
name: "Linear-list_issues",
|
|
330
|
+
description: "List issues.",
|
|
331
|
+
inputSchema: { type: "object" },
|
|
332
|
+
},
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
const callResponse = await bridge.handle(
|
|
336
|
+
rpcRequest(bridge.secret, {
|
|
337
|
+
jsonrpc: "2.0",
|
|
338
|
+
id: 2,
|
|
339
|
+
method: "tools/call",
|
|
340
|
+
params: { name: "Linear-list_issues", arguments: { query: "open" } },
|
|
341
|
+
}),
|
|
342
|
+
ref
|
|
343
|
+
);
|
|
344
|
+
expect(callResponse.status).toBe(200);
|
|
345
|
+
const callBody = (await callResponse.json()) as {
|
|
346
|
+
result: { content: Array<{ type: string; text: string }> };
|
|
347
|
+
};
|
|
348
|
+
expect(callBody.result.content).toEqual([
|
|
349
|
+
{ type: "text", text: "2 open issues" },
|
|
350
|
+
]);
|
|
351
|
+
expect(executions).toHaveLength(1);
|
|
352
|
+
expect(executions[0]).toMatchObject({
|
|
353
|
+
serverIdentifier: "Linear",
|
|
354
|
+
toolName: "Linear-list_issues",
|
|
355
|
+
args: { query: "open" },
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("sanitizes exotic connector tool names and maps them back for execution", async () => {
|
|
360
|
+
const executed: string[] = [];
|
|
361
|
+
const bridge = new CursorAccountMcpBridge({
|
|
362
|
+
client: stubClient({
|
|
363
|
+
list: () => ({
|
|
364
|
+
servers: [
|
|
365
|
+
{
|
|
366
|
+
serverIdentifier: "My Wiki",
|
|
367
|
+
status: "connected",
|
|
368
|
+
tools: [
|
|
369
|
+
{ name: "My Wiki-search pages" },
|
|
370
|
+
{ name: "My Wiki-search_pages" },
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
}),
|
|
375
|
+
execute: (body) => {
|
|
376
|
+
executed.push(body.toolName);
|
|
377
|
+
return {
|
|
378
|
+
result: { success: { content: [{ text: { text: "ok" } }] } },
|
|
379
|
+
};
|
|
380
|
+
},
|
|
381
|
+
}),
|
|
382
|
+
});
|
|
383
|
+
const ref = { name: "cursor" };
|
|
384
|
+
|
|
385
|
+
const listResponse = await bridge.handle(
|
|
386
|
+
rpcRequest(bridge.secret, {
|
|
387
|
+
jsonrpc: "2.0",
|
|
388
|
+
id: 1,
|
|
389
|
+
method: "tools/list",
|
|
390
|
+
params: {},
|
|
391
|
+
}),
|
|
392
|
+
ref
|
|
393
|
+
);
|
|
394
|
+
const listBody = (await listResponse.json()) as {
|
|
395
|
+
result: { tools: Array<{ name: string }> };
|
|
396
|
+
};
|
|
397
|
+
// Both sanitize to the same base; the collision gets a suffix.
|
|
398
|
+
expect(listBody.result.tools.map((tool) => tool.name)).toEqual([
|
|
399
|
+
"My_Wiki-search_pages",
|
|
400
|
+
"My_Wiki-search_pages_2",
|
|
401
|
+
]);
|
|
402
|
+
|
|
403
|
+
await bridge.handle(
|
|
404
|
+
rpcRequest(bridge.secret, {
|
|
405
|
+
jsonrpc: "2.0",
|
|
406
|
+
id: 2,
|
|
407
|
+
method: "tools/call",
|
|
408
|
+
params: { name: "My_Wiki-search_pages_2", arguments: {} },
|
|
409
|
+
}),
|
|
410
|
+
ref
|
|
411
|
+
);
|
|
412
|
+
// The backend receives the original fully-qualified name.
|
|
413
|
+
expect(executed).toEqual(["My Wiki-search_pages"]);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("returns a tool error (not a crash) for unknown tools", async () => {
|
|
417
|
+
let listCalls = 0;
|
|
418
|
+
const bridge = new CursorAccountMcpBridge({
|
|
419
|
+
client: stubClient({
|
|
420
|
+
list: () => CONNECTED_LINEAR,
|
|
421
|
+
onList: () => {
|
|
422
|
+
listCalls++;
|
|
423
|
+
},
|
|
424
|
+
}),
|
|
425
|
+
});
|
|
426
|
+
const ref = { name: "cursor" };
|
|
427
|
+
const response = await bridge.handle(
|
|
428
|
+
rpcRequest(bridge.secret, {
|
|
429
|
+
jsonrpc: "2.0",
|
|
430
|
+
id: 1,
|
|
431
|
+
method: "tools/call",
|
|
432
|
+
params: { name: "does_not_exist", arguments: {} },
|
|
433
|
+
}),
|
|
434
|
+
ref
|
|
435
|
+
);
|
|
436
|
+
const body = (await response.json()) as {
|
|
437
|
+
result: { isError?: boolean; content: Array<{ text: string }> };
|
|
438
|
+
};
|
|
439
|
+
expect(body.result.isError).toBe(true);
|
|
440
|
+
expect(body.result.content[0]?.text).toContain(
|
|
441
|
+
'Unknown tool "does_not_exist"'
|
|
442
|
+
);
|
|
443
|
+
expect(body.result.content[0]?.text).toContain("Linear-list_issues");
|
|
444
|
+
// The catalog is fresh; the unknown name does not force a re-fetch.
|
|
445
|
+
expect(listCalls).toBe(1);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("returns a tool error when backend execution throws", async () => {
|
|
449
|
+
const bridge = new CursorAccountMcpBridge({
|
|
450
|
+
client: stubClient({
|
|
451
|
+
list: () => CONNECTED_LINEAR,
|
|
452
|
+
execute: () => {
|
|
453
|
+
throw new Error("network down");
|
|
454
|
+
},
|
|
455
|
+
}),
|
|
456
|
+
});
|
|
457
|
+
const ref = { name: "cursor", servers: ["Linear"] };
|
|
458
|
+
const response = await bridge.handle(
|
|
459
|
+
rpcRequest(bridge.secret, {
|
|
460
|
+
jsonrpc: "2.0",
|
|
461
|
+
id: 1,
|
|
462
|
+
method: "tools/call",
|
|
463
|
+
params: { name: "Linear-list_issues", arguments: {} },
|
|
464
|
+
}),
|
|
465
|
+
ref
|
|
466
|
+
);
|
|
467
|
+
expect(response.status).toBe(200);
|
|
468
|
+
const body = (await response.json()) as {
|
|
469
|
+
result: { isError?: boolean; content: Array<{ text: string }> };
|
|
470
|
+
};
|
|
471
|
+
expect(body.result.isError).toBe(true);
|
|
472
|
+
expect(body.result.content[0]?.text).toContain("network down");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("rejects non-POST requests per the stateless MCP contract", async () => {
|
|
476
|
+
const bridge = new CursorAccountMcpBridge({ client: stubClient({}) });
|
|
477
|
+
const response = await bridge.handle(
|
|
478
|
+
new Request("http://127.0.0.1:3000/v1/cursor-account/cursor/mcp", {
|
|
479
|
+
method: "GET",
|
|
480
|
+
}),
|
|
481
|
+
{ name: "cursor" }
|
|
482
|
+
);
|
|
483
|
+
expect(response.status).toBe(405);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
describe("CursorAccountMcpBridge catalog cache", () => {
|
|
488
|
+
afterEach(() => {
|
|
489
|
+
vi.useRealTimers();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("coalesces concurrent readers and refreshes once after the TTL", async () => {
|
|
493
|
+
vi.useFakeTimers();
|
|
494
|
+
let listCalls = 0;
|
|
495
|
+
const bridge = new CursorAccountMcpBridge({
|
|
496
|
+
client: stubClient({
|
|
497
|
+
list: () => CONNECTED_LINEAR,
|
|
498
|
+
onList: () => {
|
|
499
|
+
listCalls++;
|
|
500
|
+
},
|
|
501
|
+
}),
|
|
502
|
+
});
|
|
503
|
+
const ref = { name: "cursor" };
|
|
504
|
+
const listRequest = (id: number): Request =>
|
|
505
|
+
rpcRequest(bridge.secret, {
|
|
506
|
+
jsonrpc: "2.0",
|
|
507
|
+
id,
|
|
508
|
+
method: "tools/list",
|
|
509
|
+
params: {},
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// Concurrent cold readers share one backend fetch.
|
|
513
|
+
await Promise.all([
|
|
514
|
+
bridge.handle(listRequest(1), ref),
|
|
515
|
+
bridge.handle(listRequest(2), ref),
|
|
516
|
+
]);
|
|
517
|
+
expect(listCalls).toBe(1);
|
|
518
|
+
|
|
519
|
+
// Within the TTL: served from cache.
|
|
520
|
+
await bridge.handle(listRequest(3), ref);
|
|
521
|
+
expect(listCalls).toBe(1);
|
|
522
|
+
|
|
523
|
+
// Past the TTL: concurrent stale readers coalesce onto ONE refresh.
|
|
524
|
+
vi.advanceTimersByTime(61_000);
|
|
525
|
+
await Promise.all([
|
|
526
|
+
bridge.handle(listRequest(4), ref),
|
|
527
|
+
bridge.handle(listRequest(5), ref),
|
|
528
|
+
]);
|
|
529
|
+
expect(listCalls).toBe(2);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("force-refreshes on unknown tools, throttled to the refresh floor", async () => {
|
|
533
|
+
vi.useFakeTimers();
|
|
534
|
+
let listCalls = 0;
|
|
535
|
+
const bridge = new CursorAccountMcpBridge({
|
|
536
|
+
client: stubClient({
|
|
537
|
+
list: () => CONNECTED_LINEAR,
|
|
538
|
+
onList: () => {
|
|
539
|
+
listCalls++;
|
|
540
|
+
},
|
|
541
|
+
}),
|
|
542
|
+
});
|
|
543
|
+
const ref = { name: "cursor" };
|
|
544
|
+
const unknownCall = (id: number): Request =>
|
|
545
|
+
rpcRequest(bridge.secret, {
|
|
546
|
+
jsonrpc: "2.0",
|
|
547
|
+
id,
|
|
548
|
+
method: "tools/call",
|
|
549
|
+
params: { name: "still_not_a_tool", arguments: {} },
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Fresh catalog: the unknown name joins it instead of re-fetching.
|
|
553
|
+
await bridge.handle(unknownCall(1), ref);
|
|
554
|
+
expect(listCalls).toBe(1);
|
|
555
|
+
|
|
556
|
+
// Past the force-refresh floor (but inside the TTL): the unknown name
|
|
557
|
+
// triggers exactly one re-fetch; an immediate repeat joins it.
|
|
558
|
+
vi.advanceTimersByTime(6_000);
|
|
559
|
+
await bridge.handle(unknownCall(2), ref);
|
|
560
|
+
expect(listCalls).toBe(2);
|
|
561
|
+
await bridge.handle(unknownCall(3), ref);
|
|
562
|
+
expect(listCalls).toBe(2);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("assigns collision suffixes stably regardless of backend ordering", async () => {
|
|
566
|
+
const serverA = {
|
|
567
|
+
serverIdentifier: "A Wiki",
|
|
568
|
+
status: "connected",
|
|
569
|
+
tools: [{ name: "A Wiki-search", description: "from A" }],
|
|
570
|
+
};
|
|
571
|
+
const serverB = {
|
|
572
|
+
serverIdentifier: "A_Wiki",
|
|
573
|
+
status: "connected",
|
|
574
|
+
tools: [{ name: "A_Wiki-search", description: "from B" }],
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const mappingFor = async (
|
|
578
|
+
servers: unknown[]
|
|
579
|
+
): Promise<Map<string, string>> => {
|
|
580
|
+
const bridge = new CursorAccountMcpBridge({
|
|
581
|
+
client: stubClient({ list: () => ({ servers }) }),
|
|
582
|
+
});
|
|
583
|
+
const response = await bridge.handle(
|
|
584
|
+
rpcRequest(bridge.secret, {
|
|
585
|
+
jsonrpc: "2.0",
|
|
586
|
+
id: 1,
|
|
587
|
+
method: "tools/list",
|
|
588
|
+
params: {},
|
|
589
|
+
}),
|
|
590
|
+
{ name: "cursor" }
|
|
591
|
+
);
|
|
592
|
+
const body = (await response.json()) as {
|
|
593
|
+
result: { tools: Array<{ name: string; description: string }> };
|
|
594
|
+
};
|
|
595
|
+
return new Map(
|
|
596
|
+
body.result.tools.map((tool) => [tool.name, tool.description])
|
|
597
|
+
);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// Both tools sanitize to "A_Wiki-search"; the exposed-name → backend-tool
|
|
601
|
+
// mapping must not depend on which server the backend lists first.
|
|
602
|
+
const forward = await mappingFor([serverA, serverB]);
|
|
603
|
+
const reversed = await mappingFor([serverB, serverA]);
|
|
604
|
+
expect(forward).toEqual(reversed);
|
|
605
|
+
expect([...forward.keys()].sort()).toEqual([
|
|
606
|
+
"A_Wiki-search",
|
|
607
|
+
"A_Wiki-search_2",
|
|
608
|
+
]);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("assigns cross-group exposed names stably regardless of backend ordering", async () => {
|
|
612
|
+
const serverAlpha = {
|
|
613
|
+
serverIdentifier: "Alpha",
|
|
614
|
+
status: "connected",
|
|
615
|
+
tools: [
|
|
616
|
+
{ name: "dup", description: "alpha-first" },
|
|
617
|
+
{ name: "dup", description: "alpha-second" },
|
|
618
|
+
],
|
|
619
|
+
};
|
|
620
|
+
const serverBeta = {
|
|
621
|
+
serverIdentifier: "Beta",
|
|
622
|
+
status: "connected",
|
|
623
|
+
tools: [{ name: "dup_2", description: "beta" }],
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
const mappingFor = async (
|
|
627
|
+
servers: unknown[]
|
|
628
|
+
): Promise<Map<string, string>> => {
|
|
629
|
+
const bridge = new CursorAccountMcpBridge({
|
|
630
|
+
client: stubClient({ list: () => ({ servers }) }),
|
|
631
|
+
});
|
|
632
|
+
const response = await bridge.handle(
|
|
633
|
+
rpcRequest(bridge.secret, {
|
|
634
|
+
jsonrpc: "2.0",
|
|
635
|
+
id: 1,
|
|
636
|
+
method: "tools/list",
|
|
637
|
+
params: {},
|
|
638
|
+
}),
|
|
639
|
+
{ name: "cursor" }
|
|
640
|
+
);
|
|
641
|
+
const body = (await response.json()) as {
|
|
642
|
+
result: { tools: Array<{ name: string; description: string }> };
|
|
643
|
+
};
|
|
644
|
+
return new Map(
|
|
645
|
+
body.result.tools.map((tool) => [tool.description, tool.name])
|
|
646
|
+
);
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const forward = await mappingFor([serverAlpha, serverBeta]);
|
|
650
|
+
const reversed = await mappingFor([serverBeta, serverAlpha]);
|
|
651
|
+
expect(forward).toEqual(reversed);
|
|
652
|
+
expect([...forward.values()].sort()).toEqual(["dup", "dup_2", "dup_2_2"]);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it("avoids global exposed-name collisions across sanitize groups", async () => {
|
|
656
|
+
const bridge = new CursorAccountMcpBridge({
|
|
657
|
+
client: stubClient({
|
|
658
|
+
list: () => ({
|
|
659
|
+
servers: [
|
|
660
|
+
{
|
|
661
|
+
serverIdentifier: "Linear",
|
|
662
|
+
status: "connected",
|
|
663
|
+
tools: [{ name: "search", description: "linear search" }],
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
serverIdentifier: "Other",
|
|
667
|
+
status: "connected",
|
|
668
|
+
tools: [{ name: "search_2", description: "other search" }],
|
|
669
|
+
},
|
|
670
|
+
],
|
|
671
|
+
}),
|
|
672
|
+
}),
|
|
673
|
+
});
|
|
674
|
+
const response = await bridge.handle(
|
|
675
|
+
rpcRequest(bridge.secret, {
|
|
676
|
+
jsonrpc: "2.0",
|
|
677
|
+
id: 1,
|
|
678
|
+
method: "tools/list",
|
|
679
|
+
params: {},
|
|
680
|
+
}),
|
|
681
|
+
{ name: "cursor" }
|
|
682
|
+
);
|
|
683
|
+
const body = (await response.json()) as {
|
|
684
|
+
result: { tools: Array<{ name: string }> };
|
|
685
|
+
};
|
|
686
|
+
const names = body.result.tools.map((tool) => tool.name);
|
|
687
|
+
expect(new Set(names).size).toBe(names.length);
|
|
688
|
+
expect(names.sort()).toEqual(["search", "search_2"]);
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
describe("formatServerStatusSummary", () => {
|
|
693
|
+
it("describes connected / needsAuth / error statuses", () => {
|
|
694
|
+
expect(
|
|
695
|
+
formatServerStatusSummary({
|
|
696
|
+
serverIdentifier: "Linear",
|
|
697
|
+
status: "connected",
|
|
698
|
+
tools: [{ name: "a", description: "", inputSchema: {} }],
|
|
699
|
+
})
|
|
700
|
+
).toBe("Linear (1 tools)");
|
|
701
|
+
expect(
|
|
702
|
+
formatServerStatusSummary({
|
|
703
|
+
serverIdentifier: "Notion",
|
|
704
|
+
status: "needsAuth",
|
|
705
|
+
tools: [],
|
|
706
|
+
})
|
|
707
|
+
).toBe("Notion (needsAuth — re-authorize in Cursor)");
|
|
708
|
+
expect(
|
|
709
|
+
formatServerStatusSummary(
|
|
710
|
+
{ serverIdentifier: "anytool", status: "error", tools: [] },
|
|
711
|
+
"HTTP MCP tool execution failed: fetch failed"
|
|
712
|
+
)
|
|
713
|
+
).toBe("anytool (error: HTTP MCP tool execution failed: fetch failed)");
|
|
714
|
+
expect(
|
|
715
|
+
formatServerStatusSummary({
|
|
716
|
+
serverIdentifier: "anytool",
|
|
717
|
+
status: "error",
|
|
718
|
+
tools: [],
|
|
719
|
+
})
|
|
720
|
+
).toBe("anytool (error: backend could not reach connector)");
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
describe("diagnoseConnectorError", () => {
|
|
725
|
+
it("returns the backend execute failure text", async () => {
|
|
726
|
+
const detail = await diagnoseConnectorError(
|
|
727
|
+
{
|
|
728
|
+
executeAccountMcpTool: async () => ({
|
|
729
|
+
content: [
|
|
730
|
+
{
|
|
731
|
+
type: "text",
|
|
732
|
+
text: "HTTP MCP tool execution failed: fetch failed",
|
|
733
|
+
},
|
|
734
|
+
],
|
|
735
|
+
isError: true,
|
|
736
|
+
}),
|
|
737
|
+
},
|
|
738
|
+
"anytool"
|
|
739
|
+
);
|
|
740
|
+
expect(detail).toBe("HTTP MCP tool execution failed: fetch failed");
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
describe("CursorAccountMcpBridge.logPreflight", () => {
|
|
745
|
+
it("logs connector error detail from a backend probe", async () => {
|
|
746
|
+
const lines: string[] = [];
|
|
747
|
+
const bridge = new CursorAccountMcpBridge({
|
|
748
|
+
client: stubClient({
|
|
749
|
+
list: () => ({
|
|
750
|
+
servers: [
|
|
751
|
+
{ serverIdentifier: "anytool", status: "error", tools: [] },
|
|
752
|
+
{
|
|
753
|
+
serverIdentifier: "Linear",
|
|
754
|
+
status: "connected",
|
|
755
|
+
tools: [{ name: "Linear-list_issues" }],
|
|
756
|
+
},
|
|
757
|
+
],
|
|
758
|
+
}),
|
|
759
|
+
execute: () => ({
|
|
760
|
+
result: {
|
|
761
|
+
error: {
|
|
762
|
+
error: "HTTP MCP tool execution failed: fetch failed",
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
}),
|
|
766
|
+
}),
|
|
767
|
+
logger: (line) => lines.push(line),
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
await bridge.logPreflight([{ name: "anytool", servers: ["anytool"] }]);
|
|
771
|
+
|
|
772
|
+
expect(lines[0]).toContain(
|
|
773
|
+
'Cursor account MCP connection "anytool": anytool (error: HTTP MCP tool execution failed: fetch failed), Linear (1 tools)'
|
|
774
|
+
);
|
|
775
|
+
expect(
|
|
776
|
+
lines.some((line) => line.includes("could not use this connector"))
|
|
777
|
+
).toBe(true);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it("mentions unhealthy connectors when a tool is missing", async () => {
|
|
781
|
+
const bridge = new CursorAccountMcpBridge({
|
|
782
|
+
client: stubClient({
|
|
783
|
+
list: () => ({
|
|
784
|
+
servers: [
|
|
785
|
+
{ serverIdentifier: "anytool", status: "error", tools: [] },
|
|
786
|
+
],
|
|
787
|
+
}),
|
|
788
|
+
}),
|
|
789
|
+
});
|
|
790
|
+
const response = await bridge.handle(
|
|
791
|
+
rpcRequest(bridge.secret, {
|
|
792
|
+
jsonrpc: "2.0",
|
|
793
|
+
id: 1,
|
|
794
|
+
method: "tools/call",
|
|
795
|
+
params: { name: "create_anyship_deployment", arguments: {} },
|
|
796
|
+
}),
|
|
797
|
+
{ name: "anytool", servers: ["anytool"] }
|
|
798
|
+
);
|
|
799
|
+
const body = (await response.json()) as {
|
|
800
|
+
result: { content: Array<{ text: string }>; isError?: boolean };
|
|
801
|
+
};
|
|
802
|
+
expect(body.result.isError).toBe(true);
|
|
803
|
+
expect(body.result.content[0]?.text).toContain(
|
|
804
|
+
"Unhealthy connectors: anytool=error"
|
|
805
|
+
);
|
|
806
|
+
});
|
|
807
|
+
});
|