feat(core): add mcp and compaction config schema

This commit is contained in:
Dax Raad 2026-05-28 10:27:01 -04:00
commit 864f54cc46
5 changed files with 167 additions and 9 deletions

View file

@ -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<Info>("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",
}),

View file

@ -0,0 +1,16 @@
export * as ConfigCompaction from "./compaction"
import { Schema } from "effect"
import { NonNegativeInt } from "../schema"
export class Keep extends Schema.Class<Keep>("ConfigV2.Compaction.Keep")({
turns: NonNegativeInt.pipe(Schema.optional),
tokens: NonNegativeInt.pipe(Schema.optional),
}) {}
export class Info extends Schema.Class<Info>("ConfigV2.Compaction")({
auto: Schema.Boolean.pipe(Schema.optional),
prune: Schema.Boolean.pipe(Schema.optional),
keep: Keep.pipe(Schema.optional),
buffer: NonNegativeInt.pipe(Schema.optional),
}) {}

View file

@ -0,0 +1,36 @@
export * as ConfigMCP from "./mcp"
import { Schema } from "effect"
import { PositiveInt } from "../schema"
export class Local extends Schema.Class<Local>("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<OAuth>("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<Remote>("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<Info>("ConfigV2.MCP")({
timeout: PositiveInt.pipe(Schema.optional),
servers: Schema.Record(Schema.String, Server).pipe(Schema.optional),
}) {}

View file

@ -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",

View file

@ -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.<name>.timeout` for per-server overrides. |
## Review Order