If your assistant runs inside the Vendure server, you can execute MCP tools directly, without OAuth and without an HTTP connection.
This guide builds a small chat assistant as a Vendure plugin. The assistant receives a shopper's message through
the Shop API and passes the available MCP tools to your model. It then executes whichever tool the model picks,
using the RequestContext from that request.
Use this approach when your assistant runs as part of the Vendure server, for example as a Vendure plugin.
Use a different approach when:
Inject McpToolExecutionService into your
service. Both methods take the RequestContext received by your resolver:
Each summary in tools carries name, description, and an inputSchema written as JSON Schema (draft
2020-12). Map each summary to the tool-definition shape your model provider expects.
| Behavior | What happens |
|---|---|
| Identity | The tool runs as whoever the RequestContext identifies |
| Permissions | The tool runs with the permissions of the context you pass |
| Disabled tools | An unknown or disabled tool returns an error result |
| Invalid arguments | Arguments are checked against the tool's schema before the tool runs |
| Rate limits | The configured MCP rate limits apply, and a refusal arrives as an error result |
| Destructive tools | A destructive tool needs confirm: true before it executes |
| Logging | An executed call writes a log row and publishes an McpToolCallEvent, whether the tool succeeded or failed. A call refused before execution is not logged |
Always pass the RequestContext your Shop API resolver received, and never construct a new one. The context
decides which shopper the tool runs as, so a hand-built context runs the tool as the wrong shopper or as nobody.
A failed tool call returns isError: true with a text explanation instead of throwing. An error thrown inside the
tool comes back the same way. So does a Vendure error result that a tool returns, for example the
CouponCodeInvalidError from apply_coupon_code. In that case structuredContent is the error object itself,
with its errorCode and message, so the caller can act on the code.
A result is also refused when its JSON is larger than 100,000 bytes. That is about 25,000 tokens, the amount
Claude Code accepts from one tool call before it truncates the text. The message asks for less at a time and names
only the arguments that tool has, such as a lower limit, the next page through offset, or a filter.
Passing the wrong kind of context throws instead. The 'shop' toolset requires a Shop API context, and 'admin'
requires an Admin API context.
A destructive tool called without confirm: true returns a confirmation_required result and does not run. Show
that result to the shopper, then call the tool again with confirm: true once they agree.
A confirmation-required result does not set isError, so check structuredContent.status:
Importing McpPlugin is what makes McpToolExecutionService available to inject.
The resolver passes the request context to the assistant service.
If your assistant should work for guests as well as logged-in shoppers, use the Owner permission on the Shop API
mutation. Vendure creates a shopper's session only when the operation being called asks for Owner.
Without a session, a guest has no active order, and the cart, checkout and order tools all refuse. add_to_cart
answers that it requires a Vendure session and that the mutation calling it must use the Owner permission; every
other cart tool answers "There is no active cart. Add an item with add_to_cart first." Tools that only read public
catalogue data, such as search_products, work without a session.
Owner also works for logged-in shoppers, so one permission covers both.
The example below keeps the model integration simple. It lists the available tools, then makes one fixed
search_products call instead of running a tool-calling loop. Replace that call with your model provider's
tool-calling API.
The example returns JSON.stringify(result.structuredContent) as the reply. A real assistant passes the tool
result back to the model as part of the tool-calling loop. The model then writes the reply the shopper reads.
The example service keeps history in a Map, so each conversation lives in one process's memory. A restart loses
every conversation. Run more than one instance and a shopper's next message may reach an instance that has never
seen the conversation. For a single instance that is usually fine. Beyond that, keep the history in shared
storage.
Whatever storage you choose, scope each conversation to its owner, as conversationKey does above. A
conversationId is a value the client sends, so it never proves who the caller is.
The in-process integration does not require a persistent connection or access token.
An in-process call that hits a limit comes back as a failed tool result, isError: true with a text message.
There is no HTTP 429, because there is no HTTP request. Tell the shopper to try again in a moment.
Three scopes count in-process calls: perSession, perUser for a signed-in shopper, and any perTool limit on
the tool being called. Set them in Set rate limits. The counters
also need a cache backend that only evicts on time, covered in
Where the rate-limit counters live.
Common mistake. Reusing one RequestContext across shoppers puts them all in one session bucket and one user
bucket. A single chatty conversation can then lock the others out.
Architecture reviews, custom plugin work, migrations, ongoing support. Get a hand from the team that builds Vendure.
Talk to the team