diff --git a/packages/argentina/payway/README.md b/packages/argentina/payway/README.md new file mode 100644 index 00000000..f2d246f7 --- /dev/null +++ b/packages/argentina/payway/README.md @@ -0,0 +1,105 @@ +# @codespar/mcp-payway + +> MCP server for **Payway** (ex-Prisma Medios de Pago / Decidir) — Argentina's dominant card-acquiring gateway: card tokenization, one-step and two-step charges, installments (cuotas), marketplace split and refunds. + +[![npm](https://img.shields.io/npm/v/@codespar/mcp-payway)](https://www.npmjs.com/package/@codespar/mcp-payway) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) + +## Quick Start + +### Claude Desktop + +Add to `~/.config/claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "payway": { + "command": "npx", + "args": ["-y", "@codespar/mcp-payway"], + "env": { + "PAYWAY_PUBLIC_API_KEY": "your-public-apikey", + "PAYWAY_PRIVATE_API_KEY": "your-private-apikey", + "PAYWAY_ENV": "sandbox" + } + } + } +} +``` + +### Claude Code + +```bash +claude mcp add payway -- npx @codespar/mcp-payway +``` + +### Cursor / VS Code + +Add to `.cursor/mcp.json` or `.vscode/mcp.json`: + +```json +{ + "servers": { + "payway": { + "command": "npx", + "args": ["-y", "@codespar/mcp-payway"], + "env": { + "PAYWAY_PUBLIC_API_KEY": "your-public-apikey", + "PAYWAY_PRIVATE_API_KEY": "your-private-apikey", + "PAYWAY_ENV": "sandbox" + } + } + } +} +``` + +## Authentication + +Payway authenticates with a single raw header named `apikey` (**not** `Authorization: Bearer`). Two co-equal keys exist with different powers — see the table below. + +## The two-key model + +Payway issues two co-equal API keys with different powers: + +| Key | Header | May call | +|---|---|---| +| **Public** | `apikey` | `POST /tokens` (card tokenization only) | +| **Private** | `apikey` | Everything else: payments, captures, refunds, queries | + +The auth header is a single raw header named `apikey` — **not** `Authorization: Bearer`. + +## Tools (6) + +| Tool | Endpoint | What it does | +|---|---|---| +| `create_token` | `POST /tokens` | Tokenize card data into a single-use payment token (public key) | +| `create_payment` | `POST /payments` | Charge a token — single sale or distributed/split, with installments | +| `get_payment` | `GET /payments/{id}` | Fetch a payment by id | +| `list_payments` | `GET /payments` | List payments (offset, pageSize, siteOperationId, merchantId) | +| `refund_payment` | `POST /payments/{id}/refunds` | Full refund (empty body) or partial (`amount` in minor units) | +| `confirm_payment` | `PUT /payments/{id}` | Capture a previously authorized payment (two-step flow) | + +## Environment + +| Variable | Required | Description | +|---|---|---| +| `PAYWAY_PUBLIC_API_KEY` | yes | Public apikey (tokenization) | +| `PAYWAY_PRIVATE_API_KEY` | yes | Private apikey (payments, refunds, queries) | +| `PAYWAY_ENV` | no | `sandbox` (default, Decidir sandbox host) or `production` | + +Base URLs: production `https://ventasonline.payway.com.ar/api/v2`, sandbox `https://developers.decidir.com/api/v2`. + +## Notes + +- Amounts are integers in **minor units**: `50000` = ARS 500.00. +- `payment_method_id` identifies the card brand (1 = Visa, 31 = Mastercard, 15 = Maestro, …). +- `site_transaction_id` is the merchant-side idempotency anchor — unique per sale. +- This server never invents card data; agents must collect PAN/expiry/CVV from the user or a vault. + +## Enterprise + +Need governance, budget limits, and audit trails for agent-driven payments on Payway? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds a policy engine, payment routing, and compliance templates on top of these MCP servers. + +## License + +MIT © CodeSpar diff --git a/packages/argentina/payway/package.json b/packages/argentina/payway/package.json new file mode 100644 index 00000000..ad589550 --- /dev/null +++ b/packages/argentina/payway/package.json @@ -0,0 +1,35 @@ +{ + "name": "@codespar/mcp-payway", + "version": "0.1.0", + "description": "MCP server for Payway (ex-Prisma/Decidir) — Argentina's dominant card-acquiring gateway: tokenize, charge, capture, refund", + "type": "module", + "main": "./dist/index.js", + "bin": { + "mcp-payway": "./dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + }, + "license": "MIT", + "keywords": [ + "mcp", + "payway", + "decidir", + "prisma-medios-de-pago", + "payments", + "card-acquiring", + "argentina" + ], + "mcpName": "io.github.codespar/mcp-payway" +} diff --git a/packages/argentina/payway/server.json b/packages/argentina/payway/server.json new file mode 100644 index 00000000..beebe88b --- /dev/null +++ b/packages/argentina/payway/server.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.codespar/mcp-payway", + "description": "Payway (ex-Decidir) — Argentina card acquiring: tokenize, charge, capture, refund", + "repository": { + "url": "https://github.com/codespar/mcp-dev-latam", + "source": "github", + "subfolder": "packages/argentina/payway" + }, + "version": "0.1.0", + "packages": [ + { + "registryType": "npm", + "identifier": "@codespar/mcp-payway", + "version": "0.1.0", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "name": "PAYWAY_PUBLIC_API_KEY", + "description": "Payway PUBLIC apikey — used only by create_token (card tokenization)", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "PAYWAY_PRIVATE_API_KEY", + "description": "Payway PRIVATE apikey — payments, captures, refunds and queries", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "PAYWAY_ENV", + "description": "Environment: 'sandbox' or 'production'. Defaults to 'sandbox' (Decidir sandbox host).", + "isRequired": false, + "format": "string", + "isSecret": false + } + ] + } + ], + "provider": { + "homepage": "https://www.payway.com.ar", + "logoUrl": "https://logo.clearbit.com/payway.com.ar", + "logoFallback": "https://www.google.com/s2/favicons?domain=payway.com.ar&sz=128", + "docsUrl": "https://decidirv2.api-docs.io", + "sandbox": { + "available": true + } + } +} diff --git a/packages/argentina/payway/src/index.ts b/packages/argentina/payway/src/index.ts new file mode 100644 index 00000000..84280a2a --- /dev/null +++ b/packages/argentina/payway/src/index.ts @@ -0,0 +1,317 @@ +#!/usr/bin/env node + +/** + * MCP Server for Payway — Argentina's dominant card-acquiring gateway + * (ex-Prisma Medios de Pago / Decidir, Visa-Banelco lineage). + * + * Two-step card flow: + * 1. create_token — tokenize card data (uses the PUBLIC apikey) + * 2. create_payment — charge the token (uses the PRIVATE apikey) + * + * Tools (6): + * create_token — tokenize card data into a single-use payment token + * create_payment — create a card charge from a token (single or distributed/split) + * get_payment — fetch a payment by id + * list_payments — list payments with pagination + filters + * refund_payment — refund a payment (full or partial) + * confirm_payment — capture a previously authorized payment (two-step flow) + * + * Authentication + * A single raw header named `apikey` (NOT Bearer). Payway issues two + * co-equal keys: the PUBLIC key may only tokenize (/tokens); the + * PRIVATE key drives payments, captures, refunds and queries. + * + * Environment + * PAYWAY_PUBLIC_API_KEY — public apikey (required for create_token) + * PAYWAY_PRIVATE_API_KEY — private apikey (required for everything else) + * PAYWAY_ENV — "sandbox" | "production" (default: "sandbox") + * + * Base URLs + * production: https://ventasonline.payway.com.ar/api/v2 + * sandbox: https://developers.decidir.com/api/v2 + * + * Docs: https://decidirv2.api-docs.io / https://www.payway.com.ar + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; + +const PUBLIC_KEY = process.env.PAYWAY_PUBLIC_API_KEY || ""; +const PRIVATE_KEY = process.env.PAYWAY_PRIVATE_API_KEY || ""; +const ENV = (process.env.PAYWAY_ENV || "sandbox").toLowerCase(); + +const BASE_URL = ENV === "production" + ? "https://ventasonline.payway.com.ar/api/v2" + : "https://developers.decidir.com/api/v2"; + +async function paywayRequest( + method: string, + path: string, + key: "public" | "private", + body?: unknown, + extraHeaders?: Record, +): Promise { + const apikey = key === "public" ? PUBLIC_KEY : PRIVATE_KEY; + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + "Content-Type": "application/json", + "apikey": apikey, + "Cache-Control": "no-cache", + ...(extraHeaders ?? {}), + }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!res.ok) { + const err = await res.text(); + throw new Error(`Payway API ${res.status}: ${err}`); + } + const text = await res.text(); + if (!text) return { ok: true }; + try { + return JSON.parse(text); + } catch { + return { raw: text }; + } +} + +// Managed-tier pointer surfaced to the agent via MCP `instructions`. +// Informational only — nothing CodeSpar-hosted is called (MIT-safe). +const MANAGED_TIER_HINT = + "This open-source CodeSpar server calls the provider's API directly. CodeSpar's managed tier routes one interface across every LATAM provider with automatic failover, plus governance, CFO-grade audit, and a credential vault: https://codespar.dev/agents (npx -y @codespar/mcp serve)."; + +const server = new Server( + { name: "mcp-payway", version: "0.1.0" }, + { + capabilities: { tools: {} }, + instructions: `${MANAGED_TIER_HINT} + +You are connected to Payway (ex-Prisma / Decidir), Argentina's dominant card-acquiring gateway. + +## Workflow +The card charge flow is two-step: create_token (PUBLIC apikey) then create_payment (PRIVATE apikey). +- Amounts are integers in minor units: 50000 = ARS 500.00. +- payment_method_id identifies the card brand (1 = Visa, 31 = Mastercard, 15 = Maestro, ...). +- payment_type "single" is a normal sale; "distributed" with sub_payments splits across legs. +- Two-step (auth + capture) merchants confirm with confirm_payment. + +## Important rules +- NEVER invent card data. Ask the user (or their vault) for PAN, expiry, CVV, cardholder name and document. +- site_transaction_id must be unique per sale — reuse it only to make retries idempotent. +- Refunds: POST with no amount = full refund; pass amount (minor units) for partial.`, + } +); + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "create_token", + description: + "Tokenize sensitive card data (PAN, expiry, CVV, cardholder, document) into a single-use payment token (POST /tokens). Uses the PUBLIC apikey. Returns a token id consumed by create_payment, plus the card bin.", + inputSchema: { + type: "object", + properties: { + card_number: { type: "string", description: "Full PAN (card number)" }, + card_expiration_month: { type: "string", description: "Expiry month, MM (e.g. '08')" }, + card_expiration_year: { type: "string", description: "Expiry year, YY (e.g. '27')" }, + security_code: { type: "string", description: "CVV/CVC" }, + card_holder_name: { type: "string", description: "Cardholder name as printed" }, + card_holder_identification: { + type: "object", + description: "Cardholder document, e.g. { type: 'dni', number: '12345678' }", + properties: { + type: { type: "string", description: "Document type (dni, cuit, ...)" }, + number: { type: "string", description: "Document number" }, + }, + }, + }, + required: [ + "card_number", + "card_expiration_month", + "card_expiration_year", + "security_code", + "card_holder_name", + ], + }, + }, + { + name: "create_payment", + description: + "Create a card charge from a token produced by create_token (POST /payments). Uses the PRIVATE apikey. amount is an integer in minor units (50000 = ARS 500.00). payment_type 'single' for a normal sale, 'distributed' with sub_payments for split/marketplace.", + inputSchema: { + type: "object", + properties: { + site_transaction_id: { type: "string", description: "Merchant-side unique transaction id (idempotency anchor)" }, + token: { type: "string", description: "Token id from create_token" }, + payment_method_id: { type: "number", description: "Card brand id (1=Visa, 31=Mastercard, 15=Maestro, ...)" }, + bin: { type: "string", description: "First 6 digits of the PAN (returned by create_token)" }, + amount: { type: "number", description: "Integer amount in minor units (cents)" }, + currency: { type: "string", description: "ISO currency, e.g. ARS" }, + installments: { type: "number", description: "Number of installments (cuotas)" }, + description: { type: "string", description: "Charge description" }, + payment_type: { type: "string", enum: ["single", "distributed"], description: "'single' for a normal sale; 'distributed' for split" }, + sub_payments: { type: "array", description: "Distributed/split sub-payment legs; empty array for a single payment", items: { type: "object" } }, + customer: { + type: "object", + description: "Buyer reference for fraud screening: { id, email, ip_address }", + properties: { + id: { type: "string" }, + email: { type: "string" }, + ip_address: { type: "string" }, + }, + }, + establishment_name: { type: "string", description: "Soft descriptor / establishment name" }, + }, + required: ["site_transaction_id", "token", "payment_method_id", "amount", "currency", "installments", "payment_type"], + }, + }, + { + name: "get_payment", + description: "Fetch a single payment by its Payway payment id (GET /payments/{id}). Uses the PRIVATE apikey.", + inputSchema: { + type: "object", + properties: { + id: { type: "string", description: "Payway payment id" }, + }, + required: ["id"], + }, + }, + { + name: "list_payments", + description: "List payments with optional pagination and filters (GET /payments). Uses the PRIVATE apikey.", + inputSchema: { + type: "object", + properties: { + offset: { type: "number", description: "Pagination offset" }, + pageSize: { type: "number", description: "Page size" }, + siteOperationId: { type: "string", description: "Filter by site_transaction_id" }, + merchantId: { type: "string", description: "Filter by merchant id (multi-site setups)" }, + }, + }, + }, + { + name: "refund_payment", + description: + "Refund a payment, fully or partially (POST /payments/{id}/refunds). POST with an empty body refunds the full amount; pass amount (integer minor units) for a partial refund. Uses the PRIVATE apikey.", + inputSchema: { + type: "object", + properties: { + id: { type: "string", description: "Payway payment id to refund" }, + amount: { type: "number", description: "Optional partial refund amount in minor units; omit for a full refund" }, + }, + required: ["id"], + }, + }, + { + name: "confirm_payment", + description: + "Confirm (capture) a previously authorized payment in the two-step auth + capture flow (PUT /payments/{id}). Uses the PRIVATE apikey.", + inputSchema: { + type: "object", + properties: { + id: { type: "string", description: "Payway payment id to capture" }, + amount: { type: "number", description: "Amount to capture in minor units" }, + }, + required: ["id"], + }, + }, + ], +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + try { + switch (name) { + case "create_token": { + const body = { + card_number: args?.card_number, + card_expiration_month: args?.card_expiration_month, + card_expiration_year: args?.card_expiration_year, + security_code: args?.security_code, + card_holder_name: args?.card_holder_name, + card_holder_identification: args?.card_holder_identification, + }; + return { + content: [ + { type: "text", text: JSON.stringify(await paywayRequest("POST", "/tokens", "public", body), null, 2) }, + ], + }; + } + case "create_payment": { + const body = { + site_transaction_id: args?.site_transaction_id, + token: args?.token, + payment_method_id: args?.payment_method_id, + bin: args?.bin, + amount: args?.amount, + currency: args?.currency, + installments: args?.installments, + description: args?.description, + payment_type: args?.payment_type, + sub_payments: args?.sub_payments ?? [], + customer: args?.customer, + establishment_name: args?.establishment_name, + }; + return { + content: [ + { type: "text", text: JSON.stringify(await paywayRequest("POST", "/payments", "private", body), null, 2) }, + ], + }; + } + case "get_payment": { + return { + content: [ + { type: "text", text: JSON.stringify(await paywayRequest("GET", `/payments/${args?.id}`, "private"), null, 2) }, + ], + }; + } + case "list_payments": { + const params = new URLSearchParams(); + if (args?.offset !== undefined) params.set("offset", String(args.offset)); + if (args?.pageSize !== undefined) params.set("pageSize", String(args.pageSize)); + if (args?.siteOperationId) params.set("siteOperationId", String(args.siteOperationId)); + if (args?.merchantId) params.set("merchantId", String(args.merchantId)); + const qs = params.toString(); + return { + content: [ + { type: "text", text: JSON.stringify(await paywayRequest("GET", `/payments${qs ? `?${qs}` : ""}`, "private"), null, 2) }, + ], + }; + } + case "refund_payment": { + const body = args?.amount !== undefined ? { amount: args.amount } : undefined; + return { + content: [ + { type: "text", text: JSON.stringify(await paywayRequest("POST", `/payments/${args?.id}/refunds`, "private", body), null, 2) }, + ], + }; + } + case "confirm_payment": { + const body = args?.amount !== undefined ? { amount: args.amount } : undefined; + return { + content: [ + { type: "text", text: JSON.stringify(await paywayRequest("PUT", `/payments/${args?.id}`, "private", body), null, 2) }, + ], + }; + } + default: + return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true }; + } + } catch (err) { + return { + content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } +}); + +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch(console.error); diff --git a/packages/argentina/payway/tsconfig.json b/packages/argentina/payway/tsconfig.json new file mode 100644 index 00000000..40c56c6e --- /dev/null +++ b/packages/argentina/payway/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "declaration": true + }, + "include": ["src"] +} diff --git a/packages/banking/midaz/README.md b/packages/banking/midaz/README.md new file mode 100644 index 00000000..9fa26a6a --- /dev/null +++ b/packages/banking/midaz/README.md @@ -0,0 +1,71 @@ +# @codespar/mcp-midaz + +> MCP server for **Midaz** by [Lerian](https://lerian.studio) — the open-source, multi-currency, multi-asset double-entry ledger. Your agent creates accounts, reads balances, and posts balanced transactions against **your own Midaz deployment**. + +[![npm](https://img.shields.io/npm/v/@codespar/mcp-midaz)](https://www.npmjs.com/package/@codespar/mcp-midaz) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) + +## Self-hosted model + +Midaz is **not a SaaS** — it runs in your infrastructure ([github.com/LerianStudio/midaz](https://github.com/LerianStudio/midaz)). This server points at your deployment: + +- `MIDAZ_BASE_URL` → your onboarding service (organizations / ledgers / accounts) +- `MIDAZ_TRANSACTION_URL` → your transaction service (defaults to `MIDAZ_BASE_URL` when both sit behind one gateway) +- `MIDAZ_API_KEY` → only if your deployment enforces auth + +Midaz hierarchy: **Organization → Ledger → Account → Transaction**. Tools take `organization_id` + `ledger_id` explicitly, so one connection can operate across ledgers. + +## Quick Start + +### Claude Desktop + +Add to `~/.config/claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "midaz": { + "command": "npx", + "args": ["-y", "@codespar/mcp-midaz"], + "env": { + "MIDAZ_BASE_URL": "https://midaz.your-company.internal", + "MIDAZ_API_KEY": "your-token-if-any" + } + } + } +} +``` + +### Claude Code + +```bash +claude mcp add midaz -- npx @codespar/mcp-midaz +``` + +## Tools (5) + +| Tool | What it does | +|---|---| +| `create_account` | Create an account in a ledger (`POST .../accounts`) | +| `list_accounts` | List a ledger's accounts, paginated | +| `get_balance` | Read available / on-hold balances per asset | +| `create_transaction` | Post a balanced double-entry transaction (`POST .../transactions/json`) | +| `get_transaction` | Fetch a transaction with its operations | + +## Example + +> "Move R$ 1.250,00 from the treasury account to @vendor-acme and show both balances." + +The agent calls `create_transaction` with a `send` block debiting `treasury` and crediting `@vendor-acme` (amounts must balance — Midaz rejects lopsided entries), then `get_balance` on each account. + +## Authentication + +Your deployment decides: when `MIDAZ_API_KEY` is set it is sent as a Bearer token; otherwise requests are unauthenticated (typical for network-isolated internal deployments). + +## Enterprise + +Need governance, budget limits, and audit trails for agent-driven ledger operations on Midaz? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds a policy engine, payment routing, and compliance templates on top of these MCP servers. + +## License + +MIT (this server). Midaz itself is Apache-2.0 by Lerian. diff --git a/packages/banking/midaz/package.json b/packages/banking/midaz/package.json new file mode 100644 index 00000000..dbcc4bfd --- /dev/null +++ b/packages/banking/midaz/package.json @@ -0,0 +1,37 @@ +{ + "name": "@codespar/mcp-midaz", + "version": "0.1.0", + "description": "MCP server for Midaz by Lerian — self-hosted open-source multi-currency double-entry ledger: accounts, balances, transactions", + "type": "module", + "main": "./dist/index.js", + "bin": { + "mcp-midaz": "./dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + }, + "license": "MIT", + "keywords": [ + "mcp", + "midaz", + "lerian", + "ledger", + "double-entry", + "accounting", + "multi-currency", + "open-source", + "self-hosted" + ], + "mcpName": "io.github.codespar/mcp-midaz" +} diff --git a/packages/banking/midaz/server.json b/packages/banking/midaz/server.json new file mode 100644 index 00000000..145dca15 --- /dev/null +++ b/packages/banking/midaz/server.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.codespar/mcp-midaz", + "description": "MCP server for Midaz by Lerian — self-hosted double-entry ledger: accounts, balances, transactions", + "repository": { + "url": "https://github.com/codespar/mcp-dev-latam", + "source": "github", + "subfolder": "packages/banking/midaz" + }, + "version": "0.1.0", + "packages": [ + { + "registryType": "npm", + "identifier": "@codespar/mcp-midaz", + "version": "0.1.0", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "name": "MIDAZ_BASE_URL", + "description": "Base URL of your self-hosted Midaz onboarding service", + "isRequired": true, + "format": "string", + "isSecret": false + }, + { + "name": "MIDAZ_TRANSACTION_URL", + "description": "Base URL of the Midaz transaction service. Defaults to MIDAZ_BASE_URL.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MIDAZ_API_KEY", + "description": "Bearer token when your deployment enforces auth", + "isRequired": false, + "format": "string", + "isSecret": true + } + ] + } + ], + "provider": { + "homepage": "https://lerian.studio", + "logoUrl": "https://logo.clearbit.com/lerian.studio", + "logoFallback": "https://www.google.com/s2/favicons?domain=lerian.studio&sz=128", + "docsUrl": "https://docs.lerian.studio", + "sandbox": { + "available": false + } + } +} diff --git a/packages/banking/midaz/src/index.ts b/packages/banking/midaz/src/index.ts new file mode 100644 index 00000000..41adc7d5 --- /dev/null +++ b/packages/banking/midaz/src/index.ts @@ -0,0 +1,296 @@ +#!/usr/bin/env node + +/** + * MCP Server for Midaz — Lerian's open-source, multi-currency, + * multi-asset double-entry ledger (github.com/LerianStudio/midaz). + * + * SELF-HOSTED MODEL: unlike SaaS providers in this catalog, Midaz runs + * inside the tenant's own infrastructure. This server points at YOUR + * deployment via MIDAZ_BASE_URL (onboarding service: organizations / + * ledgers / accounts) and MIDAZ_TRANSACTION_URL (transaction service; + * defaults to MIDAZ_BASE_URL when both run behind one gateway). + * + * Midaz hierarchy: Organization → Ledger → Account → Transaction + * (each transaction is a set of balanced double-entry operations). + * Tools take organization_id + ledger_id explicitly so one connection + * can operate across ledgers. + * + * Tools (5): + * create_account — create an account in a ledger + * list_accounts — list a ledger's accounts (paginated) + * get_balance — read an account's balances + * create_transaction — post a balanced double-entry transaction (JSON shape) + * get_transaction — fetch a transaction by id + * + * Environment + * MIDAZ_BASE_URL — base URL of your Midaz onboarding service (required) + * MIDAZ_TRANSACTION_URL — base URL of the transaction service (default: MIDAZ_BASE_URL) + * MIDAZ_API_KEY — bearer token, when your deployment enforces auth (optional) + * + * Docs: https://docs.lerian.studio + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; + +const BASE_URL = (process.env.MIDAZ_BASE_URL || "").replace(/\/$/, ""); +const TRANSACTION_URL = (process.env.MIDAZ_TRANSACTION_URL || BASE_URL).replace(/\/$/, ""); +const API_KEY = process.env.MIDAZ_API_KEY || ""; + +async function midazRequest( + service: "onboarding" | "transaction", + method: string, + path: string, + body?: unknown, +): Promise { + if (!BASE_URL) { + throw new Error( + "MIDAZ_BASE_URL is not set. Midaz is self-hosted: point this server at your own deployment.", + ); + } + const base = service === "transaction" ? TRANSACTION_URL : BASE_URL; + const headers: Record = { "Content-Type": "application/json" }; + if (API_KEY) headers["Authorization"] = `Bearer ${API_KEY}`; + const res = await fetch(`${base}${path}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + if (!res.ok) { + const err = await res.text(); + throw new Error(`Midaz API ${res.status}: ${err}`); + } + const text = await res.text(); + if (!text) return { ok: true }; + try { + return JSON.parse(text); + } catch { + return { raw: text }; + } +} + +// Managed-tier pointer surfaced to the agent via MCP `instructions`. +// Informational only — nothing CodeSpar-hosted is called (MIT-safe). +const MANAGED_TIER_HINT = + "This open-source CodeSpar server calls your self-hosted Midaz deployment directly. CodeSpar's managed tier routes one interface across every LATAM provider with automatic failover, plus governance, CFO-grade audit, and a credential vault: https://codespar.dev/agents (npx -y @codespar/mcp serve)."; + +const server = new Server( + { name: "mcp-midaz", version: "0.1.0" }, + { capabilities: { tools: {} }, instructions: MANAGED_TIER_HINT }, +); + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "create_account", + description: + "Create an account in a ledger (POST /v1/organizations/{org}/ledgers/{ledger}/accounts). Accounts hold balances per asset code (e.g. BRL, USD, USDC).", + inputSchema: { + type: "object", + properties: { + organization_id: { type: "string", description: "Midaz organization id" }, + ledger_id: { type: "string", description: "Ledger id inside the organization" }, + name: { type: "string", description: "Account display name" }, + asset_code: { type: "string", description: "Asset the account holds (e.g. BRL, USD)" }, + type: { type: "string", description: "Account type (e.g. deposit, savings, creditCard, expense)" }, + alias: { type: "string", description: "Optional unique alias (used as @alias in transaction operations)" }, + extra: { type: "object", description: "Any additional Midaz account fields, merged into the payload" }, + }, + required: ["organization_id", "ledger_id", "name", "asset_code", "type"], + }, + }, + { + name: "list_accounts", + description: + "List a ledger's accounts (GET /v1/organizations/{org}/ledgers/{ledger}/accounts). Paginated via limit/page.", + inputSchema: { + type: "object", + properties: { + organization_id: { type: "string", description: "Midaz organization id" }, + ledger_id: { type: "string", description: "Ledger id" }, + limit: { type: "number", description: "Page size" }, + page: { type: "number", description: "Page number (1-based)" }, + }, + required: ["organization_id", "ledger_id"], + }, + }, + { + name: "get_balance", + description: + "Read an account's balances (GET /v1/organizations/{org}/ledgers/{ledger}/accounts/{account}/balances). Returns available and on-hold amounts per asset.", + inputSchema: { + type: "object", + properties: { + organization_id: { type: "string", description: "Midaz organization id" }, + ledger_id: { type: "string", description: "Ledger id" }, + account_id: { type: "string", description: "Account id" }, + }, + required: ["organization_id", "ledger_id", "account_id"], + }, + }, + { + name: "create_transaction", + description: + "Post a balanced double-entry transaction (POST /v1/organizations/{org}/ledgers/{ledger}/transactions/json). The send block debits source accounts and credits distribute targets; amounts must balance. Accounts are referenced by id or @alias.", + inputSchema: { + type: "object", + properties: { + organization_id: { type: "string", description: "Midaz organization id" }, + ledger_id: { type: "string", description: "Ledger id" }, + description: { type: "string", description: "Human-readable transaction description" }, + send: { + type: "object", + description: + "Midaz send shape: { asset, value, scale, source: { from: [{ account, amount: { asset, value, scale } }] }, distribute: { to: [{ account, amount: {...} }] } }", + }, + metadata: { type: "object", description: "Optional key-value metadata stored on the transaction" }, + }, + required: ["organization_id", "ledger_id", "send"], + }, + }, + { + name: "get_transaction", + description: + "Fetch a transaction with its operations (GET /v1/organizations/{org}/ledgers/{ledger}/transactions/{id}).", + inputSchema: { + type: "object", + properties: { + organization_id: { type: "string", description: "Midaz organization id" }, + ledger_id: { type: "string", description: "Ledger id" }, + transaction_id: { type: "string", description: "Transaction id" }, + }, + required: ["organization_id", "ledger_id", "transaction_id"], + }, + }, + ], +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params as { + name: string; + arguments?: Record; + }; + const org = args?.organization_id; + const ledger = args?.ledger_id; + try { + switch (name) { + case "create_account": { + const { extra, organization_id: _o, ledger_id: _l, asset_code, ...fields } = + (args ?? {}) as Record; + const body = { + ...fields, + assetCode: asset_code, + ...(typeof extra === "object" && extra ? extra : {}), + }; + return { + content: [ + { + type: "text", + text: JSON.stringify( + await midazRequest("onboarding", "POST", `/v1/organizations/${org}/ledgers/${ledger}/accounts`, body), + null, + 2, + ), + }, + ], + }; + } + case "list_accounts": { + const params = new URLSearchParams(); + if (args?.limit !== undefined) params.set("limit", String(args.limit)); + if (args?.page !== undefined) params.set("page", String(args.page)); + const qs = params.toString(); + return { + content: [ + { + type: "text", + text: JSON.stringify( + await midazRequest( + "onboarding", + "GET", + `/v1/organizations/${org}/ledgers/${ledger}/accounts${qs ? `?${qs}` : ""}`, + ), + null, + 2, + ), + }, + ], + }; + } + case "get_balance": + return { + content: [ + { + type: "text", + text: JSON.stringify( + await midazRequest( + "transaction", + "GET", + `/v1/organizations/${org}/ledgers/${ledger}/accounts/${args?.account_id}/balances`, + ), + null, + 2, + ), + }, + ], + }; + case "create_transaction": { + const body: Record = { send: args?.send }; + if (args?.description) body.description = args.description; + if (args?.metadata) body.metadata = args.metadata; + return { + content: [ + { + type: "text", + text: JSON.stringify( + await midazRequest( + "transaction", + "POST", + `/v1/organizations/${org}/ledgers/${ledger}/transactions/json`, + body, + ), + null, + 2, + ), + }, + ], + }; + } + case "get_transaction": + return { + content: [ + { + type: "text", + text: JSON.stringify( + await midazRequest( + "transaction", + "GET", + `/v1/organizations/${org}/ledgers/${ledger}/transactions/${args?.transaction_id}`, + ), + null, + 2, + ), + }, + ], + }; + default: + return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true }; + } + } catch (err) { + return { + content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } +}); + +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch(console.error); diff --git a/packages/banking/midaz/tsconfig.json b/packages/banking/midaz/tsconfig.json new file mode 100644 index 00000000..40c56c6e --- /dev/null +++ b/packages/banking/midaz/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "declaration": true + }, + "include": ["src"] +} diff --git a/packages/banking/pomelo/README.md b/packages/banking/pomelo/README.md new file mode 100644 index 00000000..6b40f5d6 --- /dev/null +++ b/packages/banking/pomelo/README.md @@ -0,0 +1,106 @@ +# @codespar/mcp-pomelo + +> MCP server for **Pomelo** — pan-LATAM card issuing as a service: card-holder users, virtual and physical Visa/Mastercard issuance, lifecycle management, and the transactions feed. Argentina, Brazil, Mexico, Colombia, Peru, Chile. + +[![npm](https://img.shields.io/npm/v/@codespar/mcp-pomelo)](https://www.npmjs.com/package/@codespar/mcp-pomelo) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) + +## Quick Start + +### Claude Desktop + +Add to `~/.config/claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "pomelo": { + "command": "npx", + "args": ["-y", "@codespar/mcp-pomelo"], + "env": { + "POMELO_CLIENT_ID": "your-client-id", + "POMELO_CLIENT_SECRET": "your-client-secret", + "POMELO_ENV": "sandbox" + } + } + } +} +``` + +### Claude Code + +```bash +claude mcp add pomelo -- npx @codespar/mcp-pomelo +``` + +### Cursor / VS Code + +Add to `.cursor/mcp.json` or `.vscode/mcp.json`: + +```json +{ + "servers": { + "pomelo": { + "command": "npx", + "args": ["-y", "@codespar/mcp-pomelo"], + "env": { + "POMELO_CLIENT_ID": "your-client-id", + "POMELO_CLIENT_SECRET": "your-client-secret", + "POMELO_ENV": "sandbox" + } + } + } +} +``` + +## Environment + +| Variable | Required | Description | +|---|---|---| +| `POMELO_CLIENT_ID` | yes | OAuth2 client id | +| `POMELO_CLIENT_SECRET` | yes | OAuth2 client secret | +| `POMELO_ENV` | no | `sandbox` (default) or `production` | +| `POMELO_BASE_URL` | no | API base override (defaults per env) | +| `POMELO_AUTH_URL` | no | Auth base override (defaults per env) | +| `POMELO_AUDIENCE` | no | OAuth2 audience override (defaults per env) | + +Authentication is OAuth2 client-credentials; the server exchanges and caches the Bearer token automatically. + +## Tools (9) + +| Tool | What it does | +|---|---| +| `create_user` | Create a card-holder identity (`POST /users/v1`) | +| `get_user` | Fetch a user by id | +| `update_user` | Patch user fields (status, contact, address) | +| `create_card` | Issue a `VIRTUAL` or `PHYSICAL` card (`POST /cards/v1`) | +| `get_card` | Fetch a card (masked PAN, status, program) | +| `list_cards` | List cards, filterable by user/status | +| `update_card_status` | `ACTIVE` / `BLOCKED` / `DISABLED` lifecycle changes | +| `list_transactions` | Search the card-transactions feed | +| `get_transaction` | Fetch one transaction by id | + +Mutating calls carry an `x-idempotency-key` (auto-generated, overridable per call via `idempotency_key`). + +## Example + +> "Issue a virtual card for the new contractor and freeze the old one." + +The agent calls `create_user` (if needed) → `create_card` (`card_type: "VIRTUAL"`) → `update_card_status` (`status: "BLOCKED"`, `status_reason: "CLIENT_INTERNAL_REASON"`). + +## Notes + +- Card credentials (full PAN/CVV) are never returned by these tools; Pomelo exposes sensitive data only through its PCI-scoped widgets. +- Authorization decisioning (approving each swipe in real time) is a webhook you host, not an API call — pair this server with CodeSpar's governed authorizer if you want mandate checks per transaction. + +## Authentication + +OAuth2 client-credentials: the server exchanges `POMELO_CLIENT_ID` / `POMELO_CLIENT_SECRET` for a Bearer token at `{AUTH_URL}/oauth/token` and caches it until shortly before expiry. Mutating calls carry an `x-idempotency-key`. + +## Enterprise + +Need governance, budget limits, and audit trails for agent-driven card issuing on Pomelo? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds a policy engine, payment routing, and compliance templates on top of these MCP servers. + +## License + +MIT diff --git a/packages/banking/pomelo/package.json b/packages/banking/pomelo/package.json new file mode 100644 index 00000000..0fae8494 --- /dev/null +++ b/packages/banking/pomelo/package.json @@ -0,0 +1,39 @@ +{ + "name": "@codespar/mcp-pomelo", + "version": "0.1.0", + "description": "MCP server for Pomelo — pan-LATAM card issuing as a service: users, virtual/physical cards, lifecycle, transactions", + "type": "module", + "main": "./dist/index.js", + "bin": { + "mcp-pomelo": "./dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + }, + "license": "MIT", + "keywords": [ + "mcp", + "pomelo", + "card-issuing", + "cards", + "visa", + "mastercard", + "latam", + "argentina", + "brazil", + "mexico", + "colombia" + ], + "mcpName": "io.github.codespar/mcp-pomelo" +} diff --git a/packages/banking/pomelo/server.json b/packages/banking/pomelo/server.json new file mode 100644 index 00000000..0843f881 --- /dev/null +++ b/packages/banking/pomelo/server.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.codespar/mcp-pomelo", + "description": "MCP server for Pomelo — pan-LATAM card issuing: users, virtual/physical cards, transactions", + "repository": { + "url": "https://github.com/codespar/mcp-dev-latam", + "source": "github", + "subfolder": "packages/banking/pomelo" + }, + "version": "0.1.0", + "packages": [ + { + "registryType": "npm", + "identifier": "@codespar/mcp-pomelo", + "version": "0.1.0", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "name": "POMELO_CLIENT_ID", + "description": "OAuth2 client id", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "POMELO_CLIENT_SECRET", + "description": "OAuth2 client secret", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "POMELO_ENV", + "description": "Environment: 'sandbox' or 'production'. Defaults to 'sandbox'.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "POMELO_BASE_URL", + "description": "API base URL override. Defaults per environment.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "POMELO_AUTH_URL", + "description": "Auth base URL override. Defaults per environment.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "POMELO_AUDIENCE", + "description": "OAuth2 audience override. Defaults per environment.", + "isRequired": false, + "format": "string", + "isSecret": false + } + ] + } + ], + "provider": { + "homepage": "https://pomelo.la", + "logoUrl": "https://logo.clearbit.com/pomelo.la", + "logoFallback": "https://www.google.com/s2/favicons?domain=pomelo.la&sz=128", + "docsUrl": "https://docs.pomelo.la", + "sandbox": { + "available": true + } + } +} diff --git a/packages/banking/pomelo/src/index.ts b/packages/banking/pomelo/src/index.ts new file mode 100644 index 00000000..96b2847a --- /dev/null +++ b/packages/banking/pomelo/src/index.ts @@ -0,0 +1,381 @@ +#!/usr/bin/env node + +/** + * MCP Server for Pomelo — pan-LATAM card issuing as a service. + * + * Pomelo (pomelo.la) is a card-issuing processor for Visa/Mastercard + * across Argentina, Brazil, Mexico, Colombia, Peru and Chile. This + * server wraps the core issuing surface: identity holders (Users), + * card issuance and lifecycle (Cards), and the transactions feed. + * + * Tools (9): + * Users: + * create_user — create a card-holder identity + * get_user — fetch a user by id + * update_user — patch user data (status, contact fields) + * Cards: + * create_card — issue a VIRTUAL or PHYSICAL card for a user + * get_card — fetch a card by id + * list_cards — list cards, filterable by user + * update_card_status — activate / block / unblock a card + * Transactions: + * list_transactions — search the card-transactions feed + * get_transaction — fetch a single transaction by id + * + * Authentication + * OAuth2 client-credentials: POST {AUTH_URL}/oauth/token with + * client_id / client_secret / audience → Bearer token (cached until + * ~60s before expiry). + * + * Environment + * POMELO_CLIENT_ID — OAuth client id (required) + * POMELO_CLIENT_SECRET — OAuth client secret (required) + * POMELO_ENV — "sandbox" | "production" (default: "sandbox") + * POMELO_BASE_URL — API base override (default per env) + * POMELO_AUTH_URL — auth base override (default per env) + * POMELO_AUDIENCE — OAuth audience override (default per env) + * + * Docs: https://docs.pomelo.la + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; + +const CLIENT_ID = process.env.POMELO_CLIENT_ID || ""; +const CLIENT_SECRET = process.env.POMELO_CLIENT_SECRET || ""; +const ENV = (process.env.POMELO_ENV || "sandbox").toLowerCase(); + +const BASE_URL = + process.env.POMELO_BASE_URL || + (ENV === "production" ? "https://api.pomelo.la" : "https://api-dev.pomelo.la"); +const AUTH_URL = + process.env.POMELO_AUTH_URL || + (ENV === "production" ? "https://auth.pomelo.la" : "https://auth-dev.pomelo.la"); +const AUDIENCE = + process.env.POMELO_AUDIENCE || + (ENV === "production" ? "https://auth.pomelo.la" : "https://auth-dev.pomelo.la"); + +let cachedToken: { token: string; expiresAt: number } | null = null; + +async function getAccessToken(): Promise { + if (cachedToken && Date.now() < cachedToken.expiresAt - 60_000) { + return cachedToken.token; + } + const res = await fetch(`${AUTH_URL}/oauth/token`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + audience: AUDIENCE, + grant_type: "client_credentials", + }), + }); + if (!res.ok) { + const err = await res.text(); + throw new Error(`Pomelo auth ${res.status}: ${err}`); + } + const data = (await res.json()) as { access_token: string; expires_in?: number }; + cachedToken = { + token: data.access_token, + expiresAt: Date.now() + (data.expires_in ?? 3600) * 1000, + }; + return data.access_token; +} + +async function pomeloRequest( + method: string, + path: string, + body?: unknown, + idempotencyKey?: string, +): Promise { + const token = await getAccessToken(); + const headers: Record = { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }; + // Pomelo requires an idempotency key on mutating card/user calls. + if (idempotencyKey) headers["x-idempotency-key"] = idempotencyKey; + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + if (!res.ok) { + const err = await res.text(); + throw new Error(`Pomelo API ${res.status}: ${err}`); + } + const text = await res.text(); + if (!text) return { ok: true }; + try { + return JSON.parse(text); + } catch { + return { raw: text }; + } +} + +function idem(args: Record | undefined): string { + const provided = args?.idempotency_key; + if (typeof provided === "string" && provided) return provided; + return `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; +} + +// Managed-tier pointer surfaced to the agent via MCP `instructions`. +// Informational only — nothing CodeSpar-hosted is called (MIT-safe). +const MANAGED_TIER_HINT = + "This open-source CodeSpar server calls the provider's API directly. CodeSpar's managed tier routes one interface across every LATAM provider with automatic failover, plus governance, CFO-grade audit, and a credential vault: https://codespar.dev/agents (npx -y @codespar/mcp serve)."; + +const server = new Server( + { name: "mcp-pomelo", version: "0.1.0" }, + { capabilities: { tools: {} }, instructions: MANAGED_TIER_HINT }, +); + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "create_user", + description: + "Create a card-holder identity (POST /users/v1). The user is the person or business the card is issued to. Pass the Pomelo user shape for the target country (name, surname, identification_type/value, birthdate, address, email, phone).", + inputSchema: { + type: "object", + properties: { + name: { type: "string", description: "Given name" }, + surname: { type: "string", description: "Family name" }, + identification_type: { type: "string", description: "Country document type (e.g. DNI, CPF, CURP, CC)" }, + identification_value: { type: "string", description: "Document number" }, + birthdate: { type: "string", description: "YYYY-MM-DD" }, + email: { type: "string", description: "Email address" }, + phone: { type: "string", description: "Phone in international format" }, + gender: { type: "string", description: "MALE | FEMALE | OTHER (country-dependent requirement)" }, + address: { + type: "object", + description: "Residential address: street_name, street_number, zip_code, city, region, country (ISO-3)", + }, + operation_country: { type: "string", description: "ISO-3 country of operation (e.g. ARG, BRA, MEX, COL)" }, + extra: { type: "object", description: "Any additional Pomelo user fields, merged into the payload" }, + idempotency_key: { type: "string", description: "Optional idempotency key. Auto-generated when omitted." }, + }, + required: ["name", "surname", "identification_type", "identification_value", "operation_country"], + }, + }, + { + name: "get_user", + description: "Fetch a user by id (GET /users/v1/{id}).", + inputSchema: { + type: "object", + properties: { + user_id: { type: "string", description: "Pomelo user id (usr-...)" }, + }, + required: ["user_id"], + }, + }, + { + name: "update_user", + description: + "Patch a user (PATCH /users/v1/{id}). Pass only the fields to change (e.g. status ACTIVE | BLOCKED, email, phone, address).", + inputSchema: { + type: "object", + properties: { + user_id: { type: "string", description: "Pomelo user id (usr-...)" }, + patch: { type: "object", description: "Fields to update, Pomelo user shape" }, + idempotency_key: { type: "string", description: "Optional idempotency key. Auto-generated when omitted." }, + }, + required: ["user_id", "patch"], + }, + }, + { + name: "create_card", + description: + "Issue a card for a user (POST /cards/v1). card_type VIRTUAL is usable immediately; PHYSICAL enters embossing/shipping and must be activated on receipt. affinity_group_id selects the card program (BIN, art, limits) configured with Pomelo.", + inputSchema: { + type: "object", + properties: { + user_id: { type: "string", description: "Owner user id (usr-...)" }, + affinity_group_id: { type: "string", description: "Card program id (afg-...)" }, + card_type: { type: "string", enum: ["VIRTUAL", "PHYSICAL"], description: "Card form factor" }, + address: { + type: "object", + description: "Shipping address for PHYSICAL cards (street_name, street_number, zip_code, city, region, country)", + }, + extra: { type: "object", description: "Any additional Pomelo card fields, merged into the payload" }, + idempotency_key: { type: "string", description: "Optional idempotency key. Auto-generated when omitted." }, + }, + required: ["user_id", "affinity_group_id", "card_type"], + }, + }, + { + name: "get_card", + description: "Fetch a card by id (GET /cards/v1/{id}). Returns masked PAN, status, type and program data — never full card credentials.", + inputSchema: { + type: "object", + properties: { + card_id: { type: "string", description: "Pomelo card id (crd-...)" }, + }, + required: ["card_id"], + }, + }, + { + name: "list_cards", + description: "List cards (GET /cards/v1), filterable by user and status. Paginated.", + inputSchema: { + type: "object", + properties: { + user_id: { type: "string", description: "Filter by owner user id" }, + status: { type: "string", description: "Filter by status (e.g. ACTIVE, BLOCKED, DISABLED)" }, + page: { type: "number", description: "Page number (0-based)" }, + size: { type: "number", description: "Page size" }, + }, + }, + }, + { + name: "update_card_status", + description: + "Change a card's lifecycle status (PATCH /cards/v1/{id}): ACTIVE to unblock/activate, BLOCKED to freeze, DISABLED to cancel permanently. status_reason is required by Pomelo for blocks (e.g. CLIENT_INTERNAL_REASON, LOST, STOLEN).", + inputSchema: { + type: "object", + properties: { + card_id: { type: "string", description: "Pomelo card id (crd-...)" }, + status: { type: "string", enum: ["ACTIVE", "BLOCKED", "DISABLED"], description: "Target status" }, + status_reason: { type: "string", description: "Reason code (required for BLOCKED/DISABLED)" }, + idempotency_key: { type: "string", description: "Optional idempotency key. Auto-generated when omitted." }, + }, + required: ["card_id", "status"], + }, + }, + { + name: "list_transactions", + description: + "Search the card-transactions feed (GET /transactions/v1), filterable by card, user and time window. Paginated.", + inputSchema: { + type: "object", + properties: { + card_id: { type: "string", description: "Filter by card id" }, + user_id: { type: "string", description: "Filter by user id" }, + from: { type: "string", description: "ISO-8601 start of window" }, + to: { type: "string", description: "ISO-8601 end of window" }, + page: { type: "number", description: "Page number (0-based)" }, + size: { type: "number", description: "Page size" }, + }, + }, + }, + { + name: "get_transaction", + description: "Fetch a single card transaction by id (GET /transactions/v1/{id}).", + inputSchema: { + type: "object", + properties: { + transaction_id: { type: "string", description: "Pomelo transaction id (ctx-...)" }, + }, + required: ["transaction_id"], + }, + }, + ], +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params as { + name: string; + arguments?: Record; + }; + try { + switch (name) { + case "create_user": { + const { extra, idempotency_key: _ik, ...fields } = (args ?? {}) as Record; + const body = { ...fields, ...(typeof extra === "object" && extra ? extra : {}) }; + return { + content: [ + { type: "text", text: JSON.stringify(await pomeloRequest("POST", "/users/v1", body, idem(args)), null, 2) }, + ], + }; + } + case "get_user": + return { + content: [ + { type: "text", text: JSON.stringify(await pomeloRequest("GET", `/users/v1/${args?.user_id}`), null, 2) }, + ], + }; + case "update_user": + return { + content: [ + { type: "text", text: JSON.stringify(await pomeloRequest("PATCH", `/users/v1/${args?.user_id}`, args?.patch, idem(args)), null, 2) }, + ], + }; + case "create_card": { + const { extra, idempotency_key: _ik, ...fields } = (args ?? {}) as Record; + const body = { ...fields, ...(typeof extra === "object" && extra ? extra : {}) }; + return { + content: [ + { type: "text", text: JSON.stringify(await pomeloRequest("POST", "/cards/v1", body, idem(args)), null, 2) }, + ], + }; + } + case "get_card": + return { + content: [ + { type: "text", text: JSON.stringify(await pomeloRequest("GET", `/cards/v1/${args?.card_id}`), null, 2) }, + ], + }; + case "list_cards": { + const params = new URLSearchParams(); + if (args?.user_id) params.set("filter[user_id]", String(args.user_id)); + if (args?.status) params.set("filter[status]", String(args.status)); + if (args?.page !== undefined) params.set("page[number]", String(args.page)); + if (args?.size !== undefined) params.set("page[size]", String(args.size)); + const qs = params.toString(); + return { + content: [ + { type: "text", text: JSON.stringify(await pomeloRequest("GET", `/cards/v1${qs ? `?${qs}` : ""}`), null, 2) }, + ], + }; + } + case "update_card_status": { + const body: Record = { status: args?.status }; + if (args?.status_reason) body.status_reason = args.status_reason; + return { + content: [ + { type: "text", text: JSON.stringify(await pomeloRequest("PATCH", `/cards/v1/${args?.card_id}`, body, idem(args)), null, 2) }, + ], + }; + } + case "list_transactions": { + const params = new URLSearchParams(); + if (args?.card_id) params.set("filter[card_id]", String(args.card_id)); + if (args?.user_id) params.set("filter[user_id]", String(args.user_id)); + if (args?.from) params.set("filter[created_at][from]", String(args.from)); + if (args?.to) params.set("filter[created_at][to]", String(args.to)); + if (args?.page !== undefined) params.set("page[number]", String(args.page)); + if (args?.size !== undefined) params.set("page[size]", String(args.size)); + const qs = params.toString(); + return { + content: [ + { type: "text", text: JSON.stringify(await pomeloRequest("GET", `/transactions/v1${qs ? `?${qs}` : ""}`), null, 2) }, + ], + }; + } + case "get_transaction": + return { + content: [ + { type: "text", text: JSON.stringify(await pomeloRequest("GET", `/transactions/v1/${args?.transaction_id}`), null, 2) }, + ], + }; + default: + return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true }; + } + } catch (err) { + return { + content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } +}); + +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch(console.error); diff --git a/packages/banking/pomelo/tsconfig.json b/packages/banking/pomelo/tsconfig.json new file mode 100644 index 00000000..40c56c6e --- /dev/null +++ b/packages/banking/pomelo/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "declaration": true + }, + "include": ["src"] +} diff --git a/packages/colombia/bold-co/README.md b/packages/colombia/bold-co/README.md new file mode 100644 index 00000000..0a9233c3 --- /dev/null +++ b/packages/colombia/bold-co/README.md @@ -0,0 +1,69 @@ +# @codespar/mcp-bold-co + +> MCP server for **Bold** — Colombian acquirer (bold.co): payment links for cards, PSE, Nequi and Botón Bancolombia, plus the checkout integrity-signature and webhook-validation helpers. + +[![npm](https://img.shields.io/npm/v/@codespar/mcp-bold-co)](https://www.npmjs.com/package/@codespar/mcp-bold-co) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) + +Bold's public integrations API is link-first: create a payment link (fixed or open amount), hand the payer `payload.url`, then poll the link until it reports `PAID`. The embedded checkout button flow is secured with a SHA-256 integrity signature and webhooks are HMAC-signed — both covered here as local tools. + +## Quick Start + +### Claude Desktop + +Add to `~/.config/claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "bold-co": { + "command": "npx", + "args": ["-y", "@codespar/mcp-bold-co"], + "env": { + "BOLD_API_KEY": "your-identity-key", + "BOLD_SECRET_KEY": "your-secret-key" + } + } + } +} +``` + +### Claude Code + +```bash +claude mcp add bold-co --env BOLD_API_KEY=your-identity-key -- npx -y @codespar/mcp-bold-co +``` + +## Tools (7) + +| Tool | Description | +|---|---| +| `create_payment_link` | Create a payment link (`POST /online/link/v1`) — cards, PSE, Nequi, Botón Bancolombia | +| `create_pse_payment_link` | Convenience: link restricted to PSE bank debit | +| `create_card_payment_link` | Convenience: link restricted to cards | +| `get_payment_link` | Query a link's status: ACTIVE, PROCESSING, PAID, REJECTED, EXPIRED | +| `list_payment_methods` | Payment methods enabled for this merchant | +| `generate_checkout_signature` | SHA-256 integrity hash for the embedded checkout button (local) | +| `validate_webhook_signature` | Verify the HMAC signature on a Bold webhook (local) | + +## Environment + +| Variable | Required | Description | +|---|---|---| +| `BOLD_API_KEY` | yes | Identity key (llave de identidad), sent as `Authorization: x-api-key` | +| `BOLD_SECRET_KEY` | no | Secret key — only for the local signature/webhook tools | + +Bold has no separate sandbox host; test mode is driven by test keys from the Bold dashboard. + +## Docs + +- Provider docs: https://developers.bold.co +- Managed tier (one interface across every LATAM provider, governance + audit): https://codespar.dev/agents + +## Enterprise + +Need governance, budget limits, and audit trails for agent-driven payments on Bold? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds a policy engine, payment routing, and compliance templates on top of these MCP servers. + +## License + +MIT diff --git a/packages/colombia/bold-co/package.json b/packages/colombia/bold-co/package.json new file mode 100644 index 00000000..cea8c98d --- /dev/null +++ b/packages/colombia/bold-co/package.json @@ -0,0 +1,36 @@ +{ + "name": "@codespar/mcp-bold-co", + "version": "0.1.0", + "description": "MCP server for Bold — Colombian acquirer: payment links for cards, PSE, Nequi and Botón Bancolombia", + "type": "module", + "main": "./dist/index.js", + "bin": { + "mcp-bold-co": "./dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + }, + "license": "MIT", + "keywords": [ + "mcp", + "bold", + "payments", + "payment-links", + "pse", + "nequi", + "cards", + "colombia" + ], + "mcpName": "io.github.codespar/mcp-bold-co" +} diff --git a/packages/colombia/bold-co/server.json b/packages/colombia/bold-co/server.json new file mode 100644 index 00000000..1ed03d19 --- /dev/null +++ b/packages/colombia/bold-co/server.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.codespar/mcp-bold-co", + "description": "MCP server for Bold — Colombian acquirer: payment links for cards, PSE and Nequi", + "repository": { + "url": "https://github.com/codespar/mcp-dev-latam", + "source": "github", + "subfolder": "packages/colombia/bold-co" + }, + "version": "0.1.0", + "packages": [ + { + "registryType": "npm", + "identifier": "@codespar/mcp-bold-co", + "version": "0.1.0", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "name": "BOLD_API_KEY", + "description": "Bold identity API key (llave de identidad), sent as x-api-key", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "BOLD_SECRET_KEY", + "description": "Bold secret key. Only needed for the local checkout-signature and webhook-validation tools.", + "isRequired": false, + "format": "string", + "isSecret": true + } + ] + } + ], + "provider": { + "homepage": "https://bold.co", + "logoUrl": "https://logo.clearbit.com/bold.co", + "logoFallback": "https://www.google.com/s2/favicons?domain=bold.co&sz=128", + "docsUrl": "https://developers.bold.co", + "sandbox": { + "available": true + } + } +} diff --git a/packages/colombia/bold-co/src/index.ts b/packages/colombia/bold-co/src/index.ts new file mode 100644 index 00000000..52acba19 --- /dev/null +++ b/packages/colombia/bold-co/src/index.ts @@ -0,0 +1,321 @@ +#!/usr/bin/env node + +/** + * MCP Server for Bold — Colombian acquirer (bold.co). + * + * Bold is a Colombian payments company (cards, PSE, Nequi, Botón + * Bancolombia) known for its clean, modern REST integration surface. + * The public integrations API is link-first: you create a payment + * link (open or closed amount), hand the URL to the payer, and poll + * the link for its payment status. The checkout button flow is + * secured with a SHA-256 integrity signature; webhooks are signed + * with an HMAC over the raw body. + * + * Tools (7): + * create_payment_link — create a payment link (POST /online/link/v1) + * create_pse_payment_link — convenience: link restricted to PSE + * create_card_payment_link — convenience: link restricted to cards + * get_payment_link — query a link's status (GET /online/link/v1/{id}) + * list_payment_methods — available methods (GET /online/link/v1/payment_methods) + * generate_checkout_signature — SHA-256 integrity hash for the checkout button (local) + * validate_webhook_signature — verify the HMAC signature of a Bold webhook (local) + * + * API + * Base URL: https://integrations.api.bold.co + * Auth: Authorization: x-api-key + * + * Environment + * BOLD_API_KEY — identity (llave de identidad) API key (required) + * BOLD_SECRET_KEY — secret key, used only for local signature tools (optional) + * + * Bold has no separate sandbox host — test mode is driven by test keys. + * + * Docs: https://developers.bold.co + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { createHash, createHmac, timingSafeEqual } from "node:crypto"; + +const API_KEY = process.env.BOLD_API_KEY || ""; +const SECRET_KEY = process.env.BOLD_SECRET_KEY || ""; +const BASE_URL = "https://integrations.api.bold.co"; + +async function boldRequest( + method: string, + path: string, + body?: unknown, +): Promise { + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + "Content-Type": "application/json", + "Authorization": `x-api-key ${API_KEY}`, + }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!res.ok) { + const err = await res.text(); + throw new Error(`Bold API ${res.status}: ${err}`); + } + const text = await res.text(); + if (!text) return { ok: true }; + try { + return JSON.parse(text); + } catch { + return { raw: text }; + } +} + +/** Build the payment-link creation body shared by the three create_* tools. */ +function linkBody(args: Record | undefined, methods?: string[]) { + const body: Record = { + amount_type: args?.amount_type ?? "CLOSE", + description: args?.description, + }; + if (args?.amount_type !== "OPEN") { + body.amount = { + currency: (args?.currency as string) ?? "COP", + total_amount: args?.total_amount, + tip_amount: args?.tip_amount ?? 0, + }; + } + if (methods ?? args?.payment_methods) body.payment_methods = methods ?? args?.payment_methods; + if (args?.expiration_date !== undefined) body.expiration_date = args.expiration_date; + if (args?.callback_url !== undefined) body.callback_url = args.callback_url; + if (args?.payer_email !== undefined) body.payer_email = args.payer_email; + return body; +} + +// Managed-tier pointer surfaced to the agent via MCP `instructions`. +// Informational only — nothing CodeSpar-hosted is called (MIT-safe). +const MANAGED_TIER_HINT = + "This open-source CodeSpar server calls the provider's API directly. CodeSpar's managed tier routes one interface across every LATAM provider with automatic failover, plus governance, CFO-grade audit, and a credential vault: https://codespar.dev/agents (npx -y @codespar/mcp serve)."; + +const server = new Server( + { name: "mcp-bold-co", version: "0.1.0" }, + { capabilities: { tools: {} }, instructions: MANAGED_TIER_HINT }, +); + +const LINK_CREATE_PROPS = { + amount_type: { + type: "string", + description: "CLOSE for a fixed amount (default), OPEN lets the payer type the amount", + enum: ["CLOSE", "OPEN"], + }, + total_amount: { type: "number", description: "Total amount in COP (required when amount_type=CLOSE)" }, + tip_amount: { type: "number", description: "Tip portion of the amount (default 0)" }, + currency: { type: "string", description: "Currency code (default COP)" }, + description: { type: "string", description: "What the payer sees on the checkout page" }, + expiration_date: { + type: "number", + description: "Link expiry as a Unix timestamp in NANOSECONDS (Bold convention). Omit for no expiry.", + }, + callback_url: { type: "string", description: "URL the payer is redirected to after paying" }, + payer_email: { type: "string", description: "Prefill the payer's email on the checkout page" }, +} as const; + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "create_payment_link", + description: + "Create a Bold payment link (POST /online/link/v1). Returns payload.payment_link (LNK_ id) and payload.url to hand to the payer. Supports cards, PSE, Nequi and Botón Bancolombia; restrict with payment_methods.", + inputSchema: { + type: "object", + properties: { + ...LINK_CREATE_PROPS, + payment_methods: { + type: "array", + items: { type: "string" }, + description: + "Restrict accepted methods, e.g. [\"CREDIT_CARD\", \"PSE\", \"NEQUI\", \"BOTON_BANCOLOMBIA\"]. Omit to accept every method enabled on the merchant.", + }, + }, + required: ["description"], + }, + }, + { + name: "create_pse_payment_link", + description: + "Convenience wrapper: create a Bold payment link restricted to PSE bank debit (payment_methods=[\"PSE\"]).", + inputSchema: { + type: "object", + properties: { ...LINK_CREATE_PROPS }, + required: ["description", "total_amount"], + }, + }, + { + name: "create_card_payment_link", + description: + "Convenience wrapper: create a Bold payment link restricted to cards (payment_methods=[\"CREDIT_CARD\"]).", + inputSchema: { + type: "object", + properties: { ...LINK_CREATE_PROPS }, + required: ["description", "total_amount"], + }, + }, + { + name: "get_payment_link", + description: + "Query a Bold payment link by id (GET /online/link/v1/{payment_link}). Returns status (ACTIVE, PROCESSING, PAID, REJECTED, EXPIRED), amount, and transaction data once paid.", + inputSchema: { + type: "object", + properties: { + payment_link: { type: "string", description: "Payment link id, e.g. LNK_XXXXXXXXXX" }, + }, + required: ["payment_link"], + }, + }, + { + name: "list_payment_methods", + description: + "List the payment methods enabled for this merchant (GET /online/link/v1/payment_methods).", + inputSchema: { type: "object", properties: {} }, + }, + { + name: "generate_checkout_signature", + description: + "Generate the SHA-256 integrity signature Bold's embedded checkout button requires: sha256(orderId + amount + currency + secretKey). Runs locally; requires BOLD_SECRET_KEY.", + inputSchema: { + type: "object", + properties: { + order_id: { type: "string", description: "Merchant order id used in the checkout button" }, + amount: { type: "string", description: "Amount exactly as passed to the button (string, no separators)" }, + currency: { type: "string", description: "Currency code, e.g. COP" }, + }, + required: ["order_id", "amount", "currency"], + }, + }, + { + name: "validate_webhook_signature", + description: + "Verify the HMAC-SHA256 signature of a Bold webhook notification against BOLD_SECRET_KEY. Pass the raw request body string and the signature header value. Accepts hex or base64 encodings. Runs locally.", + inputSchema: { + type: "object", + properties: { + raw_body: { type: "string", description: "Raw webhook request body, exactly as received" }, + signature: { type: "string", description: "Signature header value sent by Bold" }, + }, + required: ["raw_body", "signature"], + }, + }, + ], +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + try { + switch (name) { + case "create_payment_link": { + return { + content: [ + { type: "text", text: JSON.stringify(await boldRequest("POST", "/online/link/v1", linkBody(args)), null, 2) }, + ], + }; + } + case "create_pse_payment_link": { + return { + content: [ + { type: "text", text: JSON.stringify(await boldRequest("POST", "/online/link/v1", linkBody(args, ["PSE"])), null, 2) }, + ], + }; + } + case "create_card_payment_link": { + return { + content: [ + { type: "text", text: JSON.stringify(await boldRequest("POST", "/online/link/v1", linkBody(args, ["CREDIT_CARD"])), null, 2) }, + ], + }; + } + case "get_payment_link": { + return { + content: [ + { type: "text", text: JSON.stringify(await boldRequest("GET", `/online/link/v1/${args?.payment_link}`), null, 2) }, + ], + }; + } + case "list_payment_methods": { + return { + content: [ + { type: "text", text: JSON.stringify(await boldRequest("GET", "/online/link/v1/payment_methods"), null, 2) }, + ], + }; + } + case "generate_checkout_signature": { + if (!SECRET_KEY) throw new Error("BOLD_SECRET_KEY is required for signature generation"); + const hash = createHash("sha256") + .update(`${args?.order_id}${args?.amount}${args?.currency}${SECRET_KEY}`) + .digest("hex"); + return { + content: [{ type: "text", text: JSON.stringify({ integrity_signature: hash }, null, 2) }], + }; + } + case "validate_webhook_signature": { + if (!SECRET_KEY) throw new Error("BOLD_SECRET_KEY is required for webhook validation"); + const raw = String(args?.raw_body ?? ""); + const provided = String(args?.signature ?? ""); + const mac = createHmac("sha256", SECRET_KEY).update(raw); + const macCopy = createHmac("sha256", SECRET_KEY).update(raw); + const hex = mac.digest("hex"); + const b64 = macCopy.digest("base64"); + const safeEq = (a: string, b: string) => { + const ab = Buffer.from(a); + const bb = Buffer.from(b); + return ab.length === bb.length && timingSafeEqual(ab, bb); + }; + const valid = safeEq(provided, hex) || safeEq(provided, b64); + return { + content: [{ type: "text", text: JSON.stringify({ valid }, null, 2) }], + }; + } + default: + return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true }; + } + } catch (err) { + return { + content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } +}); + +async function main() { + if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") { + const { default: express } = await import("express"); + const { randomUUID } = await import("node:crypto"); + const app = express(); + app.use(express.json()); + const transports = new Map(); + app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size })); + app.post("/mcp", async (req: any, res: any) => { + const sid = req.headers["mcp-session-id"] as string | undefined; + if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; } + if (!sid && isInitializeRequest(req.body)) { + const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } }); + t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); }; + const s = new Server({ name: "mcp-bold-co", version: "0.1.0" }, { capabilities: { tools: {} } }); + (server as any)._requestHandlers.forEach((v: any, k: any) => (s as any)._requestHandlers.set(k, v)); + (server as any)._notificationHandlers?.forEach((v: any, k: any) => (s as any)._notificationHandlers.set(k, v)); + await s.connect(t); + await t.handleRequest(req, res, req.body); return; + } + res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null }); + }); + app.get("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); }); + app.delete("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); }); + const port = Number(process.env.MCP_PORT) || 3000; + app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); }); + } else { + const transport = new StdioServerTransport(); + await server.connect(transport); + } +} + +main().catch(console.error); diff --git a/packages/colombia/bold-co/tsconfig.json b/packages/colombia/bold-co/tsconfig.json new file mode 100644 index 00000000..40c56c6e --- /dev/null +++ b/packages/colombia/bold-co/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "declaration": true + }, + "include": ["src"] +} diff --git a/packages/colombia/cobre/README.md b/packages/colombia/cobre/README.md new file mode 100644 index 00000000..30b4229c --- /dev/null +++ b/packages/colombia/cobre/README.md @@ -0,0 +1,69 @@ +# @codespar/mcp-cobre + +> MCP server for **Cobre** — Colombian/Mexican B2B treasury and payouts platform: balance visibility, counterparty registration, and outbound money movements over Colombia's instant rails and Mexico's SPEI. + +[![npm](https://img.shields.io/npm/v/@codespar/mcp-cobre)](https://www.npmjs.com/package/@codespar/mcp-cobre) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) + +Auth is OAuth2 client credentials (`POST /v1/auth`); the server caches the access token and refreshes before expiry. + +## Quick Start + +### Claude Desktop + +Add to `~/.config/claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "cobre": { + "command": "npx", + "args": ["-y", "@codespar/mcp-cobre"], + "env": { + "COBRE_USER_ID": "your-user-id", + "COBRE_SECRET": "your-secret" + } + } + } +} +``` + +### Claude Code + +```bash +claude mcp add cobre --env COBRE_USER_ID=uid --env COBRE_SECRET=sk -- npx -y @codespar/mcp-cobre +``` + +## Tools (8) + +| Tool | Description | +|---|---| +| `list_balances` | Balances across every visible account | +| `get_balance` | One balance by id | +| `create_counterparty` | Register a payout beneficiary (CO bank account or MX CLABE) | +| `list_counterparties` | List registered counterparties | +| `get_counterparty` | One counterparty by id | +| `create_money_movement` | Create an outbound payout (instant rails / SPEI) | +| `get_money_movement` | Movement status by id | +| `list_money_movements` | List movements | + +## Environment + +| Variable | Required | Description | +|---|---|---| +| `COBRE_USER_ID` | yes | API credential user id | +| `COBRE_SECRET` | yes | API credential secret | +| `COBRE_BASE_URL` | no | Override the API host (default `https://api.cobre.co`) | + +## Docs + +- Provider docs: https://docs.cobre.co +- Managed tier (one interface across every LATAM provider, governance + audit): https://codespar.dev/agents + +## Enterprise + +Need governance, budget limits, and audit trails for agent-driven payments on Cobre? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds a policy engine, payment routing, and compliance templates on top of these MCP servers. + +## License + +MIT diff --git a/packages/colombia/cobre/package.json b/packages/colombia/cobre/package.json new file mode 100644 index 00000000..91e58dc3 --- /dev/null +++ b/packages/colombia/cobre/package.json @@ -0,0 +1,36 @@ +{ + "name": "@codespar/mcp-cobre", + "version": "0.1.0", + "description": "MCP server for Cobre — Colombian B2B treasury and payouts: balances, counterparties and money movements", + "type": "module", + "main": "./dist/index.js", + "bin": { + "mcp-cobre": "./dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + }, + "license": "MIT", + "keywords": [ + "mcp", + "cobre", + "treasury", + "payouts", + "banking", + "bre-b", + "colombia", + "mexico" + ], + "mcpName": "io.github.codespar/mcp-cobre" +} diff --git a/packages/colombia/cobre/server.json b/packages/colombia/cobre/server.json new file mode 100644 index 00000000..d0c38325 --- /dev/null +++ b/packages/colombia/cobre/server.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.codespar/mcp-cobre", + "description": "MCP server for Cobre — CO/MX B2B treasury: balances, counterparties, money movements", + "repository": { + "url": "https://github.com/codespar/mcp-dev-latam", + "source": "github", + "subfolder": "packages/colombia/cobre" + }, + "version": "0.1.0", + "packages": [ + { + "registryType": "npm", + "identifier": "@codespar/mcp-cobre", + "version": "0.1.0", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "name": "COBRE_USER_ID", + "description": "Cobre API credential user id", + "isRequired": true, + "format": "string", + "isSecret": false + }, + { + "name": "COBRE_SECRET", + "description": "Cobre API credential secret", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "COBRE_BASE_URL", + "description": "Override the API host. Defaults to https://api.cobre.co.", + "isRequired": false, + "format": "string", + "isSecret": false + } + ] + } + ], + "provider": { + "homepage": "https://cobre.co", + "logoUrl": "https://logo.clearbit.com/cobre.co", + "logoFallback": "https://www.google.com/s2/favicons?domain=cobre.co&sz=128", + "docsUrl": "https://docs.cobre.co", + "sandbox": { + "available": false + } + } +} diff --git a/packages/colombia/cobre/src/index.ts b/packages/colombia/cobre/src/index.ts new file mode 100644 index 00000000..c8d82216 --- /dev/null +++ b/packages/colombia/cobre/src/index.ts @@ -0,0 +1,314 @@ +#!/usr/bin/env node + +/** + * MCP Server for Cobre — Colombian/Mexican B2B treasury & payouts. + * + * Cobre (cobre.co) is a treasury platform for companies moving money + * out: balance visibility across accounts, counterparty registration, + * and money movements over Colombia's instant rails and Mexico's + * SPEI. The API is resource-oriented JSON over OAuth2 client + * credentials. + * + * Tools (8): + * list_balances — GET /v1/balances + * get_balance — GET /v1/balances/{id} + * create_counterparty — POST /v1/counterparties + * list_counterparties — GET /v1/counterparties + * get_counterparty — GET /v1/counterparties/{id} + * create_money_movement — POST /v1/money_movements + * get_money_movement — GET /v1/money_movements/{id} + * list_money_movements — GET /v1/money_movements + * + * Authentication + * OAuth2 client credentials: POST /v1/auth with user_id + secret + * → access_token (cached until expiry, sent as Bearer). + * + * Environment + * COBRE_USER_ID — API credential user id (required) + * COBRE_SECRET — API credential secret (required) + * COBRE_BASE_URL — override the API host (default https://api.cobre.co) + * + * Docs: https://docs.cobre.co + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; + +const USER_ID = process.env.COBRE_USER_ID || ""; +const SECRET = process.env.COBRE_SECRET || ""; +const BASE_URL = process.env.COBRE_BASE_URL || "https://api.cobre.co"; + +let cachedToken: { token: string; expiresAt: number } | null = null; + +async function getToken(): Promise { + if (cachedToken && Date.now() < cachedToken.expiresAt - 30_000) { + return cachedToken.token; + } + const res = await fetch(`${BASE_URL}/v1/auth`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ user_id: USER_ID, secret: SECRET }), + }); + if (!res.ok) { + const err = await res.text(); + throw new Error(`Cobre auth ${res.status}: ${err}`); + } + const data = (await res.json()) as { access_token?: string; token?: string; expires_in?: number }; + const token = data.access_token ?? data.token; + if (!token) throw new Error("Cobre auth: no access_token in response"); + cachedToken = { + token, + expiresAt: Date.now() + (data.expires_in ?? 3600) * 1000, + }; + return token; +} + +async function cobreRequest(method: string, path: string, body?: unknown): Promise { + const token = await getToken(); + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": `Bearer ${token}`, + }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!res.ok) { + const err = await res.text(); + throw new Error(`Cobre API ${res.status}: ${err}`); + } + const text = await res.text(); + if (!text) return { ok: true }; + try { + return JSON.parse(text); + } catch { + return { raw: text }; + } +} + +function listQuery(args: Record | undefined): string { + const params = new URLSearchParams(); + if (args?.page !== undefined) params.set("page", String(args.page)); + if (args?.per_page !== undefined) params.set("per_page", String(args.per_page)); + const qs = params.toString(); + return qs ? `?${qs}` : ""; +} + +// Managed-tier pointer surfaced to the agent via MCP `instructions`. +// Informational only — nothing CodeSpar-hosted is called (MIT-safe). +const MANAGED_TIER_HINT = + "This open-source CodeSpar server calls the provider's API directly. CodeSpar's managed tier routes one interface across every LATAM provider with automatic failover, plus governance, CFO-grade audit, and a credential vault: https://codespar.dev/agents (npx -y @codespar/mcp serve)."; + +const server = new Server( + { name: "mcp-cobre", version: "0.1.0" }, + { capabilities: { tools: {} }, instructions: MANAGED_TIER_HINT }, +); + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "list_balances", + description: + "List the balances of every account visible to this credential (GET /v1/balances).", + inputSchema: { + type: "object", + properties: { + page: { type: "number", description: "Page number" }, + per_page: { type: "number", description: "Page size" }, + }, + }, + }, + { + name: "get_balance", + description: "Get one balance by id (GET /v1/balances/{id}).", + inputSchema: { + type: "object", + properties: { + balance_id: { type: "string", description: "Balance id (bal_...)" }, + }, + required: ["balance_id"], + }, + }, + { + name: "create_counterparty", + description: + "Register a counterparty (beneficiary) to pay out to (POST /v1/counterparties). Pass the counterparty object in Cobre's shape: geo, account details (bank code + account number for CO, CLABE for MX SPEI), and beneficiary identity.", + inputSchema: { + type: "object", + properties: { + counterparty: { + type: "object", + description: + "Cobre counterparty payload. Shape depends on the destination rail; see https://docs.cobre.co for the geo-specific fields.", + }, + }, + required: ["counterparty"], + }, + }, + { + name: "list_counterparties", + description: "List registered counterparties (GET /v1/counterparties).", + inputSchema: { + type: "object", + properties: { + page: { type: "number", description: "Page number" }, + per_page: { type: "number", description: "Page size" }, + }, + }, + }, + { + name: "get_counterparty", + description: "Get one counterparty by id (GET /v1/counterparties/{id}).", + inputSchema: { + type: "object", + properties: { + counterparty_id: { type: "string", description: "Counterparty id (cp_...)" }, + }, + required: ["counterparty_id"], + }, + }, + { + name: "create_money_movement", + description: + "Create an outbound money movement (POST /v1/money_movements): source account/balance, destination counterparty, amount and currency. Runs over Colombia's instant rails or Mexico's SPEI depending on the destination.", + inputSchema: { + type: "object", + properties: { + source_id: { type: "string", description: "Source account/balance id the funds leave from" }, + destination_id: { type: "string", description: "Destination counterparty id (cp_...)" }, + amount: { type: "number", description: "Amount in minor units unless the account currency says otherwise" }, + currency: { type: "string", description: "Currency code, e.g. COP or MXN" }, + description: { type: "string", description: "Statement / tracking description" }, + external_id: { type: "string", description: "Your idempotency / reconciliation id" }, + }, + required: ["source_id", "destination_id", "amount", "currency"], + }, + }, + { + name: "get_money_movement", + description: "Get a money movement and its status by id (GET /v1/money_movements/{id}).", + inputSchema: { + type: "object", + properties: { + money_movement_id: { type: "string", description: "Money movement id (mm_...)" }, + }, + required: ["money_movement_id"], + }, + }, + { + name: "list_money_movements", + description: "List money movements (GET /v1/money_movements).", + inputSchema: { + type: "object", + properties: { + page: { type: "number", description: "Page number" }, + per_page: { type: "number", description: "Page size" }, + }, + }, + }, + ], +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + try { + switch (name) { + case "list_balances": { + return { + content: [{ type: "text", text: JSON.stringify(await cobreRequest("GET", `/v1/balances${listQuery(args)}`), null, 2) }], + }; + } + case "get_balance": { + return { + content: [{ type: "text", text: JSON.stringify(await cobreRequest("GET", `/v1/balances/${args?.balance_id}`), null, 2) }], + }; + } + case "create_counterparty": { + return { + content: [{ type: "text", text: JSON.stringify(await cobreRequest("POST", "/v1/counterparties", args?.counterparty), null, 2) }], + }; + } + case "list_counterparties": { + return { + content: [{ type: "text", text: JSON.stringify(await cobreRequest("GET", `/v1/counterparties${listQuery(args)}`), null, 2) }], + }; + } + case "get_counterparty": { + return { + content: [{ type: "text", text: JSON.stringify(await cobreRequest("GET", `/v1/counterparties/${args?.counterparty_id}`), null, 2) }], + }; + } + case "create_money_movement": { + const body: Record = { + source_id: args?.source_id, + destination_id: args?.destination_id, + amount: args?.amount, + currency: args?.currency, + }; + if (args?.description !== undefined) body.description = args.description; + if (args?.external_id !== undefined) body.external_id = args.external_id; + return { + content: [{ type: "text", text: JSON.stringify(await cobreRequest("POST", "/v1/money_movements", body), null, 2) }], + }; + } + case "get_money_movement": { + return { + content: [{ type: "text", text: JSON.stringify(await cobreRequest("GET", `/v1/money_movements/${args?.money_movement_id}`), null, 2) }], + }; + } + case "list_money_movements": { + return { + content: [{ type: "text", text: JSON.stringify(await cobreRequest("GET", `/v1/money_movements${listQuery(args)}`), null, 2) }], + }; + } + default: + return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true }; + } + } catch (err) { + return { + content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } +}); + +async function main() { + if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") { + const { default: express } = await import("express"); + const { randomUUID } = await import("node:crypto"); + const app = express(); + app.use(express.json()); + const transports = new Map(); + app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size })); + app.post("/mcp", async (req: any, res: any) => { + const sid = req.headers["mcp-session-id"] as string | undefined; + if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; } + if (!sid && isInitializeRequest(req.body)) { + const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } }); + t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); }; + const s = new Server({ name: "mcp-cobre", version: "0.1.0" }, { capabilities: { tools: {} } }); + (server as any)._requestHandlers.forEach((v: any, k: any) => (s as any)._requestHandlers.set(k, v)); + (server as any)._notificationHandlers?.forEach((v: any, k: any) => (s as any)._notificationHandlers.set(k, v)); + await s.connect(t); + await t.handleRequest(req, res, req.body); return; + } + res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null }); + }); + app.get("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); }); + app.delete("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); }); + const port = Number(process.env.MCP_PORT) || 3000; + app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); }); + } else { + const transport = new StdioServerTransport(); + await server.connect(transport); + } +} + +main().catch(console.error); diff --git a/packages/colombia/cobre/tsconfig.json b/packages/colombia/cobre/tsconfig.json new file mode 100644 index 00000000..40c56c6e --- /dev/null +++ b/packages/colombia/cobre/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "declaration": true + }, + "include": ["src"] +} diff --git a/packages/colombia/epayco/README.md b/packages/colombia/epayco/README.md new file mode 100644 index 00000000..399eb5be --- /dev/null +++ b/packages/colombia/epayco/README.md @@ -0,0 +1,70 @@ +# @codespar/mcp-epayco + +> MCP server for **ePayco** — Colombian payment gateway (Davivienda-owned): card tokenization and charges, PSE bank debit, and cash networks (Efecty, Gana, Red Servi, Punto Red, Su Red). + +[![npm](https://img.shields.io/npm/v/@codespar/mcp-epayco)](https://www.npmjs.com/package/@codespar/mcp-epayco) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) + +The tool set mirrors the official ePayco SDKs (epayco-node): the card/token API on `api.secure.payco.co` and the restpagos API on `secure.payco.co` for PSE and cash. Test mode is a payload flag (`EPAYCO_TEST`), not a separate host. + +## Quick Start + +### Claude Desktop + +Add to `~/.config/claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "epayco": { + "command": "npx", + "args": ["-y", "@codespar/mcp-epayco"], + "env": { + "EPAYCO_PUBLIC_KEY": "your-public-key", + "EPAYCO_PRIVATE_KEY": "your-private-key", + "EPAYCO_TEST": "true" + } + } + } +} +``` + +### Claude Code + +```bash +claude mcp add epayco --env EPAYCO_PUBLIC_KEY=pk --env EPAYCO_PRIVATE_KEY=sk -- npx -y @codespar/mcp-epayco +``` + +## Tools (8) + +| Tool | Description | +|---|---| +| `tokenize_card` | Create a card token (`POST /v1/tokens`) | +| `create_customer` | Create a customer bound to a card token | +| `charge_card` | Charge a tokenized card (`POST /payment/v1/charge/create`) | +| `get_transaction` | Transaction detail by referencePayco | +| `create_pse_payment` | PSE bank debit — returns the bank redirect URL | +| `get_pse_transaction` | PSE transaction status by transaction id | +| `list_pse_banks` | PSE bank list | +| `create_cash_payment` | Cash voucher on efecty / gana / redservi / puntored / sured | + +## Environment + +| Variable | Required | Description | +|---|---|---| +| `EPAYCO_PUBLIC_KEY` | yes | Public key | +| `EPAYCO_PRIVATE_KEY` | yes | Private key | +| `EPAYCO_TEST` | no | `"true"` (default) flags payloads as test mode | + +## Docs + +- Provider docs: https://docs.epayco.com +- Managed tier (one interface across every LATAM provider, governance + audit): https://codespar.dev/agents + +## Enterprise + +Need governance, budget limits, and audit trails for agent-driven payments on ePayco? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds a policy engine, payment routing, and compliance templates on top of these MCP servers. + +## License + +MIT diff --git a/packages/colombia/epayco/package.json b/packages/colombia/epayco/package.json new file mode 100644 index 00000000..011c6ecd --- /dev/null +++ b/packages/colombia/epayco/package.json @@ -0,0 +1,36 @@ +{ + "name": "@codespar/mcp-epayco", + "version": "0.1.0", + "description": "MCP server for ePayco — Colombian gateway (Davivienda): cards, PSE, Daviplata and cash networks", + "type": "module", + "main": "./dist/index.js", + "bin": { + "mcp-epayco": "./dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + }, + "license": "MIT", + "keywords": [ + "mcp", + "epayco", + "payments", + "pse", + "efecty", + "daviplata", + "cards", + "colombia" + ], + "mcpName": "io.github.codespar/mcp-epayco" +} diff --git a/packages/colombia/epayco/server.json b/packages/colombia/epayco/server.json new file mode 100644 index 00000000..3d5f6da2 --- /dev/null +++ b/packages/colombia/epayco/server.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.codespar/mcp-epayco", + "description": "MCP server for ePayco — Colombian gateway: cards, PSE, Daviplata and cash networks", + "repository": { + "url": "https://github.com/codespar/mcp-dev-latam", + "source": "github", + "subfolder": "packages/colombia/epayco" + }, + "version": "0.1.0", + "packages": [ + { + "registryType": "npm", + "identifier": "@codespar/mcp-epayco", + "version": "0.1.0", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "name": "EPAYCO_PUBLIC_KEY", + "description": "ePayco public key", + "isRequired": true, + "format": "string", + "isSecret": false + }, + { + "name": "EPAYCO_PRIVATE_KEY", + "description": "ePayco private key", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "EPAYCO_TEST", + "description": "\"true\" flags payloads as test mode. Defaults to \"true\" — set \"false\" for production.", + "isRequired": false, + "format": "string", + "isSecret": false + } + ] + } + ], + "provider": { + "homepage": "https://epayco.com", + "logoUrl": "https://logo.clearbit.com/epayco.com", + "logoFallback": "https://www.google.com/s2/favicons?domain=epayco.com&sz=128", + "docsUrl": "https://docs.epayco.com", + "sandbox": { + "available": true + } + } +} diff --git a/packages/colombia/epayco/src/index.ts b/packages/colombia/epayco/src/index.ts new file mode 100644 index 00000000..356509ae --- /dev/null +++ b/packages/colombia/epayco/src/index.ts @@ -0,0 +1,426 @@ +#!/usr/bin/env node + +/** + * MCP Server for ePayco — Colombian payment gateway (Davivienda-owned). + * + * ePayco covers the broadest inbound mix in Colombia: cards (with + * tokenization), PSE bank debit, Daviplata, and the cash networks + * (Efecty, Gana, Red Servi, Punto Red, Su Red). This server mirrors + * the endpoint set of the official ePayco SDKs (epayco-node), which + * splits across two hosts: the card/token API (api.secure.payco.co) + * and the restpagos API (secure.payco.co) for PSE and cash. + * + * Tools (8): + * tokenize_card — create a card token (POST /v1/tokens) + * create_customer — create a customer bound to a card token (POST /payment/v1/customer/create) + * charge_card — charge a tokenized card (POST /payment/v1/charge/create) + * get_transaction — transaction detail by referencePayco (GET validation/v1/reference/{ref}) + * create_pse_payment — PSE bank-debit payment (POST /restpagos/pagos/debitos.json) + * get_pse_transaction — PSE transaction status (GET /restpagos/pse/transactioninfo.json) + * list_pse_banks — PSE bank list (GET /restpagos/pse/bancos.json) + * create_cash_payment — cash payment: efecty / gana / redservi / puntored / sured (POST /restpagos/v2/efectivo/{network}) + * + * Authentication + * Card/token API: Authorization: Basic base64(PUBLIC_KEY:PRIVATE_KEY) + * restpagos API: public key travels in the payload/query (SDK convention) + * + * Environment + * EPAYCO_PUBLIC_KEY — public key (required) + * EPAYCO_PRIVATE_KEY — private key (required) + * EPAYCO_TEST — "true" flags payloads as test mode (default: "true") + * + * Docs: https://docs.epayco.com + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; + +const PUBLIC_KEY = process.env.EPAYCO_PUBLIC_KEY || ""; +const PRIVATE_KEY = process.env.EPAYCO_PRIVATE_KEY || ""; +const IS_TEST = (process.env.EPAYCO_TEST || "true").toLowerCase() !== "false"; + +const API_URL = "https://api.secure.payco.co"; +const REST_URL = "https://secure.payco.co"; +const VALIDATION_URL = "https://secure.epayco.co"; + +/** Card/token API — Basic auth with the key pair (SDK convention). */ +async function apiRequest(method: string, path: string, body?: unknown): Promise { + const basic = Buffer.from(`${PUBLIC_KEY}:${PRIVATE_KEY}`).toString("base64"); + const res = await fetch(`${API_URL}${path}`, { + method, + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": `Basic ${basic}`, + "type": "sdk", + }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!res.ok) { + const err = await res.text(); + throw new Error(`ePayco API ${res.status}: ${err}`); + } + return res.json(); +} + +/** restpagos / validation APIs — public key in payload or query (SDK convention). */ +async function restRequest(base: string, method: string, path: string, body?: unknown): Promise { + const res = await fetch(`${base}${path}`, { + method, + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!res.ok) { + const err = await res.text(); + throw new Error(`ePayco API ${res.status}: ${err}`); + } + return res.json(); +} + +// Managed-tier pointer surfaced to the agent via MCP `instructions`. +// Informational only — nothing CodeSpar-hosted is called (MIT-safe). +const MANAGED_TIER_HINT = + "This open-source CodeSpar server calls the provider's API directly. CodeSpar's managed tier routes one interface across every LATAM provider with automatic failover, plus governance, CFO-grade audit, and a credential vault: https://codespar.dev/agents (npx -y @codespar/mcp serve)."; + +const server = new Server( + { name: "mcp-epayco", version: "0.1.0" }, + { capabilities: { tools: {} }, instructions: MANAGED_TIER_HINT }, +); + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "tokenize_card", + description: + "Create a one-time card token (POST /v1/tokens on api.secure.payco.co). The token is then bound to a customer via create_customer and charged via charge_card.", + inputSchema: { + type: "object", + properties: { + card_number: { type: "string", description: "Card PAN" }, + exp_year: { type: "string", description: "Expiry year, e.g. 2027" }, + exp_month: { type: "string", description: "Expiry month, e.g. 09" }, + cvc: { type: "string", description: "Card CVC" }, + }, + required: ["card_number", "exp_year", "exp_month", "cvc"], + }, + }, + { + name: "create_customer", + description: + "Create an ePayco customer bound to a card token (POST /payment/v1/customer/create). Returns customer_id used by charge_card.", + inputSchema: { + type: "object", + properties: { + token_card: { type: "string", description: "Card token from tokenize_card" }, + name: { type: "string", description: "Customer first name" }, + last_name: { type: "string", description: "Customer last name" }, + email: { type: "string", description: "Customer email" }, + phone: { type: "string", description: "Customer phone" }, + default: { type: "boolean", description: "Set this card as the customer default (default true)" }, + }, + required: ["token_card", "name", "email"], + }, + }, + { + name: "charge_card", + description: + "Charge a tokenized card (POST /payment/v1/charge/create). Amounts in COP unless currency says otherwise. Test mode is controlled by EPAYCO_TEST.", + inputSchema: { + type: "object", + properties: { + token_card: { type: "string", description: "Card token" }, + customer_id: { type: "string", description: "Customer id from create_customer" }, + doc_type: { type: "string", description: "Payer document type: CC, CE, NIT, ..." }, + doc_number: { type: "string", description: "Payer document number" }, + name: { type: "string", description: "Payer first name" }, + last_name: { type: "string", description: "Payer last name" }, + email: { type: "string", description: "Payer email" }, + bill: { type: "string", description: "Merchant invoice/order reference" }, + description: { type: "string", description: "Charge description" }, + value: { type: "string", description: "Amount (string, e.g. \"116000\")" }, + tax: { type: "string", description: "Tax portion (default \"0\")" }, + tax_base: { type: "string", description: "Taxable base (default value)" }, + currency: { type: "string", description: "Currency code (default COP)" }, + dues: { type: "string", description: "Installments (default \"1\")" }, + }, + required: ["token_card", "customer_id", "doc_type", "doc_number", "name", "email", "bill", "value"], + }, + }, + { + name: "get_transaction", + description: + "Get transaction detail by referencePayco (GET https://secure.epayco.co/validation/v1/reference/{ref}). Works for card, PSE and cash payments.", + inputSchema: { + type: "object", + properties: { + reference_payco: { type: "string", description: "The referencePayco returned when the payment was created" }, + }, + required: ["reference_payco"], + }, + }, + { + name: "create_pse_payment", + description: + "Create a PSE bank-debit payment (POST /restpagos/pagos/debitos.json on secure.payco.co). Returns a bank redirect URL the payer must open to authorize the debit.", + inputSchema: { + type: "object", + properties: { + bank: { type: "string", description: "PSE bank code from list_pse_banks" }, + invoice: { type: "string", description: "Merchant invoice/order reference" }, + description: { type: "string", description: "Payment description" }, + value: { type: "string", description: "Amount (string)" }, + tax: { type: "string", description: "Tax portion (default \"0\")" }, + tax_base: { type: "string", description: "Taxable base" }, + currency: { type: "string", description: "Currency (default COP)" }, + type_person: { type: "string", description: "0 = natural person, 1 = company" }, + doc_type: { type: "string", description: "Payer document type: CC, CE, NIT, ..." }, + doc_number: { type: "string", description: "Payer document number" }, + name: { type: "string", description: "Payer first name" }, + last_name: { type: "string", description: "Payer last name" }, + email: { type: "string", description: "Payer email" }, + cell_phone: { type: "string", description: "Payer mobile phone" }, + ip: { type: "string", description: "Payer IP address (PSE requirement)" }, + url_response: { type: "string", description: "URL the payer returns to after the bank flow" }, + url_confirmation: { type: "string", description: "Server-to-server confirmation webhook URL" }, + }, + required: ["bank", "invoice", "description", "value", "doc_type", "doc_number", "name", "email", "cell_phone", "ip"], + }, + }, + { + name: "get_pse_transaction", + description: + "Get a PSE transaction's status by transaction id (GET /restpagos/pse/transactioninfo.json on secure.payco.co).", + inputSchema: { + type: "object", + properties: { + transaction_id: { type: "string", description: "PSE transactionID returned by create_pse_payment" }, + }, + required: ["transaction_id"], + }, + }, + { + name: "list_pse_banks", + description: + "List PSE banks available for bank debit (GET /restpagos/pse/bancos.json on secure.payco.co).", + inputSchema: { type: "object", properties: {} }, + }, + { + name: "create_cash_payment", + description: + "Create a cash payment voucher on a Colombian cash network (POST /restpagos/v2/efectivo/{network} on secure.payco.co). The payer takes the voucher code to a physical point to pay.", + inputSchema: { + type: "object", + properties: { + network: { + type: "string", + description: "Cash network", + enum: ["efecty", "gana", "redservi", "puntored", "sured"], + }, + invoice: { type: "string", description: "Merchant invoice/order reference" }, + description: { type: "string", description: "Payment description" }, + value: { type: "string", description: "Amount (string)" }, + tax: { type: "string", description: "Tax portion (default \"0\")" }, + tax_base: { type: "string", description: "Taxable base" }, + currency: { type: "string", description: "Currency (default COP)" }, + doc_type: { type: "string", description: "Payer document type" }, + doc_number: { type: "string", description: "Payer document number" }, + name: { type: "string", description: "Payer first name" }, + last_name: { type: "string", description: "Payer last name" }, + email: { type: "string", description: "Payer email" }, + cell_phone: { type: "string", description: "Payer mobile phone" }, + end_date: { type: "string", description: "Voucher expiry date, YYYY-MM-DD" }, + ip: { type: "string", description: "Payer IP address" }, + url_response: { type: "string", description: "URL the payer returns to" }, + url_confirmation: { type: "string", description: "Server-to-server confirmation webhook URL" }, + }, + required: ["network", "invoice", "description", "value", "doc_type", "doc_number", "name", "email", "cell_phone", "end_date", "ip"], + }, + }, + ], +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + try { + switch (name) { + case "tokenize_card": { + const body = { + "card[number]": args?.card_number, + "card[exp_year]": args?.exp_year, + "card[exp_month]": args?.exp_month, + "card[cvc]": args?.cvc, + }; + return { + content: [{ type: "text", text: JSON.stringify(await apiRequest("POST", "/v1/tokens", body), null, 2) }], + }; + } + case "create_customer": { + const body = { + token_card: args?.token_card, + name: args?.name, + last_name: args?.last_name, + email: args?.email, + phone: args?.phone, + default: args?.default ?? true, + }; + return { + content: [{ type: "text", text: JSON.stringify(await apiRequest("POST", "/payment/v1/customer/create", body), null, 2) }], + }; + } + case "charge_card": { + const body = { + token_card: args?.token_card, + customer_id: args?.customer_id, + doc_type: args?.doc_type, + doc_number: args?.doc_number, + name: args?.name, + last_name: args?.last_name, + email: args?.email, + bill: args?.bill, + description: args?.description, + value: args?.value, + tax: args?.tax ?? "0", + tax_base: args?.tax_base ?? args?.value, + currency: args?.currency ?? "COP", + dues: args?.dues ?? "1", + test: IS_TEST, + }; + return { + content: [{ type: "text", text: JSON.stringify(await apiRequest("POST", "/payment/v1/charge/create", body), null, 2) }], + }; + } + case "get_transaction": { + return { + content: [ + { type: "text", text: JSON.stringify(await restRequest(VALIDATION_URL, "GET", `/validation/v1/reference/${args?.reference_payco}`), null, 2) }, + ], + }; + } + case "create_pse_payment": { + const body = { + public_key: PUBLIC_KEY, + bank: args?.bank, + invoice: args?.invoice, + description: args?.description, + value: args?.value, + tax: args?.tax ?? "0", + tax_base: args?.tax_base ?? args?.value, + currency: args?.currency ?? "COP", + type_person: args?.type_person ?? "0", + doc_type: args?.doc_type, + doc_number: args?.doc_number, + name: args?.name, + last_name: args?.last_name, + email: args?.email, + cell_phone: args?.cell_phone, + ip: args?.ip, + url_response: args?.url_response, + url_confirmation: args?.url_confirmation, + test: IS_TEST, + }; + return { + content: [ + { type: "text", text: JSON.stringify(await restRequest(REST_URL, "POST", "/restpagos/pagos/debitos.json", body), null, 2) }, + ], + }; + } + case "get_pse_transaction": { + const qs = new URLSearchParams({ + transactionID: String(args?.transaction_id ?? ""), + public_key: PUBLIC_KEY, + }); + return { + content: [ + { type: "text", text: JSON.stringify(await restRequest(REST_URL, "GET", `/restpagos/pse/transactioninfo.json?${qs}`), null, 2) }, + ], + }; + } + case "list_pse_banks": { + const qs = new URLSearchParams({ public_key: PUBLIC_KEY }); + return { + content: [ + { type: "text", text: JSON.stringify(await restRequest(REST_URL, "GET", `/restpagos/pse/bancos.json?${qs}`), null, 2) }, + ], + }; + } + case "create_cash_payment": { + const network = String(args?.network ?? "efecty"); + const body = { + public_key: PUBLIC_KEY, + invoice: args?.invoice, + description: args?.description, + value: args?.value, + tax: args?.tax ?? "0", + tax_base: args?.tax_base ?? args?.value, + currency: args?.currency ?? "COP", + doc_type: args?.doc_type, + doc_number: args?.doc_number, + name: args?.name, + last_name: args?.last_name, + email: args?.email, + cell_phone: args?.cell_phone, + end_date: args?.end_date, + ip: args?.ip, + url_response: args?.url_response, + url_confirmation: args?.url_confirmation, + test: IS_TEST, + }; + return { + content: [ + { type: "text", text: JSON.stringify(await restRequest(REST_URL, "POST", `/restpagos/v2/efectivo/${network}`, body), null, 2) }, + ], + }; + } + default: + return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true }; + } + } catch (err) { + return { + content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } +}); + +async function main() { + if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") { + const { default: express } = await import("express"); + const { randomUUID } = await import("node:crypto"); + const app = express(); + app.use(express.json()); + const transports = new Map(); + app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size })); + app.post("/mcp", async (req: any, res: any) => { + const sid = req.headers["mcp-session-id"] as string | undefined; + if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; } + if (!sid && isInitializeRequest(req.body)) { + const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } }); + t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); }; + const s = new Server({ name: "mcp-epayco", version: "0.1.0" }, { capabilities: { tools: {} } }); + (server as any)._requestHandlers.forEach((v: any, k: any) => (s as any)._requestHandlers.set(k, v)); + (server as any)._notificationHandlers?.forEach((v: any, k: any) => (s as any)._notificationHandlers.set(k, v)); + await s.connect(t); + await t.handleRequest(req, res, req.body); return; + } + res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null }); + }); + app.get("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); }); + app.delete("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); }); + const port = Number(process.env.MCP_PORT) || 3000; + app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); }); + } else { + const transport = new StdioServerTransport(); + await server.connect(transport); + } +} + +main().catch(console.error); diff --git a/packages/colombia/epayco/tsconfig.json b/packages/colombia/epayco/tsconfig.json new file mode 100644 index 00000000..40c56c6e --- /dev/null +++ b/packages/colombia/epayco/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "declaration": true + }, + "include": ["src"] +} diff --git a/packages/mexico/clip/README.md b/packages/mexico/clip/README.md new file mode 100644 index 00000000..c7e6e086 --- /dev/null +++ b/packages/mexico/clip/README.md @@ -0,0 +1,72 @@ +# @codespar/mcp-clip + +> MCP server for **Clip (PayClip)** — Mexico's leading native acquirer. Hosted payment links, transparent card-token charges, queries and refunds, in MXN. + +[![npm](https://img.shields.io/npm/v/@codespar/mcp-clip)](https://www.npmjs.com/package/@codespar/mcp-clip) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) + +## Quick Start + +### Claude Desktop + +Add to `~/.config/claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "clip": { + "command": "npx", + "args": ["-y", "@codespar/mcp-clip"], + "env": { + "CLIP_AUTH_TOKEN": "your-basic-token" + } + } + } +} +``` + +### Claude Code + +```bash +claude mcp add clip -- npx @codespar/mcp-clip +``` + +## Environment + +| Variable | Required | Description | +|---|---|---| +| `CLIP_AUTH_TOKEN` | yes | Basic auth token from the Clip developer dashboard (sent as `Authorization: Basic `) | +| `CLIP_BASE_URL` | no | API base override (default `https://api.payclip.com`) | + +Test mode: Clip distinguishes environments by the token itself — test tokens hit the sandbox processing path; there is no separate URL. + +## Tools (6) + +| Tool | What it does | +|---|---| +| `create_payment_link` | Hosted checkout link (`POST /v2/checkout`) — the no-PCI path | +| `get_payment_link` | Checkout status by id | +| `create_payment` | Transparent charge with a client-side card token (`POST /payments`) | +| `get_payment` | Payment detail | +| `list_payments` | List payments, paginated | +| `refund_payment` | Full or partial refund (`POST /payments/{id}/refund`) | + +Raw card numbers are never accepted — `create_payment` takes a token from Clip's client-side SDK. When you have no tokenization front-end, use `create_payment_link`. + +## Example + +> "Cobra 450 pesos por la consultoría y mándame el link." + +The agent calls `create_payment_link` (`amount: 450.00`, `purchase_description: "Consultoría"`) and returns the `payment_request_url` for the buyer. + +## Authentication + +Clip issues a single Basic auth token in the developer dashboard; the server sends it as `Authorization: Basic ` on every call. + +## Enterprise + +Need governance, budget limits, and audit trails for agent-driven payments on Clip? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds a policy engine, payment routing, and compliance templates on top of these MCP servers. + +## License + +MIT diff --git a/packages/mexico/clip/package.json b/packages/mexico/clip/package.json new file mode 100644 index 00000000..1b3d80ba --- /dev/null +++ b/packages/mexico/clip/package.json @@ -0,0 +1,36 @@ +{ + "name": "@codespar/mcp-clip", + "version": "0.1.0", + "description": "MCP server for Clip (PayClip) — Mexico's leading native acquirer: payment links, card charges, refunds", + "type": "module", + "main": "./dist/index.js", + "bin": { + "mcp-clip": "./dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + }, + "license": "MIT", + "keywords": [ + "mcp", + "clip", + "payclip", + "payments", + "acquirer", + "payment-link", + "checkout", + "mexico" + ], + "mcpName": "io.github.codespar/mcp-clip" +} diff --git a/packages/mexico/clip/server.json b/packages/mexico/clip/server.json new file mode 100644 index 00000000..264cc095 --- /dev/null +++ b/packages/mexico/clip/server.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.codespar/mcp-clip", + "description": "MCP server for Clip (PayClip) — Mexico's leading acquirer: payment links, card charges, refunds", + "repository": { + "url": "https://github.com/codespar/mcp-dev-latam", + "source": "github", + "subfolder": "packages/mexico/clip" + }, + "version": "0.1.0", + "packages": [ + { + "registryType": "npm", + "identifier": "@codespar/mcp-clip", + "version": "0.1.0", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "name": "CLIP_AUTH_TOKEN", + "description": "Basic auth token from the Clip developer dashboard", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "CLIP_BASE_URL", + "description": "API base URL override. Defaults to https://api.payclip.com.", + "isRequired": false, + "format": "string", + "isSecret": false + } + ] + } + ], + "provider": { + "homepage": "https://clip.mx", + "logoUrl": "https://logo.clearbit.com/clip.mx", + "logoFallback": "https://www.google.com/s2/favicons?domain=clip.mx&sz=128", + "docsUrl": "https://developer.clip.mx", + "sandbox": { + "available": true + } + } +} diff --git a/packages/mexico/clip/src/index.ts b/packages/mexico/clip/src/index.ts new file mode 100644 index 00000000..68fbef59 --- /dev/null +++ b/packages/mexico/clip/src/index.ts @@ -0,0 +1,267 @@ +#!/usr/bin/env node + +/** + * MCP Server for Clip (PayClip) — Mexico's leading native acquirer. + * + * Clip started as MX's dominant SMB card reader and its online API + * carries that footprint: hosted payment links (the fastest way to + * charge a Mexican card without PCI scope), transparent card-token + * charges, queries and refunds. Currency is MXN. + * + * Tools (6): + * create_payment_link — hosted checkout link (POST /v2/checkout) + * get_payment_link — checkout status by id (GET /v2/checkout/{id}) + * create_payment — transparent charge with a card token (POST /payments) + * get_payment — payment detail (GET /payments/{id}) + * list_payments — list payments, paginated (GET /payments) + * refund_payment — full or partial refund (POST /payments/{id}/refund) + * + * Authentication + * Clip issues a single Basic auth token in the developer dashboard + * (already the base64 of key:secret). Sent as + * `Authorization: Basic ` on every call. + * + * Environment + * CLIP_AUTH_TOKEN — Basic token from the Clip dashboard (required) + * CLIP_BASE_URL — API base override (default: https://api.payclip.com) + * + * Test mode: Clip distinguishes environments by the token itself (test + * tokens hit the sandbox processing path) — there is no separate URL. + * + * Docs: https://developer.clip.mx + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; + +const AUTH_TOKEN = process.env.CLIP_AUTH_TOKEN || ""; +const BASE_URL = (process.env.CLIP_BASE_URL || "https://api.payclip.com").replace(/\/$/, ""); + +async function clipRequest(method: string, path: string, body?: unknown): Promise { + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Authorization: `Basic ${AUTH_TOKEN}`, + }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!res.ok) { + const err = await res.text(); + throw new Error(`Clip API ${res.status}: ${err}`); + } + const text = await res.text(); + if (!text) return { ok: true }; + try { + return JSON.parse(text); + } catch { + return { raw: text }; + } +} + +// Managed-tier pointer surfaced to the agent via MCP `instructions`. +// Informational only — nothing CodeSpar-hosted is called (MIT-safe). +const MANAGED_TIER_HINT = + "This open-source CodeSpar server calls the provider's API directly. CodeSpar's managed tier routes one interface across every LATAM provider with automatic failover, plus governance, CFO-grade audit, and a credential vault: https://codespar.dev/agents (npx -y @codespar/mcp serve)."; + +const server = new Server( + { name: "mcp-clip", version: "0.1.0" }, + { capabilities: { tools: {} }, instructions: MANAGED_TIER_HINT }, +); + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "create_payment_link", + description: + "Create a hosted checkout / payment link (POST /v2/checkout). The buyer opens the returned payment_request_url and pays with card, Clip account or other enabled MX methods. Amount is in MXN pesos (decimal, e.g. 150.00).", + inputSchema: { + type: "object", + properties: { + amount: { type: "number", description: "Amount in MXN (decimal pesos, e.g. 150.00)" }, + currency: { type: "string", description: "Currency code. Clip processes MXN.", default: "MXN" }, + purchase_description: { type: "string", description: "What the buyer is paying for (shown at checkout)" }, + redirection_url: { + type: "object", + description: "Where the buyer lands after paying", + properties: { + success: { type: "string", description: "URL on success" }, + error: { type: "string", description: "URL on failure" }, + default: { type: "string", description: "Fallback URL" }, + }, + }, + metadata: { type: "object", description: "Optional metadata echoed back on queries/webhooks" }, + }, + required: ["amount", "purchase_description"], + }, + }, + { + name: "get_payment_link", + description: "Fetch a checkout / payment link by id (GET /v2/checkout/{id}) — status, amount, payment outcome.", + inputSchema: { + type: "object", + properties: { + checkout_id: { type: "string", description: "Checkout id returned by create_payment_link" }, + }, + required: ["checkout_id"], + }, + }, + { + name: "create_payment", + description: + "Create a transparent card charge (POST /payments) using a card token generated by Clip's client-side SDK. Use create_payment_link instead when you have no tokenization front-end — raw card numbers are never accepted here.", + inputSchema: { + type: "object", + properties: { + amount: { type: "number", description: "Amount in MXN (decimal pesos)" }, + currency: { type: "string", description: "Currency code. Clip processes MXN.", default: "MXN" }, + description: { type: "string", description: "Charge description" }, + card_token: { type: "string", description: "Card token from Clip's client-side SDK (never a PAN)" }, + customer: { + type: "object", + description: "Customer identification (email, first_name, last_name, phone)", + }, + installments: { type: "number", description: "Meses sin intereses installments (when enabled)" }, + metadata: { type: "object", description: "Optional metadata" }, + }, + required: ["amount", "card_token"], + }, + }, + { + name: "get_payment", + description: "Fetch a payment by id (GET /payments/{id}) — status, amounts, card summary, refunds.", + inputSchema: { + type: "object", + properties: { + payment_id: { type: "string", description: "Clip payment id" }, + }, + required: ["payment_id"], + }, + }, + { + name: "list_payments", + description: "List payments (GET /payments), newest first. Paginated via limit/offset.", + inputSchema: { + type: "object", + properties: { + limit: { type: "number", description: "Page size (max per Clip docs)" }, + offset: { type: "number", description: "Items to skip" }, + }, + }, + }, + { + name: "refund_payment", + description: + "Refund a payment (POST /payments/{id}/refund). Omit amount for a full refund; pass it for a partial refund in MXN.", + inputSchema: { + type: "object", + properties: { + payment_id: { type: "string", description: "Clip payment id" }, + amount: { type: "number", description: "Partial refund amount in MXN. Omit for full refund." }, + reason: { type: "string", description: "Refund reason (shown in the Clip panel)" }, + }, + required: ["payment_id"], + }, + }, + ], +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params as { + name: string; + arguments?: Record; + }; + try { + switch (name) { + case "create_payment_link": { + const body: Record = { + amount: args?.amount, + currency: args?.currency ?? "MXN", + purchase_description: args?.purchase_description, + }; + if (args?.redirection_url) body.redirection_url = args.redirection_url; + if (args?.metadata) body.metadata = args.metadata; + return { + content: [ + { type: "text", text: JSON.stringify(await clipRequest("POST", "/v2/checkout", body), null, 2) }, + ], + }; + } + case "get_payment_link": + return { + content: [ + { type: "text", text: JSON.stringify(await clipRequest("GET", `/v2/checkout/${args?.checkout_id}`), null, 2) }, + ], + }; + case "create_payment": { + const body: Record = { + amount: args?.amount, + currency: args?.currency ?? "MXN", + description: args?.description, + payment_method: { token: args?.card_token }, + }; + if (args?.customer) body.customer = args.customer; + if (args?.installments !== undefined) body.installments = args.installments; + if (args?.metadata) body.metadata = args.metadata; + return { + content: [ + { type: "text", text: JSON.stringify(await clipRequest("POST", "/payments", body), null, 2) }, + ], + }; + } + case "get_payment": + return { + content: [ + { type: "text", text: JSON.stringify(await clipRequest("GET", `/payments/${args?.payment_id}`), null, 2) }, + ], + }; + case "list_payments": { + const params = new URLSearchParams(); + if (args?.limit !== undefined) params.set("limit", String(args.limit)); + if (args?.offset !== undefined) params.set("offset", String(args.offset)); + const qs = params.toString(); + return { + content: [ + { type: "text", text: JSON.stringify(await clipRequest("GET", `/payments${qs ? `?${qs}` : ""}`), null, 2) }, + ], + }; + } + case "refund_payment": { + const body: Record = {}; + if (args?.amount !== undefined) body.amount = args.amount; + if (args?.reason) body.reason = args.reason; + return { + content: [ + { + type: "text", + text: JSON.stringify( + await clipRequest("POST", `/payments/${args?.payment_id}/refund`, Object.keys(body).length ? body : undefined), + null, + 2, + ), + }, + ], + }; + } + default: + return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true }; + } + } catch (err) { + return { + content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } +}); + +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch(console.error); diff --git a/packages/mexico/clip/tsconfig.json b/packages/mexico/clip/tsconfig.json new file mode 100644 index 00000000..40c56c6e --- /dev/null +++ b/packages/mexico/clip/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "declaration": true + }, + "include": ["src"] +} diff --git a/packages/payments/flow-cl/README.md b/packages/payments/flow-cl/README.md new file mode 100644 index 00000000..d66a65e6 --- /dev/null +++ b/packages/payments/flow-cl/README.md @@ -0,0 +1,81 @@ +# @codespar/mcp-flow-cl + +> MCP server for **Flow** (flow.cl) — the Chile-native PSP: one hosted payment order covers Webpay (credit/debit), ETpay bank transfer, cash networks and international cards. Refunds included. Every API call is HMAC-SHA256 signed. + +[![npm](https://img.shields.io/npm/v/@codespar/mcp-flow-cl)](https://www.npmjs.com/package/@codespar/mcp-flow-cl) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) + +## Quick Start + +### Claude Desktop + +Add to `~/.config/claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "flow-cl": { + "command": "npx", + "args": ["-y", "@codespar/mcp-flow-cl"], + "env": { + "FLOW_API_KEY": "your-flow-api-key", + "FLOW_SECRET_KEY": "your-flow-secret-key", + "FLOW_ENV": "sandbox" + } + } + } +} +``` + +### Claude Code + +```bash +claude mcp add flow-cl --env FLOW_API_KEY=... --env FLOW_SECRET_KEY=... -- npx -y @codespar/mcp-flow-cl +``` + +### Cursor / VS Code + +Add the same block to `.cursor/mcp.json` or `.vscode/mcp.json`. + +## Tools (8) + +| Tool | Endpoint | What it does | +|---|---|---| +| `create_payment` | `POST /payment/create` | Hosted payment order; returns `url + token` the customer pays at | +| `create_payment_by_email` | `POST /payment/createEmail` | Flow emails the customer a collection link | +| `get_payment_status` | `GET /payment/getStatus` | Status by Flow token (1 pending, 2 paid, 3 rejected, 4 canceled) | +| `get_payment_status_by_commerce_id` | `GET /payment/getStatusByCommerceId` | Status by your `commerceOrder` | +| `get_payment_status_by_flow_order` | `GET /payment/getStatusByFlowOrder` | Status by Flow order number | +| `list_payments` | `GET /payment/getPayments` | Payments received on a date (paged) | +| `create_refund` | `POST /refund/create` | Full or partial refund order | +| `get_refund_status` | `GET /refund/getStatus` | Refund status by refund token | + +## Authentication + +Flow signs every request: parameters are sorted alphabetically, concatenated as `name+value`, and signed with HMAC-SHA256 using your `secretKey`. This server does the signing for you — set the two env vars and call tools with plain arguments. + +| Variable | Required | Description | +|---|---|---| +| `FLOW_API_KEY` | yes | Flow merchant API key | +| `FLOW_SECRET_KEY` | yes | HMAC signing secret (never sent on the wire) | +| `FLOW_ENV` | no | `sandbox` (default) or `production` | + +Sandbox: register at [sandbox.flow.cl](https://sandbox.flow.cl) — the sandbox issues its own key pair. + +## Example + +> "Create a Flow payment order for CLP 25,000, subject 'Plan Pro mensual', email cliente@ejemplo.cl, and give me the payment link." + +The agent calls `create_payment` and returns `url?token=...` — the customer picks Webpay, ETpay or cash at Flow's hosted page. + +## Enterprise + +Need governance, budget limits, and audit trails for agent payments? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds policy engine, payment routing, and compliance templates on top of these MCP servers. + +## Managed tier + +This open-source server calls Flow's API directly with your credentials. CodeSpar's managed tier routes one interface across every LATAM provider with automatic failover, governance, audit and a credential vault: [codespar.dev/agents](https://codespar.dev/agents). + +## License + +MIT diff --git a/packages/payments/flow-cl/package.json b/packages/payments/flow-cl/package.json new file mode 100644 index 00000000..68559527 --- /dev/null +++ b/packages/payments/flow-cl/package.json @@ -0,0 +1,37 @@ +{ + "name": "@codespar/mcp-flow-cl", + "version": "0.1.0", + "description": "MCP server for Flow — Chile-native PSP: hosted payment orders (Webpay, ETpay, cash), refunds, HMAC-signed API", + "type": "module", + "main": "./dist/index.js", + "bin": { + "mcp-flow-cl": "./dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + }, + "license": "MIT", + "keywords": [ + "mcp", + "flow", + "flow-cl", + "payments", + "psp", + "webpay", + "etpay", + "chile", + "clp" + ], + "mcpName": "io.github.codespar/mcp-flow-cl" +} diff --git a/packages/payments/flow-cl/server.json b/packages/payments/flow-cl/server.json new file mode 100644 index 00000000..9a713960 --- /dev/null +++ b/packages/payments/flow-cl/server.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.codespar/mcp-flow-cl", + "description": "MCP server for Flow.cl — Chilean PSP: hosted payment orders, Webpay, refunds, HMAC-signed API", + "repository": { + "url": "https://github.com/codespar/mcp-dev-latam", + "source": "github", + "subfolder": "packages/payments/flow-cl" + }, + "version": "0.1.0", + "packages": [ + { + "registryType": "npm", + "identifier": "@codespar/mcp-flow-cl", + "version": "0.1.0", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "name": "FLOW_API_KEY", + "description": "Flow merchant API key", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "FLOW_SECRET_KEY", + "description": "Flow secret key used to HMAC-SHA256 sign every request", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "FLOW_ENV", + "description": "Environment: 'sandbox' or 'production'. Defaults to 'sandbox'.", + "isRequired": false, + "format": "string", + "isSecret": false + } + ] + } + ], + "provider": { + "homepage": "https://www.flow.cl", + "logoUrl": "https://logo.clearbit.com/flow.cl", + "logoFallback": "https://www.google.com/s2/favicons?domain=flow.cl&sz=128", + "docsUrl": "https://www.flowapi.cl/docs/api.html", + "sandbox": { + "available": true + } + } +} diff --git a/packages/payments/flow-cl/src/index.ts b/packages/payments/flow-cl/src/index.ts new file mode 100644 index 00000000..4b815526 --- /dev/null +++ b/packages/payments/flow-cl/src/index.ts @@ -0,0 +1,347 @@ +#!/usr/bin/env node + +/** + * MCP Server for Flow — Chile-native PSP (flow.cl). + * + * Flow is the Chilean payment gateway used by SMB and mid-market + * commerce: one hosted payment order covers Webpay (credit/debit), + * ETpay bank transfer, Servipag/cash and international cards. The API + * is form-encoded and every call is signed: parameters are sorted + * alphabetically, concatenated as name+value, and signed with + * HMAC-SHA256 using the merchant's secretKey. + * + * Tools (8): + * create_payment — create a hosted payment order (returns url + token) + * create_payment_by_email — Flow emails the customer a collection link + * get_payment_status — payment status by Flow token + * get_payment_status_by_commerce_id — payment status by your commerceOrder id + * get_payment_status_by_flow_order — payment status by Flow order number + * list_payments — payments received on a given date (paged) + * create_refund — create a refund order + * get_refund_status — refund status by refund token + * + * APIs + * Production: https://www.flow.cl/api + * Sandbox: https://sandbox.flow.cl/api + * + * Authentication + * apiKey: merchant API key (sent as a parameter) + * secretKey: HMAC-SHA256 signing secret (never sent) + * + * Environment + * FLOW_API_KEY — Flow API key (required) + * FLOW_SECRET_KEY — Flow secret key for HMAC signing (required) + * FLOW_ENV — "sandbox" | "production" (default: "sandbox") + * + * Docs: https://www.flowapi.cl/docs/api.html + */ + +import { createHmac } from "node:crypto"; +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; + +const API_KEY = process.env.FLOW_API_KEY || ""; +const SECRET_KEY = process.env.FLOW_SECRET_KEY || ""; +const ENV = (process.env.FLOW_ENV || "sandbox").toLowerCase(); + +const BASE_URL = ENV === "production" + ? "https://www.flow.cl/api" + : "https://sandbox.flow.cl/api"; + +/** Flow signature: sort params alphabetically by name, concatenate as + * name+value (no separators), HMAC-SHA256 hex with the secretKey. */ +function signParams(params: Record): string { + const toSign = Object.keys(params) + .sort() + .map((k) => `${k}${params[k]}`) + .join(""); + return createHmac("sha256", SECRET_KEY).update(toSign).digest("hex"); +} + +/** Build the signed param set: apiKey + caller params + s. Skips + * undefined/null values so optional tool args never sign as "undefined". */ +function buildParams(raw: Record): URLSearchParams { + const params: Record = { apiKey: API_KEY }; + for (const [k, v] of Object.entries(raw)) { + if (v === undefined || v === null) continue; + params[k] = typeof v === "object" ? JSON.stringify(v) : String(v); + } + const s = signParams(params); + const out = new URLSearchParams(params); + out.set("s", s); + return out; +} + +async function flowRequest( + method: "GET" | "POST", + path: string, + rawParams: Record, +): Promise { + const params = buildParams(rawParams); + const url = method === "GET" + ? `${BASE_URL}${path}?${params.toString()}` + : `${BASE_URL}${path}`; + const res = await fetch(url, { + method, + headers: method === "POST" + ? { "Content-Type": "application/x-www-form-urlencoded" } + : undefined, + body: method === "POST" ? params.toString() : undefined, + }); + const text = await res.text(); + if (!res.ok) { + throw new Error(`Flow API ${res.status}: ${text}`); + } + try { + return JSON.parse(text); + } catch { + return { raw: text }; + } +} + +// Managed-tier pointer surfaced to the agent via MCP `instructions`. +// Informational only — nothing CodeSpar-hosted is called (MIT-safe). +const MANAGED_TIER_HINT = + "This open-source CodeSpar server calls the provider's API directly. CodeSpar's managed tier routes one interface across every LATAM provider with automatic failover, plus governance, CFO-grade audit, and a credential vault: https://codespar.dev/agents (npx -y @codespar/mcp serve)."; + +const server = new Server( + { name: "mcp-flow-cl", version: "0.1.0" }, + { capabilities: { tools: {} }, instructions: MANAGED_TIER_HINT }, +); + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "create_payment", + description: + "Create a hosted payment order (POST /payment/create). Returns url + token + flowOrder; the customer pays at url + '?token=' + token. paymentMethod 9 offers every method Flow has enabled for the merchant (Webpay, ETpay bank transfer, cash, international cards).", + inputSchema: { + type: "object", + properties: { + commerceOrder: { type: "string", description: "Your unique order id" }, + subject: { type: "string", description: "Payment description shown to the customer" }, + currency: { type: "string", description: "Currency (CLP or UF). Default CLP" }, + amount: { type: "number", description: "Amount to charge (integer for CLP)" }, + email: { type: "string", description: "Customer email" }, + paymentMethod: { type: "number", description: "1 Webpay, 3 all cards, 5 Onepay, 8 ETpay, 9 all enabled methods. Default 9" }, + urlConfirmation: { type: "string", description: "Server-to-server confirmation callback URL (POST, receives token)" }, + urlReturn: { type: "string", description: "URL the customer returns to after paying" }, + optional: { type: "object", description: "Optional JSON of extra fields echoed back in getStatus" }, + timeout: { type: "number", description: "Seconds the payment order stays valid (omit = no expiry)" }, + }, + required: ["commerceOrder", "subject", "amount", "email", "urlConfirmation", "urlReturn"], + }, + }, + { + name: "create_payment_by_email", + description: + "Create a collection order Flow emails to the customer (POST /payment/createEmail). Same shape as create_payment but Flow delivers the payment link by email — no checkout redirect needed.", + inputSchema: { + type: "object", + properties: { + commerceOrder: { type: "string", description: "Your unique order id" }, + subject: { type: "string", description: "Payment description shown to the customer" }, + currency: { type: "string", description: "Currency (CLP or UF). Default CLP" }, + amount: { type: "number", description: "Amount to charge (integer for CLP)" }, + email: { type: "string", description: "Customer email that receives the collection link" }, + urlConfirmation: { type: "string", description: "Server-to-server confirmation callback URL" }, + urlReturn: { type: "string", description: "URL the customer returns to after paying" }, + forward_days_after: { type: "number", description: "Resend the email N days after the first send" }, + forward_times: { type: "number", description: "How many times to resend" }, + timeout: { type: "number", description: "Seconds the payment order stays valid" }, + }, + required: ["commerceOrder", "subject", "amount", "email", "urlConfirmation", "urlReturn"], + }, + }, + { + name: "get_payment_status", + description: + "Get a payment order's status by Flow token (GET /payment/getStatus). status: 1 pending, 2 paid, 3 rejected, 4 canceled.", + inputSchema: { + type: "object", + properties: { + token: { type: "string", description: "Flow payment token (from create_payment or the confirmation callback)" }, + }, + required: ["token"], + }, + }, + { + name: "get_payment_status_by_commerce_id", + description: + "Get a payment order's status by your commerceOrder id (GET /payment/getStatusByCommerceId).", + inputSchema: { + type: "object", + properties: { + commerceId: { type: "string", description: "The commerceOrder you sent on create" }, + }, + required: ["commerceId"], + }, + }, + { + name: "get_payment_status_by_flow_order", + description: + "Get a payment order's status by Flow order number (GET /payment/getStatusByFlowOrder).", + inputSchema: { + type: "object", + properties: { + flowOrder: { type: "number", description: "Flow order number (returned by create_payment)" }, + }, + required: ["flowOrder"], + }, + }, + { + name: "list_payments", + description: + "List payments received on a given date (GET /payment/getPayments). Paged; date is the merchant's local day.", + inputSchema: { + type: "object", + properties: { + date: { type: "string", description: "Date to list, format YYYY-MM-DD" }, + start: { type: "number", description: "Pagination offset (default 0)" }, + limit: { type: "number", description: "Page size, max 100 (default 10)" }, + }, + required: ["date"], + }, + }, + { + name: "create_refund", + description: + "Create a refund order (POST /refund/create). Refund a received payment fully or partially; Flow notifies urlCallBack when the refund resolves.", + inputSchema: { + type: "object", + properties: { + refundCommerceOrder: { type: "string", description: "Your unique refund order id" }, + receiverEmail: { type: "string", description: "Email of the refund receiver" }, + amount: { type: "number", description: "Amount to refund" }, + urlCallBack: { type: "string", description: "Callback URL Flow notifies when the refund resolves" }, + commerceTrxId: { type: "string", description: "Original commerceOrder (alternative to flowTrxId)" }, + flowTrxId: { type: "number", description: "Original Flow order number (alternative to commerceTrxId)" }, + }, + required: ["refundCommerceOrder", "receiverEmail", "amount", "urlCallBack"], + }, + }, + { + name: "get_refund_status", + description: + "Get a refund order's status by refund token (GET /refund/getStatus). status: created, accepted, rejected, refunded, canceled.", + inputSchema: { + type: "object", + properties: { + token: { type: "string", description: "Refund token (returned by create_refund)" }, + }, + required: ["token"], + }, + }, + ], +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + try { + switch (name) { + case "create_payment": { + return { + content: [ + { type: "text", text: JSON.stringify(await flowRequest("POST", "/payment/create", args ?? {}), null, 2) }, + ], + }; + } + case "create_payment_by_email": { + return { + content: [ + { type: "text", text: JSON.stringify(await flowRequest("POST", "/payment/createEmail", args ?? {}), null, 2) }, + ], + }; + } + case "get_payment_status": { + return { + content: [ + { type: "text", text: JSON.stringify(await flowRequest("GET", "/payment/getStatus", { token: args?.token }), null, 2) }, + ], + }; + } + case "get_payment_status_by_commerce_id": { + return { + content: [ + { type: "text", text: JSON.stringify(await flowRequest("GET", "/payment/getStatusByCommerceId", { commerceId: args?.commerceId }), null, 2) }, + ], + }; + } + case "get_payment_status_by_flow_order": { + return { + content: [ + { type: "text", text: JSON.stringify(await flowRequest("GET", "/payment/getStatusByFlowOrder", { flowOrder: args?.flowOrder }), null, 2) }, + ], + }; + } + case "list_payments": { + return { + content: [ + { type: "text", text: JSON.stringify(await flowRequest("GET", "/payment/getPayments", { date: args?.date, start: args?.start, limit: args?.limit }), null, 2) }, + ], + }; + } + case "create_refund": { + return { + content: [ + { type: "text", text: JSON.stringify(await flowRequest("POST", "/refund/create", args ?? {}), null, 2) }, + ], + }; + } + case "get_refund_status": { + return { + content: [ + { type: "text", text: JSON.stringify(await flowRequest("GET", "/refund/getStatus", { token: args?.token }), null, 2) }, + ], + }; + } + default: + return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true }; + } + } catch (err) { + return { + content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } +}); + +async function main() { + if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") { + const { default: express } = await import("express"); + const { randomUUID } = await import("node:crypto"); + const app = express(); + app.use(express.json()); + const transports = new Map(); + app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size })); + app.post("/mcp", async (req: any, res: any) => { + const sid = req.headers["mcp-session-id"] as string | undefined; + if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; } + if (!sid && isInitializeRequest(req.body)) { + const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } }); + t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); }; + const s = new Server({ name: "mcp-flow-cl", version: "0.1.0" }, { capabilities: { tools: {} } }); + (server as any)._requestHandlers.forEach((v: any, k: any) => (s as any)._requestHandlers.set(k, v)); + (server as any)._notificationHandlers?.forEach((v: any, k: any) => (s as any)._notificationHandlers.set(k, v)); + await s.connect(t); + await t.handleRequest(req, res, req.body); return; + } + res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null }); + }); + app.get("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); }); + app.delete("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); }); + const port = Number(process.env.MCP_PORT) || 3000; + app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); }); + } else { + const transport = new StdioServerTransport(); + await server.connect(transport); + } +} + +main().catch(console.error); diff --git a/packages/payments/flow-cl/tsconfig.json b/packages/payments/flow-cl/tsconfig.json new file mode 100644 index 00000000..40c56c6e --- /dev/null +++ b/packages/payments/flow-cl/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "declaration": true + }, + "include": ["src"] +} diff --git a/packages/payments/kushki/README.md b/packages/payments/kushki/README.md new file mode 100644 index 00000000..1af7d044 --- /dev/null +++ b/packages/payments/kushki/README.md @@ -0,0 +1,83 @@ +# @codespar/mcp-kushki + +> MCP server for **Kushki** — the omnichannel PSP spanning Mexico, Colombia, Chile, Peru and Ecuador: card tokenization + one-step/two-step charges, bank transfer-in (PSE / SPEI), cash networks and subscriptions, all on one REST API. + +[![npm](https://img.shields.io/npm/v/@codespar/mcp-kushki)](https://www.npmjs.com/package/@codespar/mcp-kushki) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) + +## Quick Start + +### Claude Desktop + +Add to `~/.config/claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "kushki": { + "command": "npx", + "args": ["-y", "@codespar/mcp-kushki"], + "env": { + "KUSHKI_PUBLIC_MERCHANT_ID": "your-public-merchant-id", + "KUSHKI_PRIVATE_MERCHANT_ID": "your-private-merchant-id", + "KUSHKI_ENV": "sandbox" + } + } + } +} +``` + +### Claude Code + +```bash +claude mcp add kushki --env KUSHKI_PUBLIC_MERCHANT_ID=... --env KUSHKI_PRIVATE_MERCHANT_ID=... -- npx -y @codespar/mcp-kushki +``` + +### Cursor / VS Code + +Add the same block to `.cursor/mcp.json` or `.vscode/mcp.json`. + +## Tools (10) + +| Tool | Endpoint | What it does | +|---|---|---| +| `tokenize_card` | `POST /card/v1/tokens` | Raw card → single-use token (public id) | +| `create_charge` | `POST /card/v1/charges` | One-step card charge | +| `create_preauthorization` | `POST /card/v1/preAuthorization` | Hold funds (two-step, step 1) | +| `capture_preauthorization` | `POST /card/v1/capture` | Capture a preauth (step 2, partial OK) | +| `void_charge` | `DELETE /card/v1/charges/{ticket}` | Void (same-day) or refund (settled) | +| `create_transfer_token` | `POST /transfer/v1/tokens` | Token for PSE (CO) / SPEI (MX) transfer-in | +| `create_transfer_charge` | `POST /transfer/v1/init` | Start the bank transfer; returns redirect URL | +| `create_cash_charge` | `POST /cash/v1/charges` | Cash voucher for OXXO-style networks | +| `create_subscription` | `POST /card/v1/subscriptions` | Recurring card charge on a periodicity | +| `cancel_subscription` | `DELETE /card/v1/subscriptions/{id}` | Stop future charges | + +## Authentication + +Two merchant ids with distinct powers: + +| Variable | Required | Description | +|---|---|---| +| `KUSHKI_PUBLIC_MERCHANT_ID` | yes | Tokenization only — safe for client-side use | +| `KUSHKI_PRIVATE_MERCHANT_ID` | yes | Money movement — server-side only | +| `KUSHKI_ENV` | no | `sandbox` (api-uat, default) or `production` | + +Amounts use Kushki's per-country tax shape: `{ subtotalIva, subtotalIva0, iva, currency }` — currencies MXN, COP, CLP, PEN, USD. + +## Example + +> "Tokenize this test card and charge COP 85,000 with 19% IVA." + +The agent calls `tokenize_card`, then `create_charge` with `{ subtotalIva: 85000, subtotalIva0: 0, iva: 16150, currency: "COP" }`. + +## Enterprise + +Need governance, budget limits, and audit trails for agent payments? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds policy engine, payment routing, and compliance templates on top of these MCP servers. + +## Managed tier + +This open-source server calls Kushki's API directly with your credentials. CodeSpar's managed tier routes one interface across every LATAM provider with automatic failover, governance, audit and a credential vault: [codespar.dev/agents](https://codespar.dev/agents). + +## License + +MIT diff --git a/packages/payments/kushki/package.json b/packages/payments/kushki/package.json new file mode 100644 index 00000000..26173044 --- /dev/null +++ b/packages/payments/kushki/package.json @@ -0,0 +1,41 @@ +{ + "name": "@codespar/mcp-kushki", + "version": "0.1.0", + "description": "MCP server for Kushki — omnichannel LATAM PSP: card charges, preauth/capture, PSE/SPEI transfer-in, cash, subscriptions", + "type": "module", + "main": "./dist/index.js", + "bin": { + "mcp-kushki": "./dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + }, + "license": "MIT", + "keywords": [ + "mcp", + "kushki", + "payments", + "psp", + "pse", + "spei", + "cash", + "subscriptions", + "mexico", + "colombia", + "chile", + "peru", + "ecuador" + ], + "mcpName": "io.github.codespar/mcp-kushki" +} diff --git a/packages/payments/kushki/server.json b/packages/payments/kushki/server.json new file mode 100644 index 00000000..b1d39ea6 --- /dev/null +++ b/packages/payments/kushki/server.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.codespar/mcp-kushki", + "description": "MCP server for Kushki — LATAM PSP: card charges, preauth, PSE/SPEI transfer, cash, subscriptions", + "repository": { + "url": "https://github.com/codespar/mcp-dev-latam", + "source": "github", + "subfolder": "packages/payments/kushki" + }, + "version": "0.1.0", + "packages": [ + { + "registryType": "npm", + "identifier": "@codespar/mcp-kushki", + "version": "0.1.0", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "name": "KUSHKI_PUBLIC_MERCHANT_ID", + "description": "Kushki public merchant id (card/transfer tokenization)", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "KUSHKI_PRIVATE_MERCHANT_ID", + "description": "Kushki private merchant id (charges, captures, voids, subscriptions)", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "KUSHKI_ENV", + "description": "Environment: 'sandbox' (api-uat) or 'production'. Defaults to 'sandbox'.", + "isRequired": false, + "format": "string", + "isSecret": false + } + ] + } + ], + "provider": { + "homepage": "https://www.kushki.com", + "logoUrl": "https://logo.clearbit.com/kushki.com", + "logoFallback": "https://www.google.com/s2/favicons?domain=kushki.com&sz=128", + "docsUrl": "https://docs.kushki.com", + "sandbox": { + "available": true + } + } +} diff --git a/packages/payments/kushki/src/index.ts b/packages/payments/kushki/src/index.ts new file mode 100644 index 00000000..39b72bd8 --- /dev/null +++ b/packages/payments/kushki/src/index.ts @@ -0,0 +1,412 @@ +#!/usr/bin/env node + +/** + * MCP Server for Kushki — omnichannel LATAM PSP (kushkipagos.com). + * + * Kushki spans Mexico, Colombia, Chile, Peru and Ecuador with one REST + * API: card tokenization + one-step or two-step card charges, bank + * transfer-in (PSE in Colombia, SPEI in Mexico), cash networks, and + * card subscriptions. Two credentials: the PUBLIC merchant id + * tokenizes cards (safe for client-side), the PRIVATE merchant id + * moves money (server-side only). + * + * Tools (10): + * tokenize_card — exchange raw card data for a single-use token + * create_charge — one-step card charge with a token + * create_preauthorization — two-step: hold funds on the card + * capture_preauthorization — two-step: capture a previous preauth + * void_charge — void / refund a card charge by ticketNumber + * create_transfer_token — token for a bank-transfer-in (PSE / SPEI) + * create_transfer_charge — bank transfer-in charge with a transfer token + * create_cash_charge — cash payment reference for OXXO-style networks + * create_subscription — recurring card subscription + * cancel_subscription — cancel a subscription + * + * APIs + * Production: https://api.kushkipagos.com + * Testing: https://api-uat.kushkipagos.com + * + * Environment + * KUSHKI_PUBLIC_MERCHANT_ID — public merchant id (tokenization) + * KUSHKI_PRIVATE_MERCHANT_ID — private merchant id (charges) + * KUSHKI_ENV — "sandbox" | "production" (default: "sandbox") + * + * Docs: https://docs.kushki.com + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; + +const PUBLIC_MERCHANT_ID = process.env.KUSHKI_PUBLIC_MERCHANT_ID || ""; +const PRIVATE_MERCHANT_ID = process.env.KUSHKI_PRIVATE_MERCHANT_ID || ""; +const ENV = (process.env.KUSHKI_ENV || "sandbox").toLowerCase(); + +const BASE_URL = ENV === "production" + ? "https://api.kushkipagos.com" + : "https://api-uat.kushkipagos.com"; + +async function kushkiRequest( + method: string, + path: string, + auth: "public" | "private", + body?: unknown, +): Promise { + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + "Content-Type": "application/json", + ...(auth === "public" + ? { "Public-Merchant-Id": PUBLIC_MERCHANT_ID } + : { "Private-Merchant-Id": PRIVATE_MERCHANT_ID }), + }, + body: body ? JSON.stringify(body) : undefined, + }); + const text = await res.text(); + if (!res.ok) { + throw new Error(`Kushki API ${res.status}: ${text}`); + } + if (!text) return { ok: true }; + try { + return JSON.parse(text); + } catch { + return { raw: text }; + } +} + +// Managed-tier pointer surfaced to the agent via MCP `instructions`. +// Informational only — nothing CodeSpar-hosted is called (MIT-safe). +const MANAGED_TIER_HINT = + "This open-source CodeSpar server calls the provider's API directly. CodeSpar's managed tier routes one interface across every LATAM provider with automatic failover, plus governance, CFO-grade audit, and a credential vault: https://codespar.dev/agents (npx -y @codespar/mcp serve)."; + +const server = new Server( + { name: "mcp-kushki", version: "0.1.0" }, + { capabilities: { tools: {} }, instructions: MANAGED_TIER_HINT }, +); + +const AMOUNT_SCHEMA = { + type: "object", + description: + "Kushki amount object. subtotalIva = taxed subtotal, subtotalIva0 = untaxed subtotal, iva = tax. currency per country: MXN, COP, CLP, PEN, USD (EC).", + properties: { + subtotalIva: { type: "number", description: "Taxed subtotal (0 if none)" }, + subtotalIva0: { type: "number", description: "Untaxed subtotal" }, + iva: { type: "number", description: "Tax amount (0 if none)" }, + ice: { type: "number", description: "ICE tax (Ecuador, optional)" }, + currency: { type: "string", description: "MXN | COP | CLP | PEN | USD" }, + }, + required: ["subtotalIva", "subtotalIva0", "iva", "currency"], +} as const; + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "tokenize_card", + description: + "Exchange raw card data for a single-use token (POST /card/v1/tokens, public merchant id). The token feeds create_charge / create_preauthorization / create_subscription. Card data never touches your backend.", + inputSchema: { + type: "object", + properties: { + card: { + type: "object", + description: "Card data", + properties: { + name: { type: "string", description: "Cardholder name" }, + number: { type: "string", description: "Card number (PAN)" }, + expiryMonth: { type: "string", description: "MM" }, + expiryYear: { type: "string", description: "YY" }, + cvv: { type: "string", description: "Security code" }, + }, + required: ["name", "number", "expiryMonth", "expiryYear", "cvv"], + }, + totalAmount: { type: "number", description: "Total amount the token will charge" }, + currency: { type: "string", description: "MXN | COP | CLP | PEN | USD" }, + }, + required: ["card", "totalAmount", "currency"], + }, + }, + { + name: "create_charge", + description: + "One-step card charge (POST /card/v1/charges, private merchant id). Pass the token from tokenize_card plus the amount object. Supports metadata, months (MX installments) and deferred (installment plans).", + inputSchema: { + type: "object", + properties: { + token: { type: "string", description: "Single-use token from tokenize_card" }, + amount: AMOUNT_SCHEMA, + months: { type: "number", description: "Installments (Mexico meses sin intereses)" }, + deferred: { type: "object", description: "Deferred/installment config (country-specific)" }, + metadata: { type: "object", description: "Free-form metadata echoed in webhooks" }, + contactDetails: { type: "object", description: "Customer contact: documentType, documentNumber, email, firstName, lastName, phoneNumber" }, + fullResponse: { type: "boolean", description: "Return the acquirer's full response" }, + }, + required: ["token", "amount"], + }, + }, + { + name: "create_preauthorization", + description: + "Two-step charge, step 1: hold funds on the card (POST /card/v1/preAuthorization, private merchant id). Returns a ticketNumber to capture later.", + inputSchema: { + type: "object", + properties: { + token: { type: "string", description: "Single-use token from tokenize_card" }, + amount: AMOUNT_SCHEMA, + metadata: { type: "object", description: "Free-form metadata" }, + fullResponse: { type: "boolean", description: "Return the acquirer's full response" }, + }, + required: ["token", "amount"], + }, + }, + { + name: "capture_preauthorization", + description: + "Two-step charge, step 2: capture a previous preauthorization (POST /card/v1/capture, private merchant id). Amount may be lower than the preauth (partial capture).", + inputSchema: { + type: "object", + properties: { + ticketNumber: { type: "string", description: "ticketNumber returned by create_preauthorization" }, + amount: AMOUNT_SCHEMA, + fullResponse: { type: "boolean", description: "Return the acquirer's full response" }, + }, + required: ["ticketNumber", "amount"], + }, + }, + { + name: "void_charge", + description: + "Void / refund a card charge (DELETE /card/v1/charges/{ticketNumber}, private merchant id). Same-day = void; settled = refund. Optional partial amount where the acquirer supports it.", + inputSchema: { + type: "object", + properties: { + ticketNumber: { type: "string", description: "ticketNumber of the charge to void/refund" }, + amount: { ...AMOUNT_SCHEMA, description: "Optional partial amount (defaults to full)" }, + }, + required: ["ticketNumber"], + }, + }, + { + name: "create_transfer_token", + description: + "Token for a bank-transfer-in (POST /transfer/v1/tokens, public merchant id). PSE in Colombia, SPEI in Mexico, bank transfer in Chile/Peru. Feeds create_transfer_charge.", + inputSchema: { + type: "object", + properties: { + amount: AMOUNT_SCHEMA, + callbackUrl: { type: "string", description: "URL the customer returns to after bank auth" }, + userType: { type: "string", description: "0 = natural person, 1 = company (PSE)" }, + documentType: { type: "string", description: "Customer document type (e.g. CC, NIT, CE)" }, + documentNumber: { type: "string", description: "Customer document number" }, + email: { type: "string", description: "Customer email" }, + bankId: { type: "string", description: "Bank code (from Kushki's bank list, PSE)" }, + description: { type: "string", description: "Charge description" }, + }, + required: ["amount", "callbackUrl", "email"], + }, + }, + { + name: "create_transfer_charge", + description: + "Bank transfer-in charge with a transfer token (POST /transfer/v1/init, private merchant id). Returns the bank redirect URL the customer authorizes at.", + inputSchema: { + type: "object", + properties: { + token: { type: "string", description: "Token from create_transfer_token" }, + amount: AMOUNT_SCHEMA, + metadata: { type: "object", description: "Free-form metadata" }, + }, + required: ["token", "amount"], + }, + }, + { + name: "create_cash_charge", + description: + "Cash payment reference (POST /cash/v1/charges, private merchant id). Generates a voucher/reference the customer pays at a cash network (OXXO-style). Returns reference + expiry.", + inputSchema: { + type: "object", + properties: { + name: { type: "string", description: "Customer first name" }, + lastName: { type: "string", description: "Customer last name" }, + documentType: { type: "string", description: "Customer document type" }, + documentNumber: { type: "string", description: "Customer document number" }, + email: { type: "string", description: "Customer email" }, + totalAmount: { type: "number", description: "Amount to collect" }, + currency: { type: "string", description: "MXN | COP | CLP | PEN | USD" }, + description: { type: "string", description: "Charge description on the voucher" }, + expirationDate: { type: "string", description: "Voucher expiry, YYYY-MM-DD (optional)" }, + }, + required: ["name", "lastName", "documentNumber", "email", "totalAmount", "currency"], + }, + }, + { + name: "create_subscription", + description: + "Recurring card subscription (POST /card/v1/subscriptions, private merchant id). Kushki charges the tokenized card on the given periodicity until cancelled.", + inputSchema: { + type: "object", + properties: { + token: { type: "string", description: "Single-use token from tokenize_card" }, + planName: { type: "string", description: "Plan name shown on statements" }, + periodicity: { type: "string", description: "daily | weekly | biweekly | monthly | bimonthly | quarterly | halfYearly | yearly" }, + contactDetails: { + type: "object", + description: "Customer contact", + properties: { + firstName: { type: "string", description: "First name" }, + lastName: { type: "string", description: "Last name" }, + email: { type: "string", description: "Email" }, + documentType: { type: "string", description: "Document type" }, + documentNumber: { type: "string", description: "Document number" }, + phoneNumber: { type: "string", description: "Phone" }, + }, + required: ["firstName", "lastName", "email"], + }, + amount: AMOUNT_SCHEMA, + startDate: { type: "string", description: "First charge date, YYYY-MM-DD" }, + metadata: { type: "object", description: "Free-form metadata" }, + }, + required: ["token", "planName", "periodicity", "contactDetails", "amount", "startDate"], + }, + }, + { + name: "cancel_subscription", + description: + "Cancel a subscription (DELETE /card/v1/subscriptions/{subscriptionId}, private merchant id). Stops all future charges.", + inputSchema: { + type: "object", + properties: { + subscriptionId: { type: "string", description: "Subscription id returned on create" }, + }, + required: ["subscriptionId"], + }, + }, + ], +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + try { + switch (name) { + case "tokenize_card": { + const body = { card: args?.card, totalAmount: args?.totalAmount, currency: args?.currency }; + return { + content: [ + { type: "text", text: JSON.stringify(await kushkiRequest("POST", "/card/v1/tokens", "public", body), null, 2) }, + ], + }; + } + case "create_charge": { + return { + content: [ + { type: "text", text: JSON.stringify(await kushkiRequest("POST", "/card/v1/charges", "private", args), null, 2) }, + ], + }; + } + case "create_preauthorization": { + return { + content: [ + { type: "text", text: JSON.stringify(await kushkiRequest("POST", "/card/v1/preAuthorization", "private", args), null, 2) }, + ], + }; + } + case "capture_preauthorization": { + return { + content: [ + { type: "text", text: JSON.stringify(await kushkiRequest("POST", "/card/v1/capture", "private", args), null, 2) }, + ], + }; + } + case "void_charge": { + const path = `/card/v1/charges/${args?.ticketNumber}`; + const body = args?.amount ? { amount: args.amount } : undefined; + return { + content: [ + { type: "text", text: JSON.stringify(await kushkiRequest("DELETE", path, "private", body), null, 2) }, + ], + }; + } + case "create_transfer_token": { + return { + content: [ + { type: "text", text: JSON.stringify(await kushkiRequest("POST", "/transfer/v1/tokens", "public", args), null, 2) }, + ], + }; + } + case "create_transfer_charge": { + return { + content: [ + { type: "text", text: JSON.stringify(await kushkiRequest("POST", "/transfer/v1/init", "private", args), null, 2) }, + ], + }; + } + case "create_cash_charge": { + return { + content: [ + { type: "text", text: JSON.stringify(await kushkiRequest("POST", "/cash/v1/charges", "private", args), null, 2) }, + ], + }; + } + case "create_subscription": { + return { + content: [ + { type: "text", text: JSON.stringify(await kushkiRequest("POST", "/card/v1/subscriptions", "private", args), null, 2) }, + ], + }; + } + case "cancel_subscription": { + const path = `/card/v1/subscriptions/${args?.subscriptionId}`; + return { + content: [ + { type: "text", text: JSON.stringify(await kushkiRequest("DELETE", path, "private"), null, 2) }, + ], + }; + } + default: + return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true }; + } + } catch (err) { + return { + content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } +}); + +async function main() { + if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") { + const { default: express } = await import("express"); + const { randomUUID } = await import("node:crypto"); + const app = express(); + app.use(express.json()); + const transports = new Map(); + app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size })); + app.post("/mcp", async (req: any, res: any) => { + const sid = req.headers["mcp-session-id"] as string | undefined; + if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; } + if (!sid && isInitializeRequest(req.body)) { + const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } }); + t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); }; + const s = new Server({ name: "mcp-kushki", version: "0.1.0" }, { capabilities: { tools: {} } }); + (server as any)._requestHandlers.forEach((v: any, k: any) => (s as any)._requestHandlers.set(k, v)); + (server as any)._notificationHandlers?.forEach((v: any, k: any) => (s as any)._notificationHandlers.set(k, v)); + await s.connect(t); + await t.handleRequest(req, res, req.body); return; + } + res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null }); + }); + app.get("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); }); + app.delete("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); }); + const port = Number(process.env.MCP_PORT) || 3000; + app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); }); + } else { + const transport = new StdioServerTransport(); + await server.connect(transport); + } +} + +main().catch(console.error); diff --git a/packages/payments/kushki/tsconfig.json b/packages/payments/kushki/tsconfig.json new file mode 100644 index 00000000..40c56c6e --- /dev/null +++ b/packages/payments/kushki/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "declaration": true + }, + "include": ["src"] +} diff --git a/packages/payments/niubiz/README.md b/packages/payments/niubiz/README.md new file mode 100644 index 00000000..83ca6e90 --- /dev/null +++ b/packages/payments/niubiz/README.md @@ -0,0 +1,73 @@ +# @codespar/mcp-niubiz + +> MCP server for **Niubiz** (ex-VisaNet Perú) — Peru's dominant card acquirer, 300k+ merchants. Implements the real ecommerce flow: security token → checkout session → authorization, plus same-day reverse. + +[![npm](https://img.shields.io/npm/v/@codespar/mcp-niubiz)](https://www.npmjs.com/package/@codespar/mcp-niubiz) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) + +## Quick Start + +### Claude Desktop + +Add to `~/.config/claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "niubiz": { + "command": "npx", + "args": ["-y", "@codespar/mcp-niubiz"], + "env": { + "NIUBIZ_USER": "your-api-user", + "NIUBIZ_PASSWORD": "your-api-password", + "NIUBIZ_MERCHANT_ID": "your-merchant-id", + "NIUBIZ_ENV": "sandbox" + } + } + } +} +``` + +### Claude Code + +```bash +claude mcp add niubiz --env NIUBIZ_USER=... --env NIUBIZ_PASSWORD=... --env NIUBIZ_MERCHANT_ID=... -- npx -y @codespar/mcp-niubiz +``` + +### Cursor / VS Code + +Add the same block to `.cursor/mcp.json` or `.vscode/mcp.json`. + +## Tools (4) + +| Tool | Endpoint | What it does | +|---|---|---| +| `get_security_token` | `POST /api.security/v1/security` | Bearer token (cached 20 min; other tools auto-fetch) | +| `create_session_token` | `POST /api.ecommerce/v2/ecommerce/token/session/{merchantId}` | Checkout session with amount + antifraud data | +| `authorize_transaction` | `POST /api.authorization/v3/authorization/ecommerce/{merchantId}` | Authorize with the checkout's `transactionToken` | +| `reverse_transaction` | `POST /api.authorization/v3/reverse/ecommerce/{merchantId}` | Same-day void of an authorization | + +## The Niubiz flow + +Card data never touches this server: the customer types the card into Niubiz's own checkout form (fed by `create_session_token`), the form returns a `transactionToken`, and `authorize_transaction` completes the purchase. This is Niubiz's standard 3-step ecommerce integration. + +## Authentication + +| Variable | Required | Description | +|---|---|---| +| `NIUBIZ_USER` | yes | API user | +| `NIUBIZ_PASSWORD` | yes | API password | +| `NIUBIZ_MERCHANT_ID` | yes | Merchant id (código de comercio) | +| `NIUBIZ_ENV` | no | `sandbox` (default) or `production` | + +## Enterprise + +Need governance, budget limits, and audit trails for agent payments? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds policy engine, payment routing, and compliance templates on top of these MCP servers. + +## Managed tier + +This open-source server calls Niubiz's API directly with your credentials. CodeSpar's managed tier routes one interface across every LATAM provider with automatic failover, governance, audit and a credential vault: [codespar.dev/agents](https://codespar.dev/agents). + +## License + +MIT diff --git a/packages/payments/niubiz/package.json b/packages/payments/niubiz/package.json new file mode 100644 index 00000000..6f283a86 --- /dev/null +++ b/packages/payments/niubiz/package.json @@ -0,0 +1,36 @@ +{ + "name": "@codespar/mcp-niubiz", + "version": "0.1.0", + "description": "MCP server for Niubiz — Peru's dominant card acquirer (ex-VisaNet): session, authorization, reverse", + "type": "module", + "main": "./dist/index.js", + "bin": { + "mcp-niubiz": "./dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + }, + "license": "MIT", + "keywords": [ + "mcp", + "niubiz", + "visanet", + "payments", + "acquirer", + "cards", + "peru", + "pen" + ], + "mcpName": "io.github.codespar/mcp-niubiz" +} diff --git a/packages/payments/niubiz/server.json b/packages/payments/niubiz/server.json new file mode 100644 index 00000000..5ac9b5f2 --- /dev/null +++ b/packages/payments/niubiz/server.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.codespar/mcp-niubiz", + "description": "MCP server for Niubiz — Peru card acquirer: security token, session, authorize, reverse", + "repository": { + "url": "https://github.com/codespar/mcp-dev-latam", + "source": "github", + "subfolder": "packages/payments/niubiz" + }, + "version": "0.1.0", + "packages": [ + { + "registryType": "npm", + "identifier": "@codespar/mcp-niubiz", + "version": "0.1.0", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "name": "NIUBIZ_USER", + "description": "Niubiz API user", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "NIUBIZ_PASSWORD", + "description": "Niubiz API password", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "NIUBIZ_MERCHANT_ID", + "description": "Merchant id (código de comercio)", + "isRequired": true, + "format": "string", + "isSecret": false + }, + { + "name": "NIUBIZ_ENV", + "description": "Environment: 'sandbox' or 'production'. Defaults to 'sandbox'.", + "isRequired": false, + "format": "string", + "isSecret": false + } + ] + } + ], + "provider": { + "homepage": "https://www.niubiz.com.pe", + "logoUrl": "https://logo.clearbit.com/niubiz.com.pe", + "logoFallback": "https://www.google.com/s2/favicons?domain=niubiz.com.pe&sz=128", + "docsUrl": "https://desarrolladores.niubiz.com.pe", + "sandbox": { + "available": true + } + } +} diff --git a/packages/payments/niubiz/src/index.ts b/packages/payments/niubiz/src/index.ts new file mode 100644 index 00000000..648cd143 --- /dev/null +++ b/packages/payments/niubiz/src/index.ts @@ -0,0 +1,280 @@ +#!/usr/bin/env node + +/** + * MCP Server for Niubiz — Peru's dominant card acquirer (niubiz.com.pe). + * + * Niubiz (ex-VisaNet Perú, 300k+ merchants) runs the standard + * ecommerce card flow in Peru. The API is a 3-step dance: + * 1. get_security_token — Basic-auth user/password → bearer token + * 2. create_session_token — session for the checkout form (antifraud data) + * 3. authorize_transaction — authorize with the transactionToken the + * Niubiz checkout returns after the customer enters the card + * Plus reverse_transaction to undo an authorization same-day. + * + * The security token is cached in-process and refreshed on demand — + * tools other than get_security_token fetch it automatically. + * + * Tools (4): + * get_security_token — fetch (and cache) the API bearer token + * create_session_token — create a checkout session (amount + antifraud) + * authorize_transaction — authorize a purchase with a transactionToken + * reverse_transaction — reverse (void) an authorization + * + * APIs + * Production: https://apiprod.vnforapps.com + * Sandbox: https://apisandbox.vnforappstest.com + * + * Environment + * NIUBIZ_USER — API user (required) + * NIUBIZ_PASSWORD — API password (required) + * NIUBIZ_MERCHANT_ID — merchant id / código de comercio (required) + * NIUBIZ_ENV — "sandbox" | "production" (default: "sandbox") + * + * Docs: https://desarrolladores.niubiz.com.pe + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; + +const USER = process.env.NIUBIZ_USER || ""; +const PASSWORD = process.env.NIUBIZ_PASSWORD || ""; +const MERCHANT_ID = process.env.NIUBIZ_MERCHANT_ID || ""; +const ENV = (process.env.NIUBIZ_ENV || "sandbox").toLowerCase(); + +const BASE_URL = ENV === "production" + ? "https://apiprod.vnforapps.com" + : "https://apisandbox.vnforappstest.com"; + +/** Security tokens are valid for a limited window; cache and reuse, + * refetch when older than 20 minutes. */ +let cachedToken: { value: string; fetchedAt: number } | null = null; +const TOKEN_TTL_MS = 20 * 60 * 1000; + +async function getSecurityToken(force = false): Promise { + if (!force && cachedToken && Date.now() - cachedToken.fetchedAt < TOKEN_TTL_MS) { + return cachedToken.value; + } + const res = await fetch(`${BASE_URL}/api.security/v1/security`, { + method: "POST", + headers: { + Authorization: `Basic ${Buffer.from(`${USER}:${PASSWORD}`).toString("base64")}`, + }, + }); + const text = await res.text(); + if (!res.ok) { + throw new Error(`Niubiz security API ${res.status}: ${text}`); + } + // The endpoint returns the token as a plain string body. + const token = text.trim().replace(/^"|"$/g, ""); + cachedToken = { value: token, fetchedAt: Date.now() }; + return token; +} + +async function niubizRequest( + method: string, + path: string, + body?: unknown, +): Promise { + const token = await getSecurityToken(); + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + "Content-Type": "application/json", + Authorization: token, + }, + body: body ? JSON.stringify(body) : undefined, + }); + const text = await res.text(); + if (!res.ok) { + throw new Error(`Niubiz API ${res.status}: ${text}`); + } + if (!text) return { ok: true }; + try { + return JSON.parse(text); + } catch { + return { raw: text }; + } +} + +// Managed-tier pointer surfaced to the agent via MCP `instructions`. +// Informational only — nothing CodeSpar-hosted is called (MIT-safe). +const MANAGED_TIER_HINT = + "This open-source CodeSpar server calls the provider's API directly. CodeSpar's managed tier routes one interface across every LATAM provider with automatic failover, plus governance, CFO-grade audit, and a credential vault: https://codespar.dev/agents (npx -y @codespar/mcp serve)."; + +const server = new Server( + { name: "mcp-niubiz", version: "0.1.0" }, + { capabilities: { tools: {} }, instructions: MANAGED_TIER_HINT }, +); + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "get_security_token", + description: + "Fetch the Niubiz API bearer token (POST /api.security/v1/security, Basic auth). Other tools fetch and cache this automatically — call it explicitly only to verify credentials or force a refresh.", + inputSchema: { + type: "object", + properties: { + force: { type: "boolean", description: "Force a fresh token even if one is cached" }, + }, + }, + }, + { + name: "create_session_token", + description: + "Create a checkout session (POST /api.ecommerce/v2/ecommerce/token/session/{merchantId}). Returns the sessionKey the Niubiz checkout form needs. Send the purchase amount and the customer's IP for antifraud scoring.", + inputSchema: { + type: "object", + properties: { + amount: { type: "number", description: "Purchase amount (e.g. 100.00)" }, + channel: { type: "string", description: "Sales channel: web (default) | mobile" }, + clientIp: { type: "string", description: "Customer IP address (antifraud)" }, + merchantDefineData: { type: "object", description: "Optional MDD antifraud fields (MDD4 email, MDD21 frequent-flyer, etc.)" }, + }, + required: ["amount", "clientIp"], + }, + }, + { + name: "authorize_transaction", + description: + "Authorize a purchase (POST /api.authorization/v3/authorization/ecommerce/{merchantId}). Pass the transactionToken the Niubiz checkout form returned after the customer entered the card, plus your purchaseNumber and the amount.", + inputSchema: { + type: "object", + properties: { + transactionToken: { type: "string", description: "tokenId returned by the Niubiz checkout form" }, + purchaseNumber: { type: "string", description: "Your unique purchase number (numeric string, max 12 digits)" }, + amount: { type: "number", description: "Amount to authorize (must match the session)" }, + currency: { type: "string", description: "PEN (default) or USD" }, + channel: { type: "string", description: "Sales channel: web (default) | mobile" }, + captureType: { type: "string", description: "manual | automatic (default automatic)" }, + countable: { type: "boolean", description: "true = sale (capture), false = preauth only" }, + }, + required: ["transactionToken", "purchaseNumber", "amount"], + }, + }, + { + name: "reverse_transaction", + description: + "Reverse (void) an authorization (POST /api.authorization/v3/reverse/ecommerce/{merchantId}). Same-day undo of an authorized transaction by transactionId or purchaseNumber.", + inputSchema: { + type: "object", + properties: { + transactionId: { type: "string", description: "Niubiz transaction id to reverse" }, + purchaseNumber: { type: "string", description: "Your purchase number (alternative reference)" }, + }, + }, + }, + ], +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + try { + switch (name) { + case "get_security_token": { + const token = await getSecurityToken(Boolean(args?.force)); + return { + content: [ + { type: "text", text: JSON.stringify({ token, cached_until: new Date((cachedToken?.fetchedAt ?? 0) + TOKEN_TTL_MS).toISOString() }, null, 2) }, + ], + }; + } + case "create_session_token": { + const body = { + channel: args?.channel ?? "web", + amount: args?.amount, + antifraud: { + clientIp: args?.clientIp, + merchantDefineData: args?.merchantDefineData ?? {}, + }, + }; + const path = `/api.ecommerce/v2/ecommerce/token/session/${MERCHANT_ID}`; + return { + content: [ + { type: "text", text: JSON.stringify(await niubizRequest("POST", path, body), null, 2) }, + ], + }; + } + case "authorize_transaction": { + const body = { + channel: args?.channel ?? "web", + captureType: args?.captureType ?? "manual", + countable: args?.countable ?? true, + order: { + tokenId: args?.transactionToken, + purchaseNumber: args?.purchaseNumber, + amount: args?.amount, + currency: args?.currency ?? "PEN", + }, + }; + const path = `/api.authorization/v3/authorization/ecommerce/${MERCHANT_ID}`; + return { + content: [ + { type: "text", text: JSON.stringify(await niubizRequest("POST", path, body), null, 2) }, + ], + }; + } + case "reverse_transaction": { + const body = { + order: { + transactionId: args?.transactionId, + purchaseNumber: args?.purchaseNumber, + }, + }; + const path = `/api.authorization/v3/reverse/ecommerce/${MERCHANT_ID}`; + return { + content: [ + { type: "text", text: JSON.stringify(await niubizRequest("POST", path, body), null, 2) }, + ], + }; + } + default: + return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true }; + } + } catch (err) { + return { + content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } +}); + +async function main() { + if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") { + const { default: express } = await import("express"); + const { randomUUID } = await import("node:crypto"); + const app = express(); + app.use(express.json()); + const transports = new Map(); + app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size })); + app.post("/mcp", async (req: any, res: any) => { + const sid = req.headers["mcp-session-id"] as string | undefined; + if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; } + if (!sid && isInitializeRequest(req.body)) { + const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } }); + t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); }; + const s = new Server({ name: "mcp-niubiz", version: "0.1.0" }, { capabilities: { tools: {} } }); + (server as any)._requestHandlers.forEach((v: any, k: any) => (s as any)._requestHandlers.set(k, v)); + (server as any)._notificationHandlers?.forEach((v: any, k: any) => (s as any)._notificationHandlers.set(k, v)); + await s.connect(t); + await t.handleRequest(req, res, req.body); return; + } + res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null }); + }); + app.get("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); }); + app.delete("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); }); + const port = Number(process.env.MCP_PORT) || 3000; + app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); }); + } else { + const transport = new StdioServerTransport(); + await server.connect(transport); + } +} + +main().catch(console.error); diff --git a/packages/payments/niubiz/tsconfig.json b/packages/payments/niubiz/tsconfig.json new file mode 100644 index 00000000..40c56c6e --- /dev/null +++ b/packages/payments/niubiz/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "declaration": true + }, + "include": ["src"] +} diff --git a/packages/payments/pagbrasil/README.md b/packages/payments/pagbrasil/README.md new file mode 100644 index 00000000..c7d68ee3 --- /dev/null +++ b/packages/payments/pagbrasil/README.md @@ -0,0 +1,90 @@ +# @codespar/mcp-pagbrasil + +> MCP server for **PagBrasil** — the cross-border acquirer for international merchants selling into Brazil: Pix, Automatic Pix (PagStream), Boleto Flash, PEC Flash and local credit cards with installments. Distinct from PagBank/PagSeguro. + +[![npm](https://img.shields.io/npm/v/@codespar/mcp-pagbrasil)](https://www.npmjs.com/package/@codespar/mcp-pagbrasil) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) + +## Quick Start + +### Claude Desktop + +Add to `~/.config/claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "pagbrasil": { + "command": "npx", + "args": ["-y", "@codespar/mcp-pagbrasil"], + "env": { + "PAGBRASIL_PBTOKEN": "your-merchant-token", + "PAGBRASIL_SECRET": "your-secret-phrase" + } + } + } +} +``` + +### Claude Code + +```bash +claude mcp add pagbrasil -- npx @codespar/mcp-pagbrasil +``` + +### Cursor / VS Code + +Add to `.cursor/mcp.json` or `.vscode/mcp.json`: + +```json +{ + "servers": { + "pagbrasil": { + "command": "npx", + "args": ["-y", "@codespar/mcp-pagbrasil"], + "env": { + "PAGBRASIL_PBTOKEN": "your-merchant-token", + "PAGBRASIL_SECRET": "your-secret-phrase" + } + } + } +} +``` + +## Authentication + +Every request carries the merchant token (`pbtoken`) and the secret phrase as **body fields** — there is no auth header. This server injects both from `PAGBRASIL_PBTOKEN` and `PAGBRASIL_SECRET`. + +## API shape (worth knowing) + +PagBrasil's API is **form-urlencoded** (not JSON) and answers **XML**. The merchant token (`pbtoken`) and secret phrase travel as body fields on every request — this server injects both from the environment. Tool responses hand the raw XML to the agent under `xml` so no fields are lost. + +## Tools (3) + +| Tool | Endpoint | What it does | +|---|---|---| +| `create_order` | `POST /order/add` | Create an order / request a payment (pix, boleto, creditcard). Pix responses carry `pix_code` + `pix_image`; boleto responses carry the bar code + PDF URL | +| `get_order` | `POST /order/get` | Fetch an order's current status — poll to detect settlement | +| `refund_order` | `POST /order/refund` | Refund a settled order (`amount_brl` for partial, omit for full) | + +## Environment + +| Variable | Required | Description | +|---|---|---| +| `PAGBRASIL_PBTOKEN` | yes | Merchant token from the PagBrasil Dashboard | +| `PAGBRASIL_SECRET` | yes | Secret phrase from the Dashboard | +| `PAGBRASIL_BASE_URL` | no | Defaults to `https://sandbox.pagbrasil.com/api`. Production hosts are issued per-merchant after the Payment Service Agreement — set the URL your dashboard provides | + +## Notes + +- `amount_brl` is in **major units** as a string: `'125.00'` = R$ 125,00. +- `order_number` must be unique per `customer_taxid`. Same number + same taxid = idempotent no-op; same number + different taxid = rejected (`Duplicated order`). +- `customer_taxid` must be a valid CPF or CNPJ — agents should ask the user, never invent one. + +## Enterprise + +Need governance, budget limits, and audit trails for agent-driven payments on PagBrasil? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds a policy engine, payment routing, and compliance templates on top of these MCP servers. + +## License + +MIT © CodeSpar diff --git a/packages/payments/pagbrasil/package.json b/packages/payments/pagbrasil/package.json new file mode 100644 index 00000000..2963a49e --- /dev/null +++ b/packages/payments/pagbrasil/package.json @@ -0,0 +1,35 @@ +{ + "name": "@codespar/mcp-pagbrasil", + "version": "0.1.0", + "description": "MCP server for PagBrasil — cross-border acquiring into Brazil: Pix, Boleto Flash, local cards, refunds", + "type": "module", + "main": "./dist/index.js", + "bin": { + "mcp-pagbrasil": "./dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + }, + "license": "MIT", + "keywords": [ + "mcp", + "pagbrasil", + "payments", + "pix", + "boleto", + "cross-border", + "brazil" + ], + "mcpName": "io.github.codespar/mcp-pagbrasil" +} diff --git a/packages/payments/pagbrasil/server.json b/packages/payments/pagbrasil/server.json new file mode 100644 index 00000000..ca36d2bb --- /dev/null +++ b/packages/payments/pagbrasil/server.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.codespar/mcp-pagbrasil", + "description": "PagBrasil — cross-border acquiring into Brazil: Pix, Boleto Flash, refunds", + "repository": { + "url": "https://github.com/codespar/mcp-dev-latam", + "source": "github", + "subfolder": "packages/payments/pagbrasil" + }, + "version": "0.1.0", + "packages": [ + { + "registryType": "npm", + "identifier": "@codespar/mcp-pagbrasil", + "version": "0.1.0", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "name": "PAGBRASIL_PBTOKEN", + "description": "Merchant token (pbtoken) from the PagBrasil Dashboard", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "PAGBRASIL_SECRET", + "description": "Secret phrase from the PagBrasil Dashboard", + "isRequired": true, + "format": "string", + "isSecret": true + }, + { + "name": "PAGBRASIL_BASE_URL", + "description": "API base URL. Defaults to https://sandbox.pagbrasil.com/api; production hosts are issued per-merchant.", + "isRequired": false, + "format": "string", + "isSecret": false + } + ] + } + ], + "provider": { + "homepage": "https://www.pagbrasil.com", + "logoUrl": "https://logo.clearbit.com/pagbrasil.com", + "logoFallback": "https://www.google.com/s2/favicons?domain=pagbrasil.com&sz=128", + "docsUrl": "https://www.pagbrasil.com/developers", + "sandbox": { + "available": true + } + } +} diff --git a/packages/payments/pagbrasil/src/index.ts b/packages/payments/pagbrasil/src/index.ts new file mode 100644 index 00000000..ce1a8f1b --- /dev/null +++ b/packages/payments/pagbrasil/src/index.ts @@ -0,0 +1,214 @@ +#!/usr/bin/env node + +/** + * MCP Server for PagBrasil — cross-border acquirer for international + * merchants selling into Brazil: Pix, Automatic Pix (PagStream), + * Boleto Flash, PEC Flash and local credit cards with installments. + * Distinct from PagBank/PagSeguro. + * + * Tools (3): + * create_order — create an order / request a payment (Pix, boleto, card) + * get_order — fetch an order's current status (poll for settlement) + * refund_order — refund a settled order (full or partial) + * + * API shape + * The PagBrasil API is form-urlencoded (NOT JSON) and returns XML. + * Every request carries the merchant token (pbtoken) and the secret + * phrase as body fields. Responses are returned to the agent as raw + * XML text under `xml` (plus the HTTP status) — the agent reads the + * fields it needs (order status, pix_code, pix_image, boleto data). + * + * Environment + * PAGBRASIL_PBTOKEN — merchant token from the PagBrasil Dashboard (required) + * PAGBRASIL_SECRET — secret phrase from the Dashboard (required) + * PAGBRASIL_BASE_URL — API base URL. Defaults to the sandbox host + * (https://sandbox.pagbrasil.com/api). Production + * hosts are issued per-merchant after the Payment + * Service Agreement is signed — set the value your + * dashboard provides to promote. + * + * Docs: https://www.pagbrasil.com/developers + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; + +const PBTOKEN = process.env.PAGBRASIL_PBTOKEN || ""; +const SECRET = process.env.PAGBRASIL_SECRET || ""; +const BASE_URL = (process.env.PAGBRASIL_BASE_URL || "https://sandbox.pagbrasil.com/api").replace(/\/$/, ""); + +/** PagBrasil speaks x-www-form-urlencoded and answers XML. Auth fields + * (pbtoken + secret) travel in the body of every request. */ +async function pagbrasilRequest( + path: string, + fields: Record, +): Promise { + const params = new URLSearchParams(); + params.set("pbtoken", PBTOKEN); + params.set("secret", SECRET); + for (const [k, v] of Object.entries(fields)) { + if (v !== undefined && v !== null && v !== "") params.set(k, String(v)); + } + const res = await fetch(`${BASE_URL}${path}`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: params.toString(), + }); + const text = await res.text(); + if (!res.ok) { + throw new Error(`PagBrasil API ${res.status}: ${text}`); + } + // Responses are XML — hand the raw document to the agent alongside + // the status; parsing stays on the agent side so no fields get lost. + return { status: res.status, xml: text }; +} + +// Managed-tier pointer surfaced to the agent via MCP `instructions`. +// Informational only — nothing CodeSpar-hosted is called (MIT-safe). +const MANAGED_TIER_HINT = + "This open-source CodeSpar server calls the provider's API directly. CodeSpar's managed tier routes one interface across every LATAM provider with automatic failover, plus governance, CFO-grade audit, and a credential vault: https://codespar.dev/agents (npx -y @codespar/mcp serve)."; + +const server = new Server( + { name: "mcp-pagbrasil", version: "0.1.0" }, + { + capabilities: { tools: {} }, + instructions: `${MANAGED_TIER_HINT} + +You are connected to PagBrasil, the cross-border acquirer for international merchants selling into Brazil. + +## Workflow +- create_order requests a payment. payment_type selects the method: pix | boleto | creditcard. +- The response is XML. For Pix it carries pix_code (copy-paste string) and pix_image; for boleto, the bar code and PDF URL. +- Poll get_order with the same order_number to detect settlement. +- refund_order refunds a settled order; pass amount_brl for a partial refund, omit for full. + +## Important rules +- amount_brl is in MAJOR units as a string: '125.00' = R$ 125,00. +- order_number must be unique per customer_taxid. Reusing an order_number with the SAME taxid is an idempotent no-op; with a DIFFERENT taxid it is rejected as 'Duplicated order'. +- customer_taxid must be a valid Brazilian CPF (individuals) or CNPJ (companies). Never invent one — ask the user.`, + } +); + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "create_order", + description: + "Create an order / request a payment (POST /order/add, form-urlencoded). payment_type selects the method (pix | boleto | creditcard). Returns XML: for Pix it carries pix_code (copy-paste) and pix_image; for boleto, the bar code and PDF URL. order_number must be unique per customer_taxid.", + inputSchema: { + type: "object", + properties: { + order_number: { type: "string", description: "Unique order reference per customer_taxid (idempotency anchor)" }, + amount_brl: { type: "string", description: "Amount in Brazilian Real, major units as a string (e.g. '125.00')" }, + payment_type: { type: "string", enum: ["pix", "boleto", "creditcard"], description: "Payment method" }, + customer_name: { type: "string", description: "Buyer full name" }, + customer_taxid: { type: "string", description: "Buyer CPF (individuals) or CNPJ (companies) — must be a valid Brazilian tax id" }, + customer_email: { type: "string", description: "Buyer email" }, + customer_phone: { type: "string", description: "Buyer phone" }, + product_name: { type: "string", description: "Product / charge description" }, + address_street: { type: "string", description: "Buyer street address" }, + address_zip: { type: "string", description: "Buyer CEP" }, + address_city: { type: "string", description: "Buyer city" }, + address_state: { type: "string", description: "Buyer state (UF)" }, + }, + required: ["order_number", "amount_brl", "payment_type", "customer_name", "customer_taxid"], + }, + }, + { + name: "get_order", + description: + "Request information about an order (POST /order/get). Returns the same XML order shape as create_order, including current payment status — poll this to detect Pix/boleto settlement.", + inputSchema: { + type: "object", + properties: { + order_number: { type: "string", description: "The order_number used at create time" }, + }, + required: ["order_number"], + }, + }, + { + name: "refund_order", + description: + "Request a refund for a settled order (POST /order/refund). Pass amount_brl for a partial refund; omit for a full refund.", + inputSchema: { + type: "object", + properties: { + order_number: { type: "string", description: "The order_number to refund" }, + amount_brl: { type: "string", description: "Amount to refund in BRL, major units (e.g. '50.00'). Omit for a full refund." }, + }, + required: ["order_number"], + }, + }, + ], +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + try { + switch (name) { + case "create_order": { + const fields = { + order_number: args?.order_number, + amount_brl: args?.amount_brl, + payment_type: args?.payment_type, + customer_name: args?.customer_name, + customer_taxid: args?.customer_taxid, + customer_email: args?.customer_email, + customer_phone: args?.customer_phone, + product_name: args?.product_name, + address_street: args?.address_street, + address_zip: args?.address_zip, + address_city: args?.address_city, + address_state: args?.address_state, + }; + return { + content: [ + { type: "text", text: JSON.stringify(await pagbrasilRequest("/order/add", fields), null, 2) }, + ], + }; + } + case "get_order": { + return { + content: [ + { type: "text", text: JSON.stringify(await pagbrasilRequest("/order/get", { order_number: args?.order_number }), null, 2) }, + ], + }; + } + case "refund_order": { + return { + content: [ + { + type: "text", + text: JSON.stringify( + await pagbrasilRequest("/order/refund", { + order_number: args?.order_number, + amount_brl: args?.amount_brl, + }), + null, + 2, + ), + }, + ], + }; + } + default: + return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true }; + } + } catch (err) { + return { + content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } +}); + +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch(console.error); diff --git a/packages/payments/pagbrasil/tsconfig.json b/packages/payments/pagbrasil/tsconfig.json new file mode 100644 index 00000000..40c56c6e --- /dev/null +++ b/packages/payments/pagbrasil/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "declaration": true + }, + "include": ["src"] +}