@warmio/mcp 1.0.0 → 1.0.1

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 CHANGED
@@ -2,97 +2,53 @@
2
2
 
3
3
  MCP server that gives Claude Code (or any MCP client) read-only access to your Warm financial data.
4
4
 
5
- ## Quick Install
5
+ ## Quick Start
6
6
 
7
7
  ```bash
8
8
  npx @warmio/mcp
9
9
  ```
10
10
 
11
- Works on macOS, Linux, and Windows. Auto-configures: Claude Code, Cursor, Windsurf, Claude Desktop, OpenCode, Codex CLI, Antigravity, Gemini CLI.
11
+ Detects installed MCP clients, prompts for your API key, and configures everything automatically.
12
+ Works on macOS, Linux, and Windows. Supports: Claude Code, Claude Desktop, Cursor, Windsurf, OpenCode, Codex CLI, Antigravity, Gemini CLI.
12
13
 
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"]
14
+ After setup, open your MCP client and ask:
15
+ - "What's my net worth?"
16
+ - "How much did I spend on restaurants last month?"
17
+ - "Show me my subscriptions"
56
18
 
57
- [mcp_servers.warm.env]
58
- WARM_API_KEY = "your-api-key"
59
- ```
19
+ ## Options
60
20
 
61
- Get your API key from [warm.io/settings](https://warm.io/settings) → API Keys.
21
+ | Command | Description |
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) |
62
26
 
63
27
  ## Requirements
64
28
 
65
29
  - **Pro subscription** — API access requires Warm Pro
66
- - **API key** — Generate in Settings → API Keys
30
+ - **API key** — Generate in [Settings → API Keys](https://warm.io/settings)
67
31
  - **Node.js 18+** — For running the MCP server
68
32
 
69
- ## Available Tools
33
+ ## How It Works
70
34
 
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 |
35
+ 1. You run `npx @warmio/mcp` once
36
+ 2. It detects which MCP clients you have installed
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
78
40
 
79
- ## Usage
41
+ The MCP server starts automatically when your client needs it — you never run it manually.
80
42
 
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
43
+ ## Available Tools
91
44
 
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` |
45
+ | Tool | Description |
46
+ |------|-------------|
47
+ | `get_accounts` | List all connected bank accounts with balances |
48
+ | `get_transactions` | Get transactions with date/limit filters |
49
+ | `get_recurring` | Show subscriptions and recurring payments |
50
+ | `get_snapshots` | Net worth history (daily or monthly) |
51
+ | `verify_key` | Check if API key is valid |
96
52
 
97
53
  ## Security
98
54
 
package/dist/index.d.ts CHANGED
@@ -1,8 +1,2 @@
1
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
2
  export {};
package/dist/index.js CHANGED
@@ -1,230 +1,10 @@
1
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
- }
2
+ const args = process.argv.slice(2);
3
+ if (args.includes('--server')) {
4
+ await import('./server.js');
28
5
  }
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();
6
+ else {
7
+ const { install } = await import('./install.js');
8
+ await install();
54
9
  }
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);
10
+ export {};
@@ -0,0 +1 @@
1
+ export declare function install(): Promise<void>;
@@ -0,0 +1,140 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { homedir, platform } from 'os';
4
+ import { createInterface } from 'readline';
5
+ const HOME = homedir();
6
+ function getClaudeDesktopPath() {
7
+ if (platform() === 'win32') {
8
+ return join(process.env.APPDATA || join(HOME, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
9
+ }
10
+ if (platform() === 'darwin') {
11
+ return join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
12
+ }
13
+ return join(HOME, '.config', 'claude', 'claude_desktop_config.json');
14
+ }
15
+ const ALL_CLIENTS = [
16
+ { name: 'Claude Code', configPath: join(HOME, '.claude.json'), format: 'json', alwaysInclude: true },
17
+ { name: 'Claude Desktop', configPath: getClaudeDesktopPath(), format: 'json' },
18
+ { name: 'Cursor', configPath: join(HOME, '.cursor', 'mcp.json'), format: 'json' },
19
+ { name: 'Windsurf', configPath: join(HOME, '.codeium', 'windsurf', 'mcp_config.json'), format: 'json' },
20
+ { name: 'OpenCode', configPath: join(HOME, '.config', 'opencode', 'opencode.json'), format: 'json' },
21
+ { name: 'Codex CLI', configPath: join(HOME, '.codex', 'config.toml'), format: 'toml' },
22
+ { name: 'Antigravity', configPath: join(HOME, '.gemini', 'antigravity', 'mcp_config.json'), format: 'json' },
23
+ { name: 'Gemini CLI', configPath: join(HOME, '.gemini', 'settings.json'), format: 'json' },
24
+ ];
25
+ const MCP_CONFIG = {
26
+ command: 'npx',
27
+ args: ['-y', '@warmio/mcp', '--server'],
28
+ };
29
+ function isDetected(client) {
30
+ if (client.alwaysInclude)
31
+ return true;
32
+ return existsSync(dirname(client.configPath));
33
+ }
34
+ function isConfigured(client) {
35
+ if (!existsSync(client.configPath))
36
+ return false;
37
+ try {
38
+ const content = readFileSync(client.configPath, 'utf-8');
39
+ if (client.format === 'toml')
40
+ return content.includes('[mcp_servers.warm]');
41
+ return !!JSON.parse(content)?.mcpServers?.warm;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ function configureJson(client, apiKey) {
48
+ let config = {};
49
+ if (existsSync(client.configPath)) {
50
+ try {
51
+ config = JSON.parse(readFileSync(client.configPath, 'utf-8'));
52
+ }
53
+ catch { /* start fresh */ }
54
+ }
55
+ if (!config.mcpServers)
56
+ config.mcpServers = {};
57
+ config.mcpServers.warm = {
58
+ ...MCP_CONFIG,
59
+ env: { WARM_API_KEY: apiKey },
60
+ };
61
+ mkdirSync(dirname(client.configPath), { recursive: true });
62
+ writeFileSync(client.configPath, JSON.stringify(config, null, 2) + '\n');
63
+ }
64
+ function configureToml(client, apiKey) {
65
+ let content = '';
66
+ if (existsSync(client.configPath)) {
67
+ content = readFileSync(client.configPath, 'utf-8');
68
+ if (!content.endsWith('\n'))
69
+ content += '\n';
70
+ }
71
+ content += `\n[mcp_servers.warm]\ncommand = "npx"\nargs = ["-y", "@warmio/mcp", "--server"]\n\n[mcp_servers.warm.env]\nWARM_API_KEY = "${apiKey}"\n`;
72
+ mkdirSync(dirname(client.configPath), { recursive: true });
73
+ writeFileSync(client.configPath, content);
74
+ }
75
+ function configure(client, apiKey) {
76
+ if (client.format === 'json')
77
+ configureJson(client, apiKey);
78
+ else
79
+ configureToml(client, apiKey);
80
+ }
81
+ function shortPath(p) {
82
+ return p.replace(HOME, '~');
83
+ }
84
+ function prompt(question) {
85
+ return new Promise((resolve) => {
86
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
87
+ rl.question(question, (answer) => {
88
+ rl.close();
89
+ resolve(answer.trim());
90
+ });
91
+ });
92
+ }
93
+ export async function install() {
94
+ const force = process.argv.includes('--force');
95
+ console.log('');
96
+ console.log(' Warm MCP Server Installer');
97
+ console.log(' -------------------------');
98
+ console.log('');
99
+ const detected = ALL_CLIENTS.filter(isDetected);
100
+ const needsSetup = detected.filter((c) => !isConfigured(c) || force);
101
+ // Show detected clients and their status
102
+ console.log(' MCP clients found:');
103
+ detected.forEach((client) => {
104
+ const configured = isConfigured(client);
105
+ const status = configured && !force ? 'configured' : 'not configured';
106
+ console.log(` ${client.name.padEnd(18)} ${shortPath(client.configPath).padEnd(60)} ${status}`);
107
+ });
108
+ console.log('');
109
+ // Nothing to do
110
+ if (needsSetup.length === 0) {
111
+ console.log(' All clients already configured!');
112
+ console.log(' Run with --force to update the API key.');
113
+ console.log('');
114
+ return;
115
+ }
116
+ // Prompt for API key
117
+ const apiKey = await prompt(' Warm API key: ');
118
+ if (!apiKey) {
119
+ console.log('');
120
+ console.log(' No key provided. Get one at https://warm.io/settings');
121
+ console.log('');
122
+ return;
123
+ }
124
+ console.log('');
125
+ console.log(' Configuring...');
126
+ console.log('');
127
+ needsSetup.forEach((client) => {
128
+ try {
129
+ configure(client, apiKey);
130
+ console.log(` ${client.name.padEnd(18)} done`);
131
+ }
132
+ catch (err) {
133
+ console.log(` ${client.name.padEnd(18)} failed: ${err instanceof Error ? err.message : String(err)}`);
134
+ }
135
+ });
136
+ console.log('');
137
+ console.log(' All set! Restart your MCP clients and try:');
138
+ console.log(' "What\'s my net worth?"');
139
+ console.log('');
140
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Warm MCP Server
3
+ *
4
+ * Provides financial data from the Warm API as MCP tools.
5
+ * Reads API key from WARM_API_KEY env var or ~/.config/warm/api_key.
6
+ */
7
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Warm MCP Server
3
+ *
4
+ * Provides financial data from the Warm API as MCP tools.
5
+ * Reads API key from WARM_API_KEY env var or ~/.config/warm/api_key.
6
+ */
7
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import * as os from 'os';
13
+ const API_URL = process.env.WARM_API_URL || 'https://warm.io';
14
+ function getApiKey() {
15
+ if (process.env.WARM_API_KEY) {
16
+ return process.env.WARM_API_KEY;
17
+ }
18
+ const configPath = path.join(os.homedir(), '.config', 'warm', 'api_key');
19
+ try {
20
+ return fs.readFileSync(configPath, 'utf-8').trim();
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ async function apiRequest(endpoint, params = {}) {
27
+ const apiKey = getApiKey();
28
+ if (!apiKey) {
29
+ throw new Error('WARM_API_KEY not set. Run "npx @warmio/mcp" to configure.');
30
+ }
31
+ const url = new URL(endpoint, API_URL);
32
+ Object.entries(params).forEach(([key, value]) => {
33
+ if (value)
34
+ url.searchParams.append(key, value);
35
+ });
36
+ const response = await fetch(url.toString(), {
37
+ headers: {
38
+ Authorization: `Bearer ${apiKey}`,
39
+ Accept: 'application/json',
40
+ },
41
+ });
42
+ if (!response.ok) {
43
+ const errorMessages = {
44
+ 401: 'Invalid or expired API key. Regenerate at https://warm.io/settings',
45
+ 403: 'Pro subscription required. Upgrade at https://warm.io/settings',
46
+ 429: 'Rate limit exceeded. Try again in a few minutes.',
47
+ };
48
+ throw new Error(errorMessages[response.status] || `HTTP ${response.status}`);
49
+ }
50
+ return response.json();
51
+ }
52
+ const server = new Server({ name: 'warm', version: '1.0.1' }, { capabilities: { tools: {} } });
53
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
54
+ tools: [
55
+ {
56
+ name: 'get_accounts',
57
+ description: 'Get all connected bank accounts with balances. Returns account names, types, balances, and institutions.',
58
+ inputSchema: {
59
+ type: 'object',
60
+ properties: {
61
+ since: {
62
+ type: 'string',
63
+ description: 'Filter accounts updated since this date (ISO format)',
64
+ },
65
+ },
66
+ },
67
+ },
68
+ {
69
+ name: 'get_transactions',
70
+ description: 'Get transactions from connected accounts. Supports pagination and date filtering.',
71
+ inputSchema: {
72
+ type: 'object',
73
+ properties: {
74
+ limit: {
75
+ type: 'number',
76
+ description: 'Maximum number of transactions to return (default: 100)',
77
+ },
78
+ since: {
79
+ type: 'string',
80
+ description: 'Get transactions since this date (ISO format, e.g., 2024-01-01)',
81
+ },
82
+ cursor: {
83
+ type: 'string',
84
+ description: 'Pagination cursor for fetching more results',
85
+ },
86
+ },
87
+ },
88
+ },
89
+ {
90
+ name: 'get_recurring',
91
+ description: 'Get recurring payments and subscriptions detected from transaction history.',
92
+ inputSchema: {
93
+ type: 'object',
94
+ properties: {
95
+ since: {
96
+ type: 'string',
97
+ description: 'Filter recurring items detected since this date',
98
+ },
99
+ },
100
+ },
101
+ },
102
+ {
103
+ name: 'get_snapshots',
104
+ description: 'Get net worth snapshots over time (daily or monthly aggregation).',
105
+ inputSchema: {
106
+ type: 'object',
107
+ properties: {
108
+ granularity: {
109
+ type: 'string',
110
+ enum: ['daily', 'monthly'],
111
+ description: 'Aggregation level (default: daily)',
112
+ },
113
+ limit: {
114
+ type: 'number',
115
+ description: 'Number of snapshots to return',
116
+ },
117
+ since: {
118
+ type: 'string',
119
+ description: 'Start date for snapshots (ISO format)',
120
+ },
121
+ },
122
+ },
123
+ },
124
+ {
125
+ name: 'verify_key',
126
+ description: 'Check if the Warm API key is valid and working.',
127
+ inputSchema: {
128
+ type: 'object',
129
+ properties: {},
130
+ },
131
+ },
132
+ ],
133
+ }));
134
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
135
+ const { name, arguments: args } = request.params;
136
+ try {
137
+ switch (name) {
138
+ case 'get_accounts': {
139
+ const params = {};
140
+ if (args?.since)
141
+ params.since = String(args.since);
142
+ const data = await apiRequest('/api/accounts', params);
143
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
144
+ }
145
+ case 'get_transactions': {
146
+ const params = {};
147
+ if (args?.limit)
148
+ params.limit = String(args.limit);
149
+ if (args?.since)
150
+ params.last_knowledge = String(args.since);
151
+ if (args?.cursor)
152
+ params.cursor = String(args.cursor);
153
+ const data = await apiRequest('/api/transactions', params);
154
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
155
+ }
156
+ case 'get_recurring': {
157
+ const params = {};
158
+ if (args?.since)
159
+ params.since = String(args.since);
160
+ const data = await apiRequest('/api/recurring', params);
161
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
162
+ }
163
+ case 'get_snapshots': {
164
+ const response = (await apiRequest('/api/transactions', {
165
+ limit: '1',
166
+ }));
167
+ const snapshots = response.snapshots || [];
168
+ const granularity = args?.granularity || 'daily';
169
+ const limit = args?.limit ? Number(args.limit) : granularity === 'daily' ? 100 : 0;
170
+ const since = args?.since;
171
+ let filtered = snapshots;
172
+ if (since) {
173
+ filtered = filtered.filter((s) => String(s.snapshot_date) >= since);
174
+ }
175
+ if (granularity === 'monthly') {
176
+ const byMonth = new Map();
177
+ filtered.forEach((s) => {
178
+ const month = String(s.snapshot_date).substring(0, 7);
179
+ if (!byMonth.has(month) ||
180
+ String(s.snapshot_date) > String(byMonth.get(month).snapshot_date)) {
181
+ byMonth.set(month, s);
182
+ }
183
+ });
184
+ filtered = Array.from(byMonth.values());
185
+ }
186
+ filtered.sort((a, b) => String(b.snapshot_date).localeCompare(String(a.snapshot_date)));
187
+ if (limit > 0) {
188
+ filtered = filtered.slice(0, limit);
189
+ }
190
+ const result = {
191
+ granularity,
192
+ snapshots: filtered.map((s) => ({
193
+ date: s.snapshot_date,
194
+ ...(granularity === 'monthly' && {
195
+ month: String(s.snapshot_date).substring(0, 7),
196
+ }),
197
+ net_worth: s.net_worth,
198
+ total_assets: s.total_assets,
199
+ total_liabilities: s.total_liabilities,
200
+ })),
201
+ };
202
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
203
+ }
204
+ case 'verify_key': {
205
+ const data = await apiRequest('/api/verify');
206
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
207
+ }
208
+ default:
209
+ throw new Error(`Unknown tool: ${name}`);
210
+ }
211
+ }
212
+ catch (error) {
213
+ const message = error instanceof Error ? error.message : String(error);
214
+ return {
215
+ content: [{ type: 'text', text: JSON.stringify({ error: message }, null, 2) }],
216
+ isError: true,
217
+ };
218
+ }
219
+ });
220
+ const transport = new StdioServerTransport();
221
+ await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warmio/mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "MCP server for Warm Financial API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",