@warmio/mcp 3.0.2 → 4.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/README.md +270 -37
- package/dist/config-paths.d.ts +2 -0
- package/dist/config-paths.js +23 -0
- package/dist/http.d.ts +9 -0
- package/dist/http.js +117 -0
- package/dist/index.js +140 -7
- package/dist/install.d.ts +5 -1
- package/dist/install.js +116 -42
- package/dist/schemas.d.ts +226 -0
- package/dist/schemas.js +184 -0
- package/dist/server.d.ts +5 -7
- package/dist/server.js +9 -311
- package/dist/types.d.ts +157 -0
- package/dist/types.js +6 -0
- package/dist/warm-server.d.ts +14 -0
- package/dist/warm-server.js +245 -0
- package/package.json +25 -2
package/README.md
CHANGED
|
@@ -1,60 +1,293 @@
|
|
|
1
|
-
# Warm MCP
|
|
1
|
+
# Warm MCP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Read-only MCP server for Warm financial data.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Warm supports two transport shapes from this repo:
|
|
6
|
+
|
|
7
|
+
- Local `stdio` via the npm package `@warmio/mcp`
|
|
8
|
+
- Self-hosted Streamable HTTP via `warm-mcp http`
|
|
9
|
+
|
|
10
|
+
Warm does not currently publish a Warm-hosted Streamable HTTP MCP endpoint from this repo.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
6
13
|
|
|
7
14
|
```bash
|
|
8
15
|
npx @warmio/mcp
|
|
9
16
|
```
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
The installer detects supported MCP clients, prompts for your Warm API key, and writes the local
|
|
19
|
+
`stdio` server config automatically. The key is stored once in your local Warm profile instead of
|
|
20
|
+
being duplicated into every MCP client config.
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
- "What's my net worth?"
|
|
16
|
-
- "How much did I spend on restaurants last month?"
|
|
17
|
-
- "Show me my subscriptions"
|
|
22
|
+
## Requirements
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
- Warm Pro
|
|
25
|
+
- A Warm API key from [Settings -> API Keys](https://warm.io/settings)
|
|
26
|
+
- Node.js 18+
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|---------|-------------|
|
|
23
|
-
| `npx @warmio/mcp` | Run the installer / configurator |
|
|
24
|
-
| `npx @warmio/mcp --force` | Re-run installer (updates API key in all configs) |
|
|
25
|
-
| `npx @warmio/mcp --server` | Start the MCP server (used internally by clients) |
|
|
28
|
+
## Manual `stdio` Config
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
The installer stores your API key in the Warm config directory and generated MCP client configs can
|
|
31
|
+
stay secret-free:
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"warm": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["-y", "@warmio/mcp", "--server"]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
32
43
|
|
|
33
|
-
|
|
44
|
+
Optional auth overrides:
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
3. Prompts for your Warm API key
|
|
38
|
-
4. Writes the server config into each client's settings
|
|
39
|
-
5. Each client is configured to run `npx -y @warmio/mcp --server` on demand
|
|
46
|
+
- `WARM_API_KEY`
|
|
47
|
+
- `WARM_API_KEY_FILE`
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
## Self-hosted Streamable HTTP
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
Run the HTTP server locally or behind your own reverse proxy:
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
```bash
|
|
54
|
+
npx @warmio/mcp http --host 127.0.0.1 --port 3000 --path /mcp
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Environment overrides:
|
|
58
|
+
|
|
59
|
+
- `WARM_MCP_HTTP_HOST`
|
|
60
|
+
- `WARM_MCP_HTTP_PORT`
|
|
61
|
+
- `WARM_MCP_HTTP_PATH`
|
|
62
|
+
- `WARM_MCP_ALLOWED_HOSTS`
|
|
63
|
+
- `WARM_API_KEY_FILE`
|
|
64
|
+
|
|
65
|
+
On Windows, prefer:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"warm": {
|
|
71
|
+
"command": "cmd",
|
|
72
|
+
"args": ["/c", "npx", "-y", "@warmio/mcp", "--server"]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Core Tools
|
|
79
|
+
|
|
80
|
+
Warm's published/documented MCP surface is the following four-tool core:
|
|
81
|
+
|
|
82
|
+
| Tool | Description |
|
|
83
|
+
| --------------------- | ----------------------------------------------- |
|
|
84
|
+
| `get_accounts` | List connected accounts with current balances |
|
|
85
|
+
| `get_transactions` | Page through transactions with an opaque cursor |
|
|
86
|
+
| `get_financial_state` | Return the current typed financial state bundle |
|
|
87
|
+
| `verify_key` | Validate the configured API key |
|
|
88
|
+
|
|
89
|
+
## Strict Contract
|
|
90
|
+
|
|
91
|
+
- Every tool takes a JSON object input and returns a JSON object output.
|
|
92
|
+
- Treat the contracts as closed and typed. Do not depend on undocumented fields.
|
|
93
|
+
- Calendar dates use `YYYY-MM-DD`. Incremental sync timestamps use ISO 8601 datetimes.
|
|
94
|
+
- Amounts are numbers, never formatted strings.
|
|
95
|
+
- Transaction amounts follow the Plaid sign convention:
|
|
96
|
+
positive = expense/debit, negative = income/credit.
|
|
97
|
+
- Pagination cursors are opaque strings. Do not parse them or mix them with changed filters.
|
|
98
|
+
|
|
99
|
+
### `get_accounts`
|
|
100
|
+
|
|
101
|
+
Input:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"accounts": [
|
|
112
|
+
{
|
|
113
|
+
"name": "Primary Checking",
|
|
114
|
+
"type": "depository",
|
|
115
|
+
"subtype": "checking",
|
|
116
|
+
"balance": 2450.12,
|
|
117
|
+
"institution": "Chase",
|
|
118
|
+
"mask": "1234"
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `get_transactions`
|
|
125
|
+
|
|
126
|
+
Input:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"limit": 100,
|
|
131
|
+
"cursor": "opaque-cursor-from-a-prior-page",
|
|
132
|
+
"last_knowledge": "2026-03-11T00:00:00.000Z",
|
|
133
|
+
"search": "coffee"
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"generated_at": "2026-03-11T12:00:00.000Z",
|
|
142
|
+
"next_knowledge": "2026-03-11T12:00:00.000Z",
|
|
143
|
+
"txns": [
|
|
144
|
+
{
|
|
145
|
+
"id": "txn_123",
|
|
146
|
+
"date": "2026-01-15",
|
|
147
|
+
"amount": 12.34,
|
|
148
|
+
"merchant": "Coffee Shop",
|
|
149
|
+
"description": "COFFEE SHOP",
|
|
150
|
+
"category": "FOOD_AND_DRINK",
|
|
151
|
+
"detailed_category": "FOOD_AND_DRINK_COFFEE"
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
"pagination": {
|
|
155
|
+
"limit": 100,
|
|
156
|
+
"next_cursor": "opaque-next-cursor",
|
|
157
|
+
"has_more": true
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Cursor model:
|
|
163
|
+
|
|
164
|
+
1. Omit `cursor` on the first call.
|
|
165
|
+
2. Keep `limit` and any filters such as `search` fixed while following a cursor chain.
|
|
166
|
+
3. If `pagination.next_cursor` is non-null, pass it unchanged to fetch the next page.
|
|
167
|
+
4. Stop when `next_cursor` is `null`.
|
|
168
|
+
5. Do not combine `cursor` with `last_knowledge`.
|
|
169
|
+
|
|
170
|
+
### `get_financial_state`
|
|
171
|
+
|
|
172
|
+
Input:
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"generated_at": "2026-03-11T12:00:00.000Z",
|
|
183
|
+
"snapshots": [
|
|
184
|
+
{
|
|
185
|
+
"date": "2026-03-11",
|
|
186
|
+
"net_worth": 125430.55,
|
|
187
|
+
"total_assets": 168210.77,
|
|
188
|
+
"total_liabilities": 42780.22
|
|
189
|
+
}
|
|
190
|
+
],
|
|
191
|
+
"recurring": [
|
|
192
|
+
{
|
|
193
|
+
"merchant": "Netflix",
|
|
194
|
+
"amount": 15.49,
|
|
195
|
+
"frequency": "MONTHLY",
|
|
196
|
+
"next_date": "2026-03-18",
|
|
197
|
+
"type": "subscription",
|
|
198
|
+
"active": true
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
"budgets": [
|
|
202
|
+
{
|
|
203
|
+
"name": "Dining Out",
|
|
204
|
+
"amount": 400,
|
|
205
|
+
"spent": 182.55,
|
|
206
|
+
"remaining": 217.45,
|
|
207
|
+
"percent_used": 45.64,
|
|
208
|
+
"period": "monthly",
|
|
209
|
+
"status": "on_track"
|
|
210
|
+
}
|
|
211
|
+
],
|
|
212
|
+
"goals": [
|
|
213
|
+
{
|
|
214
|
+
"name": "Emergency Fund",
|
|
215
|
+
"target": 10000,
|
|
216
|
+
"current": 4200,
|
|
217
|
+
"progress_percent": 42,
|
|
218
|
+
"target_date": null,
|
|
219
|
+
"status": "active",
|
|
220
|
+
"category": "safety",
|
|
221
|
+
"monthly_contribution_needed": 400
|
|
222
|
+
}
|
|
223
|
+
],
|
|
224
|
+
"health": {
|
|
225
|
+
"score": 78,
|
|
226
|
+
"label": "Good",
|
|
227
|
+
"data_completeness": 94,
|
|
228
|
+
"pillars": {
|
|
229
|
+
"spend": 20,
|
|
230
|
+
"save": 23,
|
|
231
|
+
"borrow": 15,
|
|
232
|
+
"build": 20
|
|
233
|
+
},
|
|
234
|
+
"message": null
|
|
235
|
+
},
|
|
236
|
+
"liabilities": [
|
|
237
|
+
{
|
|
238
|
+
"account_id": "acc_loan_1",
|
|
239
|
+
"type": "student",
|
|
240
|
+
"balance": 12450.22,
|
|
241
|
+
"apr_percentage": 5.2,
|
|
242
|
+
"minimum_payment": 145,
|
|
243
|
+
"next_payment_due_date": "2026-03-22",
|
|
244
|
+
"is_overdue": false
|
|
245
|
+
}
|
|
246
|
+
],
|
|
247
|
+
"holdings": [
|
|
248
|
+
{
|
|
249
|
+
"account_id": "acc_inv_1",
|
|
250
|
+
"security_name": "Vanguard Total Stock Market ETF",
|
|
251
|
+
"symbol": "VTI",
|
|
252
|
+
"type": "etf",
|
|
253
|
+
"quantity": 12.5,
|
|
254
|
+
"value": 3541.25,
|
|
255
|
+
"cost_basis": 3010
|
|
256
|
+
}
|
|
257
|
+
],
|
|
258
|
+
"category_spending": [
|
|
259
|
+
{
|
|
260
|
+
"category": "FOOD_AND_DRINK",
|
|
261
|
+
"amount": 182.55
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
If Warm does not have enough state data yet, nullable fields remain `null`.
|
|
268
|
+
|
|
269
|
+
### `verify_key`
|
|
270
|
+
|
|
271
|
+
Input:
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{
|
|
281
|
+
"valid": true,
|
|
282
|
+
"status": "ok"
|
|
283
|
+
}
|
|
284
|
+
```
|
|
52
285
|
|
|
53
286
|
## Security
|
|
54
287
|
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
288
|
+
- Read-only: no write, delete, transfer, or mutation tools
|
|
289
|
+
- Scoped: the key only reads the owner's Warm data
|
|
290
|
+
- Revocable: delete the key in Settings to revoke access immediately
|
|
58
291
|
|
|
59
292
|
## Development
|
|
60
293
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as os from 'node:os';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export function getWarmConfigDir() {
|
|
4
|
+
if (process.env.WARM_CONFIG_DIR?.trim()) {
|
|
5
|
+
return process.env.WARM_CONFIG_DIR.trim();
|
|
6
|
+
}
|
|
7
|
+
if (process.platform === 'win32') {
|
|
8
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Warm');
|
|
9
|
+
}
|
|
10
|
+
if (process.env.XDG_CONFIG_HOME?.trim()) {
|
|
11
|
+
return path.join(process.env.XDG_CONFIG_HOME.trim(), 'warm');
|
|
12
|
+
}
|
|
13
|
+
if (process.platform === 'darwin') {
|
|
14
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'Warm');
|
|
15
|
+
}
|
|
16
|
+
return path.join(os.homedir(), '.config', 'warm');
|
|
17
|
+
}
|
|
18
|
+
export function getWarmApiKeyPath() {
|
|
19
|
+
if (process.env.WARM_API_KEY_FILE?.trim()) {
|
|
20
|
+
return process.env.WARM_API_KEY_FILE.trim();
|
|
21
|
+
}
|
|
22
|
+
return path.join(getWarmConfigDir(), 'api_key');
|
|
23
|
+
}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Server as HttpServer } from 'http';
|
|
2
|
+
export interface WarmHttpServerOptions {
|
|
3
|
+
host?: string;
|
|
4
|
+
port?: number;
|
|
5
|
+
path?: string;
|
|
6
|
+
allowedHosts?: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveHttpServerOptions(overrides?: WarmHttpServerOptions): Required<WarmHttpServerOptions>;
|
|
9
|
+
export declare function startHttpServer(options?: WarmHttpServerOptions): Promise<HttpServer>;
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { createWarmServer } from './server.js';
|
|
4
|
+
const DEFAULT_HTTP_HOST = '127.0.0.1';
|
|
5
|
+
const DEFAULT_HTTP_PORT = 3000;
|
|
6
|
+
const DEFAULT_HTTP_PATH = '/mcp';
|
|
7
|
+
function parsePort(value, fallback) {
|
|
8
|
+
const parsed = Number(value);
|
|
9
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
10
|
+
}
|
|
11
|
+
function normalizePath(value) {
|
|
12
|
+
if (!value) {
|
|
13
|
+
return DEFAULT_HTTP_PATH;
|
|
14
|
+
}
|
|
15
|
+
return value.startsWith('/') ? value : `/${value}`;
|
|
16
|
+
}
|
|
17
|
+
function parseAllowedHosts(value) {
|
|
18
|
+
if (!value) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
return value
|
|
22
|
+
.split(',')
|
|
23
|
+
.map((host) => host.trim())
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
function jsonRpcError(message) {
|
|
27
|
+
return {
|
|
28
|
+
jsonrpc: '2.0',
|
|
29
|
+
error: {
|
|
30
|
+
code: -32000,
|
|
31
|
+
message,
|
|
32
|
+
},
|
|
33
|
+
id: null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function sendMethodNotAllowed(res) {
|
|
37
|
+
res.status(405).set('Allow', 'POST').json(jsonRpcError('Method not allowed.'));
|
|
38
|
+
}
|
|
39
|
+
export function resolveHttpServerOptions(overrides = {}) {
|
|
40
|
+
return {
|
|
41
|
+
host: overrides.host ||
|
|
42
|
+
process.env.WARM_MCP_HTTP_HOST ||
|
|
43
|
+
process.env.MCP_HOST ||
|
|
44
|
+
process.env.HOST ||
|
|
45
|
+
DEFAULT_HTTP_HOST,
|
|
46
|
+
port: overrides.port ??
|
|
47
|
+
parsePort(process.env.WARM_MCP_HTTP_PORT || process.env.MCP_PORT || process.env.PORT, DEFAULT_HTTP_PORT),
|
|
48
|
+
path: normalizePath(overrides.path || process.env.WARM_MCP_HTTP_PATH || process.env.MCP_PATH || DEFAULT_HTTP_PATH),
|
|
49
|
+
allowedHosts: overrides.allowedHosts ??
|
|
50
|
+
parseAllowedHosts(process.env.WARM_MCP_ALLOWED_HOSTS || process.env.MCP_ALLOWED_HOSTS),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export async function startHttpServer(options = {}) {
|
|
54
|
+
const resolved = resolveHttpServerOptions(options);
|
|
55
|
+
const app = createMcpExpressApp({
|
|
56
|
+
host: resolved.host,
|
|
57
|
+
...(resolved.allowedHosts.length > 0 ? { allowedHosts: resolved.allowedHosts } : {}),
|
|
58
|
+
});
|
|
59
|
+
app.post(resolved.path, async (req, res) => {
|
|
60
|
+
const server = createWarmServer();
|
|
61
|
+
const transport = new StreamableHTTPServerTransport({
|
|
62
|
+
sessionIdGenerator: undefined,
|
|
63
|
+
});
|
|
64
|
+
let cleanedUp = false;
|
|
65
|
+
const cleanup = async () => {
|
|
66
|
+
if (cleanedUp) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
cleanedUp = true;
|
|
70
|
+
await Promise.allSettled([transport.close(), server.close()]);
|
|
71
|
+
};
|
|
72
|
+
res.on('close', () => {
|
|
73
|
+
void cleanup();
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
await server.connect(transport);
|
|
77
|
+
await transport.handleRequest(req, res, req.body);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error('Error handling Warm MCP HTTP request:', error);
|
|
81
|
+
if (!res.headersSent) {
|
|
82
|
+
res.status(500).json(jsonRpcError('Internal server error'));
|
|
83
|
+
}
|
|
84
|
+
void cleanup();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
app.get(resolved.path, (_req, res) => {
|
|
88
|
+
sendMethodNotAllowed(res);
|
|
89
|
+
});
|
|
90
|
+
app.delete(resolved.path, (_req, res) => {
|
|
91
|
+
sendMethodNotAllowed(res);
|
|
92
|
+
});
|
|
93
|
+
const listener = await new Promise((resolve, reject) => {
|
|
94
|
+
const httpServer = app.listen(resolved.port, resolved.host, () => {
|
|
95
|
+
resolve(httpServer);
|
|
96
|
+
});
|
|
97
|
+
httpServer.once('error', reject);
|
|
98
|
+
});
|
|
99
|
+
let shuttingDown = false;
|
|
100
|
+
const shutdown = (signal) => {
|
|
101
|
+
if (shuttingDown) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
shuttingDown = true;
|
|
105
|
+
console.log(`Received ${signal}, shutting down Warm MCP HTTP server...`);
|
|
106
|
+
listener.close((error) => {
|
|
107
|
+
if (error) {
|
|
108
|
+
console.error('Failed to close Warm MCP HTTP server:', error);
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
process.once('SIGINT', () => shutdown('SIGINT'));
|
|
114
|
+
process.once('SIGTERM', () => shutdown('SIGTERM'));
|
|
115
|
+
console.log(`Warm MCP Streamable HTTP server listening on http://${resolved.host}:${resolved.port}${resolved.path}`);
|
|
116
|
+
return listener;
|
|
117
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,143 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { startHttpServer } from './http.js';
|
|
3
|
+
import { install } from './install.js';
|
|
4
|
+
import { startStdioServer } from './server.js';
|
|
5
|
+
function printUsage() {
|
|
6
|
+
console.log('');
|
|
7
|
+
console.log(' Warm MCP');
|
|
8
|
+
console.log(' --------');
|
|
9
|
+
console.log('');
|
|
10
|
+
console.log(' warm-mcp [install] [--force] [--no-validate]');
|
|
11
|
+
console.log(' warm-mcp stdio');
|
|
12
|
+
console.log(' warm-mcp http [--host 127.0.0.1] [--port 3000] [--path /mcp]');
|
|
13
|
+
console.log(' [--allowed-hosts host1,host2]');
|
|
14
|
+
console.log('');
|
|
15
|
+
console.log(' Aliases:');
|
|
16
|
+
console.log(' --server, --stdio Start stdio mode');
|
|
17
|
+
console.log(' --http Start HTTP mode');
|
|
18
|
+
console.log('');
|
|
5
19
|
}
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
20
|
+
function parsePort(value) {
|
|
21
|
+
const parsed = Number(value);
|
|
22
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
23
|
+
throw new Error(`Invalid port: ${value}`);
|
|
24
|
+
}
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
function parseList(value) {
|
|
28
|
+
return value
|
|
29
|
+
.split(',')
|
|
30
|
+
.map((item) => item.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
function readOption(args, index, flag) {
|
|
34
|
+
const arg = args[index];
|
|
35
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
36
|
+
return {
|
|
37
|
+
nextIndex: index,
|
|
38
|
+
value: arg.slice(flag.length + 1),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const value = args[index + 1];
|
|
42
|
+
if (!value) {
|
|
43
|
+
throw new Error(`Missing value for ${flag}`);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
nextIndex: index + 1,
|
|
47
|
+
value,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function parseCliArgs(args) {
|
|
51
|
+
const options = {
|
|
52
|
+
command: 'install',
|
|
53
|
+
force: false,
|
|
54
|
+
validateApiKey: true,
|
|
55
|
+
};
|
|
56
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
57
|
+
const arg = args[index];
|
|
58
|
+
if (arg === 'help' || arg === '--help' || arg === '-h') {
|
|
59
|
+
options.command = 'help';
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (arg === 'install' || arg === '--install') {
|
|
63
|
+
options.command = 'install';
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (arg === 'stdio' || arg === 'server' || arg === '--stdio' || arg === '--server') {
|
|
67
|
+
options.command = 'stdio';
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg === 'http' || arg === '--http') {
|
|
71
|
+
options.command = 'http';
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (arg === '--force') {
|
|
75
|
+
options.force = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (arg === '--no-validate') {
|
|
79
|
+
options.validateApiKey = false;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (arg === '--host' || arg.startsWith('--host=')) {
|
|
83
|
+
const { nextIndex, value } = readOption(args, index, '--host');
|
|
84
|
+
options.host = value;
|
|
85
|
+
index = nextIndex;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (arg === '--port' || arg.startsWith('--port=')) {
|
|
89
|
+
const { nextIndex, value } = readOption(args, index, '--port');
|
|
90
|
+
options.port = parsePort(value);
|
|
91
|
+
index = nextIndex;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (arg === '--path' || arg.startsWith('--path=')) {
|
|
95
|
+
const { nextIndex, value } = readOption(args, index, '--path');
|
|
96
|
+
options.path = value;
|
|
97
|
+
index = nextIndex;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (arg === '--allowed-hosts' || arg.startsWith('--allowed-hosts=')) {
|
|
101
|
+
const { nextIndex, value } = readOption(args, index, '--allowed-hosts');
|
|
102
|
+
options.allowedHosts = parseList(value);
|
|
103
|
+
index = nextIndex;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
107
|
+
}
|
|
108
|
+
return options;
|
|
109
|
+
}
|
|
110
|
+
async function main() {
|
|
111
|
+
const options = parseCliArgs(process.argv.slice(2));
|
|
112
|
+
switch (options.command) {
|
|
113
|
+
case 'help':
|
|
114
|
+
printUsage();
|
|
115
|
+
return;
|
|
116
|
+
case 'http':
|
|
117
|
+
await startHttpServer({
|
|
118
|
+
allowedHosts: options.allowedHosts,
|
|
119
|
+
host: options.host,
|
|
120
|
+
path: options.path,
|
|
121
|
+
port: options.port,
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
case 'stdio':
|
|
125
|
+
await startStdioServer();
|
|
126
|
+
return;
|
|
127
|
+
case 'install':
|
|
128
|
+
await install({
|
|
129
|
+
force: options.force,
|
|
130
|
+
validateApiKey: options.validateApiKey,
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
await main();
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
140
|
+
console.error(message);
|
|
141
|
+
console.error('Run "warm-mcp --help" for usage.');
|
|
142
|
+
process.exit(1);
|
|
9
143
|
}
|
|
10
|
-
export {};
|
package/dist/install.d.ts
CHANGED