upturnstudio / module-mcp
MCP (Model Context Protocol) connector exposing read-only Magento GraphQL access to Claude, authorized via OAuth 2.1 for Magento admin users.
Package info
Type:magento2-module
pkg:composer/upturnstudio/module-mcp
Requires
- php: ~8.2.0||~8.3.0||~8.4.0||~8.5.0
- magento/framework: *
- magento/module-authorization: *
- magento/module-backend: *
- magento/module-graph-ql: *
- magento/module-integration: *
- magento/module-user: *
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Magento Connector MCP Module turns a Magento 2 / Adobe Commerce store into a Model Context Protocol (MCP) server, so Claude can run read-only GraphQL queries - products, categories, customers, orders, and anything else the store's schema exposes - on behalf of an authenticated admin, using that admin's own Magento permissions.
It connects to Claude exactly like any other MCP integration: as a normal custom connector in Claude.ai / Claude Desktop (Settings > Connectors > Add - the same flow you'd use for any other service) over a standard OAuth 2.1 flow, or as a local MCP server for Claude Code. No special Claude-side support is needed either way.
Two transports, same underlying tools and safety guarantees either way:
- stdio - a local subprocess (
bin/magento mcp:serve), for Claude Code / Claude Desktop's local MCP config. No network, no OAuth - trust comes from "you can already runbin/magentoon this server." - HTTP + OAuth 2.1 - a real "custom connector" (
/mcp, with DCR/PKCE/consent screen underadmin/upturnstudio_mcp/oauth/authorize), for Claude.ai's hosted apps. Requires the site to be reachable over public HTTPS.
For background and a walkthrough, see the blog post: Magento MCP Claude Connector.
Example
Claude calling execute_graphql against a live store, then narrowing the query based on the
result - no hand-written integration code, just the schema:
Installation
composer require upturnstudio/module-mcp bin/magento module:enable UpturnStudio_Mcp bin/magento setup:upgrade bin/magento cache:flush
Running in production mode? Also run bin/magento setup:di:compile before cache:flush.
Setup
- Grant the permission. A brand-new ACL resource,
UpturnStudio_Mcp::connector("AI Connector (MCP)"), gates who can authorize this connector at all. It is not granted to any role automatically - go to Admin > System > Permissions > User Roles and add it to whichever role(s) should be allowed to connect Claude. - (HTTP transport only) Set the public base URL. Stores > Configuration > Advanced >
AI Connector (MCP) > Public Base URL must be a real
https://URL this store is reachable at, with no path (e.g.https://your-domain.com, not.../mcp) - it's the OAuth issuer and MCP resource identifier, and must never change once a client has registered against it. Not needed for the stdio transport. See "Using it remotely" below for the actual URL to give Claude, which is this value plus/mcp. - (Optional) Set a PageSpeed Insights API key. Stores > Configuration > Advanced >
AI Connector (MCP) > Core Web Vitals > PageSpeed Insights API Key. Only used by the
check_core_web_vitalstool - leave blank to use Google's free, unauthenticated tier (lower rate limit).
Using it locally (stdio)
bin/magento mcp:serve --admin-user=<username>
--admin-user is a real Magento admin username (not email, not display name) that already
has the permission above. Point Claude Code/Desktop's local MCP config at it directly:
{
"mcpServers": {
"magento-demo": {
"command": "/path/to/php",
"args": ["/path/to/bin/magento", "mcp:serve", "--admin-user=admin"]
}
}
}
To interactively poke at it with the official MCP Inspector instead of a real client:
bin/magento mcp:inspector --admin-user=<username>
This shells out to npx @modelcontextprotocol/inspector with the right invocation pre-built
(config file, protocolEra, etc.) and auto-switches to a working Node version via nvm first
if the active one is too old for the Inspector's own dependencies. Add --cli for one-shot
scripted calls (-- --method tools/list --format json) instead of the interactive web UI.
Requires Node.js/npx; this only launches the Inspector, it doesn't bundle it.
Using it remotely (HTTP + OAuth)
This works as a completely standard custom connector - no special setup on Claude's side.
In Claude.ai: Settings > Connectors > Add custom connector, and paste the public base URL
with /mcp appended - not the bare domain. For example, if the Public Base URL configured
above is https://your-domain.com, enter:
https://your-domain.com/mcp
(Claude Code / Claude Desktop: add it as a remote server pointed at that same /mcp URL
instead.) This has to be reachable over the public internet - a local .test/.localhost
domain only works with the stdio transport above, not this one.
After adding it, Claude handles the rest automatically:
- Claude requests that URL, gets a
401, and follows it to this store's OAuth endpoints (/.well-known/oauth-protected-resource, then/.well-known/oauth-authorization-server). - Claude registers itself as a client (
/register) - nothing for the admin to do here. - A browser tab opens to
admin/upturnstudio_mcp/oauth/authorize. If the admin isn't already logged into Magento admin, they're asked to log in first, then shown a consent screen naming the connector and asking them to Allow or Deny it. - On Allow, Claude exchanges the result for its own token and the connector is ready to use.
Connected apps can be reviewed and revoked afterwards at Admin > System > AI Connector (MCP).
The token Claude receives is not a Magento admin token - it only works against this
module's own /mcp endpoint. Internally, a real (short-lived, never cached) Magento admin
token is minted per call so Magento's own GraphQL resolvers and ACL checks see a genuine,
live admin identity.
Tools
execute_graphql- runs a GraphQL query. Read-only: anymutationoperation is refused before it's ever sent to Magento, regardless of what the admin's own role would otherwise permit.introspect_graphql_schema- returns the full schema via standard introspection, so Claude can discover what's queryable without guessing.check_core_web_vitals- given a store page URL, reports its Core Web Vitals (field data plus a live Lighthouse run, via Google PageSpeed Insights) and resolves that URL to the Magento entity behind it - product, category, or CMS page, including its entity ID - so a follow-upexecute_graphqlquery (or a human editing it directly) knows exactly which record to target. Read-only, like everything else here: it identifies the entity, it doesn't touch it.
execute_graphql and introspect_graphql_schema run through Magento's real /graphql
endpoint internally (not in-process GraphQL classes) - the schema's own DI wiring is scattered
across ~50 modules' etc/graphql/di.xml files and isn't safely reproducible from another
area, so this reuses it as-is instead. check_core_web_vitals doesn't need any of that: its
entity lookup is a plain url_rewrite table lookup plus a repository fetch, both globally
bound services with no area-scoping problem, so it resolves the entity in-process directly
rather than paying for a loopback HTTP call to /graphql.
Adding more tools
Other modules can register additional tools without touching this one. Implement
UpturnStudio\Mcp\Api\ToolInterface:
class MyTool implements \UpturnStudio\Mcp\Api\ToolInterface { public function getName(): string { return 'my_tool'; } public function getDefinition(): array { return [ 'description' => 'What this tool does.', 'inputSchema' => ['type' => 'object', 'properties' => (object) []], ]; } public function execute(int $adminUserId, array $arguments): array { // Return an array on success. Throw ToolExecutionException to signal a refusal // or failure (isError: true) instead of encoding it in the return value. return ['ok' => true]; } }
Then register it from your own module's etc/di.xml:
<type name="UpturnStudio\Mcp\Model\Mcp\ToolRegistry"> <arguments> <argument name="tools" xsi:type="array"> <item name="myTool" xsi:type="object">Vendor\Module\Model\Mcp\Tool\MyTool</item> </argument> </arguments> </type>
It shows up in tools/list and is callable via tools/call on both transports automatically
- nothing in this module needs to change. Tool names must be unique; a later-merged entry
with the same
getName()replaces an earlier one, the same way Magento's DI array arguments normally merge.
Security notes
- Opaque access/refresh tokens are scoped to this connector only - they're useless against
Magento's real
/restor/graphqlif leaked. - An admin's connector access is re-checked live on every single call (not just at OAuth grant time) - disabling the admin account in Magento takes effect immediately, even for an already-issued, unexpired token.
- Refresh tokens rotate on every use; replaying an already-rotated one revokes the entire token family and forces re-consent.
