Skip to content

Commit 0da8037

Browse files
committed
fix(cli): fail when MCP removal target is missing
Share configuration filenames with core through Util and use the CLI's own temporary-directory fixture.
1 parent 9436bb5 commit 0da8037

4 files changed

Lines changed: 24 additions & 16 deletions

File tree

‎packages/cli/src/commands/handlers/mcp/remove.ts‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { autocomplete, confirm, log } from "@clack/prompts"
55
import { Effect, Option } from "effect"
66
import { applyEdits, createScanner, findNodeAtLocation, parse, parseTree, type ParseError } from "jsonc-parser"
77
import { Global } from "@opencode/util/global"
8+
import { ConfigFile } from "@opencode/util/config-file"
89
import { Commands } from "../../commands"
910
import { Runtime } from "../../../framework/runtime"
1011
import { handlePromptErrors, prompt, requireInteractive } from "../../../ui/prompt"
@@ -26,11 +27,8 @@ export default Runtime.handler(
2627
inventory.warnings.forEach((message) => log.warn(`Skipping config: ${message}`))
2728
const entries = inventory.entries.filter((entry) => !name || entry.name === name)
2829
if (!entries.length) {
29-
process.stdout.write(
30-
name
31-
? `MCP server "${name}" is not configured in the selected scope${EOL}`
32-
: `No configured MCP servers found${EOL}`,
33-
)
30+
if (name) return yield* Effect.fail(new Error(`MCP server "${name}" is not configured in the selected scope`))
31+
process.stdout.write(`No configured MCP servers found${EOL}`)
3432
return
3533
}
3634
if (entries.length > 1)
@@ -90,8 +88,8 @@ export async function configuredServers(directory: string, global: string, scope
9088
const warnings: string[] = []
9189
for (const source of sources) {
9290
for (const name of source.scope === "Global"
93-
? ["opencode.json", "opencode.jsonc"]
94-
: ["opencode.json", "opencode.jsonc", ".opencode/opencode.json", ".opencode/opencode.jsonc"]) {
91+
? ConfigFile.names
92+
: [...ConfigFile.names, ...ConfigFile.names.map((name) => path.join(".opencode", name))]) {
9593
const file = path.join(source.directory, name)
9694
// A global config must not reappear as an inherited project config through a symlink.
9795
const resolved = await realpath(file).catch(() => file)

‎packages/cli/test/mcp-remove.test.ts‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { expect, test } from "bun:test"
22
import path from "node:path"
3-
import { mkdir } from "node:fs/promises"
3+
import { mkdir, realpath } from "node:fs/promises"
44
import { parse, type ParseError } from "jsonc-parser"
55
import { configuredServers, removeServer } from "../src/commands/handlers/mcp/remove"
6-
import { tmpdir } from "../../core/test/fixture/tmpdir"
6+
import { tmpdir } from "./fixture/tmpdir"
77
import { isolatedEnv } from "./fixture/environment"
88

99
test("removes only the selected local non-OAuth server, preserving JSONC and unrelated settings", async () => {
@@ -37,8 +37,9 @@ test("removes only the selected local non-OAuth server, preserving JSONC and unr
3737

3838
test("enumerates native and legacy entries by source, including reserved legacy server names", async () => {
3939
await using tmp = await tmpdir()
40-
const global = path.join(tmp.path, "global")
41-
const project = path.join(tmp.path, "project")
40+
const root = await realpath(tmp.path)
41+
const global = path.join(root, "global")
42+
const project = path.join(root, "project")
4243
const directory = path.join(project, "nested")
4344
await mkdir(directory, { recursive: true })
4445
const files = [
@@ -115,7 +116,7 @@ test("warns and skips malformed files during discovery, but refuses a malformed
115116
expect(await Bun.file(file).text()).toBe(broken)
116117
})
117118

118-
test("CLI rejects ambiguous names without edits and removes one global legacy entry with --yes", async () => {
119+
test("CLI rejects unknown or ambiguous names without edits and removes one scoped legacy entry", async () => {
119120
await using tmp = await tmpdir()
120121
const global = path.join(tmp.path, "config")
121122
const directory = path.join(tmp.path, "project")
@@ -124,7 +125,7 @@ test("CLI rejects ambiguous names without edits and removes one global legacy en
124125
const globalFile = path.join(global, "opencode.json")
125126
await Bun.write(projectFile, JSON.stringify(config))
126127
await Bun.write(globalFile, JSON.stringify(config))
127-
const cli = async (args: string[]) => {
128+
const cli = async (args: string[], name = "paper") => {
128129
const child = Bun.spawn(
129130
[
130131
process.execPath,
@@ -134,7 +135,7 @@ test("CLI rejects ambiguous names without edits and removes one global legacy en
134135
path.join(import.meta.dir, "../src/index.ts"),
135136
"mcp",
136137
"remove",
137-
"paper",
138+
name,
138139
"--yes",
139140
...args,
140141
],
@@ -152,8 +153,13 @@ test("CLI rejects ambiguous names without edits and removes one global legacy en
152153
expect(ambiguous.code).toBe(1)
153154
expect(await Bun.file(projectFile).json()).toEqual(config)
154155
expect(await Bun.file(globalFile).json()).toEqual(config)
156+
const unknown = await cli(["--global"], "missing")
157+
expect(unknown.stderr).toBe("")
158+
expect(unknown.stdout).toContain('MCP server "missing" is not configured in the selected scope')
159+
expect(unknown.code).toBe(1)
160+
expect(await Bun.file(globalFile).json()).toEqual(config)
155161
const removed = await cli(["--global"])
156-
expect(removed.stdout).toContain(`removed from ${globalFile}`)
162+
expect(removed.stdout).toContain(`removed from ${await realpath(globalFile)}`)
157163
expect(removed.stderr).toBe("")
158164
expect(removed.code).toBe(0)
159165
expect(await Bun.file(globalFile).json()).toEqual({ mcp: {} })

‎packages/core/src/config/discovery.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import os from "os"
44
import path from "path"
55
import { Effect } from "effect"
66
import { FSUtil } from "@opencode/util/fs-util"
7+
import { ConfigFile } from "@opencode/util/config-file"
78
import { Global } from "@opencode/util/global"
89
import { Location } from "../location.js"
910
import { AbsolutePath } from "../schema.js"
1011
import type { Options } from "../config.js"
1112

12-
export const names = ["opencode.json", "opencode.jsonc"]
13+
export const names = ConfigFile.names
1314

1415
/** Eligible sources in priority order, including paths that may appear later. */
1516
export interface Sources {

‎packages/util/src/config-file.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * as ConfigFile from "./config-file.js"
2+
3+
export const names = ["opencode.json", "opencode.jsonc"]

0 commit comments

Comments
 (0)