@warmio/mcp 6.0.0 → 7.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/dist/server.d.ts CHANGED
@@ -1,15 +1,17 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { type Implementation } from '@modelcontextprotocol/sdk/types.js';
3
- import { createWarmApiClient, type WarmApiClientOptions, getConfiguredApiKey, verifyWarmApiKey, apiRequest } from './warm-api-client.js';
4
- export { apiRequest, createWarmApiClient, getConfiguredApiKey, verifyWarmApiKey };
3
+ import { createWarmApiClient, type WarmApiClientOptions, getConfiguredApiKey, verifyWarmApiKey, WarmApiError } from './warm-api-client.js';
4
+ export { createWarmApiClient, getConfiguredApiKey, verifyWarmApiKey, WarmApiError };
5
5
  export type { WarmApiClientOptions } from './warm-api-client.js';
6
6
  export interface WarmServerOptions extends WarmApiClientOptions {
7
+ mode?: WarmServerMode;
7
8
  serverInfo?: Implementation;
8
9
  }
10
+ export type WarmServerMode = 'automation' | 'context';
9
11
  export declare const API_URL: string;
10
12
  export declare const WARM_SERVER_INFO: {
11
13
  name: string;
12
14
  version: string;
13
15
  };
14
- export declare function registerWarmTools(server: McpServer, options?: WarmApiClientOptions): void;
16
+ export declare function registerWarmTools(server: McpServer, options?: WarmApiClientOptions, mode?: WarmServerMode): void;
15
17
  export declare function createWarmServer(options?: WarmServerOptions): McpServer;
package/dist/server.js CHANGED
@@ -3,10 +3,10 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { toJsonSchemaCompat } from '@modelcontextprotocol/sdk/server/zod-json-schema-compat.js';
4
4
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
5
5
  import { z } from 'zod';
