Skip to content

Commit 7d09a15

Browse files
committed
docs: remove Serena-specific references, keep MCP forwarding generic
MCP forwarding supports all servers; Serena was only an example that crept into comments, tests, and docs. Replace with generic names.
1 parent 1637c42 commit 7d09a15

11 files changed

Lines changed: 247 additions & 209 deletions

File tree

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ coverage/
88
*.tgz
99

1010
# Local editor / agent tooling
11-
.serena/
1211
.opencode/
1312
.claude/
1413

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ and a permission-gated delegation tool surface.
6161
- `"blocks"` (default) emits structured, provider-executed **dynamic** `tool-call` /
6262
`tool-result` parts so opencode renders native tool blocks. Names are
6363
`cursor_`-prefixed and sanitized (`shell``cursor_shell`,
64-
`serena/find_symbol``cursor_serena_find_symbol`) so they can't collide
64+
`myserver/find_symbol``cursor_myserver_find_symbol`) so they can't collide
6565
with opencode-registered tools, and carry `providerExecuted: true` +
6666
`dynamic: true` so ai v6's `parseToolCall` accepts them without
6767
registered-tool validation. Tool-results use the V3-spec `result` +
@@ -97,7 +97,7 @@ and a permission-gated delegation tool surface.
9797
rather than showing only the fallback snapshot.
9898
- **MCP server forwarding** — opencode's configured `config.mcp` entries are
9999
translated to Cursor `McpServerConfig` and passed to the local agent so it can
100-
use the same servers (e.g. Serena). Opt out with `provider.cursor.options.forwardMcp`.
100+
use the same servers. Opt out with `provider.cursor.options.forwardMcp`.
101101
- **Model discovery** with a 24-hour cache (keyed by key fingerprint) and a
102102
built-in fallback snapshot (composer-2.5, claude-opus-4-8, claude-sonnet-4-6,
103103
gpt-5.5) for use without an API key.

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,9 @@ The Cursor agent can use the **same MCP servers you've configured in opencode**.
214214
| `{ type: "local", command: [cmd, ...args], environment }` | `{ type: "stdio", command: cmd, args, env }` |
215215
| `{ type: "remote", url, headers }` | `{ type: "http", url, headers }` |
216216

217-
So if your `opencode.json` defines Serena, your Cursor agent connects to that same Serena — MCP
218-
servers are independent processes, so opencode and the agent each connect to them directly.
217+
So whatever MCP servers your `opencode.json` defines, your Cursor agent connects to those same
218+
servers — MCP servers are independent processes, so opencode and the agent each connect to them
219+
directly.
219220
Disabled entries (`enabled: false`) are skipped. Turn this off with `forwardMcp: false`.
220221

221222
> Scope note: this forwards **MCP servers**. opencode's *loop-internal* features — its own skills

src/plugin/index.ts

Lines changed: 118 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { translateMcpServers } from "./mcp-config.js";
77
import { buildCursorTools } from "./cursor-tools.js";
88

99
function apiKeyFromAuth(auth: Auth | undefined): string | undefined {
10-
return auth?.type === "api" ? auth.key : undefined;
10+
return auth?.type === "api" ? auth.key : undefined;
1111
}
1212

