@codespar/mcp-dock 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -0
- package/dist/index.js +376 -0
- package/package.json +36 -0
- package/server.json +44 -0
- package/src/index.ts +382 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# @codespar/mcp-dock
|
|
2
|
+
|
|
3
|
+
> MCP server for **Dock** — Brazilian Banking-as-a-Service (accounts, Pix, card issuing) for fintechs and embedded-finance products
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@codespar/mcp-dock)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Why Dock
|
|
9
|
+
|
|
10
|
+
Dock is one of the two dominant BaaS providers in Brazil. Together with Matera, they power most BR fintechs.
|
|
11
|
+
|
|
12
|
+
**Matera vs Dock — pick one, same commerce patterns:**
|
|
13
|
+
|
|
14
|
+
| | Matera | Dock |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Core strength | Pix-focused core banking | Broader BaaS: accounts + Pix + **card issuing** |
|
|
17
|
+
| Historical root | Core-banking platform | Card-issuing platform that expanded into accounts + Pix |
|
|
18
|
+
| Typical customer | Fintech building on top of Pix | Fintech / retailer issuing branded cards + accounts |
|
|
19
|
+
| Pix Automático | Live (2025 BCB product) | Roadmap |
|
|
20
|
+
|
|
21
|
+
BR fintechs usually pick one based on contract terms and pricing. **Agents building fintech products can target both — the commerce patterns are identical; only the vendor relationship changes.** This server exposes Dock's surface in the same shape as `@codespar/mcp-matera` so an agent can swap backends without rewriting tool-call logic.
|
|
22
|
+
|
|
23
|
+
Use Dock when an agent needs to:
|
|
24
|
+
- Spin up **digital accounts** for end users (CPF holders)
|
|
25
|
+
- Move money via Pix (charges, transfers, DICT, refunds)
|
|
26
|
+
- **Issue cards** (debit / credit / prepaid / virtual) against those accounts — Dock's key differentiator
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
### Claude Desktop
|
|
31
|
+
|
|
32
|
+
Add to `~/.config/claude/claude_desktop_config.json`:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"mcpServers": {
|
|
37
|
+
"dock": {
|
|
38
|
+
"command": "npx",
|
|
39
|
+
"args": ["-y", "@codespar/mcp-dock"],
|
|
40
|
+
"env": {
|
|
41
|
+
"DOCK_CLIENT_ID": "your-client-id",
|
|
42
|
+
"DOCK_CLIENT_SECRET": "your-client-secret",
|
|
43
|
+
"DOCK_ENV": "sandbox"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Claude Code
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
claude mcp add dock -- npx @codespar/mcp-dock
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Cursor / VS Code
|
|
57
|
+
|
|
58
|
+
Add to `.cursor/mcp.json` or `.vscode/mcp.json`:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"servers": {
|
|
63
|
+
"dock": {
|
|
64
|
+
"command": "npx",
|
|
65
|
+
"args": ["-y", "@codespar/mcp-dock"],
|
|
66
|
+
"env": {
|
|
67
|
+
"DOCK_CLIENT_ID": "your-client-id",
|
|
68
|
+
"DOCK_CLIENT_SECRET": "your-client-secret",
|
|
69
|
+
"DOCK_ENV": "sandbox"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Tools
|
|
77
|
+
|
|
78
|
+
| Tool | Description |
|
|
79
|
+
|------|-------------|
|
|
80
|
+
| `create_account` | Open a digital account for an end user |
|
|
81
|
+
| `get_account` | Retrieve account balance, status, coordinates |
|
|
82
|
+
| `send_pix` | Outbound Pix transfer from a Dock account |
|
|
83
|
+
| `get_pix` | Retrieve Pix payment by endToEndId |
|
|
84
|
+
| `create_pix_qr_static` | Reusable Pix QR (points-of-sale, donations) |
|
|
85
|
+
| `create_pix_qr_dynamic` | Single-use expiring Pix QR (e-commerce checkouts) |
|
|
86
|
+
| `refund_pix` | Refund (devolução) a Pix payment |
|
|
87
|
+
| `resolve_dict_key` | DICT lookup — resolve Pix key to account holder |
|
|
88
|
+
| `issue_card` | Issue debit / credit / prepaid / virtual card — Dock's differentiator |
|
|
89
|
+
| `get_card` | Retrieve card status, masked PAN, limits |
|
|
90
|
+
|
|
91
|
+
## Authentication
|
|
92
|
+
|
|
93
|
+
OAuth 2.0 Client Credentials. The server calls `POST /oauth/token` with Basic auth (`client_id:client_secret`) and caches the bearer token in memory until a minute before expiry.
|
|
94
|
+
|
|
95
|
+
## Environment Variables
|
|
96
|
+
|
|
97
|
+
| Variable | Required | Description |
|
|
98
|
+
|----------|----------|-------------|
|
|
99
|
+
| `DOCK_CLIENT_ID` | Yes | OAuth2 client_id issued by Dock |
|
|
100
|
+
| `DOCK_CLIENT_SECRET` | Yes | OAuth2 client_secret (secret) |
|
|
101
|
+
| `DOCK_ENV` | No | `sandbox` (default) or `production` |
|
|
102
|
+
|
|
103
|
+
Base URL is derived from `DOCK_ENV`: `https://sandbox.api.dock.tech` (sandbox) or `https://api.dock.tech` (production).
|
|
104
|
+
|
|
105
|
+
## Status
|
|
106
|
+
|
|
107
|
+
**v0.1.0-alpha.1 — tool surface is stable, wire paths are NOT yet verified.**
|
|
108
|
+
|
|
109
|
+
Dock's developer portal at [developers.dock.tech](https://developers.dock.tech) redirects to a ReadMe.com login gate. Public docs are not accessible without a Dock merchant contract, so on 2026-04-21 we could not validate the exact URL paths.
|
|
110
|
+
|
|
111
|
+
The endpoint paths in `src/index.ts` follow **standard BR BaaS conventions** (matching Matera's shape and the BCB Pix spec) and are best-guesses. Known-suspect items are flagged inline with `TODO(verify)` comments:
|
|
112
|
+
|
|
113
|
+
| Area | Value in code | Why it's suspect |
|
|
114
|
+
|------|---------------|------------------|
|
|
115
|
+
| Token endpoint | `POST /oauth/token` | Standard candidate, but `/oauth2/token` or `/auth/v1/token` also common |
|
|
116
|
+
| Pix refund | `POST /pix/payments/{e2eid}/refund` | BCB spec names refunds `/pix/{e2eid}/devolucao/{id}` |
|
|
117
|
+
| DICT lookup | `GET /pix/dict/{key}` | BCB DICT is RSFN-gated; Dock's wrapper path unverified |
|
|
118
|
+
|
|
119
|
+
The 10 tool **names** and **input schemas** above are the public contract and will remain stable. Only the internal HTTP calls in `src/index.ts` will change when we verify against the sandbox. Using this server against a live Dock tenant today may produce 404s on most calls.
|
|
120
|
+
|
|
121
|
+
PR welcome from anyone with a Dock sandbox.
|
|
122
|
+
|
|
123
|
+
## Roadmap
|
|
124
|
+
|
|
125
|
+
### v0.2 (planned)
|
|
126
|
+
- Verified paths against Dock sandbox
|
|
127
|
+
- Card controls: `block_card`, `unblock_card`, `set_card_limits`
|
|
128
|
+
- Account statements and transaction listings
|
|
129
|
+
- Webhook event helpers
|
|
130
|
+
|
|
131
|
+
### v0.3 (planned)
|
|
132
|
+
- Boleto issuance
|
|
133
|
+
- TED / bank transfers (non-Pix rails)
|
|
134
|
+
- Credit underwriting endpoints (Dock's credit stack)
|
|
135
|
+
|
|
136
|
+
Want to contribute? [Open a PR](https://github.com/codespar/mcp-dev-brasil) or [request a tool](https://github.com/codespar/mcp-dev-brasil/issues).
|
|
137
|
+
|
|
138
|
+
## Links
|
|
139
|
+
|
|
140
|
+
- [Dock](https://www.dock.tech)
|
|
141
|
+
- [Dock Developers](https://developers.dock.tech) (gated — requires merchant login)
|
|
142
|
+
- [MCP Dev Brasil](https://github.com/codespar/mcp-dev-brasil)
|
|
143
|
+
- [Landing Page](https://codespar.dev/mcp)
|
|
144
|
+
|
|
145
|
+
## Enterprise
|
|
146
|
+
|
|
147
|
+
Need governance, budget limits, and audit trails for agent-initiated bank transfers and card issuance? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds policy engine, payment routing, and compliance templates on top of these MCP servers.
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP Server for Dock — Brazilian Banking-as-a-Service (BaaS).
|
|
4
|
+
*
|
|
5
|
+
* Dock is Matera's main competitor in BR BaaS. Together they power most
|
|
6
|
+
* Brazilian fintechs. Dock's surface is broader than Matera's: accounts,
|
|
7
|
+
* Pix, and card issuing (debit / credit / prepaid) are all first-class.
|
|
8
|
+
* Card issuing is the key differentiator vs Matera — Dock is historically
|
|
9
|
+
* a card-issuing platform that expanded into full BaaS.
|
|
10
|
+
*
|
|
11
|
+
* Tools (10) — Banking + Pix + Card Issuing for v0.1:
|
|
12
|
+
* create_account — POST /accounts (digital account for an end user)
|
|
13
|
+
* get_account — GET /accounts/{id}
|
|
14
|
+
* send_pix — POST /pix/payments (outbound Pix transfer)
|
|
15
|
+
* get_pix — GET /pix/payments/{endToEndId}
|
|
16
|
+
* create_pix_qr_static — POST /pix/qrcodes/static
|
|
17
|
+
* create_pix_qr_dynamic — POST /pix/qrcodes/dynamic
|
|
18
|
+
* refund_pix — POST /pix/payments/{endToEndId}/refund
|
|
19
|
+
* resolve_dict_key — GET /pix/dict/{key}
|
|
20
|
+
* issue_card — POST /cards (Dock's core differentiator)
|
|
21
|
+
* get_card — GET /cards/{id}
|
|
22
|
+
*
|
|
23
|
+
* -------------------------------------------------------------------------
|
|
24
|
+
* ALPHA STATUS — endpoint paths below are NOT verified against a live sandbox.
|
|
25
|
+
*
|
|
26
|
+
* Dock's developer portal (https://developers.dock.tech) redirects to a
|
|
27
|
+
* ReadMe.com login gate. Public docs are not accessible without a Dock
|
|
28
|
+
* merchant contract. The paths below follow standard BR BaaS conventions
|
|
29
|
+
* (matching Matera's shape and BCB's Pix spec) and are best-guesses.
|
|
30
|
+
*
|
|
31
|
+
* The 10 tool names + input schemas are the stable public contract. Only
|
|
32
|
+
* the internal `dockRequest(method, path, body)` calls will change when
|
|
33
|
+
* the sandbox verifies the exact paths.
|
|
34
|
+
* -------------------------------------------------------------------------
|
|
35
|
+
*
|
|
36
|
+
* Authentication
|
|
37
|
+
* OAuth 2.0 Client Credentials. POST /oauth/token with Basic auth
|
|
38
|
+
* (client_id:client_secret) + grant_type=client_credentials. Bearer
|
|
39
|
+
* token cached in memory until a minute before expiry.
|
|
40
|
+
*
|
|
41
|
+
* Environment
|
|
42
|
+
* DOCK_CLIENT_ID OAuth2 client_id
|
|
43
|
+
* DOCK_CLIENT_SECRET OAuth2 client_secret
|
|
44
|
+
* DOCK_ENV "sandbox" | "production" (default: sandbox)
|
|
45
|
+
*
|
|
46
|
+
* Docs: https://developers.dock.tech (gated — requires merchant login)
|
|
47
|
+
*/
|
|
48
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
49
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
50
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
51
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
52
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
53
|
+
const CLIENT_ID = process.env.DOCK_CLIENT_ID || "";
|
|
54
|
+
const CLIENT_SECRET = process.env.DOCK_CLIENT_SECRET || "";
|
|
55
|
+
const DOCK_ENV = (process.env.DOCK_ENV || "sandbox").toLowerCase();
|
|
56
|
+
const BASE_URL = DOCK_ENV === "production"
|
|
57
|
+
? "https://api.dock.tech"
|
|
58
|
+
: "https://sandbox.api.dock.tech";
|
|
59
|
+
let tokenCache = null;
|
|
60
|
+
async function getAccessToken() {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
if (tokenCache && tokenCache.expiresAt > now + 60_000) {
|
|
63
|
+
return tokenCache.accessToken;
|
|
64
|
+
}
|
|
65
|
+
const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64");
|
|
66
|
+
// TODO(verify): Dock's exact token path is unconfirmed behind the doc gate.
|
|
67
|
+
// `/oauth/token` and `/oauth2/token` are the standard candidates.
|
|
68
|
+
const res = await fetch(`${BASE_URL}/oauth/token`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
"Authorization": `Basic ${basic}`,
|
|
72
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
73
|
+
},
|
|
74
|
+
body: "grant_type=client_credentials",
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
throw new Error(`Dock OAuth ${res.status}: ${await res.text()}`);
|
|
78
|
+
}
|
|
79
|
+
const data = (await res.json());
|
|
80
|
+
tokenCache = {
|
|
81
|
+
accessToken: data.access_token,
|
|
82
|
+
expiresAt: now + data.expires_in * 1000,
|
|
83
|
+
};
|
|
84
|
+
return data.access_token;
|
|
85
|
+
}
|
|
86
|
+
async function dockRequest(method, path, body) {
|
|
87
|
+
const token = await getAccessToken();
|
|
88
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
89
|
+
method,
|
|
90
|
+
headers: {
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
"Authorization": `Bearer ${token}`,
|
|
93
|
+
},
|
|
94
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
throw new Error(`Dock API ${res.status}: ${await res.text()}`);
|
|
98
|
+
}
|
|
99
|
+
const text = await res.text();
|
|
100
|
+
return text ? JSON.parse(text) : {};
|
|
101
|
+
}
|
|
102
|
+
const server = new Server({ name: "mcp-dock", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
103
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
104
|
+
tools: [
|
|
105
|
+
{
|
|
106
|
+
name: "create_account",
|
|
107
|
+
description: "Create a digital account for an end user (CPF holder) on Dock. Returns the account id, agency, and account number. Account holds funds that can be moved via Pix or spent via issued cards.",
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
holder: {
|
|
112
|
+
type: "object",
|
|
113
|
+
description: "Account holder identity",
|
|
114
|
+
properties: {
|
|
115
|
+
cpf: { type: "string", description: "11-digit CPF" },
|
|
116
|
+
name: { type: "string", description: "Full name" },
|
|
117
|
+
birth_date: { type: "string", description: "ISO-8601 date" },
|
|
118
|
+
email: { type: "string" },
|
|
119
|
+
phone: { type: "string", description: "E.164 phone number" },
|
|
120
|
+
mother_name: { type: "string", description: "Required by BCB onboarding rules" },
|
|
121
|
+
},
|
|
122
|
+
required: ["cpf", "name"],
|
|
123
|
+
},
|
|
124
|
+
account_type: {
|
|
125
|
+
type: "string",
|
|
126
|
+
enum: ["PAYMENT", "CHECKING", "SAVINGS"],
|
|
127
|
+
description: "Type of account to open",
|
|
128
|
+
},
|
|
129
|
+
external_id: { type: "string", description: "Merchant-side account identifier for reconciliation" },
|
|
130
|
+
},
|
|
131
|
+
required: ["holder"],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "get_account",
|
|
136
|
+
description: "Retrieve a Dock account by id. Returns balance, status, holder info, and account coordinates (agency / account number).",
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: "object",
|
|
139
|
+
properties: {
|
|
140
|
+
account_id: { type: "string", description: "Dock account id returned by create_account" },
|
|
141
|
+
},
|
|
142
|
+
required: ["account_id"],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "send_pix",
|
|
147
|
+
description: "Initiate an outbound Pix transfer from a Dock account to any Pix key in BR. Returns endToEndId once the BCB SPI confirms.",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: {
|
|
151
|
+
debtor_account_id: { type: "string", description: "Source account id on Dock" },
|
|
152
|
+
creditor_pix_key: { type: "string", description: "Destination Pix key (CPF/CNPJ/email/phone/random UUID)" },
|
|
153
|
+
amount: { type: "number", description: "Amount in BRL (decimal, e.g. 10.50)" },
|
|
154
|
+
description: { type: "string", description: "Message shown to recipient (optional)" },
|
|
155
|
+
idempotency_key: { type: "string", description: "Merchant-side unique id to prevent double-send on retry" },
|
|
156
|
+
},
|
|
157
|
+
required: ["debtor_account_id", "creditor_pix_key", "amount"],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "get_pix",
|
|
162
|
+
description: "Retrieve an outbound Pix payment by endToEndId.",
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: "object",
|
|
165
|
+
properties: {
|
|
166
|
+
end_to_end_id: { type: "string", description: "32-char BCB endToEndId" },
|
|
167
|
+
},
|
|
168
|
+
required: ["end_to_end_id"],
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "create_pix_qr_static",
|
|
173
|
+
description: "Create a static Pix QR (reusable, tied to a merchant Pix key). Returns EMV copy-paste payload and QR image. Use for points-of-sale or donations where the same QR is shown to many payers.",
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: {
|
|
177
|
+
pix_key: { type: "string", description: "Merchant Pix key" },
|
|
178
|
+
amount: { type: "number", description: "Amount in BRL. Omit for open-amount QR." },
|
|
179
|
+
description: { type: "string", description: "Free-text description shown to payer" },
|
|
180
|
+
merchant_name: { type: "string" },
|
|
181
|
+
merchant_city: { type: "string" },
|
|
182
|
+
txid: { type: "string", description: "Optional merchant txid (26-35 alphanumerics)" },
|
|
183
|
+
},
|
|
184
|
+
required: ["pix_key"],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "create_pix_qr_dynamic",
|
|
189
|
+
description: "Create a dynamic Pix QR (single-use, expiring). Returns txid, EMV payload, and QR image. Preferred for e-commerce checkouts and invoices.",
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: "object",
|
|
192
|
+
properties: {
|
|
193
|
+
pix_key: { type: "string", description: "Merchant Pix key that receives settlement" },
|
|
194
|
+
amount: { type: "number", description: "Amount in BRL" },
|
|
195
|
+
description: { type: "string", description: "Description shown to payer" },
|
|
196
|
+
expiration: { type: "number", description: "QR lifetime in seconds (e.g. 3600 = 1h)" },
|
|
197
|
+
debtor: {
|
|
198
|
+
type: "object",
|
|
199
|
+
properties: {
|
|
200
|
+
cpf: { type: "string" },
|
|
201
|
+
cnpj: { type: "string" },
|
|
202
|
+
name: { type: "string" },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
txid: { type: "string", description: "Optional merchant txid" },
|
|
206
|
+
},
|
|
207
|
+
required: ["pix_key", "amount", "expiration"],
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: "refund_pix",
|
|
212
|
+
description: "Refund (devolução) a Pix payment. Supports full or partial amount. Use BCB MED reason codes when applicable.",
|
|
213
|
+
inputSchema: {
|
|
214
|
+
type: "object",
|
|
215
|
+
properties: {
|
|
216
|
+
end_to_end_id: { type: "string", description: "endToEndId of the payment to refund" },
|
|
217
|
+
amount: { type: "number", description: "Refund amount in BRL. Must be <= original." },
|
|
218
|
+
reason: { type: "string", description: "Reason (BCB MED code or free text)" },
|
|
219
|
+
},
|
|
220
|
+
required: ["end_to_end_id", "amount", "reason"],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "resolve_dict_key",
|
|
225
|
+
description: "Resolve a Pix DICT key to the account holder's identity and ISPB/branch/account. Use before sending large transfers to verify the counterparty. DICT queries are rate-limited and logged by BCB.",
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: "object",
|
|
228
|
+
properties: {
|
|
229
|
+
key: { type: "string", description: "Pix key to resolve (CPF/CNPJ/email/phone/random)" },
|
|
230
|
+
},
|
|
231
|
+
required: ["key"],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "issue_card",
|
|
236
|
+
description: "Issue a card (debit / credit / prepaid / virtual) against a Dock account. Card issuing is Dock's historical core product and the main differentiator vs Matera. Returns card id, PAN masked, CVV (if virtual), and shipping status.",
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
account_id: { type: "string", description: "Dock account id the card is bound to" },
|
|
241
|
+
card_type: {
|
|
242
|
+
type: "string",
|
|
243
|
+
enum: ["DEBIT", "CREDIT", "PREPAID", "VIRTUAL"],
|
|
244
|
+
description: "Type of card to issue",
|
|
245
|
+
},
|
|
246
|
+
network: {
|
|
247
|
+
type: "string",
|
|
248
|
+
enum: ["VISA", "MASTERCARD", "ELO"],
|
|
249
|
+
description: "Card network (BIN must be pre-provisioned with Dock for the given network)",
|
|
250
|
+
},
|
|
251
|
+
holder_name: { type: "string", description: "Name to emboss on the card (physical) or shown in-app (virtual)" },
|
|
252
|
+
shipping_address: {
|
|
253
|
+
type: "object",
|
|
254
|
+
description: "Required for physical cards",
|
|
255
|
+
properties: {
|
|
256
|
+
street: { type: "string" },
|
|
257
|
+
number: { type: "string" },
|
|
258
|
+
complement: { type: "string" },
|
|
259
|
+
neighborhood: { type: "string" },
|
|
260
|
+
city: { type: "string" },
|
|
261
|
+
state: { type: "string", description: "2-letter UF" },
|
|
262
|
+
zip_code: { type: "string" },
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
external_id: { type: "string", description: "Merchant-side card identifier for reconciliation" },
|
|
266
|
+
},
|
|
267
|
+
required: ["account_id", "card_type", "network", "holder_name"],
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: "get_card",
|
|
272
|
+
description: "Retrieve a card by id. Returns card status (ACTIVE / BLOCKED / CANCELED), masked PAN, expiry, and limits.",
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: "object",
|
|
275
|
+
properties: {
|
|
276
|
+
card_id: { type: "string", description: "Dock card id returned by issue_card" },
|
|
277
|
+
},
|
|
278
|
+
required: ["card_id"],
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
}));
|
|
283
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
284
|
+
const { name, arguments: args } = request.params;
|
|
285
|
+
const a = (args ?? {});
|
|
286
|
+
try {
|
|
287
|
+
switch (name) {
|
|
288
|
+
case "create_account":
|
|
289
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("POST", "/accounts", a), null, 2) }] };
|
|
290
|
+
case "get_account": {
|
|
291
|
+
const id = encodeURIComponent(String(a.account_id ?? ""));
|
|
292
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("GET", `/accounts/${id}`), null, 2) }] };
|
|
293
|
+
}
|
|
294
|
+
// TODO(verify): BCB canonical outbound-Pix path is `/pix/payments`.
|
|
295
|
+
// Dock likely wraps it but the exact wrapper is unverified.
|
|
296
|
+
case "send_pix":
|
|
297
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("POST", "/pix/payments", a), null, 2) }] };
|
|
298
|
+
case "get_pix": {
|
|
299
|
+
const e2e = encodeURIComponent(String(a.end_to_end_id ?? ""));
|
|
300
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("GET", `/pix/payments/${e2e}`), null, 2) }] };
|
|
301
|
+
}
|
|
302
|
+
case "create_pix_qr_static":
|
|
303
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("POST", "/pix/qrcodes/static", a), null, 2) }] };
|
|
304
|
+
case "create_pix_qr_dynamic":
|
|
305
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("POST", "/pix/qrcodes/dynamic", a), null, 2) }] };
|
|
306
|
+
// TODO(verify): BCB spec names refunds `/pix/{e2eid}/devolucao/{id}`.
|
|
307
|
+
// `/pix/payments/{e2eid}/refund` here is a plausible Dock wrapper.
|
|
308
|
+
case "refund_pix": {
|
|
309
|
+
const e2e = encodeURIComponent(String(a.end_to_end_id ?? ""));
|
|
310
|
+
const body = { amount: a.amount, reason: a.reason };
|
|
311
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("POST", `/pix/payments/${e2e}/refund`, body), null, 2) }] };
|
|
312
|
+
}
|
|
313
|
+
// TODO(verify): DICT lookup wrapper path is unverified behind the Dock
|
|
314
|
+
// doc gate; `/pix/dict/{key}` follows the standard BR BaaS convention.
|
|
315
|
+
case "resolve_dict_key": {
|
|
316
|
+
const key = encodeURIComponent(String(a.key ?? ""));
|
|
317
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("GET", `/pix/dict/${key}`), null, 2) }] };
|
|
318
|
+
}
|
|
319
|
+
case "issue_card":
|
|
320
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("POST", "/cards", a), null, 2) }] };
|
|
321
|
+
case "get_card": {
|
|
322
|
+
const id = encodeURIComponent(String(a.card_id ?? ""));
|
|
323
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("GET", `/cards/${id}`), null, 2) }] };
|
|
324
|
+
}
|
|
325
|
+
default:
|
|
326
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
async function main() {
|
|
334
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
335
|
+
const { default: express } = await import("express");
|
|
336
|
+
const { randomUUID } = await import("node:crypto");
|
|
337
|
+
const app = express();
|
|
338
|
+
app.use(express.json());
|
|
339
|
+
const transports = new Map();
|
|
340
|
+
app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
|
|
341
|
+
app.post("/mcp", async (req, res) => {
|
|
342
|
+
const sid = req.headers["mcp-session-id"];
|
|
343
|
+
if (sid && transports.has(sid)) {
|
|
344
|
+
await transports.get(sid).handleRequest(req, res, req.body);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
348
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
349
|
+
t.onclose = () => { if (t.sessionId)
|
|
350
|
+
transports.delete(t.sessionId); };
|
|
351
|
+
const s = new Server({ name: "mcp-dock", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
352
|
+
server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
|
|
353
|
+
server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
|
|
354
|
+
await s.connect(t);
|
|
355
|
+
await t.handleRequest(req, res, req.body);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
359
|
+
});
|
|
360
|
+
app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
361
|
+
await transports.get(sid).handleRequest(req, res);
|
|
362
|
+
else
|
|
363
|
+
res.status(400).send("Invalid session"); });
|
|
364
|
+
app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
365
|
+
await transports.get(sid).handleRequest(req, res);
|
|
366
|
+
else
|
|
367
|
+
res.status(400).send("Invalid session"); });
|
|
368
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
369
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
const transport = new StdioServerTransport();
|
|
373
|
+
await server.connect(transport);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codespar/mcp-dock",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "MCP server for Dock — Brazilian Banking-as-a-Service (accounts, Pix, card issuing) for fintechs and embedded-finance products",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-dock": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/express": "^5.0.6",
|
|
19
|
+
"@types/node": "^25.5.0",
|
|
20
|
+
"typescript": "^5.8.0"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"dock",
|
|
26
|
+
"banking",
|
|
27
|
+
"baas",
|
|
28
|
+
"card-issuing",
|
|
29
|
+
"pix",
|
|
30
|
+
"dict",
|
|
31
|
+
"accounts",
|
|
32
|
+
"brazil",
|
|
33
|
+
"fintech"
|
|
34
|
+
],
|
|
35
|
+
"mcpName": "io.github.codespar/mcp-dock"
|
|
36
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.codespar/mcp-dock",
|
|
4
|
+
"description": "MCP server for Dock — Brazilian Banking-as-a-Service (accounts, Pix, card issuing) for fintechs and embedded-finance products",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/codespar/mcp-dev-brasil",
|
|
7
|
+
"source": "github",
|
|
8
|
+
"subfolder": "packages/banking/dock"
|
|
9
|
+
},
|
|
10
|
+
"version": "0.1.0",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"identifier": "@codespar/mcp-dock",
|
|
15
|
+
"version": "0.1.0",
|
|
16
|
+
"transport": {
|
|
17
|
+
"type": "stdio"
|
|
18
|
+
},
|
|
19
|
+
"environmentVariables": [
|
|
20
|
+
{
|
|
21
|
+
"name": "DOCK_CLIENT_ID",
|
|
22
|
+
"description": "OAuth2 client_id issued by Dock",
|
|
23
|
+
"isRequired": true,
|
|
24
|
+
"format": "string",
|
|
25
|
+
"isSecret": false
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "DOCK_CLIENT_SECRET",
|
|
29
|
+
"description": "OAuth2 client_secret issued by Dock",
|
|
30
|
+
"isRequired": true,
|
|
31
|
+
"format": "string",
|
|
32
|
+
"isSecret": true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "DOCK_ENV",
|
|
36
|
+
"description": "Environment: sandbox or production. Defaults to sandbox.",
|
|
37
|
+
"isRequired": false,
|
|
38
|
+
"format": "string",
|
|
39
|
+
"isSecret": false
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP Server for Dock — Brazilian Banking-as-a-Service (BaaS).
|
|
4
|
+
*
|
|
5
|
+
* Dock is Matera's main competitor in BR BaaS. Together they power most
|
|
6
|
+
* Brazilian fintechs. Dock's surface is broader than Matera's: accounts,
|
|
7
|
+
* Pix, and card issuing (debit / credit / prepaid) are all first-class.
|
|
8
|
+
* Card issuing is the key differentiator vs Matera — Dock is historically
|
|
9
|
+
* a card-issuing platform that expanded into full BaaS.
|
|
10
|
+
*
|
|
11
|
+
* Tools (10) — Banking + Pix + Card Issuing for v0.1:
|
|
12
|
+
* create_account — POST /accounts (digital account for an end user)
|
|
13
|
+
* get_account — GET /accounts/{id}
|
|
14
|
+
* send_pix — POST /pix/payments (outbound Pix transfer)
|
|
15
|
+
* get_pix — GET /pix/payments/{endToEndId}
|
|
16
|
+
* create_pix_qr_static — POST /pix/qrcodes/static
|
|
17
|
+
* create_pix_qr_dynamic — POST /pix/qrcodes/dynamic
|
|
18
|
+
* refund_pix — POST /pix/payments/{endToEndId}/refund
|
|
19
|
+
* resolve_dict_key — GET /pix/dict/{key}
|
|
20
|
+
* issue_card — POST /cards (Dock's core differentiator)
|
|
21
|
+
* get_card — GET /cards/{id}
|
|
22
|
+
*
|
|
23
|
+
* -------------------------------------------------------------------------
|
|
24
|
+
* ALPHA STATUS — endpoint paths below are NOT verified against a live sandbox.
|
|
25
|
+
*
|
|
26
|
+
* Dock's developer portal (https://developers.dock.tech) redirects to a
|
|
27
|
+
* ReadMe.com login gate. Public docs are not accessible without a Dock
|
|
28
|
+
* merchant contract. The paths below follow standard BR BaaS conventions
|
|
29
|
+
* (matching Matera's shape and BCB's Pix spec) and are best-guesses.
|
|
30
|
+
*
|
|
31
|
+
* The 10 tool names + input schemas are the stable public contract. Only
|
|
32
|
+
* the internal `dockRequest(method, path, body)` calls will change when
|
|
33
|
+
* the sandbox verifies the exact paths.
|
|
34
|
+
* -------------------------------------------------------------------------
|
|
35
|
+
*
|
|
36
|
+
* Authentication
|
|
37
|
+
* OAuth 2.0 Client Credentials. POST /oauth/token with Basic auth
|
|
38
|
+
* (client_id:client_secret) + grant_type=client_credentials. Bearer
|
|
39
|
+
* token cached in memory until a minute before expiry.
|
|
40
|
+
*
|
|
41
|
+
* Environment
|
|
42
|
+
* DOCK_CLIENT_ID OAuth2 client_id
|
|
43
|
+
* DOCK_CLIENT_SECRET OAuth2 client_secret
|
|
44
|
+
* DOCK_ENV "sandbox" | "production" (default: sandbox)
|
|
45
|
+
*
|
|
46
|
+
* Docs: https://developers.dock.tech (gated — requires merchant login)
|
|
47
|
+
*/
|
|
48
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
49
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
50
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
51
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
52
|
+
import {
|
|
53
|
+
CallToolRequestSchema,
|
|
54
|
+
ListToolsRequestSchema,
|
|
55
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
56
|
+
|
|
57
|
+
const CLIENT_ID = process.env.DOCK_CLIENT_ID || "";
|
|
58
|
+
const CLIENT_SECRET = process.env.DOCK_CLIENT_SECRET || "";
|
|
59
|
+
const DOCK_ENV = (process.env.DOCK_ENV || "sandbox").toLowerCase();
|
|
60
|
+
const BASE_URL =
|
|
61
|
+
DOCK_ENV === "production"
|
|
62
|
+
? "https://api.dock.tech"
|
|
63
|
+
: "https://sandbox.api.dock.tech";
|
|
64
|
+
|
|
65
|
+
let tokenCache: { accessToken: string; expiresAt: number } | null = null;
|
|
66
|
+
|
|
67
|
+
async function getAccessToken(): Promise<string> {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
if (tokenCache && tokenCache.expiresAt > now + 60_000) {
|
|
70
|
+
return tokenCache.accessToken;
|
|
71
|
+
}
|
|
72
|
+
const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64");
|
|
73
|
+
// TODO(verify): Dock's exact token path is unconfirmed behind the doc gate.
|
|
74
|
+
// `/oauth/token` and `/oauth2/token` are the standard candidates.
|
|
75
|
+
const res = await fetch(`${BASE_URL}/oauth/token`, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: {
|
|
78
|
+
"Authorization": `Basic ${basic}`,
|
|
79
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
80
|
+
},
|
|
81
|
+
body: "grant_type=client_credentials",
|
|
82
|
+
});
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
throw new Error(`Dock OAuth ${res.status}: ${await res.text()}`);
|
|
85
|
+
}
|
|
86
|
+
const data = (await res.json()) as { access_token: string; expires_in: number };
|
|
87
|
+
tokenCache = {
|
|
88
|
+
accessToken: data.access_token,
|
|
89
|
+
expiresAt: now + data.expires_in * 1000,
|
|
90
|
+
};
|
|
91
|
+
return data.access_token;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function dockRequest(method: string, path: string, body?: unknown): Promise<unknown> {
|
|
95
|
+
const token = await getAccessToken();
|
|
96
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
97
|
+
method,
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
"Authorization": `Bearer ${token}`,
|
|
101
|
+
},
|
|
102
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
103
|
+
});
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
throw new Error(`Dock API ${res.status}: ${await res.text()}`);
|
|
106
|
+
}
|
|
107
|
+
const text = await res.text();
|
|
108
|
+
return text ? JSON.parse(text) : {};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const server = new Server(
|
|
112
|
+
{ name: "mcp-dock", version: "0.1.0" },
|
|
113
|
+
{ capabilities: { tools: {} } },
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
117
|
+
tools: [
|
|
118
|
+
{
|
|
119
|
+
name: "create_account",
|
|
120
|
+
description: "Create a digital account for an end user (CPF holder) on Dock. Returns the account id, agency, and account number. Account holds funds that can be moved via Pix or spent via issued cards.",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: "object",
|
|
123
|
+
properties: {
|
|
124
|
+
holder: {
|
|
125
|
+
type: "object",
|
|
126
|
+
description: "Account holder identity",
|
|
127
|
+
properties: {
|
|
128
|
+
cpf: { type: "string", description: "11-digit CPF" },
|
|
129
|
+
name: { type: "string", description: "Full name" },
|
|
130
|
+
birth_date: { type: "string", description: "ISO-8601 date" },
|
|
131
|
+
email: { type: "string" },
|
|
132
|
+
phone: { type: "string", description: "E.164 phone number" },
|
|
133
|
+
mother_name: { type: "string", description: "Required by BCB onboarding rules" },
|
|
134
|
+
},
|
|
135
|
+
required: ["cpf", "name"],
|
|
136
|
+
},
|
|
137
|
+
account_type: {
|
|
138
|
+
type: "string",
|
|
139
|
+
enum: ["PAYMENT", "CHECKING", "SAVINGS"],
|
|
140
|
+
description: "Type of account to open",
|
|
141
|
+
},
|
|
142
|
+
external_id: { type: "string", description: "Merchant-side account identifier for reconciliation" },
|
|
143
|
+
},
|
|
144
|
+
required: ["holder"],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "get_account",
|
|
149
|
+
description: "Retrieve a Dock account by id. Returns balance, status, holder info, and account coordinates (agency / account number).",
|
|
150
|
+
inputSchema: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
account_id: { type: "string", description: "Dock account id returned by create_account" },
|
|
154
|
+
},
|
|
155
|
+
required: ["account_id"],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "send_pix",
|
|
160
|
+
description: "Initiate an outbound Pix transfer from a Dock account to any Pix key in BR. Returns endToEndId once the BCB SPI confirms.",
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: {
|
|
164
|
+
debtor_account_id: { type: "string", description: "Source account id on Dock" },
|
|
165
|
+
creditor_pix_key: { type: "string", description: "Destination Pix key (CPF/CNPJ/email/phone/random UUID)" },
|
|
166
|
+
amount: { type: "number", description: "Amount in BRL (decimal, e.g. 10.50)" },
|
|
167
|
+
description: { type: "string", description: "Message shown to recipient (optional)" },
|
|
168
|
+
idempotency_key: { type: "string", description: "Merchant-side unique id to prevent double-send on retry" },
|
|
169
|
+
},
|
|
170
|
+
required: ["debtor_account_id", "creditor_pix_key", "amount"],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "get_pix",
|
|
175
|
+
description: "Retrieve an outbound Pix payment by endToEndId.",
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: "object",
|
|
178
|
+
properties: {
|
|
179
|
+
end_to_end_id: { type: "string", description: "32-char BCB endToEndId" },
|
|
180
|
+
},
|
|
181
|
+
required: ["end_to_end_id"],
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "create_pix_qr_static",
|
|
186
|
+
description: "Create a static Pix QR (reusable, tied to a merchant Pix key). Returns EMV copy-paste payload and QR image. Use for points-of-sale or donations where the same QR is shown to many payers.",
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: "object",
|
|
189
|
+
properties: {
|
|
190
|
+
pix_key: { type: "string", description: "Merchant Pix key" },
|
|
191
|
+
amount: { type: "number", description: "Amount in BRL. Omit for open-amount QR." },
|
|
192
|
+
description: { type: "string", description: "Free-text description shown to payer" },
|
|
193
|
+
merchant_name: { type: "string" },
|
|
194
|
+
merchant_city: { type: "string" },
|
|
195
|
+
txid: { type: "string", description: "Optional merchant txid (26-35 alphanumerics)" },
|
|
196
|
+
},
|
|
197
|
+
required: ["pix_key"],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "create_pix_qr_dynamic",
|
|
202
|
+
description: "Create a dynamic Pix QR (single-use, expiring). Returns txid, EMV payload, and QR image. Preferred for e-commerce checkouts and invoices.",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
pix_key: { type: "string", description: "Merchant Pix key that receives settlement" },
|
|
207
|
+
amount: { type: "number", description: "Amount in BRL" },
|
|
208
|
+
description: { type: "string", description: "Description shown to payer" },
|
|
209
|
+
expiration: { type: "number", description: "QR lifetime in seconds (e.g. 3600 = 1h)" },
|
|
210
|
+
debtor: {
|
|
211
|
+
type: "object",
|
|
212
|
+
properties: {
|
|
213
|
+
cpf: { type: "string" },
|
|
214
|
+
cnpj: { type: "string" },
|
|
215
|
+
name: { type: "string" },
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
txid: { type: "string", description: "Optional merchant txid" },
|
|
219
|
+
},
|
|
220
|
+
required: ["pix_key", "amount", "expiration"],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "refund_pix",
|
|
225
|
+
description: "Refund (devolução) a Pix payment. Supports full or partial amount. Use BCB MED reason codes when applicable.",
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: "object",
|
|
228
|
+
properties: {
|
|
229
|
+
end_to_end_id: { type: "string", description: "endToEndId of the payment to refund" },
|
|
230
|
+
amount: { type: "number", description: "Refund amount in BRL. Must be <= original." },
|
|
231
|
+
reason: { type: "string", description: "Reason (BCB MED code or free text)" },
|
|
232
|
+
},
|
|
233
|
+
required: ["end_to_end_id", "amount", "reason"],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "resolve_dict_key",
|
|
238
|
+
description: "Resolve a Pix DICT key to the account holder's identity and ISPB/branch/account. Use before sending large transfers to verify the counterparty. DICT queries are rate-limited and logged by BCB.",
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: "object",
|
|
241
|
+
properties: {
|
|
242
|
+
key: { type: "string", description: "Pix key to resolve (CPF/CNPJ/email/phone/random)" },
|
|
243
|
+
},
|
|
244
|
+
required: ["key"],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "issue_card",
|
|
249
|
+
description: "Issue a card (debit / credit / prepaid / virtual) against a Dock account. Card issuing is Dock's historical core product and the main differentiator vs Matera. Returns card id, PAN masked, CVV (if virtual), and shipping status.",
|
|
250
|
+
inputSchema: {
|
|
251
|
+
type: "object",
|
|
252
|
+
properties: {
|
|
253
|
+
account_id: { type: "string", description: "Dock account id the card is bound to" },
|
|
254
|
+
card_type: {
|
|
255
|
+
type: "string",
|
|
256
|
+
enum: ["DEBIT", "CREDIT", "PREPAID", "VIRTUAL"],
|
|
257
|
+
description: "Type of card to issue",
|
|
258
|
+
},
|
|
259
|
+
network: {
|
|
260
|
+
type: "string",
|
|
261
|
+
enum: ["VISA", "MASTERCARD", "ELO"],
|
|
262
|
+
description: "Card network (BIN must be pre-provisioned with Dock for the given network)",
|
|
263
|
+
},
|
|
264
|
+
holder_name: { type: "string", description: "Name to emboss on the card (physical) or shown in-app (virtual)" },
|
|
265
|
+
shipping_address: {
|
|
266
|
+
type: "object",
|
|
267
|
+
description: "Required for physical cards",
|
|
268
|
+
properties: {
|
|
269
|
+
street: { type: "string" },
|
|
270
|
+
number: { type: "string" },
|
|
271
|
+
complement: { type: "string" },
|
|
272
|
+
neighborhood: { type: "string" },
|
|
273
|
+
city: { type: "string" },
|
|
274
|
+
state: { type: "string", description: "2-letter UF" },
|
|
275
|
+
zip_code: { type: "string" },
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
external_id: { type: "string", description: "Merchant-side card identifier for reconciliation" },
|
|
279
|
+
},
|
|
280
|
+
required: ["account_id", "card_type", "network", "holder_name"],
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: "get_card",
|
|
285
|
+
description: "Retrieve a card by id. Returns card status (ACTIVE / BLOCKED / CANCELED), masked PAN, expiry, and limits.",
|
|
286
|
+
inputSchema: {
|
|
287
|
+
type: "object",
|
|
288
|
+
properties: {
|
|
289
|
+
card_id: { type: "string", description: "Dock card id returned by issue_card" },
|
|
290
|
+
},
|
|
291
|
+
required: ["card_id"],
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
}));
|
|
296
|
+
|
|
297
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
298
|
+
const { name, arguments: args } = request.params;
|
|
299
|
+
const a = (args ?? {}) as Record<string, unknown>;
|
|
300
|
+
try {
|
|
301
|
+
switch (name) {
|
|
302
|
+
case "create_account":
|
|
303
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("POST", "/accounts", a), null, 2) }] };
|
|
304
|
+
case "get_account": {
|
|
305
|
+
const id = encodeURIComponent(String(a.account_id ?? ""));
|
|
306
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("GET", `/accounts/${id}`), null, 2) }] };
|
|
307
|
+
}
|
|
308
|
+
// TODO(verify): BCB canonical outbound-Pix path is `/pix/payments`.
|
|
309
|
+
// Dock likely wraps it but the exact wrapper is unverified.
|
|
310
|
+
case "send_pix":
|
|
311
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("POST", "/pix/payments", a), null, 2) }] };
|
|
312
|
+
case "get_pix": {
|
|
313
|
+
const e2e = encodeURIComponent(String(a.end_to_end_id ?? ""));
|
|
314
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("GET", `/pix/payments/${e2e}`), null, 2) }] };
|
|
315
|
+
}
|
|
316
|
+
case "create_pix_qr_static":
|
|
317
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("POST", "/pix/qrcodes/static", a), null, 2) }] };
|
|
318
|
+
case "create_pix_qr_dynamic":
|
|
319
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("POST", "/pix/qrcodes/dynamic", a), null, 2) }] };
|
|
320
|
+
// TODO(verify): BCB spec names refunds `/pix/{e2eid}/devolucao/{id}`.
|
|
321
|
+
// `/pix/payments/{e2eid}/refund` here is a plausible Dock wrapper.
|
|
322
|
+
case "refund_pix": {
|
|
323
|
+
const e2e = encodeURIComponent(String(a.end_to_end_id ?? ""));
|
|
324
|
+
const body: Record<string, unknown> = { amount: a.amount, reason: a.reason };
|
|
325
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("POST", `/pix/payments/${e2e}/refund`, body), null, 2) }] };
|
|
326
|
+
}
|
|
327
|
+
// TODO(verify): DICT lookup wrapper path is unverified behind the Dock
|
|
328
|
+
// doc gate; `/pix/dict/{key}` follows the standard BR BaaS convention.
|
|
329
|
+
case "resolve_dict_key": {
|
|
330
|
+
const key = encodeURIComponent(String(a.key ?? ""));
|
|
331
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("GET", `/pix/dict/${key}`), null, 2) }] };
|
|
332
|
+
}
|
|
333
|
+
case "issue_card":
|
|
334
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("POST", "/cards", a), null, 2) }] };
|
|
335
|
+
case "get_card": {
|
|
336
|
+
const id = encodeURIComponent(String(a.card_id ?? ""));
|
|
337
|
+
return { content: [{ type: "text", text: JSON.stringify(await dockRequest("GET", `/cards/${id}`), null, 2) }] };
|
|
338
|
+
}
|
|
339
|
+
default:
|
|
340
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
async function main() {
|
|
348
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
349
|
+
const { default: express } = await import("express");
|
|
350
|
+
const { randomUUID } = await import("node:crypto");
|
|
351
|
+
const app = express();
|
|
352
|
+
app.use(express.json());
|
|
353
|
+
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
354
|
+
app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
|
|
355
|
+
app.post("/mcp", async (req, res) => {
|
|
356
|
+
const sid = req.headers["mcp-session-id"] as string | undefined;
|
|
357
|
+
if (sid && transports.has(sid)) {
|
|
358
|
+
await transports.get(sid)!.handleRequest(req, res, req.body);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
362
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
363
|
+
t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
|
|
364
|
+
const s = new Server({ name: "mcp-dock", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
365
|
+
(server as any)._requestHandlers.forEach((v: any, k: any) => (s as any)._requestHandlers.set(k, v));
|
|
366
|
+
(server as any)._notificationHandlers?.forEach((v: any, k: any) => (s as any)._notificationHandlers.set(k, v));
|
|
367
|
+
await s.connect(t);
|
|
368
|
+
await t.handleRequest(req, res, req.body);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
372
|
+
});
|
|
373
|
+
app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"] as string | undefined; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
|
|
374
|
+
app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"] as string | undefined; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
|
|
375
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
376
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
377
|
+
} else {
|
|
378
|
+
const transport = new StdioServerTransport();
|
|
379
|
+
await server.connect(transport);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
main().catch(console.error);
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"declaration": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|