Skip to content
Closed
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: 15 additions & 1 deletion packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type { EditTool } from "@/tool/edit"
import type { PatchTool } from "@/tool/patch"
import type { WebFetchTool } from "@/tool/webfetch"
import type { TaskTool } from "@/tool/task"
import type { MCPRegistryTool } from "@/tool/mcp-registry"
import { useKeyboard, useRenderer, useTerminalDimensions, type BoxProps, type JSX } from "@opentui/solid"
import { useSDK } from "@tui/context/sdk"
import { useCommandDialog } from "@tui/component/dialog-command"
Expand Down Expand Up @@ -1224,7 +1225,7 @@ type ToolProps<T extends Tool.Info> = {
}
function GenericTool(props: ToolProps<any>) {
return (
<ToolTitle icon="" fallback="Writing command..." when={true}>
<ToolTitle icon="" fallback="Writing command..." when={true}>
{props.tool} {input(props.input)}
</ToolTitle>
)
Expand Down Expand Up @@ -1621,6 +1622,19 @@ ToolRegistry.register<typeof TodoWriteTool>({
},
})

ToolRegistry.register<typeof MCPRegistryTool>({
name: "mcp_registry",
container: "inline",
render(props) {
const activate = props.input.activate
return (
<ToolTitle icon="→" fallback="Activating MCP tools..." when={activate?.length}>
MCP Registry activate [{activate?.join(", ")}]
</ToolTitle>
)
},
})

