refactor(cli): simplify mcp add options
This commit is contained in:
parent
ebb101bd54
commit
7f9fbac33c
4 changed files with 14 additions and 154 deletions
|
|
@ -63,16 +63,7 @@ type McpAddArgs = {
|
||||||
type?: "local" | "remote"
|
type?: "local" | "remote"
|
||||||
env?: string[]
|
env?: string[]
|
||||||
header?: string[]
|
header?: string[]
|
||||||
scope?: "project" | "global"
|
|
||||||
global?: boolean
|
global?: boolean
|
||||||
enabled?: boolean
|
|
||||||
timeout?: number
|
|
||||||
oauth?: boolean
|
|
||||||
oauthClientId?: string
|
|
||||||
oauthClientSecret?: string
|
|
||||||
oauthScope?: string
|
|
||||||
oauthCallbackPort?: number
|
|
||||||
oauthRedirectUri?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function configuredServers(config: Config.Info) {
|
function configuredServers(config: Config.Info) {
|
||||||
|
|
@ -486,53 +477,16 @@ export const McpAddCommand = effectCmd({
|
||||||
type: "string",
|
type: "string",
|
||||||
array: true,
|
array: true,
|
||||||
})
|
})
|
||||||
.option("scope", {
|
|
||||||
describe: "where to save the server",
|
|
||||||
type: "string",
|
|
||||||
choices: ["project", "global"] as const,
|
|
||||||
})
|
|
||||||
.option("global", {
|
.option("global", {
|
||||||
alias: ["g"],
|
alias: ["g"],
|
||||||
describe: "save to global config",
|
describe: "save to global config",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
})
|
|
||||||
.option("enabled", {
|
|
||||||
describe: "enable or disable the server on startup",
|
|
||||||
type: "boolean",
|
|
||||||
})
|
|
||||||
.option("timeout", {
|
|
||||||
describe: "timeout in milliseconds for MCP server requests",
|
|
||||||
type: "number",
|
|
||||||
})
|
|
||||||
.option("oauth", {
|
|
||||||
describe: "enable OAuth for remote servers, or use --no-oauth to disable auto-detection",
|
|
||||||
type: "boolean",
|
|
||||||
})
|
|
||||||
.option("oauth-client-id", {
|
|
||||||
describe: "OAuth client ID for remote servers",
|
|
||||||
type: "string",
|
|
||||||
})
|
|
||||||
.option("oauth-client-secret", {
|
|
||||||
describe: "OAuth client secret for remote servers",
|
|
||||||
type: "string",
|
|
||||||
})
|
|
||||||
.option("oauth-scope", {
|
|
||||||
describe: "OAuth scopes to request for remote servers",
|
|
||||||
type: "string",
|
|
||||||
})
|
|
||||||
.option("oauth-callback-port", {
|
|
||||||
describe: "OAuth local callback port for remote servers",
|
|
||||||
type: "number",
|
|
||||||
})
|
|
||||||
.option("oauth-redirect-uri", {
|
|
||||||
describe: "OAuth redirect URI for remote servers",
|
|
||||||
type: "string",
|
|
||||||
}).epilogue(`Usage:
|
}).epilogue(`Usage:
|
||||||
opencode mcp add <name> -- <command> [args...] (local MCP server)
|
opencode mcp add <name> -- <command> [args...] (local MCP server)
|
||||||
opencode mcp add <name> --env KEY=VALUE -- <command> [args...] (local MCP server with env vars)
|
opencode mcp add <name> --env KEY=VALUE -- <command> [args...] (local MCP server with env vars)
|
||||||
opencode mcp add <name> <url> (remote MCP server)
|
opencode mcp add <name> <url> (remote MCP server)
|
||||||
opencode mcp add <name> --header KEY=VALUE <url> (remote MCP server with headers)
|
opencode mcp add <name> --header KEY=VALUE <url> (remote MCP server with headers)
|
||||||
opencode mcp add <name> --scope project -- <command> [args...] (save to project config)
|
opencode mcp add <name> --global <url> (save to global config)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
opencode mcp add context7 -- npx -y @upstash/context7-mcp
|
opencode mcp add context7 -- npx -y @upstash/context7-mcp
|
||||||
|
|
@ -546,8 +500,6 @@ Examples:
|
||||||
const inlineArgs = mcpAddArgs(input)
|
const inlineArgs = mcpAddArgs(input)
|
||||||
const inlineConfig = parseInlineMcpAdd(input, inlineArgs)
|
const inlineConfig = parseInlineMcpAdd(input, inlineArgs)
|
||||||
if (inlineConfig && "error" in inlineConfig) return yield* fail(inlineConfig.error)
|
if (inlineConfig && "error" in inlineConfig) return yield* fail(inlineConfig.error)
|
||||||
if (input.global && input.scope === "project")
|
|
||||||
return yield* fail("--global cannot be combined with --scope project")
|
|
||||||
yield* Effect.promise(async () => {
|
yield* Effect.promise(async () => {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("Add MCP server")
|
prompts.intro("Add MCP server")
|
||||||
|
|
@ -560,8 +512,7 @@ Examples:
|
||||||
])
|
])
|
||||||
|
|
||||||
const configPath = await (async () => {
|
const configPath = await (async () => {
|
||||||
if (input.global || input.scope === "global") return globalConfigPath
|
if (input.global) return globalConfigPath
|
||||||
if (input.scope === "project") return projectConfigPath
|
|
||||||
if (inlineConfig) return project.vcs === "git" ? projectConfigPath : globalConfigPath
|
if (inlineConfig) return project.vcs === "git" ? projectConfigPath : globalConfigPath
|
||||||
if (project.vcs !== "git") return globalConfigPath
|
if (project.vcs !== "git") return globalConfigPath
|
||||||
const scopeResult = await prompts.select({
|
const scopeResult = await prompts.select({
|
||||||
|
|
@ -730,9 +681,6 @@ function parseInlineMcpAdd(
|
||||||
const name = input.name?.trim()
|
const name = input.name?.trim()
|
||||||
if (!name) return { error: "MCP server name is required" }
|
if (!name) return { error: "MCP server name is required" }
|
||||||
if (inlineArgs.length === 0) return { error: "URL or command is required" }
|
if (inlineArgs.length === 0) return { error: "URL or command is required" }
|
||||||
if (input.timeout !== undefined && (!Number.isInteger(input.timeout) || input.timeout <= 0)) {
|
|
||||||
return { error: "--timeout must be a positive integer" }
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = input.type ?? (inlineArgs.length === 1 && URL.canParse(inlineArgs[0]) ? "remote" : "local")
|
const type = input.type ?? (inlineArgs.length === 1 && URL.canParse(inlineArgs[0]) ? "remote" : "local")
|
||||||
if (type === "local") return parseInlineLocalMcp(input, inlineArgs)
|
if (type === "local") return parseInlineLocalMcp(input, inlineArgs)
|
||||||
|
|
@ -740,26 +688,11 @@ function parseInlineMcpAdd(
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasInlineMcpAdd(input: McpAddArgs, inlineArgs: string[]) {
|
function hasInlineMcpAdd(input: McpAddArgs, inlineArgs: string[]) {
|
||||||
return !!(
|
return !!(input.name || inlineArgs.length > 0 || input.type || input.env?.length || input.header?.length)
|
||||||
input.name ||
|
|
||||||
inlineArgs.length > 0 ||
|
|
||||||
input.type ||
|
|
||||||
input.env?.length ||
|
|
||||||
input.header?.length ||
|
|
||||||
input.enabled !== undefined ||
|
|
||||||
input.timeout !== undefined ||
|
|
||||||
input.oauth !== undefined ||
|
|
||||||
input.oauthClientId ||
|
|
||||||
input.oauthClientSecret ||
|
|
||||||
input.oauthScope ||
|
|
||||||
input.oauthCallbackPort !== undefined ||
|
|
||||||
input.oauthRedirectUri
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseInlineLocalMcp(args: McpAddArgs, command: string[]): { config: ConfigMCP.Info } | { error: string } {
|
function parseInlineLocalMcp(args: McpAddArgs, command: string[]): { config: ConfigMCP.Info } | { error: string } {
|
||||||
if (args.header?.length) return { error: "--header can only be used with --type remote" }
|
if (args.header?.length) return { error: "--header can only be used with --type remote" }
|
||||||
if (hasOAuthOptions(args)) return { error: "OAuth options can only be used with --type remote" }
|
|
||||||
const environment = parseEnv(args.env)
|
const environment = parseEnv(args.env)
|
||||||
if ("error" in environment) return environment
|
if ("error" in environment) return environment
|
||||||
return {
|
return {
|
||||||
|
|
@ -767,8 +700,6 @@ function parseInlineLocalMcp(args: McpAddArgs, command: string[]): { config: Con
|
||||||
type: "local",
|
type: "local",
|
||||||
command,
|
command,
|
||||||
...(environment.value && { environment: environment.value }),
|
...(environment.value && { environment: environment.value }),
|
||||||
...(args.enabled !== undefined && { enabled: args.enabled }),
|
|
||||||
...(args.timeout !== undefined && { timeout: args.timeout }),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -777,26 +708,13 @@ function parseInlineRemoteMcp(args: McpAddArgs, url: string[]): { config: Config
|
||||||
if (url.length !== 1) return { error: "Remote MCP servers require exactly one URL" }
|
if (url.length !== 1) return { error: "Remote MCP servers require exactly one URL" }
|
||||||
if (!URL.canParse(url[0])) return { error: "Remote MCP server URL is invalid" }
|
if (!URL.canParse(url[0])) return { error: "Remote MCP server URL is invalid" }
|
||||||
if (args.env?.length) return { error: "--env can only be used with --type local" }
|
if (args.env?.length) return { error: "--env can only be used with --type local" }
|
||||||
if (
|
|
||||||
args.oauthCallbackPort !== undefined &&
|
|
||||||
(!Number.isInteger(args.oauthCallbackPort) || args.oauthCallbackPort < 1 || args.oauthCallbackPort > 65535)
|
|
||||||
) {
|
|
||||||
return { error: "--oauth-callback-port must be an integer between 1 and 65535" }
|
|
||||||
}
|
|
||||||
if (args.oauth === false && hasOAuthConfigOptions(args)) {
|
|
||||||
return { error: "--no-oauth cannot be combined with OAuth options" }
|
|
||||||
}
|
|
||||||
const headers = parseHeader(args.header)
|
const headers = parseHeader(args.header)
|
||||||
if ("error" in headers) return headers
|
if ("error" in headers) return headers
|
||||||
const oauth = parseOAuth(args)
|
|
||||||
return {
|
return {
|
||||||
config: {
|
config: {
|
||||||
type: "remote",
|
type: "remote",
|
||||||
url: url[0],
|
url: url[0],
|
||||||
...(headers.value && { headers: headers.value }),
|
...(headers.value && { headers: headers.value }),
|
||||||
...(args.enabled !== undefined && { enabled: args.enabled }),
|
|
||||||
...(args.timeout !== undefined && { timeout: args.timeout }),
|
|
||||||
...(oauth !== undefined && { oauth }),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -829,32 +747,6 @@ function parseHeader(entries?: string[]): { value?: Record<string, string> } | {
|
||||||
return { value: Object.fromEntries(parsed.map((entry) => [entry.key, entry.value])) }
|
return { value: Object.fromEntries(parsed.map((entry) => [entry.key, entry.value])) }
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasOAuthOptions(args: McpAddArgs) {
|
|
||||||
return !!(args.oauth !== undefined || hasOAuthConfigOptions(args))
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasOAuthConfigOptions(args: McpAddArgs) {
|
|
||||||
return !!(
|
|
||||||
args.oauthClientId ||
|
|
||||||
args.oauthClientSecret ||
|
|
||||||
args.oauthScope ||
|
|
||||||
args.oauthCallbackPort !== undefined ||
|
|
||||||
args.oauthRedirectUri
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseOAuth(args: McpAddArgs): ConfigMCP.Remote["oauth"] | undefined {
|
|
||||||
if (args.oauth === false) return false
|
|
||||||
if (!hasOAuthOptions(args)) return undefined
|
|
||||||
return {
|
|
||||||
...(args.oauthClientId && { clientId: args.oauthClientId }),
|
|
||||||
...(args.oauthClientSecret && { clientSecret: args.oauthClientSecret }),
|
|
||||||
...(args.oauthScope && { scope: args.oauthScope }),
|
|
||||||
...(args.oauthCallbackPort !== undefined && { callbackPort: args.oauthCallbackPort }),
|
|
||||||
...(args.oauthRedirectUri && { redirectUri: args.oauthRedirectUri }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const McpDebugCommand = effectCmd({
|
export const McpDebugCommand = effectCmd({
|
||||||
command: "debug <name>",
|
command: "debug <name>",
|
||||||
describe: "debug OAuth connection for an MCP server",
|
describe: "debug OAuth connection for an MCP server",
|
||||||
|
|
|
||||||
|
|
@ -434,32 +434,22 @@ Positionals:
|
||||||
args URL for remote servers or command and arguments for local servers [array] [default: []]
|
args URL for remote servers or command and arguments for local servers [array] [default: []]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help show help [boolean]
|
-h, --help show help [boolean]
|
||||||
-v, --version show version number [boolean]
|
-v, --version show version number [boolean]
|
||||||
--print-logs print logs to stderr [boolean]
|
--print-logs print logs to stderr [boolean]
|
||||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||||
--pure run without external plugins [boolean]
|
--pure run without external plugins [boolean]
|
||||||
--type server type: local or remote [string] [choices: "local", "remote"]
|
--type server type: local or remote [string] [choices: "local", "remote"]
|
||||||
--env environment variable for local servers (KEY=VALUE) [array]
|
--env environment variable for local servers (KEY=VALUE) [array]
|
||||||
--header HTTP header for remote servers (KEY=VALUE or 'KEY: VALUE') [array]
|
--header HTTP header for remote servers (KEY=VALUE or 'KEY: VALUE') [array]
|
||||||
--scope where to save the server [string] [choices: "project", "global"]
|
-g, --global save to global config [boolean]
|
||||||
-g, --global save to global config [boolean]
|
|
||||||
--enabled enable or disable the server on startup [boolean]
|
|
||||||
--timeout timeout in milliseconds for MCP server requests [number]
|
|
||||||
--oauth enable OAuth for remote servers, or use --no-oauth to disable
|
|
||||||
auto-detection [boolean]
|
|
||||||
--oauth-client-id OAuth client ID for remote servers [string]
|
|
||||||
--oauth-client-secret OAuth client secret for remote servers [string]
|
|
||||||
--oauth-scope OAuth scopes to request for remote servers [string]
|
|
||||||
--oauth-callback-port OAuth local callback port for remote servers [number]
|
|
||||||
--oauth-redirect-uri OAuth redirect URI for remote servers [string]
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
opencode mcp add <name> -- <command> [args...] (local MCP server)
|
opencode mcp add <name> -- <command> [args...] (local MCP server)
|
||||||
opencode mcp add <name> --env KEY=VALUE -- <command> [args...] (local MCP server with env vars)
|
opencode mcp add <name> --env KEY=VALUE -- <command> [args...] (local MCP server with env vars)
|
||||||
opencode mcp add <name> <url> (remote MCP server)
|
opencode mcp add <name> <url> (remote MCP server)
|
||||||
opencode mcp add <name> --header KEY=VALUE <url> (remote MCP server with headers)
|
opencode mcp add <name> --header KEY=VALUE <url> (remote MCP server with headers)
|
||||||
opencode mcp add <name> --scope project -- <command> [args...] (save to project config)
|
opencode mcp add <name> --global <url> (save to global config)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
opencode mcp add context7 -- npx -y @upstash/context7-mcp
|
opencode mcp add context7 -- npx -y @upstash/context7-mcp
|
||||||
|
|
|
||||||
|
|
@ -62,28 +62,6 @@ describe("opencode mcp", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const noOAuth = yield* opencode.spawn([
|
|
||||||
"mcp",
|
|
||||||
"add",
|
|
||||||
"public",
|
|
||||||
"https://example.com/mcp",
|
|
||||||
"--no-oauth",
|
|
||||||
"--global",
|
|
||||||
])
|
|
||||||
opencode.expectExit(noOAuth, 0, "opencode mcp add no oauth")
|
|
||||||
|
|
||||||
expect(
|
|
||||||
yield* Effect.promise(() => Bun.file(path.join(home, ".config/opencode/opencode.json")).json()),
|
|
||||||
).toMatchObject({
|
|
||||||
mcp: {
|
|
||||||
public: {
|
|
||||||
type: "remote",
|
|
||||||
url: "https://example.com/mcp",
|
|
||||||
oauth: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
120_000,
|
120_000,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ opencode mcp add github https://api.githubcopilot.com/mcp \
|
||||||
--header "Authorization: Bearer YOUR_GITHUB_PAT"
|
--header "Authorization: Bearer YOUR_GITHUB_PAT"
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `--env KEY=VALUE` for local server environment variables and repeat it for multiple values. Use `--scope project` or `--global` to choose where the server is saved.
|
Use `--env KEY=VALUE` for local server environment variables and repeat it for multiple values. Use `--global` to save to global config.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue