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 b1e00fccb85..0a692860386 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1202,6 +1202,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 75bd3c9dfac..71c5c4140a0 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -32,6 +32,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" @@ -45,6 +46,7 @@ import { LLM } from "./llm" import { iife } from "@/util/iife" import { Shell } from "@/shell/shell" import { Truncate } from "@/tool/truncation" +import { processMcpResult } from "@/tool/mcp-result" // @ts-ignore globalThis.AI_SDK_LOG_WARNINGS = false @@ -648,7 +650,11 @@ export namespace SessionPrompt { await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs }) // Build system prompt, adding structured output instruction if needed - const system = [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())] + const system = [ + ...(await SystemPrompt.environment(model)), + ...(await SystemPrompt.mcpServers()), + ...(await InstructionPrompt.system()), + ] const format = lastUser.format ?? { type: "text" } if (format.type === "json_schema") { system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT) @@ -825,97 +831,76 @@ 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 - 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, - args, - }, - result, - ) + const result = await execute(args, opts) - const textParts: string[] = [] - const attachments: Omit[] = [] - - for (const contentItem of result.content) { - if (contentItem.type === "text") { - textParts.push(contentItem.text) - } else if (contentItem.type === "image") { - attachments.push({ - 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) { - attachments.push({ - type: "file", - mime: resource.mimeType ?? "application/octet-stream", - url: `data:${resource.mimeType ?? "application/octet-stream"};base64,${resource.blob}`, - filename: resource.uri, - }) - } - } - } + await Plugin.trigger( + "tool.execute.after", + { + tool: key, + sessionID: ctx.sessionID, + callID: opts.toolCallId, + args, + }, + result, + ) - const truncated = await Truncate.output(textParts.join("\n\n"), {}, input.agent) - const metadata = { - ...(result.metadata ?? {}), - truncated: truncated.truncated, - ...(truncated.truncated && { outputPath: truncated.outputPath }), - } + const processed = processMcpResult(result) - return { - title: "", - metadata, - output: truncated.content, - attachments: attachments.map((attachment) => ({ - ...attachment, - id: Identifier.ascending("part"), - sessionID: ctx.sessionID, - messageID: input.processor.message.id, - })), - content: result.content, // directly return content to preserve ordering when outputting to model + const truncated = await Truncate.output(processed.output, {}, input.agent) + const metadata = { + ...processed.metadata, + truncated: truncated.truncated, + ...(truncated.truncated && { outputPath: truncated.outputPath }), + } + + return { + title: "", + metadata, + output: truncated.content, + attachments: processed.attachments.map((attachment) => ({ + ...attachment, + id: Identifier.ascending("part"), + sessionID: ctx.sessionID, + messageID: input.processor.message.id, + })), + 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-result.ts b/packages/opencode/src/tool/mcp-result.ts new file mode 100644 index 00000000000..13a1f65d488 --- /dev/null +++ b/packages/opencode/src/tool/mcp-result.ts @@ -0,0 +1,41 @@ +import type { MessageV2 } from "../session/message-v2" + +export function processMcpResult(result: { + content: Array<{ + type: "text" | "image" | "resource" + text?: string + mimeType?: string + data?: string + resource?: { + text?: string + blob?: string + mimeType?: string + uri?: string + } + }> + metadata?: Record +}): { + output: string + attachments: Omit[] + metadata: Record +} { + const { text, attachments } = result.content.reduce( + (acc, item) => { + if (item.type === "text") acc.text.push(item.text!) + if (item.type === "image") acc.attachments.push({ type: "file", mime: item.mimeType!, url: `data:${item.mimeType};base64,${item.data}` }) + if (item.type === "resource") { + const r = item.resource! + if (r.text) acc.text.push(r.text) + if (r.blob) acc.attachments.push({ type: "file", mime: r.mimeType ?? "application/octet-stream", url: `data:${r.mimeType ?? "application/octet-stream"};base64,${r.blob}`, filename: r.uri }) + } + return acc + }, + { text: [] as string[], attachments: [] as Omit[] }, + ) + + return { + output: text.join("\n\n"), + attachments, + metadata: result.metadata ?? {}, + } +} diff --git a/packages/opencode/src/tool/mcp-search.ts b/packages/opencode/src/tool/mcp-search.ts new file mode 100644 index 00000000000..3fa3864f3f7 --- /dev/null +++ b/packages/opencode/src/tool/mcp-search.ts @@ -0,0 +1,192 @@ +import z from "zod" +import { Tool } from "./tool" +import { MCP } from "../mcp" +import { Plugin } from "../plugin" +import { processMcpResult } from "./mcp-result" +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) return s.tools.map((t) => ({ server: s.name, ...t })) + if (s.name.toLowerCase().includes(q)) return s.tools.map((t) => ({ server: s.name, ...t })) + const filtered = s.tools.filter( + (t) => t.name.toLowerCase().includes(q) || (t.description?.toLowerCase().includes(q) ?? false), + ) + return filtered.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, args }, result) + + const processed = processMcpResult(result) + + return { + title: `${server}/${tool}`, + output: processed.output || "Success (no output)", + metadata: { ...processed.metadata, server, tool }, + attachments: processed.attachments, + } +} + +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..c3c7b325e42 --- /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 matches against server names, tool names, and descriptions. If the server name matches, all tools from that server are returned. Otherwise, individual tools whose name or description matches are returned. + - 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 playwright tools, search "screenshot" to find screenshot tools across servers, search "context7" to find documentation lookup tools. diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index ef0e78ffa86..8fc69348588 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -28,6 +28,7 @@ import { Truncate } from "./truncation" import { PlanExitTool, PlanEnterTool } from "./plan" import { ApplyPatchTool } from "./apply_patch" import { Glob } from "../util/glob" +import { McpSearchTool } from "./mcp-search" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) @@ -118,10 +119,12 @@ 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 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 4050ef15738..bf243c920b5 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -548,6 +548,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 @@ -715,75 +784,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: { @@ -956,6 +956,12 @@ export type Event = | EventMessagePartUpdated | EventMessagePartDelta | EventMessagePartRemoved + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventMcpBrowserOpenFailed | EventPermissionAsked | EventPermissionReplied | EventSessionStatus @@ -966,12 +972,6 @@ export type Event = | EventSessionCompacted | EventFileWatcherUpdated | EventTodoUpdated - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventMcpToolsChanged - | EventMcpBrowserOpenFailed | EventCommandExecuted | EventSessionCreated | EventSessionUpdated @@ -1887,6 +1887,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 2741c2362ec..4fcbe5fd5a4 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7487,6 +7487,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": { @@ -7900,162 +8056,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": { @@ -8551,52 +8551,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" @@ -10140,6 +10140,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" } } }