function normalizePath(input?: string) {
if (!input) return ""
if (path.isAbsolute(input)) {
Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,10 @@ export namespace Config {
chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
disable_paste_summary: z.boolean().optional(),
batch_tool: z.boolean().optional().describe("Enable the batch tool"),
mcp_registry: z
.boolean()
.optional()
.describe("Hide MCP tools behind a registry tool that the model can query"),
})
.optional(),
})
Expand Down
16 changes: 13 additions & 3 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { mergeDeep, pipe } from "remeda"
import { ToolRegistry } from "../tool/registry"
import { Wildcard } from "../util/wildcard"
import { MCP } from "../mcp"
import { getActivatedTools, decrementActivatedTools } from "../tool/mcp-registry"
import { LSP } from "../lsp"
import { ReadTool } from "../tool/read"
import { ListTool } from "../tool/ls"
Expand All @@ -41,6 +42,7 @@ import { ulid } from "ulid"
import { spawn } from "child_process"
import { Command } from "../command"
import { $, fileURLToPath } from "bun"
import { Config } from "../config/config"
import { ConfigMarkdown } from "../config/markdown"
import { SessionSummary } from "./summary"
import { NamedError } from "@/util/error"
Expand Down Expand Up @@ -196,6 +198,7 @@ export namespace SessionPrompt {

const message = await createUserMessage(input)
await Session.touch(input.sessionID)
decrementActivatedTools(input.sessionID)

if (input.noReply === true) {
return message
Expand Down Expand Up @@ -468,7 +471,7 @@ export namespace SessionPrompt {
agent,
system: lastUser.system,
})
const tools = await resolveTools({
const { tools, hiddenTools } = await resolveTools({
agent,
sessionID,
model: lastUser.model,
Expand Down Expand Up @@ -544,7 +547,7 @@ export namespace SessionPrompt {
},
// set to 0, we handle loop
maxRetries: 0,
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
activeTools: Object.keys(tools).filter((x) => x !== "invalid" && !hiddenTools.has(x)),
maxOutputTokens: ProviderTransform.maxOutputTokens(
model.providerID,
params.options,
Expand Down Expand Up @@ -651,6 +654,7 @@ export namespace SessionPrompt {
processor: SessionProcessor.Info
}) {
const tools: Record<string, AITool> = {}
const hiddenTools = new Set<string>()
const enabledTools = pipe(
input.agent.tools,
mergeDeep(await ToolRegistry.enabled(input.model.providerID, input.model.modelID, input.agent)),
Expand Down Expand Up @@ -724,6 +728,9 @@ export namespace SessionPrompt {
})
}

const config = await Config.get()
const mcpRegistryEnabled = config.experimental?.mcp_registry === true
const activatedTools = mcpRegistryEnabled ? getActivatedTools(input.sessionID) : undefined
for (const [key, item] of Object.entries(await MCP.tools())) {
if (Wildcard.all(key, enabledTools) === false) continue
const execute = item.execute
Expand Down Expand Up @@ -786,8 +793,11 @@ export namespace SessionPrompt {
}
}
tools[key] = item
if (activatedTools && !activatedTools.has(key)) {
hiddenTools.add(key)
}
}
return tools
return { tools, hiddenTools }
}

async function createUserMessage(input: PromptInput) {
Expand Down
80 changes: 80 additions & 0 deletions packages/opencode/src/tool/mcp-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import z from "zod"
import { Tool } from "./tool"
import { MCP } from "../mcp"
import DESCRIPTION from "./mcp-registry.txt"

// Track which MCP tools are activated per session (after being described)
// Key: sessionID, Value: Map of toolName -> remaining turns
const activatedTools = new Map<string, Map<string, number>>()
const ACTIVATION_TURNS = 3

export function getActivatedTools(sessionID: string): Set<string> {
const session = activatedTools.get(sessionID)
if (!session) return new Set()
return new Set(session.keys())
}

export function decrementActivatedTools(sessionID: string) {
const session = activatedTools.get(sessionID)
if (!session) return
for (const [tool, turns] of session) {
if (turns <= 1) {
session.delete(tool)
} else {
session.set(tool, turns - 1)
}
}
if (session.size === 0) {
activatedTools.delete(sessionID)
}
}

function activateTool(sessionID: string, toolName: string) {
let session = activatedTools.get(sessionID)
if (!session) {
session = new Map()
activatedTools.set(sessionID, session)
}
session.set(toolName, ACTIVATION_TURNS)
}

export const MCPRegistryTool = Tool.define("mcp_registry", async () => {
const mcpTools = await MCP.tools()
const names = Object.keys(mcpTools).map((n) => n.replaceAll("-", "_"))
const description = `${DESCRIPTION}\n\nAvailable MCP tools: [${names.join(", ")}]`

return {
description,
parameters: z.object({
activate: z.array(z.string()).describe("Array of exact tool names to activate."),
}),
async execute(params, ctx) {
const currentTools = await MCP.tools()
const results: string[] = []
const activated: string[] = []

for (const name of params.activate) {
const tool = currentTools[name]
if (!tool) {
results.push(`Tool "${name}" not found`)
continue
}
activateTool(ctx.sessionID, name)
activated.push(name)
}

if (activated.length > 0) {
results.push(
`Activated ${activated.length} tool(s): ${activated.join(", ")}`,
"These tools are now temporarily available in your environment.",
)
}

return {
title: activated.length ? `Activated ${activated.length} tools` : "No tools activated",
output: results.join("\n"),
metadata: {},
}
},
}
})
8 changes: 8 additions & 0 deletions packages/opencode/src/tool/mcp-registry.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Activate MCP tools from connected servers.

To use an MCP tool, you must first activate it using this tool. Once activated, the tool becomes available in this environment for a few turns.

Usage:
- Review the available MCP tools listed below
- Call this tool with the exact tool name(s) you want to activate
- Once activated, the tools and their descriptions are available in context so you can use them directly
2 changes: 2 additions & 0 deletions packages/opencode/src/tool/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TodoWriteTool, TodoReadTool } from "./todo"
import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
import { InvalidTool } from "./invalid"
import { MCPRegistryTool } from "./mcp-registry"
import type { Agent } from "../agent/agent"
import { Tool } from "./tool"
import { Instance } from "../project/instance"
Expand Down Expand Up @@ -98,6 +99,7 @@ export namespace ToolRegistry {
TodoWriteTool,
TodoReadTool,
...(config.experimental?.batch_tool === true ? [BatchTool] : []),
...(config.experimental?.mcp_registry === true ? [MCPRegistryTool] : []),
...(Flag.OPENCODE_EXPERIMENTAL_EXA ? [WebSearchTool, CodeSearchTool] : []),
...custom,
]
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
}
}
}
2 changes: 1 addition & 1 deletion packages/sdk/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@
"publishConfig": {
"directory": "dist"
}
}
}
4 changes: 4 additions & 0 deletions packages/sdk/js/src/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1218,6 +1218,10 @@ export type Config = {
* Enable the batch tool
*/
batch_tool?: boolean
/**
* Hide MCP tools behind a registry tool that the model can query
*/
mcp_registry?: boolean
}
}

Expand Down