Skip to content

feat(skeleton): add Storefront MCP proxy#3572

Open
itsjustriley wants to merge 8 commits intomainfrom
add-storefront-mcp-route
Open

feat(skeleton): add Storefront MCP proxy#3572
itsjustriley wants to merge 8 commits intomainfrom
add-storefront-mcp-route

Conversation

@itsjustriley
Copy link
Contributor

@itsjustriley itsjustriley commented Mar 11, 2026

WHY are these changes introduced?

Enables AI agents (Claude, ChatGPT, and other MCP clients) to interact with Hydrogen storefronts through the standardized Model Context Protocol. This allows AI assistants to help customers browse products, manage carts, and get store information in natural language.

Related: https://shopify.dev/docs/apps/build/storefront-mcp/servers/storefront

WHAT is this pull request doing?

Adds automatic MCP proxy support to Hydrogen's createRequestHandler. When requests are made to /api/mcp, Hydrogen now automatically forwards them to Shopify's centrally-hosted Storefront MCP server.

The implementation:

  • Detects MCP requests via the /api/mcp path pattern
  • Forwards requests to ${shopDomain}/api/mcp with proper headers
  • Preserves authentication (Storefront Access Token), cookies, and buyer IP for geolocalization
  • Works automatically for all Hydrogen storefronts without requiring route files

This approach mirrors how Hydrogen already proxies Storefront API GraphQL requests at /api/:version/graphql.json (which this PR also cleans up by removing the now-redundant manual route file).

Additional cleanup: Removes the api.$version.[graphql.json].tsx route from the skeleton template since createRequestHandler has automatically proxied these requests since December 2025 via proxyStandardRoutes: true (enabled by default).

HOW to test your changes?

  1. Build and start the skeleton template:

    cd templates/skeleton
    pnpm install
    pnpm build
    pnpm preview
  2. Test the tools/list endpoint:

    curl -X POST http://localhost:3000/api/mcp \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'

    Should return JSON with available MCP tools from Shopify's Storefront MCP server.

  3. Test product search:

    curl -X POST http://localhost:3000/api/mcp \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc": "2.0", "method": "tools/call", "id": 2, "params": {"name": "search_shop_catalog", "arguments": {"query": "snowboard", "context": "customer looking for snowboard"}}}'

    Should return product data from the store.

  4. Verify the redundant Storefront API route was removed:

    • Confirm templates/skeleton/app/routes/api.$version.[graphql.json].tsx no longer exists
    • Test that /api/2024-10/graphql.json still works via the automatic proxy

Implementation Notes

Proxy Architecture

Similar to the existing Storefront API proxy (isStorefrontApiUrl + forwardStorefrontApi), this adds:

  • isMcpUrl() - Checks if request matches /api/mcp
  • forwardMcp() - Forwards request to Shopify's hosted MCP endpoint

The proxy forwards essential headers:

  • Request headers: accept, content-type, cookie, origin, referer, user-agent
  • Shop identification: Shopify-Storefront-Private-Token (from storefront config)
  • Geolocalization: x-forwarded-for with buyer IP for market/currency detection

Why Proxy Instead of Local Implementation?

Shopify's centrally-hosted MCP server provides:

  • Consistent tooling across all storefronts
  • Automatic updates to MCP capabilities
  • Reduced bundle size for Hydrogen apps
  • No need for merchants to maintain MCP implementation code

Checklist

  • I've read the Contributing Guidelines
  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've added a changeset if this PR contains user-facing or noteworthy changes
  • I've added tests to cover my changes
  • I've added or updated the documentation

@shopify
Copy link
Contributor

shopify bot commented Mar 11, 2026

Oxygen deployed a preview of your add-storefront-mcp-route branch. Details:

Storefront Status Preview link Deployment details Last update (UTC)
Skeleton (skeleton.hydrogen.shop) ✅ Successful (Logs) Preview deployment Inspect deployment March 18, 2026 1:13 PM

Learn more about Hydrogen's GitHub integration.

@itsjustriley itsjustriley force-pushed the add-storefront-mcp-route branch 2 times, most recently from da1d725 to 1f5a330 Compare March 11, 2026 21:08
@itsjustriley itsjustriley marked this pull request as ready for review March 11, 2026 21:14
@itsjustriley itsjustriley requested a review from a team as a code owner March 11, 2026 21:14
@itsjustriley itsjustriley changed the title feat(skeleton): add Storefront MCP server endpoint feat(skeleton): add Storefront MCP proxy Mar 12, 2026
@itsjustriley itsjustriley requested a review from kdaviduik March 12, 2026 13:19
@itsjustriley itsjustriley force-pushed the add-storefront-mcp-route branch from 824ff80 to 35c0000 Compare March 16, 2026 15:08
- Add proper TypeScript types for JSON-RPC request handling
- Use generated GraphQL types instead of 'as any' type assertions
- Fix cart mutations to intelligently route between cartCreate/cartLinesAdd/cartLinesUpdate
- Add null safety and input validation throughout
- Add null checks for all GraphQL query/mutation responses
- Use consistent error messages that state observable facts
- Add comment explaining policy search behavior returns all policies
- Remove optional chaining where null checks are now explicit
- Add 'mcp' to ROUTE_MAP to include api.mcp.tsx when generating routes
- Fixes failing tests that expect api.mcp to be copied with skeleton template
@itsjustriley itsjustriley force-pushed the add-storefront-mcp-route branch from d72a35c to 5135637 Compare March 18, 2026 13:11
@itsjustriley itsjustriley requested a review from kdaviduik March 18, 2026 14:51
Copy link
Contributor

