Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions packages/server/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,6 @@ export class Server extends Protocol<ServerContext> {
private _requestStateVerify?: (state: string, ctx: ServerContext) => unknown | Promise<unknown>;
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({
Expand Down Expand Up @@ -793,7 +792,19 @@ export class Server extends Protocol<ServerContext> {
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(
Expand Down Expand Up @@ -879,6 +890,20 @@ export class Server extends Protocol<ServerContext> {
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) {
Expand Down Expand Up @@ -1298,6 +1323,7 @@ export class Server extends Protocol<ServerContext> {
}

async sendResourceUpdated(params: ResourceUpdatedNotification['params']) {
// assertNotificationCapability requires resources.subscribe (#2545).
return this.notification({
method: 'notifications/resources/updated',
params
Expand Down
39 changes: 39 additions & 0 deletions packages/server/test/server/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,43 @@ describe('Server', () => {
expect(result.structuredContent).toEqual({ ok: true });
});
});

describe('resource subscription capabilities (#2545)', () => {
async function connectServer(server: Server): Promise<void> {
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();
});
});
});
Loading