mirror of
https://github.com/sst/opencode.git
synced 2025-07-07 16:14:59 +00:00
Merge 38435689f5
into d87922c0eb
This commit is contained in:
commit
a576123e91
7 changed files with 1480 additions and 0 deletions
|
@ -288,6 +288,13 @@
|
|||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable or disable the MCP server on startup"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "HTTP headers for remote connection"
|
||||
}
|
||||
},
|
||||
"required": ["type", "url"],
|
||||
|
|
574
packages/opencode/src/cli/cmd/mcp.ts
Normal file
574
packages/opencode/src/cli/cmd/mcp.ts
Normal file
|
@ -0,0 +1,574 @@
|
|||
import { cmd } from "./cmd"
|
||||
import { Config } from "../../config/config"
|
||||
import { UI } from "../ui"
|
||||
import { Global } from "../../global"
|
||||
import path from "path"
|
||||
import { z } from "zod"
|
||||
|
||||
async function loadProjectConfig(configPath: string): Promise<Config.Info> {
|
||||
try {
|
||||
const file = Bun.file(configPath)
|
||||
const data = await file.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
// If file doesn't exist, return empty config
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export const McpCommand = cmd({
|
||||
command: "mcp [command]",
|
||||
describe: "Configure and manage MCP servers",
|
||||
builder: (yargs) => {
|
||||
const configured = yargs
|
||||
.command(McpAddCommand)
|
||||
.command(McpRemoveCommand)
|
||||
.command(McpListCommand)
|
||||
.command(McpGetCommand)
|
||||
.command(McpAddJsonCommand)
|
||||
.command(McpEnableCommand)
|
||||
.command(McpDisableCommand)
|
||||
.demandCommand()
|
||||
.help()
|
||||
|
||||
return configured
|
||||
},
|
||||
handler: async () => {},
|
||||
})
|
||||
|
||||
export const McpAddCommand = cmd({
|
||||
command: "add <name> <commandOrUrl> [args...]",
|
||||
describe: "Add a server",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional("name", {
|
||||
type: "string",
|
||||
describe: "Name of the MCP server",
|
||||
demandOption: true,
|
||||
})
|
||||
.positional("commandOrUrl", {
|
||||
type: "string",
|
||||
describe: "Command to run (for stdio) or URL (for SSE)",
|
||||
demandOption: true,
|
||||
})
|
||||
.positional("args", {
|
||||
type: "string",
|
||||
array: true,
|
||||
describe: "Additional arguments for stdio command",
|
||||
default: [],
|
||||
})
|
||||
.option("scope", {
|
||||
alias: "s",
|
||||
type: "string",
|
||||
choices: ["user", "project"] as const,
|
||||
default: "project",
|
||||
describe: "Configuration scope (user, or project)",
|
||||
})
|
||||
.option("transport", {
|
||||
alias: "t",
|
||||
type: "string",
|
||||
choices: ["stdio", "sse"] as const,
|
||||
default: "stdio",
|
||||
describe: "Transport type (stdio, sse)",
|
||||
})
|
||||
.option("env", {
|
||||
alias: "e",
|
||||
type: "string",
|
||||
array: true,
|
||||
describe: "Set environment variables (e.g. -e KEY=value)",
|
||||
default: [],
|
||||
})
|
||||
.option("header", {
|
||||
alias: "H",
|
||||
type: "string",
|
||||
array: true,
|
||||
describe:
|
||||
'Set HTTP headers for SSE transport (e.g. -H "X-Api-Key: abc123")',
|
||||
default: [],
|
||||
}),
|
||||
handler: async (args) => {
|
||||
// Parse environment variables
|
||||
const environment: Record<string, string> = {}
|
||||
for (const envVar of args.env) {
|
||||
const [key, ...valueParts] = envVar.split("=")
|
||||
if (!key || valueParts.length === 0) {
|
||||
UI.error(
|
||||
`Invalid environment variable format: ${envVar}. Use KEY=VALUE format.`,
|
||||
)
|
||||
return
|
||||
}
|
||||
environment[key] = valueParts.join("=")
|
||||
}
|
||||
|
||||
// Parse headers
|
||||
const headers: Record<string, string> = {}
|
||||
for (const header of args.header) {
|
||||
const [key, ...valueParts] = header.split(":")
|
||||
if (!key || valueParts.length === 0) {
|
||||
UI.error(`Invalid header format: ${header}. Use "Key: Value" format.`)
|
||||
return
|
||||
}
|
||||
headers[key.trim()] = valueParts.join(":").trim()
|
||||
}
|
||||
|
||||
// Determine server type based on transport and URL
|
||||
const serverType = args.transport === "stdio" ? "local" : "remote"
|
||||
const isUrl =
|
||||
args.commandOrUrl.startsWith("http://") ||
|
||||
args.commandOrUrl.startsWith("https://")
|
||||
|
||||
// Validate transport constraints
|
||||
if (args.transport === "stdio") {
|
||||
if (isUrl) {
|
||||
UI.error("stdio transport requires a command, not a URL")
|
||||
return
|
||||
}
|
||||
if (Object.keys(headers).length > 0) {
|
||||
UI.error("stdio transport doesn't support headers")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// sse transport
|
||||
if (!isUrl) {
|
||||
UI.error(`${args.transport} transport requires a URL`)
|
||||
return
|
||||
}
|
||||
if (args.args.length > 0) {
|
||||
UI.error(
|
||||
`${args.transport} transport doesn't accept additional arguments`,
|
||||
)
|
||||
return
|
||||
}
|
||||
if (Object.keys(environment).length > 0) {
|
||||
UI.error(
|
||||
`${args.transport} transport doesn't support environment variables`,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create config
|
||||
const mcpConfig: Config.Mcp =
|
||||
serverType === "remote"
|
||||
? {
|
||||
type: "remote",
|
||||
url: args.commandOrUrl,
|
||||
...(Object.keys(headers).length > 0 && { headers }),
|
||||
}
|
||||
: {
|
||||
type: "local",
|
||||
command: [args.commandOrUrl, ...args.args],
|
||||
...(Object.keys(environment).length > 0 && { environment }),
|
||||
}
|
||||
|
||||
// Determine config path based on scope
|
||||
const configPath =
|
||||
args.scope === "user"
|
||||
? path.join(Global.Path.config, "config.json")
|
||||
: path.join(process.cwd(), "opencode.json")
|
||||
|
||||
// Load current config
|
||||
const currentConfig =
|
||||
args.scope === "user"
|
||||
? await Config.global()
|
||||
: await loadProjectConfig(configPath)
|
||||
|
||||
const updatedConfig = {
|
||||
...currentConfig,
|
||||
mcp: {
|
||||
...currentConfig.mcp,
|
||||
[args.name]: mcpConfig,
|
||||
},
|
||||
}
|
||||
|
||||
await Bun.write(configPath, JSON.stringify(updatedConfig, null, 2))
|
||||
|
||||
UI.println(
|
||||
`Added MCP server "${args.name}" (${args.transport}) to ${args.scope} config`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export const McpRemoveCommand = cmd({
|
||||
command: "remove <name>",
|
||||
describe: "Remove an MCP server",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional("name", {
|
||||
type: "string",
|
||||
describe: "Name of the MCP server to remove",
|
||||
demandOption: true,
|
||||
})
|
||||
.option("scope", {
|
||||
alias: "s",
|
||||
type: "string",
|
||||
choices: ["user", "project"] as const,
|
||||
default: "project",
|
||||
describe: "Configuration scope (user, or project)",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
// Determine config path based on scope
|
||||
const configPath =
|
||||
args.scope === "user"
|
||||
? path.join(Global.Path.config, "config.json")
|
||||
: path.join(process.cwd(), "opencode.json")
|
||||
|
||||
// Load current config
|
||||
const currentConfig =
|
||||
args.scope === "user"
|
||||
? await Config.global()
|
||||
: await loadProjectConfig(configPath)
|
||||
|
||||
if (!currentConfig.mcp || !currentConfig.mcp[args.name]) {
|
||||
UI.error(`MCP server "${args.name}" not found in ${args.scope} config`)
|
||||
return
|
||||
}
|
||||
|
||||
const { [args.name]: removed, ...remainingMcp } = currentConfig.mcp
|
||||
const updatedConfig = {
|
||||
...currentConfig,
|
||||
mcp: Object.keys(remainingMcp).length > 0 ? remainingMcp : undefined,
|
||||
}
|
||||
|
||||
await Bun.write(configPath, JSON.stringify(updatedConfig, null, 2))
|
||||
|
||||
UI.println(`Removed MCP server "${args.name}" from ${args.scope} config`)
|
||||
},
|
||||
})
|
||||
|
||||
export const McpListCommand = cmd({
|
||||
command: "list",
|
||||
describe: "List configured MCP servers",
|
||||
handler: async () => {
|
||||
const globalConfig = await Config.global()
|
||||
const projectConfigPath = path.join(process.cwd(), "opencode.json")
|
||||
const projectConfig = await loadProjectConfig(projectConfigPath)
|
||||
|
||||
const hasGlobalServers =
|
||||
globalConfig.mcp && Object.keys(globalConfig.mcp).length > 0
|
||||
const hasProjectServers =
|
||||
projectConfig.mcp && Object.keys(projectConfig.mcp).length > 0
|
||||
|
||||
if (!hasGlobalServers && !hasProjectServers) {
|
||||
UI.println("No MCP servers configured")
|
||||
return
|
||||
}
|
||||
|
||||
// Display global servers
|
||||
if (hasGlobalServers) {
|
||||
UI.println("Global MCP servers:")
|
||||
UI.empty()
|
||||
|
||||
for (const [name, mcpConfig] of Object.entries(globalConfig.mcp!)) {
|
||||
const status = mcpConfig.enabled === false ? " (disabled)" : ""
|
||||
UI.println(` ${name} (${mcpConfig.type})${status}`)
|
||||
if (mcpConfig.type === "local") {
|
||||
UI.println(` Command: ${mcpConfig.command.join(" ")}`)
|
||||
if (
|
||||
mcpConfig.environment &&
|
||||
Object.keys(mcpConfig.environment).length > 0
|
||||
) {
|
||||
UI.println(` Environment:`)
|
||||
for (const [key, value] of Object.entries(mcpConfig.environment)) {
|
||||
UI.println(` ${key}=${value}`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
UI.println(` URL: ${mcpConfig.url}`)
|
||||
}
|
||||
UI.empty()
|
||||
}
|
||||
}
|
||||
|
||||
// Display project servers
|
||||
if (hasProjectServers) {
|
||||
UI.println("Project MCP servers:")
|
||||
UI.empty()
|
||||
|
||||
for (const [name, mcpConfig] of Object.entries(projectConfig.mcp!)) {
|
||||
const status = mcpConfig.enabled === false ? " (disabled)" : ""
|
||||
UI.println(` ${name} (${mcpConfig.type})${status}`)
|
||||
if (mcpConfig.type === "local") {
|
||||
UI.println(` Command: ${mcpConfig.command.join(" ")}`)
|
||||
if (
|
||||
mcpConfig.environment &&
|
||||
Object.keys(mcpConfig.environment).length > 0
|
||||
) {
|
||||
UI.println(` Environment:`)
|
||||
for (const [key, value] of Object.entries(mcpConfig.environment)) {
|
||||
UI.println(` ${key}=${value}`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
UI.println(` URL: ${mcpConfig.url}`)
|
||||
}
|
||||
UI.empty()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const McpGetCommand = cmd({
|
||||
command: "get <name>",
|
||||
describe: "Get details about an MCP server",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("name", {
|
||||
type: "string",
|
||||
describe: "Name of the MCP server",
|
||||
demandOption: true,
|
||||
}),
|
||||
handler: async (args) => {
|
||||
const globalConfig = await Config.global()
|
||||
const projectConfigPath = path.join(process.cwd(), "opencode.json")
|
||||
const projectConfig = await loadProjectConfig(projectConfigPath)
|
||||
|
||||
let foundConfig: Config.Mcp | null = null
|
||||
let foundScope: string | null = null
|
||||
|
||||
// Check project config first (takes priority)
|
||||
if (projectConfig.mcp && projectConfig.mcp[args.name]) {
|
||||
foundConfig = projectConfig.mcp[args.name]
|
||||
foundScope = "project"
|
||||
}
|
||||
// Then check global config
|
||||
else if (globalConfig.mcp && globalConfig.mcp[args.name]) {
|
||||
foundConfig = globalConfig.mcp[args.name]
|
||||
foundScope = "user"
|
||||
}
|
||||
|
||||
if (!foundConfig || !foundScope) {
|
||||
UI.error(`MCP server "${args.name}" not found`)
|
||||
return
|
||||
}
|
||||
|
||||
UI.println(`MCP Server: ${args.name}`)
|
||||
UI.println(`Scope: ${foundScope}`)
|
||||
UI.println(`Type: ${foundConfig.type}`)
|
||||
UI.println(`Enabled: ${foundConfig.enabled !== false ? "true" : "false"}`)
|
||||
|
||||
if (foundConfig.type === "local") {
|
||||
UI.println(`Command: ${foundConfig.command.join(" ")}`)
|
||||
if (
|
||||
foundConfig.environment &&
|
||||
Object.keys(foundConfig.environment).length > 0
|
||||
) {
|
||||
UI.println(`Environment variables:`)
|
||||
for (const [key, value] of Object.entries(foundConfig.environment)) {
|
||||
UI.println(` ${key}=${value}`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
UI.println(`URL: ${foundConfig.url}`)
|
||||
if (foundConfig.headers && Object.keys(foundConfig.headers).length > 0) {
|
||||
UI.println(`Headers:`)
|
||||
for (const [key, value] of Object.entries(foundConfig.headers)) {
|
||||
UI.println(` ${key}: ${value}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const McpAddJsonCommand = cmd({
|
||||
command: "add-json <name> <json>",
|
||||
describe: "Add an MCP server (stdio or SSE) with a JSON string",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional("name", {
|
||||
type: "string",
|
||||
describe: "Name of the MCP server",
|
||||
demandOption: true,
|
||||
})
|
||||
.positional("json", {
|
||||
type: "string",
|
||||
describe: "JSON configuration for the MCP server",
|
||||
demandOption: true,
|
||||
})
|
||||
.option("scope", {
|
||||
alias: "s",
|
||||
type: "string",
|
||||
choices: ["user", "project"] as const,
|
||||
default: "project",
|
||||
describe: "Configuration scope (user, or project)",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
try {
|
||||
const jsonConfig = JSON.parse(args.json)
|
||||
|
||||
// Infer type and transform to match schema
|
||||
let mcpConfig
|
||||
if ("command" in jsonConfig) {
|
||||
// Transform stdio transport format
|
||||
const { type, command, args, env, ...rest } = jsonConfig
|
||||
|
||||
// Build command array
|
||||
const commandArray = Array.isArray(command) ? command : [command]
|
||||
if (args && Array.isArray(args)) {
|
||||
commandArray.push(...args)
|
||||
}
|
||||
|
||||
mcpConfig = Config.Mcp.parse({
|
||||
type: "local",
|
||||
command: commandArray,
|
||||
...(env && { environment: env }),
|
||||
...rest,
|
||||
})
|
||||
} else if ("url" in jsonConfig) {
|
||||
// Transform sse transport format
|
||||
const { type, ...rest } = jsonConfig
|
||||
mcpConfig = Config.Mcp.parse({
|
||||
type: "remote",
|
||||
...rest,
|
||||
})
|
||||
} else {
|
||||
UI.error(
|
||||
"Invalid MCP configuration: Unable to determine transport type from JSON. Must include either 'command' for stdio or 'url' for sse.",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine config path based on scope
|
||||
const configPath =
|
||||
args.scope === "user"
|
||||
? path.join(Global.Path.config, "config.json")
|
||||
: path.join(process.cwd(), "opencode.json")
|
||||
|
||||
// Load current config
|
||||
const currentConfig =
|
||||
args.scope === "user"
|
||||
? await Config.global()
|
||||
: await loadProjectConfig(configPath)
|
||||
|
||||
const updatedConfig = {
|
||||
...currentConfig,
|
||||
mcp: {
|
||||
...currentConfig.mcp,
|
||||
[args.name]: mcpConfig,
|
||||
},
|
||||
}
|
||||
|
||||
await Bun.write(configPath, JSON.stringify(updatedConfig, null, 2))
|
||||
|
||||
UI.println(
|
||||
`Added MCP server "${args.name}" (${mcpConfig.type}) to ${args.scope} config`,
|
||||
)
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
UI.error(`Invalid JSON: ${error.message}`)
|
||||
return
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
UI.error(`Invalid MCP configuration:`)
|
||||
for (const issue of error.issues) {
|
||||
UI.error(` ${issue.path.join(".")}: ${issue.message}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
UI.error(`Failed to add MCP server: ${error}`)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const McpEnableCommand = cmd({
|
||||
command: "enable <name>",
|
||||
describe: "Enable an MCP server",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional("name", {
|
||||
type: "string",
|
||||
describe: "Name of the MCP server to enable",
|
||||
demandOption: true,
|
||||
})
|
||||
.option("scope", {
|
||||
alias: "s",
|
||||
type: "string",
|
||||
choices: ["user", "project"] as const,
|
||||
default: "project",
|
||||
describe: "Configuration scope (user, or project)",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
// Determine config path based on scope
|
||||
const configPath =
|
||||
args.scope === "user"
|
||||
? path.join(Global.Path.config, "config.json")
|
||||
: path.join(process.cwd(), "opencode.json")
|
||||
|
||||
// Load current config
|
||||
const currentConfig =
|
||||
args.scope === "user"
|
||||
? await Config.global()
|
||||
: await loadProjectConfig(configPath)
|
||||
|
||||
if (!currentConfig.mcp || !currentConfig.mcp[args.name]) {
|
||||
UI.error(`MCP server "${args.name}" not found in ${args.scope} config`)
|
||||
return
|
||||
}
|
||||
|
||||
const updatedConfig = {
|
||||
...currentConfig,
|
||||
mcp: {
|
||||
...currentConfig.mcp,
|
||||
[args.name]: {
|
||||
...currentConfig.mcp[args.name],
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
await Bun.write(configPath, JSON.stringify(updatedConfig, null, 2))
|
||||
|
||||
UI.println(`Enabled MCP server "${args.name}" in ${args.scope} config`)
|
||||
},
|
||||
})
|
||||
|
||||
export const McpDisableCommand = cmd({
|
||||
command: "disable <name>",
|
||||
describe: "Disable an MCP server",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional("name", {
|
||||
type: "string",
|
||||
describe: "Name of the MCP server to disable",
|
||||
demandOption: true,
|
||||
})
|
||||
.option("scope", {
|
||||
alias: "s",
|
||||
type: "string",
|
||||
choices: ["user", "project"] as const,
|
||||
default: "project",
|
||||
describe: "Configuration scope (user, or project)",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
// Determine config path based on scope
|
||||
const configPath =
|
||||
args.scope === "user"
|
||||
? path.join(Global.Path.config, "config.json")
|
||||
: path.join(process.cwd(), "opencode.json")
|
||||
|
||||
// Load current config
|
||||
const currentConfig =
|
||||
args.scope === "user"
|
||||
? await Config.global()
|
||||
: await loadProjectConfig(configPath)
|
||||
|
||||
if (!currentConfig.mcp || !currentConfig.mcp[args.name]) {
|
||||
UI.error(`MCP server "${args.name}" not found in ${args.scope} config`)
|
||||
return
|
||||
}
|
||||
|
||||
const updatedConfig = {
|
||||
...currentConfig,
|
||||
mcp: {
|
||||
...currentConfig.mcp,
|
||||
[args.name]: {
|
||||
...currentConfig.mcp[args.name],
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
await Bun.write(configPath, JSON.stringify(updatedConfig, null, 2))
|
||||
|
||||
UI.println(`Disabled MCP server "${args.name}" in ${args.scope} config`)
|
||||
},
|
||||
})
|
|
@ -55,6 +55,10 @@ export namespace Config {
|
|||
.boolean()
|
||||
.optional()
|
||||
.describe("Enable or disable the MCP server on startup"),
|
||||
headers: z
|
||||
.record(z.string(), z.string())
|
||||
.optional()
|
||||
.describe("HTTP headers for remote connection"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Log } from "./util/log"
|
|||
import { AuthCommand } from "./cli/cmd/auth"
|
||||
import { UpgradeCommand } from "./cli/cmd/upgrade"
|
||||
import { ModelsCommand } from "./cli/cmd/models"
|
||||
import { McpCommand } from "./cli/cmd/mcp"
|
||||
import { UI } from "./cli/ui"
|
||||
import { Installation } from "./installation"
|
||||
import { NamedError } from "./util/error"
|
||||
|
@ -54,6 +55,7 @@ const cli = yargs(hideBin(process.argv))
|
|||
.command(UpgradeCommand)
|
||||
.command(ServeCommand)
|
||||
.command(ModelsCommand)
|
||||
.command(McpCommand)
|
||||
.fail((msg) => {
|
||||
if (
|
||||
msg.startsWith("Unknown argument") ||
|
||||
|
|
|
@ -37,6 +37,7 @@ export namespace MCP {
|
|||
transport: {
|
||||
type: "sse",
|
||||
url: mcp.url,
|
||||
...(mcp.headers && { headers: mcp.headers }),
|
||||
},
|
||||
}).catch(() => {})
|
||||
if (!client) {
|
||||
|
|
774
packages/opencode/test/cmd/mcp.test.ts
Normal file
774
packages/opencode/test/cmd/mcp.test.ts
Normal file
|
@ -0,0 +1,774 @@
|
|||
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"
|
||||
import {
|
||||
McpAddCommand,
|
||||
McpRemoveCommand,
|
||||
McpListCommand,
|
||||
McpGetCommand,
|
||||
McpAddJsonCommand,
|
||||
McpEnableCommand,
|
||||
McpDisableCommand,
|
||||
} from "../../src/cli/cmd/mcp"
|
||||
import { Config } from "../../src/config/config"
|
||||
import { Global } from "../../src/global"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
|
||||
const testConfigDir = path.join(process.cwd(), "test-config")
|
||||
const testConfigPath = path.join(testConfigDir, "config.json")
|
||||
const testProjectDir = path.join(process.cwd(), "test-project")
|
||||
const testProjectConfigPath = path.join(testProjectDir, "opencode.json")
|
||||
|
||||
let originalCwd: string
|
||||
let originalGlobalPath: string
|
||||
let configGlobalSpy: ReturnType<typeof spyOn> | undefined
|
||||
let configGetSpy: ReturnType<typeof spyOn> | undefined
|
||||
|
||||
// Helper to capture stderr output
|
||||
function captureStderr() {
|
||||
const outputs: string[] = []
|
||||
|
||||
const spy = spyOn(Bun.stderr, "write").mockImplementation(async (data) => {
|
||||
if (typeof data === "string") {
|
||||
outputs.push(data)
|
||||
} else if (data instanceof Uint8Array) {
|
||||
outputs.push(new TextDecoder().decode(data))
|
||||
} else {
|
||||
outputs.push(String(data))
|
||||
}
|
||||
return typeof data === "string" ? data.length : 0
|
||||
})
|
||||
|
||||
return {
|
||||
getOutput: () => outputs.join(""),
|
||||
restore: () => spy.mockRestore(),
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create test directories
|
||||
await fs.promises.mkdir(testConfigDir, { recursive: true })
|
||||
await fs.promises.mkdir(testProjectDir, { recursive: true })
|
||||
|
||||
// Store original values
|
||||
originalGlobalPath = Global.Path.config
|
||||
originalCwd = process.cwd()
|
||||
|
||||
// Mock Global.Path.config to use test directory
|
||||
// @ts-expect-error - Mocking for tests
|
||||
Global.Path.config = testConfigDir
|
||||
|
||||
// Mock process.cwd to use test project directory
|
||||
process.cwd = () => testProjectDir
|
||||
|
||||
// Create empty configs with proper structure
|
||||
await Bun.write(testConfigPath, JSON.stringify({}, null, 2))
|
||||
await Bun.write(testProjectConfigPath, JSON.stringify({}, null, 2))
|
||||
|
||||
// Spy on Config.global to use test config
|
||||
configGlobalSpy = spyOn(Config, "global").mockImplementation(async () => {
|
||||
try {
|
||||
const file = Bun.file(testConfigPath)
|
||||
const data = await file.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
|
||||
// Spy on Config.get to use test configs (merged global + project)
|
||||
configGetSpy = spyOn(Config, "get").mockImplementation(async () => {
|
||||
const globalConfig = await configGlobalSpy!()
|
||||
let projectConfig = {}
|
||||
try {
|
||||
const file = Bun.file(testProjectConfigPath)
|
||||
projectConfig = await file.json()
|
||||
} catch (error) {
|
||||
// Project config doesn't exist yet
|
||||
}
|
||||
// Simple merge - project config takes precedence
|
||||
return { ...globalConfig, ...projectConfig }
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Restore original values
|
||||
process.cwd = () => originalCwd
|
||||
// @ts-expect-error - Restoring mocked value
|
||||
Global.Path.config = originalGlobalPath
|
||||
|
||||
// Restore spies
|
||||
configGlobalSpy?.mockRestore()
|
||||
configGetSpy?.mockRestore()
|
||||
|
||||
// Clean up test directories
|
||||
await fs.promises.rm(testConfigDir, { recursive: true, force: true })
|
||||
await fs.promises.rm(testProjectDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe("mcp command", () => {
|
||||
test("add local MCP server", async () => {
|
||||
const args = {
|
||||
name: "test-server",
|
||||
commandOrUrl: "node",
|
||||
args: ["server.js"],
|
||||
scope: "project" as const,
|
||||
transport: "stdio" as const,
|
||||
env: ["NODE_ENV=test", "PORT=3000"],
|
||||
header: [],
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
|
||||
await McpAddCommand.handler(args)
|
||||
|
||||
// Read project config
|
||||
const projectConfig = await Config.get()
|
||||
expect(projectConfig.mcp).toBeDefined()
|
||||
expect(projectConfig.mcp?.["test-server"]).toMatchObject({
|
||||
type: "local",
|
||||
command: ["node", "server.js"],
|
||||
environment: {
|
||||
NODE_ENV: "test",
|
||||
PORT: "3000",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("add remote MCP server with SSE", async () => {
|
||||
const args = {
|
||||
name: "remote-server",
|
||||
commandOrUrl: "https://example.com/mcp",
|
||||
args: [],
|
||||
scope: "user" as const,
|
||||
transport: "sse" as const,
|
||||
env: [],
|
||||
header: ["Authorization: Bearer token123"],
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
|
||||
await McpAddCommand.handler(args)
|
||||
|
||||
const config = await Config.global()
|
||||
expect(config.mcp).toBeDefined()
|
||||
expect(config.mcp?.["remote-server"]).toMatchObject({
|
||||
type: "remote",
|
||||
url: "https://example.com/mcp",
|
||||
headers: {
|
||||
Authorization: "Bearer token123",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("add SSE transport server to project scope", async () => {
|
||||
const args = {
|
||||
name: "sse-server",
|
||||
commandOrUrl: "http://localhost:8080/mcp",
|
||||
args: [],
|
||||
scope: "project" as const,
|
||||
transport: "sse" as const,
|
||||
env: [],
|
||||
header: ["X-API-Key: abc123", "Content-Type: application/json"],
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
|
||||
await McpAddCommand.handler(args)
|
||||
|
||||
const projectConfig = await Config.get()
|
||||
expect(projectConfig.mcp?.["sse-server"]).toMatchObject({
|
||||
type: "remote",
|
||||
url: "http://localhost:8080/mcp",
|
||||
headers: {
|
||||
"X-API-Key": "abc123",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("validate transport constraints", async () => {
|
||||
const stderr = captureStderr()
|
||||
|
||||
// Test stdio with URL should fail
|
||||
const args = {
|
||||
name: "invalid-stdio",
|
||||
commandOrUrl: "https://example.com",
|
||||
args: [],
|
||||
scope: "project" as const,
|
||||
transport: "stdio" as const,
|
||||
env: [],
|
||||
header: [],
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
|
||||
await McpAddCommand.handler(args)
|
||||
|
||||
expect(stderr.getOutput()).toContain(
|
||||
"stdio transport requires a command, not a URL",
|
||||
)
|
||||
|
||||
stderr.restore()
|
||||
})
|
||||
|
||||
test("add server to user scope", async () => {
|
||||
const args = {
|
||||
name: "user-server",
|
||||
commandOrUrl: "bun",
|
||||
args: ["run", "server.ts"],
|
||||
scope: "user" as const,
|
||||
transport: "stdio" as const,
|
||||
env: ["DEBUG=true"],
|
||||
header: [],
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
|
||||
await McpAddCommand.handler(args)
|
||||
|
||||
const config = await Config.global()
|
||||
expect(config.mcp).toBeDefined()
|
||||
expect(config.mcp?.["user-server"]).toMatchObject({
|
||||
type: "local",
|
||||
command: ["bun", "run", "server.ts"],
|
||||
environment: {
|
||||
DEBUG: "true",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("remove MCP server from project scope", async () => {
|
||||
// First add a server to project config
|
||||
await Bun.write(
|
||||
testProjectConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcp: {
|
||||
"test-server": {
|
||||
type: "local",
|
||||
command: ["node", "server.js"],
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
const args = {
|
||||
name: "test-server",
|
||||
scope: "project" as const,
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
await McpRemoveCommand.handler(args)
|
||||
|
||||
const projectConfig = await Config.get()
|
||||
expect(projectConfig.mcp).toBeUndefined()
|
||||
})
|
||||
|
||||
test("remove MCP server from user scope", async () => {
|
||||
// First add a server to user config
|
||||
await Bun.write(
|
||||
testConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcp: {
|
||||
"user-test-server": {
|
||||
type: "local",
|
||||
command: ["node", "server.js"],
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
const args = {
|
||||
name: "user-test-server",
|
||||
scope: "user" as const,
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
await McpRemoveCommand.handler(args)
|
||||
|
||||
const config = await Config.global()
|
||||
expect(config.mcp).toBeUndefined()
|
||||
})
|
||||
|
||||
test("remove non-existent MCP server shows error", async () => {
|
||||
const stderr = captureStderr()
|
||||
|
||||
const args = {
|
||||
name: "non-existent",
|
||||
scope: "project" as const,
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
await McpRemoveCommand.handler(args)
|
||||
|
||||
expect(stderr.getOutput()).toContain(
|
||||
'MCP server "non-existent" not found in project config',
|
||||
)
|
||||
|
||||
stderr.restore()
|
||||
})
|
||||
|
||||
test("add server with JSON to project scope", async () => {
|
||||
const args = {
|
||||
name: "json-server",
|
||||
scope: "project" as const,
|
||||
json: JSON.stringify({
|
||||
type: "local",
|
||||
command: ["bun", "run", "mcp-server.ts"],
|
||||
environment: { DEBUG: "true" },
|
||||
}),
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
|
||||
await McpAddJsonCommand.handler(args)
|
||||
|
||||
const projectConfig = await Config.get()
|
||||
expect(projectConfig.mcp?.["json-server"]).toMatchObject({
|
||||
type: "local",
|
||||
command: ["bun", "run", "mcp-server.ts"],
|
||||
environment: { DEBUG: "true" },
|
||||
})
|
||||
})
|
||||
|
||||
test("add server with JSON to user scope", async () => {
|
||||
const args = {
|
||||
name: "user-json-server",
|
||||
scope: "user" as const,
|
||||
json: JSON.stringify({
|
||||
type: "remote",
|
||||
url: "https://example.com/mcp",
|
||||
headers: { Authorization: "Bearer token" },
|
||||
}),
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
|
||||
await McpAddJsonCommand.handler(args)
|
||||
|
||||
const config = await Config.global()
|
||||
expect(config.mcp?.["user-json-server"]).toMatchObject({
|
||||
type: "remote",
|
||||
url: "https://example.com/mcp",
|
||||
headers: { Authorization: "Bearer token" },
|
||||
})
|
||||
})
|
||||
|
||||
test("list empty MCP servers", async () => {
|
||||
const stderr = captureStderr()
|
||||
|
||||
await McpListCommand.handler({ $0: "opencode", _: [] })
|
||||
|
||||
expect(stderr.getOutput()).toContain("No MCP servers configured")
|
||||
stderr.restore()
|
||||
})
|
||||
|
||||
test("list global and project MCP servers", async () => {
|
||||
// Add a server to user config
|
||||
await Bun.write(
|
||||
testConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcp: {
|
||||
"global-server": {
|
||||
type: "local",
|
||||
command: ["node", "global-server.js"],
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
// Add a server to project config
|
||||
await Bun.write(
|
||||
testProjectConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcp: {
|
||||
"project-server": {
|
||||
type: "remote",
|
||||
url: "https://example.com/mcp",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
const stderr = captureStderr()
|
||||
|
||||
await McpListCommand.handler({ $0: "opencode", _: [] })
|
||||
|
||||
const output = stderr.getOutput()
|
||||
expect(output).toContain("Global MCP servers:")
|
||||
expect(output).toContain("Project MCP servers:")
|
||||
expect(output).toContain("global-server")
|
||||
expect(output).toContain("project-server")
|
||||
stderr.restore()
|
||||
})
|
||||
|
||||
test("get MCP server details from user scope", async () => {
|
||||
// Add a server to user config
|
||||
await Bun.write(
|
||||
testConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcp: {
|
||||
"detail-server": {
|
||||
type: "local",
|
||||
command: ["node", "server.js"],
|
||||
environment: { PORT: "3000" },
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
const stderr = captureStderr()
|
||||
|
||||
const args = {
|
||||
name: "detail-server",
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
await McpGetCommand.handler(args)
|
||||
|
||||
const output = stderr.getOutput()
|
||||
expect(output).toContain("MCP Server: detail-server")
|
||||
expect(output).toContain("Scope: user")
|
||||
expect(output).toContain("Type: local")
|
||||
expect(output).toContain("Enabled: true")
|
||||
stderr.restore()
|
||||
})
|
||||
|
||||
test("get MCP server details from project scope", async () => {
|
||||
// Add a server to project config
|
||||
await Bun.write(
|
||||
testProjectConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcp: {
|
||||
"project-server": {
|
||||
type: "remote",
|
||||
url: "https://example.com/mcp",
|
||||
headers: { Authorization: "Bearer token" },
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
const stderr = captureStderr()
|
||||
|
||||
const args = {
|
||||
name: "project-server",
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
await McpGetCommand.handler(args)
|
||||
|
||||
const output = stderr.getOutput()
|
||||
expect(output).toContain("MCP Server: project-server")
|
||||
expect(output).toContain("Scope: project")
|
||||
expect(output).toContain("Type: remote")
|
||||
expect(output).toContain("Enabled: true")
|
||||
expect(output).toContain("Headers:")
|
||||
stderr.restore()
|
||||
})
|
||||
|
||||
test("get MCP server prioritizes project over user scope", async () => {
|
||||
// Add server with same name to both configs
|
||||
await Bun.write(
|
||||
testConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcp: {
|
||||
"shared-server": {
|
||||
type: "local",
|
||||
command: ["node", "user-server.js"],
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
await Bun.write(
|
||||
testProjectConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcp: {
|
||||
"shared-server": {
|
||||
type: "local",
|
||||
command: ["node", "project-server.js"],
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
const stderr = captureStderr()
|
||||
|
||||
const args = {
|
||||
name: "shared-server",
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
await McpGetCommand.handler(args)
|
||||
|
||||
const output = stderr.getOutput()
|
||||
expect(output).toContain("Scope: project")
|
||||
expect(output).toContain("project-server.js")
|
||||
expect(output).not.toContain("user-server.js")
|
||||
stderr.restore()
|
||||
})
|
||||
|
||||
test("enable MCP server in project scope", async () => {
|
||||
// First add a disabled server to project config
|
||||
await Bun.write(
|
||||
testProjectConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcp: {
|
||||
"disabled-server": {
|
||||
type: "local",
|
||||
command: ["node", "server.js"],
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
const args = {
|
||||
name: "disabled-server",
|
||||
scope: "project" as const,
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
await McpEnableCommand.handler(args)
|
||||
|
||||
const projectConfig = await Config.get()
|
||||
expect(projectConfig.mcp?.["disabled-server"].enabled).toBe(true)
|
||||
})
|
||||
|
||||
test("enable MCP server in user scope", async () => {
|
||||
// First add a disabled server to user config
|
||||
await Bun.write(
|
||||
testConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcp: {
|
||||
"user-disabled-server": {
|
||||
type: "remote",
|
||||
url: "https://example.com/mcp",
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
const args = {
|
||||
name: "user-disabled-server",
|
||||
scope: "user" as const,
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
await McpEnableCommand.handler(args)
|
||||
|
||||
const config = await Config.global()
|
||||
expect(config.mcp?.["user-disabled-server"].enabled).toBe(true)
|
||||
})
|
||||
|
||||
test("disable MCP server in project scope", async () => {
|
||||
// First add an enabled server to project config
|
||||
await Bun.write(
|
||||
testProjectConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcp: {
|
||||
"enabled-server": {
|
||||
type: "local",
|
||||
command: ["node", "server.js"],
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
const args = {
|
||||
name: "enabled-server",
|
||||
scope: "project" as const,
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
await McpDisableCommand.handler(args)
|
||||
|
||||
const projectConfig = await Config.get()
|
||||
expect(projectConfig.mcp?.["enabled-server"].enabled).toBe(false)
|
||||
})
|
||||
|
||||
test("disable MCP server in user scope", async () => {
|
||||
// First add an enabled server to user config
|
||||
await Bun.write(
|
||||
testConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcp: {
|
||||
"user-enabled-server": {
|
||||
type: "remote",
|
||||
url: "https://example.com/mcp",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
const args = {
|
||||
name: "user-enabled-server",
|
||||
scope: "user" as const,
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
await McpDisableCommand.handler(args)
|
||||
|
||||
const config = await Config.global()
|
||||
expect(config.mcp?.["user-enabled-server"].enabled).toBe(false)
|
||||
})
|
||||
|
||||
test("enable non-existent MCP server shows error", async () => {
|
||||
const stderr = captureStderr()
|
||||
|
||||
const args = {
|
||||
name: "non-existent",
|
||||
scope: "project" as const,
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
await McpEnableCommand.handler(args)
|
||||
|
||||
expect(stderr.getOutput()).toContain(
|
||||
'MCP server "non-existent" not found in project config',
|
||||
)
|
||||
|
||||
stderr.restore()
|
||||
})
|
||||
|
||||
test("disable non-existent MCP server shows error", async () => {
|
||||
const stderr = captureStderr()
|
||||
|
||||
const args = {
|
||||
name: "non-existent",
|
||||
scope: "user" as const,
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
await McpDisableCommand.handler(args)
|
||||
|
||||
expect(stderr.getOutput()).toContain(
|
||||
'MCP server "non-existent" not found in user config',
|
||||
)
|
||||
|
||||
stderr.restore()
|
||||
})
|
||||
|
||||
test("list shows disabled status for MCP servers", async () => {
|
||||
// Add servers with different enabled states
|
||||
await Bun.write(
|
||||
testConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcp: {
|
||||
"enabled-server": {
|
||||
type: "local",
|
||||
command: ["node", "enabled.js"],
|
||||
enabled: true,
|
||||
},
|
||||
"disabled-server": {
|
||||
type: "local",
|
||||
command: ["node", "disabled.js"],
|
||||
enabled: false,
|
||||
},
|
||||
"default-server": {
|
||||
type: "local",
|
||||
command: ["node", "default.js"],
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
const stderr = captureStderr()
|
||||
|
||||
await McpListCommand.handler({ $0: "opencode", _: [] })
|
||||
|
||||
const output = stderr.getOutput()
|
||||
expect(output).toContain("enabled-server (local)")
|
||||
expect(output).toContain("disabled-server (local) (disabled)")
|
||||
expect(output).toContain("default-server (local)")
|
||||
stderr.restore()
|
||||
})
|
||||
|
||||
test("get shows enabled status for MCP servers", async () => {
|
||||
// Add a disabled server to project config
|
||||
await Bun.write(
|
||||
testProjectConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcp: {
|
||||
"status-server": {
|
||||
type: "local",
|
||||
command: ["node", "server.js"],
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
const stderr = captureStderr()
|
||||
|
||||
const args = {
|
||||
name: "status-server",
|
||||
_: [],
|
||||
$0: "opencode",
|
||||
}
|
||||
await McpGetCommand.handler(args)
|
||||
|
||||
const output = stderr.getOutput()
|
||||
expect(output).toContain("Enabled: false")
|
||||
stderr.restore()
|
||||
})
|
||||
})
|
|
@ -118,6 +118,124 @@ opencode upgrade v0.1.48
|
|||
|
||||
---
|
||||
|
||||
### mcp
|
||||
|
||||
Manage Model Context Protocol (MCP) servers for enhanced AI capabilities.
|
||||
|
||||
```bash
|
||||
opencode mcp [command]
|
||||
```
|
||||
|
||||
#### add
|
||||
|
||||
Add a new MCP server configuration.
|
||||
|
||||
```bash
|
||||
opencode mcp add <name> [command]
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Add a local MCP server using stdio transport
|
||||
opencode mcp add file-server npx @modelcontextprotocol/server-filesystem /path/to/files
|
||||
|
||||
# Add a remote MCP server using SSE transport
|
||||
opencode mcp add weather-server --transport sse --url https://api.weather.com/mcp
|
||||
|
||||
# Add server to global config (default is project scope)
|
||||
opencode mcp add git-server git-mcp-server --scope user
|
||||
|
||||
# Add server with environment variables
|
||||
opencode mcp add db-server db-mcp-server -e DB_HOST=localhost -e DB_PORT=5432
|
||||
|
||||
# Add remote server with custom headers
|
||||
opencode mcp add api-server --transport sse --url https://api.example.com/mcp -H "Authorization: Bearer token"
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
|
||||
| Flag | Short | Description |
|
||||
| ----------------- | ----- | --------------------- |
|
||||
| `--scope` | `-s` | Configuration scope: `user` (global) or `project` (default) |
|
||||
| `--transport` | `-t` | Transport type: `stdio` (default) or `sse` |
|
||||
| `--url` | `-u` | Server URL (required for SSE transport) |
|
||||
| `--env` | `-e` | Environment variable (can be used multiple times) |
|
||||
| `--header` | `-H` | HTTP header for SSE transport (can be used multiple times) |
|
||||
|
||||
#### remove
|
||||
|
||||
Remove an MCP server configuration.
|
||||
|
||||
```bash
|
||||
opencode mcp remove <name>
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Remove server from project config
|
||||
opencode mcp remove file-server
|
||||
|
||||
# Remove server from global config
|
||||
opencode mcp remove weather-server --scope user
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
|
||||
| Flag | Short | Description |
|
||||
| ----------------- | ----- | --------------------- |
|
||||
| `--scope` | `-s` | Configuration scope: `user` (global) or `project` (default) |
|
||||
|
||||
#### list
|
||||
|
||||
List all configured MCP servers from both global and project configurations.
|
||||
|
||||
```bash
|
||||
opencode mcp list
|
||||
```
|
||||
|
||||
Shows servers from both scopes with their transport type and configuration details.
|
||||
|
||||
#### get
|
||||
|
||||
Get details for a specific MCP server. Checks both global and project configurations, with project taking priority.
|
||||
|
||||
```bash
|
||||
opencode mcp get <name>
|
||||
```
|
||||
|
||||
Displays the server configuration including transport type, command/URL, environment variables, and headers.
|
||||
|
||||
#### add-json
|
||||
|
||||
Add an MCP server using JSON configuration. Automatically determines if the server is local or remote based on the provided configuration.
|
||||
|
||||
```bash
|
||||
opencode mcp add-json <name> <json>
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Add local server via JSON
|
||||
opencode mcp add-json files '{"command": "npx @modelcontextprotocol/server-filesystem", "args": ["/path"]}'
|
||||
|
||||
# Add remote server via JSON
|
||||
opencode mcp add-json weather '{"url": "https://api.weather.com/mcp", "headers": {"Auth": "token"}}'
|
||||
|
||||
# Add to global scope
|
||||
opencode mcp add-json api-server '{"url": "https://api.com/mcp"}' --scope user
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
|
||||
| Flag | Short | Description |
|
||||
| ----------------- | ----- | --------------------- |
|
||||
| `--scope` | `-s` | Configuration scope: `user` (global) or `project` (default) |
|
||||
|
||||
---
|
||||
|
||||
## Flags
|
||||
|
||||
The opencode CLI takes the following flags.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue