基于MCP协议封装Xendit支付API:构建标准化支付服务层

2026-08-04 17:04:3718 阅读量

1. 项目概述:一个连接Xendit支付网关的MCP服务器

最近在折腾东南亚市场的支付集成,发现很多开发者对Xendit这个支付网关又爱又恨。爱的是它在印尼、菲律宾等地的覆盖确实广,恨的是它的API文档虽然全,但真要快速集成到自己的应用里,尤其是想实现一个统一、可复用的支付服务层,还是得花不少功夫。这不,我最近就基于MCP(Model Context Protocol)协议,搞了一个专门对接Xendit的服务器项目,名字就叫 mrslbt/xendit-mcp

相关服务:菲律宾服务器

简单来说,这个项目就是一个“翻译官”或者说“适配器”。它把Xendit那套复杂的REST API,封装成了MCP协议标准化的工具(Tools)和资源(Resources)。这样一来,任何支持MCP协议的客户端(比如一些AI助手、自动化工作流平台,或者你自己的后台服务),都能用一种统一、声明式的方式来调用Xendit的功能,比如创建支付链接、查询交易状态、处理退款等等,而不用再去深究Xendit API的每个细节。

这个项目最适合谁呢?我觉得有两类朋友会特别需要。一类是正在或计划开拓东南亚市场的电商、SaaS开发者,你们需要快速、稳定地接入本地化支付,但又不想把支付逻辑和业务代码强耦合。另一类是对AI Agent或者自动化流程感兴趣的技术爱好者,你们可能想构建一个能自动处理支付、对账的智能助手, xendit-mcp 能提供一个标准化的支付操作接口。接下来,我就把这个项目的设计思路、核心实现、踩过的坑以及怎么用,掰开揉碎了跟大家聊聊。

2. 核心设计思路:为什么选择MCP协议来封装支付API?

2.1 直面Xendit API集成的典型痛点

在决定用MCP之前,我复盘了一下直接调用Xendit API的几个常见麻烦事。首先, 认证复杂 。Xendit主要使用Secret Key进行HTTP Basic Auth,每个API请求都要在Header里正确编码。虽然不复杂,但容易写错,而且密钥管理本身就是一个安全课题。其次, 数据模型转换繁琐 。Xendit的请求和响应体字段很多,有些嵌套很深,比如创建虚拟账户(Virtual Account)时,不同银行的参数要求还不完全一样。在业务代码里直接处理这些JSON,会让代码变得臃肿且难以测试。第三, 错误处理不统一 。网络错误、API限流、业务逻辑错误(如余额不足)混在一起,需要一套健壮的机制来区分和处理。最后,也是最关键的, 难以复用和抽象 。今天我在Node.js服务里写了一套调用逻辑,明天另一个Python的定时任务也需要调用,又得重写一遍,不仅效率低,还容易产生不一致。

2.2 MCP协议带来的范式转变

MCP(Model Context Protocol)是新兴的一个协议,它的核心思想是让服务器(Server)向客户端(Client) 声明式地 暴露自己能做什么(Tools)和有什么(Resources)。客户端不需要知道服务器内部是用什么语言实现的、怎么调用的第三方API,它只需要按照协议描述去调用这些工具或读取资源就行。

xendit-mcp 这个项目来说,我作为服务器开发者,要做的事情就是:

  1. 告诉客户端:“嗨,我这儿有个工具叫 create_payment_link ,你可以用这些参数(金额、货币、描述……)来调用它。”
  2. 当客户端调用时,我(服务器)在内部完成对Xendit API的实际调用、错误处理、数据格式转换。
  3. 最后,我把一个标准化、简化后的结果返回给客户端。

