MCP AI Integration
Overview
InnoShop exposes its backend capabilities as MCP (Model Context Protocol) tools, allowing AI assistants like Claude Code, Cursor, and Cline to interact with your store data — querying products, managing orders, checking inventory, and more.
MCP is built on JSON-RPC 2.0 over HTTP (Streamable HTTP transport) and is disabled by default for security.
Architecture
The MCP system has three layers, fully decoupled:
┌─────────────────────────────────────┐
│ Claude Code / Cursor / Cline │ ← MCP Clients
│ (JSON-RPC 2.0 over HTTP) │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ innopacks/mcp │ ← Protocol Layer
│ InnoShopMcpServer │ Sanctum auth, origin validation,
│ RegistryToolAdapter │ feature toggle, response formatting
│ McpController (welcome page) │
└──────────────┬──────────────────────┘
│ ToolInterface contract
┌──────────────▼──────────────────────┐
│ innopacks/ai │ ← Tool Definitions
│ ToolRegistry (singleton) │ 45+ built-in tools,
│ BaseTool, ToolInterface │ plugin extension via hook
│ PanelChatAgent │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ innopacks/common │ ← Business Logic
│ Repositories, Models, Services │
└─────────────────────────────────────┘Key principle: Tools are thin adapters over existing repositories — they contain no business logic.
Enabling MCP
MCP is controlled by a system setting and can be toggled from the admin panel:
- Go to Panel → System Settings → Tools → AI
- Find the MCP Service card
- Toggle the switch to enable
Or programmatically:
system_setting('mcp_enabled', true);When disabled, all MCP endpoints return 404.
Security Layers
| Layer | Mechanism | Purpose |
|---|---|---|
| Feature toggle | system_setting('mcp_enabled') | Master on/off switch |
| Origin validation | ValidateMcpOrigin middleware | Blocks DNS rebinding attacks |
| Authentication | Laravel Sanctum (auth:sanctum) | Requires admin API token |
| Authorization | $user->can($permission) | Tool-level permission checks |
Getting an API Token
curl -X POST https://your-store.com/api/panel/login \
-H "Content-Type: application/json" \
-d '{"email": "admin@example.com", "password": "your-password"}'The response contains a token field. Use it as a Bearer token:
Authorization: Bearer <token>Connecting AI Assistants
Claude Code
claude mcp add --transport http innoshop https://your-store.com/mcp \
--header "Authorization: Bearer YOUR_TOKEN"Cursor / Cline
Add to your MCP configuration:
{
"mcpServers": {
"innoshop": {
"url": "https://your-store.com/mcp",
"headers": {
"Authorization": "Bearer YOUR_TOKEN"
}
}
}
}Built-in Tools
InnoShop ships with 45+ tools covering all store operations. Tools marked with ⚠️ are write operations.
| Category | Tools |
|---|---|
| Products | product_list, product_detail, product_create ⚠️, product_update ⚠️, product_autocomplete, sku_autocomplete |
| Orders | order_list, order_detail, order_update_status ⚠️, order_note ⚠️, order_return_list, order_return_detail |
| Customers | customer_list, customer_detail, customer_update ⚠️, customer_autocomplete |
| Categories | category_list, category_detail, category_create ⚠️, category_update ⚠️, category_autocomplete |
| Brands | brand_list |
| Shipping | shipment_list, shipment_detail, shipment_create ⚠️, shipment_traces |
| Analytics | dashboard, sales_stats, stock_report |
| Content | article_list, article_detail, catalog_list, page_list, tag_list |
| Files | file_list, file_upload ⚠️ |
| Reviews | review_list, review_detail |
| Attributes | attribute_list, option_list |
| Localization | locale_list, currency_list, country_list, region_list |
| Tax | tax_rate_list, tax_class_list |
The full list is always available at GET /mcp (public welcome page).
Developing MCP Tools in a Plugin
Plugins can register custom MCP tools via the ai.tools hook. This is the primary extension point — once registered, your tool automatically appears in MCP tools/list, tools/call, the welcome page, and the panel AI chat.
Core Contracts
ToolInterface (innopacks/ai/src/Contracts/ToolInterface.php):
interface ToolInterface
{
public function name(): string; // Unique snake_case identifier
public function description(): string; // LLM-readable description
public function inputSchema(): array; // JSON Schema for arguments
public function requiredPermission(): ?string; // Permission slug, null = public
public function execute(array $arguments): mixed; // Run with validated arguments
}BaseTool (innopacks/ai/src/Tools/BaseTool.php):
abstract class BaseTool implements ToolInterface
{
public function inputSchema(): array
{
return ['type' => 'object', 'properties' => new \stdClass];
}
public function requiredPermission(): ?string
{
return null; // No permission required by default
}
}Step-by-Step: Read Tool
Let's build a plugin that exposes a recent_orders tool. Create the following files:
1. Plugin Directory Structure
plugins/RecentOrders/
├── Boot.php
├── config.json
├── Lang/
│ ├── en/common.php
│ └── zh-cn/common.php
└── Tools/
└── RecentOrdersTool.php2. config.json
{
"code": "recent_orders",
"name": { "en": "Recent Orders MCP", "zh-cn": "最近订单 MCP" },
"description": { "en": "Expose recent orders via MCP", "zh-cn": "通过 MCP 暴露最近订单" },
"type": "feature",
"version": "v1.0.0",
"author": { "name": "InnoShop", "email": "team@innoshop.com" }
}3. Tools/RecentOrdersTool.php
<?php
namespace Plugin\RecentOrders\Tools;
use InnoShop\AI\Tools\BaseTool;
use InnoShop\Common\Repositories\OrderRepo;
class RecentOrdersTool extends BaseTool
{
public function name(): string
{
return 'recent_orders';
}
public function description(): string
{
return 'Get the most recent orders with optional status filter.';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'limit' => [
'type' => 'integer',
'description' => 'Number of orders to return, default 10, max 50',
],
'status' => [
'type' => 'string',
'description' => 'Filter by status: unpaid, paid, shipped, completed, cancelled',
],
],
];
}
public function requiredPermission(): ?string
{
return 'orders_index';
}
public function execute(array $arguments): mixed
{
$filters = ['per_page' => min(50, max(1, (int) ($arguments['limit'] ?? 10)))];
if (!empty($arguments['status'])) {
$filters['status'] = $arguments['status'];
}
$paginator = OrderRepo::getInstance()->list($filters);
return [
'total' => $paginator->total(),
'items' => $paginator->map(fn($order) => [
'number' => $order->number,
'customer' => $order->customer_name,
'total' => (float) $order->total,
'status' => $order->status,
'created_at' => (string) $order->created_at,
])->values()->all(),
];
}
}4. Boot.php
<?php
namespace Plugin\RecentOrders;
class Boot
{
public function init(): void
{
add_hook_filter('ai.tools', function ($registry) {
$registry->register(new Tools\RecentOrdersTool);
return $registry;
});
}
}That's it. After installing and enabling the plugin, the recent_orders tool appears automatically on the MCP welcome page and in tools/list.
Step-by-Step: Write Tool
Write tools follow the same pattern but with stricter permissions:
<?php
namespace Plugin\RecentOrders\Tools;
use InnoShop\AI\Tools\BaseTool;
use InnoShop\Common\Repositories\OrderRepo;
use InvalidArgumentException;
class OrderMarkPaidTool extends BaseTool
{
public function name(): string
{
return 'order_mark_paid';
}
public function description(): string
{
return '⚠️ WRITE: Mark an unpaid order as paid.';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'number' => ['type' => 'string', 'description' => 'Order number'],
'comment' => ['type' => 'string', 'description' => 'Optional note'],
],
'required' => ['number'],
];
}
public function requiredPermission(): ?string
{
return 'orders_update_status';
}
public function execute(array $arguments): mixed
{
$order = OrderRepo::getInstance()->findByNumber($arguments['number']);
if (!$order) {
throw new InvalidArgumentException("Order [{$arguments['number']}] not found.");
}
$order->status = 'paid';
$order->save();
return [
'number' => $order->number,
'status' => $order->status,
'updated_at' => (string) $order->updated_at,
];
}
}WRITE tool conventions
- Prefix the description with
⚠️ WRITE:so the LLM knows it mutates data - Use
requiredPermission()to gate write operations behind an admin permission - Always validate that the target entity exists before mutating
- Throw
InvalidArgumentExceptionfor missing/invalid input — the MCP layer catches it
Hook Timing
The ai.tools hook is fired lazily on first access to ToolRegistry. This means plugins that boot after AIServiceProvider can still register tools. No ordering concerns.
Permission Reference
| Permission Slug | Required For |
|---|---|
products_index | Reading product lists |
products_show | Reading product details |
products_create | Creating products |
products_update | Updating products |
orders_index | Reading order lists |
orders_show | Reading order details |
orders_update_status | Changing order status |
customers_index | Reading customer lists |
customers_show | Reading customer details |
customers_update | Updating customers |
files_index | Browsing media files |
files_create | Uploading files |
shipments_create | Creating shipments |
Tools without requiredPermission() (returns null) are available to any authenticated admin.
Tool Naming Conventions
- Use
snake_casefor tool names - List tools:
{entity}_list(e.g.product_list,order_list) - Detail tools:
{entity}_detail(e.g.product_detail,customer_detail) - Create/Update:
{entity}_create,{entity}_update - Autocomplete:
{entity}_autocomplete - Use the plural entity name where applicable (matching the panel permission slugs)
Response Format
Tools should return arrays, not JSON strings. The MCP layer handles serialization:
// List responses
return [
'total' => $paginator->total(),
'page' => $paginator->currentPage(),
'items' => [...],
];
// Detail responses
return [
'id' => $entity->id,
'name' => $entity->name,
// ... entity fields
];The MCP layer automatically injects _shop metadata (shop name, host, URL) into the response so the LLM knows which store it's interacting with.
Testing Your Tool
<?php
use Plugin\RecentOrders\Tools\RecentOrdersTool;
use InnoShop\AI\Services\ToolRegistry;
test('recent_orders tool is registered', function () {
$registry = app(ToolRegistry::class);
$registry->register(new RecentOrdersTool);
expect($registry->has('recent_orders'))->toBeTrue();
});
test('recent_orders tool returns data', function () {
$tool = new RecentOrdersTool;
$result = $tool->execute(['limit' => 5]);
expect($result)->toHaveKeys(['total', 'items']);
expect(count($result['items']))->toBeLessThanOrEqual(5);
});
test('recent_orders tool respects permission', function () {
$tool = new RecentOrdersTool;
expect($tool->requiredPermission())->toBe('orders_index');
});AI Chat Integration
Tools registered via ai.tools are also available in the Panel AI Chat (the built-in admin panel assistant). The same ToolInterface implementation is adapted for both MCP and the panel chat via LaravelAiToolAdapter.
Welcome Page
Every InnoShop instance with MCP enabled has a public welcome page at GET /mcp showing:
- Server endpoint URL
- Connection instructions for Claude Code / Cursor
- Token generation instructions
- Complete tool catalog with descriptions
- Plugin extension code snippet
No authentication is required to view the welcome page.
Troubleshooting
404 on /mcp
MCP is disabled. Enable it in Panel → System Settings → Tools → AI → MCP Service.
401 Unauthenticated
- Make sure you're sending the
Authorization: Bearer <token>header - Verify the token is still valid (Sanctum tokens don't expire by default)
- The authenticated user must be an
Adminmodel instance
Permission denied for tool
The tool's requiredPermission() returns a permission the current admin doesn't have. Either:
- Grant the permission to the admin role in Panel → Roles
- Remove the
requiredPermission()override if the tool should be unrestricted
Tool not appearing
- Ensure the plugin is enabled in Panel → Plugins
- Check that
Boot.phpcallsadd_hook_filter('ai.tools', ...)in itsinit()method - Verify the tool class implements
ToolInterface - Check for duplicate tool names (throws
LogicException)