Skip to content

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:

  1. Go to Panel → System Settings → Tools → AI
  2. Find the MCP Service card
  3. Toggle the switch to enable

Or programmatically:

php
system_setting('mcp_enabled', true);

When disabled, all MCP endpoints return 404.

Security Layers ​

LayerMechanismPurpose
Feature togglesystem_setting('mcp_enabled')Master on/off switch
Origin validationValidateMcpOrigin middlewareBlocks DNS rebinding attacks
AuthenticationLaravel Sanctum (auth:sanctum)Requires admin API token
Authorization$user->can($permission)Tool-level permission checks

Getting an API Token ​

bash
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 ​

bash
claude mcp add --transport http innoshop https://your-store.com/mcp \
  --header "Authorization: Bearer YOUR_TOKEN"

Cursor / Cline ​

Add to your MCP configuration:

json
{
  "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.

CategoryTools
Productsproduct_list, product_detail, product_create ⚠️, product_update ⚠️, product_autocomplete, sku_autocomplete
Ordersorder_list, order_detail, order_update_status ⚠️, order_note ⚠️, order_return_list, order_return_detail
Customerscustomer_list, customer_detail, customer_update ⚠️, customer_autocomplete
Categoriescategory_list, category_detail, category_create ⚠️, category_update ⚠️, category_autocomplete
Brandsbrand_list
Shippingshipment_list, shipment_detail, shipment_create ⚠️, shipment_traces
Analyticsdashboard, sales_stats, stock_report
Contentarticle_list, article_detail, catalog_list, page_list, tag_list
Filesfile_list, file_upload ⚠️
Reviewsreview_list, review_detail
Attributesattribute_list, option_list
Localizationlocale_list, currency_list, country_list, region_list
Taxtax_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):

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):

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.php

2. config.json

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
<?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
<?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
<?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 InvalidArgumentException for 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 SlugRequired For
products_indexReading product lists
products_showReading product details
products_createCreating products
products_updateUpdating products
orders_indexReading order lists
orders_showReading order details
orders_update_statusChanging order status
customers_indexReading customer lists
customers_showReading customer details
customers_updateUpdating customers
files_indexBrowsing media files
files_createUploading files
shipments_createCreating shipments

Tools without requiredPermission() (returns null) are available to any authenticated admin.

Tool Naming Conventions ​

  • Use snake_case for 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:

php
// 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
<?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 Admin model 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.php calls add_hook_filter('ai.tools', ...) in its init() method
  • Verify the tool class implements ToolInterface
  • Check for duplicate tool names (throws LogicException)