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)