这样做的好处立竿见影:

  • 解耦与标准化 :业务代码(客户端)和支付服务(Xendit)彻底解耦。客户端只依赖MCP协议,不依赖Xendit SDK或具体的HTTP客户端。
  • 语言无关性 :我的 xendit-mcp 服务器可以用任何语言写(比如我用的是TypeScript),但只要遵循MCP协议,任何语言的客户端都能用。
  • 可发现性与安全性 :客户端在连接时就能动态获取所有可用的操作列表及其严格的输入模式(Schema),避免了误用。同时,敏感密钥完全由服务器管理,不会泄露给客户端。
  • 易于与AI集成 :这是M协议的一大亮点。像Claude等AI助手可以直接理解MCP工具的描述,并代表用户去调用。你可以直接对AI说“给客户XXX创建一个100K印尼盾的支付链接”,AI就知道该调用哪个工具、传递什么参数。

2.3 项目架构选型与权衡

基于上述思路,我选择了Node.js(TypeScript)作为实现语言,并使用官方 @modelcontextprotocol/sdk 来快速构建MCP服务器。为什么不直接用Express写个传统REST API中间层呢?主要考虑是 面向未来和生态 。传统REST API中间层也能解耦,但它不具备MCP的声明式和标准化优势。MCP协议正在被越来越多的AI和自动化平台原生支持,用MCP意味着我的支付服务能更容易地嵌入这些新兴的工作流中。

在工具设计上,我遵循“单一职责”和“幂等性”原则。例如,将“创建支付”和“查询支付”拆分成两个独立的工具( create_payment_link get_payment_status )。即使创建工具的调用因为网络问题被重试,我也会先在内部通过订单ID等唯一标识检查是否已创建,避免重复创建支付单,确保幂等。

3. 核心功能拆解与实操要点

3.1 工具(Tools)设计:覆盖核心支付流程

我目前实现了几个最核心的工具,基本覆盖了80%的日常支付操作场景。

3.1.1 创建支付链接 ( create_payment_link ) 这是最常用的工具。Xendit支持多种支付方式,如信用卡、便利店支付、电子钱包、银行转账等。我的设计是让这个工具尽可能通用。

  • 输入参数
    • amount (必填):金额。这里有个坑,Xendit的金额单位是 该货币的最小单位 。比如印尼盾(IDR)没有小数,1000印尼盾就直接传 1000 。但菲律宾比索(PHP)有两位小数,100比索需要传 10000 (即100 * 100)。我在工具描述里会重点强调这一点,并在服务器内部做好验证和提示。
    • currency (必填):货币代码,如 IDR , PHP , USD
    • description :订单描述,会显示在支付页面上。
    • customer :客户信息对象,包含姓名、邮箱等。提供这些信息有助于Xendit进行风险控制和客户管理。
    • payment_method_types (可选):指定允许的支付方式数组,如 ["CREDIT_CARD", "OVO", "DANA"] 。如果不指定,则开放所有Xendit支持的支付方式。
  • 内部实现逻辑
    1. 参数校验与转换:检查金额是否为正整数,根据货币代码验证金额范围是否合理(例如IDR不能小于1)。
    2. 调用Xendit API:向 https://api.xendit.co/v2/invoices 发送POST请求。请求体需要精心构造,特别是 customer items (如果拆分为商品列表)字段的映射。
    3. 响应处理:从Xendit的响应中提取最关键的信息: invoice_url (支付链接)、 id (Xendit交易ID)、 status (初始状态)。过滤掉不必要的内部字段。
    4. 返回标准化结果:返回一个包含 paymentUrl , paymentId , status 的简单对象给客户端。

注意 :Xendit的支付链接有有效期(默认24小时)。对于电商场景,如果用户关闭了页面,你需要有机制重新获取或延长链接。 xendit-mcp 目前返回的是原始链接,后续可以考虑增加一个 recreate_expired_link 工具。

3.1.2 查询支付状态 ( get_payment_status ) 支付创建后,需要轮询或等待回调来确认状态。这个工具用于主动查询。

  • 输入参数 :主要就是 payment_id (即创建链接时返回的Xendit交易ID)。
  • 内部实现逻辑
    1. 调用Xendit API:向 https://api.xendit.co/v2/invoices/{invoice_id} 发送GET请求。
    2. 状态映射与简化:Xendit的状态比较细,如 PENDING , PAID , EXPIRED , SETTLED 。我会将它们映射为更通用的状态,如 pending , success , failed , expired ,并额外返回原始的 xendit_status 供需要时查阅。
    3. 返回丰富信息:除了状态,还会返回金额、货币、支付完成时间、支付方式等有用信息。

