}) {
"bg-icon-success-base": status() === "connected",
"bg-icon-critical-base": status() === "failed",
"bg-border-weak-base": status() === "disabled",
- "bg-icon-warning-base animate-pulse": status() === "connecting",
"bg-icon-warning-base":
status() === "needs_auth" || status() === "needs_client_registration",
}}
@@ -487,9 +484,8 @@ export function StatusPopoverBody(props: { shown: Accessor
}) {
event.stopPropagation()}>
{
- if (connecting()) return
if (toggleMcp.isPending) return
toggleMcp.mutate(name)
}}
diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx
index 5b9bca444e..2e9ac17071 100644
--- a/packages/app/src/context/global-sync.tsx
+++ b/packages/app/src/context/global-sync.tsx
@@ -1,17 +1,7 @@
import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, Todo } from "@opencode-ai/sdk/v2/client"
import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/core/util/path"
-import {
- batch,
- createContext,
- createSignal,
- getOwner,
- onCleanup,
- onMount,
- type ParentProps,
- untrack,
- useContext,
-} from "solid-js"
+import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store"
import { useLanguage } from "@/context/language"
import type { InitError } from "../pages/error"
@@ -59,8 +49,6 @@ export const loadMcpQuery = (directory: string, sdk: OpencodeClient) =>
queryOptions({
queryKey: [directory, "mcp"] as const,
queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}),
- refetchInterval: (query) =>
- Object.values(query.state.data ?? {}).some((status) => status.status === "connecting") ? 1000 : false,
})
export const loadLspQuery = (directory: string, sdk: OpencodeClient) =>
@@ -144,7 +132,6 @@ function createGlobalSync() {
let bootingRoot = false
let eventFrame: number | undefined
let eventTimer: ReturnType | undefined
- const [childVersion, setChildVersion] = createSignal(0)
onCleanup(() => {
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
@@ -217,7 +204,6 @@ function createGlobalSync() {
onBootstrap: (directory) => {
void bootstrapInstance(directory)
},
- onCreate: () => setChildVersion((value) => value + 1),
onDispose: (directory) => {
const key = directoryKey(directory)
queue.clear(key)
@@ -452,8 +438,6 @@ function createGlobalSync() {
return globalStore.error
},
child: children.child,
- childVersion,
- existing: children.existing,
peek: children.peek,
queryOptions: queryOptionsApi,
// bootstrap,
diff --git a/packages/app/src/context/global-sync/child-store.test.ts b/packages/app/src/context/global-sync/child-store.test.ts
index 2a5e5d1d2d..89d6ee1d8e 100644
--- a/packages/app/src/context/global-sync/child-store.test.ts
+++ b/packages/app/src/context/global-sync/child-store.test.ts
@@ -73,7 +73,6 @@ describe("createChildStoreManager", () => {
isBooting: () => false,
isLoadingSessions: () => false,
onBootstrap() {},
- onCreate() {},
onDispose() {},
translate: (key) => key,
queryOptions: queryOptionsApi,
@@ -104,7 +103,6 @@ describe("createChildStoreManager", () => {
onBootstrap(directory) {
bootstraps.push(directory)
},
- onCreate() {},
onDispose() {},
translate: (key) => key,
queryOptions: queryOptionsApi,
diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts
index bb599b0d31..40c3c3ae92 100644
--- a/packages/app/src/context/global-sync/child-store.ts
+++ b/packages/app/src/context/global-sync/child-store.ts
@@ -24,7 +24,6 @@ export function createChildStoreManager(input: {
isBooting: (directory: string) => boolean
isLoadingSessions: (directory: string) => boolean
onBootstrap: (directory: string) => void
- onCreate: () => void
onDispose: (directory: string) => void
translate: (key: string, vars?: Record) => string
queryOptions: QueryOptionsApi
@@ -236,7 +235,6 @@ export function createChildStoreManager(input: {
part_text_accum_delta: {},
})
children[key] = child
- input.onCreate()
disposers.set(key, dispose)
const onPersistedInit = (init: Promise | string | null, run: () => void) => {
@@ -283,10 +281,6 @@ export function createChildStoreManager(input: {
return childStore
}
- function existing(directory: string) {
- return children[directoryKey(directory)]
- }
-
function peek(directory: string, options: ChildOptions = {}) {
const key = directoryKey(directory)
const childStore = ensureChild(directory)
@@ -328,7 +322,6 @@ export function createChildStoreManager(input: {
return {
children,
ensureChild,
- existing,
child,
peek,
projectMeta,
diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts
index 8b4e4dcd71..5b72d37f9d 100644
--- a/packages/app/src/context/global-sync/event-reducer.ts
+++ b/packages/app/src/context/global-sync/event-reducer.ts
@@ -2,7 +2,6 @@ import { Binary } from "@opencode-ai/core/util/binary"
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import type {
Message,
- McpStatus,
Part,
PermissionRequest,
Project,
@@ -183,11 +182,6 @@ export function applyDirectoryEvent(input: {
input.setStore("session_status", props.sessionID, reconcile(props.status))
break
}
- case "mcp.status.changed": {
- const props = event.properties as { name: string; status: McpStatus }
- input.setStore("mcp", props.name, reconcile(props.status))
- break
- }
case "message.updated": {
const info = clean((event.properties as { info: Message }).info)
const messages = input.store.message[info.sessionID]
diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx
index 53a5e2f0e9..0d37dd26af 100644
--- a/packages/app/src/context/layout.tsx
+++ b/packages/app/src/context/layout.tsx
@@ -386,16 +386,17 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}
function enrich(project: { worktree: string; expanded: boolean }) {
- const childStore = globalSync.existing(project.worktree)?.[0]
- const projectID = childStore?.project
+ const [childStore] = globalSync.child(project.worktree, { bootstrap: false })
+ const projectID = childStore.project
const metadata = projectID
? globalSync.data.project.find((x) => x.id === projectID)
: globalSync.data.project.find((x) => x.worktree === project.worktree)
- // Use child metadata only after the workspace is already loaded. Creating child
- // stores here fans out workspace bootstrap requests while rendering the project list.
+ // Preserve local icon override from per-workspace localStorage cache (childStore.icon).
+ // Without this, different subdirectories of the same git repo would share the same
+ // icon from the database instead of using their individual overrides.
const base = { ...metadata, ...project }
- if (childStore?.icon) {
+ if (childStore.icon) {
return { ...base, icon: { ...base.icon, override: childStore.icon } }
}
return base
@@ -474,7 +475,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
for (const project of projects) {
if (!project.id) continue
if (project.id === "global") continue
- if (!globalSync.existing(project.worktree)) continue
globalSync.project.icon(project.worktree, project.icon?.override)
}
})
@@ -521,6 +521,28 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}
})
+ let sessionFrame: number | undefined
+ let sessionTimer: number | undefined
+
+ onMount(() => {
+ sessionFrame = requestAnimationFrame(() => {
+ sessionFrame = undefined
+ sessionTimer = window.setTimeout(() => {
+ sessionTimer = undefined
+ void Promise.all(
+ server.projects.list().map((project) => {
+ return globalSync.project.loadSessions(project.worktree)
+ }),
+ )
+ }, 0)
+ })
+ })
+
+ onCleanup(() => {
+ if (sessionFrame !== undefined) cancelAnimationFrame(sessionFrame)
+ if (sessionTimer !== undefined) window.clearTimeout(sessionTimer)
+ })
+
return {
ready,
handoff: {
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 024dd23379..d29b070b29 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -304,7 +304,6 @@ export const dict = {
"dialog.plugins.empty": "Plugins configured in opencode.json",
"mcp.status.connected": "connected",
- "mcp.status.connecting": "connecting",
"mcp.status.failed": "failed",
"mcp.status.needs_auth": "needs auth",
"mcp.status.disabled": "disabled",
diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx
index 671b58da72..37d91a7fb1 100644
--- a/packages/app/src/pages/home.tsx
+++ b/packages/app/src/pages/home.tsx
@@ -79,9 +79,7 @@ function HomeDesign() {
const sessionLoad = useQuery(() => ({
queryKey: ["home", "sessions", ...projectDirectories()] as const,
queryFn: async () => {
- const [root] = projectDirectories()
- if (!root) return null
- await sync.project.loadSessions(root)
+ await Promise.all(projectDirectories().map((directory) => sync.project.loadSessions(directory)))
return null
},
}))
@@ -89,16 +87,11 @@ function HomeDesign() {
const projectByID = createMemo(
() => new Map(projects().flatMap((project) => (project.id ? [[project.id, project] as const] : []))),
)
- const records = createMemo(() => {
- sync.childVersion()
- return [
+ const records = createMemo(() =>
+ [
...new Map(
projectDirectories()
- .flatMap((directory) => {
- const store = sync.existing(directory)?.[0]
- if (!store) return []
- return sortedRootSessions(store, Date.now())
- })
+ .flatMap((directory) => sortedRootSessions(sync.child(directory, { bootstrap: false })[0], Date.now()))
.map((session) => [`${pathKey(session.directory)}:${session.id}`, session] as const),
).values(),
]
@@ -117,8 +110,8 @@ function HomeDesign() {
if (!value) return true
return `${record.session.title} ${record.projectName}`.toLowerCase().includes(value)
})
- .slice(0, HOME_SESSION_LIMIT)
- })
+ .slice(0, HOME_SESSION_LIMIT),
+ )
const groups = createMemo(() => groupSessions(records(), language))
function selectProject(directory: string) {
diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts
index beba9345dd..f7ea030aa9 100644
--- a/packages/opencode/src/cli/cmd/mcp.ts
+++ b/packages/opencode/src/cli/cmd/mcp.ts
@@ -143,9 +143,6 @@ export const McpListCommand = effectCmd({
} else if (status.status === "disabled") {
statusIcon = "○"
statusText = "disabled"
- } else if (status.status === "connecting") {
- statusIcon = "…"
- statusText = "connecting"
} else if (status.status === "needs_auth") {
statusIcon = "⚠"
statusText = "needs authentication"
diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts
index 22d316e755..85985876e7 100644
--- a/packages/opencode/src/mcp/index.ts
+++ b/packages/opencode/src/mcp/index.ts
@@ -26,7 +26,7 @@ import { BusEvent } from "../bus/bus-event"
import { Bus } from "@/bus"
import { TuiEvent } from "@/cli/cmd/tui/event"
import open from "open"
-import { Effect, Exit, Layer, Option, Context, Schema, Stream, Scope, Semaphore } from "effect"
+import { Effect, Exit, Layer, Option, Context, Schema, Stream } from "effect"
import { EffectBridge } from "@/effect/bridge"
import { InstanceState } from "@/effect/instance-state"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
@@ -79,9 +79,6 @@ const StatusConnected = Schema.Struct({ status: Schema.Literal("connected") }).a
const StatusDisabled = Schema.Struct({ status: Schema.Literal("disabled") }).annotate({
identifier: "MCPStatusDisabled",
})
-const StatusConnecting = Schema.Struct({ status: Schema.Literal("connecting") }).annotate({
- identifier: "MCPStatusConnecting",
-})
const StatusFailed = Schema.Struct({ status: Schema.Literal("failed"), error: Schema.String }).annotate({
identifier: "MCPStatusFailed",
})
@@ -96,21 +93,12 @@ const StatusNeedsClientRegistration = Schema.Struct({
export const Status = Schema.Union([
StatusConnected,
StatusDisabled,
- StatusConnecting,
StatusFailed,
StatusNeedsAuth,
StatusNeedsClientRegistration,
]).annotate({ identifier: "MCPStatus", discriminator: "status" })
export type Status = Schema.Schema.Type
-export const StatusChanged = BusEvent.define(
- "mcp.status.changed",
- Schema.Struct({
- name: Schema.String,
- status: Status,
- }),
-)
-
// Store transports for OAuth servers to allow finishing auth
type TransportWithAuth = StreamableHTTPClientTransport | SSEClientTransport
const pendingOAuthTransports = new Map()
@@ -249,7 +237,6 @@ interface State {
status: Record
clients: Record
defs: Record
- revision: Record
}
export interface Interface {
@@ -493,7 +480,6 @@ export const layer = Layer.effect(
return { mcpClient, status, defs: listed } satisfies CreateResult
})
const cfgSvc = yield* Config.Service
- const startupLock = Semaphore.makeUnsafe(1)
const descendants = Effect.fnUntraced(
function* (pid: number) {
@@ -533,133 +519,43 @@ export const layer = Layer.effect(
})
}
- function failedStatus(error: unknown): Status {
- return { status: "failed", error: error instanceof Error ? error.message : String(error) }
- }
-
- function bump(s: State, name: string) {
- const next = (s.revision[name] ?? 0) + 1
- s.revision[name] = next
- return next
- }
-
- function closeClient(s: State, name: string) {
- const client = s.clients[name]
- delete s.defs[name]
- if (!client) return Effect.void
- return Effect.tryPromise(() => client.close()).pipe(Effect.ignore)
- }
-
- function closeCreateResult(result: CreateResult) {
- const client = result.mcpClient
- if (!client) return Effect.void
- return Effect.tryPromise(() => client.close()).pipe(Effect.ignore)
- }
-
- const setStatus = Effect.fnUntraced(function* (s: State, name: string, status: Status) {
- s.status[name] = status
- yield* bus.publish(StatusChanged, { name, status }).pipe(Effect.ignore)
- return status
- })
-
- const storeClient = Effect.fnUntraced(function* (
- s: State,
- name: string,
- client: MCPClient,
- listed: MCPToolDef[],
- timeout?: number,
- ) {
- const bridge = yield* EffectBridge.make()
- yield* closeClient(s, name)
- s.clients[name] = client
- s.defs[name] = listed
- watch(s, name, client, bridge, timeout)
- return yield* setStatus(s, name, { status: "connected" })
- })
-
- const applyCreateResult = Effect.fnUntraced(function* (
- s: State,
- name: string,
- result: CreateResult,
- timeout?: number,
- ) {
- const client = result.mcpClient
- if (!client) {
- yield* closeClient(s, name)
- delete s.clients[name]
- return yield* setStatus(s, name, result.status)
- }
-
- if (!result.defs) {
- yield* closeCreateResult(result)
- yield* closeClient(s, name)
- delete s.clients[name]
- return yield* setStatus(s, name, { status: "failed", error: "Failed to get tools" })
- }
-
- return yield* storeClient(s, name, client, result.defs, timeout)
- })
-
- const createSafely = (key: string, mcp: ConfigMCP.Info) =>
- create(key, mcp).pipe(
- Effect.catch((error) => {
- log.error("mcp startup failed", { key, error })
- return Effect.succeed({ status: failedStatus(error) } satisfies CreateResult)
- }),
- )
-
- const startConfigured = Effect.fn("MCP.startConfigured")(function* (
- s: State,
- entries: ReadonlyArray,
- ) {
- yield* startupLock.withPermits(1)(
- Effect.forEach(
- entries,
- ([key, mcp]) =>
- Effect.gen(function* () {
- const revision = s.revision[key] ?? 0
- const result = yield* createSafely(key, mcp)
- if ((s.revision[key] ?? 0) !== revision) {
- yield* closeCreateResult(result)
- return
- }
- yield* applyCreateResult(s, key, result, mcp.timeout)
- }),
- { concurrency: "unbounded", discard: true },
- ),
- )
- })
-
const state = yield* InstanceState.make(
Effect.fn("MCP.state")(function* () {
const cfg = yield* cfgSvc.get()
- const scope = yield* Scope.Scope
+ const bridge = yield* EffectBridge.make()
const config = cfg.mcp ?? {}
const s: State = {
status: {},
clients: {},
defs: {},
- revision: {},
}
- const configured = Object.entries(config).flatMap(([key, mcp]) => {
- if (!isMcpConfigured(mcp)) {
- log.error("Ignoring MCP config entry without type", { key })
- return []
- }
+ yield* Effect.forEach(
+ Object.entries(config),
+ ([key, mcp]) =>
+ Effect.gen(function* () {
+ if (!isMcpConfigured(mcp)) {
+ log.error("Ignoring MCP config entry without type", { key })
+ return
+ }
- if (mcp.enabled === false) {
- s.status[key] = { status: "disabled" }
- return []
- }
+ if (mcp.enabled === false) {
+ s.status[key] = { status: "disabled" }
+ return
+ }
- s.status[key] = { status: "connecting" }
- return [[key, mcp] as const]
- })
+ const result = yield* create(key, mcp).pipe(Effect.catch(() => Effect.void))
+ if (!result) return
- if (configured.length > 0) {
- yield* startConfigured(s, configured).pipe(Effect.ignore, Effect.forkIn(scope), Effect.asVoid)
- }
+ s.status[key] = result.status
+ if (result.mcpClient) {
+ s.clients[key] = result.mcpClient
+ s.defs[key] = result.defs!
+ watch(s, key, result.mcpClient, bridge, mcp.timeout)
+ }
+ }),
+ { concurrency: "unbounded" },
+ )
yield* Effect.addFinalizer(() =>
Effect.gen(function* () {
@@ -688,6 +584,29 @@ export const layer = Layer.effect(
}),
)
+ function closeClient(s: State, name: string) {
+ const client = s.clients[name]
+ delete s.defs[name]
+ if (!client) return Effect.void
+ return Effect.tryPromise(() => client.close()).pipe(Effect.ignore)
+ }
+
+ const storeClient = Effect.fnUntraced(function* (
+ s: State,
+ name: string,
+ client: MCPClient,
+ listed: MCPToolDef[],
+ timeout?: number,
+ ) {
+ const bridge = yield* EffectBridge.make()
+ yield* closeClient(s, name)
+ s.status[name] = { status: "connected" }
+ s.clients[name] = client
+ s.defs[name] = listed
+ watch(s, name, client, bridge, timeout)
+ return s.status[name]
+ })
+
const status = Effect.fn("MCP.status")(function* () {
const s = yield* InstanceState.get(state)
@@ -710,15 +629,16 @@ export const layer = Layer.effect(
const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: ConfigMCP.Info) {
const s = yield* InstanceState.get(state)
- const revision = bump(s, name)
- yield* setStatus(s, name, mcp.enabled === false ? { status: "disabled" } : { status: "connecting" })
- const result = yield* createSafely(name, mcp)
- if ((s.revision[name] ?? 0) !== revision) {
- yield* closeCreateResult(result)
- return s.status[name] ?? result.status
+ const result = yield* create(name, mcp)
+
+ s.status[name] = result.status
+ if (!result.mcpClient) {
+ yield* closeClient(s, name)
+ delete s.clients[name]
+ return result.status
}
- return yield* applyCreateResult(s, name, result, mcp.timeout)
+ return yield* storeClient(s, name, result.mcpClient, result.defs!, mcp.timeout)
})
const add = Effect.fn("MCP.add")(function* (name: string, mcp: ConfigMCP.Info) {
@@ -735,10 +655,9 @@ export const layer = Layer.effect(
const disconnect = Effect.fn("MCP.disconnect")(function* (name: string) {
yield* requireMcpConfig(name)
const s = yield* InstanceState.get(state)
- bump(s, name)
yield* closeClient(s, name)
delete s.clients[name]
- yield* setStatus(s, name, { status: "disabled" })
+ s.status[name] = { status: "disabled" }
})
const tools = Effect.fn("MCP.tools")(function* () {
diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts
index a7ff40006d..dddfd9875e 100644
--- a/packages/opencode/test/mcp/lifecycle.test.ts
+++ b/packages/opencode/test/mcp/lifecycle.test.ts
@@ -1,9 +1,7 @@
import { expect, mock, beforeEach } from "bun:test"
import { Cause, Effect, Exit } from "effect"
-import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import type { MCP as MCPNS } from "../../src/mcp/index"
-import { provideInstance, tmpdirScoped } from "../fixture/fixture"
-import { awaitWithTimeout, pollWithTimeout, testEffect } from "../lib/effect"
+import { testEffect } from "../lib/effect"
// --- Mock infrastructure ---
@@ -27,10 +25,6 @@ let lastCreatedClientName: string | undefined
let connectShouldFail = false
let connectShouldHang = false
let connectError = "Mock transport cannot connect"
-let connectHook: (() => Promise) | undefined
-let activeConnects = 0
-let maxActiveConnects = 0
-let connectStarts = 0
// Tracks how many Client instances were created (detects leaks)
let clientCreateCount = 0
// Tracks how many times transport.close() is called across all mock transports
@@ -58,19 +52,6 @@ function getOrCreateClientState(name?: string): MockClientState {
return state
}
-async function runMockConnect() {
- activeConnects++
- connectStarts++
- maxActiveConnects = Math.max(maxActiveConnects, activeConnects)
- try {
- await connectHook?.()
- if (connectShouldHang) return new Promise(() => {}) // never resolves
- if (connectShouldFail) throw new Error(connectError)
- } finally {
- activeConnects--
- }
-}
-
// Mock transport that succeeds or fails based on connectShouldFail / connectShouldHang
class MockStdioTransport {
stderr: null = null
@@ -78,7 +59,8 @@ class MockStdioTransport {
// oxlint-disable-next-line no-useless-constructor
constructor(_opts: any) {}
async start() {
- return runMockConnect()
+ if (connectShouldHang) return new Promise(() => {}) // never resolves
+ if (connectShouldFail) throw new Error(connectError)
}
async close() {
transportCloseCount++
@@ -89,7 +71,8 @@ class MockStreamableHTTP {
// oxlint-disable-next-line no-useless-constructor
constructor(_url: URL, _opts?: any) {}
async start() {
- return runMockConnect()
+ if (connectShouldHang) return new Promise(() => {}) // never resolves
+ if (connectShouldFail) throw new Error(connectError)
}
async close() {
transportCloseCount++
@@ -101,7 +84,8 @@ class MockSSE {
// oxlint-disable-next-line no-useless-constructor
constructor(_url: URL, _opts?: any) {}
async start() {
- return runMockConnect()
+ if (connectShouldHang) return new Promise(() => {}) // never resolves
+ if (connectShouldFail) throw new Error(connectError)
}
async close() {
transportCloseCount++
@@ -189,10 +173,6 @@ beforeEach(() => {
connectShouldFail = false
connectShouldHang = false
connectError = "Mock transport cannot connect"
- connectHook = undefined
- activeConnects = 0
- maxActiveConnects = 0
- connectStarts = 0
clientCreateCount = 0
transportCloseCount = 0
})
@@ -208,91 +188,6 @@ function statusName(status: Record | MCPNS.Status, server:
return status[server]?.status
}
-function deferred() {
- let resolve = () => {}
- const promise = new Promise((done) => {
- resolve = done
- })
- return { promise, resolve }
-}
-
-it.instance(
- "status() returns connecting without waiting for configured startup",
- () =>
- MCP.Service.use((mcp: MCPNS.Interface) =>
- Effect.gen(function* () {
- const connect = deferred()
- connectHook = () => connect.promise
-
- const status = yield* awaitWithTimeout(mcp.status(), "mcp status blocked on startup", "200 millis")
- expect(status["slow-server"]?.status).toBe("connecting")
-
- yield* pollWithTimeout(
- Effect.sync(() => (connectStarts === 1 ? true : undefined)),
- "configured mcp startup did not begin",
- )
-
- connect.resolve()
-
- yield* pollWithTimeout(
- Effect.gen(function* () {
- const next = yield* mcp.status()
- return next["slow-server"]?.status === "connected" ? true : undefined
- }),
- "configured mcp startup did not complete",
- )
- }),
- ),
- {
- config: {
- mcp: {
- "slow-server": {
- type: "local",
- command: ["echo", "test"],
- },
- },
- },
- },
-)
-
-it.live("configured MCP startup runs for one project at a time", () =>
- Effect.gen(function* () {
- const connect = deferred()
- connectHook = () => connect.promise
- const config = {
- mcp: {
- "slow-server": {
- type: "local" as const,
- command: ["echo", "test"],
- },
- },
- }
-
- const first = yield* tmpdirScoped({ config })
- const second = yield* tmpdirScoped({ config })
- const mcp = yield* MCP.Service
-
- yield* mcp.status().pipe(provideInstance(first))
- yield* pollWithTimeout(
- Effect.sync(() => (connectStarts === 1 ? true : undefined)),
- "first configured mcp startup did not begin",
- )
-
- yield* mcp.status().pipe(provideInstance(second))
- yield* Effect.sleep("100 millis")
- expect(connectStarts).toBe(1)
- expect(maxActiveConnects).toBe(1)
-
- connect.resolve()
-
- yield* pollWithTimeout(
- Effect.sync(() => (connectStarts === 2 && activeConnects === 0 ? true : undefined)),
- "second configured mcp startup did not run after first completed",
- )
- expect(maxActiveConnects).toBe(1)
- }).pipe(Effect.provide(CrossSpawnSpawner.defaultLayer)),
-)
-
// ========================================================================
// Test: tools() are cached after connect
// ========================================================================
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index dec92747f4..2199c9afc1 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -29,7 +29,6 @@ export type Event =
| EventSessionIdle
| EventMcpToolsChanged
| EventMcpBrowserOpenFailed
- | EventMcpStatusChanged
| EventCommandExecuted
| EventProjectUpdated
| EventSessionCompacted
@@ -353,40 +352,6 @@ export type SessionStatus =
type: "busy"
}
-export type McpStatusConnected = {
- status: "connected"
-}
-
-export type McpStatusDisabled = {
- status: "disabled"
-}
-
-export type McpStatusConnecting = {
- status: "connecting"
-}
-
-export type McpStatusFailed = {
- status: "failed"
- error: string
-}
-
-export type McpStatusNeedsAuth = {
- status: "needs_auth"
-}
-
-export type McpStatusNeedsClientRegistration = {
- status: "needs_client_registration"
- error: string
-}
-
-export type McpStatus =
- | McpStatusConnected
- | McpStatusDisabled
- | McpStatusConnecting
- | McpStatusFailed
- | McpStatusNeedsAuth
- | McpStatusNeedsClientRegistration
-
export type Project = {
id: string
worktree: string
@@ -865,7 +830,6 @@ export type GlobalEvent = {
| EventSessionIdle
| EventMcpToolsChanged
| EventMcpBrowserOpenFailed
- | EventMcpStatusChanged
| EventCommandExecuted
| EventProjectUpdated
| EventSessionCompacted
@@ -1698,6 +1662,35 @@ export type FormatterStatus = {
enabled: boolean
}
+export type McpStatusConnected = {
+ status: "connected"
+}
+
+export type McpStatusDisabled = {
+ status: "disabled"
+}
+
+export type McpStatusFailed = {
+ status: "failed"
+ error: string
+}
+
+export type McpStatusNeedsAuth = {
+ status: "needs_auth"
+}
+
+export type McpStatusNeedsClientRegistration = {
+ status: "needs_client_registration"
+ error: string
+}
+
+export type McpStatus =
+ | McpStatusConnected
+ | McpStatusDisabled
+ | McpStatusFailed
+ | McpStatusNeedsAuth
+ | McpStatusNeedsClientRegistration
+
export type McpUnsupportedOAuthError = {
error: string
}
@@ -2679,15 +2672,6 @@ export type EventMcpBrowserOpenFailed = {
}
}
-export type EventMcpStatusChanged = {
- id: string
- type: "mcp.status.changed"
- properties: {
- name: string
- status: McpStatus
- }
-}
-
export type EventCommandExecuted = {
id: string
type: "command.executed"