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
143 changes: 143 additions & 0 deletions examples/servers/typescript/everything-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,149 @@ app.use(
// Handle POST requests - stateful mode
app.post('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
const reqVersion = req.headers['mcp-protocol-version'] as string | undefined;
const body = req.body || {};
const method = body.method;
const id = body.id ?? null;
const params = body.params || {};
const meta = params._meta;
const metaVersion = meta?.['io.modelcontextprotocol/protocolVersion'];

// If it's a stateless request (no session ID, and has either _meta or MCP-Protocol-Version header indicating stateless mode)
if (!sessionId && (reqVersion || meta)) {
if (process.env.STATELESS_NEGATIVE === 'true') {
return res.json({
jsonrpc: '2.0',
id,
result: {}
});
}

if (!reqVersion) {
return res.status(400).json({
jsonrpc: '2.0',
id,
error: { code: -32600, message: 'Missing MCP-Protocol-Version header' }
});
}

if (
!meta ||
!meta['io.modelcontextprotocol/protocolVersion'] ||
!meta['io.modelcontextprotocol/clientInfo'] ||
!meta['io.modelcontextprotocol/clientCapabilities']
) {
return res.status(200).json({
jsonrpc: '2.0',
id,
error: {
code: -32602,
message: 'Invalid params: missing _meta or required fields'
}
});
}

if (reqVersion !== metaVersion) {
return res.status(400).json({
jsonrpc: '2.0',
id,
error: {
code: -32001,
message: 'Mismatched MCP-Protocol-Version header'
}
});
}

if (metaVersion !== 'DRAFT-2026-v1') {
return res.status(400).json({
jsonrpc: '2.0',
id,
error: {
code: -32602,
message: 'UnsupportedProtocolVersionError',
data: { supported: ['DRAFT-2026-v1'] }
}
});
}

res.setHeader('mcp-protocol-version', 'DRAFT-2026-v1');

if (method === 'server/discover') {
return res.json({
jsonrpc: '2.0',
id,
result: {
supportedVersions: ['DRAFT-2026-v1'],
capabilities: { tools: {} },
serverInfo: { name: 'everything-stateless-server', version: '1.0.0' }
}
});
}

if (method === 'tools/list') {
return res.json({
jsonrpc: '2.0',
id,
result: {
tools: [
{
name: 'test_missing_capability',
description: 'Test tool requiring sampling',
inputSchema: { type: 'object', properties: {} }
}
]
}
});
}

if (method === 'tools/call') {
const name = params.name;
if (name === 'test_missing_capability') {
const clientCaps = meta['io.modelcontextprotocol/clientCapabilities'];
if (!clientCaps?.sampling) {
return res.status(400).json({
jsonrpc: '2.0',
id,
error: {
code: -32003,
message: 'MissingRequiredClientCapabilityError',
data: { requiredCapabilities: ['sampling'] }
}
});
}
return res.json({
jsonrpc: '2.0',
id,
result: { content: [{ type: 'text', text: 'Success' }] }
});
}
}

if (
[
'initialize',
'ping',
'logging/setLevel',
'resources/subscribe',
'resources/unsubscribe'
].includes(method)
) {
return res.status(200).json({
jsonrpc: '2.0',
id,
error: {
code: -32601,
message: 'Method not found: removed stateful RPC'
}
});
}

return res.status(404).json({
jsonrpc: '2.0',
id,
error: { code: -32601, message: 'Method not found' }
});
}

try {
let transport: StreamableHTTPServerTransport;
Expand Down
7 changes: 6 additions & 1 deletion src/scenarios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { SSERetryScenario } from './client/sse-retry';

// Import all new server test scenarios
import { ServerInitializeScenario } from './server/lifecycle';
import { ServerStatelessScenario } from './server/stateless';

import {
PingScenario,
Expand Down Expand Up @@ -81,13 +82,17 @@ const pendingClientScenariosList: ClientScenario[] = [

// On hold until server-side SSE improvements are made
// https://github.com/modelcontextprotocol/typescript-sdk/pull/1129
new ServerSSEPollingScenario()
new ServerSSEPollingScenario(),

// Stateless MCP architecture (SEP-2575)
new ServerStatelessScenario()
];

// All client scenarios
const allClientScenariosList: ClientScenario[] = [
// Lifecycle scenarios
new ServerInitializeScenario(),
new ServerStatelessScenario(),

// Utilities scenarios
new LoggingSetLevelScenario(),
Expand Down
112 changes: 112 additions & 0 deletions src/scenarios/server/stateless.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { spawn, ChildProcess } from 'child_process';
import path from 'path';
import { ServerStatelessScenario } from './stateless';

function startServer(
scriptPath: string,
port: number,
envOverrides?: Record<string, string>
): Promise<ChildProcess> {
return new Promise((resolve, reject) => {
const isWindows = process.platform === 'win32';
const proc = spawn('npx', ['tsx', scriptPath], {
env: { ...process.env, PORT: port.toString(), ...envOverrides },
stdio: ['ignore', 'pipe', 'pipe'],
shell: isWindows
});
let stderr = '';
proc.stderr?.on('data', (d) => (stderr += d.toString()));
const timeout = setTimeout(() => {
proc.kill('SIGKILL');
reject(
new Error(`Server ${scriptPath} failed to start within 30s: ${stderr}`)
);
}, 30000);
proc.stdout?.on('data', (data) => {
if (data.toString().includes('running on')) {
clearTimeout(timeout);
resolve(proc);
}
});
proc.on('error', (err) => {
clearTimeout(timeout);
reject(err);
});
});
}

function stopServer(proc: ChildProcess | null): Promise<void> {
return new Promise((resolve) => {
if (!proc || proc.killed) return resolve();
const t = setTimeout(() => {
proc.kill('SIGKILL');
resolve();
}, 5000);
proc.once('exit', () => {
clearTimeout(t);
resolve();
});
proc.kill('SIGTERM');
});
}

describe('ServerStatelessScenario tests', () => {
describe('passing server', () => {
let serverProcess: ChildProcess | null = null;
const PORT = 3010;

beforeAll(async () => {
serverProcess = await startServer(
path.join(
process.cwd(),
'examples/servers/typescript/everything-server.ts'
),
PORT
);
}, 35000);

afterAll(async () => {
await stopServer(serverProcess);
});

it('emits SUCCESS for all checks against a compliant stateless server', async () => {
const scenario = new ServerStatelessScenario();
const checks = await scenario.run(`http://localhost:${PORT}/mcp`);

for (const check of checks) {
if (check.status !== 'SUCCESS') {
console.error('FAILED CHECK:', JSON.stringify(check, null, 2));
}
expect(check.status).toBe('SUCCESS');
}
}, 15000);
});

describe('negative server', () => {
let serverProcess: ChildProcess | null = null;
const PORT = 3012;

beforeAll(async () => {
serverProcess = await startServer(
path.join(
process.cwd(),
'examples/servers/typescript/everything-server.ts'
),
PORT,
{ STATELESS_NEGATIVE: 'true' }
);
}, 35000);

afterAll(async () => {
await stopServer(serverProcess);
});

it('emits FAILURE for checks against a broken stateless server', async () => {
const scenario = new ServerStatelessScenario();
const checks = await scenario.run(`http://localhost:${PORT}/mcp`);

const failures = checks.filter((c) => c.status === 'FAILURE');
expect(failures.length).toBeGreaterThan(0);
}, 15000);
});
});
Loading