1313
/**
@@ -23,117 +23,130 @@ function apiKeyFromAuth(auth: Auth | undefined): string | undefined {
2323
* - `tool.cursor_refresh_models`: force-refresh the model catalog.
2424
*/
2525
export const CursorPlugin: Plugin = async (input) => {
26-
// The Cursor API key resolved by opencode's auth loader, captured so the
27-
// delegation tools (which don't receive auth directly) can reuse it. Falls
28-
// back to the CURSOR_API_KEY env var when the loader hasn't run.
29-
let capturedApiKey: string | undefined;
26+
// The Cursor API key resolved by opencode's auth loader, captured so the
27+
// delegation tools (which don't receive auth directly) can reuse it. Falls
28+
// back to the CURSOR_API_KEY env var when the loader hasn't run.
29+
let capturedApiKey: string | undefined;
3030

31-
return {
32-
auth: {
33-
provider: PROVIDER_ID,
34-
loader: async (getAuth) => {
35-
const apiKey = resolveCursorApiKey(apiKeyFromAuth(await getAuth().catch(() => undefined)));
36-
if (apiKey) {
37-
capturedApiKey = apiKey;
38-
// The `config` hook (which seeds opencode's model picker) runs without
39-
// a key. Warm the catalog cache here — the loader is the hook that
40-
// reliably has the key — so the next launch seeds the full live
41-
// catalog instead of the static fallback. Fire-and-forget: discovery
42-
// never throws and must not block auth/provider load.
43-
void discoverModels({ apiKey });
44-
}
45-
return apiKey ? { apiKey } : {};
46-
},
47-
// A single API-key method. opencode always shows its built-in "Enter your
48-
// API key" prompt for `type: "api"`, so we intentionally do NOT declare
49-
// custom `prompts` (that asks for the key a second time) or an `authorize`
50-
// callback. opencode only passes `authorize` the *custom-prompt* inputs —
51-
// never the built-in key — so validating the key in `authorize` would
52-
// force that redundant extra prompt. Instead the key is validated on first
53-
// use (model discovery / the first call both surface a bad key clearly).
54-
methods: [{ type: "api", label: "Cursor API Key" }],
55-
},
31+
return {
32+
auth: {
33+
provider: PROVIDER_ID,
34+
loader: async (getAuth) => {
35+
const apiKey = resolveCursorApiKey(
36+
apiKeyFromAuth(await getAuth().catch(() => undefined)),
37+
);
38+
if (apiKey) {
39+
capturedApiKey = apiKey;
40+
// The `config` hook (which seeds opencode's model picker) runs without
41+
// a key. Warm the catalog cache here — the loader is the hook that
42+
// reliably has the key — so the next launch seeds the full live
43+
// catalog instead of the static fallback. Fire-and-forget: discovery
44+
// never throws and must not block auth/provider load.
45+
void discoverModels({ apiKey });
46+
}
47+
return apiKey ? { apiKey } : {};
48+
},
49+
// A single API-key method. opencode always shows its built-in "Enter your
50+
// API key" prompt for `type: "api"`, so we intentionally do NOT declare
51+
// custom `prompts` (that asks for the key a second time) or an `authorize`
52+
// callback. opencode only passes `authorize` the *custom-prompt* inputs —
53+
// never the built-in key — so validating the key in `authorize` would
54+
// force that redundant extra prompt. Instead the key is validated on first
55+
// use (model discovery / the first call both surface a bad key clearly).
56+
methods: [{ type: "api", label: "Cursor API Key" }],
57+
},
5658

57-
config: async (config) => {
58-
const { models } = await discoverModels({});
59-
config.provider ??= {};
60-
const existing = config.provider[PROVIDER_ID] ?? {};
61-
const existingOptions = (existing.options ?? {}) as Record<string, unknown>;
59+
config: async (config) => {
60+
const { models } = await discoverModels({});
61+
config.provider ??= {};
62+
const existing = config.provider[PROVIDER_ID] ?? {};
63+
const existingOptions = (existing.options ?? {}) as Record<
64+
string,
65+
unknown
66+
>;
6267

63-
// Forward opencode's configured MCP servers (e.g. Serena) to the Cursor
64-
// agent so it can use the same servers. Opt out via
65-
// `provider.cursor.options.forwardMcp: false`.
66-
const forwardMcp = existingOptions["forwardMcp"] !== false;
67-
const userMcp = (existingOptions["mcpServers"] ?? {}) as Record<string, unknown>;
68-
const mcpServers = forwardMcp
69-
? { ...userMcp, ...translateMcpServers(config.mcp) }
70-
: userMcp;
68+
// Forward opencode's configured MCP servers to the Cursor
69+
// agent so it can use the same servers. Opt out via
70+
// `provider.cursor.options.forwardMcp: false`.
71+
const forwardMcp = existingOptions["forwardMcp"] !== false;
72+
const userMcp = (existingOptions["mcpServers"] ?? {}) as Record<
73+
string,
74+
unknown
75+
>;
76+
const mcpServers = forwardMcp
77+
? { ...userMcp, ...translateMcpServers(config.mcp) }
78+
: userMcp;
7179

72-
config.provider[PROVIDER_ID] = {
73-
name: "Cursor",
74-
npm: providerNpm(),
75-
...existing,
76-
options: {
77-
...existingOptions,
78-
...(Object.keys(mcpServers).length > 0 ? { mcpServers } : {}),
79-
},
80-
models: { ...toOpencodeModels(models), ...(existing.models ?? {}) },
81-
};
82-
},
80+
config.provider[PROVIDER_ID] = {
81+
name: "Cursor",
82+
npm: providerNpm(),
83+
...existing,
84+
options: {
85+
...existingOptions,
86+
...(Object.keys(mcpServers).length > 0 ? { mcpServers } : {}),
87+
},
88+
models: { ...toOpencodeModels(models), ...(existing.models ?? {}) },
89+
};
90+
},
8391

84-
provider: {
85-
id: PROVIDER_ID,
86-
models: async (_provider, ctx) => {
87-
const apiKey = apiKeyFromAuth(ctx.auth);
88-
const { models } = await discoverModels({ apiKey });
89-
return buildModelV2Map(models);
90-
},
91-
},
92+
provider: {
93+
id: PROVIDER_ID,
94+
models: async (_provider, ctx) => {
95+
const apiKey = apiKeyFromAuth(ctx.auth);
96+
const { models } = await discoverModels({ apiKey });
97+
return buildModelV2Map(models);
98+
},
99+
},
92100

93-
// Bridge opencode's session id to the provider: it lands in
94-
// providerOptions.cursor.sessionID, which the provider reads to pool/resume a
95-
// Cursor agent per session (when the `session` option is enabled).
96-
//
97-
// Also map opencode's plan AGENT to Cursor's plan mode. This hook fires
98-
// after opencode merges the selected variant into `output.options`, so an
99-
// explicit mode from the `plan` variant (or model options) wins — the
100-
// agent-based default only applies when no mode was set.
101-
"chat.params": async (input, output) => {
102-
if (input.model?.providerID !== PROVIDER_ID) return;
103-
output.options = { ...(output.options ?? {}), sessionID: input.sessionID };
104-
if (input.agent === "plan" && output.options["mode"] === undefined) {
105-
output.options["mode"] = "plan";
106-
}
107-
},
101+
// Bridge opencode's session id to the provider: it lands in
102+
// providerOptions.cursor.sessionID, which the provider reads to pool/resume a
103+
// Cursor agent per session (when the `session` option is enabled).
104+
//
105+
// Also map opencode's plan AGENT to Cursor's plan mode. This hook fires
106+
// after opencode merges the selected variant into `output.options`, so an
107+
// explicit mode from the `plan` variant (or model options) wins — the
108+
// agent-based default only applies when no mode was set.
109+
"chat.params": async (input, output) => {
110+
if (input.model?.providerID !== PROVIDER_ID) return;
111+
output.options = {
112+
...(output.options ?? {}),
113+
sessionID: input.sessionID,
114+
};
115+
if (input.agent === "plan" && output.options["mode"] === undefined) {
116+
output.options["mode"] = "plan";
117+
}
118+
},
108119

109-
tool: {
110-
cursor_refresh_models: {
111-
description:
112-
"Refresh the live Cursor model catalog (bypasses the 24h cache) and report the available models.",
113-
args: {},
114-
execute: async () => {
115-
const result = await discoverModels({ forceRefresh: true });
116-
const lines = result.models.map((m) => `- ${m.id}${m.displayName}`);
117-
const header =
118-
result.source === "live"
119-
? `Refreshed ${result.models.length} Cursor models (live):`
120-
: `Could not fetch live models (${result.source}). ${result.warning ?? ""}`.trim();
121-
return {
122-
title: `Cursor models (${result.source})`,
123-
output: [header, ...lines].join("\n"),
124-
metadata: { source: result.source, count: result.models.length },
125-
};
126-
},
127-
},
128-
// Delegation tools that complement the provider: a cloud/background agent
129-
// and a permission-gated local delegate. They resolve the Cursor key from
130-
// the auth loader (captured above) or CURSOR_API_KEY.
131-
...buildCursorTools({
132-
resolveApiKey: () => resolveCursorApiKey(capturedApiKey),
133-
defaultCwd: () => input?.directory ?? process.cwd(),
134-
}),
135-
},
136-
};
120+
tool: {
121+
cursor_refresh_models: {
122+
description:
123+
"Refresh the live Cursor model catalog (bypasses the 24h cache) and report the available models.",
124+
args: {},
125+
execute: async () => {
126+
const result = await discoverModels({ forceRefresh: true });
127+
const lines = result.models.map(
128+
(m) => `- ${m.id}${m.displayName}`,
129+
);
130+
const header =
131+
result.source === "live"
132+
? `Refreshed ${result.models.length} Cursor models (live):`
133+
: `Could not fetch live models (${result.source}). ${result.warning ?? ""}`.trim();
134+
return {
135+
title: `Cursor models (${result.source})`,
136+
output: [header, ...lines].join("\n"),
137+
metadata: { source: result.source, count: result.models.length },
138+
};
139+
},
140+
},
141+
// Delegation tools that complement the provider: a cloud/background agent
142+
// and a permission-gated local delegate. They resolve the Cursor key from
143+
// the auth loader (captured above) or CURSOR_API_KEY.
144+
...buildCursorTools({
145+
resolveApiKey: () => resolveCursorApiKey(capturedApiKey),
146+
defaultCwd: () => input?.directory ?? process.cwd(),
147+
}),
148+
},
149+
};
137150
};
138151

