@warmio/mcp 4.2.2 → 4.3.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.
@@ -1,245 +1,2 @@
1
- import * as fs from 'node:fs';
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import { emptyInputSchema, getAccountsOutputSchema, getFinancialStateOutputSchema, getTransactionsInputSchema, getTransactionsOutputSchema, verifyKeyOutputSchema, } from './schemas.js';
4
- import { getWarmApiKeyPath } from './config-paths.js';
5
- const DEFAULT_API_URL = process.env.WARM_API_URL || 'https://warm.io';
6
- const DEFAULT_REQUEST_TIMEOUT_MS = (() => {
7
- const raw = Number(process.env.WARM_API_TIMEOUT_MS || 10_000);
8
- return Number.isFinite(raw) && raw > 0 ? raw : 10_000;
9
- })();
10
- const READ_ONLY_TOOL_ANNOTATIONS = {
11
- readOnlyHint: true,
12
- destructiveHint: false,
13
- idempotentHint: true,
14
- openWorldHint: false,
15
- };
16
- let cachedApiKey;
17
- export const WARM_SERVER_INFO = {
18
- name: 'warm',
19
- version: getPackageVersion(),
20
- };
21
- export const API_URL = DEFAULT_API_URL;
22
- function getPackageVersion() {
23
- try {
24
- const packageJson = fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8');
25
- const parsed = JSON.parse(packageJson);
26
- return parsed.version || '0.0.0';
27
- }
28
- catch {
29
- return '0.0.0';
30
- }
31
- }
32
- function roundMoney(value) {
33
- return Math.round(value * 100) / 100;
34
- }
35
- function getRequestSignal(timeoutMs) {
36
- if (typeof AbortSignal.timeout === 'function') {
37
- return AbortSignal.timeout(timeoutMs);
38
- }
39
- const controller = new AbortController();
40
- const timer = setTimeout(() => controller.abort(), timeoutMs);
41
- timer.unref?.();
42
- return controller.signal;
43
- }
44
- function normalizeAccountType(rawType) {
45
- switch (rawType) {
46
- case 'depository':
47
- case 'credit':
48
- case 'loan':
49
- case 'investment':
50
- return rawType;
51
- default:
52
- return 'other';
53
- }
54
- }
55
- function normalizeAccount(account) {
56
- return {
57
- name: account.name || 'Unknown Account',
58
- type: normalizeAccountType(account.type),
59
- subtype: account.subtype ?? null,
60
- balance: roundMoney(account.current_balance ?? 0),
61
- institution: account.institution_name ?? null,
62
- mask: account.mask ?? null,
63
- };
64
- }
65
- function createWarmApiClientConfig(options) {
66
- return {
67
- apiUrl: options.apiUrl || DEFAULT_API_URL,
68
- apiKeyResolver: options.apiKeyResolver,
69
- fetchImplementation: options.fetchImplementation || fetch,
70
- requestTimeoutMs: options.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
71
- };
72
- }
73
- export function getConfiguredApiKey() {
74
- if (cachedApiKey !== undefined) {
75
- return cachedApiKey;
76
- }
77
- if (process.env.WARM_API_KEY?.trim()) {
78
- cachedApiKey = process.env.WARM_API_KEY.trim();
79
- return cachedApiKey;
80
- }
81
- const configPath = getWarmApiKeyPath();
82
- try {
83
- cachedApiKey = fs.readFileSync(configPath, 'utf-8').trim() || null;
84
- }
85
- catch {
86
- cachedApiKey = null;
87
- }
88
- return cachedApiKey;
89
- }
90
- export async function apiRequest(endpoint, params = {}, options = {}) {
91
- const requestOptions = createWarmApiClientConfig(options);
92
- const apiKey = requestOptions.apiKeyResolver?.() ?? getConfiguredApiKey();
93
- if (!apiKey) {
94
- throw new Error('WARM_API_KEY not set. Run "npx @warmio/mcp" to configure.');
95
- }
96
- const url = new URL(endpoint, requestOptions.apiUrl);
97
- Object.entries(params).forEach(([key, value]) => {
98
- if (value) {
99
- url.searchParams.append(key, value);
100
- }
101
- });
102
- let response;
103
- try {
104
- response = await requestOptions.fetchImplementation(url.toString(), {
105
- headers: {
106
- Authorization: `Bearer ${apiKey}`,
107
- Accept: 'application/json',
108
- },
109
- signal: getRequestSignal(requestOptions.requestTimeoutMs),
110
- });
111
- }
112
- catch (error) {
113
- if (error instanceof Error && error.name === 'TimeoutError') {
114
- throw new Error(`Warm API timed out after ${requestOptions.requestTimeoutMs}ms`);
115
- }
116
- if (error instanceof Error && error.name === 'AbortError') {
117
- throw new Error(`Warm API request aborted after ${requestOptions.requestTimeoutMs}ms`);
118
- }
119
- throw error;
120
- }
121
- if (!response.ok) {
122
- const errorMessages = {
123
- 401: 'Invalid or expired API key. Regenerate at https://warm.io/settings',
124
- 403: 'Pro subscription required. Upgrade at https://warm.io/settings',
125
- 429: 'Rate limit exceeded. Try again in a few minutes.',
126
- };
127
- if (errorMessages[response.status]) {
128
- throw new Error(errorMessages[response.status]);
129
- }
130
- let detail = `HTTP ${response.status}`;
131
- try {
132
- const body = (await response.json());
133
- if (body?.error) {
134
- detail = body.error;
135
- }
136
- }
137
- catch {
138
- // Ignore response parse failures and fall back to status text.
139
- }
140
- throw new Error(detail);
141
- }
142
- return (await response.json());
143
- }
144
- function createStructuredToolResult(structuredContent) {
145
- return {
146
- content: [
147
- {
148
- type: 'text',
149
- text: JSON.stringify(structuredContent, null, 2),
150
- },
151
- ],
152
- structuredContent,
153
- };
154
- }
155
- export function createWarmApiClient(options = {}) {
156
- async function getAccounts() {
157
- const response = await apiRequest('/api/export', { dataset: 'accounts' }, options);
158
- return {
159
- accounts: (response.accounts || []).map(normalizeAccount),
160
- };
161
- }
162
- async function getTransactions(input) {
163
- const response = await apiRequest('/api/transactions', {
164
- limit: String(input.limit),
165
- cursor: input.cursor,
166
- last_knowledge: input.last_knowledge,
167
- search: input.search,
168
- }, options);
169
- const nextCursor = response.pagination?.next_cursor ?? null;
170
- return {
171
- generated_at: response.generated_at ?? null,
172
- next_knowledge: response.next_knowledge ?? null,
173
- txns: (response.transactions || []).map((transaction) => ({
174
- id: transaction.id ?? null,
175
- date: transaction.date ?? null,
176
- amount: roundMoney(transaction.amount ?? 0),
177
- merchant: transaction.merchant_name ?? null,
178
- description: transaction.name ?? null,
179
- category: transaction.primary_category ?? null,
180
- detailed_category: transaction.detailed_category ?? null,
181
- })),
182
- pagination: {
183
- limit: response.pagination?.limit ?? input.limit,
184
- next_cursor: nextCursor,
185
- has_more: nextCursor !== null,
186
- },
187
- };
188
- }
189
- async function getFinancialState() {
190
- return apiRequest('/api/financial-state', {}, options);
191
- }
192
- async function verifyKey() {
193
- const response = await apiRequest('/api/verify', {}, options);
194
- return {
195
- valid: response.valid === true,
196
- status: response.status || (response.valid ? 'ok' : 'invalid'),
197
- };
198
- }
199
- return {
200
- getAccounts,
201
- getTransactions,
202
- getFinancialState,
203
- verifyKey,
204
- };
205
- }
206
- export function registerWarmTools(server, options = {}) {
207
- const client = createWarmApiClient(options);
208
- return {
209
- get_accounts: server.registerTool('get_accounts', {
210
- description: 'Read-only list of connected financial accounts with balances, subtypes, institutions, and masks when available.',
211
- inputSchema: emptyInputSchema,
212
- outputSchema: getAccountsOutputSchema,
213
- annotations: READ_ONLY_TOOL_ANNOTATIONS,
214
- }, async () => createStructuredToolResult(await client.getAccounts())),
215
- get_transactions: server.registerTool('get_transactions', {
216
- description: 'Read-only transaction export with strict opaque cursor pagination and optional last_knowledge incremental sync.',
217
- inputSchema: getTransactionsInputSchema,
218
- outputSchema: getTransactionsOutputSchema,
219
- annotations: READ_ONLY_TOOL_ANNOTATIONS,
220
- }, async (args) => createStructuredToolResult(await client.getTransactions(args))),
221
- get_financial_state: server.registerTool('get_financial_state', {
222
- description: 'Read-only broad financial state bundle with snapshots, recurring payments, budgets, goals, financial health, liabilities, holdings, and category spending.',
223
- inputSchema: emptyInputSchema,
224
- outputSchema: getFinancialStateOutputSchema,
225
- annotations: READ_ONLY_TOOL_ANNOTATIONS,
226
- }, async () => createStructuredToolResult(await client.getFinancialState())),
227
- verify_key: server.registerTool('verify_key', {
228
- description: 'Read-only API key validation for the configured Warm account.',
229
- inputSchema: emptyInputSchema,
230
- outputSchema: verifyKeyOutputSchema,
231
- annotations: READ_ONLY_TOOL_ANNOTATIONS,
232
- }, async () => createStructuredToolResult(await client.verifyKey())),
233
- };
234
- }
235
- export function createWarmServer(options = {}) {
236
- const server = new McpServer(options.serverInfo || WARM_SERVER_INFO);
237
- registerWarmTools(server, options);
238
- return server;
239
- }
240
- export async function verifyWarmApiKey(apiKey) {
241
- const client = createWarmApiClient({
242
- apiKeyResolver: () => apiKey,
243
- });
244
- return client.verifyKey();
245
- }
1
+ export * from './server.js';
2
+ export * from './warm-api-client.js';
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@warmio/mcp",
3
- "version": "4.2.2",
4
- "description": "MCP server for Warm Financial API",
3
+ "version": "4.3.1",
4
+ "description": "Warm MCP server for local stdio and Streamable HTTP access",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
8
11
  "exports": {
9
12
  ".": {
10
13
  "types": "./dist/index.d.ts",
@@ -14,6 +17,10 @@
14
17
  "types": "./dist/server.d.ts",
15
18
  "default": "./dist/server.js"
16
19
  },
20
+ "./warm-api-client": {
21
+ "types": "./dist/warm-api-client.d.ts",
22
+ "default": "./dist/warm-api-client.js"
23
+ },
17
24
  "./install": {
18
25
  "types": "./dist/install.d.ts",
19
26
  "default": "./dist/install.js"
@@ -28,6 +35,7 @@
28
35
  "scripts": {
29
36
  "build": "tsc",
30
37
  "start": "node dist/index.js --stdio",
38
+ "start:http": "node dist/index.js http",
31
39
  "start:install": "node dist/index.js install",
32
40
  "prepublishOnly": "npm run build"
33
41
  },
@@ -40,11 +48,12 @@
40
48
  ],
41
49
  "license": "MIT",
42
50
  "dependencies": {
43
- "@modelcontextprotocol/sdk": "^1.25.0",
44
- "zod": "^4.3.5"
51
+ "@modelcontextprotocol/sdk": "^1.27.1",
52
+ "dotenv": "^17.2.3",
53
+ "zod": "^3.25.76"
45
54
  },
46
55
  "devDependencies": {
47
- "@types/node": "^22.0.0",
48
- "typescript": "^5.0.0"
56
+ "@types/node": "^24.10.9",
57
+ "typescript": "^5.9.3"
49
58
  }
50
59
  }