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
91 changes: 91 additions & 0 deletions examples/servers/typescript/accepts-json-rpc-batch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env node

/**
* Negative test server that incorrectly accepts JSON-RPC batch arrays.
*
* AGENTS.md negative-fixture pattern: deliberately broken server in
* examples/servers/typescript/, exercised from negative.test.ts (not
* everything-server). Proves json-rpc-batch-rejected emits FAILURE when a
* server returns 200 with a batch response array.
*/

import express from 'express';

function handleSingle(body: {
id?: number | string | null;
method?: string;
params?: Record<string, unknown>;
}) {
const id = body.id ?? null;
const method = body.method;

switch (method) {
case 'initialize':
return {
jsonrpc: '2.0' as const,
id,
result: {
protocolVersion:
(body.params?.protocolVersion as string | undefined) ??
'2025-11-25',
capabilities: {},
serverInfo: { name: 'accepts-json-rpc-batch', version: '1.0.0' }
}
};
case 'ping':
return { jsonrpc: '2.0' as const, id, result: {} };
case 'server/discover':
return {
jsonrpc: '2.0' as const,
id,
result: {
supportedVersions: ['2026-07-28'],
capabilities: {},
serverInfo: { name: 'accepts-json-rpc-batch', version: '1.0.0' }
}
};
case 'tools/list':
return {
jsonrpc: '2.0' as const,
id,
result: { tools: [] }
};
default:
return {
jsonrpc: '2.0' as const,
id,
error: { code: -32601, message: 'Method not found' }
};
}
}

const app = express();
app.use(express.json());

app.post('/mcp', (req, res) => {
const body = req.body;

if (Array.isArray(body)) {
const responses = body.map((item) =>
handleSingle(
typeof item === 'object' && item !== null
? (item as {
id?: number | string | null;
method?: string;
params?: Record<string, unknown>;
})
: { id: null }
)
);
return res.status(200).json(responses);
}

return res.json(handleSingle(body ?? {}));
});

const PORT = parseInt(process.env.PORT || '3008', 10);
app.listen(PORT, '127.0.0.1', () => {
console.log(
`JSON-RPC batch acceptance negative test server running on http://localhost:${PORT}/mcp`
);
});
34 changes: 34 additions & 0 deletions examples/servers/typescript/everything-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1235,10 +1235,44 @@ const LEGACY_SESSION_PROTOCOL_VERSIONS = [
'2025-11-25'
];

// Protocol revisions that MUST support receiving JSON-RPC batches
// (2025-03-26 base protocol §Batching). Later revisions removed that
// requirement; the array guard below rejects batches only for those.
const BATCHING_REQUIRED_PROTOCOL_VERSIONS = new Set([
'2024-11-05',
'2025-03-26'
]);

// 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;

// AGENTS.md: all-scenarios.test.ts runs every active scenario against
// everything-server as the reference "does not false-positive" fixture.
// From 2025-06-18 onward, batch arrays must be rejected so the
// json-rpc-batch-rejection scenario does not false-positive. This handler
// reads req.body.method before the SDK transport sees the POST body;
// without an explicit array guard, batch probes either hit session routing
// (-32000) for the wrong reason or get processed. Failure proof lives in
// accepts-json-rpc-batch.ts + negative.test.ts.
//
// Do NOT reject for 2025-03-26 (or when the version header is absent — the
// transport SHOULD assume 2025-03-26): that revision MUST support receiving
// JSON-RPC batches. Fall through to the existing session/SDK path instead.
if (Array.isArray(req.body)) {
const protocolVersion = reqVersion ?? '2025-03-26';
if (!BATCHING_REQUIRED_PROTOCOL_VERSIONS.has(protocolVersion)) {
return res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32600,
message: 'Invalid Request: JSON-RPC batch requests are not supported'
},
id: null
});
}
}
const body = req.body || {};
const method = body.method;
const id = body.id ?? null;
Expand Down
3 changes: 3 additions & 0 deletions src/scenarios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
} from './server/prompts';

import { DNSRebindingProtectionScenario } from './server/dns-rebinding';
import { JsonRpcBatchRejectionScenario } from './server/json-rpc-batch-rejection';
import { CachingScenario } from './server/caching';

// InputRequiredResult scenarios from (SEP-2322)
Expand Down Expand Up @@ -209,6 +210,8 @@ const allClientScenariosList: ClientScenario[] = [

// Security scenarios
new DNSRebindingProtectionScenario(),
// 2025-06-18+ wire requirement; negative proof in accepts-json-rpc-batch.ts
new JsonRpcBatchRejectionScenario(),

// Caching scenarios (SEP-2549)
new CachingScenario(),
Expand Down
63 changes: 63 additions & 0 deletions src/scenarios/server/json-rpc-batch-rejection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Unit tests for batch acceptance/rejection helpers used by
// json-rpc-batch-rejection.ts (AGENTS.md: prove the check logic, not only E2E).
import { describe, it, expect } from 'vitest';
import {
isBatchAccepted,
isBatchRejected,
jsonRpcErrorCode
} from './json-rpc-batch-rejection.js';

describe('json-rpc batch rejection helpers', () => {
it('detects a successful batch array response as accepted', () => {
expect(
isBatchAccepted(200, [
{ jsonrpc: '2.0', id: 1, result: {} },
{ jsonrpc: '2.0', id: 2, result: {} }
])
).toBe(true);
});

it('detects a single-object success response as accepted', () => {
expect(isBatchAccepted(200, { jsonrpc: '2.0', id: 1, result: {} })).toBe(
true
);
});

it('detects HTTP 4xx JSON-RPC errors as rejected', () => {
expect(
isBatchRejected(400, {
jsonrpc: '2.0',
id: null,
error: { code: -32600, message: 'Invalid Request' }
})
).toBe(true);
expect(
isBatchRejected(400, {
jsonrpc: '2.0',
id: null,
error: { code: -32000, message: 'Invalid or missing session ID' }
})
).toBe(true);
});

it('does not treat HTTP 5xx as batch rejection', () => {
expect(
isBatchRejected(500, {
jsonrpc: '2.0',
id: null,
error: { code: -32603, message: 'Internal error' }
})
).toBe(false);
});

it('extracts JSON-RPC error codes from single-object bodies', () => {
expect(
jsonRpcErrorCode({
jsonrpc: '2.0',
id: null,
error: { code: -32600, message: 'Invalid Request' }
})
).toBe(-32600);
expect(jsonRpcErrorCode([{ error: { code: -32600 } }])).toBeUndefined();
});
});
Loading