Skip to content

Commit 5c5e636

Browse files
jknlsnrekram1-node
andauthored
feat: add per-project MCP config overrides (anomalyco#5406)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
1 parent da6df3d commit 5c5e636

3 files changed

Lines changed: 71 additions & 15 deletions

File tree

packages/opencode/src/cli/cmd/mcp.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ function getAuthStatusText(status: MCP.AuthStatus): string {
3636
}
3737
}
3838

39+
type McpEntry = NonNullable<Config.Info["mcp"]>[string]
40+
41+
type McpConfigured = Config.Mcp
42+
function isMcpConfigured(config: McpEntry): config is McpConfigured {
43+
return typeof config === "object" && config !== null && "type" in config
44+
}
45+
46+
type McpRemote = Extract<McpConfigured, { type: "remote" }>
47+
function isMcpRemote(config: McpEntry): config is McpRemote {
48+
return isMcpConfigured(config) && config.type === "remote"
49+
}
50+
3951
export const McpCommand = cmd({
4052
command: "mcp",
4153
builder: (yargs) =>
@@ -64,15 +76,19 @@ export const McpListCommand = cmd({
6476
const mcpServers = config.mcp ?? {}
6577
const statuses = await MCP.status()
6678

67-
if (Object.keys(mcpServers).length === 0) {
79+
const servers = Object.entries(mcpServers).filter((entry): entry is [string, McpConfigured] =>
80+
isMcpConfigured(entry[1]),
81+
)
82+
83+
if (servers.length === 0) {
6884
prompts.log.warn("No MCP servers configured")
6985
prompts.outro("Add servers with: opencode mcp add")
7086
return
7187
}
7288

73-
for (const [name, serverConfig] of Object.entries(mcpServers)) {
89+
for (const [name, serverConfig] of servers) {
7490
const status = statuses[name]
75-
const hasOAuth = serverConfig.type === "remote" && !!serverConfig.oauth
91+
const hasOAuth = isMcpRemote(serverConfig) && !!serverConfig.oauth
7692
const hasStoredTokens = await MCP.hasStoredTokens(name)
7793

7894
let statusIcon: string
@@ -110,7 +126,7 @@ export const McpListCommand = cmd({
110126
)
111127
}
112128

113-
prompts.outro(`${Object.keys(mcpServers).length} server(s)`)
129+
prompts.outro(`${servers.length} server(s)`)
114130
},
115131
})
116132
},
@@ -138,7 +154,7 @@ export const McpAuthCommand = cmd({
138154

139155
// Get OAuth-capable servers (remote servers with oauth not explicitly disabled)
140156
const oauthServers = Object.entries(mcpServers).filter(
141-
([_, cfg]) => cfg.type === "remote" && cfg.oauth !== false,
157+
(entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
142158
)
143159

144160
if (oauthServers.length === 0) {
@@ -163,7 +179,7 @@ export const McpAuthCommand = cmd({
163179
const authStatus = await MCP.getAuthStatus(name)
164180
const icon = getAuthStatusIcon(authStatus)
165181
const statusText = getAuthStatusText(authStatus)
166-
const url = cfg.type === "remote" ? cfg.url : ""
182+
const url = cfg.url
167183
return {
168184
label: `${icon} ${name} (${statusText})`,
169185
value: name,
@@ -187,8 +203,8 @@ export const McpAuthCommand = cmd({
187203
return
188204
}
189205

190-
if (serverConfig.type !== "remote" || serverConfig.oauth === false) {
191-
prompts.log.error(`MCP server ${serverName} does not support OAuth (oauth is disabled)`)
206+
if (!isMcpRemote(serverConfig) || serverConfig.oauth === false) {
207+
prompts.log.error(`MCP server ${serverName} is not an OAuth-capable remote server`)
192208
prompts.outro("Done")
193209
return
194210
}
@@ -263,7 +279,7 @@ export const McpAuthListCommand = cmd({
263279

264280
// Get OAuth-capable servers
265281
const oauthServers = Object.entries(mcpServers).filter(
266-
([_, cfg]) => cfg.type === "remote" && cfg.oauth !== false,
282+
(entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
267283
)
268284

269285
if (oauthServers.length === 0) {
@@ -276,7 +292,7 @@ export const McpAuthListCommand = cmd({
276292
const authStatus = await MCP.getAuthStatus(name)
277293
const icon = getAuthStatusIcon(authStatus)
278294
const statusText = getAuthStatusText(authStatus)
279-
const url = serverConfig.type === "remote" ? serverConfig.url : ""
295+
const url = serverConfig.url
280296

281297
prompts.log.info(`${icon} ${name} ${UI.Style.TEXT_DIM}${statusText}\n ${UI.Style.TEXT_DIM}${url}`)
282298
}
@@ -506,7 +522,7 @@ export const McpDebugCommand = cmd({
506522
return
507523
}
508524

509-
if (serverConfig.type !== "remote") {
525+
if (!isMcpRemote(serverConfig)) {
510526
prompts.log.error(`MCP server ${serverName} is not a remote server`)
511527
prompts.outro("Done")
512528
return

packages/opencode/src/config/config.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,20 @@ export namespace Config {
817817
.record(z.string(), Provider)
818818
.optional()
819819
.describe("Custom provider configurations and model overrides"),
820-
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
820+
mcp: z
821+
.record(
822+
z.string(),
823+
z.union([
824+
Mcp,
825+
z
826+
.object({
827+
enabled: z.boolean(),
828+
})
829+
.strict(),
830+
]),
831+
)
832+
.optional()
833+
.describe("MCP (Model Context Protocol) server configurations"),
821834
formatter: z
822835
.union([
823836
z.literal(false),

packages/opencode/src/mcp/index.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ export namespace MCP {
135135
// Prompt cache types
136136
type PromptInfo = Awaited<ReturnType<MCPClient["listPrompts"]>>["prompts"][number]
137137

138+
type McpEntry = NonNullable<Config.Info["mcp"]>[string]
139+
function isMcpConfigured(entry: McpEntry): entry is Config.Mcp {
140+
return typeof entry === "object" && entry !== null && "type" in entry
141+
}
142+
138143
const state = Instance.state(
139144
async () => {
140145
const cfg = await Config.get()
@@ -144,6 +149,11 @@ export namespace MCP {
144149

145150
await Promise.all(
146151
Object.entries(config).map(async ([key, mcp]) => {
152+
if (!isMcpConfigured(mcp)) {
153+
log.error("Ignoring MCP config entry without type", { key })
154+
return
155+
}
156+
147157
// If disabled by config, mark as disabled without trying to connect
148158
if (mcp.enabled === false) {
149159
status[key] = { status: "disabled" }
@@ -237,6 +247,7 @@ export namespace MCP {
237247
status: { status: "disabled" as const },
238248
}
239249
}
250+
240251
log.info("found", { key, type: mcp.type })
241252
let mcpClient: MCPClient | undefined
242253
let status: Status | undefined = undefined
@@ -434,8 +445,9 @@ export namespace MCP {
434445
const config = cfg.mcp ?? {}
435446
const result: Record<string, Status> = {}
436447

437-
// Include all MCPs from config, not just connected ones
438-
for (const key of Object.keys(config)) {
448+
// Include all configured MCPs from config, not just connected ones
449+
for (const [key, mcp] of Object.entries(config)) {
450+
if (!isMcpConfigured(mcp)) continue
439451
result[key] = s.status[key] ?? { status: "disabled" }
440452
}
441453

@@ -455,6 +467,11 @@ export namespace MCP {
455467
return
456468
}
457469

470+
if (!isMcpConfigured(mcp)) {
471+
log.error("Ignoring MCP connect request for config without type", { name })
472+
return
473+
}
474+
458475
const result = await create(name, { ...mcp, enabled: true })
459476

460477
if (!result) {
@@ -579,6 +596,10 @@ export namespace MCP {
579596
throw new Error(`MCP server not found: ${mcpName}`)
580597
}
581598

599+
if (!isMcpConfigured(mcpConfig)) {
600+
throw new Error(`MCP server ${mcpName} is disabled or missing configuration`)
601+
}
602+
582603
if (mcpConfig.type !== "remote") {
583604
throw new Error(`MCP server ${mcpName} is not a remote server`)
584605
}
@@ -705,6 +726,10 @@ export namespace MCP {
705726
throw new Error(`MCP server not found: ${mcpName}`)
706727
}
707728

729+
if (!isMcpConfigured(mcpConfig)) {
730+
throw new Error(`MCP server ${mcpName} is disabled or missing configuration`)
731+
}
732+
708733
// Re-add the MCP server to establish connection
709734
pendingOAuthTransports.delete(mcpName)
710735
const result = await add(mcpName, mcpConfig)
@@ -737,7 +762,9 @@ export namespace MCP {
737762
export async function supportsOAuth(mcpName: string): Promise<boolean> {
738763
const cfg = await Config.get()
739764
const mcpConfig = cfg.mcp?.[mcpName]
740-
return mcpConfig?.type === "remote" && mcpConfig.oauth !== false
765+
if (!mcpConfig) return false
766+
if (!isMcpConfigured(mcpConfig)) return false
767+
return mcpConfig.type === "remote" && mcpConfig.oauth !== false
741768
}
742769

743770
/**

0 commit comments

Comments
 (0)