feat(core): Advertise the MCP 2026-07-28 routing headers in CORS (#35471)

This commit is contained in:
Ricardo Espinoza 2026-08-11 05:28:43 -04:00 committed by GitHub
parent c44c7de59b
commit a14ab0b086
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 45 additions and 5 deletions

View File

@ -0,0 +1,21 @@
# Instance MCP server
Serves the instance-level MCP endpoint at `POST /mcp-server/http`. It speaks the
2026-07-28 protocol revision and, through the SDK's stateless legacy fallback,
still serves 2025-era clients on the same endpoint.
## Routing headers (2026-07-28)
The revision requires two headers on every Streamable HTTP `POST` so that
gateways, load balancers, and WAFs can route and rate-limit without parsing the
JSON-RPC body:
- `Mcp-Method` — the JSON-RPC method (e.g. `tools/call`, `tools/list`,
`server/discover`).
- `Mcp-Name` — for `tools/call`, the tool being called (e.g. `execute_workflow`).
The SDK validates these against the parsed body and rejects a disagreement with
`HeaderMismatchError` (`-32020`), so a client cannot mislabel the header to route
around a rule keyed on it. Browser-based clients can only send these when they
are in the CORS allow-list, which is why `setCorsHeaders` advertises
`MCP-Protocol-Version`, `Mcp-Method`, and `Mcp-Name`.

View File

@ -1,12 +1,14 @@
import type { Mock } from 'vitest';
import { Logger } from '@n8n/backend-common';
import { ApiKeyRepository, type AuthenticatedRequest } from '@n8n/db';
import { ControllerRegistryMetadata, type Controller } from '@n8n/decorators';
import { Container } from '@n8n/di';
import type { Request } from 'express';
import type { Mock } from 'vitest';
import { mock, mockDeep } from 'vitest-mock-extended';
// eslint-disable-next-line import-x/order
import { Telemetry } from '@/telemetry';
import { McpProtectedResource } from '../mcp-protected-resource';
import { McpServerMiddlewareService } from '../mcp-server-middleware.service';
const mockAuthMiddleware = vi.fn().mockImplementation(async function (_req, _res, next) {
@ -23,11 +25,9 @@ Container.set(McpServerMiddlewareService, mcpServerMiddlewareService);
import { McpConfig } from '../mcp.config';
import { MCP_CLIENT_INFO_META_KEY, MCP_PROTOCOL_VERSION_META_KEY } from '../mcp.constants';
import { McpProtectedResource } from '../mcp-protected-resource';
import type { McpController as McpControllerType, FlushableResponse } from '../mcp.controller';
import { McpService } from '../mcp.service';
import { McpSettingsService } from '../mcp.settings.service';
import { Telemetry } from '@/telemetry';
import type { UserConnectedToMCPEventPayload } from '../mcp.types';
const mockHandleRequest = vi.fn().mockResolvedValue(undefined);
@ -145,6 +145,19 @@ describe('McpController', () => {
expect(mcpService.resolveFeatureFlags as Mock).not.toHaveBeenCalled();
});
test('advertises the MCP routing headers in the CORS allow-list', async () => {
(mcpSettingsService.getEnabled as Mock).mockResolvedValue(false);
const res = createRes();
res.header = vi.fn().mockReturnThis();
await controller.build(createReq(), res);
expect(res.header).toHaveBeenCalledWith(
'Access-Control-Allow-Headers',
'Content-Type, Authorization, X-Requested-With, MCP-Protocol-Version, Mcp-Method, Mcp-Name',
);
});
test('creates mcp server if MCP access is enabled', async () => {
(mcpSettingsService.getEnabled as Mock).mockResolvedValue(true);
(mcpService.getServer as unknown as Mock).mockReturnValue({

View File

@ -45,7 +45,13 @@ export class McpController {
// Allow requests from Claude AI playground and other MCP clients
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
// MCP-Protocol-Version, Mcp-Method and Mcp-Name are the 2026-07-28 routing
// headers. Without listing them here a browser-based client can't send
// them, so the server would reject its requests with a header mismatch.
res.header(
'Access-Control-Allow-Headers',
'Content-Type, Authorization, X-Requested-With, MCP-Protocol-Version, Mcp-Method, Mcp-Name',
);
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Max-Age', '86400'); // 24 hours
}