summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMatt Gillard <[email protected]>2025-10-30 17:25:24 +1100
committerGitHub <[email protected]>2025-10-30 01:25:24 -0500
commit3ae75d70310347023f16a7d25156824e630fd9c0 (patch)
tree6cd360a7d5a1f61b39707ddce3254f33797b22ea
parent4b5e44796156a39f386b26b88f16743011b66323 (diff)
downloadopencode-3ae75d70310347023f16a7d25156824e630fd9c0.tar.gz
opencode-3ae75d70310347023f16a7d25156824e630fd9c0.zip
add optional timeout field to mcp config to allow users to use responding servers (#3558)
Co-authored-by: Matt Gillard <[email protected]> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: rekram1-node <[email protected]>
-rw-r--r--packages/opencode/src/config/config.ts234
-rw-r--r--packages/opencode/src/mcp/index.ts2
-rw-r--r--packages/web/src/content/docs/mcp-servers.mdx44
3 files changed, 220 insertions, 60 deletions
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 83c518a68..12a5c1624 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -12,7 +12,11 @@ import { NamedError } from "../util/error"
import matter from "gray-matter"
import { Flag } from "../flag/flag"
import { Auth } from "../auth"
-import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
+import {
+ type ParseError as JsoncParseError,
+ parse as parseJsonc,
+ printParseErrorCode,
+} from "jsonc-parser"
import { Instance } from "../project/instance"
import { LSPServer } from "../lsp/server"
import { BunProc } from "@/bun"
@@ -46,7 +50,10 @@ export namespace Config {
if (value.type === "wellknown") {
process.env[value.key] = value.token
const wellknown = await fetch(`${key}/.well-known/opencode`).then((x) => x.json())
- result = mergeDeep(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
+ result = mergeDeep(
+ result,
+ await load(JSON.stringify(wellknown.config ?? {}), process.cwd()),
+ )
}
}
@@ -57,7 +64,11 @@ export namespace Config {
const directories = [
Global.Path.config,
...(await Array.fromAsync(
- Filesystem.up({ targets: [".opencode"], start: Instance.directory, stop: Instance.worktree }),
+ Filesystem.up({
+ targets: [".opencode"],
+ start: Instance.directory,
+ stop: Instance.worktree,
+ }),
)),
]
@@ -153,10 +164,18 @@ export namespace Config {
const gitignore = path.join(dir, ".gitignore")
const hasGitIgnore = await Bun.file(gitignore).exists()
- if (!hasGitIgnore) await Bun.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))
+ if (!hasGitIgnore)
+ await Bun.write(
+ gitignore,
+ ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"),
+ )
await BunProc.run(
- ["add", "@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION), "--exact"],
+ [
+ "add",
+ "@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION),
+ "--exact",
+ ],
{
cwd: dir,
},
@@ -166,7 +185,12 @@ export namespace Config {
const COMMAND_GLOB = new Bun.Glob("command/**/*.md")
async function loadCommand(dir: string) {
const result: Record<string, Command> = {}
- for await (const item of COMMAND_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) {
+ for await (const item of COMMAND_GLOB.scan({
+ absolute: true,
+ followSymlinks: true,
+ dot: true,
+ cwd: dir,
+ })) {
const content = await Bun.file(item).text()
const md = matter(content)
if (!md.data) continue
@@ -201,7 +225,12 @@ export namespace Config {
async function loadAgent(dir: string) {
const result: Record<string, Agent> = {}
- for await (const item of AGENT_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) {
+ for await (const item of AGENT_GLOB.scan({
+ absolute: true,
+ followSymlinks: true,
+ dot: true,
+ cwd: dir,
+ })) {
const content = await Bun.file(item).text()
const md = matter(content)
if (!md.data) continue
@@ -239,7 +268,12 @@ export namespace Config {
const MODE_GLOB = new Bun.Glob("mode/*.md")
async function loadMode(dir: string) {
const result: Record<string, Agent> = {}
- for await (const item of MODE_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) {
+ for await (const item of MODE_GLOB.scan({
+ absolute: true,
+ followSymlinks: true,
+ dot: true,
+ cwd: dir,
+ })) {
const content = await Bun.file(item).text()
const md = matter(content)
if (!md.data) continue
@@ -265,7 +299,12 @@ export namespace Config {
async function loadPlugin(dir: string) {
const plugins: string[] = []
- for await (const item of PLUGIN_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) {
+ for await (const item of PLUGIN_GLOB.scan({
+ absolute: true,
+ followSymlinks: true,
+ dot: true,
+ cwd: dir,
+ })) {
plugins.push("file://" + item)
}
return plugins
@@ -280,6 +319,14 @@ export namespace Config {
.optional()
.describe("Environment variables to set when running the MCP server"),
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
+ timeout: z
+ .number()
+ .int()
+ .positive()
+ .optional()
+ .describe(
+ "Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.",
+ ),
})
.strict()
.meta({
@@ -291,7 +338,18 @@ export namespace Config {
type: z.literal("remote").describe("Type of MCP server connection"),
url: z.string().describe("URL of the remote MCP server"),
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
- headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
+ headers: z
+ .record(z.string(), z.string())
+ .optional()
+ .describe("Headers to send with the request"),
+ timeout: z
+ .number()
+ .int()
+ .positive()
+ .optional()
+ .describe(
+ "Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.",
+ ),
})
.strict()
.meta({
@@ -339,72 +397,148 @@ export namespace Config {
export const Keybinds = z
.object({
- leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
+ leader: z
+ .string()
+ .optional()
+ .default("ctrl+x")
+ .describe("Leader key for keybind combinations"),
app_help: z.string().optional().default("<leader>h").describe("Show help dialog"),
app_exit: z.string().optional().default("ctrl+c,<leader>q").describe("Exit the application"),
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
project_init: z.string().optional().default("<leader>i").describe("Create/update AGENTS.md"),
tool_details: z.string().optional().default("<leader>d").describe("Toggle tool details"),
- thinking_blocks: z.string().optional().default("<leader>b").describe("Toggle thinking blocks"),
- session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
+ thinking_blocks: z
+ .string()
+ .optional()
+ .default("<leader>b")
+ .describe("Toggle thinking blocks"),
+ session_export: z
+ .string()
+ .optional()
+ .default("<leader>x")
+ .describe("Export session to editor"),
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
- session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
+ session_timeline: z
+ .string()
+ .optional()
+ .default("<leader>g")
+ .describe("Show session timeline"),
session_share: z.string().optional().default("<leader>s").describe("Share current session"),
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
session_interrupt: z.string().optional().default("esc").describe("Interrupt current session"),
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
- session_child_cycle: z.string().optional().default("ctrl+right").describe("Cycle to next child session"),
+ session_child_cycle: z
+ .string()
+ .optional()
+ .default("ctrl+right")
+ .describe("Cycle to next child session"),
session_child_cycle_reverse: z
.string()
.optional()
.default("ctrl+left")
.describe("Cycle to previous child session"),
- messages_page_up: z.string().optional().default("pgup").describe("Scroll messages up by one page"),
- messages_page_down: z.string().optional().default("pgdown").describe("Scroll messages down by one page"),
- messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
+ messages_page_up: z
+ .string()
+ .optional()
+ .default("pgup")
+ .describe("Scroll messages up by one page"),
+ messages_page_down: z
+ .string()
+ .optional()
+ .default("pgdown")
+ .describe("Scroll messages down by one page"),
+ messages_half_page_up: z
+ .string()
+ .optional()
+ .default("ctrl+alt+u")
+ .describe("Scroll messages up by half page"),
messages_half_page_down: z
.string()
.optional()
.default("ctrl+alt+d")
.describe("Scroll messages down by half page"),
messages_first: z.string().optional().default("ctrl+g").describe("Navigate to first message"),
- messages_last: z.string().optional().default("ctrl+alt+g").describe("Navigate to last message"),
+ messages_last: z
+ .string()
+ .optional()
+ .default("ctrl+alt+g")
+ .describe("Navigate to last message"),
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
model_list: z.string().optional().default("<leader>m").describe("List available models"),
model_cycle_recent: z.string().optional().default("f2").describe("Next recent model"),
- model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recent model"),
+ model_cycle_recent_reverse: z
+ .string()
+ .optional()
+ .default("shift+f2")
+ .describe("Previous recent model"),
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
input_submit: z.string().optional().default("enter").describe("Submit input"),
- input_newline: z.string().optional().default("shift+enter,ctrl+j").describe("Insert newline in input"),
+ input_newline: z
+ .string()
+ .optional()
+ .default("shift+enter,ctrl+j")
+ .describe("Insert newline in input"),
// Deprecated commands
- switch_mode: z.string().optional().default("none").describe("@deprecated use agent_cycle. Next mode"),
+ switch_mode: z
+ .string()
+ .optional()
+ .default("none")
+ .describe("@deprecated use agent_cycle. Next mode"),
switch_mode_reverse: z
.string()
.optional()
.default("none")
.describe("@deprecated use agent_cycle_reverse. Previous mode"),
- switch_agent: z.string().optional().default("tab").describe("@deprecated use agent_cycle. Next agent"),
+ switch_agent: z
+ .string()
+ .optional()
+ .default("tab")
+ .describe("@deprecated use agent_cycle. Next agent"),
switch_agent_reverse: z
.string()
.optional()
.default("shift+tab")
.describe("@deprecated use agent_cycle_reverse. Previous agent"),
- file_list: z.string().optional().default("none").describe("@deprecated Currently not available. List files"),
+ file_list: z
+ .string()
+ .optional()
+ .default("none")
+ .describe("@deprecated Currently not available. List files"),
file_close: z.string().optional().default("none").describe("@deprecated Close file"),
file_search: z.string().optional().default("none").describe("@deprecated Search file"),
- file_diff_toggle: z.string().optional().default("none").describe("@deprecated Split/unified diff"),
- messages_previous: z.string().optional().default("none").describe("@deprecated Navigate to previous message"),
- messages_next: z.string().optional().default("none").describe("@deprecated Navigate to next message"),
- messages_layout_toggle: z.string().optional().default("none").describe("@deprecated Toggle layout"),
- messages_revert: z.string().optional().default("none").describe("@deprecated use messages_undo. Revert message"),
+ file_diff_toggle: z
+ .string()
+ .optional()
+ .default("none")
+ .describe("@deprecated Split/unified diff"),
+ messages_previous: z
+ .string()
+ .optional()
+ .default("none")
+ .describe("@deprecated Navigate to previous message"),
+ messages_next: z
+ .string()
+ .optional()
+ .default("none")
+ .describe("@deprecated Navigate to next message"),
+ messages_layout_toggle: z
+ .string()
+ .optional()
+ .default("none")
+ .describe("@deprecated Toggle layout"),
+ messages_revert: z
+ .string()
+ .optional()
+ .default("none")
+ .describe("@deprecated use messages_undo. Revert message"),
})
.strict()
.meta({
@@ -446,13 +580,23 @@ export namespace Config {
autoshare: z
.boolean()
.optional()
- .describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
+ .describe(
+ "@deprecated Use 'share' field instead. Share newly created sessions automatically",
+ ),
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
- disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
- model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
+ disabled_providers: z
+ .array(z.string())
+ .optional()
+ .describe("Disable providers that are loaded automatically"),
+ model: z
+ .string()
+ .describe("Model to use in the format of provider/model, eg anthropic/claude-2")
+ .optional(),
small_model: z
.string()
- .describe("Small model to use for tasks like title generation in the format of provider/model")
+ .describe(
+ "Small model to use for tasks like title generation in the format of provider/model",
+ )
.optional(),
username: z
.string()
@@ -508,7 +652,10 @@ export namespace Config {
)
.optional()
.describe("Custom provider configurations and model overrides"),
- mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
+ mcp: z
+ .record(z.string(), Mcp)
+ .optional()
+ .describe("MCP (Model Context Protocol) server configurations"),
formatter: z
.record(
z.string(),
@@ -552,7 +699,10 @@ export namespace Config {
error: "For custom LSP servers, 'extensions' array is required.",
},
),
- instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
+ instructions: z
+ .array(z.string())
+ .optional()
+ .describe("Additional instruction files or patterns to include"),
layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
permission: z
.object({
@@ -586,7 +736,10 @@ export namespace Config {
.optional(),
})
.optional(),
- chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
+ chatMaxRetries: z
+ .number()
+ .optional()
+ .describe("Number of retries for chat completions on failure"),
disable_paste_summary: z.boolean().optional(),
})
.optional(),
@@ -616,7 +769,10 @@ export namespace Config {
if (provider && model) result.model = `${provider}/${model}`
result["$schema"] = "https://opencode.ai/config.json"
result = mergeDeep(result, rest)
- await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
+ await Bun.write(
+ path.join(Global.Path.config, "config.json"),
+ JSON.stringify(result, null, 2),
+ )
await fs.unlink(path.join(Global.Path.config, "config"))
})
.catch(() => {})
@@ -655,7 +811,9 @@ export namespace Config {
if (filePath.startsWith("~/")) {
filePath = path.join(os.homedir(), filePath.slice(2))
}
- const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
+ const resolvedPath = path.isAbsolute(filePath)
+ ? filePath
+ : path.resolve(configDir, filePath)
const fileContent = (
await Bun.file(resolvedPath)
.text()
diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts
index 988426b13..0df6a5a72 100644
--- a/packages/opencode/src/mcp/index.ts
+++ b/packages/opencode/src/mcp/index.ts
@@ -167,7 +167,7 @@ export namespace MCP {
return
}
- const result = await withTimeout(mcpClient.tools(), 5000).catch(() => {})
+ const result = await withTimeout(mcpClient.tools(), mcp.timeout ?? 5000).catch(() => { })
if (!result) {
log.warn("mcp client verification failed, dropping client", { name })
return
diff --git a/packages/web/src/content/docs/mcp-servers.mdx b/packages/web/src/content/docs/mcp-servers.mdx
index ab87449e0..132537b05 100644
--- a/packages/web/src/content/docs/mcp-servers.mdx
+++ b/packages/web/src/content/docs/mcp-servers.mdx
@@ -45,12 +45,12 @@ with a unique name. You can refer to that MCP by name when prompting the LLM.
"mcp": {
"name-of-mcp-server": {
// ...
- "enabled": true
+ "enabled": true,
},
"name-of-other-mcp-server": {
// ...
- }
- }
+ },
+ },
}
```
@@ -72,10 +72,10 @@ Add local MCP servers using `type` to `"local"` within the MCP object.
"command": ["npx", "-y", "my-mcp-command"],
"enabled": true,
"environment": {
- "MY_ENV_VAR": "my_env_var_value"
- }
- }
- }
+ "MY_ENV_VAR": "my_env_var_value",
+ },
+ },
+ },
}
```
@@ -91,8 +91,8 @@ For example, here's how I can add the test
"mcp_everything": {
"type": "local",
"command": ["npx", "-y", "@modelcontextprotocol/server-everything"],
- }
- }
+ },
+ },
}
```
@@ -106,12 +106,13 @@ use the mcp_everything tool to add the number 3 and 4
Here are all the options for configuring a local MCP server.
-| Option | Type | Required | Description |
-| ------------- | ------- | -------- | ----------------------------------------------------- |
-| `type` | String | Y | Type of MCP server connection, must be `"local"`. |
-| `command` | Array | Y | Command and arguments to run the MCP server. |
-| `environment` | Object | | Environment variables to set when running the server. |
-| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
+| Option | Type | Required | Description |
+| ------------- | ------- | -------- | ----------------------------------------------------------------------------------- |
+| `type` | String | Y | Type of MCP server connection, must be `"local"`. |
+| `command` | Array | Y | Command and arguments to run the MCP server. |
+| `environment` | Object | | Environment variables to set when running the server. |
+| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
+| `timeout` | Number | | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds). |
---
@@ -139,12 +140,13 @@ Here the `url` is the URL of the remote MCP server and with the `headers` option
#### Options
-| Option | Type | Required | Description |
-| --------- | ------- | -------- | -------------------------------------------------- |
-| `type` | String | Y | Type of MCP server connection, must be `"remote"`. |
-| `url` | String | Y | URL of the remote MCP server. |
-| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
-| `headers` | Object | | Headers to send with the request. |
+| Option | Type | Required | Description |
+| --------- | ------- | -------- | ----------------------------------------------------------------------------------- |
+| `type` | String | Y | Type of MCP server connection, must be `"remote"`. |
+| `url` | String | Y | URL of the remote MCP server. |
+| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
+| `headers` | Object | | Headers to send with the request. |
+| `timeout` | Number | | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds). |
---