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
5 changes: 5 additions & 0 deletions .changeset/sampling-context-capability-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/server': patch
---

`Server.createMessage()` (and `ctx.mcpReq.requestSampling()`, which calls it) now refuses `includeContext: 'thisServer'` or `'allServers'` when the client did not declare the `sampling.context` capability, throwing `SdkError` with `SdkErrorCode.CapabilityNotSupported` before anything is sent, as it already does for `tools` without `sampling.tools`. The 2025-11-25 spec says servers SHOULD only use these values when the client declares `sampling.context`. A server that sends them to such a client must now omit `includeContext` or use `'none'`. Connections negotiated at a revision before 2025-11-25, which has no `sampling.context` capability, are unaffected.
9 changes: 9 additions & 0 deletions packages/server/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,15 @@ export class Server extends Protocol<ServerContext> {
if ((params.tools || params.toolChoice) && !this._clientCapabilities?.sampling?.tools) {
throw new SdkError(SdkErrorCode.CapabilityNotSupported, 'Client does not support sampling tools capability.');
}
// sampling.context is a 2025-11-25 capability: older revisions allow thisServer/allServers without it
const predatesContextCapability = this._negotiatedProtocolVersion !== undefined && this._negotiatedProtocolVersion < '2025-11-25';
if (
(params.includeContext === 'thisServer' || params.includeContext === 'allServers') &&
!predatesContextCapability &&
!this._clientCapabilities?.sampling?.context
) {
throw new SdkError(SdkErrorCode.CapabilityNotSupported, 'Client does not support sampling context capability.');
}

// Message structure validation - always validate tool_use/tool_result pairs.
// These may appear even without tools/toolChoice in the current request when
Expand Down
7 changes: 1 addition & 6 deletions test/e2e/requirements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1113,12 +1113,7 @@ export const REQUIREMENTS: Record<string, Requirement> = {
behavior:
'The server does not use includeContext values thisServer or allServers unless the client declared the sampling.context capability.',
transports: ['inMemory', 'stdio', 'streamableHttp'],
note: 'Stateless hosting creates a fresh server per request and has no standalone GET stream, so there is no server→client channel to deliver/observe these.',
knownFailures: [
{
note: "Server.createMessage (src/server/index.ts ~line 497) only gates tools/toolChoice on clientCapabilities.sampling.tools; there is no check of clientCapabilities.sampling.context for includeContext 'thisServer'/'allServers', so the request reaches the client callback instead of being refused."
}
]
note: 'Stateless hosting creates a fresh server per request and has no standalone GET stream, so there is no server→client channel to deliver/observe these.'
},
'sampling:create:not-supported': {
source: 'https://modelcontextprotocol.io/specification/2025-11-25/client/sampling#capabilities',
Expand Down
3 changes: 2 additions & 1 deletion test/e2e/scenarios/sampling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ verifies('sampling:create:basic', async ({ transport }: TestArgs) => {

verifies('sampling:create:include-context', async ({ transport }: TestArgs) => {
const received: CreateMessageRequest[] = [];
const client = newClient();
// thisServer and allServers are only sent to a client that declares sampling.context.
const client = newClient({ sampling: { context: {} } });
client.setRequestHandler('sampling/createMessage', async req => {
received.push(req);
return { model: 'm', role: 'assistant', stopReason: 'endTurn', content: { type: 'text', text: 'ok' } };
Expand Down
54 changes: 54 additions & 0 deletions test/integration/test/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1540,6 +1540,60 @@ describe('createMessage validation', () => {
).rejects.toThrow('Client does not support sampling tools capability.');
});

test('should throw when includeContext thisServer or allServers is used without sampling.context capability', async () => {
const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });

const client = new Client(
{ name: 'test client', version: '1.0' },
{ capabilities: { sampling: {} } } // No context capability
);

client.setRequestHandler('sampling/createMessage', async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Response' }
}));

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);

for (const includeContext of ['thisServer', 'allServers'] as const) {
await expect(
server.createMessage({
messages: [{ role: 'user', content: { type: 'text', text: 'hello' } }],
maxTokens: 100,
includeContext
})
).rejects.toThrow('Client does not support sampling context capability.');
}
});

test('should allow includeContext thisServer on a 2025-06-18 connection, which predates sampling.context', async () => {
const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });

const client = new Client(
{ name: 'test client', version: '1.0' },
{ capabilities: { sampling: {} }, supportedProtocolVersions: ['2025-06-18'] }
);

client.setRequestHandler('sampling/createMessage', async request => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: `includeContext=${request.params.includeContext}` }
}));

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
expect(client.getNegotiatedProtocolVersion()).toBe('2025-06-18');

const result = await server.createMessage({
messages: [{ role: 'user', content: { type: 'text', text: 'hello' } }],
maxTokens: 100,
includeContext: 'thisServer'
});
expect(result.content).toEqual({ type: 'text', text: 'includeContext=thisServer' });
});

test('should throw when tool_result is mixed with other content', async () => {
const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });

Expand Down
Loading