@guiie/buda-mcp 1.1.2 → 1.2.0

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/CHANGELOG.md CHANGED
@@ -7,6 +7,41 @@ This project uses [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.2.0] – 2026-04-11
11
+
12
+ ### Added
13
+
14
+ - **`src/validation.ts`** — new `validateMarketId` helper that enforces `/^[A-Z0-9]{2,10}-[A-Z0-9]{2,10}$/i` on all market ID inputs before URL interpolation. Returns a structured `isError: true` response with a helpful message on failure. Applied to all tools that accept `market_id`.
15
+ - **`.env.example`** — documents `BUDA_API_KEY` and `BUDA_API_SECRET` with comments and a link to the Buda token management page. Referenced in the README auth section.
16
+ - **`scripts/sync-version.mjs`** — reads `package.json` version and writes it to `server.json`. Run via `npm run sync-version` to keep the MCP registry manifest in sync after a version bump.
17
+ - **`test/unit.ts`** — 23 unit tests (no live API required):
18
+ - **HMAC signing** (3 tests): exact output verified for GET (no body) and POST (with base64 body); determinism check.
19
+ - **Cache deduplication** (3 tests): concurrent `getOrFetch` calls share the same in-flight promise; expiry triggers a new fetch; rejected fetcher clears the entry so the next call retries.
20
+ - **confirmation_token guard** (4 tests): `place_order` and `cancel_order` return `isError: true` without `"CONFIRM"`.
21
+ - **Input sanitization** (9 tests): malformed market IDs (path traversal, no hyphen, empty, oversized segments, special characters) all return a validation error; valid IDs (uppercase, lowercase, USDC) pass.
22
+ - **429 retry** (4 tests): mock 429→200 asserts fetch is called exactly twice and the 200 data is returned; double-429 asserts `BudaApiError` with `retryAfterMs`; `Retry-After: 2` header parsed as 2000 ms (RFC 7231 seconds); absent header defaults to 1000 ms.
23
+ - **`npm run test:unit`** and **`npm run test:integration`** scripts for running test subsets independently.
24
+
25
+ ### Changed
26
+
27
+ - **Single version source-of-truth** — all version references now derive from `package.json` at startup:
28
+ - `src/version.ts` (new shared module): reads `package.json` via `readFileSync` + `fileURLToPath`.
29
+ - `src/client.ts`, `src/index.ts`, `src/http.ts` import `VERSION` from `src/version.ts`.
30
+ - `server.json` is kept in sync via `npm run sync-version`.
31
+ - **`http.ts` server-card generated programmatically** — the `/.well-known/mcp/server-card.json` endpoint now assembles tool schemas from each tool module's exported `toolSchema` constant instead of a 100-line hardcoded JSON block. Adding a tool only requires exporting its `toolSchema`.
32
+ - **All tool files** export a `toolSchema` constant `{ name, description, inputSchema }` used by both `register()` and the server-card endpoint, ensuring descriptions are always in sync.
33
+ - **`place_order.ts` and `cancel_order.ts`** expose their handler logic via exported `handlePlaceOrder` / `handleCancelOrder` functions, used by the register wrapper and directly testable in unit tests.
34
+ - **`get_price_history`** improvements:
35
+ - Shallow-window limitation moved to the **front** of the description (was buried in a `note` field).
36
+ - UTC bucket boundary format documented explicitly in tool description and `note` field.
37
+ - `limit` max raised from `100` to `1000`; description updated accordingly.
38
+ - **429 retry with Retry-After** — `BudaClient.get`, `.post`, and `.put` now retry once on a 429 response. The `Retry-After` header is parsed as integer seconds (per RFC 7231; Buda's docs describe 429 but do not document the header — the standard interpretation applies). Defaults to 1 second if the header is absent. A double-429 throws `BudaApiError` with `retryAfterMs` set.
39
+ - **`test/run-all.ts`** — integration tests now perform a 3-second connectivity pre-check at startup and skip gracefully (exit 0 with a message) when the Buda API is unreachable, preventing CI failures on networks without internet access.
40
+ - **`package.json`** version bumped to `1.2.0`.
41
+ - **`marketplace/`** and **`PUBLISH_CHECKLIST.md`** updated to reflect v1.2.0 changes.
42
+
43
+ ---
44
+
10
45
  ## [1.1.2] – 2026-04-10
11
46
 
12
47
  ### Fixed
package/PUBLISH.md CHANGED
@@ -1,4 +1,4 @@
1
- # Publishing checklist
1
+ # Publishing checklist — v1.2.0
2
2
 
3
3
  Everything that could be automated is already done.
4
4
  This file contains only what requires your manual action, in order.
@@ -63,7 +63,7 @@ mcp-publisher login github
63
63
  mcp-publisher publish
64
64
  ```
65
65
 
66
- You'll see: `✓ Successfully published — io.github.gtorreal/buda-mcp version 1.0.0`
66
+ You'll see: `✓ Successfully published — io.github.gtorreal/buda-mcp version 1.2.0`
67
67
 
68
68
  ### 2b. Put your MCP Registry token into GitHub Actions (for auto-publish on release)
69
69
 
@@ -177,9 +177,9 @@ For the Gemini Extensions marketplace (currently invite-only):
177
177
  Once npm token and MCP registry token are set in GitHub Actions secrets (Steps 1c and 2b):
178
178
 
179
179
  ```bash
180
- gh release create v1.0.0 \
181
- --title "v1.0.0 — Initial release" \
182
- --notes "5 public market data tools for Buda.com: get_markets, get_ticker, get_orderbook, get_trades, get_market_volume." \
180
+ gh release create v1.2.0 \
181
+ --title "v1.2.0 — Input validation, 429 retry, unit tests" \
182
+ --notes "Input sanitization, 429 Retry-After support, get_price_history limit raised to 1000, 23 unit tests, single version source-of-truth." \
183
183
  --latest
184
184
  ```
185
185
 
@@ -1,6 +1,6 @@
1
- # Publish Checklist — buda-mcp v1.1.0
1
+ # Publish Checklist — buda-mcp v1.2.0
2
2
 
3
- Steps to publish `v1.1.0` to npm, the MCP registry, and notify community directories.
3
+ Steps to publish `v1.2.0` to npm, the MCP registry, and notify community directories.
4
4
 
5
5
  ---
6
6
 
@@ -8,12 +8,15 @@ Steps to publish `v1.1.0` 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.1.0
11
+ node -e "console.log(require('./package.json').version)" # should print 1.2.0
12
12
 
13
13
  # Build and test
14
14
  npm run build
15
15
  npm test
16
16
 
17
+ # Sync server.json version (already done, but run again to confirm)
18
+ npm run sync-version
19
+
17
20
  # Verify no credentials are logged (audit)
18
21
  grep -r "apiKey\|apiSecret\|BUDA_API" dist/ --include="*.js" | grep -v "process.env\|hasAuth\|X-SBTC-APIKEY\|authHeaders\|constructor"
19
22
  # Should return empty or only header name strings — never credential values
@@ -36,17 +39,18 @@ Verify: https://www.npmjs.com/package/@guiie/buda-mcp
36
39
 
37
40
  ```bash
38
41
  git add -A
39
- git commit -m "chore: release v1.1.0
40
-
41
- - 3 new public tools: get_spread, compare_markets, get_price_history
42
- - HMAC-SHA384 auth scaffold (BUDA_API_KEY / BUDA_API_SECRET)
43
- - 4 auth-gated tools: get_balances, get_orders, place_order, cancel_order
44
- - TTL caching (markets 60s, tickers 5s, orderbooks 3s)
45
- - MCP Resources: buda://markets, buda://ticker/{market}
46
- - Structured error responses for all tools
47
- - Updated README, marketplace files, CHANGELOG"
48
-
49
- git tag v1.1.0
42
+ git commit -m "chore: release v1.2.0
43
+
44
+ - Single version source-of-truth via src/version.ts (no more hardcoded strings)
45
+ - Programmatic server-card in http.ts (toolSchema exported per tool)
46
+ - .env.example with documented BUDA_API_KEY / BUDA_API_SECRET
47
+ - Input sanitization: validateMarketId regex on all market_id inputs
48
+ - 429 retry with Retry-After (seconds, per RFC 7231), default 1s
49
+ - get_price_history: limit raised to 1000, UTC bucketing documented
50
+ - 23 unit tests: HMAC signing, cache dedup, confirmation guard, sanitization, 429 retry
51
+ - Integration tests: graceful skip when Buda API is unreachable"
52
+
53
+ git tag v1.2.0
50
54
  git push origin main --tags
51
55
  ```
52
56
 
@@ -57,25 +61,27 @@ Then create a GitHub Release from the tag with the following release notes:
57
61
  **Release notes template (GitHub):**
58
62
 
59
63
  ```
60
- ## buda-mcp v1.1.0
64
+ ## buda-mcp v1.2.0
61
65
 
62
66
  ### What's new
63
67
 
64
- **3 new public tools**
65
- - `get_spread` bid/ask spread (absolute and %) for any market
66
- - `compare_markets` — side-by-side ticker data for a base currency across all quote currencies
67
- - `get_price_history` — OHLCV candles derived from recent trades (1h / 4h / 1d)
68
+ **Bug fixes & maintenance**
69
+ - Single version source-of-truth: all version strings now read from `package.json` at startup via `src/version.ts` no more drift between files
70
+ - `http.ts` server-card assembled programmatically from exported `toolSchema` constants — adding a tool no longer requires touching `http.ts`
71
+
72
+ **Security / reliability**
73
+ - Input sanitization: all `market_id` inputs validated against `/^[A-Z0-9]{2,10}-[A-Z0-9]{2,10}$/i` before URL interpolation — rejects path traversal and malformed IDs with structured errors
74
+ - 429 retry: `BudaClient` retries once on rate-limit responses, honoring the `Retry-After` header (seconds, per RFC 7231; defaults to 1s if absent). Double-429 throws `BudaApiError` with `retryAfterMs`.
68
75
 
69
- **HMAC auth scaffold**
70
- - Set `BUDA_API_KEY` + `BUDA_API_SECRET` to unlock 4 authenticated tools
71
- - `get_balances`, `get_orders`, `place_order`, `cancel_order`
72
- - Public-only mode unchanged when no credentials are set
76
+ **DX improvements**
77
+ - `.env.example` added for easy credential setup
78
+ - `get_price_history` limit raised from 100 to 1000 trades; UTC bucketing documented prominently
79
+ - `npm run sync-version` syncs `server.json` from `package.json` automatically
73
80
 
74
- **Platform improvements**
75
- - TTL caching: markets (60s), tickers (5s), order books (3s)
76
- - MCP Resources: `buda://markets` and `buda://ticker/{market}`
77
- - Structured `isError: true` responses for all tools
78
- - Updated README with npx quickstart and per-tool examples
81
+ **Test suite**
82
+ - 23 new unit tests (no live API needed): HMAC signing exactness, cache deduplication, confirmation_token guards, input sanitization, 429 retry behavior
83
+ - Integration tests skip gracefully when Buda API is unreachable (CI-friendly)
84
+ - New scripts: `npm run test:unit`, `npm run test:integration`
79
85
 
80
86
  ```bash
81
87
  npx @guiie/buda-mcp
@@ -86,16 +92,13 @@ npx @guiie/buda-mcp
86
92
 
87
93
  ## 4. MCP Registry update
88
94
 
89
- The GitHub Actions workflow (`.github/workflows/publish.yml`) runs automatically on GitHub release. It runs `mcp publish` via `mcp-publisher`. Verify the registry entry at:
95
+ The GitHub Actions workflow (`.github/workflows/publish.yml`) runs automatically on GitHub release. Verify at:
90
96
 
91
97
  https://registry.modelcontextprotocol.io/servers/io.github.gtorreal/buda-mcp
92
98
 
93
99
  If the workflow doesn't trigger, run manually:
94
100
 
95
101
  ```bash
96
- # Download mcp-publisher from GitHub releases (check for latest version)
97
- curl -L https://github.com/modelcontextprotocol/mcp-publisher/releases/latest/download/mcp-publisher-macos -o mcp-publisher
98
- chmod +x mcp-publisher
99
102
  MCP_REGISTRY_TOKEN=<token> ./mcp-publisher publish
100
103
  ```
101
104
 
@@ -111,23 +114,22 @@ Verify: https://smithery.ai/server/@guiie/buda-mcp
111
114
 
112
115
  ## 6. Notify mcp.so
113
116
 
114
- **Method:** Submit via the mcp.so listing update form or open a PR to their repository.
115
-
116
117
  **Email/message template:**
117
118
 
118
119
  ```
119
- Subject: [Update] buda-mcp v1.1.0 — new tools + auth
120
+ Subject: [Update] buda-mcp v1.2.0 — input sanitization, 429 retry, 23 unit tests
120
121
 
121
122
  Hi mcp.so team,
122
123
 
123
- I've released v1.1.0 of buda-mcp (@guiie/buda-mcp on npm).
124
+ I've released v1.2.0 of buda-mcp (@guiie/buda-mcp on npm).
124
125
 
125
126
  Key changes:
126
- - 3 new public tools: get_spread, compare_markets, get_price_history (OHLCV)
127
- - Optional HMAC auth scaffold (BUDA_API_KEY / BUDA_API_SECRET) unlocks 4 private tools: get_balances, get_orders, place_order, cancel_order
128
- - TTL caching for all repeated data fetches
129
- - MCP Resources: buda://markets and buda://ticker/{market}
130
- - Structured error responses
127
+ - Input sanitization: all market IDs validated against a strict regex before URL use
128
+ - 429 rate-limit retry: honors Retry-After header (seconds, RFC 7231), defaults to 1s
129
+ - get_price_history: limit raised to 1000 trades for deeper history
130
+ - 23 unit tests added (no live API required): HMAC, cache dedup, confirmation guards, sanitization, retry
131
+ - Single version source-of-truth (package.json → all files via src/version.ts)
132
+ - .env.example added for easy credential setup
131
133
 
132
134
  Links:
133
135
  - npm: https://www.npmjs.com/package/@guiie/buda-mcp
@@ -143,31 +145,25 @@ Thank you!
143
145
 
144
146
  ## 7. Notify Glama.ai
145
147
 
146
- **Method:** Use Glama's submission form at https://glama.ai/mcp/servers or open an issue/PR on their directory repository.
147
-
148
148
  **Message template:**
149
149
 
150
150
  ```
151
- Subject: [Update] buda-mcp v1.1.0
151
+ Subject: [Update] buda-mcp v1.2.0
152
152
 
153
153
  Hi Glama team,
154
154
 
155
- buda-mcp has been updated to v1.1.0. Here's a summary of what's new:
155
+ buda-mcp has been updated to v1.2.0.
156
156
 
157
157
  Package: @guiie/buda-mcp (npm)
158
158
  Registry: io.github.gtorreal/buda-mcp (MCP Registry)
159
- Version: 1.1.0
160
-
161
- New tools added:
162
- - get_spread: bid/ask spread for any market
163
- - compare_markets: cross-currency price comparison for a base asset
164
- - get_price_history: OHLCV candles from trade history (1h/4h/1d)
165
- - get_balances, get_orders, place_order, cancel_order (authenticated, local-only)
159
+ Version: 1.2.0
166
160
 
167
- New capabilities:
168
- - MCP Resources protocol: buda://markets, buda://ticker/{market}
169
- - TTL caching (60s/5s/3s by data type)
170
- - Structured error responses (isError: true)
161
+ Changes:
162
+ - Input validation on all market_id inputs (structured isError: true on failure)
163
+ - 429 retry with Retry-After support (RFC 7231 seconds; default 1s)
164
+ - get_price_history limit raised to 1000 trades; UTC bucket timestamps documented
165
+ - 23 unit tests: HMAC signing, cache deduplication, confirmation guards, sanitization, retry
166
+ - Single version source (package.json); .env.example added
171
167
 
172
168
  Quick start:
173
169
  npx @guiie/buda-mcp
@@ -182,11 +178,12 @@ Thank you!
182
178
 
183
179
  ## 8. Post-publish verification
184
180
 
185
- - [ ] `npx @guiie/buda-mcp@1.1.0` starts successfully
186
- - [ ] `npm info @guiie/buda-mcp version` returns `1.1.0`
187
- - [ ] GitHub release tag `v1.1.0` is visible
188
- - [ ] MCP Registry entry reflects v1.1.0
189
- - [ ] Smithery server card lists 8 public tools
181
+ - [ ] `npx @guiie/buda-mcp@1.2.0` starts successfully
182
+ - [ ] `npm info @guiie/buda-mcp version` returns `1.2.0`
183
+ - [ ] GitHub release tag `v1.2.0` is visible
184
+ - [ ] MCP Registry entry reflects v1.2.0
185
+ - [ ] Smithery server card lists 8 public tools (with updated get_price_history description)
186
+ - [ ] `GET /health` returns `"version":"1.2.0"` on Railway deployment
187
+ - [ ] `GET /.well-known/mcp/server-card.json` returns tools with updated schemas (no hardcoded JSON)
190
188
  - [ ] mcp.so listing updated
191
189
  - [ ] Glama.ai listing updated
192
- - [ ] Railway deployment health check returns `"version":"1.1.0"` at `/health`
package/README.md CHANGED
@@ -161,13 +161,13 @@ Side-by-side ticker data for all pairs of a given base currency (CLP, COP, PEN,
161
161
  ---
162
162
 
163
163
  #### `get_price_history`
164
- OHLCV candles derived from recent trade history (no native candlestick endpoint exists on Buda — candles are aggregated client-side from up to 100 raw trades).
164
+ OHLCV candles derived from recent trade history (Buda has no native candlestick endpoint — candles are aggregated client-side from raw trades). Candle timestamps are UTC bucket boundaries. Increasing `limit` gives deeper history at the cost of a slower response.
165
165
 
166
166
  | Parameter | Type | Required | Description |
167
167
  |-----------|------|----------|-------------|
168
168
  | `market_id` | string | Yes | Market ID. |
169
169
  | `period` | `1h` \| `4h` \| `1d` | No | Candle period (default `1h`). |
170
- | `limit` | number | No | Raw trades to fetch before aggregation (default 100, max 100). |
170
+ | `limit` | number | No | Raw trades to fetch before aggregation (default 100, max 1000). |
171
171
 
172
172
  **Example prompts:**
173
173
  - *"Show me hourly price candles for BTC-CLP"*
@@ -252,7 +252,7 @@ In addition to tools, the server exposes two MCP Resources that clients can read
252
252
 
253
253
  The server defaults to **public-only mode** — no API key needed, no breaking changes for existing users.
254
254
 
255
- To enable authenticated tools, set environment variables before running:
255
+ To enable authenticated tools, copy `.env.example` to `.env` and fill in your credentials, then set them as environment variables before running:
256
256
 
257
257
  ```bash
258
258
  BUDA_API_KEY=your_api_key BUDA_API_SECRET=your_api_secret npx @guiie/buda-mcp
@@ -1,4 +1,4 @@
1
- # Marketplace Submission Assets
1
+ # Marketplace Submission Assets — v1.2.0
2
2
 
3
3
  Ready-to-use assets for submitting buda-mcp to every major AI marketplace.
4
4
  Replace `gtorreal` / `gtorreal` with your actual handles before submitting.
@@ -50,8 +50,8 @@ Side-by-side ticker data for all trading pairs of a given base currency across a
50
50
  **Parameters:** `base_currency` *(required)* — e.g. `BTC`, `ETH`, `XRP`.
51
51
 
52
52
  ### `get_price_history`
53
- OHLCV (open/high/low/close/volume) candles derived from recent trade history. Supports `1h`, `4h`, and `1d` periods.
54
- **Parameters:** `market_id` *(required)*, `period` *(optional: `1h`/`4h`/`1d`, default `1h`)*, `limit` *(optional, max 100 trades)*.
53
+ OHLCV (open/high/low/close/volume) candles derived from recent trade history (Buda has no native candlestick endpoint). Supports `1h`, `4h`, and `1d` periods. Candle timestamps are UTC bucket boundaries.
54
+ **Parameters:** `market_id` *(required)*, `period` *(optional: `1h`/`4h`/`1d`, default `1h`)*, `limit` *(optional, default 100, max 1000 trades — more = deeper history)*.
55
55
 
56
56
  ### Authenticated tools (require `BUDA_API_KEY` + `BUDA_API_SECRET`)
57
57
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "Gemini function declarations for buda-mcp v1.1.0. Pass this array as the `tools[0].functionDeclarations` field when calling the Gemini API. See: https://ai.google.dev/gemini-api/docs/function-calling",
2
+ "_comment": "Gemini function declarations for buda-mcp v1.2.0. Pass this array as the `tools[0].functionDeclarations` field when calling the Gemini API. See: https://ai.google.dev/gemini-api/docs/function-calling",
3
3
  "functionDeclarations": [
4
4
  {
5
5
  "name": "get_markets",
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "get_price_history",
116
- "description": "Get OHLCV (open/high/low/close/volume) price history for a Buda.com market, derived from recent trade history. Candles are aggregated client-side from up to 100 raw trades. Supports 1h, 4h, and 1d candle periods.",
116
+ "description": "IMPORTANT: Candles are aggregated client-side from raw trades (Buda has no native candlestick endpoint) — increase 'limit' for deeper history. Returns OHLCV (open/high/low/close/volume) price history for a Buda.com market. Candle timestamps are UTC bucket boundaries. Supports 1h, 4h, and 1d candle periods.",
117
117
  "parameters": {
118
118
  "type": "OBJECT",
119
119
  "properties": {
@@ -127,7 +127,7 @@
127
127
  },
128
128
  "limit": {
129
129
  "type": "INTEGER",
130
- "description": "Number of raw trades to fetch before aggregation. Default is 100, maximum is 100."
130
+ "description": "Number of raw trades to fetch before aggregation. Default is 100, maximum is 1000. More trades = deeper history."
131
131
  }
132
132
  },
133
133
  "required": ["market_id"]
@@ -11,7 +11,7 @@ info:
11
11
  stdio server. Deploy locally with mcp-proxy:
12
12
  mcp-proxy --port 8000 -- npx -y @guiie/buda-mcp
13
13
  Or point `servers[0].url` at your hosted instance.
14
- version: 1.1.0
14
+ version: 1.2.0
15
15
  contact:
16
16
  url: https://github.com/gtorreal/buda-mcp
17
17
 
@@ -231,9 +231,10 @@ paths:
231
231
  operationId: getPriceHistory
232
232
  summary: OHLCV price history derived from trade history
233
233
  description: |
234
- Returns OHLCV (open/high/low/close/volume) candles derived from recent trade history.
235
- Buda.com has no native candlestick endpoint; candles are aggregated client-side from
236
- up to 100 raw trades. Supports 1h, 4h, and 1d candle periods.
234
+ IMPORTANT: Candles are aggregated client-side from raw trades (Buda has no native
235
+ candlestick endpoint) increase 'limit' for deeper history but expect slower responses.
236
+ Returns OHLCV (open/high/low/close/volume) candles. Candle timestamps are UTC bucket
237
+ boundaries. Supports 1h, 4h, and 1d candle periods.
237
238
  parameters:
238
239
  - name: market_id
239
240
  in: query
@@ -253,11 +254,11 @@ paths:
253
254
  - name: limit
254
255
  in: query
255
256
  required: false
256
- description: Number of raw trades to fetch before aggregation (default 100, max 100).
257
+ description: Number of raw trades to fetch before aggregation (default 100, max 1000). More trades = deeper history.
257
258
  schema:
258
259
  type: integer
259
260
  minimum: 1
260
- maximum: 100
261
+ maximum: 1000
261
262
  responses:
262
263
  "200":
263
264
  description: OHLCV candles
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guiie/buda-mcp",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "mcpName": "io.github.gtorreal/buda-mcp",
5
5
  "description": "MCP server for Buda.com's public cryptocurrency exchange API (Chile, Colombia, Peru)",
6
6
  "type": "module",
@@ -14,7 +14,10 @@
14
14
  "start:stdio": "node dist/index.js",
15
15
  "dev:http": "tsx src/http.ts",
16
16
  "dev": "tsx src/index.ts",
17
- "test": "tsx test/run-all.ts"
17
+ "test": "npm run test:unit && npm run test:integration",
18
+ "test:unit": "tsx test/unit.ts",
19
+ "test:integration": "tsx test/run-all.ts",
20
+ "sync-version": "node scripts/sync-version.mjs"
18
21
  },
19
22
  "engines": {
20
23
  "node": ">=18"
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Reads the version from package.json and writes it into server.json.
3
+ * Run after bumping the version in package.json:
4
+ * node scripts/sync-version.mjs
5
+ */
6
+ import { readFileSync, writeFileSync } from "fs";
7
+ import { dirname, join } from "path";
8
+ import { fileURLToPath } from "url";
9
+
10
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
11
+
12
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
13
+ const server = JSON.parse(readFileSync(join(root, "server.json"), "utf8"));
14
+
15
+ server.version = pkg.version;
16
+ server.packages[0].version = pkg.version;
17
+
18
+ writeFileSync(join(root, "server.json"), JSON.stringify(server, null, 2) + "\n");
19
+ console.log(`server.json synced to v${pkg.version}`);
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.1.2",
9
+ "version": "1.2.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@guiie/buda-mcp",
14
- "version": "1.1.2",
14
+ "version": "1.2.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }
package/src/client.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createHmac } from "crypto";
2
+ import { VERSION } from "./version.js";
2
3
 
3
4
  const BASE_URL = "https://www.buda.com/api/v2";
4
5
 
@@ -7,6 +8,7 @@ export class BudaApiError extends Error {
7
8
  public readonly status: number,
8
9
  public readonly path: string,
9
10
  message: string,
11
+ public readonly retryAfterMs?: number,
10
12
  ) {
11
13
  super(message);
12
14
  this.name = "BudaApiError";
@@ -56,24 +58,51 @@ export class BudaClient {
56
58
  };
57
59
  }
58
60
 
59
- async get<T>(path: string, params?: Record<string, string | number>): Promise<T> {
60
- const url = new URL(`${this.baseUrl}${path}.json`);
61
+ /**
62
+ * Parses the Retry-After header value into milliseconds.
63
+ * Per RFC 7231, Retry-After is an integer number of seconds.
64
+ * Defaults to 1000ms (1 second) if absent or unparseable.
65
+ */
66
+ private parseRetryAfterMs(headers: Headers): number {
67
+ const raw = headers.get("Retry-After");
68
+ if (!raw) return 1000;
69
+ const secs = parseInt(raw, 10);
70
+ return isNaN(secs) ? 1000 : secs * 1000;
71
+ }
61
72
 
62
- if (params) {
63
- for (const [key, value] of Object.entries(params)) {
64
- url.searchParams.set(key, String(value));
65
- }
73
+ /**
74
+ * Executes a fetch call with a single 429 retry.
75
+ * On the first 429, waits for Retry-After seconds (default 1s), then retries once.
76
+ * If the retry also returns 429, throws a BudaApiError with retryAfterMs set.
77
+ */
78
+ private async fetchWithRetry(
79
+ url: URL,
80
+ options: RequestInit,
81
+ path: string,
82
+ ): Promise<Response> {
83
+ const response = await fetch(url.toString(), options);
84
+
85
+ if (response.status !== 429) return response;
86
+
87
+ const retryAfterMs = this.parseRetryAfterMs(response.headers);
88
+ await new Promise((r) => setTimeout(r, retryAfterMs));
89
+
90
+ const retry = await fetch(url.toString(), options);
91
+
92
+ if (retry.status === 429) {
93
+ const retryAgainMs = this.parseRetryAfterMs(retry.headers);
94
+ throw new BudaApiError(
95
+ 429,
96
+ path,
97
+ `Buda API rate limit exceeded. Retry after ${retryAgainMs}ms.`,
98
+ retryAgainMs,
99
+ );
66
100
  }
67
101
 
68
- const urlPath = url.pathname + url.search;
69
- const headers: Record<string, string> = {
70
- Accept: "application/json",
71
- "User-Agent": "buda-mcp/1.1.1",
72
- ...this.authHeaders("GET", urlPath),
73
- };
74
-
75
- const response = await fetch(url.toString(), { headers });
102
+ return retry;
103
+ }
76
104
 
105
+ private async handleResponse<T>(response: Response, path: string): Promise<T> {
77
106
  if (!response.ok) {
78
107
  let detail = response.statusText;
79
108
  try {
@@ -84,10 +113,29 @@ export class BudaClient {
84
113
  }
85
114
  throw new BudaApiError(response.status, path, `Buda API ${response.status}: ${detail}`);
86
115
  }
87
-
88
116
  return response.json() as Promise<T>;
89
117
  }
90
118
 
119
+ async get<T>(path: string, params?: Record<string, string | number>): Promise<T> {
120
+ const url = new URL(`${this.baseUrl}${path}.json`);
121
+
122
+ if (params) {
123
+ for (const [key, value] of Object.entries(params)) {
124
+ url.searchParams.set(key, String(value));
125
+ }
126
+ }
127
+
128
+ const urlPath = url.pathname + url.search;
129
+ const headers: Record<string, string> = {
130
+ Accept: "application/json",
131
+ "User-Agent": `buda-mcp/${VERSION}`,
132
+ ...this.authHeaders("GET", urlPath),
133
+ };
134
+
135
+ const response = await this.fetchWithRetry(url, { headers }, path);
136
+ return this.handleResponse<T>(response, path);
137
+ }
138
+
91
139
  async post<T>(path: string, payload: unknown): Promise<T> {
92
140
  const url = new URL(`${this.baseUrl}${path}.json`);
93
141
  const bodyStr = JSON.stringify(payload);
@@ -95,28 +143,16 @@ export class BudaClient {
95
143
  const headers: Record<string, string> = {
96
144
  Accept: "application/json",
97
145
  "Content-Type": "application/json",
98
- "User-Agent": "buda-mcp/1.1.1",
146
+ "User-Agent": `buda-mcp/${VERSION}`,
99
147
  ...this.authHeaders("POST", urlPath, bodyStr),
100
148
  };
101
149
 
102
- const response = await fetch(url.toString(), {
103
- method: "POST",
104
- headers,
105
- body: bodyStr,
106
- });
107
-
108
- if (!response.ok) {
109
- let detail = response.statusText;
110
- try {
111
- const body = (await response.json()) as { message?: string };
112
- if (body.message) detail = body.message;
113
- } catch {
114
- // ignore parse error, use statusText
115
- }
116
- throw new BudaApiError(response.status, path, `Buda API ${response.status}: ${detail}`);
117
- }
118
-
119
- return response.json() as Promise<T>;
150
+ const response = await this.fetchWithRetry(
151
+ url,
152
+ { method: "POST", headers, body: bodyStr },
153
+ path,
154
+ );
155
+ return this.handleResponse<T>(response, path);
120
156
  }
121
157
 
122
158
  async put<T>(path: string, payload: unknown): Promise<T> {
@@ -126,27 +162,15 @@ export class BudaClient {
126
162
  const headers: Record<string, string> = {
127
163
  Accept: "application/json",
128
164
  "Content-Type": "application/json",
129
- "User-Agent": "buda-mcp/1.1.1",
165
+ "User-Agent": `buda-mcp/${VERSION}`,
130
166
  ...this.authHeaders("PUT", urlPath, bodyStr),
131
167
  };
132
168
 
133
- const response = await fetch(url.toString(), {
134
- method: "PUT",
135
- headers,
136
- body: bodyStr,
137
- });
138
-
139
- if (!response.ok) {
140
- let detail = response.statusText;
141
- try {
142
- const body = (await response.json()) as { message?: string };
143
- if (body.message) detail = body.message;
144
- } catch {
145
- // ignore parse error, use statusText
146
- }
147
- throw new BudaApiError(response.status, path, `Buda API ${response.status}: ${detail}`);
148
- }
149
-
150
- return response.json() as Promise<T>;
169
+ const response = await this.fetchWithRetry(
170
+ url,
171
+ { method: "PUT", headers, body: bodyStr },
172
+ path,
173
+ );
174
+ return this.handleResponse<T>(response, path);
151
175
  }
152
176
  }