Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <span style={{ fg: theme.textMuted }}>⋯ Loading</span>
}
if (props.lazy) {
return <span style={{ fg: theme.success, attributes: TextAttributes.BOLD }}>⦿ Lazy</span>
}
if (props.enabled) {
return <span style={{ fg: theme.success, attributes: TextAttributes.BOLD }}>✓ Enabled</span>
}
Expand All @@ -26,10 +29,13 @@ export function DialogMcp() {
const [, setRef] = createSignal<DialogSelectRef<unknown>>()
const [loading, setLoading] = createSignal<string | null>(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 ?? {},
Expand All @@ -39,7 +45,13 @@ export function DialogMcp() {
value: name,
title: name,
description: status.status === "failed" ? "failed" : status.status,
footer: <Status enabled={local.mcp.isEnabled(name)} loading={loadingMcp === name} />,
footer: (
<Status
enabled={local.mcp.isEnabled(name)}
loading={loadingMcp === name}
lazy={lazy && local.mcp.isEnabled(name)}
/>
),
category: undefined,
})),
)
Expand Down
6 changes: 6 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,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(),
})
Expand Down
150 changes: 80 additions & 70 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { spawn } from "child_process"
import { Command } from "../command"
import { $, fileURLToPath } from "bun"
import { ConfigMarkdown } from "../config/markdown"
import { Config } from "../config/config"
import { SessionSummary } from "./summary"
import { NamedError } from "@opencode-ai/util/error"
import { fn } from "@/util/fn"
Expand Down Expand Up @@ -595,7 +596,11 @@ export namespace SessionPrompt {
agent,
abort,
sessionID,
system: [...(await SystemPrompt.environment()), ...(await SystemPrompt.custom())],
system: [
...(await SystemPrompt.environment()),
...(await SystemPrompt.mcpServers()),
...(await SystemPrompt.custom()),
],
messages: [
...MessageV2.toModelMessage(sessionMessages),
...(isLastStep
Expand Down Expand Up @@ -725,94 +730,99 @@ 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())

// 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,
},
)
// 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,
})
}
}
}
}

return {
title: "",
metadata: result.metadata ?? {},
output: textParts.join("\n\n"),
attachments,
content: result.content, // directly return content to preserve ordering when outputting to model
return {
title: "",
metadata: result.metadata ?? {},
output: textParts.join("\n\n"),
attachments,
content: result.content, // directly return content to preserve ordering when outputting to model
}
}
}
item.toModelOutput = (result) => {
return {
type: "text",
value: result.output,
item.toModelOutput = (result) => {
return {
type: "text",
value: result.output,
}
}
tools[key] = item
}
tools[key] = item
}

return tools
Expand Down
22 changes: 22 additions & 0 deletions packages/opencode/src/session/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Ripgrep } from "../file/ripgrep"
import { Global } from "../global"
import { Filesystem } from "../util/filesystem"
import { Config } from "../config/config"
import { MCP } from "../mcp"

import { Instance } from "../project/instance"
import path from "path"
Expand Down Expand Up @@ -76,6 +77,27 @@ export namespace SystemPrompt {
GLOBAL_RULE_FILES.push(path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md"))
}

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 [
[
`<mcp_servers>`,
`Available MCP servers: ${servers.join(", ")}`,
`Use mcp_search tool to discover and call tools from these servers.`,
`</mcp_servers>`,
].join("\n"),
]
}

export async function custom() {
const config = await Config.get()
const paths = new Set<string>()
Expand Down
Loading