@warmio/mcp 1.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 +113 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +230 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Warm MCP Server
|
|
2
|
+
|
|
3
|
+
MCP server that gives Claude Code (or any MCP client) read-only access to your Warm financial data.
|
|
4
|
+
|
|
5
|
+
## Quick Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @warmio/mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Works on macOS, Linux, and Windows. Auto-configures: Claude Code, Cursor, Windsurf, Claude Desktop, OpenCode, Codex CLI, Antigravity, Gemini CLI.
|
|
12
|
+
|
|
13
|
+
## Manual Installation
|
|
14
|
+
|
|
15
|
+
### Claude Code (easiest)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
claude mcp add --scope user --env WARM_API_KEY=your-key warm -- npx -y @warmio/mcp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Other Tools
|
|
22
|
+
|
|
23
|
+
Add to your tool's MCP config file:
|
|
24
|
+
|
|
25
|
+
| Tool | Config Path | Format |
|
|
26
|
+
| -------------- | ----------------------------------------------------------------- | ------ |
|
|
27
|
+
| Claude Code | `~/.claude.json` | JSON |
|
|
28
|
+
| Cursor | `~/.cursor/mcp.json` | JSON |
|
|
29
|
+
| Windsurf | `~/.codeium/windsurf/mcp_config.json` | JSON |
|
|
30
|
+
| Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` | JSON |
|
|
31
|
+
| OpenCode | `~/.config/opencode/opencode.json` | JSON |
|
|
32
|
+
| Codex CLI | `~/.codex/config.toml` | TOML |
|
|
33
|
+
| Antigravity | `~/.gemini/antigravity/mcp_config.json` | JSON |
|
|
34
|
+
| Gemini CLI | `~/.gemini/settings.json` | JSON |
|
|
35
|
+
|
|
36
|
+
**JSON format** (most tools):
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"warm": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["-y", "@warmio/mcp"],
|
|
44
|
+
"env": { "WARM_API_KEY": "your-api-key" }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**TOML format** (Codex CLI):
|
|
51
|
+
|
|
52
|
+
```toml
|
|
53
|
+
[mcp_servers.warm]
|
|
54
|
+
command = "npx"
|
|
55
|
+
args = ["-y", "@warmio/mcp"]
|
|
56
|
+
|
|
57
|
+
[mcp_servers.warm.env]
|
|
58
|
+
WARM_API_KEY = "your-api-key"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Get your API key from [warm.io/settings](https://warm.io/settings) → API Keys.
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- **Pro subscription** — API access requires Warm Pro
|
|
66
|
+
- **API key** — Generate in Settings → API Keys
|
|
67
|
+
- **Node.js 18+** — For running the MCP server
|
|
68
|
+
|
|
69
|
+
## Available Tools
|
|
70
|
+
|
|
71
|
+
| Tool | Description |
|
|
72
|
+
| ------------------ | ---------------------------------------------- |
|
|
73
|
+
| `get_accounts` | List all connected bank accounts with balances |
|
|
74
|
+
| `get_transactions` | Get transactions with date/limit filters |
|
|
75
|
+
| `get_recurring` | Show subscriptions and recurring payments |
|
|
76
|
+
| `get_snapshots` | Net worth history (daily or monthly) |
|
|
77
|
+
| `verify_key` | Check if API key is valid |
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
Once configured, just ask Claude naturally:
|
|
82
|
+
|
|
83
|
+
- "How much did I spend on restaurants last month?"
|
|
84
|
+
- "What's my net worth?"
|
|
85
|
+
- "Show my subscriptions"
|
|
86
|
+
- "What are my biggest expenses?"
|
|
87
|
+
|
|
88
|
+
Claude will automatically use the MCP tools to query your data.
|
|
89
|
+
|
|
90
|
+
## Configuration
|
|
91
|
+
|
|
92
|
+
| Environment Variable | Description | Default |
|
|
93
|
+
| -------------------- | ---------------------------- | ----------------- |
|
|
94
|
+
| `WARM_API_KEY` | Your Warm API key (required) | — |
|
|
95
|
+
| `WARM_API_URL` | API base URL | `https://warm.io` |
|
|
96
|
+
|
|
97
|
+
## Security
|
|
98
|
+
|
|
99
|
+
- **Read-only** — Cannot modify, delete, or transfer data
|
|
100
|
+
- **Scoped** — Key only accesses your accounts
|
|
101
|
+
- **Revocable** — Delete key in Settings to revoke instantly
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
cd mcp
|
|
107
|
+
npm install
|
|
108
|
+
npm run build
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Warm MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Provides financial data from the Warm API as MCP tools.
|
|
6
|
+
* Reads API key from ~/.config/warm/api_key or WARM_API_KEY env var.
|
|
7
|
+
*/
|
|
8
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
9
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
const API_URL = process.env.WARM_API_URL || 'https://warm.io';
|
|
15
|
+
function getApiKey() {
|
|
16
|
+
// Environment variable takes precedence
|
|
17
|
+
if (process.env.WARM_API_KEY) {
|
|
18
|
+
return process.env.WARM_API_KEY;
|
|
19
|
+
}
|
|
20
|
+
// Fall back to config file
|
|
21
|
+
const configPath = path.join(os.homedir(), '.config', 'warm', 'api_key');
|
|
22
|
+
try {
|
|
23
|
+
return fs.readFileSync(configPath, 'utf-8').trim();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function apiRequest(endpoint, params = {}) {
|
|
30
|
+
const apiKey = getApiKey();
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
throw new Error('WARM_API_KEY not set. Add your API key from https://warm.io/settings');
|
|
33
|
+
}
|
|
34
|
+
const url = new URL(endpoint, API_URL);
|
|
35
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
36
|
+
if (value)
|
|
37
|
+
url.searchParams.append(key, value);
|
|
38
|
+
});
|
|
39
|
+
const response = await fetch(url.toString(), {
|
|
40
|
+
headers: {
|
|
41
|
+
Authorization: `Bearer ${apiKey}`,
|
|
42
|
+
Accept: 'application/json',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const errorMessages = {
|
|
47
|
+
401: 'Invalid or expired API key. Regenerate at https://warm.io/settings',
|
|
48
|
+
403: 'Pro subscription required. Upgrade at https://warm.io/settings',
|
|
49
|
+
429: 'Rate limit exceeded. Try again in a few minutes.',
|
|
50
|
+
};
|
|
51
|
+
throw new Error(errorMessages[response.status] || `HTTP ${response.status}`);
|
|
52
|
+
}
|
|
53
|
+
return response.json();
|
|
54
|
+
}
|
|
55
|
+
const server = new Server({ name: 'warm', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
56
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
57
|
+
tools: [
|
|
58
|
+
{
|
|
59
|
+
name: 'get_accounts',
|
|
60
|
+
description: 'Get all connected bank accounts with balances. Returns account names, types, balances, and institutions.',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
since: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
description: 'Filter accounts updated since this date (ISO format)',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'get_transactions',
|
|
73
|
+
description: 'Get transactions from connected accounts. Supports pagination and date filtering.',
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: {
|
|
77
|
+
limit: {
|
|
78
|
+
type: 'number',
|
|
79
|
+
description: 'Maximum number of transactions to return (default: 100)',
|
|
80
|
+
},
|
|
81
|
+
since: {
|
|
82
|
+
type: 'string',
|
|
83
|
+
description: 'Get transactions since this date (ISO format, e.g., 2024-01-01)',
|
|
84
|
+
},
|
|
85
|
+
cursor: {
|
|
86
|
+
type: 'string',
|
|
87
|
+
description: 'Pagination cursor for fetching more results',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'get_recurring',
|
|
94
|
+
description: 'Get recurring payments and subscriptions detected from transaction history.',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
since: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description: 'Filter recurring items detected since this date',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'get_snapshots',
|
|
107
|
+
description: 'Get net worth snapshots over time (daily or monthly aggregation).',
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
granularity: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
enum: ['daily', 'monthly'],
|
|
114
|
+
description: 'Aggregation level (default: daily)',
|
|
115
|
+
},
|
|
116
|
+
limit: {
|
|
117
|
+
type: 'number',
|
|
118
|
+
description: 'Number of snapshots to return',
|
|
119
|
+
},
|
|
120
|
+
since: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
description: 'Start date for snapshots (ISO format)',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'verify_key',
|
|
129
|
+
description: 'Check if the Warm API key is valid and working.',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
}));
|
|
137
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
138
|
+
const { name, arguments: args } = request.params;
|
|
139
|
+
try {
|
|
140
|
+
switch (name) {
|
|
141
|
+
case 'get_accounts': {
|
|
142
|
+
const params = {};
|
|
143
|
+
if (args?.since)
|
|
144
|
+
params.since = String(args.since);
|
|
145
|
+
const data = await apiRequest('/api/accounts', params);
|
|
146
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
147
|
+
}
|
|
148
|
+
case 'get_transactions': {
|
|
149
|
+
const params = {};
|
|
150
|
+
if (args?.limit)
|
|
151
|
+
params.limit = String(args.limit);
|
|
152
|
+
if (args?.since)
|
|
153
|
+
params.last_knowledge = String(args.since);
|
|
154
|
+
if (args?.cursor)
|
|
155
|
+
params.cursor = String(args.cursor);
|
|
156
|
+
const data = await apiRequest('/api/transactions', params);
|
|
157
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
158
|
+
}
|
|
159
|
+
case 'get_recurring': {
|
|
160
|
+
const params = {};
|
|
161
|
+
if (args?.since)
|
|
162
|
+
params.since = String(args.since);
|
|
163
|
+
const data = await apiRequest('/api/recurring', params);
|
|
164
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
165
|
+
}
|
|
166
|
+
case 'get_snapshots': {
|
|
167
|
+
// Fetch transactions which includes snapshots
|
|
168
|
+
const response = (await apiRequest('/api/transactions', {
|
|
169
|
+
limit: '1',
|
|
170
|
+
}));
|
|
171
|
+
const snapshots = response.snapshots || [];
|
|
172
|
+
const granularity = args?.granularity || 'daily';
|
|
173
|
+
const limit = args?.limit ? Number(args.limit) : granularity === 'daily' ? 100 : 0;
|
|
174
|
+
const since = args?.since;
|
|
175
|
+
let filtered = snapshots;
|
|
176
|
+
if (since) {
|
|
177
|
+
filtered = filtered.filter((s) => String(s.snapshot_date) >= since);
|
|
178
|
+
}
|
|
179
|
+
if (granularity === 'monthly') {
|
|
180
|
+
// Group by month, take last snapshot of each month
|
|
181
|
+
const byMonth = new Map();
|
|
182
|
+
filtered.forEach((s) => {
|
|
183
|
+
const month = String(s.snapshot_date).substring(0, 7);
|
|
184
|
+
if (!byMonth.has(month) ||
|
|
185
|
+
String(s.snapshot_date) > String(byMonth.get(month).snapshot_date)) {
|
|
186
|
+
byMonth.set(month, s);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
filtered = Array.from(byMonth.values());
|
|
190
|
+
}
|
|
191
|
+
// Sort descending and limit
|
|
192
|
+
filtered.sort((a, b) => String(b.snapshot_date).localeCompare(String(a.snapshot_date)));
|
|
193
|
+
if (limit > 0) {
|
|
194
|
+
filtered = filtered.slice(0, limit);
|
|
195
|
+
}
|
|
196
|
+
const result = {
|
|
197
|
+
granularity,
|
|
198
|
+
snapshots: filtered.map((s) => ({
|
|
199
|
+
date: s.snapshot_date,
|
|
200
|
+
...(granularity === 'monthly' && {
|
|
201
|
+
month: String(s.snapshot_date).substring(0, 7),
|
|
202
|
+
}),
|
|
203
|
+
net_worth: s.net_worth,
|
|
204
|
+
total_assets: s.total_assets,
|
|
205
|
+
total_liabilities: s.total_liabilities,
|
|
206
|
+
})),
|
|
207
|
+
};
|
|
208
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
209
|
+
}
|
|
210
|
+
case 'verify_key': {
|
|
211
|
+
const data = await apiRequest('/api/verify');
|
|
212
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
213
|
+
}
|
|
214
|
+
default:
|
|
215
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
220
|
+
return {
|
|
221
|
+
content: [{ type: 'text', text: JSON.stringify({ error: message }, null, 2) }],
|
|
222
|
+
isError: true,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
async function main() {
|
|
227
|
+
const transport = new StdioServerTransport();
|
|
228
|
+
await server.connect(transport);
|
|
229
|
+
}
|
|
230
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@warmio/mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Warm Financial API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"warm-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"warm",
|
|
20
|
+
"financial",
|
|
21
|
+
"api",
|
|
22
|
+
"claude"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.25.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"typescript": "^5.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|