diff --git a/packages/server/src/server/server.ts b/packages/server/src/server/server.ts index 1582f8c6eb..ce5aa35f3b 100644 --- a/packages/server/src/server/server.ts +++ b/packages/server/src/server/server.ts @@ -289,7 +289,6 @@ export class Server extends Protocol { private _requestStateVerify?: (state: string, ctx: ServerContext) => unknown | Promise; private _inputRequiredServing: { maxRounds: number; roundTimeoutMs: number; legacyShim: boolean }; private _legacyShim?: LegacyInputRequiredShim; - /** Lazily-built legacy shim; the loop lives in legacyInputRequiredShim.ts behind a narrow host contract. */ private _legacyInputRequiredShim(): LegacyInputRequiredShim { return (this._legacyShim ??= new LegacyInputRequiredShim({ @@ -793,7 +792,19 @@ export class Server extends Protocol { break; } - case 'notifications/resources/updated': + case 'notifications/resources/updated': { + // Resource updates only reach clients that opted in via + // resources/subscribe, which requires the advertised + // resources.subscribe capability (#2545). + if (!this._capabilities.resources?.subscribe) { + throw new SdkError( + SdkErrorCode.CapabilityNotSupported, + `Server does not support resource subscriptions (required for ${method})` + ); + } + break; + } + case 'notifications/resources/list_changed': { if (!this._capabilities.resources) { throw new SdkError( @@ -879,6 +890,20 @@ export class Server extends Protocol { break; } + case 'resources/subscribe': + case 'resources/unsubscribe': { + // Handler registration must match the advertised bit; otherwise + // clients that subscribe (or the server's own handlers) see a + // silent dead-end when the capability was never declared (#2545). + if (!this._capabilities.resources?.subscribe) { + throw new SdkError( + SdkErrorCode.CapabilityNotSupported, + `Server does not support resource subscriptions (required for ${method})` + ); + } + break; + } + case 'tools/call': case 'tools/list': { if (!this._capabilities.tools) { @@ -1298,6 +1323,7 @@ export class Server extends Protocol { } async sendResourceUpdated(params: ResourceUpdatedNotification['params']) { + // assertNotificationCapability requires resources.subscribe (#2545). return this.notification({ method: 'notifications/resources/updated', params diff --git a/packages/server/test/server/server.test.ts b/packages/server/test/server/server.test.ts index 0a96a0bb66..4070e5c85e 100644 --- a/packages/server/test/server/server.test.ts +++ b/packages/server/test/server/server.test.ts @@ -242,4 +242,43 @@ describe('Server', () => { expect(result.structuredContent).toEqual({ ok: true }); }); }); + + describe('resource subscription capabilities (#2545)', () => { + async function connectServer(server: Server): Promise { + const [, serverTransport] = InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + } + + it('sendResourceUpdated throws without resources.subscribe', async () => { + const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: { resources: { listChanged: true } } }); + await connectServer(server); + + await expect(server.sendResourceUpdated({ uri: 'test://resource' })).rejects.toThrow(/resource subscriptions/); + }); + + it('sendResourceUpdated succeeds when resources.subscribe is advertised', async () => { + const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: { resources: { subscribe: true } } }); + await connectServer(server); + + await expect(server.sendResourceUpdated({ uri: 'test://resource' })).resolves.toBeUndefined(); + }); + + it('setRequestHandler rejects resources/subscribe without the capability', () => { + const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: { resources: { listChanged: true } } }); + + expect(() => server.setRequestHandler('resources/subscribe', async () => ({}))).toThrow(/resource subscriptions/); + }); + + it('setRequestHandler rejects resources/unsubscribe without the capability', () => { + const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: { resources: {} } }); + + expect(() => server.setRequestHandler('resources/unsubscribe', async () => ({}))).toThrow(/resource subscriptions/); + }); + + it('setRequestHandler allows resources/subscribe when the capability is advertised', () => { + const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: { resources: { subscribe: true } } }); + + expect(() => server.setRequestHandler('resources/subscribe', async () => ({}))).not.toThrow(); + }); + }); });