@warmio/mcp 5.0.1 → 7.0.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/README.md +167 -165
- package/dist/config-paths.d.ts +2 -1
- package/dist/config-paths.js +6 -4
- package/dist/http.d.ts +6 -0
- package/dist/http.js +36 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +100 -6
- package/dist/install.d.ts +2 -0
- package/dist/install.js +209 -94
- package/dist/schemas.d.ts +1198 -186
- package/dist/schemas.js +226 -139
- package/dist/server.d.ts +5 -3
- package/dist/server.js +96 -32
- package/dist/types.d.ts +5 -2
- package/dist/types.js +8 -3
- package/dist/warm-api-client.d.ts +222 -163
- package/dist/warm-api-client.js +110 -222
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Warmio
|
|
2
2
|
|
|
3
|
-
Warmio is the one-command CLI
|
|
3
|
+
Warmio is the one-command CLI with separate compact MCP modes for financial context and supported automations.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,26 +8,32 @@ Warmio is the one-command CLI and read-only MCP server for Warm financial data.
|
|
|
8
8
|
npx -y @warmio/mcp@latest
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
The installer detects supported MCP clients, prompts for
|
|
12
|
-
writes the local `stdio` MCP config automatically.
|
|
13
|
-
|
|
11
|
+
The installer detects supported MCP clients, prompts for a mode-specific Warm API key, validates
|
|
12
|
+
its audience, and writes the local `stdio` MCP config automatically. Context and automation keys
|
|
13
|
+
are stored separately and are never shared between modes.
|
|
14
14
|
|
|
15
15
|
## Requirements
|
|
16
16
|
|
|
17
17
|
- Warm Pro
|
|
18
|
-
- A
|
|
18
|
+
- A context key or an automation key from [Settings -> API Keys](https://warm.io/settings)
|
|
19
19
|
- Node.js 18+
|
|
20
20
|
|
|
21
21
|
## Manual MCP Config
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Context is the default read-only mode. Install it with its own key:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx -y @warmio/mcp@latest install --mode context
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Use this when a client asks you to paste a context MCP JSON config:
|
|
24
30
|
|
|
25
31
|
```json
|
|
26
32
|
{
|
|
27
33
|
"mcpServers": {
|
|
28
34
|
"warm": {
|
|
29
35
|
"command": "npx",
|
|
30
|
-
"args": ["-y", "@warmio/mcp@latest", "mcp"]
|
|
36
|
+
"args": ["-y", "@warmio/mcp@latest", "mcp", "--mode", "context"]
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
}
|
|
@@ -40,18 +46,48 @@ On Windows, prefer:
|
|
|
40
46
|
"mcpServers": {
|
|
41
47
|
"warm": {
|
|
42
48
|
"command": "cmd",
|
|
43
|
-
"args": ["/c", "npx", "-y", "@warmio/mcp@latest", "mcp"]
|
|
49
|
+
"args": ["/c", "npx", "-y", "@warmio/mcp@latest", "mcp", "--mode", "context"]
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
```
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
Context credential overrides:
|
|
56
|
+
|
|
57
|
+
- `WARM_CONTEXT_API_KEY`
|
|
58
|
+
- `WARM_CONTEXT_API_KEY_FILE`
|
|
59
|
+
|
|
60
|
+
Automation is intentionally opt-in and must use a distinct automation key:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx -y @warmio/mcp@latest install --mode automation
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"warm-automation": {
|
|
70
|
+
"command": "npx",
|
|
71
|
+
"args": ["-y", "@warmio/mcp@latest", "mcp", "--mode", "automation"]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
50
76
|
|
|
51
|
-
|
|
52
|
-
|
|
77
|
+
Automation credential overrides are `WARM_AUTOMATION_API_KEY` and
|
|
78
|
+
`WARM_AUTOMATION_API_KEY_FILE`. The default key files are `${WARM_CONFIG_DIR}/context_api_key`
|
|
79
|
+
and `${WARM_CONFIG_DIR}/automation_api_key`; `WARM_API_KEY` and `WARM_API_KEY_FILE` are not read.
|
|
80
|
+
The API verifies the key audience before every MCP tool call, so a context key cannot operate the
|
|
81
|
+
automation server and an automation key cannot operate the context server.
|
|
53
82
|
|
|
54
|
-
|
|
83
|
+
Installer validation fails closed for API responses `401` and `403`. A `429` rate limit or a
|
|
84
|
+
temporary network failure remains a soft validation failure so setup can finish; the MCP server
|
|
85
|
+
will still validate the key audience before serving tools.
|
|
86
|
+
|
|
87
|
+
Both modes send API requests to `https://app.warm.io` by default. Set `WARM_API_URL` only when an
|
|
88
|
+
explicit API base URL override is required.
|
|
89
|
+
|
|
90
|
+
## Universal Copy/Paste Setup Prompt
|
|
55
91
|
|
|
56
92
|
```text
|
|
57
93
|
I want to connect Warm to this AI app using MCP.
|
|
@@ -62,7 +98,7 @@ Use this MCP server config:
|
|
|
62
98
|
"mcpServers": {
|
|
63
99
|
"warm": {
|
|
64
100
|
"command": "npx",
|
|
65
|
-
"args": ["-y", "@warmio/mcp@latest", "mcp"]
|
|
101
|
+
"args": ["-y", "@warmio/mcp@latest", "mcp", "--mode", "context"]
|
|
66
102
|
}
|
|
67
103
|
}
|
|
68
104
|
}
|
|
@@ -73,18 +109,29 @@ Do not ask me to paste my Warm API key into this chat. Help me store it locally
|
|
|
73
109
|
flow, then test the connection by calling Warm's verify_key tool.
|
|
74
110
|
|
|
75
111
|
After setup, I should be able to ask:
|
|
76
|
-
- What
|
|
112
|
+
- What is my net worth?
|
|
77
113
|
- Summarize my recent transactions.
|
|
78
114
|
- What changed recently?
|
|
79
115
|
- What recurring charges do I have?
|
|
80
116
|
```
|
|
81
117
|
|
|
118
|
+
## CLI
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npx -y @warmio/mcp@latest context get
|
|
122
|
+
npx -y @warmio/mcp@latest context meta
|
|
123
|
+
npx -y @warmio/mcp@latest transactions get --month 2026-07
|
|
124
|
+
npx -y @warmio/mcp@latest transactions get --latest
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
All CLI read commands print JSON.
|
|
128
|
+
|
|
82
129
|
## Optional Streamable HTTP
|
|
83
130
|
|
|
84
131
|
If your MCP client supports Streamable HTTP, you can run:
|
|
85
132
|
|
|
86
133
|
```bash
|
|
87
|
-
npx -y @warmio/mcp@latest http --host 127.0.0.1 --port 3000 --path /mcp
|
|
134
|
+
npx -y @warmio/mcp@latest http --mode context --host 127.0.0.1 --port 3000 --path /mcp
|
|
88
135
|
```
|
|
89
136
|
|
|
90
137
|
Relevant optional environment variables:
|
|
@@ -93,29 +140,52 @@ Relevant optional environment variables:
|
|
|
93
140
|
- `WARM_MCP_HTTP_PORT`
|
|
94
141
|
- `WARM_MCP_HTTP_PATH`
|
|
95
142
|
- `WARM_MCP_ALLOWED_HOSTS`
|
|
143
|
+
- `WARM_MCP_AUTH_TOKEN`
|
|
96
144
|
|
|
97
|
-
|
|
145
|
+
`WARM_MCP_AUTH_TOKEN` is a separate bearer token for the MCP HTTP transport, never a Warm API
|
|
146
|
+
key. Automation HTTP always requires it, including on loopback. Context HTTP requires it whenever
|
|
147
|
+
the server binds anything other than the exact SDK-protected loopback hosts `127.0.0.1`,
|
|
148
|
+
`localhost`, or `::1`. Send it on every request as
|
|
149
|
+
`Authorization: Bearer <WARM_MCP_AUTH_TOKEN>`.
|
|
98
150
|
|
|
99
|
-
|
|
151
|
+
## MCP Tools
|
|
100
152
|
|
|
101
|
-
|
|
102
|
-
| --------------------- | ----------------------------------------------- |
|
|
103
|
-
| `get_accounts` | List connected accounts with current balances |
|
|
104
|
-
| `get_transactions` | Page through transactions with an opaque cursor |
|
|
105
|
-
| `get_financial_state` | Return the current typed financial state bundle |
|
|
106
|
-
| `verify_key` | Validate the configured API key |
|
|
153
|
+
Context mode remains exactly three read-only tools:
|
|
107
154
|
|
|
108
|
-
|
|
155
|
+
| Tool | Description |
|
|
156
|
+
| ----------------------- | ------------------------------------------------ |
|
|
157
|
+
| `get_financial_context` | Return compact `FinancialContext` JSON |
|
|
158
|
+
| `get_transactions` | Return one month page or the fixed latest window |
|
|
159
|
+
| `verify_key` | Validate the configured API key |
|
|
109
160
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
161
|
+
Automation mode is also exactly three tools:
|
|
162
|
+
|
|
163
|
+
| Tool | Description |
|
|
164
|
+
| -------------------- | ------------------------------------------------------------------ |
|
|
165
|
+
| `search_operations` | Find supported operations without loading the full catalog |
|
|
166
|
+
| `describe_operation` | Read an operation schema and request approval for a write input |
|
|
167
|
+
| `invoke_operation` | Invoke the operation; every write requires an `approval_id` |
|
|
117
168
|
|
|
118
|
-
|
|
169
|
+
## Parameter Contract
|
|
170
|
+
|
|
171
|
+
- Every tool takes a JSON object input and returns a JSON object output.
|
|
172
|
+
- `get_financial_context` takes `{}` and returns compact context JSON without transaction items.
|
|
173
|
+
- `get_transactions` takes either `{ "month": "YYYY-MM" }`, `{ "latest": true }`, or `{}`.
|
|
174
|
+
- A bare `get_transactions` call with no arguments defaults to `{ "latest": true }`.
|
|
175
|
+
- `month` must use `YYYY-MM` format, for example `2026-07`.
|
|
176
|
+
- `latest` is a fixed 10-day window anchored to the current artifact; the caller cannot configure the window.
|
|
177
|
+
- `month` and `latest` are mutually exclusive.
|
|
178
|
+
- A malformed month returns an error.
|
|
179
|
+
- A well-formed month outside the covered range returns an error.
|
|
180
|
+
- A well-formed month inside the covered range with no transactions returns `count: 0` and `items: []`.
|
|
181
|
+
- Transaction amounts follow the Plaid sign convention: positive = expense/debit, negative = income/credit.
|
|
182
|
+
- `describe_operation` returns `approval: { id, status, approval_url, expires_at }` for every write operation.
|
|
183
|
+
- `invoke_operation` accepts that approval as `approval_id`; confirmation tokens are not supported.
|
|
184
|
+
- Tool and Warm API failures return structured MCP results with `isError: true`; malformed MCP input remains a protocol error.
|
|
185
|
+
|
|
186
|
+
## Shapes
|
|
187
|
+
|
|
188
|
+
### `get_financial_context`
|
|
119
189
|
|
|
120
190
|
Input:
|
|
121
191
|
|
|
@@ -123,168 +193,89 @@ Input:
|
|
|
123
193
|
{}
|
|
124
194
|
```
|
|
125
195
|
|
|
126
|
-
Returns
|
|
196
|
+
Returns compact `FinancialContext`:
|
|
127
197
|
|
|
128
198
|
```json
|
|
129
199
|
{
|
|
130
|
-
"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
|
|
139
|
-
|
|
200
|
+
"version": "v1",
|
|
201
|
+
"updated_at": "2026-07-06T11:58:41.000Z",
|
|
202
|
+
"currency": "USD",
|
|
203
|
+
"status": {
|
|
204
|
+
"position": {
|
|
205
|
+
"date": "2026-07-06",
|
|
206
|
+
"net_worth": 84500,
|
|
207
|
+
"cash": 18200,
|
|
208
|
+
"debt": 6400,
|
|
209
|
+
"investments": 72700,
|
|
210
|
+
"other_assets": 0,
|
|
211
|
+
"total_assets": 90900
|
|
212
|
+
},
|
|
213
|
+
"accounts": []
|
|
214
|
+
},
|
|
215
|
+
"transactions": {
|
|
216
|
+
"total": 2,
|
|
217
|
+
"months": [{ "month": "2026-07", "count": 2 }]
|
|
218
|
+
},
|
|
219
|
+
"recurring": [],
|
|
220
|
+
"budgets": [],
|
|
221
|
+
"goals": [],
|
|
222
|
+
"snapshots": [],
|
|
223
|
+
"liabilities": [],
|
|
224
|
+
"holdings": [],
|
|
225
|
+
"health": null
|
|
140
226
|
}
|
|
141
227
|
```
|
|
142
228
|
|
|
143
229
|
### `get_transactions`
|
|
144
230
|
|
|
145
|
-
|
|
231
|
+
Month input:
|
|
146
232
|
|
|
147
233
|
```json
|
|
148
|
-
{
|
|
149
|
-
"limit": 100,
|
|
150
|
-
"cursor": "opaque-cursor-from-a-prior-page",
|
|
151
|
-
"last_knowledge": "2026-03-11T00:00:00.000Z",
|
|
152
|
-
"search": "coffee"
|
|
153
|
-
}
|
|
234
|
+
{ "month": "2026-07" }
|
|
154
235
|
```
|
|
155
236
|
|
|
156
|
-
|
|
237
|
+
Month response:
|
|
157
238
|
|
|
158
239
|
```json
|
|
159
240
|
{
|
|
160
|
-
"
|
|
161
|
-
"
|
|
162
|
-
"
|
|
241
|
+
"month": "2026-07",
|
|
242
|
+
"start_date": "2026-07-01",
|
|
243
|
+
"end_date": "2026-07-31",
|
|
244
|
+
"count": 1,
|
|
245
|
+
"items": [
|
|
163
246
|
{
|
|
164
|
-
"id": "
|
|
165
|
-
"
|
|
166
|
-
"
|
|
167
|
-
"
|
|
168
|
-
"
|
|
247
|
+
"id": "txn_1",
|
|
248
|
+
"account_id": "acct_1",
|
|
249
|
+
"date": "2026-07-05",
|
|
250
|
+
"amount": 42.18,
|
|
251
|
+
"merchant": "Whole Foods",
|
|
252
|
+
"name": "WHOLE FOODS",
|
|
169
253
|
"category": "FOOD_AND_DRINK",
|
|
170
|
-
"
|
|
254
|
+
"subcategory": "FOOD_AND_DRINK_GROCERIES",
|
|
255
|
+
"pending": false,
|
|
256
|
+
"currency": "USD"
|
|
171
257
|
}
|
|
172
|
-
]
|
|
173
|
-
"pagination": {
|
|
174
|
-
"limit": 100,
|
|
175
|
-
"next_cursor": "opaque-next-cursor",
|
|
176
|
-
"has_more": true
|
|
177
|
-
}
|
|
258
|
+
]
|
|
178
259
|
}
|
|
179
260
|
```
|
|
180
261
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
1. Omit `cursor` on the first call.
|
|
184
|
-
2. Keep `limit` and any filters such as `search` fixed while following a cursor chain.
|
|
185
|
-
3. If `pagination.next_cursor` is non-null, pass it unchanged to fetch the next page.
|
|
186
|
-
4. Stop when `next_cursor` is `null`.
|
|
187
|
-
5. Do not combine `cursor` with `last_knowledge`.
|
|
188
|
-
|
|
189
|
-
### `get_financial_state`
|
|
190
|
-
|
|
191
|
-
Input:
|
|
262
|
+
Latest input:
|
|
192
263
|
|
|
193
264
|
```json
|
|
194
|
-
{}
|
|
265
|
+
{ "latest": true }
|
|
195
266
|
```
|
|
196
267
|
|
|
197
|
-
|
|
268
|
+
Latest response:
|
|
198
269
|
|
|
199
270
|
```json
|
|
200
271
|
{
|
|
201
|
-
"
|
|
202
|
-
"
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
"net_worth": 125430.55,
|
|
206
|
-
"total_assets": 168210.77,
|
|
207
|
-
"total_liabilities": 42780.22
|
|
208
|
-
}
|
|
209
|
-
],
|
|
210
|
-
"recurring": [
|
|
211
|
-
{
|
|
212
|
-
"merchant": "Netflix",
|
|
213
|
-
"amount": 15.49,
|
|
214
|
-
"frequency": "MONTHLY",
|
|
215
|
-
"next_date": "2026-03-18",
|
|
216
|
-
"type": "subscription",
|
|
217
|
-
"active": true
|
|
218
|
-
}
|
|
219
|
-
],
|
|
220
|
-
"budgets": [
|
|
221
|
-
{
|
|
222
|
-
"name": "Dining Out",
|
|
223
|
-
"amount": 400,
|
|
224
|
-
"spent": 182.55,
|
|
225
|
-
"remaining": 217.45,
|
|
226
|
-
"percent_used": 45.64,
|
|
227
|
-
"period": "monthly",
|
|
228
|
-
"status": "on_track"
|
|
229
|
-
}
|
|
230
|
-
],
|
|
231
|
-
"goals": [
|
|
232
|
-
{
|
|
233
|
-
"name": "Emergency Fund",
|
|
234
|
-
"target": 10000,
|
|
235
|
-
"current": 4200,
|
|
236
|
-
"progress_percent": 42,
|
|
237
|
-
"target_date": null,
|
|
238
|
-
"status": "active",
|
|
239
|
-
"category": "safety",
|
|
240
|
-
"monthly_contribution_needed": 400
|
|
241
|
-
}
|
|
242
|
-
],
|
|
243
|
-
"health": {
|
|
244
|
-
"score": 78,
|
|
245
|
-
"label": "Good",
|
|
246
|
-
"data_completeness": 94,
|
|
247
|
-
"pillars": {
|
|
248
|
-
"spend": 20,
|
|
249
|
-
"save": 23,
|
|
250
|
-
"borrow": 15,
|
|
251
|
-
"build": 20
|
|
252
|
-
},
|
|
253
|
-
"message": null
|
|
254
|
-
},
|
|
255
|
-
"liabilities": [
|
|
256
|
-
{
|
|
257
|
-
"account_id": "acc_loan_1",
|
|
258
|
-
"type": "student",
|
|
259
|
-
"balance": 12450.22,
|
|
260
|
-
"apr_percentage": 5.2,
|
|
261
|
-
"minimum_payment": 145,
|
|
262
|
-
"next_payment_due_date": "2026-03-22",
|
|
263
|
-
"is_overdue": false
|
|
264
|
-
}
|
|
265
|
-
],
|
|
266
|
-
"holdings": [
|
|
267
|
-
{
|
|
268
|
-
"account_id": "acc_inv_1",
|
|
269
|
-
"security_name": "Vanguard Total Stock Market ETF",
|
|
270
|
-
"symbol": "VTI",
|
|
271
|
-
"type": "etf",
|
|
272
|
-
"quantity": 12.5,
|
|
273
|
-
"value": 3541.25,
|
|
274
|
-
"cost_basis": 3010
|
|
275
|
-
}
|
|
276
|
-
],
|
|
277
|
-
"category_spending": [
|
|
278
|
-
{
|
|
279
|
-
"category": "FOOD_AND_DRINK",
|
|
280
|
-
"amount": 182.55
|
|
281
|
-
}
|
|
282
|
-
]
|
|
272
|
+
"since": "2026-06-26",
|
|
273
|
+
"window_days": 10,
|
|
274
|
+
"count": 1,
|
|
275
|
+
"items": []
|
|
283
276
|
}
|
|
284
277
|
```
|
|
285
278
|
|
|
286
|
-
If Warm does not have enough state data yet, nullable fields remain `null`.
|
|
287
|
-
|
|
288
279
|
### `verify_key`
|
|
289
280
|
|
|
290
281
|
Input:
|
|
@@ -302,10 +293,20 @@ Returns:
|
|
|
302
293
|
}
|
|
303
294
|
```
|
|
304
295
|
|
|
296
|
+
## v7 Breaking Changes
|
|
297
|
+
|
|
298
|
+
v7 is a clean pre-launch cutover with no backwards compatibility or alias window.
|
|
299
|
+
|
|
300
|
+
- API keys are audience-bound: context keys cannot invoke actions and automation keys cannot run context mode.
|
|
301
|
+
- `X-API-Key` is removed. Use `Authorization: Bearer`.
|
|
302
|
+
- Automation is a separate opt-in MCP server mode, not extra tools on the context server.
|
|
303
|
+
|
|
305
304
|
## Security
|
|
306
305
|
|
|
307
|
-
-
|
|
308
|
-
-
|
|
306
|
+
- Context mode is read-only and uses a context-only key.
|
|
307
|
+
- Automation mode uses a separate automation-only key and a curated server-side operation allowlist.
|
|
308
|
+
- Streamable HTTP is bearer-protected with an independent `WARM_MCP_AUTH_TOKEN` where required.
|
|
309
|
+
- Write operations return an approval `{ id, status, approval_url, expires_at }`; invoke the exact input with its `approval_id`.
|
|
309
310
|
- Revocable: delete the key in Settings to revoke access immediately
|
|
310
311
|
|
|
311
312
|
## Development
|
|
@@ -313,6 +314,7 @@ Returns:
|
|
|
313
314
|
```bash
|
|
314
315
|
npm install
|
|
315
316
|
npm run build
|
|
317
|
+
npm test
|
|
316
318
|
```
|
|
317
319
|
|
|
318
320
|
## License
|
package/dist/config-paths.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type WarmApiAudience = 'automation' | 'context';
|
|
1
2
|
export declare function getWarmConfigDir(): string;
|
|
2
|
-
export declare function getWarmApiKeyPath(): string;
|
|
3
|
+
export declare function getWarmApiKeyPath(audience?: WarmApiAudience): string;
|
|
3
4
|
export declare function readConfigFile(configPath: string): string | null;
|
package/dist/config-paths.js
CHANGED
|
@@ -16,11 +16,13 @@ export function getWarmConfigDir() {
|
|
|
16
16
|
}
|
|
17
17
|
return path.join(os.homedir(), '.config', 'warm');
|
|
18
18
|
}
|
|
19
|
-
export function getWarmApiKeyPath() {
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
export function getWarmApiKeyPath(audience = 'context') {
|
|
20
|
+
const envKey = audience === 'automation' ? 'WARM_AUTOMATION_API_KEY_FILE' : 'WARM_CONTEXT_API_KEY_FILE';
|
|
21
|
+
const configuredPath = process.env[envKey]?.trim();
|
|
22
|
+
if (configuredPath) {
|
|
23
|
+
return configuredPath;
|
|
22
24
|
}
|
|
23
|
-
return path.join(getWarmConfigDir(),
|
|
25
|
+
return path.join(getWarmConfigDir(), `${audience}_api_key`);
|
|
24
26
|
}
|
|
25
27
|
export function readConfigFile(configPath) {
|
|
26
28
|
try {
|
package/dist/http.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { type WarmServerMode } from './server.js';
|
|
1
2
|
export interface HttpServerOptions {
|
|
2
3
|
allowedHosts?: string[];
|
|
4
|
+
authToken?: string;
|
|
3
5
|
host?: string;
|
|
6
|
+
mode?: WarmServerMode;
|
|
4
7
|
path?: string;
|
|
5
8
|
port?: number;
|
|
6
9
|
}
|
|
10
|
+
export declare function isLoopbackHost(host: string): boolean;
|
|
11
|
+
export declare function validateHttpSecurity(options: Pick<HttpServerOptions, 'authToken' | 'host' | 'mode'>): void;
|
|
12
|
+
export declare function hasValidTransportBearerToken(authorization: string | undefined, authToken: string): boolean;
|
|
7
13
|
export declare function resolveHttpServerOptions(overrides?: HttpServerOptions): Required<HttpServerOptions>;
|
|
8
14
|
export declare function startHttpServer(options?: HttpServerOptions): Promise<any>;
|
package/dist/http.js
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
|
|
2
2
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
3
4
|
import { createWarmServer } from './server.js';
|
|
4
5
|
const DEFAULT_HTTP_HOST = '127.0.0.1';
|
|
5
6
|
const DEFAULT_HTTP_PORT = 3000;
|
|
6
7
|
const DEFAULT_HTTP_PATH = '/mcp';
|
|
8
|
+
export function isLoopbackHost(host) {
|
|
9
|
+
const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
10
|
+
return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1';
|
|
11
|
+
}
|
|
12
|
+
export function validateHttpSecurity(options) {
|
|
13
|
+
const mode = options.mode || 'context';
|
|
14
|
+
const hasAuthToken = Boolean(options.authToken?.trim());
|
|
15
|
+
if (mode === 'automation' && !hasAuthToken) {
|
|
16
|
+
throw new Error('Automation MCP HTTP requires WARM_MCP_AUTH_TOKEN.');
|
|
17
|
+
}
|
|
18
|
+
if (!isLoopbackHost(options.host || DEFAULT_HTTP_HOST) && !hasAuthToken) {
|
|
19
|
+
throw new Error('Non-loopback MCP HTTP requires WARM_MCP_AUTH_TOKEN.');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function hasValidTransportBearerToken(authorization, authToken) {
|
|
23
|
+
const expected = `Bearer ${authToken}`;
|
|
24
|
+
if (!authorization) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const actualBuffer = Buffer.from(authorization);
|
|
28
|
+
const expectedBuffer = Buffer.from(expected);
|
|
29
|
+
if (actualBuffer.length !== expectedBuffer.length) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return timingSafeEqual(actualBuffer, expectedBuffer);
|
|
33
|
+
}
|
|
7
34
|
function parsePort(value, fallback) {
|
|
8
35
|
const parsed = Number(value);
|
|
9
36
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
@@ -37,11 +64,13 @@ export function resolveHttpServerOptions(overrides = {}) {
|
|
|
37
64
|
return {
|
|
38
65
|
allowedHosts: overrides.allowedHosts ??
|
|
39
66
|
parseAllowedHosts(process.env.WARM_MCP_ALLOWED_HOSTS || process.env.MCP_ALLOWED_HOSTS),
|
|
67
|
+
authToken: overrides.authToken ?? process.env.WARM_MCP_AUTH_TOKEN?.trim() ?? '',
|
|
40
68
|
host: overrides.host ||
|
|
41
69
|
process.env.WARM_MCP_HTTP_HOST ||
|
|
42
70
|
process.env.MCP_HOST ||
|
|
43
71
|
process.env.HOST ||
|
|
44
72
|
DEFAULT_HTTP_HOST,
|
|
73
|
+
mode: overrides.mode ?? 'context',
|
|
45
74
|
path: normalizePath(overrides.path || process.env.WARM_MCP_HTTP_PATH || process.env.MCP_PATH || DEFAULT_HTTP_PATH),
|
|
46
75
|
port: overrides.port ??
|
|
47
76
|
parsePort(process.env.WARM_MCP_HTTP_PORT || process.env.MCP_PORT || process.env.PORT, DEFAULT_HTTP_PORT),
|
|
@@ -54,12 +83,18 @@ function sendJsonMethodNotAllowed(res) {
|
|
|
54
83
|
}
|
|
55
84
|
export async function startHttpServer(options = {}) {
|
|
56
85
|
const resolved = resolveHttpServerOptions(options);
|
|
86
|
+
validateHttpSecurity(resolved);
|
|
57
87
|
const app = createMcpExpressApp({
|
|
58
88
|
...(resolved.allowedHosts.length > 0 ? { allowedHosts: resolved.allowedHosts } : {}),
|
|
59
89
|
host: resolved.host,
|
|
60
90
|
});
|
|
61
91
|
app.post(resolved.path, async (req, res) => {
|
|
62
|
-
|
|
92
|
+
if (resolved.authToken &&
|
|
93
|
+
!hasValidTransportBearerToken(req.get('authorization'), resolved.authToken)) {
|
|
94
|
+
res.status(401).json(jsonRpcError('Unauthorized MCP transport request.'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const server = createWarmServer({ mode: resolved.mode });
|
|
63
98
|
const transport = new StreamableHTTPServerTransport({
|
|
64
99
|
sessionIdGenerator: undefined,
|
|
65
100
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import { type WarmServerMode } from './server.js';
|
|
3
|
+
export declare function startStdioServer(mode?: WarmServerMode): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer>;
|