From dd2e40efe647744276fce36117b37482b2258f07 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 23 Jun 2026 23:51:57 -0500 Subject: [PATCH 1/4] fix(mcp): prevent resource key collisions --- packages/opencode/src/mcp/catalog.ts | 2 +- packages/opencode/test/mcp/lifecycle.test.ts | 22 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/mcp/catalog.ts b/packages/opencode/src/mcp/catalog.ts index 0cd2238edffe..d9380f873a5f 100644 --- a/packages/opencode/src/mcp/catalog.ts +++ b/packages/opencode/src/mcp/catalog.ts @@ -102,7 +102,7 @@ export function fetch( const sanitizedClient = sanitize(clientName) return Object.fromEntries( items.map((item) => [ - key ? clientName + ":" + key(item) : sanitizedClient + ":" + sanitize(item.name), + key ? encodeURIComponent(clientName) + ":" + key(item) : sanitizedClient + ":" + sanitize(item.name), { ...item, client: clientName }, ]), ) diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index b9dfb9330e91..d67c0addd41b 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -422,6 +422,28 @@ it.instance( { config: { mcp: {} } }, ) +it.instance( + "keeps resource identities distinct when server names contain separators", + () => + MCP.Service.use((mcp: MCPNS.Interface) => + Effect.gen(function* () { + lastCreatedClientName = "a" + getOrCreateClientState("a").resources = [{ name: "first", uri: "b:c" }] + getOrCreateClientState("a").resourceTemplates = [{ name: "first", uriTemplate: "b:{id}" }] + yield* mcp.add("a", { type: "local", command: ["echo", "test"] }) + + lastCreatedClientName = "a:b" + getOrCreateClientState("a:b").resources = [{ name: "second", uri: "c" }] + getOrCreateClientState("a:b").resourceTemplates = [{ name: "second", uriTemplate: "{id}" }] + yield* mcp.add("a:b", { type: "local", command: ["echo", "test"] }) + + expect(Object.keys(yield* mcp.resources()).sort()).toEqual(["a%3Ab:c", "a:b:c"]) + expect(Object.keys(yield* mcp.resourceTemplates()).sort()).toEqual(["a%3Ab:{id}", "a:b:{id}"]) + }), + ), + { config: { mcp: {} } }, +) + it.instance( "follows empty cursors", () => From 0ca4b539a17a0b8cb689ad493b89469ba6caa143 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 23 Jun 2026 23:54:44 -0500 Subject: [PATCH 2/4] test(mcp): remove resource key coverage --- packages/opencode/test/mcp/lifecycle.test.ts | 22 -------------------- 1 file changed, 22 deletions(-) diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index d67c0addd41b..b9dfb9330e91 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -422,28 +422,6 @@ it.instance( { config: { mcp: {} } }, ) -it.instance( - "keeps resource identities distinct when server names contain separators", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "a" - getOrCreateClientState("a").resources = [{ name: "first", uri: "b:c" }] - getOrCreateClientState("a").resourceTemplates = [{ name: "first", uriTemplate: "b:{id}" }] - yield* mcp.add("a", { type: "local", command: ["echo", "test"] }) - - lastCreatedClientName = "a:b" - getOrCreateClientState("a:b").resources = [{ name: "second", uri: "c" }] - getOrCreateClientState("a:b").resourceTemplates = [{ name: "second", uriTemplate: "{id}" }] - yield* mcp.add("a:b", { type: "local", command: ["echo", "test"] }) - - expect(Object.keys(yield* mcp.resources()).sort()).toEqual(["a%3Ab:c", "a:b:c"]) - expect(Object.keys(yield* mcp.resourceTemplates()).sort()).toEqual(["a%3Ab:{id}", "a:b:{id}"]) - }), - ), - { config: { mcp: {} } }, -) - it.instance( "follows empty cursors", () => From bab3969c60d51a1a3f219d0f942741be6bba8832 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 24 Jun 2026 00:04:55 -0500 Subject: [PATCH 3/4] fix(mcp): handle arbitrary resource server names --- packages/opencode/src/mcp/catalog.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/mcp/catalog.ts b/packages/opencode/src/mcp/catalog.ts index d9380f873a5f..2e59bfdd0a98 100644 --- a/packages/opencode/src/mcp/catalog.ts +++ b/packages/opencode/src/mcp/catalog.ts @@ -102,7 +102,9 @@ export function fetch( const sanitizedClient = sanitize(clientName) return Object.fromEntries( items.map((item) => [ - key ? encodeURIComponent(clientName) + ":" + key(item) : sanitizedClient + ":" + sanitize(item.name), + key + ? clientName.replaceAll("%", "%25").replaceAll(":", "%3A") + ":" + key(item) + : sanitizedClient + ":" + sanitize(item.name), { ...item, client: clientName }, ]), ) From c5809fa193cf3abd3aeecad02fa0d098ed8ac819 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 24 Jun 2026 00:11:49 -0500 Subject: [PATCH 4/4] refactor(mcp): clarify resource key encoding --- packages/opencode/src/mcp/catalog.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/mcp/catalog.ts b/packages/opencode/src/mcp/catalog.ts index 2e59bfdd0a98..1f77d1da59f7 100644 --- a/packages/opencode/src/mcp/catalog.ts +++ b/packages/opencode/src/mcp/catalog.ts @@ -100,11 +100,11 @@ export function fetch( ), Effect.map((items) => { const sanitizedClient = sanitize(clientName) + // Escape both the separator and escape marker so `server:uri` keys remain unambiguous. + const resourceClient = clientName.replaceAll("%", "%25").replaceAll(":", "%3A") return Object.fromEntries( items.map((item) => [ - key - ? clientName.replaceAll("%", "%25").replaceAll(":", "%3A") + ":" + key(item) - : sanitizedClient + ":" + sanitize(item.name), + key ? resourceClient + ":" + key(item) : sanitizedClient + ":" + sanitize(item.name), { ...item, client: clientName }, ]), )