3.1.3 处理退款 ( create_refund ) 退款是支付的重要闭环。

  • 输入参数
    • payment_id :原支付ID。
    • amount :退款金额(同样需注意货币单位)。
    • reason :退款原因,用于记录。
  • 内部实现逻辑
    1. 校验:检查原支付是否已成功(只有成功的支付才能退款),检查退款金额是否小于等于原支付金额。
    2. 调用Xendit退款API:Xendit的退款API相对独立,需要构造特定的请求体。
    3. 处理异步结果:退款请求通常是异步处理的。工具会立即返回一个 refund_id 和初始状态(如 PROCESSING )。客户端需要后续通过另一个工具(如 get_refund_status ,我计划中)来查询最终结果。

3.2 资源(Resources)设计:暴露静态配置与动态数据

MCP的Resources用于暴露一些只读的、结构化的数据。我在 xendit-mcp 里设计了两个资源:

  • xendit://config/supported-currencies :列出当前项目配置支持的货币列表及其最小单位信息。客户端可以在UI上根据这个信息来正确格式化金额输入框。
  • xendit://config/available-payment-methods :列出Xendit在当前商户账号下启用的所有支付方式类型。这比硬编码在客户端更灵活,当Xendit后台启用或禁用某种方式时,客户端能动态感知。

3.3 安全与配置管理

这是服务器端的关键。所有Xendit的API密钥都通过环境变量注入。

  • 环境变量 XENDIT_SECRET_KEY 是必须的。为了区分沙箱和生产环境,还可以用 XENDIT_ENV (可选,默认为 production )来控制请求发往哪个域名。
  • 服务器内部 :密钥被加载后,用于配置一个全局的、带认证头的HTTP客户端(比如Axios实例)。所有对外请求都通过这个客户端发出,确保认证一致。
  • 错误处理 :服务器会捕获所有Xendit API返回的错误(通常有详细的错误码和消息),并将它们转换为MCP协议定义的标准化错误格式返回给客户端,同时避免泄露任何内部堆栈信息或敏感细节。

4. 从零开始部署与使用指南

4.1 环境准备与服务器启动

假设你已经有了Node.js环境(版本16+)和一个Xendit商户账号(可以去官网注册,有沙箱环境供测试)。

第一步:获取项目并安装依赖

git clone https://github.com/mrslbt/xendit-mcp.git
cd xendit-mcp
npm install

第二步:配置环境变量 创建一个 .env 文件在项目根目录:

XENDIT_SECRET_KEY=your_xendit_secret_key_here
# 可选,默认为 production,测试时设为 sandbox
XENDIT_ENV=sandbox
PORT=3000 # MCP服务器监听的端口

第三步:构建并启动服务器

npm run build  # 如果是TypeScript项目,需要先编译
npm start

如果看到日志输出 Xendit MCP Server running on port 3000 ,说明服务器就启动好了。它现在正在等待MCP客户端通过stdio或SSE等方式连接。

4.2 客户端连接与调用实战

MCP客户端有很多种。这里我以使用一个简单的Node.js脚本客户端为例,演示如何连接并调用工具。

第一步:安装MCP客户端SDK

npm install @modelcontextprotocol/sdk

第二步:编写客户端脚本

// client.mjs
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

async function main() {
  // 1. 创建客户端
  const client = new Client(
    { name: 'my-xendit-client', version: '1.0.0' },
    { capabilities: {} }
  );

  // 2. 创建传输层 - 这里使用stdio,连接到我们本地运行的服务器进程
  // 实际部署时,可能是通过SSE连接到远程服务器URL
  const transport = new StdioClientTransport({
    command: 'node',
    args: ['path/to/your/xendit-mcp/build/index.js'] // 指向编译后的服务器入口文件
  });

  // 3. 连接服务器
  await client.connect(transport);
  console.log('Connected to Xendit MCP server');

  // 4. 列出服务器提供的所有工具(可选,用于发现)
  const tools = await client.listTools();
  console.log('Available tools:', tools);

  // 5. 调用“创建支付链接”工具
  const result = await client.callTool({
    name: 'create_payment_link',
    arguments: {
      amount: 150000, // 150,000 印尼盾
      currency: 'IDR',
      description: 'Invoice #12345 for Premium Plan',
      customer: {
        given_names: 'John',
        surname: 'Doe',
        email: '[email protected]'
      }
    }
  });

  console.log('Payment created:', result);
  // 输出会类似:{ paymentUrl: 'https://checkout.xendit.co/web/xxx', paymentId: 'inv_xxx', status: 'PENDING' }

  // 6. 断开连接
  await client.close();
}

