mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-23 21:16:06 +00:00
fix(mcp): invalidate stale startup work
This commit is contained in:
parent
6a477c4cf5
commit
4eab26aaaf
2 changed files with 75 additions and 84 deletions
|
|
@ -610,14 +610,14 @@ export const layer = Layer.effect(
|
|||
|
||||
const startConfigured = Effect.fn("MCP.startConfigured")(function* (
|
||||
s: State,
|
||||
entries: ReadonlyArray<readonly [string, ConfigMCP.Info]>,
|
||||
entries: ReadonlyArray<readonly [string, ConfigMCP.Info, number]>,
|
||||
) {
|
||||
yield* startupLock.withPermits(1)(
|
||||
Effect.forEach(
|
||||
entries,
|
||||
([key, mcp]) =>
|
||||
([key, mcp, revision]) =>
|
||||
Effect.gen(function* () {
|
||||
const revision = s.revision[key] ?? 0
|
||||
if ((s.revision[key] ?? 0) !== revision) return
|
||||
const result = yield* createSafely(key, mcp)
|
||||
if ((s.revision[key] ?? 0) !== revision) {
|
||||
yield* closeCreateResult(result)
|
||||
|
|
@ -654,7 +654,7 @@ export const layer = Layer.effect(
|
|||
}
|
||||
|
||||
s.status[key] = { status: "connecting" }
|
||||
return [[key, mcp] as const]
|
||||
return [[key, mcp, bump(s, key)] as const]
|
||||
})
|
||||
|
||||
if (configured.length > 0) {
|
||||
|
|
|
|||
|
|
@ -216,6 +216,15 @@ function deferred() {
|
|||
return { promise, resolve }
|
||||
}
|
||||
|
||||
const waitConnected = (mcp: MCPNS.Interface, name: string) =>
|
||||
pollWithTimeout(
|
||||
Effect.gen(function* () {
|
||||
const status = yield* mcp.status()
|
||||
return status[name]?.status === "connected" ? true : undefined
|
||||
}),
|
||||
`configured mcp ${name} did not connect`,
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"status() returns connecting without waiting for configured startup",
|
||||
() =>
|
||||
|
|
@ -255,42 +264,60 @@ it.instance(
|
|||
},
|
||||
)
|
||||
|
||||
it.live("configured MCP startup runs for one project at a time", () =>
|
||||
Effect.gen(function* () {
|
||||
const connect = deferred()
|
||||
connectHook = () => connect.promise
|
||||
const config = {
|
||||
it.instance(
|
||||
"configured MCP startup runs for one project at a time",
|
||||
() =>
|
||||
MCP.Service.use((mcp: MCPNS.Interface) =>
|
||||
Effect.gen(function* () {
|
||||
const connect = deferred()
|
||||
connectHook = () => connect.promise
|
||||
const config = {
|
||||
mcp: {
|
||||
"slow-server": {
|
||||
type: "local" as const,
|
||||
command: ["echo", "test"],
|
||||
},
|
||||
},
|
||||
}
|
||||
const second = yield* tmpdirScoped({ config })
|
||||
|
||||
yield* mcp.status()
|
||||
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.gen(function* () {
|
||||
const first = yield* mcp.status()
|
||||
const next = yield* mcp.status().pipe(provideInstance(second))
|
||||
return first["slow-server"]?.status === "connected" && next["slow-server"]?.status === "connected"
|
||||
? true
|
||||
: undefined
|
||||
}),
|
||||
"configured mcp startups did not complete",
|
||||
)
|
||||
expect(connectStarts).toBe(2)
|
||||
expect(maxActiveConnects).toBe(1)
|
||||
}),
|
||||
).pipe(Effect.provide(CrossSpawnSpawner.defaultLayer)),
|
||||
{
|
||||
config: {
|
||||
mcp: {
|
||||
"slow-server": {
|
||||
type: "local" as const,
|
||||
type: "local",
|
||||
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)),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// ========================================================================
|
||||
|
|
@ -377,10 +404,8 @@ it.instance(
|
|||
lastCreatedClientName = "disc-server"
|
||||
getOrCreateClientState("disc-server")
|
||||
|
||||
yield* mcp.add("disc-server", {
|
||||
type: "local",
|
||||
command: ["echo", "test"],
|
||||
})
|
||||
yield* mcp.status()
|
||||
yield* waitConnected(mcp, "disc-server")
|
||||
|
||||
const statusBefore = yield* mcp.status()
|
||||
expect(statusBefore["disc-server"]?.status).toBe("connected")
|
||||
|
|
@ -418,10 +443,8 @@ it.instance(
|
|||
{ name: "my_tool", description: "a tool", inputSchema: { type: "object", properties: {} } },
|
||||
]
|
||||
|
||||
yield* mcp.add("reconn-server", {
|
||||
type: "local",
|
||||
command: ["echo", "test"],
|
||||
})
|
||||
yield* mcp.status()
|
||||
yield* waitConnected(mcp, "reconn-server")
|
||||
|
||||
yield* mcp.disconnect("reconn-server")
|
||||
expect((yield* mcp.status())["reconn-server"]?.status).toBe("disabled")
|
||||
|
|
@ -488,7 +511,7 @@ it.instance(
|
|||
// ========================================================================
|
||||
|
||||
it.instance(
|
||||
"init connects available servers even when one fails",
|
||||
"connects available servers even when one fails",
|
||||
() =>
|
||||
MCP.Service.use((mcp: MCPNS.Interface) =>
|
||||
Effect.gen(function* () {
|
||||
|
|
@ -526,14 +549,8 @@ it.instance(
|
|||
{
|
||||
config: {
|
||||
mcp: {
|
||||
"good-server": {
|
||||
type: "local",
|
||||
command: ["echo", "good"],
|
||||
},
|
||||
"bad-server": {
|
||||
type: "local",
|
||||
command: ["echo", "bad"],
|
||||
},
|
||||
"good-server": { type: "local", command: ["echo", "good"], enabled: false },
|
||||
"bad-server": { type: "local", command: ["echo", "bad"], enabled: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -658,16 +675,7 @@ it.instance(
|
|||
expect(key).toContain("my-prompt")
|
||||
}),
|
||||
),
|
||||
{
|
||||
config: {
|
||||
mcp: {
|
||||
"prompt-server": {
|
||||
type: "local",
|
||||
command: ["echo", "test"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ config: { mcp: {} } },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
|
|
@ -691,16 +699,7 @@ it.instance(
|
|||
expect(key).toContain("my-resource")
|
||||
}),
|
||||
),
|
||||
{
|
||||
config: {
|
||||
mcp: {
|
||||
"resource-server": {
|
||||
type: "local",
|
||||
command: ["echo", "test"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ config: { mcp: {} } },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
|
|
@ -712,10 +711,8 @@ it.instance(
|
|||
const serverState = getOrCreateClientState("prompt-disc-server")
|
||||
serverState.prompts = [{ name: "hidden-prompt", description: "Should not appear" }]
|
||||
|
||||
yield* mcp.add("prompt-disc-server", {
|
||||
type: "local",
|
||||
command: ["echo", "test"],
|
||||
})
|
||||
yield* mcp.status()
|
||||
yield* waitConnected(mcp, "prompt-disc-server")
|
||||
|
||||
yield* mcp.disconnect("prompt-disc-server")
|
||||
|
||||
|
|
@ -824,10 +821,7 @@ it.instance(
|
|||
{
|
||||
config: {
|
||||
mcp: {
|
||||
"fail-connect": {
|
||||
type: "local",
|
||||
command: ["echo", "test"],
|
||||
},
|
||||
"fail-connect": { type: "local", command: ["echo", "test"], enabled: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -895,10 +889,7 @@ it.instance(
|
|||
{
|
||||
config: {
|
||||
mcp: {
|
||||
"my.special-server": {
|
||||
type: "local",
|
||||
command: ["echo", "test"],
|
||||
},
|
||||
"my.special-server": { type: "local", command: ["echo", "test"], enabled: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue