@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 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
@@ -0,0 +1,8 @@
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
+ export {};
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
+ }