@warmio/mcp 6.0.0 → 7.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 +78 -25
- package/dist/config-paths.d.ts +2 -1
- package/dist/config-paths.js +6 -4
- package/dist/http.d.ts +6 -0
- package/dist/http.js +36 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +18 -6
- package/dist/install.d.ts +2 -0
- package/dist/install.js +209 -94
- package/dist/schemas.d.ts +46 -6
- package/dist/schemas.js +24 -0
- package/dist/server.d.ts +5 -3
- package/dist/server.js +76 -12
- package/dist/types.d.ts +5 -2
- package/dist/types.js +7 -1
- package/dist/warm-api-client.d.ts +54 -3
- package/dist/warm-api-client.js +99 -41
- package/package.json +2 -2
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,
|
|
4
|
-
export {
|
|
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,
|
|
7
|
-
import { emptyInputSchema, getTransactionsInputSchema, } from './schemas.js';
|
|
8
|
-
export {
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {};
|
package/dist/warm-api-client.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
27
|
-
return
|
|
26
|
+
export function getConfiguredApiKey(audience = 'context') {
|
|
27
|
+
if (cachedApiKeys.has(audience)) {
|
|
28
|
+
return cachedApiKeys.get(audience) ?? null;
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
82
|
-
if (body?.error) {
|
|
83
|
-
detail = body.error;
|
|
84
|
-
}
|
|
102
|
+
body = JSON.parse(text);
|
|
85
103
|
}
|
|
86
104
|
catch {
|
|
87
|
-
|
|
105
|
+
body = text;
|
|
88
106
|
}
|
|
89
|
-
throw new Error(detail);
|
|
90
107
|
}
|
|
91
|
-
return
|
|
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
|
|
95
|
-
const getFinancialContextMeta = async () => await
|
|
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
|
|
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
|
|
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
|
|
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,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warmio/mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Warm CLI and
|
|
3
|
+
"version": "7.0.0",
|
|
4
|
+
"description": "Warm CLI and compact context or automation MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|