diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index d8c0e93009..c31348950e 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -11,9 +11,11 @@ import { Policy } from "./policy" import { AbsolutePath } from "./schema" import { ConfigAgent } from "./config/agent" import { ConfigAttachments } from "./config/attachments" +import { ConfigCompaction } from "./config/compaction" import { ConfigExperimental } from "./config/experimental" import { ConfigFormatter } from "./config/formatter" import { ConfigLSP } from "./config/lsp" +import { ConfigMCP } from "./config/mcp" import { ConfigPlugin } from "./config/plugin" import { ConfigProvider } from "./config/provider" import { ConfigReference } from "./config/reference" @@ -68,6 +70,12 @@ export class Info extends Schema.Class("Config.Info")({ tool_output: ConfigToolOutput.Info.pipe(Schema.optional).annotate({ description: "Tool output truncation thresholds", }), + mcp: ConfigMCP.Info.pipe(Schema.optional).annotate({ + description: "MCP server configuration", + }), + compaction: ConfigCompaction.Info.pipe(Schema.optional).annotate({ + description: "Conversation compaction behavior", + }), skills: Schema.String.pipe(Schema.Array, Schema.optional).annotate({ description: "Additional paths or URLs to discover skills from", }), diff --git a/packages/core/src/config/compaction.ts b/packages/core/src/config/compaction.ts new file mode 100644 index 0000000000..eef67ee26a --- /dev/null +++ b/packages/core/src/config/compaction.ts @@ -0,0 +1,16 @@ +export * as ConfigCompaction from "./compaction" + +import { Schema } from "effect" +import { NonNegativeInt } from "../schema" + +export class Keep extends Schema.Class("ConfigV2.Compaction.Keep")({ + turns: NonNegativeInt.pipe(Schema.optional), + tokens: NonNegativeInt.pipe(Schema.optional), +}) {} + +export class Info extends Schema.Class("ConfigV2.Compaction")({ + auto: Schema.Boolean.pipe(Schema.optional), + prune: Schema.Boolean.pipe(Schema.optional), + keep: Keep.pipe(Schema.optional), + buffer: NonNegativeInt.pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/mcp.ts b/packages/core/src/config/mcp.ts new file mode 100644 index 0000000000..fce853815b --- /dev/null +++ b/packages/core/src/config/mcp.ts @@ -0,0 +1,36 @@ +export * as ConfigMCP from "./mcp" + +import { Schema } from "effect" +import { PositiveInt } from "../schema" + +export class Local extends Schema.Class("ConfigV2.MCP.Local")({ + type: Schema.Literal("local"), + command: Schema.String.pipe(Schema.Array), + environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + disabled: Schema.Boolean.pipe(Schema.optional), + timeout: PositiveInt.pipe(Schema.optional), +}) {} + +export class OAuth extends Schema.Class("ConfigV2.MCP.OAuth")({ + client_id: Schema.String.pipe(Schema.optional), + client_secret: Schema.String.pipe(Schema.optional), + scope: Schema.String.pipe(Schema.optional), + callback_port: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 })).pipe(Schema.optional), + redirect_uri: Schema.String.pipe(Schema.optional), +}) {} + +export class Remote extends Schema.Class("ConfigV2.MCP.Remote")({ + type: Schema.Literal("remote"), + url: Schema.String, + headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + oauth: Schema.Union([OAuth, Schema.Literal(false)]).pipe(Schema.optional), + disabled: Schema.Boolean.pipe(Schema.optional), + timeout: PositiveInt.pipe(Schema.optional), +}) {} + +export const Server = Schema.Union([Local, Remote]).pipe(Schema.toTaggedUnion("type")) + +export class Info extends Schema.Class("ConfigV2.MCP")({ + timeout: PositiveInt.pipe(Schema.optional), + servers: Schema.Record(Schema.String, Server).pipe(Schema.optional), +}) {} diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 4dc86bd73e..2bf3784b52 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -200,6 +200,31 @@ describe("Config", () => { lsp: { typescript: { disabled: true }, custom: { command: ["custom-lsp"], extensions: [".foo"] } }, attachments: { image: { auto_resize: false, max_width: 1200, max_height: 900, max_base64_bytes: 1048576 } }, tool_output: { max_lines: 1000, max_bytes: 32768 }, + mcp: { + timeout: 5000, + servers: { + local: { + type: "local", + command: ["node", "./mcp/server.js"], + environment: { API_KEY: "secret" }, + disabled: false, + timeout: 10000, + }, + remote: { + type: "remote", + url: "https://mcp.example.com/mcp", + headers: { Authorization: "Bearer token" }, + oauth: { client_id: "client", scope: "read write", callback_port: 19876 }, + disabled: true, + }, + }, + }, + compaction: { + auto: true, + prune: false, + keep: { turns: 3, tokens: 2000 }, + buffer: 10000, + }, skills: ["./skills", "~/shared-skills", "https://example.com/.well-known/skills/"], instructions: ["CONTRIBUTING.md", ".cursor/rules/*.md", "https://example.com/shared-rules.md"], references: { @@ -261,6 +286,31 @@ describe("Config", () => { image: { auto_resize: false, max_width: 1200, max_height: 900, max_base64_bytes: 1048576 }, }) expect(documents[0]?.info.tool_output).toEqual({ max_lines: 1000, max_bytes: 32768 }) + expect(documents[0]?.info.mcp).toEqual({ + timeout: 5000, + servers: { + local: { + type: "local", + command: ["node", "./mcp/server.js"], + environment: { API_KEY: "secret" }, + disabled: false, + timeout: 10000, + }, + remote: { + type: "remote", + url: "https://mcp.example.com/mcp", + headers: { Authorization: "Bearer token" }, + oauth: { client_id: "client", scope: "read write", callback_port: 19876 }, + disabled: true, + }, + }, + }) + expect(documents[0]?.info.compaction).toEqual({ + auto: true, + prune: false, + keep: { turns: 3, tokens: 2000 }, + buffer: 10000, + }) expect(documents[0]?.info.skills).toEqual([ "./skills", "~/shared-skills", diff --git a/specs/v2/config.md b/specs/v2/config.md index 49d5987b1d..269dd46dfe 100644 --- a/specs/v2/config.md +++ b/specs/v2/config.md @@ -276,7 +276,39 @@ External protocol and server integration configuration. | Field | Current Purpose | Status | Notes | | ----- | ------------------------------------- | ------- | ----- | -| `mcp` | MCP server definitions and enablement | pending | | +| `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. | + +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. + +```jsonc +{ + "mcp": { + "timeout": 5000, + "servers": { + "github": { + "type": "local", + "command": ["npx", "-y", "@github/github-mcp-server"], + "environment": { "GITHUB_TOKEN": "{env:GITHUB_TOKEN}" }, + "disabled": false, + "timeout": 10000, + }, + "docs": { + "type": "remote", + "url": "https://docs.example.com/mcp", + "headers": { "Authorization": "Bearer {env:DOCS_TOKEN}" }, + "oauth": { + "client_id": "{env:MCP_CLIENT_ID}", + "client_secret": "{env:MCP_CLIENT_SECRET}", + "scope": "read write", + "callback_port": 19876, + "redirect_uri": "http://127.0.0.1:19876/mcp/oauth/callback", + }, + "disabled": false, + }, + }, + }, +} +``` ## Group 10: Conversation Lifecycle @@ -284,7 +316,23 @@ Behavior affecting long-running conversations and context management. | Field | Current Purpose | Status | Notes | | ------------ | ----------------------------------------------------------- | ------- | ----- | -| `compaction` | Automatic compaction, pruning, and context reserve settings | pending | | +| `compaction` | Automatic compaction, pruning, and context reserve settings | redesign | Group retained verbatim history under `keep` and rename context headroom to `buffer`. | + +Retain the compaction capability but redesign the less clear limits. `keep.turns` is the maximum number of recent user turns to preserve verbatim after compaction, and `keep.tokens` is the token budget for those retained turns. `buffer` is the token headroom reserved so automatic compaction triggers before the input window is exhausted. + +```jsonc +{ + "compaction": { + "auto": true, + "prune": true, + "keep": { + "turns": 2, + "tokens": 2000, + }, + "buffer": 10000, + }, +} +``` ## Group 11: Deprecated And Experimental Settings @@ -292,13 +340,13 @@ Fields that should not be ported by inertia; each needs an explicit justificatio | Field | Current Purpose | Status | Notes | | ------------------------------------ | --------------------------------------- | ------- | ------------------------------------------------------------------- | -| `layout` | Legacy layout selection | pending | Deprecated; current description says stretch layout is always used. | -| `experimental.disable_paste_summary` | Disable pasted-content summary behavior | pending | | -| `experimental.batch_tool` | Enable batch tool | pending | | -| `experimental.openTelemetry` | Enable AI SDK telemetry spans | pending | | -| `experimental.primary_tools` | Restrict tools to primary agents | pending | | -| `experimental.continue_loop_on_deny` | Continue loop after denied tool call | pending | | -| `experimental.mcp_timeout` | MCP request timeout | pending | May belong with MCP rather than experiments. | +| `layout` | Legacy layout selection | remove | Do not port deprecated option; stretch layout is always used. | +| `experimental.disable_paste_summary` | Disable pasted-content summary behavior | remove | Do not port; pasted-input presentation behavior belongs to the client/UI surface. | +| `experimental.batch_tool` | Enable batch tool | remove | Do not port; batch tool is no longer a supported feature. | +| `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.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..timeout` for per-server overrides. | ## Review Order