Any Vendure plugin can add custom MCP tools. Define an injectable service, decorate it with
@McpTool, and register it as a provider. The MCP plugin
discovers the tool at bootstrap and exposes it alongside Vendure's built-in tools.
Install @vendure/mcp-sdk in the plugin that defines your tool:
Your tools should import MCP types and decorators from @vendure/mcp-sdk, rather than from @vendure/core or
@vendure/mcp-plugin. The MCP server itself still requires @vendure/mcp-plugin and McpPlugin in your Vendure
configuration.
Make sure your application resolves a single version of @vendure/mcp-sdk. Multiple copies can cause the
registry to miss decorated tools: check with npm ls @vendure/mcp-sdk.
To add a tool, you will:
Optionally add destructive confirmation, discovery keywords, or caller information.
A tool is an @Injectable() class with the @McpTool decorator and an execute(ctx, input) method. Implement
McpToolHandler from @vendure/mcp-sdk for type-safety:
To refuse a call, throw an error that extends I18nError from @vendure/core. It can be a core class such as
UserInputError, or a class your plugin defines.
What the agent receives depends on how the tool ends:
| The tool | The agent receives |
|---|---|
Throws an I18nError, apart from the cases in the last row | isError: true and the translated message |
Returns a Vendure error result, an object with errorCode, message and __typename, such as the CouponCodeInvalidError from OrderService.applyCouponCode | isError: true, the translated message, and the error object as the structured content |
Throws InternalServerError, an I18nError whose logLevel is LogLevel.Error, or an error that does not extend I18nError | isError: true and a fixed message saying the tool failed unexpectedly. The server logs the real error and its stack trace |
Tool names share a flat namespace within each toolset, including built-in tools. Use a plugin-specific prefix
such as wishlist_add_item rather than a generic name such as add_item, to avoid colliding with a future
Vendure release.
Register the class as a provider in the plugin that owns it:
The MCP registry discovers every @McpTool provider across the running app at bootstrap. To confirm the tool is
being served, list the tools with the MCP Inspector: see
Test & debug with the MCP Inspector.
inputSchema accepts either a plain JSON Schema object
(McpJsonSchema) or a
Standard Schema object
(McpStandardSchema) from a library such as Zod 4,
ArkType, or Valibot. Zod users need Zod 4.2 or later: Zod 3 cannot produce the JSON Schema that MCP requires.
With a Standard Schema, the schema library validates and parses the input before execute() is called. The
server converts the schema to JSON Schema once at startup to advertise the tool. You define the shape once and
derive the TypeScript type from it:
To avoid a schema library, use a plain JSON Schema object instead, as the first example on this page does. In
either form, the server validates every call's arguments before your execute runs. There is no per-request
validation to wire up. A schema that is neither form is a TypeScript error at the decorator. If a neither-form
schema reaches the registry anyway, startup aborts with an error naming the offending tool.
inputSchema means the tool accepts no arguments (the server publishes
{ type: 'object', properties: {}, additionalProperties: false }), not "accepts anything".additionalProperties: false so unknown fields are
rejected. Zod strips unknown keys by default. Use z.strictObject() to reject them instead.outputSchema does not reject invalid output. The server detects drift, logs a warning, and the call still
succeeds.
permissions is a list of Permission values. Multiple
permissions use OR semantics: the caller only needs one of the listed permissions.
| The tool | Set permissions to |
|---|---|
| Is a shop tool callable by anyone | [Permission.Public], or omit the field: omitted or empty means Permission.Public on a shop tool |
| Needs a signed-in user | [Permission.Authenticated] |
| Wraps an admin operation | The permissions the equivalent Admin GraphQL operation requires, so the tool is never more permissive than the API it wraps |
Admin tools must declare at least one permission. The server fails at startup, naming the tool, if permissions
is missing or empty.
If any administrator may call it, say so with [Permission.Authenticated]. An explicit [Permission.Public]
behaves the same on the admin endpoint but reads as "anonymous allowed".
Permission.Owner is refused at startup. It is not a granted permission, so no MCP caller could ever pass it. Use
[Permission.Authenticated] and check ownership inside the tool.
If your config sets an EntityAccessControlStrategy,
tool calls go through it:
canAccess() can refuse a tool that permissions allows, and tool listings leave a refused tool out. Unlike GraphQL,
it cannot allow a tool that permissions refuses.canAccess() runs once per distinct permission list on each MCP request. Tools with the same list share the result.
A throw refuses those tools and is logged.canAccess() receives the tool's permissions, or [Permission.Public] for a Shop tool that declares none.prepareAccessControl() runs on the context passed to the tool handler, so applyAccessControl() filters the
queries made with that context. A tool or Core service can copy the context, for example to open its own
transaction. Data your strategy stores per context object is not found on the copy, so key it on something a copy
keeps, such as ctx.session.Set usesActiveOrder: true on a Shop tool that uses the shopper's active Order (cart). This lets anonymous callers continue the same cart across tool calls using a sessionToken.
When the shopper already has a cart, ctx.currencyCode is the cart's currency instead of the channel's default, so changes to the cart keep its currency and prices.
Session creation depends on the tool's behavior:
readonly tools only use an existing session.mutating and confirmed destructive tools create an anonymous session when needed.Leave usesActiveOrder unset for tools that do not need the shopper's cart.
Do not add or handle sessionToken yourself. The MCP plugin manages it automatically and provides execute with the resolved RequestContext.
To add to the cart, get the cart with findOrCreateActiveOrder
instead of ActiveOrderService.getActiveOrder(ctx, undefined, true). It locks the session row first, so two calls that
run at the same time on one session add to one cart and do not create two. It returns the cart and a RequestContext
in the cart's currency. Pass that context to the core calls that change the cart. You can call it from a service that
your tool calls, with the ctx the tool received.
Use behavior: 'destructive' for tools that delete, cancel, refund, or otherwise make an irreversible or
difficult-to-reverse change. The registry then does the confirmation handshake for you:
confirm: boolean parameter on the tool's schema.confirm: true, it returns a non-error approval request with the tool's name and description, never
its arguments; your execute is not run.confirm: true, it strips confirm and runs your execute.Do not add confirm to your schema or read it in execute. The registry adds and handles it automatically, and
a destructive tool that declares its own confirm property fails at bootstrap.
This holds for both schema forms. For a Standard Schema tool, the registry validates your fields
with your schema and handles confirm itself. Even a strict Zod schema never sees it. One
constraint: a destructive tool's schema must parse to a plain object. A schema whose output is
transformed to a string or an array is rejected at call time.
See How tool safety is derived for what each
behavior value means.
The optional keywords field is used only when tools are discovered through search_tools. In discovery mode
the server ranks tools against a user's search_tools query. Your keywords join the tool's name, title, and
description in the searchable text. Use keywords for synonyms or phrases that users might search for but that do
not appear in the tool's name or description:
Keywords are never serialized into a tool listing or sent to the agent. They only tune search. They have no
effect in the default direct exposure mode. See
Tool exposure for how discovery works.
The full field list, types, and defaults are on
McpToolMetadata. In brief:
| Field | Purpose |
|---|---|
name | Unique name within the toolset; snake_case by convention. search_tools and execute_tool are reserved |
title | Optional human-friendly title |
description | Agent-facing description — write it for the model |
toolset | 'shop' or 'admin' — which endpoint serves it |
permissions | Permission[], OR semantics. Omitted/empty on shop defaults to Public; required on admin — boot fails without it |
behavior | Safety classification: 'readonly', 'mutating' (default), or 'destructive' |
usesActiveOrder | Set on Shop tools that need the active cart; enables registry-managed anonymous sessionToken handling, and ctx.currencyCode is the cart's currency |
inputSchema / outputSchema | JSON Schema or Standard Schema (e.g. Zod v4); input validated pre-call, output drift-checked post-call |
keywords | Search-only synonyms for discovery mode |
execute(ctx, input) is enough for almost every tool. To learn who is calling, execute takes an optional third
argument, McpCallerInfo, from @vendure/mcp-sdk. It
carries the OAuth grant behind the call and the client IP:
caller is always passed. Its fields vary by call type:
| Field | When it is set |
|---|---|
caller.grant | OAuth-authenticated calls. Undefined for anonymous shop calls and for in-process calls made via McpToolExecutionService |
caller.clientIp | HTTP calls, anonymous included. Undefined for in-process calls |
A tool never needs to depend on @vendure/mcp-plugin. Everything a tool implements comes from
@vendure/mcp-sdk.
| What you see | Cause | Fix |
|---|---|---|
| Your tool does not appear in the tool list | Multiple versions of @vendure/mcp-sdk are installed | Run npm ls @vendure/mcp-sdk and make sure only one version resolves |
TypeScript rejects a Zod schema, saying it is not assignable to McpJsonSchema | Zod 3 is installed, often as a transitive dependency | Upgrade to Zod 4.2 or later, or use a plain JSON Schema object |
| An admin tool fails at startup | Its permissions is missing or empty | Add at least one permission |
| A destructive tool fails at bootstrap | Its schema declares confirm | Remove confirm; the registry adds it automatically |
| A destructive tool is rejected at runtime | Its schema does not parse to a plain object | Make the schema produce a plain object |
| An active-order tool fails at bootstrap | It is an admin tool, or its schema declares sessionToken | Use usesActiveOrder only on Shop tools and let the registry manage sessionToken |
| A startup error about a duplicate tool name | Another tool in the same toolset uses that name, built-in tools included | Rename yours with a plugin-specific prefix |
McpToolMetadata — the full field list, types, and defaults.Architecture reviews, custom plugin work, migrations, ongoing support. Get a hand from the team that builds Vendure.
Talk to the team