Skip to content

Commit 6a477c4

Browse files
committed
feat(mcp): start configured servers asynchronously
1 parent 2c4ad9f commit 6a477c4

12 files changed

Lines changed: 369 additions & 99 deletions

File tree

packages/app/src/components/dialog-select-mcp.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { pathKey } from "@/utils/path-key"
1111

1212
const statusLabels = {
1313
connected: "mcp.status.connected",
14+
connecting: "mcp.status.connecting",
1415
failed: "mcp.status.failed",
1516
needs_auth: "mcp.status.needs_auth",
1617
needs_client_registration: "mcp.status.needs_client_registration",
@@ -79,6 +80,7 @@ export const DialogSelectMcp: Component = () => {
7980
if (s?.status === "failed" || s?.status === "needs_client_registration") return s.error
8081
}
8182
const enabled = () => status() === "connected"
83+
const connecting = () => status() === "connecting"
8284
return (
8385
<div class="w-full flex items-center justify-between gap-x-3">
8486
<div class="flex flex-col gap-0.5 min-w-0">
@@ -95,8 +97,9 @@ export const DialogSelectMcp: Component = () => {
9597
<div onClick={(e) => e.stopPropagation()}>
9698
<Switch
9799
checked={enabled()}
98-
disabled={toggle.isPending && toggle.variables === i.name}
100+
disabled={connecting() || (toggle.isPending && toggle.variables === i.name)}
99101
onChange={() => {
102+
if (connecting()) return
100103
if (toggle.isPending) return
101104
toggle.mutate(i.name)
102105
}}

packages/app/src/components/status-popover-body.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,22 +321,25 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
321321
{(name) => {
322322
const status = () => mcpStatus(name)
323323
const enabled = () => status() === "connected"
324+
const connecting = () => status() === "connecting"
324325
return (
325326
<button
326327
type="button"
327328
class="flex items-center gap-2 w-full min-h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
328329
onClick={() => {
330+
if (connecting()) return
329331
if (toggleMcp.isPending) return
330332
toggleMcp.mutate(name)
331333
}}
332-
disabled={toggleMcp.isPending && toggleMcp.variables === name}
334+
disabled={connecting() || (toggleMcp.isPending && toggleMcp.variables === name)}
333335
>
334336
<div
335337
classList={{
336338
"size-1.5 rounded-full shrink-0": true,
337339
"bg-icon-success-base": status() === "connected",
338340
"bg-icon-critical-base": status() === "failed",
339341
"bg-border-weak-base": status() === "disabled",
342+
"bg-icon-warning-base animate-pulse": status() === "connecting",
340343
"bg-icon-warning-base":
341344
status() === "needs_auth" || status() === "needs_client_registration",
342345
}}
@@ -354,8 +357,9 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
354357
<div onClick={(event) => event.stopPropagation()}>
355358
<Switch
356359
checked={enabled()}
357-
disabled={toggleMcp.isPending && toggleMcp.variables === name}
360+
disabled={connecting() || (toggleMcp.isPending && toggleMcp.variables === name)}
358361
onChange={() => {
362+
if (connecting()) return
359363
if (toggleMcp.isPending) return
360364
toggleMcp.mutate(name)
361365
}}

packages/app/src/context/global-sync.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export const loadMcpQuery = (directory: string, sdk: OpencodeClient) =>
4949
queryOptions({
5050
queryKey: [directory, "mcp"] as const,
5151
queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}),
52+
refetchInterval: (query) =>
53+
Object.values(query.state.data ?? {}).some((status) => status.status === "connecting") ? 1000 : false,
5254
})
5355

5456
export const loadLspQuery = (directory: string, sdk: OpencodeClient) =>

packages/app/src/context/global-sync/event-reducer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Binary } from "@opencode-ai/core/util/binary"
22
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
33
import type {
44
Message,
5+
McpStatus,
56
Part,
67
PermissionRequest,
78
Project,
@@ -182,6 +183,11 @@ export function applyDirectoryEvent(input: {
182183
input.setStore("session_status", props.sessionID, reconcile(props.status))
183184
break
184185
}
186+
case "mcp.status.changed": {
187+
const props = event.properties as { name: string; status: McpStatus }
188+
input.setStore("mcp", props.name, reconcile(props.status))
189+
break
190+
}
185191
case "message.updated": {
186192
const info = clean((event.properties as { info: Message }).info)
187193
const messages = input.store.message[info.sessionID]

packages/app/src/i18n/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ export const dict = {
303303
"dialog.plugins.empty": "Plugins configured in opencode.json",
304304

305305
"mcp.status.connected": "connected",
306+
"mcp.status.connecting": "connecting",
306307
"mcp.status.failed": "failed",
307308
"mcp.status.needs_auth": "needs auth",
308309
"mcp.status.disabled": "disabled",

packages/opencode/src/cli/cmd/mcp.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ export const McpListCommand = effectCmd({
143143
} else if (status.status === "disabled") {
144144
statusIcon = "○"
145145
statusText = "disabled"
146+
} else if (status.status === "connecting") {
147+
statusIcon = "…"
148+
statusText = "connecting"
146149
} else if (status.status === "needs_auth") {
147150
statusIcon = "⚠"
148151
statusText = "needs authentication"

packages/opencode/src/cli/cmd/tui/context/sync.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
363363
break
364364
}
365365

366+
case "mcp.status.changed": {
367+
if (workspace === project.workspace.current()) {
368+
setStore("mcp", event.properties.name, reconcile(event.properties.status))
369+
}
370+
break
371+
}
372+
366373
case "vcs.branch.updated": {
367374
if (workspace === project.workspace.current()) {
368375
setStore("vcs", { branch: event.properties.branch })

packages/opencode/src/config/mcp.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const Local = Schema.Struct({
1313
description: "Enable or disable the MCP server on startup",
1414
}),
1515
timeout: Schema.optional(PositiveInt).annotate({
16-
description: "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
16+
description: "Timeout in ms for MCP server requests. Defaults to 30000 (30 seconds) if not specified.",
1717
}),
1818
}).annotate({ identifier: "McpLocalConfig" })
1919
export type Local = Schema.Schema.Type<typeof Local>
@@ -49,7 +49,7 @@ export const Remote = Schema.Struct({
4949
description: "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.",
5050
}),
5151
timeout: Schema.optional(PositiveInt).annotate({
52-
description: "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
52+
description: "Timeout in ms for MCP server requests. Defaults to 30000 (30 seconds) if not specified.",
5353
}),
5454
}).annotate({ identifier: "McpRemoteConfig" })
5555
export type Remote = Schema.Schema.Type<typeof Remote>

0 commit comments

Comments
 (0)