From e510580bc7562a54565612f80164d0d14f314dfb Mon Sep 17 00:00:00 2001 From: James Tippett Date: Fri, 20 Feb 2026 16:06:34 +0700 Subject: [PATCH 01/12] feat(mcp): add resource subscription SDK imports and bus events --- packages/opencode/src/mcp/index.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 3c29fe03d30a..7f8e460f54d7 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -8,6 +8,8 @@ import { CallToolResultSchema, type Tool as MCPToolDef, ToolListChangedNotificationSchema, + ResourceUpdatedNotificationSchema, + ResourceListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js" import { Config } from "../config/config" import { Log } from "../util/log" @@ -54,6 +56,21 @@ export namespace MCP { }), ) + export const ResourceUpdated = BusEvent.define( + "mcp.resource.updated", + z.object({ + server: z.string(), + uri: z.string(), + }), + ) + + export const ResourceListChanged = BusEvent.define( + "mcp.resource.list.changed", + z.object({ + server: z.string(), + }), + ) + export const Failed = NamedError.create( "MCPFailed", z.object({ From 4b420add63021d0b932f01848eb9d2f9344e57ec Mon Sep 17 00:00:00 2001 From: James Tippett Date: Fri, 20 Feb 2026 16:07:54 +0700 Subject: [PATCH 02/12] feat(mcp): add subscription state tracking and notification handlers --- packages/opencode/src/mcp/index.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 7f8e460f54d7..65585220eef0 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -131,6 +131,21 @@ export namespace MCP { log.info("tools list changed notification received", { server: serverName }) Bus.publish(ToolsChanged, { server: serverName }) }) + + client.setNotificationHandler(ResourceUpdatedNotificationSchema, async (notification) => { + const uri = notification.params.uri + log.info("resource updated notification received", { server: serverName, uri }) + Bus.publish(ResourceUpdated, { server: serverName, uri }) + }) + + client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => { + log.info("resource list changed notification received", { server: serverName }) + Bus.publish(ResourceListChanged, { server: serverName }) + }) + } + + function supportsSubscriptions(client: MCPClient): boolean { + return client.getServerCapabilities()?.resources?.subscribe === true } // Convert MCP tool definition to AI SDK Tool type @@ -210,6 +225,7 @@ export namespace MCP { return { status, clients, + subscriptions: new Map>(), } }, async (state) => { From af83a9cb2400550f3f8f29c3fc02a0700d1d5f97 Mon Sep 17 00:00:00 2001 From: James Tippett Date: Fri, 20 Feb 2026 16:09:02 +0700 Subject: [PATCH 03/12] feat(mcp): add subscribe, unsubscribe, and subscriptions functions --- packages/opencode/src/mcp/index.ts | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 65585220eef0..c3075a36f520 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -735,6 +735,82 @@ export namespace MCP { return result } + export async function subscribe(clientName: string, uri: string): Promise { + const clientsSnapshot = await clients() + const client = clientsSnapshot[clientName] + + if (!client) { + log.warn("client not found for subscription", { clientName }) + return false + } + + if (!supportsSubscriptions(client)) { + log.debug("server does not support resource subscriptions", { clientName }) + return false + } + + const s = await state() + const serverSubs = s.subscriptions.get(clientName) + if (serverSubs?.has(uri)) { + return true // already subscribed + } + + try { + await client.subscribeResource({ uri }) + if (!s.subscriptions.has(clientName)) { + s.subscriptions.set(clientName, new Set()) + } + s.subscriptions.get(clientName)!.add(uri) + log.info("subscribed to resource", { clientName, uri }) + return true + } catch (e) { + log.error("failed to subscribe to resource", { + clientName, + uri, + error: e instanceof Error ? e.message : String(e), + }) + return false + } + } + + export async function unsubscribe(clientName: string, uri: string): Promise { + const clientsSnapshot = await clients() + const client = clientsSnapshot[clientName] + const s = await state() + + // Remove from tracking regardless of whether the server call succeeds + s.subscriptions.get(clientName)?.delete(uri) + + if (!client) { + log.warn("client not found for unsubscription", { clientName }) + return false + } + + try { + await client.unsubscribeResource({ uri }) + log.info("unsubscribed from resource", { clientName, uri }) + return true + } catch (e) { + log.error("failed to unsubscribe from resource", { + clientName, + uri, + error: e instanceof Error ? e.message : String(e), + }) + return false + } + } + + export async function subscriptions(): Promise> { + const s = await state() + const result: Record = {} + for (const [server, uris] of s.subscriptions) { + if (uris.size > 0) { + result[server] = [...uris] + } + } + return result + } + /** * Start OAuth authentication flow for an MCP server. * Returns the authorization URL that should be opened in a browser. From 15ceb8b19435fbc7c0e13c0841bc086b90f068f7 Mon Sep 17 00:00:00 2001 From: James Tippett Date: Fri, 20 Feb 2026 16:09:51 +0700 Subject: [PATCH 04/12] feat(mcp): auto-subscribe to resources on read --- packages/opencode/src/mcp/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index c3075a36f520..eba1e3547e9f 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -732,6 +732,11 @@ export namespace MCP { return undefined }) + // Auto-subscribe to the resource for update notifications (fire-and-forget) + if (result) { + subscribe(clientName, resourceUri).catch(() => {}) + } + return result } From dea6809d01f81300547701c99539066a109e2c69 Mon Sep 17 00:00:00 2001 From: James Tippett Date: Fri, 20 Feb 2026 16:10:43 +0700 Subject: [PATCH 05/12] feat(config): add subscriptions field to MCP server config schemas --- packages/opencode/src/config/config.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 31188471991c..fda9fa2745a2 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -546,6 +546,10 @@ export namespace Config { .positive() .optional() .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), + subscriptions: z + .array(z.string()) + .optional() + .describe("Resource URIs to automatically subscribe to for update notifications"), }) .strict() .meta({ @@ -585,6 +589,10 @@ export namespace Config { .positive() .optional() .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), + subscriptions: z + .array(z.string()) + .optional() + .describe("Resource URIs to automatically subscribe to for update notifications"), }) .strict() .meta({ From 16618753dc33a383530f400b27830f5f7157b22e Mon Sep 17 00:00:00 2001 From: James Tippett Date: Fri, 20 Feb 2026 16:12:14 +0700 Subject: [PATCH 06/12] feat(mcp): subscribe to config-based resources on server connect --- packages/opencode/src/mcp/index.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index eba1e3547e9f..01737d8e2c62 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -222,10 +222,38 @@ export namespace MCP { } }), ) + const subscriptionMap = new Map>() + + // Set up config-based subscriptions for connected clients + for (const [key, client] of Object.entries(clients)) { + if (status[key]?.status !== "connected") continue + const mcp = config[key] + if (!isMcpConfigured(mcp) || !mcp.subscriptions?.length) continue + if (!supportsSubscriptions(client)) continue + + const uris = new Set() + for (const uri of mcp.subscriptions) { + client + .subscribeResource({ uri }) + .then(() => { + uris.add(uri) + log.info("subscribed to config resource", { key, uri }) + }) + .catch((e) => { + log.error("failed to subscribe to config resource", { + key, + uri, + error: e instanceof Error ? e.message : String(e), + }) + }) + } + subscriptionMap.set(key, uris) + } + return { status, clients, - subscriptions: new Map>(), + subscriptions: subscriptionMap, } }, async (state) => { From 94c85a085fb7048b0dad89d658fca99713ead37b Mon Sep 17 00:00:00 2001 From: James Tippett Date: Fri, 20 Feb 2026 16:14:39 +0700 Subject: [PATCH 07/12] feat(mcp): cleanup subscriptions on disconnect, re-subscribe on reconnect --- packages/opencode/src/mcp/index.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 01737d8e2c62..914572c5e0c2 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -609,6 +609,31 @@ export namespace MCP { }) } s.clients[name] = result.mcpClient + + // Re-subscribe to previously tracked resources and config-based subscriptions + const previousSubs = s.subscriptions.get(name) ?? new Set() + const configSubs = new Set(isMcpConfigured(mcp) && mcp.subscriptions ? mcp.subscriptions : []) + const allSubs = new Set([...previousSubs, ...configSubs]) + + if (allSubs.size > 0 && supportsSubscriptions(result.mcpClient)) { + const activeSubs = new Set() + for (const uri of allSubs) { + result.mcpClient + .subscribeResource({ uri }) + .then(() => { + activeSubs.add(uri) + log.info("re-subscribed to resource", { name, uri }) + }) + .catch((e) => { + log.error("failed to re-subscribe to resource", { + name, + uri, + error: e instanceof Error ? e.message : String(e), + }) + }) + } + s.subscriptions.set(name, activeSubs) + } } } @@ -621,6 +646,7 @@ export namespace MCP { }) delete s.clients[name] } + s.subscriptions.delete(name) s.status[name] = { status: "disabled" } } From d8f9a3b32575b0d1c4c7307f388086d8af443632 Mon Sep 17 00:00:00 2001 From: James Tippett Date: Fri, 20 Feb 2026 16:17:31 +0700 Subject: [PATCH 08/12] feat(tui): add toast notifications and AI triggering for resource updates --- packages/opencode/src/cli/cmd/tui/app.tsx | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index ab3d09689252..25a93d31b793 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -6,6 +6,7 @@ import { RouteProvider, useRoute } from "@tui/context/route" import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js" import { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "./win32" import { Installation } from "@/installation" +import { MCP } from "@/mcp" import { Flag } from "@/flag/flag" import { DialogProvider, useDialog } from "@tui/ui/dialog" import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider" @@ -729,6 +730,44 @@ function App() { }) }) + sdk.event.on(MCP.ResourceUpdated.type, (evt) => { + const { server, uri } = evt.properties + toast.show({ + title: "Resource Updated", + message: `${uri} (${server})`, + variant: "info", + duration: 5000, + }) + + // If current session is idle, trigger AI with updated resource info + if (route.data.type === "session") { + const sessionID = route.data.sessionID + const status = sync.data.session_status?.[sessionID] + if (!status || status.type === "idle") { + sdk.client.session + .prompt({ + sessionID, + parts: [ + { + type: "text" as const, + text: `[System: MCP resource updated]\nResource "${uri}" from server "${server}" has been updated. Please review the changes.`, + }, + ], + }) + .catch(() => {}) + } + } + }) + + sdk.event.on(MCP.ResourceListChanged.type, (evt) => { + toast.show({ + title: "MCP Resources Changed", + message: `Server "${evt.properties.server}" resource list updated`, + variant: "info", + duration: 3000, + }) + }) + return ( Date: Fri, 20 Feb 2026 16:24:55 +0700 Subject: [PATCH 09/12] fix(mcp): address code review findings - Fix race condition in subscription tracking: use optimistic set population (populate immediately, delete on failure) instead of populating in async .then() callbacks after set is already stored - Add supportsSubscriptions() guard to unsubscribe() to avoid errors against servers that don't support subscriptions - Fix misleading "prompt" log messages in readResource to say "resource" - Remove unnecessary "as const" cast in TUI event handler --- packages/opencode/src/cli/cmd/tui/app.tsx | 2 +- packages/opencode/src/mcp/index.ts | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 25a93d31b793..a341fe1b2d9d 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -749,7 +749,7 @@ function App() { sessionID, parts: [ { - type: "text" as const, + type: "text", text: `[System: MCP resource updated]\nResource "${uri}" from server "${server}" has been updated. Please review the changes.`, }, ], diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 914572c5e0c2..b27cac9edc5d 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -231,15 +231,15 @@ export namespace MCP { if (!isMcpConfigured(mcp) || !mcp.subscriptions?.length) continue if (!supportsSubscriptions(client)) continue - const uris = new Set() + const uris = new Set(mcp.subscriptions) for (const uri of mcp.subscriptions) { client .subscribeResource({ uri }) .then(() => { - uris.add(uri) log.info("subscribed to config resource", { key, uri }) }) .catch((e) => { + uris.delete(uri) log.error("failed to subscribe to config resource", { key, uri, @@ -616,15 +616,15 @@ export namespace MCP { const allSubs = new Set([...previousSubs, ...configSubs]) if (allSubs.size > 0 && supportsSubscriptions(result.mcpClient)) { - const activeSubs = new Set() + const activeSubs = new Set(allSubs) for (const uri of allSubs) { result.mcpClient .subscribeResource({ uri }) .then(() => { - activeSubs.add(uri) log.info("re-subscribed to resource", { name, uri }) }) .catch((e) => { + activeSubs.delete(uri) log.error("failed to re-subscribe to resource", { name, uri, @@ -767,7 +767,7 @@ export namespace MCP { const client = clientsSnapshot[clientName] if (!client) { - log.warn("client not found for prompt", { + log.warn("client not found for resource", { clientName: clientName, }) return undefined @@ -778,7 +778,7 @@ export namespace MCP { uri: resourceUri, }) .catch((e) => { - log.error("failed to get prompt from MCP server", { + log.error("failed to read resource from MCP server", { clientName: clientName, resourceUri: resourceUri, error: e.message, @@ -845,6 +845,10 @@ export namespace MCP { return false } + if (!supportsSubscriptions(client)) { + return true // already removed from tracking above + } + try { await client.unsubscribeResource({ uri }) log.info("unsubscribed from resource", { clientName, uri }) From 481851e0dcfd4d2f209589068021fe528f12e0ed Mon Sep 17 00:00:00 2001 From: James Tippett Date: Fri, 20 Feb 2026 16:34:17 +0700 Subject: [PATCH 10/12] style: align MCP subscription code with project style guide Convert try/catch to .catch(), remove destructuring, use functional array methods over for loops, and inline single-use variables. --- packages/opencode/src/cli/cmd/tui/app.tsx | 10 +- packages/opencode/src/mcp/index.ts | 147 +++++++++++----------- 2 files changed, 78 insertions(+), 79 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index a341fe1b2d9d..0056cf31f4cb 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -731,26 +731,24 @@ function App() { }) sdk.event.on(MCP.ResourceUpdated.type, (evt) => { - const { server, uri } = evt.properties toast.show({ title: "Resource Updated", - message: `${uri} (${server})`, + message: `${evt.properties.uri} (${evt.properties.server})`, variant: "info", duration: 5000, }) // If current session is idle, trigger AI with updated resource info if (route.data.type === "session") { - const sessionID = route.data.sessionID - const status = sync.data.session_status?.[sessionID] + const status = sync.data.session_status?.[route.data.sessionID] if (!status || status.type === "idle") { sdk.client.session .prompt({ - sessionID, + sessionID: route.data.sessionID, parts: [ { type: "text", - text: `[System: MCP resource updated]\nResource "${uri}" from server "${server}" has been updated. Please review the changes.`, + text: `[System: MCP resource updated]\nResource "${evt.properties.uri}" from server "${evt.properties.server}" has been updated. Please review the changes.`, }, ], }) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index b27cac9edc5d..792bfbde1b32 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -225,30 +225,32 @@ export namespace MCP { const subscriptionMap = new Map>() // Set up config-based subscriptions for connected clients - for (const [key, client] of Object.entries(clients)) { - if (status[key]?.status !== "connected") continue - const mcp = config[key] - if (!isMcpConfigured(mcp) || !mcp.subscriptions?.length) continue - if (!supportsSubscriptions(client)) continue - - const uris = new Set(mcp.subscriptions) - for (const uri of mcp.subscriptions) { - client - .subscribeResource({ uri }) - .then(() => { - log.info("subscribed to config resource", { key, uri }) - }) - .catch((e) => { - uris.delete(uri) - log.error("failed to subscribe to config resource", { - key, - uri, - error: e instanceof Error ? e.message : String(e), + Object.entries(clients) + .filter(([key]) => status[key]?.status === "connected") + .filter(([key]) => { + const mcp = config[key] + return isMcpConfigured(mcp) && mcp.subscriptions?.length && supportsSubscriptions(clients[key]) + }) + .forEach(([key]) => { + const mcp = config[key] as Config.Mcp + const uris = new Set(mcp.subscriptions!) + mcp.subscriptions!.forEach((uri) => { + clients[key] + .subscribeResource({ uri }) + .then(() => { + log.info("subscribed to config resource", { key, uri }) }) - }) - } - subscriptionMap.set(key, uris) - } + .catch((e) => { + uris.delete(uri) + log.error("failed to subscribe to config resource", { + key, + uri, + error: e instanceof Error ? e.message : String(e), + }) + }) + }) + subscriptionMap.set(key, uris) + }) return { status, @@ -611,28 +613,28 @@ export namespace MCP { s.clients[name] = result.mcpClient // Re-subscribe to previously tracked resources and config-based subscriptions - const previousSubs = s.subscriptions.get(name) ?? new Set() - const configSubs = new Set(isMcpConfigured(mcp) && mcp.subscriptions ? mcp.subscriptions : []) - const allSubs = new Set([...previousSubs, ...configSubs]) - - if (allSubs.size > 0 && supportsSubscriptions(result.mcpClient)) { - const activeSubs = new Set(allSubs) - for (const uri of allSubs) { - result.mcpClient + const tracked = new Set([ + ...(s.subscriptions.get(name) ?? []), + ...(isMcpConfigured(mcp) && mcp.subscriptions ? mcp.subscriptions : []), + ]) + + if (tracked.size > 0 && supportsSubscriptions(result.mcpClient)) { + ;[...tracked].forEach((uri) => { + result.mcpClient! .subscribeResource({ uri }) .then(() => { log.info("re-subscribed to resource", { name, uri }) }) .catch((e) => { - activeSubs.delete(uri) + tracked.delete(uri) log.error("failed to re-subscribe to resource", { name, uri, error: e instanceof Error ? e.message : String(e), }) }) - } - s.subscriptions.set(name, activeSubs) + }) + s.subscriptions.set(name, tracked) } } } @@ -795,8 +797,7 @@ export namespace MCP { } export async function subscribe(clientName: string, uri: string): Promise { - const clientsSnapshot = await clients() - const client = clientsSnapshot[clientName] + const client = (await clients())[clientName] if (!client) { log.warn("client not found for subscription", { clientName }) @@ -809,32 +810,32 @@ export namespace MCP { } const s = await state() - const serverSubs = s.subscriptions.get(clientName) - if (serverSubs?.has(uri)) { + if (s.subscriptions.get(clientName)?.has(uri)) { return true // already subscribed } - try { - await client.subscribeResource({ uri }) - if (!s.subscriptions.has(clientName)) { - s.subscriptions.set(clientName, new Set()) - } - s.subscriptions.get(clientName)!.add(uri) - log.info("subscribed to resource", { clientName, uri }) - return true - } catch (e) { - log.error("failed to subscribe to resource", { - clientName, - uri, - error: e instanceof Error ? e.message : String(e), + return client + .subscribeResource({ uri }) + .then(() => { + if (!s.subscriptions.has(clientName)) { + s.subscriptions.set(clientName, new Set()) + } + s.subscriptions.get(clientName)!.add(uri) + log.info("subscribed to resource", { clientName, uri }) + return true + }) + .catch((e) => { + log.error("failed to subscribe to resource", { + clientName, + uri, + error: e instanceof Error ? e.message : String(e), + }) + return false }) - return false - } } export async function unsubscribe(clientName: string, uri: string): Promise { - const clientsSnapshot = await clients() - const client = clientsSnapshot[clientName] + const client = (await clients())[clientName] const s = await state() // Remove from tracking regardless of whether the server call succeeds @@ -849,29 +850,29 @@ export namespace MCP { return true // already removed from tracking above } - try { - await client.unsubscribeResource({ uri }) - log.info("unsubscribed from resource", { clientName, uri }) - return true - } catch (e) { - log.error("failed to unsubscribe from resource", { - clientName, - uri, - error: e instanceof Error ? e.message : String(e), + return client + .unsubscribeResource({ uri }) + .then(() => { + log.info("unsubscribed from resource", { clientName, uri }) + return true + }) + .catch((e) => { + log.error("failed to unsubscribe from resource", { + clientName, + uri, + error: e instanceof Error ? e.message : String(e), + }) + return false }) - return false - } } export async function subscriptions(): Promise> { const s = await state() - const result: Record = {} - for (const [server, uris] of s.subscriptions) { - if (uris.size > 0) { - result[server] = [...uris] - } - } - return result + return Object.fromEntries( + [...s.subscriptions] + .filter(([, uris]) => uris.size > 0) + .map(([server, uris]) => [server, [...uris]]), + ) } /** From 2928fd6b180b1126b3e9e5539b22e75226e14d25 Mon Sep 17 00:00:00 2001 From: James Tippett Date: Sat, 21 Feb 2026 00:07:49 +0700 Subject: [PATCH 11/12] feat(mcp): auto-subscribe to all server resources and add autoprompt config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auto-subscribe to all listed resources on connect for servers that support subscriptions, not just config-specified URIs - Subscribe to newly listed resources when ResourceListChanged fires - On reconnect, merge previous subs + config + fresh listResources() - Add `autoprompt` config option (default false) to trigger AI when a subscribed resource updates — uses system prompt for instructions and creates a new session if not already in one --- packages/opencode/src/cli/cmd/tui/app.tsx | 42 ++++--- packages/opencode/src/config/config.ts | 8 ++ packages/opencode/src/mcp/index.ts | 131 ++++++++++++++-------- 3 files changed, 120 insertions(+), 61 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 0056cf31f4cb..42aef1908249 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -738,21 +738,37 @@ function App() { duration: 5000, }) - // If current session is idle, trigger AI with updated resource info - if (route.data.type === "session") { - const status = sync.data.session_status?.[route.data.sessionID] - if (!status || status.type === "idle") { + // If autoprompt is enabled for this server, trigger AI with updated resource info + const mcp = sync.data.config.mcp?.[evt.properties.server] + if (mcp && typeof mcp === "object" && "autoprompt" in mcp && mcp.autoprompt) { + const prompt = { + system: `An MCP resource has been updated. Resource URI: "${evt.properties.uri}" from server "${evt.properties.server}". Read the resource to review the latest content and take appropriate action.`, + parts: [ + { + type: "text" as const, + text: `Resource updated: ${evt.properties.uri} (${evt.properties.server})`, + }, + ], + } + if (route.data.type === "session") { + const status = sync.data.session_status?.[route.data.sessionID] + if (!status || status.type === "idle") { + sdk.client.session + .promptAsync({ sessionID: route.data.sessionID, ...prompt }) + .catch((e) => console.error("failed to trigger AI for resource update", e)) + } + } else { sdk.client.session - .prompt({ - sessionID: route.data.sessionID, - parts: [ - { - type: "text", - text: `[System: MCP resource updated]\nResource "${evt.properties.uri}" from server "${evt.properties.server}" has been updated. Please review the changes.`, - }, - ], + .create({}) + .then((res) => { + const id = res.data?.id + if (!id) return + route.navigate({ type: "session", sessionID: id }) + sdk.client.session + .promptAsync({ sessionID: id, ...prompt }) + .catch((e) => console.error("failed to trigger AI for resource update", e)) }) - .catch(() => {}) + .catch((e) => console.error("failed to create session for resource update", e)) } } }) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index fda9fa2745a2..b1fcaed979cf 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -550,6 +550,10 @@ export namespace Config { .array(z.string()) .optional() .describe("Resource URIs to automatically subscribe to for update notifications"), + autoprompt: z + .boolean() + .optional() + .describe("Automatically prompt the AI when a subscribed resource is updated. Defaults to false."), }) .strict() .meta({ @@ -593,6 +597,10 @@ export namespace Config { .array(z.string()) .optional() .describe("Resource URIs to automatically subscribe to for update notifications"), + autoprompt: z + .boolean() + .optional() + .describe("Automatically prompt the AI when a subscribed resource is updated. Defaults to false."), }) .strict() .meta({ diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 792bfbde1b32..4f44d2ba614c 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -141,6 +141,30 @@ export namespace MCP { client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => { log.info("resource list changed notification received", { server: serverName }) Bus.publish(ResourceListChanged, { server: serverName }) + // Subscribe to any newly listed resources + if (supportsSubscriptions(client)) { + const s = await state() + const existing = s.subscriptions.get(serverName) ?? new Set() + const listed = await client.listResources().catch((e) => { + log.warn("failed to list resources after list changed", { server: serverName, error: e instanceof Error ? e.message : String(e) }) + return undefined + }) + listed?.resources + .filter((r) => !existing.has(r.uri)) + .forEach((r) => { + existing.add(r.uri) + client + .subscribeResource({ uri: r.uri }) + .then(() => { + log.info("subscribed to new resource", { server: serverName, uri: r.uri }) + }) + .catch((e) => { + existing.delete(r.uri) + log.error("failed to subscribe to new resource", { server: serverName, uri: r.uri, error: e instanceof Error ? e.message : String(e) }) + }) + }) + if (existing.size > 0) s.subscriptions.set(serverName, existing) + } }) } @@ -224,33 +248,37 @@ export namespace MCP { ) const subscriptionMap = new Map>() - // Set up config-based subscriptions for connected clients - Object.entries(clients) - .filter(([key]) => status[key]?.status === "connected") - .filter(([key]) => { - const mcp = config[key] - return isMcpConfigured(mcp) && mcp.subscriptions?.length && supportsSubscriptions(clients[key]) - }) - .forEach(([key]) => { - const mcp = config[key] as Config.Mcp - const uris = new Set(mcp.subscriptions!) - mcp.subscriptions!.forEach((uri) => { - clients[key] - .subscribeResource({ uri }) - .then(() => { - log.info("subscribed to config resource", { key, uri }) - }) - .catch((e) => { - uris.delete(uri) - log.error("failed to subscribe to config resource", { - key, - uri, - error: e instanceof Error ? e.message : String(e), + // Auto-subscribe to all resources for servers that support subscriptions + await Promise.all( + Object.entries(clients) + .filter(([key]) => status[key]?.status === "connected" && supportsSubscriptions(clients[key])) + .map(async ([key, client]) => { + const mcp = config[key] + const cfg = isMcpConfigured(mcp) && mcp.subscriptions ? mcp.subscriptions : [] + const listed = await client.listResources().catch((e) => { + log.warn("failed to list resources for auto-subscribe", { key, error: e instanceof Error ? e.message : String(e) }) + return undefined + }) + const uris = new Set([...cfg, ...(listed?.resources.map((r) => r.uri) ?? [])]) + if (uris.size === 0) return + uris.forEach((uri) => { + client + .subscribeResource({ uri }) + .then(() => { + log.info("auto-subscribed to resource", { key, uri }) }) - }) - }) - subscriptionMap.set(key, uris) - }) + .catch((e) => { + uris.delete(uri) + log.error("failed to auto-subscribe to resource", { + key, + uri, + error: e instanceof Error ? e.message : String(e), + }) + }) + }) + subscriptionMap.set(key, uris) + }), + ) return { status, @@ -612,29 +640,36 @@ export namespace MCP { } s.clients[name] = result.mcpClient - // Re-subscribe to previously tracked resources and config-based subscriptions - const tracked = new Set([ - ...(s.subscriptions.get(name) ?? []), - ...(isMcpConfigured(mcp) && mcp.subscriptions ? mcp.subscriptions : []), - ]) - - if (tracked.size > 0 && supportsSubscriptions(result.mcpClient)) { - ;[...tracked].forEach((uri) => { - result.mcpClient! - .subscribeResource({ uri }) - .then(() => { - log.info("re-subscribed to resource", { name, uri }) - }) - .catch((e) => { - tracked.delete(uri) - log.error("failed to re-subscribe to resource", { - name, - uri, - error: e instanceof Error ? e.message : String(e), - }) - }) + // Re-subscribe: merge previous subscriptions, config URIs, and freshly listed resources + if (supportsSubscriptions(result.mcpClient)) { + const cfg = isMcpConfigured(mcp) && mcp.subscriptions ? mcp.subscriptions : [] + const listed = await result.mcpClient.listResources().catch((e) => { + log.warn("failed to list resources on reconnect", { name, error: e instanceof Error ? e.message : String(e) }) + return undefined }) - s.subscriptions.set(name, tracked) + const tracked = new Set([ + ...(s.subscriptions.get(name) ?? []), + ...cfg, + ...(listed?.resources.map((r) => r.uri) ?? []), + ]) + if (tracked.size > 0) { + tracked.forEach((uri) => { + result.mcpClient! + .subscribeResource({ uri }) + .then(() => { + log.info("re-subscribed to resource", { name, uri }) + }) + .catch((e) => { + tracked.delete(uri) + log.error("failed to re-subscribe to resource", { + name, + uri, + error: e instanceof Error ? e.message : String(e), + }) + }) + }) + s.subscriptions.set(name, tracked) + } } } } From bcf1df52bc167a7132577542c8b0d8904ed11853 Mon Sep 17 00:00:00 2001 From: James Tippett Date: Sat, 21 Feb 2026 00:26:54 +0700 Subject: [PATCH 12/12] chore(sdk): regenerate SDK types for new MCP resource events --- packages/sdk/js/src/v2/gen/types.gen.ts | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index efb7e202e120..416aaedc71e0 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -784,6 +784,21 @@ export type EventMcpBrowserOpenFailed = { } } +export type EventMcpResourceUpdated = { + type: "mcp.resource.updated" + properties: { + server: string + uri: string + } +} + +export type EventMcpResourceListChanged = { + type: "mcp.resource.list.changed" + properties: { + server: string + } +} + export type EventCommandExecuted = { type: "command.executed" properties: { @@ -972,6 +987,8 @@ export type Event = | EventTuiSessionSelect | EventMcpToolsChanged | EventMcpBrowserOpenFailed + | EventMcpResourceUpdated + | EventMcpResourceListChanged | EventCommandExecuted | EventSessionCreated | EventSessionUpdated @@ -1616,6 +1633,14 @@ export type McpLocalConfig = { * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. */ timeout?: number + /** + * Resource URIs to automatically subscribe to for update notifications + */ + subscriptions?: Array + /** + * Automatically prompt the AI when a subscribed resource is updated. Defaults to false. + */ + autoprompt?: boolean } export type McpOAuthConfig = { @@ -1660,6 +1685,14 @@ export type McpRemoteConfig = { * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. */ timeout?: number + /** + * Resource URIs to automatically subscribe to for update notifications + */ + subscriptions?: Array + /** + * Automatically prompt the AI when a subscribed resource is updated. Defaults to false. + */ + autoprompt?: boolean } /**