@kdaviduik kdaviduik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM aside from a few comments. Feel free to merge tomorrow if you address them and get another ✅ since I'll be OOO. Also:

Stale cookbook references (non-blocking): Three files reference the deleted api.$version.[graphql.json].tsx route:

  • cookbook/llms/express.prompt.md (line 2602)
  • cookbook/recipes/express/README.md (line 2612)
  • cookbook/recipes/express/recipe.yaml (line 82)

I think these should be updated in this PR since the PR is the direct cause of the breakage.

/**
* Checks if the request is targeting the Storefront MCP endpoint.
*/
isMcpUrl(request) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: unit tests for isMcpUrl() and forwardMcp() are still missing.

This was blocking in the previous review and remains unaddressed. Test infrastructure already exists in storefront.test.ts (tests createStorefrontClient with mocked fetchWithServerCache, tests header forwarding) and request.test.ts (tests extractHeaders and getHeader). At minimum the test cases from the prior review should be covered:

  • isMcpUrl returns true for /api/mcp and false for other paths (e.g., /api/mcp/foo, /api/mcps, /api/2024-10/graphql.json)
  • isMcpUrl returns false for /api/mcp/ (trailing slash)
  • isMcpUrl returns true for full URLs with query parameters (e.g., https://store.com/api/mcp?session=abc) - getSafePathname strips query params before matching
  • forwardMcp forwards the correct headers to {shopDomain}/api/mcp
  • forwardMcp preserves x-forwarded-for from buyer IP
  • Integration with createRequestHandler routes MCP requests to the proxy

Note: the existing SFAPI proxy also lacks unit tests - that's pre-existing debt, but new code is the right time to establish the pattern.

// Add headers for shop identification and geolocalization
...extractHeaders(
(key) => defaultHeaders[key],
[
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking: basically same comment as here.

I think forwardMcp should forward STOREFRONT_REQUEST_GROUP_ID_HEADER (and ideally SHOPIFY_STOREFRONT_ID_HEADER) for observability.

The existing SFAPI forward method includes both of these from defaultHeaders for tracing/debugging and storefront identification. Without the request group ID header, MCP requests are invisible in Hydrogen's request correlation/debugging story.

Re: content-length - you're right that it's a forbidden request header per the Fetch spec, so omitting it is appropriate.


const mcpUrl = `${getShopifyDomain()}/api/mcp`;

const mcpResponse = await fetch(mcpUrl, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking: I mentioned this in my previous review and it was marked as resolved but my agents brought it up again, and I don't see any "jsonrpc" stuff in the code.

If fetch(mcpUrl, ...) throws (DNS failure, connection refused, TLS error), the error propagates as an unhandled exception. MCP clients speak JSON-RPC and expect structured error responses ({"jsonrpc": "2.0", "error": {...}}). An unhandled exception produces an opaque 500 or HTML error page - the MCP client can't distinguish "the server crashed" from "the proxy is misconfigured" from "the upstream is down", and an AI assistant might silently break with no useful error message for the end user.

The existing SFAPI forward has the same gap (pre-existing), but for new code a try/catch returning a proper JSON-RPC error would be an improvement - and a natural test case to add alongside the other missing tests.

Also worth noting: if request.body has already been consumed by upstream middleware, fetch silently sends an empty body - another scenario where a clear error response would help debugging.

return response;
}

if (storefront?.isMcpUrl(request)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking as long as you follow up: every Hydrogen merchant who upgrades gets this new publicly-accessible /api/mcp proxy endpoint. It's gated behind proxyStandardRoutes (defaults true), which is consistent with the SFAPI proxy - merchants who have accepted proxyStandardRoutes: true have implicitly accepted this class of auto-proxied endpoint, and the risk profile is comparable.

I think the changeset should clarify the gating mechanism. Suggested wording: "This feature is automatically available when proxyStandardRoutes is enabled (the default) - no code changes required."

@kdaviduik kdaviduik dismissed their stale review March 20, 2026 03:30

i'm OOO; dismissing so others can approve and unblock this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants