1. 项目概述与核心价值

最近在折腾一个很有意思的项目,叫 orbita-pos/inariwatch-mcp 。乍一看这个标题,可能有点摸不着头脑,它不像常见的 react-app 或者 docker-compose 那样直白。但如果你对现代软件开发,特别是前端、后端与AI工具链的集成感兴趣,那这个项目绝对值得你花时间研究。简单来说,这是一个 模型上下文协议(Model Context Protocol, MCP) 的服务器实现,专门为 InariWatch 这个POS(销售终端)系统提供AI能力。

我为什么会对它感兴趣?因为在当前这个AI工具遍地开花的时代,如何让AI真正“理解”并“操作”你的业务系统,而不是仅仅停留在聊天层面,是一个巨大的挑战。 inariwatch-mcp 项目提供了一个非常具体的答案:它通过MCP协议,将InariWatch POS系统的内部数据(如库存、订单、客户信息)和操作能力(如创建订单、查询商品)暴露给AI助手(比如Claude Desktop、Cursor等),让AI能像一名资深店员一样,在获得授权后,帮你处理实际的业务查询和操作。

想象一下这个场景:你在Claude里问“我们店里还有多少瓶2022年的赤霞珠?”,AI不仅能告诉你库存数量,还能告诉你具体在哪个仓库、货架位置,甚至能帮你创建一个补货提醒。这就是 inariwatch-mcp 要实现的愿景—— 让AI成为业务系统的“超级接口” 。它解决的痛点非常明确:业务人员或开发者无需为了一个简单的数据查询去写复杂的API调用代码,也无需在多个系统间手动切换复制粘贴数据,直接用自然语言与AI对话,就能完成工作。

这个项目适合几类人:一是InariWatch POS系统的开发者或运维人员,希望为其赋予AI能力;二是对MCP协议感兴趣,想学习如何为自己的应用构建MCP服务器的开发者;三是任何想探索AI Agent如何与真实业务系统深度集成的技术爱好者。接下来,我会带你深入拆解这个项目的设计思路、技术实现,并分享从零搭建和调试的完整过程。

2. 核心架构与MCP协议解析

2.1 什么是MCP?为什么是它?

要理解 inariwatch-mcp ,必须先搞懂MCP。Model Context Protocol 是由 Anthropic 公司提出的一种开放协议,你可以把它想象成 AI 世界的“USB标准” 。在物理世界,USB接口让键盘、鼠标、U盘可以即插即用到任何电脑上。在AI世界,MCP的目标是让任何工具(数据库、API、文件系统)都能以一种标准化的方式“即插即用”到任何兼容MCP的AI助手(客户端)中。

在没有MCP之前,如果你想让Claude访问你的数据库,可能需要:

  1. 写一个专门的插件,深度绑定Claude的特定接口。
  2. 或者,自己搭建一个中间API服务器,然后通过复杂的提示词工程,教AI如何调用这个API。

这两种方式都耦合严重,扩展性差。MCP的核心思想是 解耦 标准化 。它定义了一套简单的JSON-RPC over stdio/HTTP的通信规范,以及几个核心概念:

  • 资源(Resources) : 代表AI可以读取的数据,比如一个数据库表、一个API的响应、一个文件的内容。在 inariwatch-mcp 里, inventory://current 就是一个资源,代表当前库存列表。
  • 工具(Tools) : 代表AI可以执行的操作,每个工具都有明确的输入参数和输出格式。比如 create_sales_order 就是一个工具,AI调用它时,需要提供 customer_id , items 等参数。
  • 提示词模板(Prompts) : 预定义的对话模板,可以快速引导AI完成特定任务。

inariwatch-mcp 就是一个MCP服务器(Server),它实现了上述能力,将自己(InariWatch系统)的“资源”和“工具”暴露出去。而Claude Desktop、Cursor等则是MCP客户端(Client),它们发现并连接这个服务器,然后就能使用这些资源和工具了。这种架构的优势在于, 一次开发,多处使用 。你为InariWatch写的这个MCP服务器,理论上可以被任何兼容MCP的客户端使用,未来生态会越来越丰富。

2.2 InariWatch MCP 服务器设计思路

了解了MCP,我们再来看 orbita-pos/inariwatch-mcp 的具体设计。它的核心任务是在InariWatch的业务领域和MCP的标准协议之间架起一座桥梁。

首先,它需要认证与安全。 InariWatch作为业务系统,肯定有访问控制。MCP服务器启动时,通常会通过环境变量或配置文件读取InariWatch后端的API地址( INARI_API_BASE_URL )和认证密钥( INARI_API_KEY )。所有通过MCP发起的请求,都会由这个服务器带着有效的凭证去调用真实的InariWatch API,这样就保证了业务安全,AI客户端本身不持有敏感密钥。

其次,它需要做领域建模与协议适配。 开发者需要仔细思考:InariWatch的哪些数据和操作对AI最有价值?如何将它们映射成MCP的资源(Resources)和工具(Tools)?

  • 资源映射 : 比如,将“今日销售简报”映射为一个只读资源 report://daily_sales ,将“热门商品列表”映射为 products://top_selling 。AI可以“读取”这些资源来获取信息。
  • 工具映射 : 比如,将“创建销售订单”这个API封装成一个名为 create_sales_order 的工具,并明确定义其输入参数(JSON Schema)。AI在需要时会“调用”这个工具。

最后,是协议实现与通信。 项目需要选择一个MCP的SDK来简化开发。从项目名和常见技术栈推断,它很可能使用的是 TypeScript/JavaScript 生态,基于官方或社区的 @modelcontextprotocol/sdk 来构建。服务器需要实现SDK要求的生命周期(初始化、连接、关闭),并注册上述定义好的资源和工具。通信层面,MCP服务器通常以 独立进程 方式运行,通过标准输入输出(stdio)或HTTP与客户端通信,这使得它非常轻量和便携。

注意: 在设计工具时, 权限粒度 操作幂等性 至关重要。给AI一个“删除所有数据”的工具是极其危险的。通常只提供查询、创建等低风险操作,并且对于创建类操作,要做好重复提交的防护(例如,让工具调用在传入相同参数时返回已存在的订单,而非创建新订单)。

3. 环境准备与项目初探

3.1 前置条件与工具链

在开始动手之前,你需要准备好以下环境,这和你日常做Node.js或TypeScript项目差不多:

  1. Node.js 运行环境 : 推荐使用LTS版本(如18.x, 20.x)。你可以使用 nvm (Node Version Manager) 来管理多个版本。这是运行服务器的基础。

    # 检查Node.js和npm版本
    node --version
    npm --version
    
  2. 代码编辑器或IDE : VS Code 是绝佳选择,它对TypeScript和JavaScript的支持非常好,而且有丰富的插件生态。务必安装 ESLint Prettier 插件来保持代码风格统一。

  3. Git : 用于克隆项目和版本控制。

    git --version
    
  4. InariWatch 系统的访问权限 : 这是最关键的一环。你需要知道你的InariWatch后端的API地址,并且拥有一个有效的API密钥(通常具有读取库存、订单等权限)。没有这个,MCP服务器就只是一个空壳,无法获取真实数据。这部分信息通常需要从你的InariWatch系统管理员那里获取。

  5. 一个MCP客户端用于测试 : 你需要一个能连接MCP服务器的客户端来验证功能。最常用的就是 Claude Desktop 。你需要在其配置文件中添加你即将启动的MCP服务器信息。另一个选择是 Cursor IDE ,它也内置了MCP客户端支持。

3.2 获取与初始化项目

假设项目托管在GitHub上,我们首先克隆代码到本地:

git clone https://github.com/orbita-pos/inariwatch-mcp.git
cd inariwatch-mcp

进入项目后,第一件事是查看 package.json 文件。这个文件是项目的“说明书”,它会告诉我们项目的依赖、启动脚本等信息。

// 这是一个推测的 package.json 内容示例
{
  "name": "inariwatch-mcp-server",
  "version": "0.1.0",
  "description": "MCP server for InariWatch POS system",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "ts-node src/index.ts",
    "test": "jest"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.4.0", // 核心MCP SDK
    "axios": "^1.6.0", // 用于调用InariWatch API
    "dotenv": "^16.3.0" // 用于加载环境变量
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0",
    "ts-node": "^10.9.0",
    "jest": "^29.0.0"
  }
}

package.json 可以看出,这是一个TypeScript项目,依赖了官方的MCP SDK、用于HTTP请求的axios以及环境变量管理工具dotenv。

接下来,安装项目依赖:

npm install

然后,我们需要创建环境变量配置文件。在项目根目录下创建一个 .env 文件(如果项目已有 .env.example ,可以复制一份):

cp .env.example .env

编辑 .env 文件,填入你的InariWatch系统凭证:

# InariWatch API 配置
INARI_API_BASE_URL=https://your-inari-instance.com/api/v1
INARI_API_KEY=your_super_secret_api_key_here

# MCP 服务器配置(可选)
MCP_SERVER_NAME=inariwatch
LOG_LEVEL=info

重要提示: .env 文件包含敏感信息, 绝对不能 提交到Git仓库中。请确保 .gitignore 文件中包含 .env

3.3 项目结构解析

让我们看看一个典型的 inariwatch-mcp 项目目录结构可能是什么样的:

inariwatch-mcp/
├── src/
│   ├── index.ts              # 服务器主入口,初始化MCP服务器
│   ├── resources/            # 资源定义模块
│   │   ├── inventory.ts      # 库存资源
│   │   ├── products.ts       # 商品资源
│   │   └── reports.ts        # 报表资源
│   ├── tools/                # 工具定义模块
│   │   ├── sales.ts          # 销售相关工具(创建订单)
│   │   └── queries.ts        # 查询类工具
│   ├── clients/              # InariWatch API客户端封装
│   │   └── inariClient.ts    # 基于axios的API客户端,处理认证和请求
│   └── types/                # TypeScript类型定义
│       └── index.ts
├── dist/                     # TypeScript编译后的输出目录
├── package.json
├── tsconfig.json             # TypeScript配置文件
├── .env                      # 环境变量(本地,勿提交)
└── README.md

这种结构清晰地将MCP协议层(资源、工具)、业务逻辑层(API客户端)和类型定义分离开,是构建可维护MCP服务器的良好实践。

4. 核心模块实现深度剖析

4.1 InariWatch API 客户端封装

这是所有数据和操作的源头,一个健壮、易用的API客户端是基石。我们通常在 src/clients/inariClient.ts 中实现。

// src/clients/inariClient.ts
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import dotenv from 'dotenv';

dotenv.config(); // 加载 .env 文件中的变量

// 定义从InariWatch API返回的通用响应结构(根据实际API调整)
export interface InariApiResponse<T = any> {
  success: boolean;
  data: T;
  message?: string;
}

// 定义一些业务实体类型
export interface InventoryItem {
  id: string;
  product_id: string;
  product_name: string;
  sku: string;
  quantity: number;
  location?: string;
  low_stock_threshold: number;
}

export interface SalesOrderInput {
  customer_id: string;
  items: Array<{
    product_id: string;
    quantity: number;
    unit_price?: number; // 可选,系统可能自动定价
  }>;
  notes?: string;
}

export class InariClient {
  private client: AxiosInstance;

  constructor() {
    const baseURL = process.env.INARI_API_BASE_URL;
    const apiKey = process.env.INARI_API_KEY;

    if (!baseURL || !apiKey) {
      throw new Error('INARI_API_BASE_URL and INARI_API_KEY must be set in environment variables.');
    }

    this.client = axios.create({
      baseURL,
      timeout: 10000, // 10秒超时
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
    });

    // 可选:添加响应拦截器进行统一错误处理
    this.client.interceptors.response.use(
      (response: AxiosResponse<InariApiResponse>) => {
        // 如果API自身有 success: false 的标识,也抛出错误
        if (response.data && response.data.success === false) {
          return Promise.reject(new Error(response.data.message || 'API request failed'));
        }
        return response;
      },
      (error) => {
        // 网络错误或HTTP状态码非2xx
        console.error(`[InariClient] API Error: ${error.message}`);
        return Promise.reject(error);
      }
    );
  }

  // 获取当前库存列表
  async getInventory(): Promise<InventoryItem[]> {
    const response = await this.client.get<InariApiResponse<InventoryItem[]>>('/inventory');
    return response.data.data; // 返回 data 字段中的数组
  }

  // 根据ID获取单个商品详情
  async getProduct(productId: string): Promise<any> { // 类型可细化
    const response = await this.client.get<InariApiResponse>(`/products/${productId}`);
    return response.data.data;
  }

