mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-18 05:59:42 +02:00
feat(core): Advertise the MCP 2026-07-28 routing headers in CORS (#35471)
This commit is contained in:
parent
c44c7de59b
commit
a14ab0b086
21
packages/cli/src/modules/mcp/README.md
Normal file
21
packages/cli/src/modules/mcp/README.md
Normal 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`.
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user