139152
export default CursorPlugin;

src/plugin/mcp-config.ts

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,47 @@ type OpencodeMcpEntry = OpencodeMcp[string];
77

88
/**
99
* Translate opencode's configured MCP servers (`config.mcp`) into the Cursor
10-
* SDK's `McpServerConfig` shape so the same servers (e.g. Serena) can be handed
10+
* SDK's `McpServerConfig` shape so the same servers can be handed
1111
* to the Cursor agent via `Agent.create({ mcpServers })`.
1212
*
1313
* MCP servers are independent processes addressed by a launch spec, so opencode
1414
* and the Cursor agent can each connect to the same server. Disabled entries
1515
* (`enabled: false`) are skipped. opencode-only fields with no Cursor
1616
* equivalent (timeout, oauth) are dropped.
1717
*/
18-
export function translateMcpServers(mcp: Config["mcp"]): Record<string, McpServerConfig> {
19-
const out: Record<string, McpServerConfig> = {};
20-
if (!mcp) return out;
18+
export function translateMcpServers(
19+
mcp: Config["mcp"],
20+
): Record<string, McpServerConfig> {
21+
const out: Record<string, McpServerConfig> = {};
22+
if (!mcp) return out;
2123

22-
for (const [name, entry] of Object.entries(mcp) as Array<[string, OpencodeMcpEntry]>) {
23-
if (!entry || entry.enabled === false) continue;
24+
for (const [name, entry] of Object.entries(mcp) as Array<
25+
[string, OpencodeMcpEntry]
26+
>) {
27+
if (!entry || entry.enabled === false) continue;
2428

25-
if (entry.type === "local") {
26-
const [command, ...args] = entry.command ?? [];
27-
if (!command) continue;
28-
out[name] = {
29-
type: "stdio",
30-
command,
31-
...(args.length > 0 ? { args } : {}),
32-
...(entry.environment && Object.keys(entry.environment).length > 0
33-
? { env: entry.environment }
34-
: {}),
35-
};
36-
} else if (entry.type === "remote") {
37-
if (!entry.url) continue;
38-
out[name] = {
39-
type: "http",
40-
url: entry.url,
41-
...(entry.headers && Object.keys(entry.headers).length > 0
42-
? { headers: entry.headers }
43-
: {}),
44-
};
45-
}
46-
}
29+
if (entry.type === "local") {
30+
const [command, ...args] = entry.command ?? [];
31+
if (!command) continue;
32+
out[name] = {
33+
type: "stdio",
34+
command,
35+
...(args.length > 0 ? { args } : {}),
36+
...(entry.environment && Object.keys(entry.environment).length > 0
37+
? { env: entry.environment }
38+
: {}),
39+
};
40+
} else if (entry.type === "remote") {
41+
if (!entry.url) continue;
42+
out[name] = {
43+
type: "http",
44+
url: entry.url,
45+
...(entry.headers && Object.keys(entry.headers).length > 0
46+
? { headers: entry.headers }
47+
: {}),
48+
};
49+
}
50+
}
4751

48-
return out;
52+
return out;
4953
}

src/provider/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ export interface CursorProviderOptions {
3535
/**
3636
* MCP servers to make available to the Cursor agent, keyed by name. The
3737
* plugin's `config` hook populates this by translating opencode's configured
38-
* `config.mcp` servers, so the agent can use the same MCP servers (e.g.
39-
* Serena) that opencode does.
38+
* `config.mcp` servers, so the agent can use the same MCP servers that
39+
* opencode does.
4040
*/
4141
mcpServers?: Record<string, McpServerConfig>;
4242
/**

src/provider/language-model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export interface CursorModelConfig {
3535
mode: AgentModeOption;
3636
/** Default Cursor model params (id -> value); overridable per-request. */
3737
params?: Record<string, string>;
38-
/** MCP servers forwarded to the Cursor agent (e.g. opencode's Serena). */
38+
/** MCP servers forwarded to the Cursor agent from opencode's config. */
3939
mcpServers?: Record<string, McpServerConfig>;
4040
/** Cursor settings layers to load from disk (skills, rules, .cursor/mcp.json). */
4141
settingSources?: SettingSource[];

src/provider/stream-map.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function safeJsonString(input: unknown): string {
4343
/**
4444
* Tool name as it crosses into opencode in "blocks" mode. Prefixed so it can
4545
* never collide with a tool opencode has registered, and sanitized because MCP
46-
* names contain `/` (e.g. `serena/find_symbol` → `cursor_serena_find_symbol`).
46+
* names contain `/` (e.g. `myserver/find_symbol` → `cursor_myserver_find_symbol`).
4747
*/
4848
function blockToolName(name: string): string {
4949
return `cursor_${name.replace(/[^A-Za-z0-9_-]/g, "_")}`;

0 commit comments

Comments
 (0)