diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx index 9cfa30d4df9..546bff485cb 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx @@ -8,11 +8,14 @@ import { Keybind } from "@/util/keybind" import { TextAttributes } from "@opentui/core" import { useSDK } from "@tui/context/sdk" -function Status(props: { enabled: boolean; loading: boolean }) { +function Status(props: { enabled: boolean; loading: boolean; lazy: boolean }) { const { theme } = useTheme() if (props.loading) { return ⋯ Loading } + if (props.lazy) { + return ⦿ Lazy + } if (props.enabled) { return ✓ Enabled } @@ -26,10 +29,13 @@ export function DialogMcp() { const [, setRef] = createSignal>() const [loading, setLoading] = createSignal(null) + const mcpLazy = createMemo(() => sync.data.config.experimental?.mcp_lazy === true) + const options = createMemo(() => { // Track sync data and loading state to trigger re-render when they change const mcpData = sync.data.mcp const loadingMcp = loading() + const lazy = mcpLazy() return pipe( mcpData ?? {}, @@ -39,7 +45,13 @@ export function DialogMcp() { value: name, title: name, description: status.status === "failed" ? "failed" : status.status, - footer: , + footer: ( + + ), category: undefined, })), ) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index a231a530072..059b585c701 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1182,6 +1182,12 @@ export namespace Config { .positive() .optional() .describe("Timeout in milliseconds for model context protocol (MCP) requests"), + mcp_lazy: z + .boolean() + .optional() + .describe( + "Enable lazy loading of MCP tools. When enabled, MCP tools are not loaded into context automatically. Instead, use the mcp_search tool to discover and call MCP tools on-demand.", + ), }) .optional(), }) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index ad7b6f1a91d..ef14802b14f 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -33,6 +33,7 @@ import { ulid } from "ulid" import { spawn } from "child_process" import { Command } from "../command" import { $, fileURLToPath, pathToFileURL } from "bun" +import { Config } from "../config/config" import { ConfigMarkdown } from "../config/markdown" import { SessionSummary } from "./summary" import { NamedError } from "@opencode-ai/util/error" @@ -614,7 +615,11 @@ export namespace SessionPrompt { agent, abort, sessionID, - system: [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())], + system: [ + ...(await SystemPrompt.environment(model)), + ...(await SystemPrompt.mcpServers()), + ...(await InstructionPrompt.system()), + ], messages: [ ...MessageV2.toModelMessages(sessionMessages, model), ...(isLastStep @@ -743,97 +748,102 @@ export namespace SessionPrompt { }) } - for (const [key, item] of Object.entries(await MCP.tools())) { - const execute = item.execute - if (!execute) continue + const cfg = await Config.get() + const mcpLazy = cfg.experimental?.mcp_lazy === true || (await ToolRegistry.hasMcpSearch()) - const transformed = ProviderTransform.schema(input.model, asSchema(item.inputSchema).jsonSchema) - item.inputSchema = jsonSchema(transformed) - // Wrap execute to add plugin hooks and format output - item.execute = async (args, opts) => { - const ctx = context(args, opts) + if (!mcpLazy) { + for (const [key, item] of Object.entries(await MCP.tools())) { + const execute = item.execute + if (!execute) continue - await Plugin.trigger( - "tool.execute.before", - { - tool: key, - sessionID: ctx.sessionID, - callID: opts.toolCallId, - }, - { - args, - }, - ) + const transformed = ProviderTransform.schema(input.model, asSchema(item.inputSchema).jsonSchema) + item.inputSchema = jsonSchema(transformed) + // Wrap execute to add plugin hooks and format output + item.execute = async (args, opts) => { + const ctx = context(args, opts) - await ctx.ask({ - permission: key, - metadata: {}, - patterns: ["*"], - always: ["*"], - }) + await Plugin.trigger( + "tool.execute.before", + { + tool: key, + sessionID: ctx.sessionID, + callID: opts.toolCallId, + }, + { + args, + }, + ) - const result = await execute(args, opts) + await ctx.ask({ + permission: key, + metadata: {}, + patterns: ["*"], + always: ["*"], + }) - await Plugin.trigger( - "tool.execute.after", - { - tool: key, - sessionID: ctx.sessionID, - callID: opts.toolCallId, - }, - result, - ) + const result = await execute(args, opts) - const textParts: string[] = [] - const attachments: MessageV2.FilePart[] = [] + await Plugin.trigger( + "tool.execute.after", + { + tool: key, + sessionID: ctx.sessionID, + callID: opts.toolCallId, + }, + result, + ) - for (const contentItem of result.content) { - if (contentItem.type === "text") { - textParts.push(contentItem.text) - } else if (contentItem.type === "image") { - attachments.push({ - id: Identifier.ascending("part"), - sessionID: input.session.id, - messageID: input.processor.message.id, - type: "file", - mime: contentItem.mimeType, - url: `data:${contentItem.mimeType};base64,${contentItem.data}`, - }) - } else if (contentItem.type === "resource") { - const { resource } = contentItem - if (resource.text) { - textParts.push(resource.text) - } - if (resource.blob) { + const textParts: string[] = [] + const attachments: MessageV2.FilePart[] = [] + + for (const contentItem of result.content) { + if (contentItem.type === "text") { + textParts.push(contentItem.text) + } else if (contentItem.type === "image") { attachments.push({ id: Identifier.ascending("part"), sessionID: input.session.id, messageID: input.processor.message.id, type: "file", - mime: resource.mimeType ?? "application/octet-stream", - url: `data:${resource.mimeType ?? "application/octet-stream"};base64,${resource.blob}`, - filename: resource.uri, + mime: contentItem.mimeType, + url: `data:${contentItem.mimeType};base64,${contentItem.data}`, }) + } else if (contentItem.type === "resource") { + const { resource } = contentItem + if (resource.text) { + textParts.push(resource.text) + } + if (resource.blob) { + attachments.push({ + id: Identifier.ascending("part"), + sessionID: input.session.id, + messageID: input.processor.message.id, + type: "file", + mime: resource.mimeType ?? "application/octet-stream", + url: `data:${resource.mimeType ?? "application/octet-stream"};base64,${resource.blob}`, + filename: resource.uri, + }) + } } } - } - const truncated = await Truncate.output(textParts.join("\n\n"), {}, input.agent) - const metadata = { - ...(result.metadata ?? {}), - truncated: truncated.truncated, - ...(truncated.truncated && { outputPath: truncated.outputPath }), - } + const truncated = await Truncate.output(textParts.join("\n\n"), {}, input.agent) + const metadata = { + ...(result.metadata ?? {}), + truncated: truncated.truncated, + ...(truncated.truncated && { outputPath: truncated.outputPath }), + } - return { - title: "", - metadata, - output: truncated.content, - attachments, - content: result.content, // directly return content to preserve ordering when outputting to model + return { + title: "", + metadata, + output: truncated.content, + attachments, + content: result.content, // directly return content to preserve ordering when outputting to model + } } + tools[key] = item } - tools[key] = item } return tools diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index a61dd8cba55..c27a09ca90f 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -10,6 +10,8 @@ import PROMPT_GEMINI from "./prompt/gemini.txt" import PROMPT_CODEX from "./prompt/codex_header.txt" import PROMPT_TRINITY from "./prompt/trinity.txt" import type { Provider } from "@/provider/provider" +import { Config } from "../config/config" +import { MCP } from "../mcp" export namespace SystemPrompt { export function instructions() { @@ -26,6 +28,27 @@ export namespace SystemPrompt { return [PROMPT_ANTHROPIC_WITHOUT_TODO] } + export async function mcpServers() { + const config = await Config.get() + if (config.experimental?.mcp_lazy !== true) return [] + + const status = await MCP.status() + const servers = Object.entries(status) + .filter(([_, s]) => s.status === "connected") + .map(([name]) => name) + + if (servers.length === 0) return [] + + return [ + [ + ``, + `Available MCP servers: ${servers.join(", ")}`, + `Use mcp_search tool to discover and call tools from these servers.`, + ``, + ].join("\n"), + ] + } + export async function environment(model: Provider.Model) { const project = Instance.project return [ diff --git a/packages/opencode/src/tool/mcp-search.ts b/packages/opencode/src/tool/mcp-search.ts new file mode 100644 index 00000000000..fa85d768740 --- /dev/null +++ b/packages/opencode/src/tool/mcp-search.ts @@ -0,0 +1,188 @@ +import z from "zod" +import { Tool } from "./tool" +import { MCP } from "../mcp" +import { Plugin } from "../plugin" +import DESCRIPTION from "./mcp-search.txt" + +function sanitize(name: string) { + return name.replace(/[^a-zA-Z0-9_-]/g, "_") +} + +function extractSchema(input: unknown): Record | undefined { + if (!input || typeof input !== "object") return undefined + if ("jsonSchema" in input) return (input as { jsonSchema: Record }).jsonSchema + return input as Record +} + +function formatSchema(schema: Record, indent = 0): string { + const properties = schema.properties as Record> | undefined + const required = new Set((schema.required as string[]) ?? []) + if (!properties || Object.keys(properties).length === 0) return " ".repeat(indent) + "No parameters required" + + const pad = " ".repeat(indent) + return Object.entries(properties) + .flatMap(([name, prop]) => { + const lines = [`${pad}- **${name}**${required.has(name) ? " (required)" : " (optional)"}: ${prop.type ?? "any"}`] + if (prop.description) lines.push(`${pad} ${prop.description}`) + if (prop.type === "object" && prop.properties) lines.push(formatSchema(prop, indent + 1)) + if (prop.enum) lines.push(`${pad} Allowed values: ${(prop.enum as string[]).join(", ")}`) + return lines + }) + .join("\n") +} + +const parameters = z.object({ + operation: z.enum(["list", "search", "describe", "call"]).describe("Operation to perform"), + query: z.string().optional().describe("Search query (for 'search')"), + server: z.string().optional().describe("MCP server name (for 'describe'/'call')"), + tool: z.string().optional().describe("Tool name (for 'describe'/'call')"), + args: z.record(z.string(), z.any()).optional().describe("Tool arguments (for 'call')"), +}) + +async function getConnectedServers() { + const [status, allTools] = await Promise.all([MCP.status(), MCP.tools()]) + const toolEntries = Object.entries(allTools) + + return Object.entries(status) + .filter(([, s]) => s.status === "connected") + .map(([name]) => { + const prefix = sanitize(name) + "_" + const tools = toolEntries + .filter(([key]) => key.startsWith(prefix)) + .map(([key, tool]) => ({ name: key.slice(prefix.length), description: tool.description })) + return { name, tools } + }) +} + +async function resolveTool(server: string, tool: string) { + const [status, allTools] = await Promise.all([MCP.status(), MCP.tools()]) + + if (status[server]?.status !== "connected") throw new Error(`MCP server "${server}" is not connected`) + + const prefix = sanitize(server) + const key = `${prefix}_${sanitize(tool)}` + const mcpTool = allTools[key] + + if (mcpTool) return { key, mcpTool } + + const available = Object.keys(allTools) + .filter((k) => k.startsWith(prefix + "_")) + .map((k) => k.slice(prefix.length + 1)) + throw new Error(`Tool "${tool}" not found on "${server}". Available: ${available.join(", ") || "none"}`) +} + +async function list() { + const servers = await getConnectedServers() + if (servers.length === 0) return { title: "No MCP servers", output: "No connected MCP servers.", metadata: {} } + + const output = servers + .map((s) => `## ${s.name}\n${s.tools.map((t) => `- ${t.name}: ${t.description ?? "No description"}`).join("\n")}`) + .join("\n\n") + + return { title: `${servers.length} MCP servers`, output, metadata: { servers: servers.length } } +} + +async function search(query?: string) { + const servers = await getConnectedServers() + const q = query?.toLowerCase() ?? "" + + const matches = servers.flatMap((s) => { + if (q && !s.name.toLowerCase().includes(q)) return [] + return s.tools.map((t) => ({ server: s.name, ...t })) + }) + + if (matches.length === 0) { + return { + title: "No matches", + output: query ? `No tools matching "${query}"` : "No MCP tools available", + metadata: {}, + } + } + + const output = matches.map((m) => `- ${m.server}/${m.name}: ${m.description ?? "No description"}`).join("\n") + return { + title: `${matches.length} tools found`, + output: `Found ${matches.length} tool(s)${query ? ` matching "${query}"` : ""}:\n\n${output}\n\nYou MUST use describe before calling any of these tools.`, + metadata: { count: matches.length }, + } +} + +async function describe(server: string, tool: string) { + const { mcpTool } = await resolveTool(server, tool) + const schema = extractSchema(mcpTool.inputSchema) + + return { + title: `${server}/${tool}`, + output: [ + `## ${server}/${tool}`, + "", + `**Description:** ${mcpTool.description ?? "No description"}`, + "", + "**Parameters:**", + schema ? formatSchema(schema) : "No parameters required", + "", + "**Example:**", + "```", + `mcp_search(operation: "call", server: "${server}", tool: "${tool}", args: { ... })`, + "```", + ].join("\n"), + metadata: { server, tool }, + } +} + +async function call(server: string, tool: string, args: Record, ctx: Tool.Context) { + const { key, mcpTool } = await resolveTool(server, tool) + const schema = extractSchema(mcpTool.inputSchema) + const required = (schema?.required as string[]) ?? [] + const missing = required.filter((r) => !(r in args)) + + if (missing.length > 0) { + return { + title: "Arguments required", + output: [ + `Tool "${tool}" requires arguments.`, + "", + `**Missing:** ${missing.join(", ")}`, + "", + `**Tool:** ${server}/${tool}`, + `**Description:** ${mcpTool.description ?? "No description"}`, + "", + "**Parameters:**", + schema ? formatSchema(schema) : "No schema available", + "", + "**Example:**", + `mcp_search(operation: "call", server: "${server}", tool: "${tool}", args: { ${required.map((r) => `"${r}": ...`).join(", ")} })`, + ].join("\n"), + metadata: { server, tool, missing }, + } + } + + await ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] }) + await Plugin.trigger("tool.execute.before", { tool: key, sessionID: ctx.sessionID, callID: ctx.callID }, { args }) + + const result = await mcpTool.execute!(args, { toolCallId: ctx.callID ?? "", abortSignal: ctx.abort, messages: [] }) + + await Plugin.trigger("tool.execute.after", { tool: key, sessionID: ctx.sessionID, callID: ctx.callID }, result) + + const parts: string[] = [] + for (const c of result.content) { + if (c.type === "text") parts.push(c.text) + else if (c.type === "image") parts.push(`[Image: ${c.mimeType}, ${c.data.length} bytes]`) + else if (c.type === "resource") parts.push(c.resource.text ?? `[Resource: ${c.resource.uri}]`) + } + const output = parts.join("\n\n") + + return { title: `${server}/${tool}`, output: output || "Success (no output)", metadata: { server, tool } } +} + +export const McpSearchTool = Tool.define>("mcp_search", { + description: DESCRIPTION, + parameters, + async execute(params, ctx) { + if (params.operation === "list") return list() + if (params.operation === "search") return search(params.query) + if (!params.server || !params.tool) throw new Error("Both 'server' and 'tool' parameters are required") + if (params.operation === "describe") return describe(params.server, params.tool) + return call(params.server, params.tool, params.args ?? {}, ctx) + }, +}) diff --git a/packages/opencode/src/tool/mcp-search.txt b/packages/opencode/src/tool/mcp-search.txt new file mode 100644 index 00000000000..35137350a7c --- /dev/null +++ b/packages/opencode/src/tool/mcp-search.txt @@ -0,0 +1,13 @@ +Search and call MCP server tools. + +Operations: +- "list": List all MCP servers and their tools +- "search": Find tools by server name +- "describe": Get tool's parameter schema +- "call": Execute a tool + +Usage notes: + - The "search" query MUST be a server name (e.g., "playwright", "context7"). It only matches against server names and returns all tools from matching servers. + - You MUST call "describe" before "call" to check the tool's parameters. + - You SHOULD NOT call a tool without first checking its parameters. + - Examples: search "playwright" to find all browser automation tools, search "context7" to find documentation lookup tools. diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 5ed5a879b48..e3d796946a1 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -27,6 +27,7 @@ import { LspTool } from "./lsp" import { Truncate } from "./truncation" import { PlanExitTool, PlanEnterTool } from "./plan" import { ApplyPatchTool } from "./apply_patch" +import { McpSearchTool } from "./mcp-search" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) @@ -115,10 +116,16 @@ export namespace ToolRegistry { ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []), ...(config.experimental?.batch_tool === true ? [BatchTool] : []), ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []), + ...(config.experimental?.mcp_lazy === true ? [McpSearchTool] : []), ...custom, ] } + export async function hasMcpSearch(): Promise { + const tools = await all() + return tools.some((t) => t.id === "mcp_search") + } + export async function ids() { return all().then((x) => x.map((t) => t.id)) } diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9543e5b5796..60dbbc1d629 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -509,6 +509,75 @@ export type EventMessagePartRemoved = { } } +export type EventTuiPromptAppend = { + type: "tui.prompt.append" + properties: { + text: string + } +} + +export type EventTuiCommandExecute = { + type: "tui.command.execute" + properties: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.line.up" + | "session.line.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string + } +} + +export type EventTuiToastShow = { + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + /** + * Duration in milliseconds + */ + duration?: number + } +} + +export type EventTuiSessionSelect = { + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string + } +} + +export type EventMcpToolsChanged = { + type: "mcp.tools.changed" + properties: { + server: string + } +} + +export type EventMcpBrowserOpenFailed = { + type: "mcp.browser.open.failed" + properties: { + mcpName: string + url: string + } +} + export type PermissionRequest = { id: string sessionID: string @@ -680,75 +749,6 @@ export type EventTodoUpdated = { } } -export type EventTuiPromptAppend = { - type: "tui.prompt.append" - properties: { - text: string - } -} - -export type EventTuiCommandExecute = { - type: "tui.command.execute" - properties: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.line.up" - | "session.line.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - | string - } -} - -export type EventTuiToastShow = { - type: "tui.toast.show" - properties: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - /** - * Duration in milliseconds - */ - duration?: number - } -} - -export type EventTuiSessionSelect = { - type: "tui.session.select" - properties: { - /** - * Session ID to navigate to - */ - sessionID: string - } -} - -export type EventMcpToolsChanged = { - type: "mcp.tools.changed" - properties: { - server: string - } -} - -export type EventMcpBrowserOpenFailed = { - type: "mcp.browser.open.failed" - properties: { - mcpName: string - url: string - } -} - export type EventCommandExecuted = { type: "command.executed" properties: { @@ -919,6 +919,12 @@ export type Event = | EventMessageRemoved | EventMessagePartUpdated | EventMessagePartRemoved + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventMcpBrowserOpenFailed | EventPermissionAsked | EventPermissionReplied | EventSessionStatus @@ -929,12 +935,6 @@ export type Event = | EventSessionCompacted | EventFileWatcherUpdated | EventTodoUpdated - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventMcpToolsChanged - | EventMcpBrowserOpenFailed | EventCommandExecuted | EventSessionCreated | EventSessionUpdated @@ -1845,6 +1845,10 @@ export type Config = { * Timeout in milliseconds for model context protocol (MCP) requests */ mcp_timeout?: number + /** + * Enable lazy loading of MCP tools. When enabled, MCP tools are not loaded into context automatically. Instead, use the mcp_search tool to discover and call MCP tools on-demand. + */ + mcp_lazy?: boolean } } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 18022a3384d..ed459e89682 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7284,6 +7284,162 @@ }, "required": ["type", "properties"] }, + "Event.tui.prompt.append": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tui.prompt.append" + }, + "properties": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"] + } + }, + "required": ["type", "properties"] + }, + "Event.tui.command.execute": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tui.command.execute" + }, + "properties": { + "type": "object", + "properties": { + "command": { + "anyOf": [ + { + "type": "string", + "enum": [ + "session.list", + "session.new", + "session.share", + "session.interrupt", + "session.compact", + "session.page.up", + "session.page.down", + "session.line.up", + "session.line.down", + "session.half.page.up", + "session.half.page.down", + "session.first", + "session.last", + "prompt.clear", + "prompt.submit", + "agent.cycle" + ] + }, + { + "type": "string" + } + ] + } + }, + "required": ["command"] + } + }, + "required": ["type", "properties"] + }, + "Event.tui.toast.show": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tui.toast.show" + }, + "properties": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "variant": { + "type": "string", + "enum": ["info", "success", "warning", "error"] + }, + "duration": { + "description": "Duration in milliseconds", + "default": 5000, + "type": "number" + } + }, + "required": ["message", "variant"] + } + }, + "required": ["type", "properties"] + }, + "Event.tui.session.select": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tui.session.select" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "description": "Session ID to navigate to", + "type": "string", + "pattern": "^ses" + } + }, + "required": ["sessionID"] + } + }, + "required": ["type", "properties"] + }, + "Event.mcp.tools.changed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "mcp.tools.changed" + }, + "properties": { + "type": "object", + "properties": { + "server": { + "type": "string" + } + }, + "required": ["server"] + } + }, + "required": ["type", "properties"] + }, + "Event.mcp.browser.open.failed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "mcp.browser.open.failed" + }, + "properties": { + "type": "object", + "properties": { + "mcpName": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": ["mcpName", "url"] + } + }, + "required": ["type", "properties"] + }, "PermissionRequest": { "type": "object", "properties": { @@ -7701,162 +7857,6 @@ }, "required": ["type", "properties"] }, - "Event.tui.prompt.append": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "tui.prompt.append" - }, - "properties": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"] - } - }, - "required": ["type", "properties"] - }, - "Event.tui.command.execute": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "tui.command.execute" - }, - "properties": { - "type": "object", - "properties": { - "command": { - "anyOf": [ - { - "type": "string", - "enum": [ - "session.list", - "session.new", - "session.share", - "session.interrupt", - "session.compact", - "session.page.up", - "session.page.down", - "session.line.up", - "session.line.down", - "session.half.page.up", - "session.half.page.down", - "session.first", - "session.last", - "prompt.clear", - "prompt.submit", - "agent.cycle" - ] - }, - { - "type": "string" - } - ] - } - }, - "required": ["command"] - } - }, - "required": ["type", "properties"] - }, - "Event.tui.toast.show": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "tui.toast.show" - }, - "properties": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": ["info", "success", "warning", "error"] - }, - "duration": { - "description": "Duration in milliseconds", - "default": 5000, - "type": "number" - } - }, - "required": ["message", "variant"] - } - }, - "required": ["type", "properties"] - }, - "Event.tui.session.select": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "tui.session.select" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "description": "Session ID to navigate to", - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"] - } - }, - "required": ["type", "properties"] - }, - "Event.mcp.tools.changed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "mcp.tools.changed" - }, - "properties": { - "type": "object", - "properties": { - "server": { - "type": "string" - } - }, - "required": ["server"] - } - }, - "required": ["type", "properties"] - }, - "Event.mcp.browser.open.failed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "mcp.browser.open.failed" - }, - "properties": { - "type": "object", - "properties": { - "mcpName": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": ["mcpName", "url"] - } - }, - "required": ["type", "properties"] - }, "Event.command.executed": { "type": "object", "properties": { @@ -8346,52 +8346,52 @@ "$ref": "#/components/schemas/Event.message.part.removed" }, { - "$ref": "#/components/schemas/Event.permission.asked" + "$ref": "#/components/schemas/Event.tui.prompt.append" }, { - "$ref": "#/components/schemas/Event.permission.replied" + "$ref": "#/components/schemas/Event.tui.command.execute" }, { - "$ref": "#/components/schemas/Event.session.status" + "$ref": "#/components/schemas/Event.tui.toast.show" }, { - "$ref": "#/components/schemas/Event.session.idle" + "$ref": "#/components/schemas/Event.tui.session.select" }, { - "$ref": "#/components/schemas/Event.question.asked" + "$ref": "#/components/schemas/Event.mcp.tools.changed" }, { - "$ref": "#/components/schemas/Event.question.replied" + "$ref": "#/components/schemas/Event.mcp.browser.open.failed" }, { - "$ref": "#/components/schemas/Event.question.rejected" + "$ref": "#/components/schemas/Event.permission.asked" }, { - "$ref": "#/components/schemas/Event.session.compacted" + "$ref": "#/components/schemas/Event.permission.replied" }, { - "$ref": "#/components/schemas/Event.file.watcher.updated" + "$ref": "#/components/schemas/Event.session.status" }, { - "$ref": "#/components/schemas/Event.todo.updated" + "$ref": "#/components/schemas/Event.session.idle" }, { - "$ref": "#/components/schemas/Event.tui.prompt.append" + "$ref": "#/components/schemas/Event.question.asked" }, { - "$ref": "#/components/schemas/Event.tui.command.execute" + "$ref": "#/components/schemas/Event.question.replied" }, { - "$ref": "#/components/schemas/Event.tui.toast.show" + "$ref": "#/components/schemas/Event.question.rejected" }, { - "$ref": "#/components/schemas/Event.tui.session.select" + "$ref": "#/components/schemas/Event.session.compacted" }, { - "$ref": "#/components/schemas/Event.mcp.tools.changed" + "$ref": "#/components/schemas/Event.file.watcher.updated" }, { - "$ref": "#/components/schemas/Event.mcp.browser.open.failed" + "$ref": "#/components/schemas/Event.todo.updated" }, { "$ref": "#/components/schemas/Event.command.executed" @@ -9927,6 +9927,10 @@ "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 + }, + "mcp_lazy": { + "description": "Enable lazy loading of MCP tools. When enabled, MCP tools are not loaded into context automatically. Instead, use the mcp_search tool to discover and call MCP tools on-demand.", + "type": "boolean" } } }