main().catch(console.error);

第三步:运行客户端 你需要确保服务器已经在运行(另一个终端),然后执行:

node client.mjs

如果一切正常,你将看到工具列表和创建成功的支付链接信息。你可以将这个链接发送给用户完成支付。

4.3 与AI助手(如Claude Desktop)集成

这才是MCP协议发挥威力的地方。以Claude Desktop为例:

  1. 在Claude Desktop的设置中,找到MCP服务器配置部分。
  2. 添加一个新的服务器配置,类型选择“stdio”或“command”。
  3. 在“Command”字段中,填入启动你的 xendit-mcp 服务器的命令,例如: node /absolute/path/to/xendit-mcp/build/index.js 。同时,确保环境变量(如 XENDIT_SECRET_KEY )在Claude Desktop的进程环境中是可用的(可能需要通过启动脚本或全局环境变量设置)。
  4. 保存并重启Claude Desktop。

重启后,当你和Claude对话时,它就能“意识”到新增的支付工具了。你可以直接说:“帮我的客户Jane创建一个500菲律宾比索的发票,用于购买年度会员。” Claude会理解你的意图,自动调用 create_payment_link 工具,并返回给你一个可以直接使用的支付链接。这极大地简化了在聊天环境中处理支付任务的流程。

5. 开发与部署中的常见问题与排查

在实际开发和测试中,我遇到了不少典型问题,这里总结一下,希望能帮你避坑。

5.1 认证失败与403错误

这是最常见的问题,几乎都出在 XENDIT_SECRET_KEY 上。

  • 症状 :调用任何工具都返回认证错误或403 Forbidden。
  • 排查步骤
    1. 检查密钥是否正确 :登录Xendit后台,确认复制的Secret Key完整无误,没有多余的空格或换行。沙箱环境和生产环境的密钥是不同的,务必对应。
    2. 检查环境变量 :确保服务器进程确实读取到了 .env 文件。可以在服务器启动后,临时加一句 console.log(process.env.XENDIT_SECRET_KEY ? 'Loaded' : 'Missing') 来验证。注意, .env 文件通常不应提交到代码仓库。
    3. 检查XENDIT_ENV :如果你在使用沙箱密钥,但 XENDIT_ENV 设置为 production ,请求会发往生产API域名,必然导致认证失败。确保环境匹配。
  • 解决 :核对并修正密钥与环境变量,重启服务器。

5.2 金额单位错误导致支付失败

  • 症状 :创建支付链接成功,但用户支付时显示金额异常(如多了100倍),或Xendit API直接返回参数验证错误。
  • 原因 :没有遵守Xendit的“最小货币单位”规则。这是集成Xendit时最高频的坑。
  • 示例
    • 正确(IDR 15000): amount: 15000
    • 错误(IDR 15000): amount: 15000.00 amount: 150
    • 正确(PHP 299.99): amount: 29999 (因为PHP最小单位是分,即0.01 PHP)
  • 排查 :在调用 create_payment_link 前,仔细核对输入的 amount 值。对于有小数位的货币,务必先乘以100(或对应的小数位数)再取整。
  • 建议 :在客户端或服务器的工具逻辑入口处,增加一个根据 currency 验证 amount 是否为整数的校验,并给出明确的错误提示。

5.3 支付状态回调(Webhook)处理

