diff --git a/.changeset/mcp-default-user-agent.md b/.changeset/mcp-default-user-agent.md new file mode 100644 index 0000000000..b557d205b6 --- /dev/null +++ b/.changeset/mcp-default-user-agent.md @@ -0,0 +1,5 @@ +--- +"executor": patch +--- + +Send a default User-Agent on remote MCP requests so servers that reject requests without one accept the connection. A configured User-Agent header still wins. diff --git a/packages/plugins/mcp/src/sdk/connection.ts b/packages/plugins/mcp/src/sdk/connection.ts index 344de1f640..94fed481d4 100644 --- a/packages/plugins/mcp/src/sdk/connection.ts +++ b/packages/plugins/mcp/src/sdk/connection.ts @@ -72,6 +72,17 @@ const buildEndpointUrl = (endpoint: string, queryParams: Record) return url; }; +// Some upstreams (Cloudflare bot rules among them) reject requests without a +// User-Agent, and Workers' fetch adds none. Both remote SDK transports apply +// `requestInit.headers` to every request they send, including OAuth metadata +// discovery, so the default goes there. A configured User-Agent wins. +const DEFAULT_USER_AGENT = "executor"; + +const withDefaultUserAgent = (headers: Record): Record => + Object.keys(headers).some((name) => name.toLowerCase() === "user-agent") + ? headers + : { "User-Agent": DEFAULT_USER_AGENT, ...headers }; + type HttpMethod = Parameters[0]; const HTTP_METHODS = new Set([ "DELETE", @@ -626,9 +637,9 @@ export const createMcpConnector = (input: ConnectorInput): McpConnector => { } // Remote transport - const headers = input.headers ?? {}; + const headers = withDefaultUserAgent(input.headers ?? {}); const remoteTransport = input.remoteTransport ?? "auto"; - const requestInit = Object.keys(headers).length > 0 ? { headers } : undefined; + const requestInit = { headers }; const fetch = input.httpClientLayer ? fetchFromHttpClientLayer(input.httpClientLayer, input.staticOAuthBearer === true) : undefined;