feat(core): split MCP timeout configuration (#33977)
Co-authored-by: Test <test@opencode.test>
This commit is contained in:
co-authored by
Test
parent
17166b271f
commit
f428755851
@@ -3,6 +3,15 @@ export * as ConfigMCP from "./mcp"
|
|||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { PositiveInt } from "../schema"
|
import { PositiveInt } from "../schema"
|
||||||
|
|
||||||
|
export class Timeout extends Schema.Class<Timeout>("ConfigV2.MCP.Timeout")({
|
||||||
|
startup: PositiveInt.pipe(Schema.optional).annotate({
|
||||||
|
description: "Maximum time in milliseconds to establish and initialize the MCP server.",
|
||||||
|
}),
|
||||||
|
request: PositiveInt.pipe(Schema.optional).annotate({
|
||||||
|
description: "Maximum time in milliseconds to wait for each MCP request after initialization.",
|
||||||
|
}),
|
||||||
|
}) {}
|
||||||
|
|
||||||
export class Local extends Schema.Class<Local>("ConfigV2.MCP.Local")({
|
export class Local extends Schema.Class<Local>("ConfigV2.MCP.Local")({
|
||||||
type: Schema.Literal("local"),
|
type: Schema.Literal("local"),
|
||||||
command: Schema.String.pipe(Schema.Array),
|
command: Schema.String.pipe(Schema.Array),
|
||||||
@@ -11,7 +20,7 @@ export class Local extends Schema.Class<Local>("ConfigV2.MCP.Local")({
|
|||||||
}),
|
}),
|
||||||
environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||||
timeout: PositiveInt.pipe(Schema.optional),
|
timeout: Timeout.pipe(Schema.optional),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export class OAuth extends Schema.Class<OAuth>("ConfigV2.MCP.OAuth")({
|
export class OAuth extends Schema.Class<OAuth>("ConfigV2.MCP.OAuth")({
|
||||||
@@ -28,12 +37,12 @@ export class Remote extends Schema.Class<Remote>("ConfigV2.MCP.Remote")({
|
|||||||
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||||
oauth: Schema.Union([OAuth, Schema.Literal(false)]).pipe(Schema.optional),
|
oauth: Schema.Union([OAuth, Schema.Literal(false)]).pipe(Schema.optional),
|
||||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||||
timeout: PositiveInt.pipe(Schema.optional),
|
timeout: Timeout.pipe(Schema.optional),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export const Server = Schema.Union([Local, Remote]).pipe(Schema.toTaggedUnion("type"))
|
export const Server = Schema.Union([Local, Remote]).pipe(Schema.toTaggedUnion("type"))
|
||||||
|
|
||||||
export class Info extends Schema.Class<Info>("ConfigV2.MCP")({
|
export class Info extends Schema.Class<Info>("ConfigV2.MCP")({
|
||||||
timeout: PositiveInt.pipe(Schema.optional),
|
timeout: Timeout.pipe(Schema.optional),
|
||||||
servers: Schema.Record(Schema.String, Server).pipe(Schema.optional),
|
servers: Schema.Record(Schema.String, Server).pipe(Schema.optional),
|
||||||
}) {}
|
}) {}
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ function mcp(info: typeof ConfigV1.Info.Type) {
|
|||||||
)
|
)
|
||||||
const timeout = info.experimental?.mcp_timeout
|
const timeout = info.experimental?.mcp_timeout
|
||||||
if (!timeout && !Object.keys(servers).length) return undefined
|
if (!timeout && !Object.keys(servers).length) return undefined
|
||||||
return { timeout, servers }
|
return { timeout: timeout === undefined ? undefined : { request: timeout }, servers }
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateMcp(info: ConfigMCPV1.Info) {
|
function migrateMcp(info: ConfigMCPV1.Info) {
|
||||||
@@ -144,7 +144,7 @@ function migrateMcp(info: ConfigMCPV1.Info) {
|
|||||||
cwd: info.cwd,
|
cwd: info.cwd,
|
||||||
environment: info.environment,
|
environment: info.environment,
|
||||||
disabled,
|
disabled,
|
||||||
timeout: info.timeout,
|
timeout: info.timeout === undefined ? undefined : { request: info.timeout },
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
type: info.type,
|
type: info.type,
|
||||||
@@ -158,7 +158,7 @@ function migrateMcp(info: ConfigMCPV1.Info) {
|
|||||||
redirect_uri: info.oauth.redirectUri,
|
redirect_uri: info.oauth.redirectUri,
|
||||||
},
|
},
|
||||||
disabled,
|
disabled,
|
||||||
timeout: info.timeout,
|
timeout: info.timeout === undefined ? undefined : { request: info.timeout },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -298,14 +298,14 @@ describe("Config", () => {
|
|||||||
},
|
},
|
||||||
tool_output: { max_lines: 1000, max_bytes: 32768 },
|
tool_output: { max_lines: 1000, max_bytes: 32768 },
|
||||||
mcp: {
|
mcp: {
|
||||||
timeout: 5000,
|
timeout: { startup: 5000, request: 60000 },
|
||||||
servers: {
|
servers: {
|
||||||
local: {
|
local: {
|
||||||
type: "local",
|
type: "local",
|
||||||
command: ["node", "./mcp/server.js"],
|
command: ["node", "./mcp/server.js"],
|
||||||
environment: { API_KEY: "secret" },
|
environment: { API_KEY: "secret" },
|
||||||
disabled: false,
|
disabled: false,
|
||||||
timeout: 10000,
|
timeout: { request: 10000 },
|
||||||
},
|
},
|
||||||
remote: {
|
remote: {
|
||||||
type: "remote",
|
type: "remote",
|
||||||
@@ -313,6 +313,7 @@ describe("Config", () => {
|
|||||||
headers: { Authorization: "Bearer token" },
|
headers: { Authorization: "Bearer token" },
|
||||||
oauth: { client_id: "client", scope: "read write", callback_port: 19876 },
|
oauth: { client_id: "client", scope: "read write", callback_port: 19876 },
|
||||||
disabled: true,
|
disabled: true,
|
||||||
|
timeout: { startup: 15000 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -383,14 +384,14 @@ describe("Config", () => {
|
|||||||
})
|
})
|
||||||
expect(documents[0]?.info.tool_output).toEqual({ max_lines: 1000, max_bytes: 32768 })
|
expect(documents[0]?.info.tool_output).toEqual({ max_lines: 1000, max_bytes: 32768 })
|
||||||
expect(documents[0]?.info.mcp).toEqual({
|
expect(documents[0]?.info.mcp).toEqual({
|
||||||
timeout: 5000,
|
timeout: { startup: 5000, request: 60000 },
|
||||||
servers: {
|
servers: {
|
||||||
local: {
|
local: {
|
||||||
type: "local",
|
type: "local",
|
||||||
command: ["node", "./mcp/server.js"],
|
command: ["node", "./mcp/server.js"],
|
||||||
environment: { API_KEY: "secret" },
|
environment: { API_KEY: "secret" },
|
||||||
disabled: false,
|
disabled: false,
|
||||||
timeout: 10000,
|
timeout: { request: 10000 },
|
||||||
},
|
},
|
||||||
remote: {
|
remote: {
|
||||||
type: "remote",
|
type: "remote",
|
||||||
@@ -398,6 +399,7 @@ describe("Config", () => {
|
|||||||
headers: { Authorization: "Bearer token" },
|
headers: { Authorization: "Bearer token" },
|
||||||
oauth: { client_id: "client", scope: "read write", callback_port: 19876 },
|
oauth: { client_id: "client", scope: "read write", callback_port: 19876 },
|
||||||
disabled: true,
|
disabled: true,
|
||||||
|
timeout: { startup: 15000 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -541,11 +543,12 @@ describe("Config", () => {
|
|||||||
compaction: { auto: true, tail_turns: 3, preserve_recent_tokens: 2000, reserved: 10000 },
|
compaction: { auto: true, tail_turns: 3, preserve_recent_tokens: 2000, reserved: 10000 },
|
||||||
experimental: { mcp_timeout: 5000 },
|
experimental: { mcp_timeout: 5000 },
|
||||||
mcp: {
|
mcp: {
|
||||||
local: { type: "local", command: ["node", "server.js"], enabled: false },
|
local: { type: "local", command: ["node", "server.js"], enabled: false, timeout: 10000 },
|
||||||
remote: {
|
remote: {
|
||||||
type: "remote",
|
type: "remote",
|
||||||
url: "https://mcp.example.com",
|
url: "https://mcp.example.com",
|
||||||
oauth: { clientId: "client", callbackPort: 19876 },
|
oauth: { clientId: "client", callbackPort: 19876 },
|
||||||
|
timeout: 20000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -623,13 +626,19 @@ describe("Config", () => {
|
|||||||
buffer: 10000,
|
buffer: 10000,
|
||||||
})
|
})
|
||||||
expect(documents[0]?.info.mcp).toMatchObject({
|
expect(documents[0]?.info.mcp).toMatchObject({
|
||||||
timeout: 5000,
|
timeout: { request: 5000 },
|
||||||
servers: {
|
servers: {
|
||||||
local: { type: "local", command: ["node", "server.js"], disabled: true },
|
local: {
|
||||||
|
type: "local",
|
||||||
|
command: ["node", "server.js"],
|
||||||
|
disabled: true,
|
||||||
|
timeout: { request: 10000 },
|
||||||
|
},
|
||||||
remote: {
|
remote: {
|
||||||
type: "remote",
|
type: "remote",
|
||||||
url: "https://mcp.example.com",
|
url: "https://mcp.example.com",
|
||||||
oauth: { client_id: "client", callback_port: 19876 },
|
oauth: { client_id: "client", callback_port: 19876 },
|
||||||
|
timeout: { request: 20000 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
+9
-6
@@ -305,22 +305,24 @@ Rename legacy `permission` to `permissions` and expose the normalized ordered ru
|
|||||||
External protocol and server integration configuration.
|
External protocol and server integration configuration.
|
||||||
|
|
||||||
| Field | Current Purpose | Status | Notes |
|
| Field | Current Purpose | Status | Notes |
|
||||||
| ----- | ------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ----- | ------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `mcp` | MCP server definitions and enablement | redesign | Keep opencode's explicit local/remote server entry format, nested under `mcp.servers`; use `disabled` for inactive entries and move timeout here. |
|
| `mcp` | MCP server definitions and enablement | redesign | Keep opencode's explicit local/remote server entry format, nested under `mcp.servers`; use `disabled` for inactive entries and move timeout defaults here. |
|
||||||
|
|
||||||
Keep the opencode MCP server entry format instead of adopting the common `mcpServers` copy/paste shape. Local servers remain explicit `type: "local"` entries with command arrays and `environment`; remote servers remain explicit `type: "remote"` entries with `url`, `headers`, and optional `oauth`. Nest the server map under `mcp.servers` so protocol-wide settings such as default timeout can live under the same subsystem.
|
Keep the opencode MCP server entry format instead of adopting the common `mcpServers` copy/paste shape. Local servers remain explicit `type: "local"` entries with command arrays and `environment`; remote servers remain explicit `type: "remote"` entries with `url`, `headers`, and optional `oauth`. Nest the server map under `mcp.servers` so protocol-wide settings such as timeout defaults can live under the same subsystem.
|
||||||
|
|
||||||
|
MCP timeouts have separate startup and request budgets, expressed in milliseconds. `startup` covers establishing the transport and completing MCP initialization. `request` applies independently to each post-initialization MCP request. A server may override either default without repeating the other.
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"timeout": 5000,
|
"timeout": { "startup": 30000, "request": 300000 },
|
||||||
"servers": {
|
"servers": {
|
||||||
"github": {
|
"github": {
|
||||||
"type": "local",
|
"type": "local",
|
||||||
"command": ["npx", "-y", "@github/github-mcp-server"],
|
"command": ["npx", "-y", "@github/github-mcp-server"],
|
||||||
"environment": { "GITHUB_TOKEN": "{env:GITHUB_TOKEN}" },
|
"environment": { "GITHUB_TOKEN": "{env:GITHUB_TOKEN}" },
|
||||||
"disabled": false,
|
"disabled": false,
|
||||||
"timeout": 10000,
|
"timeout": { "startup": 60000 },
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"type": "remote",
|
"type": "remote",
|
||||||
@@ -334,6 +336,7 @@ Keep the opencode MCP server entry format instead of adopting the common `mcpSer
|
|||||||
"redirect_uri": "http://127.0.0.1:19876/mcp/oauth/callback",
|
"redirect_uri": "http://127.0.0.1:19876/mcp/oauth/callback",
|
||||||
},
|
},
|
||||||
"disabled": false,
|
"disabled": false,
|
||||||
|
"timeout": { "request": 600000 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -375,7 +378,7 @@ Fields that should not be ported by inertia; each needs an explicit justificatio
|
|||||||
| `experimental.openTelemetry` | Enable AI SDK telemetry spans | remove | Do not port; observability is process-level and should use standard OpenTelemetry environment or declarative configuration. |
|
| `experimental.openTelemetry` | Enable AI SDK telemetry spans | remove | Do not port; observability is process-level and should use standard OpenTelemetry environment or declarative configuration. |
|
||||||
| `experimental.primary_tools` | Restrict tools to primary agents | remove | Do not port obsolete gating; agent tool access is configured through permissions. |
|
| `experimental.primary_tools` | Restrict tools to primary agents | remove | Do not port obsolete gating; agent tool access is configured through permissions. |
|
||||||
| `experimental.continue_loop_on_deny` | Continue loop after denied tool call | remove | Do not port legacy denied-tool loop behavior. |
|
| `experimental.continue_loop_on_deny` | Continue loop after denied tool call | remove | Do not port legacy denied-tool loop behavior. |
|
||||||
| `experimental.mcp_timeout` | MCP request timeout | redesign | Move to `mcp.timeout` for the default and `mcp.servers.<name>.timeout` for per-server overrides. |
|
| `experimental.mcp_timeout` | MCP request timeout | redesign | Move to `mcp.timeout.request` for the default and `mcp.servers.<name>.timeout.request` for per-server overrides. |
|
||||||
|
|
||||||
## Review Order
|
## Review Order
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user