xendit-mcp 服务器本身主要处理主动调用。但支付成功的异步通知通常通过Webhook进行。

  • 问题 :如何将Xendit的Webhook通知与我的业务逻辑关联?
  • 方案 :在创建支付链接时,可以利用Xendit API的 callback_virtual_account_id 或外部ID( external_id )字段。我通常在 create_payment_link 工具中要求客户端传入一个 reference_id (你的业务订单ID),然后我将这个 reference_id 设置为 external_id 传给Xendit。当Xendit发送Webhook到你的业务服务器时,通知体里会包含这个 external_id ,这样你就能轻松找到对应的内部订单进行更新。
  • 注意 :Webhook端点需要是公网可访问的HTTPS地址。务必验证Webhook签名(Xendit会提供一个签名头)以确保通知来源真实可信,防止伪造支付成功通知。

5.4 性能与错误重试

  • 网络超时 :东南亚地区的网络情况可能不稳定。在服务器内部调用Xendit API的HTTP客户端应设置合理的超时时间(如10-15秒),并考虑实现简单的重试机制(针对网络错误或5xx状态码),重试时需注意幂等性。
  • 速率限制 :Xendit API有速率限制。如果短时间内触发大量请求,可能会被限流。在服务器实现中,应考虑加入简单的请求队列或限流逻辑,避免同时爆发大量请求。更佳实践是使用支持重试和退避策略的HTTP客户端库。
  • 日志与监控 :务必将所有对Xendit的请求和响应(脱敏后)、工具调用记录、错误信息详细日志化。这不仅是调试的需要,也是后续对账、审计和监控系统健康度的基础。建议集成像Winston、Pino这样的日志库,并输出到标准输出或日志文件,方便容器化部署时收集。

5.5 多商户与多环境支持

当前版本的 xendit-mcp 设计为单商户。如果你的业务需要为多个不同的Xendit子商户账号提供服务,就需要扩展架构。

  • 思路一:单服务器多密钥 :修改服务器,使其能根据客户端传入的某个标识(如 merchant_id )来动态选择对应的Xendit密钥。这要求服务器能安全地存储和管理多个密钥,增加了复杂性。
  • 思路二:多服务器实例 :为每个商户或每个环境(沙箱、生产)部署独立的服务器实例,每个实例配置自己的环境变量。这种方式更简单、隔离性更好,但运维成本稍高。
  • 建议 :对于初期或商户数不多的情况,采用思路二更稳妥。可以在Docker或Kubernetes中通过配置不同环境变量来轻松创建多个实例。

6. 总结与进阶思考

通过这个项目,我深刻体会到MCP协议在抽象和标准化外部服务方面的潜力。它不仅仅是为了让AI能调用支付接口,更是为任何需要与复杂外部API交互的系统提供了一种清晰、安全的中间层模式。

我个人在实际操作中的体会是 ,前期花时间设计好工具的参数Schema和错误处理规范,比后期反复修改要省力得多。一定要站在客户端(尤其是非技术背景的AI)的角度去思考,如何让工具调用更直观、更不易出错。例如,对于“金额”这个参数,也许未来可以设计成接受一个带小数的字符串(如 "150000" "299.99" ),然后在服务器内部根据货币类型自动完成单位转换,这样对调用方就更友好了。

最后再分享一个小技巧 ,在开发调试MCP服务器时,除了写客户端脚本,还可以使用一些MCP调试工具,比如 mcp-cli 或一些支持MCP的IDE插件。它们可以动态连接到你的服务器,交互式地列出和调用工具,比反复重启完整客户端要方便很多。

基于MCP协议封装Xendit支付API:构建标准化支付服务层

这个 xendit-mcp 项目目前还处于早期阶段,主要实现了发票(Invoice)相关的核心操作。Xendit还有虚拟账户(VA)、零售店支付(Retail Outlets)、二维码(QR Code)等多种产品线。后续我计划逐步将这些功能也封装成MCP工具,并考虑加入更完善的监控、仪表盘功能。如果你也对东南亚支付集成或MCP协议感兴趣,欢迎一起交流探讨,甚至贡献代码。毕竟,把复杂的事情变简单,是我们工程师最大的乐趣之一。

本文地址:https://www.idc504.com/news/9_211099.html