Summary
RubyLLM::MCP::Native::Transports::StreamableHTTP#build_common_headers sets:
headers["Origin"] = @url.to_s
https://github.com/patvice/ruby_llm-mcp/blob/v1.0.0/lib/ruby_llm/mcp/native/transports/streamable_http.rb#L277
This is incorrect on two counts, and it causes GitHub's remote MCP server (api.githubcopilot.com/mcp) to reject every request with HTTP 403.
Why the current behavior is wrong
- RFC 6454 §7 defines
Origin as a user-agent-only header — "The user agent MAY include an Origin header field in any HTTP request." Server-to-server HTTP clients should not send it. Its value must also be scheme + host (+ port), not a full URL with a path.
- The MCP spec (Streamable HTTP transport, revisions
2025-03-26, 2025-06-18, and draft) only mandates server-side Origin validation for DNS-rebinding protection. It does not require or recommend clients send Origin. The draft explicitly says: "If the Origin header is present and invalid, servers MUST respond with 403 Forbidden."
- The MCP authorization spec never references the HTTP
Origin header.
- The reference MCP SDKs (TypeScript and Python) do not set
Origin on server-to-server fetches.
Assigning the full endpoint URL (including the path /mcp) as the Origin value is invalid per RFC 6454.
Reproduction
Any client using ruby_llm-mcp 1.0.0 connecting to GitHub's remote MCP server at https://api.githubcopilot.com/mcp with a valid OAuth token fails:
Outgoing request:
POST https://api.githubcopilot.com/mcp
user-agent: httpx.rb/1.7.2
accept: application/json, text/event-stream
x-client-id: 67267522-1918-4cc3-90e2-bbdbe416fef8
origin: https://api.githubcopilot.com/mcp ← invalid: includes a path
authorization: Bearer ghu_...
content-type: application/json
Response:
HTTP/2 403
server: github-mcp-server-remote
content-type: text/plain; charset=utf-8
content-length: 120
cross-origin request detected, and/or browser is out of date: Sec-Fetch-Site is missing, and Origin does not match Host
Root cause (server side)
GitHub's MCP server uses Go's net/http.CrossOriginProtection.Check() (or equivalent), which performs (simplified):
- If
Sec-Fetch-Site is same-origin or none → allow.
- Else if
Origin is absent → allow (non-browser assumed).
- Else if
url.Parse(origin).Host == req.Host → allow.
- Else → reject with the 403 above.
Even with the path stripped (so Origin: https://api.githubcopilot.com), url.Parse(origin).Host is api.githubcopilot.com but req.Host in Go may include the default port (api.githubcopilot.com:443), so the string comparison still fails. The robust fix is to simply not send the header.
Workaround (monkey-patch)
We ship the following initializer in our Rails app until this is fixed upstream:
module RubyLLMMCPOriginHeaderFix
def build_common_headers
super.except("Origin")
end
end
RubyLLM::MCP::Native::Transports::StreamableHTTP.prepend(RubyLLMMCPOriginHeaderFix)
This resolves the 403 against GitHub's MCP server and is spec-compliant (clients are not required to send Origin).
Suggested fix
Remove line 277 of lib/ruby_llm/mcp/native/transports/streamable_http.rb. Origin should not be sent on server-to-server requests. If you want to keep it for completeness (e.g. for MCP servers that do happen to allow-list client origins), at minimum strip it to scheme+host(+non-default port), never include a path — but omitting it entirely matches the reference SDKs and RFC 6454.
Environment
ruby_llm-mcp 1.0.0
- Ruby 3.4.9
- Target:
https://api.githubcopilot.com/mcp (GitHub remote MCP server)
References
Summary
RubyLLM::MCP::Native::Transports::StreamableHTTP#build_common_headerssets:https://github.com/patvice/ruby_llm-mcp/blob/v1.0.0/lib/ruby_llm/mcp/native/transports/streamable_http.rb#L277
This is incorrect on two counts, and it causes GitHub's remote MCP server (
api.githubcopilot.com/mcp) to reject every request with HTTP 403.Why the current behavior is wrong
Originas a user-agent-only header — "The user agent MAY include an Origin header field in any HTTP request." Server-to-server HTTP clients should not send it. Its value must also be scheme + host (+ port), not a full URL with a path.2025-03-26,2025-06-18, and draft) only mandates server-sideOriginvalidation for DNS-rebinding protection. It does not require or recommend clients sendOrigin. The draft explicitly says: "If the Origin header is present and invalid, servers MUST respond with 403 Forbidden."Originheader.Originon server-to-server fetches.Assigning the full endpoint URL (including the path
/mcp) as theOriginvalue is invalid per RFC 6454.Reproduction
Any client using
ruby_llm-mcp1.0.0 connecting to GitHub's remote MCP server athttps://api.githubcopilot.com/mcpwith a valid OAuth token fails:Outgoing request:
Response:
Root cause (server side)
GitHub's MCP server uses Go's
net/http.CrossOriginProtection.Check()(or equivalent), which performs (simplified):Sec-Fetch-Siteissame-originornone→ allow.Originis absent → allow (non-browser assumed).url.Parse(origin).Host == req.Host→ allow.Even with the path stripped (so
Origin: https://api.githubcopilot.com),url.Parse(origin).Hostisapi.githubcopilot.combutreq.Hostin Go may include the default port (api.githubcopilot.com:443), so the string comparison still fails. The robust fix is to simply not send the header.Workaround (monkey-patch)
We ship the following initializer in our Rails app until this is fixed upstream:
This resolves the 403 against GitHub's MCP server and is spec-compliant (clients are not required to send
Origin).Suggested fix
Remove line 277 of
lib/ruby_llm/mcp/native/transports/streamable_http.rb.Originshould not be sent on server-to-server requests. If you want to keep it for completeness (e.g. for MCP servers that do happen to allow-list client origins), at minimum strip it to scheme+host(+non-default port), never include a path — but omitting it entirely matches the reference SDKs and RFC 6454.Environment
ruby_llm-mcp1.0.0https://api.githubcopilot.com/mcp(GitHub remote MCP server)References
net/httpCSRF/cross-origin protection — https://pkg.go.dev/net/http#CrossOriginProtection