6
- import { createWarmApiClient, getConfiguredApiKey, verifyWarmApiKey, apiRequest, } from './warm-api-client.js';
7
- import { emptyInputSchema, getTransactionsInputSchema, } from './schemas.js';
8
- export { apiRequest, createWarmApiClient, getConfiguredApiKey, verifyWarmApiKey };
9
- export const API_URL = process.env.WARM_API_URL || 'https://warm.io';
6
+ import { createWarmApiClient, getConfiguredApiKey, verifyWarmApiKey, WarmApiError, } from './warm-api-client.js';
7
+ import { describeOperationInputSchema, emptyInputSchema, getTransactionsInputSchema, invokeOperationInputSchema, searchOperationsInputSchema, } from './schemas.js';
8
+ export { createWarmApiClient, getConfiguredApiKey, verifyWarmApiKey, WarmApiError };
9
+ export const API_URL = process.env.WARM_API_URL || 'https://app.warm.io';
10
10
  function getPackageVersion() {
11
11
  try {
12
12
  const packageJson = fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8');
@@ -21,7 +21,7 @@ export const WARM_SERVER_INFO = {
21
21
  name: 'warm',
22
22
  version: getPackageVersion(),
23
23
  };
24
- function createStructuredToolResult(structuredContent) {
24
+ function createStructuredToolResult(structuredContent, isError = false) {
25
25
  return {
26
26
  content: [
27
27
  {
@@ -29,9 +29,25 @@ function createStructuredToolResult(structuredContent) {
29
29
  text: JSON.stringify(structuredContent, null, 2),
30
30
  },
31
31
  ],
32
+ ...(isError ? { isError: true } : {}),
32
33
  structuredContent,
33
34
  };
34
35
  }
36
+ function createToolFailure(error) {
37
+ if (error instanceof WarmApiError) {
38
+ return createStructuredToolResult({
39
+ body: error.body,
40
+ headers: error.headers,
41
+ message: error.message,
42
+ status: error.status,
43
+ }, true);
44
+ }
45
+ return createStructuredToolResult({
46
+ error: {
47
+ message: error instanceof Error ? error.message : String(error),
48
+ },
49
+ }, true);
50
+ }
35
51
  function parseGetTransactionsArgs(args) {
36
52
  const hasMonth = typeof args.month === 'string';
37
53
  const hasLatestKey = Object.prototype.hasOwnProperty.call(args, 'latest');
@@ -47,16 +63,18 @@ function parseGetTransactionsArgs(args) {
47
63
  }
48
64
  throw new Error('Call get_transactions with `month` in YYYY-MM format, `latest: true`, or no arguments.');
49
65
  }
50
- export function registerWarmTools(server, options = {}) {
51
- const client = createWarmApiClient(options);
52
- const tools = [
66
+ export function registerWarmTools(server, options = {}, mode = 'context') {
67
+ const client = createWarmApiClient({ ...options, audience: mode });
68
+ const contextTools = [
53
69
  {
70
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
54
71
  description: 'Read-only compact FinancialContext JSON. Includes status.position, status.accounts, transaction index total/months, recurring, budgets, goals, snapshots, liabilities, holdings, and health. Transaction items are not inline; use get_transactions for items.',
55
72
  handler: async () => await client.getFinancialContext(),
56
73
  inputShape: emptyInputSchema,
57
74
  name: 'get_financial_context',
58
75
  },
59
76
  {
77
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
60
78
  description: 'Read-only transactions from the FinancialContext artifact. Pass exactly one selector: `month` in YYYY-MM format for a month page, or `latest: true` for the fixed latest window. A bare call with no arguments defaults to `latest: true`. The latest window is fixed at 10 days and is not caller-configurable. `month` and `latest` are mutually exclusive. Months outside the covered range return an error.',
61
79
  handler: async (args) => {
62
80
  return await client.getTransactions(parseGetTransactionsArgs(args));
@@ -65,12 +83,37 @@ export function registerWarmTools(server, options = {}) {
65
83
  name: 'get_transactions',
66
84
  },
67
85
  {
86
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
68
87
  description: 'Read-only API key validation for the configured Warm account.',
69
- handler: async () => await client.verifyKey(),
88
+ handler: async () => await client.validateAudience(),
70
89
  inputShape: emptyInputSchema,
71
90
  name: 'verify_key',
72
91
  },
73
92
  ];
93
+ const automationTools = [
94
+ {
95
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
96
+ description: 'Search the supported Warm operation catalog. Use this first to find the smallest matching operation.',
97
+ handler: async (args) => await client.searchOperations(typeof args.query === 'string' ? args.query : undefined),
98
+ inputShape: searchOperationsInputSchema,
99
+ name: 'search_operations',
100
+ },
101
+ {
102
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
103
+ description: 'Describe one operation, including its input schema and risk. For write operations, pass the exact intended input to receive an approval with its ID, status, URL, and expiry.',
104
+ handler: async (args) => await client.describeOperation(args.operation_id, args.input),
105
+ inputShape: describeOperationInputSchema,
106
+ name: 'describe_operation',
107
+ },
108
+ {
109
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
110
+ description: 'Invoke one previously discovered Warm operation. Every write operation requires the approval_id returned for the exact same input by describe_operation.',
111
+ handler: async (args) => await client.invokeOperation(args.operation_id, args.input, typeof args.approval_id === 'string' ? args.approval_id : undefined),
112
+ inputShape: invokeOperationInputSchema,
113
+ name: 'invoke_operation',
114
+ },
115
+ ];
116
+ const tools = mode === 'automation' ? automationTools : contextTools;
74
117
  server.server.registerCapabilities({
75
118
  tools: {
76
119
  listChanged: true,
@@ -79,6 +122,7 @@ export function registerWarmTools(server, options = {}) {
79
122
  server.server.setRequestHandler(ListToolsRequestSchema, () => ({
80
123
  tools: tools.map((tool) => ({
81
124
  description: tool.description,
125
+ annotations: tool.annotations,
82
126
  inputSchema: toJsonSchemaCompat(z.object(tool.inputShape).strict(), {
83
127
  pipeStrategy: 'input',
84
128
  strictUnions: true,
@@ -96,12 +140,32 @@ export function registerWarmTools(server, options = {}) {
96
140
  if (!parseResult.success) {
97
141
  throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for tool ${request.params.name}: ${parseResult.error.message}`);
98
142
  }
99
- return createStructuredToolResult(await tool.handler(parseResult.data));
143
+ if (tool.name === 'get_transactions') {
144
+ try {
145
+ parseGetTransactionsArgs(parseResult.data);
146
+ }
147
+ catch (error) {
148
+ throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error));
149
+ }
150
+ }
151
+ try {
152
+ if (tool.name !== 'verify_key') {
153
+ await client.validateAudience();
154
+ }
155
+ const result = await tool.handler(parseResult.data);
156
+ const isFailedAutomationOperation = tool.name === 'invoke_operation' &&
157
+ typeof result.status === 'number' &&
158
+ result.status >= 400;
159
+ return createStructuredToolResult(result, isFailedAutomationOperation);
160
+ }
161
+ catch (error) {
162
+ return createToolFailure(error);
163
+ }
100
164
  });
101
165
  }
102
166
  export function createWarmServer(options = {}) {
103
- const { serverInfo, ...clientOptions } = options;
167
+ const { mode = 'context', serverInfo, ...clientOptions } = options;
104
168
  const server = new McpServer(serverInfo || WARM_SERVER_INFO);
105
- registerWarmTools(server, clientOptions);
169
+ registerWarmTools(server, clientOptions, mode);
106
170
  return server;
107
171
  }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,7 @@
1
- export declare const WARM_TOOL_NAMES: readonly ["get_financial_context", "get_transactions", "verify_key"];
1
+ export declare const WARM_CONTEXT_TOOL_NAMES: readonly ["get_financial_context", "get_transactions", "verify_key"];
2
+ export declare const WARM_AUTOMATION_TOOL_NAMES: readonly ["search_operations", "describe_operation", "invoke_operation"];
3
+ export declare const WARM_TOOL_NAMES: readonly ["get_financial_context", "get_transactions", "verify_key", "search_operations", "describe_operation", "invoke_operation"];
2
4
  export type WarmToolName = (typeof WARM_TOOL_NAMES)[number];
3
- export type { Account, Budget, FinancialContext, FinancialContextMeta, GetTransactionsInput, GetTransactionsOutput, Goal, Health, Holding, LatestTransactions, Liability, Position, Recurring, Snapshot, Status, Transaction, TransactionIndex, TransactionMonth, VerifyKeyOutput, WarmApiClient, WarmApiClientOptions, } from './warm-api-client.js';
5
+ export type { Account, AutomationInput, AutomationOperation, AutomationOperationDescription, Budget, DescribeOperationOutput, FinancialContext, FinancialContextMeta, GetTransactionsInput, GetTransactionsOutput, Goal, Health, Holding, InvokeOperationOutput, LatestTransactions, Liability, Position, Recurring, Snapshot, Status, Transaction, TransactionIndex, TransactionMonth, VerifyKeyOutput, WarmApiClient, WarmApiClientOptions, } from './warm-api-client.js';
6
+ export type { WarmApiAudience } from './config-paths.js';
4
7
  export type { WarmServerOptions } from './server.js';
package/dist/types.js CHANGED
@@ -1,5 +1,11 @@
1
- export const WARM_TOOL_NAMES = [
1
+ export const WARM_CONTEXT_TOOL_NAMES = [
2
2
  'get_financial_context',
3
3
  'get_transactions',
4
4
  'verify_key',
5
5
  ];
6
+ export const WARM_AUTOMATION_TOOL_NAMES = [
7
+ 'search_operations',
8
+ 'describe_operation',
9
+ 'invoke_operation',
10
+ ];
11
+ export const WARM_TOOL_NAMES = [...WARM_CONTEXT_TOOL_NAMES, ...WARM_AUTOMATION_TOOL_NAMES];
@@ -1,4 +1,6 @@
1
+ import { type WarmApiAudience } from './config-paths.js';
1
2
  export interface WarmApiClientOptions {
3
+ audience?: WarmApiAudience;
2
4
  apiKeyResolver?: () => string | null;
3
5
  apiUrl?: string;
4
6
  fetchImplementation?: typeof fetch;
@@ -176,17 +178,66 @@ export interface FinancialContextMeta extends Record<string, unknown> {
176
178
  };
177
179
  }
178
180
  export interface VerifyKeyOutput extends Record<string, unknown> {
181
+ audience?: 'automation' | 'context' | 'oauth';
179
182
  status: string;
180
183
  valid: boolean;
181
184
  }
185
+ export interface AutomationOperation extends Record<string, unknown> {
186
+ confirmation_required: boolean;
187
+ description: string;
188
+ method: 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT';
189
+ operation_id: string;
190
+ path: string;
191
+ risk: Array<'read' | 'write' | 'destructive' | 'external_effect'>;
192
+ summary: string;
193
+ }
194
+ export interface AutomationInput {
195
+ body?: Record<string, unknown>;
196
+ params?: Record<string, string>;
197
+ }
198
+ export interface AutomationOperationDescription extends AutomationOperation {
199
+ input_schema: Record<string, unknown>;
200
+ }
201
+ export interface DescribeOperationOutput extends Record<string, unknown> {
202
+ approval?: {
203
+ approval_url: string;
204
+ expires_at: string;
205
+ id: string;
206
+ status: string;
207
+ };
208
+ operation: AutomationOperationDescription;
209
+ }
210
+ export interface InvokeOperationOutput extends Record<string, unknown> {
211
+ body: unknown;
212
+ headers: Record<string, string>;
213
+ operation_id: string;
214
+ status: number;
215
+ }
182
216
  export interface WarmApiClient {
183
217
  getFinancialContext(): Promise<FinancialContext>;
184
218
  getFinancialContextMeta(): Promise<FinancialContextMeta>;
185
219
  getTransactions(input: GetTransactionsInput): Promise<GetTransactionsOutput>;
220
+ searchOperations(query?: string): Promise<{
221
+ operations: AutomationOperation[];
222
+ }>;
223
+ describeOperation(operationId: string, input?: AutomationInput): Promise<DescribeOperationOutput>;
224
+ invokeOperation(operationId: string, input?: AutomationInput, approvalId?: string): Promise<InvokeOperationOutput>;
225
+ validateAudience(): Promise<VerifyKeyOutput>;
186
226
  verifyKey(): Promise<VerifyKeyOutput>;
187
227
  }
188
- export declare function getConfiguredApiKey(): string | null;
228
+ export declare function getConfiguredApiKey(audience?: WarmApiAudience): string | null;
189
229
  export declare function resetConfiguredApiKeyCache(): void;
190
- export declare function apiRequest<TResponse>(endpoint: string, params?: Record<string, string | undefined>, options?: WarmApiClientOptions): Promise<TResponse>;
230
+ interface WarmApiResponse<TBody> {
231
+ body: TBody;
232
+ headers: Record<string, string>;
233
+ status: number;
234
+ }
235
+ export declare class WarmApiError extends Error {
236
+ readonly body: unknown;
237
+ readonly headers: Record<string, string>;
238
+ readonly status: number;
239
+ constructor(message: string, response: WarmApiResponse<unknown>);
240
+ }
191
241
  export declare function createWarmApiClient(options?: WarmApiClientOptions): WarmApiClient;
192
- export declare function verifyWarmApiKey(apiKey: string): Promise<VerifyKeyOutput>;
242
+ export declare function verifyWarmApiKey(apiKey: string, audience?: WarmApiAudience): Promise<VerifyKeyOutput>;
243
+ export {};
@@ -1,10 +1,10 @@
1
- import { getWarmApiKeyPath, readConfigFile } from './config-paths.js';
2
- const DEFAULT_API_URL = process.env.WARM_API_URL || 'https://warm.io';
1
+ import { getWarmApiKeyPath, readConfigFile, } from './config-paths.js';
2
+ const DEFAULT_API_URL = process.env.WARM_API_URL || 'https://app.warm.io';
3
3
  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
- let cachedApiKey;
7
+ const cachedApiKeys = new Map();
8
8
  function getRequestSignal(timeoutMs) {
9
9
  if (typeof AbortSignal.timeout === 'function') {
10
10
  return AbortSignal.timeout(timeoutMs);
@@ -16,34 +16,59 @@ function getRequestSignal(timeoutMs) {
16
16
  }
17
17
  function createWarmApiClientConfig(options) {
18
18
  return {
19
+ audience: options.audience || 'context',
19
20
  apiKeyResolver: options.apiKeyResolver,
20
21
  apiUrl: options.apiUrl || DEFAULT_API_URL,
21
22
  fetchImplementation: options.fetchImplementation || fetch,
22
23
  requestTimeoutMs: options.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
23
24
  };
24
25
  }
25
- export function getConfiguredApiKey() {
26
- if (cachedApiKey !== undefined) {
27
- return cachedApiKey;
26
+ export function getConfiguredApiKey(audience = 'context') {
27
+ if (cachedApiKeys.has(audience)) {
28
+ return cachedApiKeys.get(audience) ?? null;
28
29
  }
29
- if (process.env.WARM_API_KEY?.trim()) {
30
- cachedApiKey = process.env.WARM_API_KEY.trim();
31
- return cachedApiKey;
32
- }
33
- cachedApiKey = readConfigFile(getWarmApiKeyPath());
34
- return cachedApiKey;
30
+ const environmentKey = audience === 'automation' ? 'WARM_AUTOMATION_API_KEY' : 'WARM_CONTEXT_API_KEY';
31
+ const apiKey = process.env[environmentKey]?.trim() || readConfigFile(getWarmApiKeyPath(audience));
32
+ cachedApiKeys.set(audience, apiKey);
33
+ return apiKey;
35
34
  }
36
35
  export function resetConfiguredApiKeyCache() {
37
- cachedApiKey = undefined;
36
+ cachedApiKeys.clear();
37
+ }
38
+ export class WarmApiError extends Error {
39
+ body;
40
+ headers;
41
+ status;
42
+ constructor(message, response) {
43
+ super(message);
44
+ this.name = 'WarmApiError';
45
+ this.body = response.body;
46
+ this.headers = response.headers;
47
+ this.status = response.status;
48
+ }
38
49
  }
39
- export async function apiRequest(endpoint, params = {}, options = {}) {
50
+ function toWarmApiError(response) {
51
+ const errorMessages = {
52
+ 401: 'Invalid or expired API key. Regenerate at https://warm.io/settings',
53
+ 403: 'API access is available on paid plans only. Upgrade at https://warm.io/settings',
54
+ 429: 'Rate limit exceeded. Try again in a few minutes.',
55
+ };
56
+ if (errorMessages[response.status]) {
57
+ return new WarmApiError(errorMessages[response.status], response);
58
+ }
59
+ const detail = typeof response.body === 'object' && response.body !== null && 'error' in response.body
60
+ ? String(response.body.error)
61
+ : `HTTP ${response.status}`;
62
+ return new WarmApiError(detail, response);
63
+ }
64
+ async function requestWarmApi(request, options) {
40
65
  const requestOptions = createWarmApiClientConfig(options);
41
- const apiKey = requestOptions.apiKeyResolver?.() ?? getConfiguredApiKey();
66
+ const apiKey = requestOptions.apiKeyResolver?.() ?? getConfiguredApiKey(requestOptions.audience);
42
67
  if (!apiKey) {
43
- throw new Error('WARM_API_KEY not set. Run "npx @warmio/mcp" to configure or set WARM_API_KEY.');
68
+ throw new Error(`No ${requestOptions.audience} API key configured. Run "npx @warmio/mcp install --mode ${requestOptions.audience}" or set ${requestOptions.audience === 'automation' ? 'WARM_AUTOMATION_API_KEY' : 'WARM_CONTEXT_API_KEY'}.`);
44
69
  }
45
- const url = new URL(endpoint, requestOptions.apiUrl);
46
- Object.entries(params).forEach(([key, value]) => {
70
+ const url = new URL(request.endpoint, requestOptions.apiUrl);
71
+ Object.entries(request.query || {}).forEach(([key, value]) => {
47
72
  if (value) {
48
73
  url.searchParams.append(key, value);
49
74
  }
@@ -51,10 +76,13 @@ export async function apiRequest(endpoint, params = {}, options = {}) {
51
76
  let response;
52
77
  try {
53
78
  response = await requestOptions.fetchImplementation(url.toString(), {
79
+ ...(request.method && request.method !== 'GET' ? { method: request.method } : {}),
54
80
  headers: {
55
81
  Accept: 'application/json',
56
82
  Authorization: `Bearer ${apiKey}`,
83
+ ...(request.body ? { 'Content-Type': 'application/json' } : {}),
57
84
  },
85
+ ...(request.body ? { body: JSON.stringify(request.body) } : {}),
58
86
  signal: getRequestSignal(requestOptions.requestTimeoutMs),
59
87
  });
60
88
  }
@@ -67,61 +95,91 @@ export async function apiRequest(endpoint, params = {}, options = {}) {
67
95
  }
68
96
  throw error;
69
97
  }
70
- if (!response.ok) {
71
- const errorMessages = {
72
- 401: 'Invalid or expired API key. Regenerate at https://warm.io/settings',
73
- 403: 'API access is available on paid plans only. Upgrade at https://warm.io/settings',
74
- 429: 'Rate limit exceeded. Try again in a few minutes.',
75
- };
76
- if (errorMessages[response.status]) {
77
- throw new Error(errorMessages[response.status]);
78
- }
79
- let detail = `HTTP ${response.status}`;
98
+ const text = await response.text();
99
+ let body = null;
100
+ if (text) {
80
101
  try {
81
- const body = (await response.json());
82
- if (body?.error) {
83
- detail = body.error;
84
- }
102
+ body = JSON.parse(text);
85
103
  }
86
104
  catch {
87
- // Fall back to the HTTP status when the error body is not JSON.
105
+ body = text;
88
106
  }
89
- throw new Error(detail);
90
107
  }
91
- return (await response.json());
108
+ return {
109
+ body: body,
110
+ headers: Object.fromEntries(response.headers.entries()),
111
+ status: response.status,
112
+ };
113
+ }
114
+ async function requestSuccessfulJson(request, options) {
115
+ const response = await requestWarmApi(request, options);
116
+ if (response.status >= 400) {
117
+ throw toWarmApiError(response);
118
+ }
119
+ return response.body;
92
120
  }
93
121
  export function createWarmApiClient(options = {}) {
94
- const getFinancialContext = async () => await apiRequest('/api/financial-context', {}, options);
95
- const getFinancialContextMeta = async () => await apiRequest('/api/financial-context/meta', {}, options);
122
+ const getFinancialContext = async () => await requestSuccessfulJson({ endpoint: '/api/financial-context' }, options);
123
+ const getFinancialContextMeta = async () => await requestSuccessfulJson({ endpoint: '/api/financial-context/meta' }, options);
96
124
  const getTransactions = async (input) => {
97
125
  const rawInput = input;
98
126
  if (typeof rawInput.month === 'string' && rawInput.latest !== undefined) {
99
127
  throw new Error('`month` and `latest` are mutually exclusive.');
100
128
  }
101
129
  if (typeof rawInput.month === 'string') {
102
- return await apiRequest('/api/financial-context/transactions', { month: rawInput.month, latest: undefined }, options);
130
+ return await requestSuccessfulJson({ endpoint: '/api/financial-context/transactions', query: { month: rawInput.month } }, options);
103
131
  }
104
132
  if (rawInput.latest === true) {
105
- return await apiRequest('/api/financial-context/transactions', { month: undefined, latest: '1' }, options);
133
+ return await requestSuccessfulJson({ endpoint: '/api/financial-context/transactions', query: { latest: '1' } }, options);
106
134
  }
107
135
  throw new Error('Call getTransactions with `month` in YYYY-MM format or `latest: true`.');
108
136
  };
109
137
  const verifyKey = async () => {
110
- const response = await apiRequest('/api/verify', {}, options);
138
+ const response = await requestSuccessfulJson({ endpoint: '/api/verify' }, options);
111
139
  return {
140
+ audience: response.audience,
112
141
  status: response.status || (response.valid ? 'ok' : 'invalid'),
113
142
  valid: response.valid === true,
114
143
  };
115
144
  };
145
+ const validateAudience = async () => {
146
+ const result = await verifyKey();
147
+ const expectedAudience = createWarmApiClientConfig(options).audience;
148
+ if (!result.valid || result.audience !== expectedAudience) {
149
+ const article = expectedAudience === 'automation' ? 'an' : 'a';
150
+ throw new Error(`This MCP mode requires ${article} ${expectedAudience} API key. Run "npx @warmio/mcp install --mode ${expectedAudience}" to configure a separate key.`);
151
+ }
152
+ return result;
153
+ };
154
+ const searchOperations = async (query) => await requestSuccessfulJson({ body: query ? { query } : {}, endpoint: '/api/automation/operations/search', method: 'POST' }, options);
155
+ const describeOperation = async (operationId, input) => await requestSuccessfulJson({
156
+ body: { operation_id: operationId, ...(input ? { input } : {}) },
157
+ endpoint: '/api/automation/operations/describe',
158
+ method: 'POST',
159
+ }, options);
160
+ const invokeOperation = async (operationId, input, approvalId) => await requestSuccessfulJson({
161
+ body: {
162
+ operation_id: operationId,
163
+ input: input ?? {},
164
+ ...(approvalId ? { approval_id: approvalId } : {}),
165
+ },
166
+ endpoint: '/api/automation/operations/invoke',
167
+ method: 'POST',
168
+ }, options);
116
169
  return {
117
170
  getFinancialContext,
118
171
  getFinancialContextMeta,
119
172
  getTransactions,
173
+ searchOperations,
174
+ describeOperation,
175
+ invokeOperation,
176
+ validateAudience,
120
177
  verifyKey,
121
178
  };
122
179
  }
123
- export async function verifyWarmApiKey(apiKey) {
180
+ export async function verifyWarmApiKey(apiKey, audience = 'context') {
124
181
  return createWarmApiClient({
182
+ audience,
125
183
  apiKeyResolver: () => apiKey,
126
184
  }).verifyKey();
127
185
  }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@warmio/mcp",
3
- "version": "6.0.0",
4
- "description": "Warm CLI and MCP server for personal financial data",
3
+ "version": "7.0.1",
4
+ "mcpName": "io.github.jasonstockman/warmio",
5
+ "description": "Warm CLI and compact context or automation MCP server",
5
6
  "type": "module",
6
7
  "main": "dist/index.js",
7
8
  "types": "dist/index.d.ts",