@@ -5,26 +5,112 @@ import type { McpServerConfig } from "@cursor/sdk";
55type OpencodeMcp = NonNullable < Config [ "mcp" ] > ;
66type OpencodeMcpEntry = OpencodeMcp [ string ] ;
77
8+ /**
9+ * Live MCP server status, keyed by server name, as reported by opencode's
10+ * `client.mcp.status()`. Only the `status` field is consumed; `"connected"`
11+ * means the server is currently usable. Mirrors the SDK's `McpStatus` union
12+ * without importing it (keeps this module dependency-light).
13+ */
14+ export type McpStatusMap = Record < string , { status ?: string } | undefined > ;
15+
16+ /** opencode runtime statuses that mean a server still needs OAuth to connect. */
17+ const NEEDS_AUTH_STATUS = new Set ( [ "needs_auth" , "needs_client_registration" ] ) ;
18+
19+ /** The OAuth client registration on a remote entry, or undefined when none. */
20+ function oauthConfig (
21+ entry : OpencodeMcpEntry ,
22+ ) : { clientId ?: string ; clientSecret ?: string ; scope ?: string } | undefined {
23+ if ( entry . type !== "remote" ) return undefined ;
24+ // `oauth` is `McpOAuthConfig | false | undefined`; both false and undefined
25+ // are falsy, so a truthy value is the client-registration object.
26+ return entry . oauth ? entry . oauth : undefined ;
27+ }
28+
29+ /**
30+ * Map opencode's OAuth client registration to the Cursor SDK's `auth` block so
31+ * the Cursor agent can run its own OAuth flow. Returns undefined when there is
32+ * no `clientId` to share (e.g. RFC 7591 dynamic registration) — opencode's
33+ * access token itself never reaches `config.mcp`, so a bare URL would fail.
34+ */
35+ function toCursorAuth (
36+ oauth :
37+ | { clientId ?: string ; clientSecret ?: string ; scope ?: string }
38+ | undefined ,
39+ ) :
40+ | { CLIENT_ID : string ; CLIENT_SECRET ?: string ; scopes ?: string [ ] }
41+ | undefined {
42+ if ( ! oauth ?. clientId ) return undefined ;
43+ const scopes = oauth . scope ?. split ( / \s + / ) . filter ( Boolean ) ;
44+ return {
45+ CLIENT_ID : oauth . clientId ,
46+ ...( oauth . clientSecret ? { CLIENT_SECRET : oauth . clientSecret } : { } ) ,
47+ ...( scopes && scopes . length > 0 ? { scopes } : { } ) ,
48+ } ;
49+ }
50+
51+ /**
52+ * Names of remote servers that require OAuth but cannot be forwarded to the
53+ * Cursor agent because no shareable client registration exists (dynamic
54+ * registration, or a `needs_auth` runtime status with no configured
55+ * `clientId`). The plugin surfaces these to the user instead of silently
56+ * forwarding a spec that would 401.
57+ */
58+ export function findUnshareableOAuthServers (
59+ mcp : Config [ "mcp" ] ,
60+ status ?: McpStatusMap ,
61+ ) : string [ ] {
62+ const names : string [ ] = [ ] ;
63+ if ( ! mcp ) return names ;
64+ for ( const [ name , entry ] of Object . entries ( mcp ) as Array <
65+ [ string , OpencodeMcpEntry ]
66+ > ) {
67+ if ( ! entry || entry . type !== "remote" ) continue ;
68+ if ( ! status && entry . enabled === false ) continue ;
69+ const s = status ?. [ name ] ?. status ;
70+ if ( status && s !== "connected" && ! NEEDS_AUTH_STATUS . has ( s ?? "" ) )
71+ continue ;
72+ const oauth = oauthConfig ( entry ) ;
73+ const needsOAuth = Boolean ( oauth ) || NEEDS_AUTH_STATUS . has ( s ?? "" ) ;
74+ if ( needsOAuth && ! toCursorAuth ( oauth ) ) names . push ( name ) ;
75+ }
76+ return names ;
77+ }
78+
879/**
980 * Translate opencode's configured MCP servers (`config.mcp`) into the Cursor
1081 * SDK's `McpServerConfig` shape so the same servers can be handed
1182 * to the Cursor agent via `Agent.create({ mcpServers })`.
1283 *
1384 * MCP servers are independent processes addressed by a launch spec, so opencode
1485 * and the Cursor agent can each connect to the same server. Disabled entries
15- * (`enabled: false`) are skipped. opencode-only fields with no Cursor
16- * equivalent (timeout, oauth) are dropped.
86+ * (`enabled: false`) are skipped. The `timeout` field is dropped (no Cursor
87+ * equivalent). OAuth is mapped where possible: a remote server's `oauth` client
88+ * registration becomes Cursor's `auth` block so the agent runs its own OAuth
89+ * flow; servers needing OAuth with no shareable `clientId` are skipped (the
90+ * plugin reports them via {@link findUnshareableOAuthServers}).
1791 */
1892export function translateMcpServers (
1993 mcp : Config [ "mcp" ] ,
94+ status ?: McpStatusMap ,
2095) : Record < string , McpServerConfig > {
2196 const out : Record < string , McpServerConfig > = { } ;
2297 if ( ! mcp ) return out ;
2398
2499 for ( const [ name , entry ] of Object . entries ( mcp ) as Array <
25100 [ string , OpencodeMcpEntry ]
26101 > ) {
27- if ( ! entry || entry . enabled === false ) continue ;
102+ if ( ! entry ) continue ;
103+
104+ // When a live status map is supplied (per-turn dynamic forwarding), it is
105+ // the source of truth: forward only servers opencode has currently
106+ // connected, so mid-session enable/disable propagates to the Cursor agent.
107+ // Without it (the startup config snapshot), fall back to the static
108+ // `enabled` flag.
109+ if ( status ) {
110+ if ( status [ name ] ?. status !== "connected" ) continue ;
111+ } else if ( entry . enabled === false ) {
112+ continue ;
113+ }
28114
29115 if ( entry . type === "local" ) {
30116 const [ command , ...args ] = entry . command ?? [ ] ;
@@ -39,12 +125,20 @@ export function translateMcpServers(
39125 } ;
40126 } else if ( entry . type === "remote" ) {
41127 if ( ! entry . url ) continue ;
128+ const oauth = oauthConfig ( entry ) ;
129+ const auth = toCursorAuth ( oauth ) ;
130+ // OAuth server with no shareable client registration: opencode holds the
131+ // token and it never lands in config.mcp, so skip rather than forward a
132+ // bare URL that would 401. The plugin notifies the user (see
133+ // findUnshareableOAuthServers).
134+ if ( oauth && ! auth ) continue ;
42135 out [ name ] = {
43136 type : "http" ,
44137 url : entry . url ,
45138 ...( entry . headers && Object . keys ( entry . headers ) . length > 0
46139 ? { headers : entry . headers }
47140 : { } ) ,
141+ ...( auth ? { auth } : { } ) ,
48142 } ;
49143 }
50144 }
0 commit comments