Skip to content

MCP AI 集成

概述

InnoShop 通过 MCP(Model Context Protocol) 将后台能力暴露为 AI 可调用的工具,让 Claude Code、Cursor、Cline 等 AI 助手可以直接与你的店铺数据交互 — 查询产品、管理订单、检查库存等。

MCP 基于 JSON-RPC 2.0 over HTTP(Streamable HTTP 传输),出于安全考虑默认关闭。

架构

MCP 系统分为三层,完全解耦:

┌─────────────────────────────────────┐
│  Claude Code / Cursor / Cline       │  ← MCP 客户端
│  (JSON-RPC 2.0 over HTTP)           │
└──────────────┬──────────────────────┘

┌──────────────▼──────────────────────┐
│  innopacks/mcp                      │  ← 协议层
│  InnoShopMcpServer                  │     Sanctum 认证、Origin 校验、
│  RegistryToolAdapter                 │     开关控制、响应格式化
│  McpController (欢迎页)             │
└──────────────┬──────────────────────┘
               │ ToolInterface 契约
┌──────────────▼──────────────────────┐
│  innopacks/ai                       │  ← 工具定义
│  ToolRegistry (单例)                │     45+ 内置工具、
│  BaseTool, ToolInterface            │     插件通过 Hook 扩展
│  PanelChatAgent                     │
└──────────────┬──────────────────────┘

┌──────────────▼──────────────────────┐
│  innopacks/common                   │  ← 业务逻辑
│  Repositories, Models, Services     │
└─────────────────────────────────────┘

核心原则:工具只是现有 Repository 的薄适配层 — 不包含任何业务逻辑。

启用 MCP

MCP 由系统设置控制,可在管理后台开启:

  1. 进入 面板 → 系统设置 → 工具 → AI
  2. 找到 MCP 服务 卡片
  3. 打开开关

或通过代码:

php
system_setting('mcp_enabled', true);

关闭后,所有 MCP 端点返回 404

安全层级

层级机制用途
功能开关system_setting('mcp_enabled')总开关
Origin 校验ValidateMcpOrigin 中间件阻止 DNS 重绑定攻击
身份认证Laravel Sanctum (auth:sanctum)需要管理员 API Token
权限控制$user->can($permission)工具级别的权限检查

获取 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"}'

返回的 token 字段即 Bearer Token:

Authorization: Bearer <token>

连接 AI 助手

Claude Code

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

Cursor / Cline

在 MCP 配置中添加:

json
{
  "mcpServers": {
    "innoshop": {
      "url": "https://your-store.com/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_TOKEN"
      }
    }
  }
}

内置工具

InnoShop 内置 45+ 个工具,覆盖全部店铺操作。标记 ⚠️ 的为写入操作。

分类工具
产品product_list, product_detail, product_create ⚠️, product_update ⚠️, product_autocomplete, sku_autocomplete
订单order_list, order_detail, order_update_status ⚠️, order_note ⚠️, order_return_list, order_return_detail
客户customer_list, customer_detail, customer_update ⚠️, customer_autocomplete
分类category_list, category_detail, category_create ⚠️, category_update ⚠️, category_autocomplete
品牌brand_list
物流shipment_list, shipment_detail, shipment_create ⚠️, shipment_traces
统计dashboard, sales_stats, stock_report
内容article_list, article_detail, catalog_list, page_list, tag_list
文件file_list, file_upload ⚠️
评价review_list, review_detail
属性attribute_list, option_list
本地化locale_list, currency_list, country_list, region_list
税费tax_rate_list, tax_class_list

完整列表始终可在 GET /mcp 公开欢迎页查看。

在插件中开发 MCP 工具

插件可以通过 ai.tools Hook 注册自定义 MCP 工具。这是主要的扩展点 — 注册后,工具自动出现在 MCP tools/listtools/call、欢迎页和后台 AI 聊天中。

核心契约

ToolInterface (innopacks/ai/src/Contracts/ToolInterface.php):

php
interface ToolInterface
{
    public function name(): string;                  // 唯一标识,snake_case
    public function description(): string;            // 给 LLM 看的描述
    public function inputSchema(): array;             // 参数的 JSON Schema
    public function requiredPermission(): ?string;    // 权限标识,null = 无需权限
    public function execute(array $arguments): mixed; // 执行,参数已经过校验
}

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;  // 默认无需权限
    }
}

分步指南:只读工具

我们来构建一个暴露 recent_orders 工具的插件。创建以下文件:

1. 插件目录结构

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 '获取最近订单,支持按状态筛选。';
    }

    public function inputSchema(): array
    {
        return [
            'type'       => 'object',
            'properties' => [
                'limit'  => [
                    'type'        => 'integer',
                    'description' => '返回数量,默认 10,最大 50',
                ],
                'status' => [
                    'type'        => 'string',
                    'description' => '按状态筛选: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;
        });
    }
}

