@warmio/mcp 5.0.0 → 6.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.
@@ -1,184 +1,192 @@
1
- type NormalizedAccountType = 'depository' | 'credit' | 'loan' | 'investment' | 'other';
2
1
  export interface WarmApiClientOptions {
3
2
  apiKeyResolver?: () => string | null;
4
3
  apiUrl?: string;
5
4
  fetchImplementation?: typeof fetch;
6
5
  requestTimeoutMs?: number;
7
6
  }
8
- export interface WarmApiAccount {
9
- account_id?: string | null;
10
- available_balance?: number | null;
11
- current_balance?: number | null;
12
- institution_name?: string | null;
13
- mask?: string | null;
14
- name?: string | null;
15
- subtype?: string | null;
16
- type?: string | null;
17
- }
18
- export interface WarmApiTransaction {
19
- account_id?: string | null;
20
- amount?: number | null;
21
- date?: string | null;
22
- detailed_category?: string | null;
23
- id?: string | null;
24
- merchant_name?: string | null;
25
- name?: string | null;
26
- primary_category?: string | null;
27
- }
28
- export interface WarmApiBudget {
29
- amount?: number | null;
30
- name?: string | null;
31
- period?: string | null;
32
- percent_used?: number | null;
33
- remaining?: number | null;
34
- spent?: number | null;
35
- status?: string | null;
36
- }
37
- export interface WarmApiGoal {
38
- category?: string | null;
39
- current?: number | null;
40
- monthly_contribution_needed?: number | null;
41
- name?: string | null;
42
- progress_percent?: number | null;
43
- status?: string | null;
44
- target?: number | null;
45
- target_date?: string | null;
46
- }
47
- export interface WarmFinancialState extends Record<string, unknown> {
48
- budgets: Array<{
49
- amount: number;
50
- name: string;
51
- percent_used: number;
52
- period: string;
53
- remaining: number;
54
- spent: number;
55
- status: string | null;
56
- }>;
57
- category_spending: Array<{
58
- amount: number;
59
- category: string;
60
- }>;
61
- generated_at: string;
62
- goals: Array<{
63
- category: string | null;
64
- current: number;
65
- monthly_contribution_needed: number | null;
66
- name: string;
67
- progress_percent: number;
68
- status: string | null;
69
- target: number;
70
- target_date: string | null;
71
- }>;
72
- health: {
73
- data_completeness: number | null;
74
- label: string | null;
75
- message: string | null;
76
- pillars: {
77
- borrow: number | null;
78
- build: number | null;
79
- save: number | null;
80
- spend: number | null;
81
- } | null;
82
- score: number | null;
83
- };
84
- holdings: Array<{
85
- account_id: string;
86
- cost_basis: number | null;
87
- quantity: number;
88
- security_name: string | null;
89
- symbol: string | null;
90
- type: string | null;
91
- value: number | null;
92
- }>;
93
- liabilities: Array<{
94
- account_id: string;
95
- apr_percentage: number | null;
96
- balance: number | null;
97
- is_overdue: boolean | null;
98
- minimum_payment: number | null;
99
- next_payment_due_date: string | null;
100
- type: string;
101
- }>;
102
- recurring: Array<{
103
- active: boolean;
104
- amount: number;
105
- frequency: string;
106
- merchant: string;
107
- next_date: string | null;
108
- type: string | null;
109
- }>;
110
- snapshots: Array<{
111
- date: string;
112
- net_worth: number;
113
- total_assets: number;
114
- total_liabilities: number;
7
+ export interface TransactionIndex {
8
+ total: number;
9
+ months: Array<{
10
+ month: string;
11
+ count: number;
115
12
  }>;
116
13
  }
117
- export interface GetTransactionsInput {
118
- cursor?: string;
119
- last_knowledge?: string;
120
- limit: number;
121
- search?: string;
122
- }
123
- export interface GetTransactionsOutput extends Record<string, unknown> {
124
- generated_at: string | null;
125
- next_knowledge: string | null;
126
- pagination: {
127
- has_more: boolean;
128
- limit: number;
129
- next_cursor: string | null;
130
- };
131
- txns: Array<{
132
- amount: number;
133
- category: string | null;
134
- date: string | null;
135
- description: string | null;
136
- detailed_category: string | null;
137
- id: string | null;
138
- merchant: string | null;
139
- }>;
14
+ export interface Position {
15
+ date: string | null;
16
+ net_worth: number | null;
17
+ cash: number | null;
18
+ debt: number | null;
19
+ investments: number | null;
20
+ other_assets: number | null;
21
+ total_assets: number | null;
140
22
  }
141
- export interface GetAccountsOutput extends Record<string, unknown> {
142
- accounts: Array<{
143
- balance: number;
144
- institution: string | null;
145
- mask: string | null;
146
- name: string;
147
- subtype: string | null;
148
- type: NormalizedAccountType;
149
- }>;
23
+ export interface Account {
24
+ id: string;
25
+ name: string;
26
+ type: 'depository' | 'credit' | 'loan' | 'investment' | 'brokerage' | 'other';
27
+ subtype: string | null;
28
+ group: 'cash' | 'debt' | 'investments' | null;
29
+ institution: string | null;
30
+ mask: string | null;
31
+ balance: number | null;
32
+ available: number | null;
33
+ currency: string | null;
34
+ updated_at: string | null;
35
+ }
36
+ export interface Status {
37
+ position: Position | null;
38
+ accounts: Account[];
39
+ }
40
+ export interface Recurring {
41
+ id: string;
42
+ account_id: string | null;
43
+ direction: 'inflow' | 'outflow' | null;
44
+ frequency: 'WEEKLY' | 'BIWEEKLY' | 'MONTHLY' | 'QUARTERLY' | 'SEMI_ANNUALLY' | 'ANNUALLY' | null;
45
+ status: 'active' | 'inactive' | 'dismissed' | null;
46
+ merchant: string | null;
47
+ amount: number | null;
48
+ next_date: string | null;
49
+ }
50
+ export interface Budget {
51
+ id: string;
52
+ name: string;
53
+ type: 'category' | 'merchant';
54
+ period: 'weekly' | 'biweekly' | 'monthly';
55
+ status: 'under' | 'warning' | 'over';
56
+ amount: number;
57
+ spent: number;
58
+ remaining: number;
59
+ used_percent: number;
60
+ }
61
+ export interface Goal {
62
+ id: string;
63
+ name: string;
64
+ category: string;
65
+ status: 'not_started' | 'in_progress' | 'on_track' | 'behind' | 'completed';
66
+ target: number;
67
+ current: number;
68
+ progress_percent: number;
69
+ target_date: string | null;
70
+ }
71
+ export interface Snapshot {
72
+ date: string;
73
+ net_worth: number | null;
74
+ cash: number | null;
75
+ debt: number | null;
76
+ income: number | null;
77
+ expenses: number | null;
78
+ cash_flow: number | null;
79
+ savings_rate: number | null;
80
+ investments: number | null;
81
+ assets: number | null;
82
+ health_score: number | null;
83
+ }
84
+ export interface Liability {
85
+ account_id: string | null;
86
+ type: 'credit' | 'student' | 'mortgage' | 'other' | null;
87
+ balance: number | null;
88
+ minimum_payment: number | null;
89
+ due_date: string | null;
90
+ interest_rate: number | null;
91
+ rate_type: 'fixed' | 'variable' | null;
92
+ overdue: boolean | null;
93
+ }
94
+ export interface Holding {
95
+ account_id: string | null;
96
+ symbol: string | null;
97
+ name: string | null;
98
+ type: 'cash' | 'derivative' | 'equity' | 'etf' | 'fixed_income' | 'mutual_fund' | 'other' | null;
99
+ quantity: number | null;
100
+ value: number | null;
101
+ cost_basis: number | null;
102
+ }
103
+ export interface Health {
104
+ score: number;
105
+ label: 'Needs Attention' | 'Good' | 'Strong';
106
+ level: 'critical' | 'poor' | 'fair' | 'good' | 'strong';
107
+ summary: string;
108
+ data_completeness: number;
109
+ pillars: {
110
+ spend: number | null;
111
+ save: number | null;
112
+ borrow: number | null;
113
+ build: number | null;
114
+ } | null;
115
+ }
116
+ export interface FinancialContext extends Record<string, unknown> {
117
+ version: 'v1';
118
+ updated_at: string;
119
+ currency: 'USD';
120
+ status: Status;
121
+ transactions: TransactionIndex;
122
+ recurring: Recurring[];
123
+ budgets: Budget[];
124
+ goals: Goal[];
125
+ snapshots: Snapshot[];
126
+ liabilities: Liability[];
127
+ holdings: Holding[];
128
+ health: Health | null;
129
+ }
130
+ export interface Transaction {
131
+ id: string;
132
+ account_id: string | null;
133
+ date: string | null;
134
+ amount: number;
135
+ merchant: string | null;
136
+ name: string | null;
137
+ category: string | null;
138
+ subcategory: string | null;
139
+ pending: boolean | null;
140
+ currency: string | null;
141
+ }
142
+ export interface TransactionMonth extends Record<string, unknown> {
143
+ month: string;
144
+ start_date: string;
145
+ end_date: string;
146
+ count: number;
147
+ items: Transaction[];
148
+ }
149
+ export interface LatestTransactions extends Record<string, unknown> {
150
+ since: string;
151
+ window_days: 10;
152
+ count: number;
153
+ items: Transaction[];
154
+ }
155
+ export type GetTransactionsInput = {
156
+ month: string;
157
+ latest?: never;
158
+ } | {
159
+ latest: true;
160
+ month?: never;
161
+ };
162
+ export type GetTransactionsOutput = TransactionMonth | LatestTransactions;
163
+ export interface FinancialContextMeta extends Record<string, unknown> {
164
+ version: 'v1';
165
+ user_id: string;
166
+ context_id: string;
167
+ generated_at: string;
168
+ updated_at: string;
169
+ content_hash: string;
170
+ byte_length: number;
171
+ counts: {
172
+ accounts: number;
173
+ transaction_months: number;
174
+ transactions: number;
175
+ snapshots: number;
176
+ };
150
177
  }
151
178
  export interface VerifyKeyOutput extends Record<string, unknown> {
152
179
  status: string;
153
180
  valid: boolean;
154
181
  }
155
182
  export interface WarmApiClient {
156
- getAccounts(): Promise<GetAccountsOutput>;
157
- getFinancialState(): Promise<WarmFinancialState>;
183
+ getFinancialContext(): Promise<FinancialContext>;
184
+ getFinancialContextMeta(): Promise<FinancialContextMeta>;
158
185
  getTransactions(input: GetTransactionsInput): Promise<GetTransactionsOutput>;
159
186
  verifyKey(): Promise<VerifyKeyOutput>;
160
187
  }
161
- export interface WarmApiLiability {
162
- account_id?: string | null;
163
- apr_percentage?: number | null;
164
- balance?: number | null;
165
- is_overdue?: boolean | null;
166
- minimum_payment?: number | null;
167
- next_payment_due_date?: string | null;
168
- type?: string | null;
169
- }
170
- export interface WarmApiHolding {
171
- account_id?: string | null;
172
- cost_basis?: number | null;
173
- quantity?: number | null;
174
- security_name?: string | null;
175
- symbol?: string | null;
176
- type?: string | null;
177
- value?: number | null;
178
- }
179
188
  export declare function getConfiguredApiKey(): string | null;
180
189
  export declare function resetConfiguredApiKeyCache(): void;
181
190
  export declare function apiRequest<TResponse>(endpoint: string, params?: Record<string, string | undefined>, options?: WarmApiClientOptions): Promise<TResponse>;
182
191
  export declare function createWarmApiClient(options?: WarmApiClientOptions): WarmApiClient;
183
192
  export declare function verifyWarmApiKey(apiKey: string): Promise<VerifyKeyOutput>;
184
- export {};
@@ -4,11 +4,7 @@ const DEFAULT_REQUEST_TIMEOUT_MS = (() => {
4
4
  const raw = Number(process.env.WARM_API_TIMEOUT_MS || 10_000);
5
5
  return Number.isFinite(raw) && raw > 0 ? raw : 10_000;
6
6
  })();
7
- const DEFAULT_SPENDING_MONTHS = 6;
8
7
  let cachedApiKey;
9
- function roundMoney(value) {
10
- return Math.round(value * 100) / 100;
11
- }
12
8
  function getRequestSignal(timeoutMs) {
13
9
  if (typeof AbortSignal.timeout === 'function') {
14
10
  return AbortSignal.timeout(timeoutMs);
@@ -18,125 +14,6 @@ function getRequestSignal(timeoutMs) {
18
14
  timer.unref?.();
19
15
  return controller.signal;
20
16
  }
21
- function normalizeAccountType(rawType) {
22
- switch (rawType) {
23
- case 'depository':
24
- case 'credit':
25
- case 'loan':
26
- case 'investment':
27
- return rawType;
28
- default:
29
- return 'other';
30
- }
31
- }
32
- function normalizeStateAccount(account) {
33
- return {
34
- available_balance: account.available_balance == null ? null : roundMoney(account.available_balance),
35
- balance: roundMoney(account.current_balance ?? 0),
36
- institution: account.institution_name ?? null,
37
- mask: account.mask ?? null,
38
- name: account.name || 'Unknown Account',
39
- subtype: account.subtype ?? null,
40
- type: normalizeAccountType(account.type),
41
- };
42
- }
43
- function normalizeAccounts(accounts) {
44
- return {
45
- accounts: accounts.map((account) => {
46
- const normalized = normalizeStateAccount(account);
47
- return {
48
- balance: normalized.balance,
49
- institution: normalized.institution,
50
- mask: normalized.mask,
51
- name: normalized.name,
52
- subtype: normalized.subtype,
53
- type: normalized.type,
54
- };
55
- }),
56
- };
57
- }
58
- function normalizeFinancialState(input) {
59
- return {
60
- budgets: (input.budgets || []).map((budget) => ({
61
- amount: budget.amount ?? 0,
62
- name: budget.name || '',
63
- percent_used: budget.percent_used ?? 0,
64
- period: budget.period || '',
65
- remaining: budget.remaining ?? 0,
66
- spent: budget.spent ?? 0,
67
- status: budget.status ?? null,
68
- })),
69
- category_spending: (input.categorySpending || []).map((item) => ({
70
- amount: roundMoney(item?.total ?? 0),
71
- category: item?.category || 'Uncategorized',
72
- })),
73
- generated_at: input.generatedAt,
74
- goals: (input.goals || []).map((goal) => ({
75
- category: goal.category ?? null,
76
- current: goal.current ?? 0,
77
- monthly_contribution_needed: goal.monthly_contribution_needed ?? null,
78
- name: goal.name || '',
79
- progress_percent: goal.progress_percent ?? 0,
80
- status: goal.status ?? null,
81
- target: goal.target ?? 0,
82
- target_date: goal.target_date ?? null,
83
- })),
84
- health: {
85
- data_completeness: input.health?.data_completeness ?? null,
86
- label: input.health?.label ?? null,
87
- message: input.health?.message ?? null,
88
- pillars: input.health?.pillars
89
- ? {
90
- borrow: input.health.pillars.borrow ?? null,
91
- build: input.health.pillars.build ?? null,
92
- save: input.health.pillars.save ?? null,
93
- spend: input.health.pillars.spend ?? null,
94
- }
95
- : null,
96
- score: input.health?.score ?? null,
97
- },
98
- holdings: (input.holdings || []).map((holding) => ({
99
- account_id: holding.account_id || '',
100
- cost_basis: holding.cost_basis ?? null,
101
- quantity: roundMoney(holding.quantity ?? 0),
102
- security_name: holding.security_name ?? null,
103
- symbol: holding.symbol ?? null,
104
- type: holding.type ?? null,
105
- value: holding.value == null ? null : roundMoney(holding.value),
106
- })),
107
- liabilities: (input.liabilities || []).map((liability) => ({
108
- account_id: liability.account_id || '',
109
- apr_percentage: liability.apr_percentage ?? null,
110
- balance: liability.balance == null ? null : roundMoney(liability.balance),
111
- is_overdue: liability.is_overdue ?? null,
112
- minimum_payment: liability.minimum_payment == null ? null : roundMoney(liability.minimum_payment),
113
- next_payment_due_date: liability.next_payment_due_date ?? null,
114
- type: liability.type || 'unknown',
115
- })),
116
- recurring: (input.recurring || []).map((stream) => ({
117
- active: stream.active === true || stream.is_active === true,
118
- amount: roundMoney(stream.amount ?? stream.last_amount ?? stream.average_amount ?? 0),
119
- frequency: stream.frequency || '',
120
- merchant: stream.merchant || stream.merchant_name || stream.description || '',
121
- next_date: stream.next_date ?? null,
122
- type: stream.type ?? stream.stream_type ?? null,
123
- })),
124
- snapshots: (input.snapshots || [])
125
- .map((snapshot) => ({
126
- date: snapshot.date || snapshot.snapshot_date || snapshot.period_end || null,
127
- net_worth: snapshot.net_worth ?? 0,
128
- total_assets: snapshot.total_assets ?? 0,
129
- total_liabilities: snapshot.total_liabilities ?? 0,
130
- }))
131
- .filter((snapshot) => typeof snapshot.date === 'string')
132
- .map((snapshot) => ({
133
- date: snapshot.date,
134
- net_worth: roundMoney(snapshot.net_worth),
135
- total_assets: roundMoney(snapshot.total_assets),
136
- total_liabilities: roundMoney(snapshot.total_liabilities),
137
- })),
138
- };
139
- }
140
17
  function createWarmApiClientConfig(options) {
141
18
  return {
142
19
  apiKeyResolver: options.apiKeyResolver,
@@ -193,7 +70,7 @@ export async function apiRequest(endpoint, params = {}, options = {}) {
193
70
  if (!response.ok) {
194
71
  const errorMessages = {
195
72
  401: 'Invalid or expired API key. Regenerate at https://warm.io/settings',
196
- 403: 'Pro subscription required. Upgrade at https://warm.io/settings',
73
+ 403: 'API access is available on paid plans only. Upgrade at https://warm.io/settings',
197
74
  429: 'Rate limit exceeded. Try again in a few minutes.',
198
75
  };
199
76
  if (errorMessages[response.status]) {
@@ -214,69 +91,20 @@ export async function apiRequest(endpoint, params = {}, options = {}) {
214
91
  return (await response.json());
215
92
  }
216
93
  export function createWarmApiClient(options = {}) {
217
- const getAccounts = async () => {
218
- const response = await apiRequest('/api/export', { dataset: 'accounts' }, options);
219
- return normalizeAccounts(response.accounts || []);
220
- };
221
- const getFinancialState = async () => {
222
- const [snapshotsResponse, recurringResponse, budgetsResponse, goalsResponse, healthResponse, liabilitiesResponse, holdingsResponse, spendingResponse,] = await Promise.all([
223
- apiRequest('/api/export', { dataset: 'snapshots' }, options),
224
- apiRequest('/api/export', { dataset: 'recurring' }, options),
225
- apiRequest('/api/export', { dataset: 'budgets' }, options),
226
- apiRequest('/api/export', { dataset: 'goals' }, options),
227
- apiRequest('/api/export', { dataset: 'health' }, options),
228
- apiRequest('/api/export', { dataset: 'liabilities' }, options),
229
- apiRequest('/api/export', { dataset: 'holdings' }, options),
230
- apiRequest('/api/spending', { months: String(DEFAULT_SPENDING_MONTHS) }, options),
231
- ]);
232
- const generatedAt = spendingResponse.generated_at ||
233
- holdingsResponse.generated_at ||
234
- liabilitiesResponse.generated_at ||
235
- healthResponse.generated_at ||
236
- goalsResponse.generated_at ||
237
- budgetsResponse.generated_at ||
238
- recurringResponse.generated_at ||
239
- snapshotsResponse.generated_at ||
240
- new Date().toISOString();
241
- return normalizeFinancialState({
242
- budgets: budgetsResponse.budgets,
243
- categorySpending: spendingResponse.spending,
244
- generatedAt,
245
- goals: goalsResponse.goals,
246
- health: healthResponse,
247
- holdings: holdingsResponse.holdings,
248
- liabilities: liabilitiesResponse.liabilities,
249
- recurring: recurringResponse.recurring || recurringResponse.recurring_transactions,
250
- snapshots: snapshotsResponse.snapshots,
251
- });
252
- };
94
+ const getFinancialContext = async () => await apiRequest('/api/financial-context', {}, options);
95
+ const getFinancialContextMeta = async () => await apiRequest('/api/financial-context/meta', {}, options);
253
96
  const getTransactions = async (input) => {
254
- const response = await apiRequest('/api/export', {
255
- cursor: input.cursor,
256
- dataset: 'transactions',
257
- last_knowledge: input.last_knowledge,
258
- limit: String(input.limit),
259
- search: input.search,
260
- }, options);
261
- const nextCursor = response.pagination?.next_cursor ?? null;
262
- return {
263
- generated_at: response.generated_at ?? null,
264
- next_knowledge: response.next_knowledge ?? null,
265
- pagination: {
266
- has_more: nextCursor !== null,
267
- limit: response.pagination?.limit ?? input.limit,
268
- next_cursor: nextCursor,
269
- },
270
- txns: (response.transactions || []).map((transaction) => ({
271
- amount: roundMoney(transaction.amount ?? 0),
272
- category: transaction.primary_category ?? null,
273
- date: transaction.date ?? null,
274
- description: transaction.name ?? null,
275
- detailed_category: transaction.detailed_category ?? null,
276
- id: transaction.id ?? null,
277
- merchant: transaction.merchant_name ?? null,
278
- })),
279
- };
97
+ const rawInput = input;
98
+ if (typeof rawInput.month === 'string' && rawInput.latest !== undefined) {
99
+ throw new Error('`month` and `latest` are mutually exclusive.');
100
+ }
101
+ if (typeof rawInput.month === 'string') {
102
+ return await apiRequest('/api/financial-context/transactions', { month: rawInput.month, latest: undefined }, options);
103
+ }
104
+ if (rawInput.latest === true) {
105
+ return await apiRequest('/api/financial-context/transactions', { month: undefined, latest: '1' }, options);
106
+ }
107
+ throw new Error('Call getTransactions with `month` in YYYY-MM format or `latest: true`.');
280
108
  };
281
109
  const verifyKey = async () => {
282
110
  const response = await apiRequest('/api/verify', {}, options);
@@ -286,8 +114,8 @@ export function createWarmApiClient(options = {}) {
286
114
  };
287
115
  };
288
116
  return {
289
- getAccounts,
290
- getFinancialState,
117
+ getFinancialContext,
118
+ getFinancialContextMeta,
291
119
  getTransactions,
292
120
  verifyKey,
293
121
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warmio/mcp",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Warm CLI and MCP server for personal financial data",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,6 +34,7 @@
34
34
  ],
35
35
  "scripts": {
36
36
  "build": "tsc",
37
+ "test": "npm run build && node --test test/*.mjs",
37
38
  "start": "node dist/index.js mcp",
38
39
  "start:http": "node dist/index.js http",
39
40
  "start:install": "node dist/index.js install",