@guiie/buda-mcp 1.5.3 → 1.5.4
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/.github/workflows/publish.yml +17 -7
- package/CHANGELOG.md +26 -0
- package/PUBLISH_CHECKLIST.md +5 -5
- package/dist/http.js +9 -4
- package/dist/tools/arbitrage.d.ts.map +1 -1
- package/dist/tools/arbitrage.js +11 -0
- package/dist/tools/lightning.d.ts.map +1 -1
- package/dist/tools/lightning.js +7 -1
- package/dist/tools/withdrawals.d.ts.map +1 -1
- package/dist/tools/withdrawals.js +5 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +10 -5
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/http.ts +9 -4
- package/src/tools/arbitrage.ts +12 -0
- package/src/tools/lightning.ts +8 -2
- package/src/tools/withdrawals.ts +5 -1
- package/src/utils.ts +10 -4
|
@@ -10,8 +10,8 @@ jobs:
|
|
|
10
10
|
name: Build & test
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
steps:
|
|
13
|
-
- uses: actions/checkout@v4
|
|
14
|
-
- uses: actions/setup-node@v4
|
|
13
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
14
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
15
15
|
with:
|
|
16
16
|
node-version: "20"
|
|
17
17
|
cache: "npm"
|
|
@@ -28,8 +28,8 @@ jobs:
|
|
|
28
28
|
contents: read
|
|
29
29
|
id-token: write
|
|
30
30
|
steps:
|
|
31
|
-
- uses: actions/checkout@v4
|
|
32
|
-
- uses: actions/setup-node@v4
|
|
31
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
32
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
33
33
|
with:
|
|
34
34
|
node-version: "20"
|
|
35
35
|
registry-url: "https://registry.npmjs.org"
|
|
@@ -53,15 +53,25 @@ jobs:
|
|
|
53
53
|
name: Publish to MCP Registry
|
|
54
54
|
needs: npm
|
|
55
55
|
runs-on: ubuntu-latest
|
|
56
|
+
permissions:
|
|
57
|
+
contents: read
|
|
56
58
|
env:
|
|
57
59
|
MCP_REGISTRY_TOKEN: ${{ secrets.MCP_REGISTRY_TOKEN }}
|
|
58
60
|
steps:
|
|
59
|
-
- uses: actions/checkout@v4
|
|
61
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
60
62
|
- name: Install mcp-publisher
|
|
61
63
|
run: |
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
RELEASE_TAG=$(gh api repos/modelcontextprotocol/registry/releases/latest --jq '.tag_name')
|
|
65
|
+
VER="${RELEASE_TAG#v}"
|
|
66
|
+
curl -fsSL -o mcp-publisher.tar.gz \
|
|
67
|
+
"https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz"
|
|
68
|
+
curl -fsSL -o checksums.txt \
|
|
69
|
+
"https://github.com/modelcontextprotocol/registry/releases/download/${RELEASE_TAG}/registry_${VER}_checksums.txt"
|
|
70
|
+
grep "mcp-publisher_linux_amd64.tar.gz" checksums.txt | sha256sum --check
|
|
71
|
+
tar xzf mcp-publisher.tar.gz mcp-publisher
|
|
64
72
|
sudo mv mcp-publisher /usr/local/bin/
|
|
73
|
+
env:
|
|
74
|
+
GH_TOKEN: ${{ github.token }}
|
|
65
75
|
- name: Authenticate and publish to MCP Registry
|
|
66
76
|
run: |
|
|
67
77
|
mcp-publisher login token "$MCP_REGISTRY_TOKEN"
|
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,32 @@ This project uses [Semantic Versioning](https://semver.org/).
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## [1.5.4] – 2026-04-11
|
|
15
|
+
|
|
16
|
+
### Security
|
|
17
|
+
|
|
18
|
+
- **CI/CD supply-chain hardening** — `publish.yml` now verifies the SHA256 checksum of the `mcp-publisher` binary against the official `registry_*_checksums.txt` file before extraction. The download uses `curl -fsSL` (strict) and aborts if the checksum does not match. Previously the binary was piped directly from the network into `tar` without any integrity check.
|
|
19
|
+
|
|
20
|
+
- **GitHub Actions pinned to immutable commit SHAs** — all three `actions/checkout` and `actions/setup-node` usages in `publish.yml` are now pinned to their exact commit SHA (`11bd71901...` / `49933ea5...`) with the human-readable tag in a comment. Tag-based references (`@v4`) are mutable and could be silently redirected.
|
|
21
|
+
|
|
22
|
+
- **`DELETE /mcp` protected by rate limiter and auth middleware** — the endpoint was previously unprotected and returned 405 to anyone without any throttling. It now passes through the same `mcpRateLimiter` and `mcpAuthMiddleware` as the `POST`/`GET` `/mcp` handlers.
|
|
23
|
+
|
|
24
|
+
- **Version removed from unauthenticated `/health` response** — the `version` field was removed from the public health endpoint to prevent fingerprinting of the exact server version. `status`, `server`, and `auth_mode` are still returned.
|
|
25
|
+
|
|
26
|
+
- **`/.well-known/mcp/server-card.json` gated by auth when credentials are configured** — when `MCP_AUTH_TOKEN` is set, the server-card endpoint now requires the same Bearer token as `/mcp`, preventing unauthenticated enumeration of all tool schemas including authenticated ones.
|
|
27
|
+
|
|
28
|
+
- **`validateCurrency` added to `get_arbitrage_opportunities`** — the `base_currency` input was the only tool parameter that bypassed the shared currency validator. It now runs `validateCurrency()` before any business logic. The Zod schema in `register()` was also tightened with `.min(2).max(10).regex(/^[A-Z0-9]+$/i)`.
|
|
29
|
+
|
|
30
|
+
- **`network` field in `create_withdrawal` validated by regex** — the blockchain network identifier for crypto withdrawals is now validated against `/^[a-z][a-z0-9-]{1,29}$/` in the Zod schema, rejecting unexpected values before they reach the Buda API.
|
|
31
|
+
|
|
32
|
+
- **Audit log for `lightning_withdrawal` now includes amount** — `args_summary` was previously empty (`{}`), making the audit trail useless for this operation. The confirmed withdrawal amount (`amount_btc`) is now included so anomaly detection and post-incident review have meaningful context. The invoice string is still never logged.
|
|
33
|
+
|
|
34
|
+
- **`safeTokenEqual` now eliminates token-length timing oracle** — both strings are written into equal-length zero-padded `Buffer.alloc(maxLen)` before `timingSafeEqual`, so execution time no longer varies with the difference in string lengths. A final `aByteLen === bByteLen` guard prevents a padded match from returning `true`.
|
|
35
|
+
|
|
36
|
+
- **CORS policy documented explicitly** — an inline comment clarifies that CORS is intentionally not configured because `buda-mcp` is a server-to-server MCP transport, not a browser client target. `helmet()` already sets the relevant browser security headers.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
14
40
|
## [1.5.3] – 2026-04-11
|
|
15
41
|
|
|
16
42
|
### Security
|
package/PUBLISH_CHECKLIST.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Publish Checklist — buda-mcp v1.5.
|
|
1
|
+
# Publish Checklist — buda-mcp v1.5.4
|
|
2
2
|
|
|
3
|
-
Steps to publish `v1.5.
|
|
3
|
+
Steps to publish `v1.5.4` to npm, the MCP registry, and notify community directories.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Steps to publish `v1.5.3` to npm, the MCP registry, and notify community directo
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
# Confirm version
|
|
11
|
-
node -e "console.log(require('./package.json').version)" # should print 1.5.
|
|
11
|
+
node -e "console.log(require('./package.json').version)" # should print 1.5.4
|
|
12
12
|
|
|
13
13
|
# Build and test
|
|
14
14
|
npm run build
|
|
@@ -37,9 +37,9 @@ Verify: https://www.npmjs.com/package/@guiie/buda-mcp
|
|
|
37
37
|
|
|
38
38
|
## 3. GitHub release
|
|
39
39
|
|
|
40
|
-
Tag and release already created via `gh release create v1.5.
|
|
40
|
+
Tag and release already created via `gh release create v1.5.4`. Verify at:
|
|
41
41
|
|
|
42
|
-
https://github.com/gtorreal/buda-mcp/releases/tag/v1.5.
|
|
42
|
+
https://github.com/gtorreal/buda-mcp/releases/tag/v1.5.4
|
|
43
43
|
|
|
44
44
|
---
|
|
45
45
|
|
package/dist/http.js
CHANGED
|
@@ -197,6 +197,9 @@ function createServer() {
|
|
|
197
197
|
}
|
|
198
198
|
const app = express();
|
|
199
199
|
app.use(helmet());
|
|
200
|
+
// CORS: intentionally not configured. This server is designed for server-to-server MCP
|
|
201
|
+
// communication only (AI agents, Claude Desktop, etc.) — not for browser clients.
|
|
202
|
+
// Helmet already sets X-Content-Type-Options, X-Frame-Options, and related headers.
|
|
200
203
|
// trust proxy: 1 = trust exactly one hop (Railway's reverse proxy).
|
|
201
204
|
// If Cloudflare or another proxy is added in front, increment this value.
|
|
202
205
|
// Affects: req.ip and express-rate-limit client IP detection.
|
|
@@ -246,18 +249,20 @@ function mcpAuthMiddleware(req, res, next) {
|
|
|
246
249
|
}
|
|
247
250
|
next();
|
|
248
251
|
}
|
|
249
|
-
// Health check for Railway / uptime monitors
|
|
252
|
+
// Health check for Railway / uptime monitors.
|
|
253
|
+
// version is intentionally omitted to avoid fingerprinting by unauthenticated callers.
|
|
250
254
|
app.get("/health", staticRateLimiter, (_req, res) => {
|
|
251
255
|
res.json({
|
|
252
256
|
status: "ok",
|
|
253
257
|
server: "buda-mcp",
|
|
254
|
-
version: VERSION,
|
|
255
258
|
auth_mode: authEnabled ? "authenticated" : "public",
|
|
256
259
|
});
|
|
257
260
|
});
|
|
258
261
|
// Smithery static server card — assembled programmatically from tool definitions.
|
|
259
262
|
// Adding a new tool only requires exporting its toolSchema; this handler needs no changes.
|
|
260
|
-
|
|
263
|
+
// When auth is enabled, the server card is gated behind the same bearer token as /mcp
|
|
264
|
+
// to avoid leaking the full tool schema to unauthenticated callers.
|
|
265
|
+
app.get("/.well-known/mcp/server-card.json", staticRateLimiter, mcpAuthMiddleware, (_req, res) => {
|
|
261
266
|
res.json({
|
|
262
267
|
serverInfo: { name: "buda-mcp", version: VERSION },
|
|
263
268
|
authentication: { required: authEnabled },
|
|
@@ -294,7 +299,7 @@ app.get("/mcp", mcpRateLimiter, mcpAuthMiddleware, async (req, res) => {
|
|
|
294
299
|
await server.connect(transport);
|
|
295
300
|
await transport.handleRequest(req, res);
|
|
296
301
|
});
|
|
297
|
-
app.delete("/mcp", async (_req, res) => {
|
|
302
|
+
app.delete("/mcp", mcpRateLimiter, mcpAuthMiddleware, async (_req, res) => {
|
|
298
303
|
res.status(405).json({ error: "Sessions not supported (stateless server)" });
|
|
299
304
|
});
|
|
300
305
|
app.listen(PORT, () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"arbitrage.d.ts","sourceRoot":"","sources":["../../src/tools/arbitrage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,WAAW,EAAa,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"arbitrage.d.ts","sourceRoot":"","sources":["../../src/tools/arbitrage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,WAAW,EAAa,MAAM,aAAa,CAAC;AAIrD,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;CAyBtB,CAAC;AAYF,UAAU,cAAc;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wBAAsB,4BAA4B,CAChD,EAAE,aAAa,EAAE,aAAmB,EAAE,EAAE,cAAc,EACtD,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAwIhF;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAsBxF"}
|
package/dist/tools/arbitrage.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { BudaApiError } from "../client.js";
|
|
3
3
|
import { CACHE_TTL } from "../cache.js";
|
|
4
|
+
import { validateCurrency } from "../validation.js";
|
|
4
5
|
export const toolSchema = {
|
|
5
6
|
name: "get_arbitrage_opportunities",
|
|
6
7
|
description: "Detects cross-country price discrepancies for a given asset across Buda's CLP, COP, and PEN markets, " +
|
|
@@ -26,6 +27,13 @@ export const toolSchema = {
|
|
|
26
27
|
},
|
|
27
28
|
};
|
|
28
29
|
export async function handleArbitrageOpportunities({ base_currency, threshold_pct = 0.5 }, client, cache) {
|
|
30
|
+
const currencyError = validateCurrency(base_currency);
|
|
31
|
+
if (currencyError) {
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: JSON.stringify({ error: currencyError, code: "INVALID_CURRENCY" }) }],
|
|
34
|
+
isError: true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
29
37
|
try {
|
|
30
38
|
const base = base_currency.toUpperCase();
|
|
31
39
|
const data = await cache.getOrFetch("tickers:all", CACHE_TTL.TICKER, () => client.get("/tickers"));
|
|
@@ -130,6 +138,9 @@ export function register(server, client, cache) {
|
|
|
130
138
|
server.tool(toolSchema.name, toolSchema.description, {
|
|
131
139
|
base_currency: z
|
|
132
140
|
.string()
|
|
141
|
+
.min(2)
|
|
142
|
+
.max(10)
|
|
143
|
+
.regex(/^[A-Z0-9]+$/i, "Must be 2–10 alphanumeric characters (e.g. 'BTC', 'ETH').")
|
|
133
144
|
.describe("Base asset to scan (e.g. 'BTC', 'ETH', 'XRP')."),
|
|
134
145
|
threshold_pct: z
|
|
135
146
|
.number()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lightning.d.ts","sourceRoot":"","sources":["../../src/tools/lightning.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AAKxD,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;CAuBzC,CAAC;AAEF,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;;;CAwB5C,CAAC;AAEF,KAAK,uBAAuB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,uBAAuB,EAC7B,MAAM,EAAE,UAAU,EAClB,SAAS,GAAE,MAAM,GAAG,OAAiB,GACpC,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"lightning.d.ts","sourceRoot":"","sources":["../../src/tools/lightning.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AAKxD,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;CAuBzC,CAAC;AAEF,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;;;CAwB5C,CAAC;AAEF,KAAK,uBAAuB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,uBAAuB,EAC7B,MAAM,EAAE,UAAU,EAClB,SAAS,GAAE,MAAM,GAAG,OAAiB,GACpC,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAsFhF;AAED,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,0BAA0B,EAChC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CA8ChF;AAED,wBAAgB,QAAQ,CACtB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,UAAU,EAClB,SAAS,GAAE,MAAM,GAAG,OAAiB,GACpC,IAAI,CA2CN"}
|
package/dist/tools/lightning.js
CHANGED
|
@@ -103,7 +103,13 @@ export async function handleLightningWithdrawal(args, client, transport = "stdio
|
|
|
103
103
|
},
|
|
104
104
|
],
|
|
105
105
|
};
|
|
106
|
-
logAudit({
|
|
106
|
+
logAudit({
|
|
107
|
+
ts: new Date().toISOString(),
|
|
108
|
+
tool: "lightning_withdrawal",
|
|
109
|
+
transport,
|
|
110
|
+
args_summary: { amount_btc: amount.value },
|
|
111
|
+
success: true,
|
|
112
|
+
});
|
|
107
113
|
return result;
|
|
108
114
|
}
|
|
109
115
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withdrawals.d.ts","sourceRoot":"","sources":["../../src/tools/withdrawals.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AAMxD,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;CA8B1C,CAAC;AAEF,KAAK,wBAAwB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,mBAAmB,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAqBF,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,wBAAwB,EAC9B,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CA+ChF;AAED,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuBtC,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,oBAAoB,EAC1B,MAAM,EAAE,UAAU,EAClB,SAAS,GAAE,MAAM,GAAG,OAAiB,GACpC,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAkGhF;AAED,wBAAgB,QAAQ,CACtB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,UAAU,EAClB,SAAS,GAAE,MAAM,GAAG,OAAiB,GACpC,IAAI,
|
|
1
|
+
{"version":3,"file":"withdrawals.d.ts","sourceRoot":"","sources":["../../src/tools/withdrawals.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AAMxD,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;CA8B1C,CAAC;AAEF,KAAK,wBAAwB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,mBAAmB,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAqBF,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,wBAAwB,EAC9B,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CA+ChF;AAED,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuBtC,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,oBAAoB,EAC1B,MAAM,EAAE,UAAU,EAClB,SAAS,GAAE,MAAM,GAAG,OAAiB,GACpC,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAkGhF;AAED,wBAAgB,QAAQ,CACtB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,UAAU,EAClB,SAAS,GAAE,MAAM,GAAG,OAAiB,GACpC,IAAI,CAmCN"}
|
|
@@ -215,7 +215,11 @@ export function register(server, client, transport = "stdio") {
|
|
|
215
215
|
currency: z.string().min(2).max(10).describe("Currency code (e.g. 'BTC', 'CLP')."),
|
|
216
216
|
amount: z.number().positive().describe("Withdrawal amount."),
|
|
217
217
|
address: z.string().optional().describe("Destination crypto address. Mutually exclusive with bank_account_id."),
|
|
218
|
-
network: z
|
|
218
|
+
network: z
|
|
219
|
+
.string()
|
|
220
|
+
.regex(/^[a-z][a-z0-9-]{1,29}$/, "Must be a lowercase alphanumeric network identifier (e.g. 'bitcoin', 'ethereum').")
|
|
221
|
+
.optional()
|
|
222
|
+
.describe("Blockchain network for crypto withdrawals (e.g. 'bitcoin', 'ethereum')."),
|
|
219
223
|
bank_account_id: z.number().int().positive().optional().describe("Fiat bank account ID. Mutually exclusive with address."),
|
|
220
224
|
confirmation_token: z
|
|
221
225
|
.string()
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Amount, OhlcvCandle } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Constant-time string comparison to prevent timing attacks on bearer tokens.
|
|
4
|
+
* Both strings are written into equal-length buffers before comparing so that
|
|
5
|
+
* neither token length nor content can be inferred from execution time.
|
|
4
6
|
*/
|
|
5
7
|
export declare function safeTokenEqual(a: string, b: string): boolean;
|
|
6
8
|
/**
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEtD
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAS5D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GACX,MAAM,CASR;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAIjF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAI/E;AAWD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,EAC3C,MAAM,EAAE,MAAM,GACb,WAAW,EAAE,CAoCf"}
|
package/dist/utils.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { timingSafeEqual } from "crypto";
|
|
2
2
|
/**
|
|
3
3
|
* Constant-time string comparison to prevent timing attacks on bearer tokens.
|
|
4
|
+
* Both strings are written into equal-length buffers before comparing so that
|
|
5
|
+
* neither token length nor content can be inferred from execution time.
|
|
4
6
|
*/
|
|
5
7
|
export function safeTokenEqual(a, b) {
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const aByteLen = Buffer.byteLength(a);
|
|
9
|
+
const bByteLen = Buffer.byteLength(b);
|
|
10
|
+
const maxLen = Math.max(aByteLen, bByteLen);
|
|
11
|
+
const aBuf = Buffer.alloc(maxLen);
|
|
12
|
+
const bBuf = Buffer.alloc(maxLen);
|
|
13
|
+
aBuf.write(a);
|
|
14
|
+
bBuf.write(b);
|
|
15
|
+
return timingSafeEqual(aBuf, bBuf) && aByteLen === bByteLen;
|
|
11
16
|
}
|
|
12
17
|
/**
|
|
13
18
|
* Parses a raw string (from an environment variable) as an integer within [min, max].
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/gtorreal/buda-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.5.
|
|
9
|
+
"version": "1.5.4",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@guiie/buda-mcp",
|
|
14
|
-
"version": "1.5.
|
|
14
|
+
"version": "1.5.4",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|
package/src/http.ts
CHANGED
|
@@ -233,6 +233,9 @@ function createServer(): McpServer {
|
|
|
233
233
|
|
|
234
234
|
const app = express();
|
|
235
235
|
app.use(helmet());
|
|
236
|
+
// CORS: intentionally not configured. This server is designed for server-to-server MCP
|
|
237
|
+
// communication only (AI agents, Claude Desktop, etc.) — not for browser clients.
|
|
238
|
+
// Helmet already sets X-Content-Type-Options, X-Frame-Options, and related headers.
|
|
236
239
|
// trust proxy: 1 = trust exactly one hop (Railway's reverse proxy).
|
|
237
240
|
// If Cloudflare or another proxy is added in front, increment this value.
|
|
238
241
|
// Affects: req.ip and express-rate-limit client IP detection.
|
|
@@ -297,19 +300,21 @@ function mcpAuthMiddleware(
|
|
|
297
300
|
next();
|
|
298
301
|
}
|
|
299
302
|
|
|
300
|
-
// Health check for Railway / uptime monitors
|
|
303
|
+
// Health check for Railway / uptime monitors.
|
|
304
|
+
// version is intentionally omitted to avoid fingerprinting by unauthenticated callers.
|
|
301
305
|
app.get("/health", staticRateLimiter, (_req, res) => {
|
|
302
306
|
res.json({
|
|
303
307
|
status: "ok",
|
|
304
308
|
server: "buda-mcp",
|
|
305
|
-
version: VERSION,
|
|
306
309
|
auth_mode: authEnabled ? "authenticated" : "public",
|
|
307
310
|
});
|
|
308
311
|
});
|
|
309
312
|
|
|
310
313
|
// Smithery static server card — assembled programmatically from tool definitions.
|
|
311
314
|
// Adding a new tool only requires exporting its toolSchema; this handler needs no changes.
|
|
312
|
-
|
|
315
|
+
// When auth is enabled, the server card is gated behind the same bearer token as /mcp
|
|
316
|
+
// to avoid leaking the full tool schema to unauthenticated callers.
|
|
317
|
+
app.get("/.well-known/mcp/server-card.json", staticRateLimiter, mcpAuthMiddleware, (_req, res) => {
|
|
313
318
|
res.json({
|
|
314
319
|
serverInfo: { name: "buda-mcp", version: VERSION },
|
|
315
320
|
authentication: { required: authEnabled },
|
|
@@ -353,7 +358,7 @@ app.get("/mcp", mcpRateLimiter, mcpAuthMiddleware, async (req, res) => {
|
|
|
353
358
|
await transport.handleRequest(req, res);
|
|
354
359
|
});
|
|
355
360
|
|
|
356
|
-
app.delete("/mcp", async (_req, res) => {
|
|
361
|
+
app.delete("/mcp", mcpRateLimiter, mcpAuthMiddleware, async (_req, res) => {
|
|
357
362
|
res.status(405).json({ error: "Sessions not supported (stateless server)" });
|
|
358
363
|
});
|
|
359
364
|
|
package/src/tools/arbitrage.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { BudaClient, BudaApiError } from "../client.js";
|
|
4
4
|
import { MemoryCache, CACHE_TTL } from "../cache.js";
|
|
5
|
+
import { validateCurrency } from "../validation.js";
|
|
5
6
|
import type { AllTickersResponse, Ticker } from "../types.js";
|
|
6
7
|
|
|
7
8
|
export const toolSchema = {
|
|
@@ -51,6 +52,14 @@ export async function handleArbitrageOpportunities(
|
|
|
51
52
|
client: BudaClient,
|
|
52
53
|
cache: MemoryCache,
|
|
53
54
|
): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
|
|
55
|
+
const currencyError = validateCurrency(base_currency);
|
|
56
|
+
if (currencyError) {
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text: JSON.stringify({ error: currencyError, code: "INVALID_CURRENCY" }) }],
|
|
59
|
+
isError: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
54
63
|
try {
|
|
55
64
|
const base = base_currency.toUpperCase();
|
|
56
65
|
const data = await cache.getOrFetch<AllTickersResponse>(
|
|
@@ -187,6 +196,9 @@ export function register(server: McpServer, client: BudaClient, cache: MemoryCac
|
|
|
187
196
|
{
|
|
188
197
|
base_currency: z
|
|
189
198
|
.string()
|
|
199
|
+
.min(2)
|
|
200
|
+
.max(10)
|
|
201
|
+
.regex(/^[A-Z0-9]+$/i, "Must be 2–10 alphanumeric characters (e.g. 'BTC', 'ETH').")
|
|
190
202
|
.describe("Base asset to scan (e.g. 'BTC', 'ETH', 'XRP')."),
|
|
191
203
|
threshold_pct: z
|
|
192
204
|
.number()
|
package/src/tools/lightning.ts
CHANGED
|
@@ -140,7 +140,13 @@ export async function handleLightningWithdrawal(
|
|
|
140
140
|
},
|
|
141
141
|
],
|
|
142
142
|
};
|
|
143
|
-
logAudit({
|
|
143
|
+
logAudit({
|
|
144
|
+
ts: new Date().toISOString(),
|
|
145
|
+
tool: "lightning_withdrawal",
|
|
146
|
+
transport,
|
|
147
|
+
args_summary: { amount_btc: amount.value },
|
|
148
|
+
success: true,
|
|
149
|
+
});
|
|
144
150
|
return result;
|
|
145
151
|
} catch (err) {
|
|
146
152
|
const msg =
|
|
@@ -148,7 +154,7 @@ export async function handleLightningWithdrawal(
|
|
|
148
154
|
? { error: err.message, code: err.status }
|
|
149
155
|
: { error: String(err), code: "UNKNOWN" };
|
|
150
156
|
const result = { content: [{ type: "text" as const, text: JSON.stringify(msg) }], isError: true as const };
|
|
151
|
-
logAudit({ ts: new Date().toISOString(), tool: "lightning_withdrawal", transport, args_summary: {}, success: false, error_code: msg.code });
|
|
157
|
+
logAudit({ ts: new Date().toISOString(), tool: "lightning_withdrawal", transport, args_summary: {}, success: false, error_code: msg.code as string | number });
|
|
152
158
|
return result;
|
|
153
159
|
}
|
|
154
160
|
}
|
package/src/tools/withdrawals.ts
CHANGED
|
@@ -281,7 +281,11 @@ export function register(
|
|
|
281
281
|
currency: z.string().min(2).max(10).describe("Currency code (e.g. 'BTC', 'CLP')."),
|
|
282
282
|
amount: z.number().positive().describe("Withdrawal amount."),
|
|
283
283
|
address: z.string().optional().describe("Destination crypto address. Mutually exclusive with bank_account_id."),
|
|
284
|
-
network: z
|
|
284
|
+
network: z
|
|
285
|
+
.string()
|
|
286
|
+
.regex(/^[a-z][a-z0-9-]{1,29}$/, "Must be a lowercase alphanumeric network identifier (e.g. 'bitcoin', 'ethereum').")
|
|
287
|
+
.optional()
|
|
288
|
+
.describe("Blockchain network for crypto withdrawals (e.g. 'bitcoin', 'ethereum')."),
|
|
285
289
|
bank_account_id: z.number().int().positive().optional().describe("Fiat bank account ID. Mutually exclusive with address."),
|
|
286
290
|
confirmation_token: z
|
|
287
291
|
.string()
|
package/src/utils.ts
CHANGED
|
@@ -3,12 +3,18 @@ import type { Amount, OhlcvCandle } from "./types.js";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Constant-time string comparison to prevent timing attacks on bearer tokens.
|
|
6
|
+
* Both strings are written into equal-length buffers before comparing so that
|
|
7
|
+
* neither token length nor content can be inferred from execution time.
|
|
6
8
|
*/
|
|
7
9
|
export function safeTokenEqual(a: string, b: string): boolean {
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const aByteLen = Buffer.byteLength(a);
|
|
11
|
+
const bByteLen = Buffer.byteLength(b);
|
|
12
|
+
const maxLen = Math.max(aByteLen, bByteLen);
|
|
13
|
+
const aBuf = Buffer.alloc(maxLen);
|
|
14
|
+
const bBuf = Buffer.alloc(maxLen);
|
|
15
|
+
aBuf.write(a);
|
|
16
|
+
bBuf.write(b);
|
|
17
|
+
return timingSafeEqual(aBuf, bBuf) && aByteLen === bByteLen;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
/**
|