完成。安装并启用插件后,recent_orders 工具自动出现在 MCP 欢迎页和 tools/list 中。

分步指南:写入工具

写入工具遵循相同模式,但权限更严格:

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: 将未付款订单标记为已付款。';
    }

    public function inputSchema(): array
    {
        return [
            'type'       => 'object',
            'properties' => [
                'number'  => ['type' => 'string', 'description' => '订单号'],
                'comment' => ['type' => 'string', 'description' => '备注(可选)'],
            ],
            '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("订单 [{$arguments['number']}] 不存在。");
        }

        $order->status = 'paid';
        $order->save();

        return [
            'number'     => $order->number,
            'status'     => $order->status,
            'updated_at' => (string) $order->updated_at,
        ];
    }
}

写入工具约定

  • 在描述开头加 ⚠️ WRITE: 让 LLM 知道这是会修改数据的操作
  • requiredPermission() 将写入操作限制在特定后台权限之后
  • 始终在修改前验证目标实体是否存在
  • 对缺失/无效输入抛出 InvalidArgumentException — MCP 层会捕获并转为错误响应

Hook 时机

ai.tools Hook 在首次访问 ToolRegistry延迟触发。这意味着在 AIServiceProvider 之后启动的插件也能注册工具,无需关心启动顺序。

权限参考

权限标识所需场景
products_index读取产品列表
products_show读取产品详情
products_create创建产品
products_update更新产品
orders_index读取订单列表
orders_show读取订单详情
orders_update_status修改订单状态
customers_index读取客户列表
customers_show读取客户详情
customers_update更新客户
files_index浏览媒体文件
files_create上传文件
shipments_create创建发货单

不设置 requiredPermission()(返回 null)的工具对所有已认证管理员开放。

工具命名规范

  • 使用 snake_case 命名
  • 列表类:{entity}_list(如 product_listorder_list
  • 详情类:{entity}_detail(如 product_detailcustomer_detail
  • 创建/更新:{entity}_create{entity}_update
  • 自动补全:{entity}_autocomplete
  • 实体名使用复数形式(与后台权限标识保持一致)

响应格式

工具应返回数组而非 JSON 字符串,MCP 层负责序列化:

php
// 列表响应
return [
    'total' => $paginator->total(),
    'page'  => $paginator->currentPage(),
    'items' => [...],
];

// 详情响应
return [
    'id'    => $entity->id,
    'name'  => $entity->name,
    // ... 实体字段
];

MCP 层会自动在响应中注入 _shop 元数据(店铺名称、域名、URL),让 LLM 知道自己在操作哪个店铺。

测试工具

php
<?php

use Plugin\RecentOrders\Tools\RecentOrdersTool;
use InnoShop\AI\Services\ToolRegistry;

test('recent_orders 工具已注册', function () {
    $registry = app(ToolRegistry::class);
    $registry->register(new RecentOrdersTool);

    expect($registry->has('recent_orders'))->toBeTrue();
});

test('recent_orders 工具返回数据', function () {
    $tool = new RecentOrdersTool;
    $result = $tool->execute(['limit' => 5]);

    expect($result)->toHaveKeys(['total', 'items']);
    expect(count($result['items']))->toBeLessThanOrEqual(5);
});

test('recent_orders 工具有正确的权限要求', function () {
    $tool = new RecentOrdersTool;
    expect($tool->requiredPermission())->toBe('orders_index');
});

AI 聊天集成

通过 ai.tools 注册的工具同时也在后台 AI 聊天(内置管理面板助手)中可用。同一个 ToolInterface 实现由 LaravelAiToolAdapter 适配为 MCP 和面板聊天两套接口。

欢迎页

每个启用 MCP 的 InnoShop 实例都有一个公开欢迎页 GET /mcp,展示:

  • 服务端点 URL
  • Claude Code / Cursor 连接说明
  • Token 获取指引
  • 完整工具目录及描述
  • 插件扩展示例代码

欢迎页无需认证即可访问。

常见问题

访问 /mcp 返回 404

MCP 功能未开启。在 面板 → 系统设置 → 工具 → AI → MCP 服务 中启用。

401 Unauthenticated

  • 确认请求携带 Authorization: Bearer <token>
  • 验证 Token 仍然有效(Sanctum Token 默认不过期)
  • 认证用户必须是 Admin 模型实例

工具返回 Permission denied

工具的 requiredPermission() 返回了当前管理员不具备的权限。解决方案:

  • 在 面板 → 角色管理 中授予相应权限
  • 如果工具应该无限制使用,移除 requiredPermission() 的返回值

工具没有出现

  • 确认插件已在 面板 → 插件管理 中启用
  • 检查 Boot.phpinit() 方法中是否调用了 add_hook_filter('ai.tools', ...)
  • 确认工具类实现了 ToolInterface 接口
  • 检查是否有重名工具(会抛出 LogicException

帆连科技 · 基于 OSL 3.0 许可发布