  // 创建销售订单
  async createSalesOrder(order: SalesOrderInput): Promise<{ order_id: string; total: number }> {
    const response = await this.client.post<InariApiResponse<{ order_id: string; total: number }>>('/sales/orders', order);
    return response.data.data;
  }

  // 可以继续添加其他API方法:getCustomers, getDailyReport等
}

关键点解析:

  1. 环境变量验证 :在构造函数中立即检查关键配置是否存在,及早失败,避免运行时出现神秘错误。
  2. Axios实例配置 :统一设置基础URL、超时和认证头,避免在每个请求中重复。
  3. 响应拦截器 :这是提升健壮性的关键。它可以将API返回的业务逻辑错误(如 success: false )和HTTP错误统一转化为JavaScript Error,便于上层(资源/工具层)用 try...catch 一致处理。
  4. 清晰的接口 :每个方法对应一个明确的业务操作,返回强类型(或至少是 any 但注明)的结果。这为后续编写资源(Resource)和工具(Tool)提供了坚实基础。

4.2 资源(Resources)定义与实现

资源是AI获取信息的窗口。MCP中的资源有一个唯一的URI(如 inventory://current ),并且有对应的 mimeType (如 application/json )。服务器需要提供一个 readResource 方法来处理客户端的资源读取请求。

让我们实现库存资源,在 src/resources/inventory.ts 中:

// src/resources/inventory.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { InariClient, InventoryItem } from '../clients/inariClient.js';

// 定义资源URI的常量,避免硬编码
export const INVENTORY_RESOURCE_URI = 'inari://inventory/current';

export function setupInventoryResources(server: Server, inariClient: InariClient) {
  // 1. 声明服务器提供了哪些资源(用于客户端发现)
  server.setRequestHandler('resources/list', async () => {
    return {
      resources: [
        {
          uri: INVENTORY_RESOURCE_URI,
          name: 'Current Inventory Levels',
          description: '实时查看所有商品的当前库存数量及位置信息。',
          mimeType: 'application/json',
        },
        // 可以在此添加更多资源,如 `inari://inventory/low-stock`
      ],
    };
  });

  // 2. 处理客户端对特定资源的读取请求
  server.setRequestHandler('resources/read', async (request) => {
    if (request.params.uri === INVENTORY_RESOURCE_URI) {
      try {
        const inventoryData = await inariClient.getInventory();
        
        // 将数据转换为MCP资源要求的格式
        // 内容通常是字符串,对于JSON,我们将其序列化
        return {
          contents: [
            {
              uri: request.params.uri,
              mimeType: 'application/json',
              // 可以在这里对数据进行一些处理,使其对AI更友好
              // 例如,只保留关键字段,或添加总结性文字
              text: JSON.stringify({
                summary: `当前共有 ${inventoryData.length} 种商品在库存中。`,
                items: inventoryData.map(item => ({
                  name: item.product_name,
                  sku: item.sku,
                  quantity: item.quantity,
                  location: item.location || '主仓库',
                  isLow: item.quantity <= item.low_stock_threshold,
                })),
                timestamp: new Date().toISOString(),
              }, null, 2), // 美化JSON输出,方便AI阅读
            },
          ],
        };
      } catch (error: any) {
        // 优雅地处理错误,返回给客户端
        console.error(`Failed to fetch inventory: ${error.message}`);
        return {
          contents: [
            {
              uri: request.params.uri,
              mimeType: 'text/plain',
              text: `无法获取库存数据:${error.message}`,
            },
          ],
        };
      }
    }
    // 如果请求的URI不是本服务器处理的,可以返回空或抛出错误
    return { contents: [] };
  });
}

设计要点与心得:

  • URI设计 :采用 scheme://path 的形式,如 inari://inventory/current ,清晰且有命名空间,避免冲突。
  • 数据格式化 :直接返回原始API数据可能对AI来说信息过载或结构不佳。在 text 字段中,我们构造了一个更清晰的结构,包含总结、物品列表和时间戳。 让数据对AI友好 是资源设计的重要原则。
  • 错误处理 :不要因为后端API出错就让整个MCP请求崩溃。捕获异常并返回一个包含错误信息的纯文本资源,AI助手能够理解并告知用户“暂时无法获取数据”。
  • 资源列表 resources/list 处理程序相当于一个目录,告诉客户端“我这里有什么可以看的”。保持描述准确、有用。

4.3 工具(Tools)定义与实现

工具是AI的“手”,让AI可以执行操作。工具需要定义清晰的输入参数模式(JSON Schema)和一个执行函数。

以创建销售订单工具为例,在 src/tools/sales.ts 中:

// src/tools/sales.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { InariClient, SalesOrderInput } from '../clients/inariClient.js';

export function setupSalesTools(server: Server, inariClient: InariClient) {
  // 声明服务器提供的工具列表
  server.setRequestHandler('tools/list', async () => {
    return {
      tools: [
        {
          name: 'create_sales_order',
          description: '在InariWatch系统中创建一个新的销售订单。需要客户ID和商品列表。',
          inputSchema: {
            type: 'object',
            properties: {
              customer_id: {
                type: 'string',
                description: '客户在系统中的唯一标识符(ID)。',
              },
              items: {
                type: 'array',
                description: '订单中包含的商品列表。',
                items: {
                  type: 'object',
                  properties: {
                    product_id: {
                      type: 'string',
                      description: '商品的唯一标识符(ID)。',
                    },
                    quantity: {
                      type: 'number',
                      description: '购买数量,必须大于0。',
                      minimum: 1,
                    },
                    unit_price: {
                      type: 'number',
                      description: '商品单价(可选)。如果省略,将使用系统默认价格。',
                      minimum: 0,
                    },
                  },
                  required: ['product_id', 'quantity'],
                },
              },
              notes: {
                type: 'string',
                description: '订单备注(可选)。',
              },
            },
            required: ['customer_id', 'items'],
          },
        },
        // 可以在此添加更多工具,如 `search_products`, `update_inventory` 等
      ],
    };
  });

  // 处理工具调用请求
  server.setRequestHandler('tools/call', async (request) => {
    if (request.params.name === 'create_sales_order') {
      const args = request.params.arguments as unknown as SalesOrderInput; // 信任Schema验证后的类型
      
      // 在实际调用前,可以进行一些业务逻辑验证
      if (!args.items || args.items.length === 0) {
        return {
          content: [
            {
              type: 'text',
              text: '错误:订单商品列表不能为空。',
            },
          ],
          isError: true,
        };
      }

      try {
        console.log(`[Tool Call] Creating sales order for customer: ${args.customer_id}`);
        const result = await inariClient.createSalesOrder(args);
        
        // 返回一个对AI和用户都友好的成功消息
        return {
          content: [
            {
              type: 'text',
              text: `✅ 销售订单创建成功!\n订单号:${result.order_id}\n订单总额:¥${result.total.toFixed(2)}\n\n系统已处理该订单,库存已相应更新。`,
            },
          ],
        };
      } catch (error: any) {
        console.error(`[Tool Call Failed] create_sales_order: ${error.message}`);
        // 根据错误类型返回更具体的提示
        let errorMessage = `创建订单失败:${error.message}`;
        if (error.response?.status === 404) {
          errorMessage = '创建失败:未找到指定的客户或商品,请检查ID是否正确。';
        } else if (error.response?.status === 422) {
          errorMessage = '创建失败:商品库存不足或数据验证错误。';
        }
        
        return {
          content: [
            {
              type: 'text',
              text: `❌ ${errorMessage}`,
            },
          ],
          isError: true,
        };
      }
    }
    // 如果调用的工具名不匹配,返回工具未找到错误
    return {
      content: [
        {
          type: 'text',
          text: `错误:未知工具 "${request.params.name}"。`,
        },
      ],
      isError: true,
    };
  });
}

工具设计的核心经验:

  1. 详尽的输入模式(Input Schema) :这是AI能正确调用工具的关键。 description 字段要尽可能清晰,告诉AI这个参数是什么、从哪里获取(例如,“客户在系统中的唯一标识符”)。使用 required minimum 等约束条件,能帮助AI在调用前进行自我检查,减少无效调用。
  2. 前置验证 :即使在Schema验证通过后,在工具执行函数内部进行额外的业务逻辑验证也是好习惯(如检查商品列表非空)。这比让API调用失败后再处理更清晰。
  3. 友好的输出 :工具返回的 text 是直接呈现给用户的(通过AI转述)。因此,信息要完整、清晰、可读。使用表情符号(如 ✅, ❌)、换行和格式化,能极大提升用户体验。
  4. 细致的错误处理 :捕获API调用错误,并根据HTTP状态码或错误信息,返回对用户有指导意义的错误提示,而不是堆栈跟踪。 isError: true 的标记能让AI客户端知道这是一个错误结果。
  5. 日志记录 :在工具调用关键节点(开始、成功、失败)添加日志,对于后期调试和监控至关重要。

5. 服务器集成、启动与客户端配置

5.1 主服务器入口集成

现在我们需要把资源、工具和客户端整合起来,在 src/index.ts 中创建MCP服务器实例并启动它。

// src/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { InariClient } from './clients/inariClient.js';
import { setupInventoryResources } from './resources/inventory.js';
import { setupSalesTools } from './tools/sales.js';
// 导入其他资源和工具模块...

async function main() {
  // 1. 初始化InariWatch API客户端
  const inariClient = new InariClient();
  console.error('[InariWatch MCP] API客户端初始化成功。'); // 使用 stderr 输出日志,避免干扰 stdio 通信

  // 2. 创建MCP服务器实例
  const server = new Server(
    {
      name: process.env.MCP_SERVER_NAME || 'inariwatch-mcp',
      version: '0.1.0',
    },
    {
      capabilities: {
        // 声明服务器具备哪些能力
        resources: {}, // 提供资源
        tools: {},     // 提供工具
        // prompts: {}, // 如果提供提示词模板,也需声明
      },
    }
  );

  // 3. 设置资源、工具等处理器
  setupInventoryResources(server, inariClient);
  setupSalesTools(server, inariClient);
  // 设置其他模块...

  // 4. 处理连接错误和关闭信号
  server.onerror = (error) => {
    console.error('[MCP Server Error]', error);
  };
  
  process.on('SIGINT', async () => {
    await server.close();
    process.exit(0);
  });

  // 5. 创建传输层并连接(使用标准输入输出)
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('[InariWatch MCP] 服务器已启动,正在通过 stdio 等待客户端连接...');
}

main().catch((error) => {
  console.error('[Fatal Error] Failed to start server:', error);
  process.exit(1);
});

5.2 编译与运行

由于我们使用TypeScript开发,需要先编译再运行,或者使用 ts-node 在开发时直接运行。

生产/测试运行:

# 编译TypeScript到JavaScript
npm run build

# 运行编译后的代码
npm start
# 或者直接运行
node dist/index.js

开发模式运行(使用ts-node,方便调试):

npm run dev

如果 package.json dev 脚本配置为 ts-node src/index.ts ,这将直接执行TypeScript源码。

当服务器成功启动后,你会在控制台看到日志,然后它就会挂起,等待MCP客户端通过标准输入输出与其建立连接。

5.3 配置 Claude Desktop 客户端

要让Claude Desktop能使用我们的服务器,需要编辑其配置文件。配置文件的位置因操作系统而异:

  • macOS : ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows : %APPDATA%\Claude\claude_desktop_config.json
  • Linux : ~/.config/Claude/claude_desktop_config.json

如果文件不存在,就创建它。然后添加以下配置:

{
  "mcpServers": {
    "inariwatch": {
      "command": "node",
      "args": [
        "/ABSOLUTE/PATH/TO/YOUR/inariwatch-mcp/dist/index.js"
      ],
      "env": {
        "INARI_API_BASE_URL": "https://your-inari-instance.com/api/v1",
        "INARI_API_KEY": "your_super_secret_api_key_here",
        "LOG_LEVEL": "debug"
      }
    }
  }
}

配置详解与注意事项:

  1. command : 是启动服务器的命令。因为我们编译成了Node.js可执行文件,所以是 node
  2. args : 是传递给命令的参数。 这里必须使用 dist/index.js 的绝对路径 。相对路径在Claude Desktop的上下文中可能无法正确解析。你可以使用 pwd 命令获取项目的绝对路径。
  3. env : 这里设置的环境变量会覆盖项目 .env 文件中的设置。 这是一种更安全的做法 ,尤其是对于API密钥,你可以将其保留在Claude的配置中,而不是项目代码目录里。注意,在配置文件中明文存储密钥仍有风险,请确保系统安全。
  4. 配置完成后, 需要完全重启Claude Desktop 才能加载新的MCP服务器配置。

重启Claude后,当你新建一个对话,理论上在输入框附近或设置里,应该能看到一个“连接的工具”或类似提示,显示 inariwatch 已可用。你也可以直接问Claude:“你现在可以使用哪些工具?” 或者 “你能访问InariWatch的库存信息吗?”,它应该能列出你定义的工具和资源。

6. 调试、测试与问题排查实录

即使一切配置看起来都正确,第一次运行时也难免会遇到问题。下面是我在搭建和调试过程中遇到的一些典型情况及解决方法。

6.1 常见问题速查表

问题现象 可能原因 排查步骤与解决方案
Claude Desktop 完全看不到MCP服务器 1. 配置文件路径或格式错误。
2. 服务器启动命令失败。
3. Claude Desktop 未重启。
1. 检查配置文件JSON语法,确保无错误。
2. 在终端手动运行配置中的 command args ,看服务器能否独立启动并打印日志。
3. 务必彻底关闭并重启Claude Desktop
服务器启动后立即退出,报错 Error: INARI_API_BASE_URL and INARI_API_KEY must be set 环境变量未正确设置。 1. 检查 .env 文件是否存在且格式正确(无空格,无引号)。
2. 如果通过Claude配置的 env 设置,确保键名正确。
3. 在服务器代码启动处临时添加 console.log(process.env) 打印所有环境变量进行调试。
Claude 能看到工具/资源列表,但调用时失败,提示“无法连接”或超时 1. InariWatch API 地址不可达。
2. API 密钥无效或权限不足。
3. 服务器代码中存在未处理的异常导致崩溃。
1. 使用 curl 或 Postman 直接测试 INARI_API_BASE_URL 的端点,验证网络和认证。
2. 在 InariClient 的构造函数或方法中添加详细日志,查看请求是否发出、响应是什么。
3. 确保服务器代码有全面的 try...catch ,并将错误信息通过MCP协议返回,而不是让进程崩溃。
工具调用时,AI传递的参数格式不对 1. 工具的 inputSchema 定义不够严格或描述不清。
2. AI对业务逻辑理解有偏差。
1. 仔细检查 inputSchema required 字段和每个属性的 description 。描述应尽可能指导AI如何获取该参数(如“客户的ID,可以从客户列表资源中获取”)。
2. 在工具的实现函数开头打印接收到的 args ,确认实际传入的数据结构。
资源可以读取,但AI无法理解内容 资源返回的 text 格式对AI不友好。 优化资源返回的数据结构。避免嵌套过深的JSON。可以添加一个 summary 字段用自然语言总结,或将列表数据以更清晰的键值对形式呈现。目标是让AI能轻松提取关键信息。
服务器日志正常,但Claude中无反应 MCP通信协议版本不匹配或序列化问题。 1. 检查使用的 @modelcontextprotocol/sdk 版本是否与Claude Desktop兼容。查看SDK和Claude的更新日志。
2. 确保服务器返回的JSON严格符合MCP协议规范。使用简单的 console.error 输出原始请求和响应对象进行对比分析。

6.2 高级调试技巧:启用详细日志

在开发阶段,开启详细日志是定位问题的利器。除了在代码中手动添加 console.error (注意MCP服务器建议将日志输出到 stderr ),还可以利用环境变量控制日志级别。

修改你的 InariClient 和主服务器文件,引入一个简单的日志函数:

// src/utils/logger.ts
const LOG_LEVEL = (process.env.LOG_LEVEL || 'info').toLowerCase();

export function debug(...args: any[]) {
  if (LOG_LEVEL === 'debug') {
    console.error('[DEBUG]', new Date().toISOString(), ...args);
  }
}

export function info(...args: any[]) {
  if (LOG_LEVEL === 'debug' || LOG_LEVEL === 'info') {
    console.error('[INFO]', new Date().toISOString(), ...args);
  }
}

export function error(...args: any[]) {
  console.error('[ERROR]', new Date().toISOString(), ...args);
}

然后在客户端和工具调用处使用:

import { debug, info } from '../utils/logger.js';

// 在 InariClient 请求前
debug(`Making API call to: ${url}`);

// 在 tools/call 处理程序中
info(`Tool called: ${request.params.name} with args:`, JSON.stringify(args));

在Claude配置或启动命令中设置 LOG_LEVEL=debug ,你就能在控制台看到所有详细的请求和响应信息,这对于理解AI与服务器的交互流程非常有帮助。

6.3 安全与生产环境考量

当这个MCP服务器准备投入生产环境使用时,有几个关键点必须考虑:

  1. 权限最小化 :授予MCP服务器使用的API密钥 绝对最小 的权限。通常只赋予“读取”库存、商品和“创建”订单的权限,绝不要赋予“删除”或“修改系统设置”等高风险权限。
  2. 输入验证与净化 :虽然MCP SDK和工具的JSON Schema会做基础验证,但在调用真实业务API前,务必再次验证数据。例如,检查商品ID是否存在、数量是否为正整数等。
  3. 速率限制与防滥用 :考虑在MCP服务器层面添加简单的速率限制(例如,使用 express-rate-limit 如果走HTTP传输,或自定义计数器),防止被恶意工具调用轰炸。
  4. 配置管理 :生产环境的API密钥、URL等不应写在代码或普通配置文件中。应使用安全的密钥管理服务(如AWS Secrets Manager, HashiCorp Vault)或至少是容器环境变量。
  5. 进程监控与守护 :确保MCP服务器进程在异常退出时能自动重启。可以使用 systemd , pm2 或容器编排平台(如Kubernetes)的健康检查来实现。

7. 扩展思路与最佳实践

一个基础的MCP服务器运行起来后,你可以从以下几个方向扩展它,使其更加强大和实用。

7.1 实现更复杂的工具链

  • 链式工具调用 :设计工具时,考虑它们的组合性。例如,一个 find_customer_by_phone 工具返回客户ID,然后这个ID可以直接用于 create_sales_order 工具。在工具描述中明确说明这一点。
  • 查询类工具 :实现 search_products 工具,允许AI根据名称、SKU或分类模糊搜索商品,并返回一个列表供用户或AI选择。
  • 只读操作工具 :实现 get_daily_sales_summary 工具,它可能内部调用多个API,聚合数据后返回一份格式精美的文本摘要,比单纯的资源读取更智能。

7.2 设计对AI更友好的资源

  • 聚合资源 :创建一个 dashboard://overview 资源,它内部调用库存、当日销售、低库存预警等多个API,整合成一个全面的“仪表盘”视图,让AI能一次性获取业务全景。
  • 动态资源URI :支持带参数的资源URI,如 inari://product/{id} 。在 resources/read 处理程序中解析URI中的变量,然后动态查询对应商品的信息。
  • 资源更新通知 :虽然MCP协议本身是请求-响应式,但你可以通过设计,让资源的内容摘要反映出“最后更新时间”,提示AI信息的时效性。

7.3 性能优化

  • 缓存策略 :对于不常变化的数据(如商品分类、门店列表),可以在MCP服务器内存中设置短期缓存(例如,使用 node-cache ),减少对后端API的重复调用,提升AI助手的响应速度。
  • 批量操作 :如果AI经常需要同时查询多个商品的信息,可以考虑实现一个 get_multiple_products 的工具或资源,接受ID数组,服务器内部批量调用API,比多次单个查询更高效。

7.4 测试策略

  • 单元测试 :为 InariClient 和工具函数编写单元测试,使用 jest axios-mock-adapter 来模拟API响应,确保业务逻辑正确。
  • 集成测试 :编写一个简单的测试脚本,模拟MCP客户端向你的服务器发送标准的JSON-RPC请求(如 tools/list , tools/call ),验证端到端的流程。
  • 端到端测试 :在安全的环境下,配置好Claude Desktop,进行真实的人工对话测试,覆盖各种正常和边缘用例。

构建 inariwatch-mcp 这样的项目,最大的成就感来自于看到冰冷的业务系统通过一个协议,突然拥有了自然语言的交互界面。它不仅仅是技术集成,更是工作流的一次革新。从我的经验来看, 起步时功能不必求全,但每个暴露给AI的工具或资源都必须极其可靠和安全 。先实现一个最小可行集(如查询库存和创建订单),稳定运行并收集用户反馈,再逐步迭代扩展,这样能最有效地控制风险并持续交付价值。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