diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index e7fa4a6de..226de4796 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -12,6 +12,7 @@ import { SDKProvider, useSDK } from "@tui/context/sdk"
import { SyncProvider, useSync } from "@tui/context/sync"
import { LocalProvider, useLocal } from "@tui/context/local"
import { DialogModel, useConnected } from "@tui/component/dialog-model"
+import { DialogMcp } from "@tui/component/dialog-mcp"
import { DialogStatus } from "@tui/component/dialog-status"
import { DialogThemeList } from "@tui/component/dialog-theme-list"
import { DialogHelp } from "./ui/dialog-help"
@@ -301,6 +302,14 @@ function App() {
dialog.replace(() => )
},
},
+ {
+ title: "Toggle MCPs",
+ value: "mcp.list",
+ category: "Agent",
+ onSelect: () => {
+ dialog.replace(() => )
+ },
+ },
{
title: "Agent cycle",
value: "agent.cycle",
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx
new file mode 100644
index 000000000..9cfa30d4d
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx
@@ -0,0 +1,86 @@
+import { createMemo, createSignal } from "solid-js"
+import { useLocal } from "@tui/context/local"
+import { useSync } from "@tui/context/sync"
+import { map, pipe, entries, sortBy } from "remeda"
+import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select"
+import { useTheme } from "../context/theme"
+import { Keybind } from "@/util/keybind"
+import { TextAttributes } from "@opentui/core"
+import { useSDK } from "@tui/context/sdk"
+
+function Status(props: { enabled: boolean; loading: boolean }) {
+ const { theme } = useTheme()
+ if (props.loading) {
+ return ⋯ Loading
+ }
+ if (props.enabled) {
+ return ✓ Enabled
+ }
+ return ○ Disabled
+}
+
+export function DialogMcp() {
+ const local = useLocal()
+ const sync = useSync()
+ const sdk = useSDK()
+ const [, setRef] = createSignal>()
+ const [loading, setLoading] = createSignal(null)
+
+ const options = createMemo(() => {
+ // Track sync data and loading state to trigger re-render when they change
+ const mcpData = sync.data.mcp
+ const loadingMcp = loading()
+
+ return pipe(
+ mcpData ?? {},
+ entries(),
+ sortBy(([name]) => name),
+ map(([name, status]) => ({
+ value: name,
+ title: name,
+ description: status.status === "failed" ? "failed" : status.status,
+ footer: ,
+ category: undefined,
+ })),
+ )
+ })
+
+ const keybinds = createMemo(() => [
+ {
+ keybind: Keybind.parse("space")[0],
+ title: "toggle",
+ onTrigger: async (option: DialogSelectOption) => {
+ // Prevent toggling while an operation is already in progress
+ if (loading() !== null) return
+
+ setLoading(option.value)
+ try {
+ await local.mcp.toggle(option.value)
+ // Refresh MCP status from server
+ const status = await sdk.client.mcp.status()
+ if (status.data) {
+ sync.set("mcp", status.data)
+ } else {
+ console.error("Failed to refresh MCP status: no data returned")
+ }
+ } catch (error) {
+ console.error("Failed to toggle MCP:", error)
+ } finally {
+ setLoading(null)
+ }
+ },
+ },
+ ])
+
+ return (
+ {
+ // Don't close on select, only on escape
+ }}
+ />
+ )
+}
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
index 7ef465368..f41b33852 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
@@ -307,10 +307,14 @@ export function Autocomplete(props: {
},
{
display: "/status",
- aliases: ["/mcp"],
description: "show status",
onSelect: () => command.trigger("opencode.status"),
},
+ {
+ display: "/mcp",
+ description: "toggle MCPs",
+ onSelect: () => command.trigger("mcp.list"),
+ },
{
display: "/theme",
description: "toggle theme",
diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx
index 3bfedf34e..6cc97e041 100644
--- a/packages/opencode/src/cli/cmd/tui/context/local.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx
@@ -10,12 +10,14 @@ import { createSimpleContext } from "./helper"
import { useToast } from "../ui/toast"
import { Provider } from "@/provider/provider"
import { useArgs } from "./args"
+import { useSDK } from "./sdk"
import { RGBA } from "@opentui/core"
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local",
init: () => {
const sync = useSync()
+ const sdk = useSDK()
const toast = useToast()
function isModelValid(model: { providerID: string; modelID: string }) {
@@ -310,9 +312,27 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
})
+ const mcp = {
+ isEnabled(name: string) {
+ const status = sync.data.mcp[name]
+ return status?.status === "connected"
+ },
+ async toggle(name: string) {
+ const status = sync.data.mcp[name]
+ if (status?.status === "connected") {
+ // Disable: disconnect the MCP
+ await sdk.client.mcp.disconnect({ name })
+ } else {
+ // Enable/Retry: connect the MCP (handles disabled, failed, and other states)
+ await sdk.client.mcp.connect({ name })
+ }
+ },
+ }
+
const result = {
model,
agent,
+ mcp,
}
return result
},
diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx
index cd308b44c..d0bb296eb 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx
@@ -24,8 +24,12 @@ export function Home() {
return Object.values(sync.data.mcp).some((x) => x.status === "failed")
})
+ const connectedMcpCount = createMemo(() => {
+ return Object.values(sync.data.mcp).filter((x) => x.status === "connected").length
+ })
+
const Hint = (
- 0}>
+ 0}>
@@ -35,7 +39,7 @@ export function Home() {
•{" "}
- {Locale.pluralize(Object.values(sync.data.mcp).length, "{} mcp server", "{} mcp servers")}
+ {Locale.pluralize(connectedMcpCount(), "{} mcp server", "{} mcp servers")}
@@ -85,7 +89,7 @@ export function Home() {
⊙
- {Object.keys(sync.data.mcp).length} MCP
+ {connectedMcpCount()} MCP
/status
diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts
index 82a9a3d36..e030f83b5 100644
--- a/packages/opencode/src/mcp/index.ts
+++ b/packages/opencode/src/mcp/index.ts
@@ -86,6 +86,12 @@ export namespace MCP {
await Promise.all(
Object.entries(config).map(async ([key, mcp]) => {
+ // If disabled by config, mark as disabled without trying to connect
+ if (mcp.enabled === false) {
+ status[key] = { status: "disabled" }
+ return
+ }
+
const result = await create(key, mcp).catch(() => undefined)
if (!result) return
@@ -319,18 +325,73 @@ export namespace MCP {
}
export async function status() {
- return state().then((state) => state.status)
+ const s = await state()
+ const cfg = await Config.get()
+ const config = cfg.mcp ?? {}
+ const result: Record = {}
+
+ // Include all MCPs from config, not just connected ones
+ for (const key of Object.keys(config)) {
+ result[key] = s.status[key] ?? { status: "disabled" }
+ }
+
+ return result
}
export async function clients() {
return state().then((state) => state.clients)
}
+ export async function connect(name: string) {
+ const cfg = await Config.get()
+ const config = cfg.mcp ?? {}
+ const mcp = config[name]
+ if (!mcp) {
+ log.error("MCP config not found", { name })
+ return
+ }
+
+ const result = await create(name, { ...mcp, enabled: true })
+
+ if (!result) {
+ const s = await state()
+ s.status[name] = {
+ status: "failed",
+ error: "Unknown error during connection",
+ }
+ return
+ }
+
+ const s = await state()
+ s.status[name] = result.status
+ if (result.mcpClient) {
+ s.clients[name] = result.mcpClient
+ }
+ }
+
+ export async function disconnect(name: string) {
+ const s = await state()
+ const client = s.clients[name]
+ if (client) {
+ await client.close().catch((error) => {
+ log.error("Failed to close MCP client", { name, error })
+ })
+ delete s.clients[name]
+ }
+ s.status[name] = { status: "disabled" }
+ }
+
export async function tools() {
const result: Record = {}
const s = await state()
const clientsSnapshot = await clients()
+
for (const [clientName, client] of Object.entries(clientsSnapshot)) {
+ // Only include tools from connected MCPs (skip disabled ones)
+ if (s.status[clientName]?.status !== "connected") {
+ continue
+ }
+
const tools = await client.tools().catch((e) => {
log.error("failed to get tools", { clientName, error: e.message })
const failedStatus = {
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 78ef5924e..855663cb9 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -1984,6 +1984,52 @@ export namespace Server {
return c.json({ success: true as const })
},
)
+ .post(
+ "/mcp/:name/connect",
+ describeRoute({
+ description: "Connect an MCP server",
+ operationId: "mcp.connect",
+ responses: {
+ 200: {
+ description: "MCP server connected successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ validator("param", z.object({ name: z.string() })),
+ async (c) => {
+ const { name } = c.req.valid("param")
+ await MCP.connect(name)
+ return c.json(true)
+ },
+ )
+ .post(
+ "/mcp/:name/disconnect",
+ describeRoute({
+ description: "Disconnect an MCP server",
+ operationId: "mcp.disconnect",
+ responses: {
+ 200: {
+ description: "MCP server disconnected successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ validator("param", z.object({ name: z.string() })),
+ async (c) => {
+ const { name } = c.req.valid("param")
+ await MCP.disconnect(name)
+ return c.json(true)
+ },
+ )
.get(
"/lsp",
describeRoute({
diff --git a/packages/sdk/js/openapi.json b/packages/sdk/js/openapi.json
index 1fef9e623..421edfaf1 100644
--- a/packages/sdk/js/openapi.json
+++ b/packages/sdk/js/openapi.json
@@ -3996,6 +3996,88 @@
]
}
},
+ "/mcp/{name}/connect": {
+ "post": {
+ "operationId": "mcp.connect",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "name",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "description": "Connect an MCP server",
+ "responses": {
+ "200": {
+ "description": "MCP server connected successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.connect({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/mcp/{name}/disconnect": {
+ "post": {
+ "operationId": "mcp.disconnect",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "name",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "description": "Disconnect an MCP server",
+ "responses": {
+ "200": {
+ "description": "MCP server disconnected successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.disconnect({\n ...\n})"
+ }
+ ]
+ }
+ },
"/lsp": {
"get": {
"operationId": "lsp.status",
diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts
index af69b42ff..5e3e67e1c 100644
--- a/packages/sdk/js/src/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/gen/sdk.gen.ts
@@ -160,6 +160,10 @@ import type {
McpAuthAuthenticateData,
McpAuthAuthenticateResponses,
McpAuthAuthenticateErrors,
+ McpConnectData,
+ McpConnectResponses,
+ McpDisconnectData,
+ McpDisconnectResponses,
LspStatusData,
LspStatusResponses,
FormatterStatusData,
@@ -945,6 +949,27 @@ class Mcp extends _HeyApiClient {
},
})
}
+
+ /**
+ * Connect an MCP server
+ */
+ public connect(options: Options) {
+ return (options.client ?? this._client).post({
+ url: "/mcp/{name}/connect",
+ ...options,
+ })
+ }
+
+ /**
+ * Disconnect an MCP server
+ */
+ public disconnect(options: Options) {
+ return (options.client ?? this._client).post({
+ url: "/mcp/{name}/disconnect",
+ ...options,
+ })
+ }
+
auth = new Auth({ client: this._client })
}
diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts
index 5267c0e51..8d4550525 100644
--- a/packages/sdk/js/src/gen/types.gen.ts
+++ b/packages/sdk/js/src/gen/types.gen.ts
@@ -3494,6 +3494,46 @@ export type McpAuthAuthenticateResponses = {
export type McpAuthAuthenticateResponse = McpAuthAuthenticateResponses[keyof McpAuthAuthenticateResponses]
+export type McpConnectData = {
+ body?: never
+ path: {
+ name: string
+ }
+ query?: {
+ directory?: string
+ }
+ url: "/mcp/{name}/connect"
+}
+
+export type McpConnectResponses = {
+ /**
+ * MCP server connected successfully
+ */
+ 200: boolean
+}
+
+export type McpConnectResponse = McpConnectResponses[keyof McpConnectResponses]
+
+export type McpDisconnectData = {
+ body?: never
+ path: {
+ name: string
+ }
+ query?: {
+ directory?: string
+ }
+ url: "/mcp/{name}/disconnect"
+}
+
+export type McpDisconnectResponses = {
+ /**
+ * MCP server disconnected successfully
+ */
+ 200: boolean
+}
+
+export type McpDisconnectResponse = McpDisconnectResponses[keyof McpDisconnectResponses]
+
export type LspStatusData = {
body?: never
path?: never
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index 77e3ba1f5..38f39b2a9 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -41,6 +41,8 @@ import type {
McpAuthRemoveResponses,
McpAuthStartErrors,
McpAuthStartResponses,
+ McpConnectResponses,
+ McpDisconnectResponses,
McpLocalConfig,
McpRemoteConfig,
McpStatusResponses,
@@ -2077,6 +2079,62 @@ export class Mcp extends HeyApiClient {
})
}
+ /**
+ * Connect an MCP server
+ */
+ public connect(
+ parameters: {
+ name: string
+ directory?: string
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "path", key: "name" },
+ { in: "query", key: "directory" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).post({
+ url: "/mcp/{name}/connect",
+ ...options,
+ ...params,
+ })
+ }
+
+ /**
+ * Disconnect an MCP server
+ */
+ public disconnect(
+ parameters: {
+ name: string
+ directory?: string
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "path", key: "name" },
+ { in: "query", key: "directory" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).post({
+ url: "/mcp/{name}/disconnect",
+ ...options,
+ ...params,
+ })
+ }
+
auth = new Auth({ client: this.client })
}
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index ffd7ee373..9b80026f0 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -3503,6 +3503,46 @@ export type McpAuthAuthenticateResponses = {
export type McpAuthAuthenticateResponse = McpAuthAuthenticateResponses[keyof McpAuthAuthenticateResponses]
+export type McpConnectData = {
+ body?: never
+ path: {
+ name: string
+ }
+ query?: {
+ directory?: string
+ }
+ url: "/mcp/{name}/connect"
+}
+
+export type McpConnectResponses = {
+ /**
+ * MCP server connected successfully
+ */
+ 200: boolean
+}
+
+export type McpConnectResponse = McpConnectResponses[keyof McpConnectResponses]
+
+export type McpDisconnectData = {
+ body?: never
+ path: {
+ name: string
+ }
+ query?: {
+ directory?: string
+ }
+ url: "/mcp/{name}/disconnect"
+}
+
+export type McpDisconnectResponses = {
+ /**
+ * MCP server disconnected successfully
+ */
+ 200: boolean
+}
+
+export type McpDisconnectResponse = McpDisconnectResponses[keyof McpDisconnectResponses]
+
export type LspStatusData = {
body?: never
path?: never
diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json
index 1fef9e623..421edfaf1 100644
--- a/packages/sdk/openapi.json
+++ b/packages/sdk/openapi.json
@@ -3996,6 +3996,88 @@
]
}
},
+ "/mcp/{name}/connect": {
+ "post": {
+ "operationId": "mcp.connect",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "name",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "description": "Connect an MCP server",
+ "responses": {
+ "200": {
+ "description": "MCP server connected successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.connect({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/mcp/{name}/disconnect": {
+ "post": {
+ "operationId": "mcp.disconnect",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "name",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "description": "Disconnect an MCP server",
+ "responses": {
+ "200": {
+ "description": "MCP server disconnected successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.disconnect({\n ...\n})"
+ }
+ ]
+ }
+ },
"/lsp": {
"get": {
"operationId": "lsp.status",