@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.
- package/README.md +16 -1
- package/dist/config-paths.d.ts +1 -0
- package/dist/config-paths.js +9 -0
- package/dist/http.d.ts +5 -6
- package/dist/http.js +12 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +47 -3
- package/dist/load-env.d.ts +1 -0
- package/dist/load-env.js +36 -0
- package/dist/schemas.d.ts +242 -191
- package/dist/schemas.js +140 -163
- package/dist/server.d.ts +15 -8
- package/dist/server.js +73 -12
- package/dist/stdio.d.ts +29 -0
- package/dist/stdio.js +200 -0
- package/dist/types.d.ts +2 -158
- package/dist/warm-api-client.d.ts +184 -0
- package/dist/warm-api-client.js +299 -0
- package/dist/warm-server.d.ts +2 -14
- package/dist/warm-server.js +2 -245
- package/package.json +15 -6
package/dist/schemas.js
CHANGED
|
@@ -1,184 +1,161 @@
|
|
|
1
|
-
import
|
|
2
|
-
const dateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Expected
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const dateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Expected YYYY-MM-DD date.');
|
|
3
3
|
const dateTimeSchema = z.string().datetime({ offset: true });
|
|
4
|
-
export const emptyInputSchema =
|
|
5
|
-
export const accountTypeSchema = z.enum([
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.min(1)
|
|
26
|
-
.max(1000)
|
|
27
|
-
.default(500)
|
|
28
|
-
.describe('Maximum transactions to return in this page (default 500, max 1000).'),
|
|
4
|
+
export const emptyInputSchema = {};
|
|
5
|
+
export const accountTypeSchema = z.enum([
|
|
6
|
+
'depository',
|
|
7
|
+
'credit',
|
|
8
|
+
'loan',
|
|
9
|
+
'investment',
|
|
10
|
+
'other',
|
|
11
|
+
]);
|
|
12
|
+
export const getAccountsOutputSchema = {
|
|
13
|
+
accounts: z.array(z
|
|
14
|
+
.object({
|
|
15
|
+
balance: z.number().finite(),
|
|
16
|
+
institution: z.string().nullable(),
|
|
17
|
+
mask: z.string().nullable(),
|
|
18
|
+
name: z.string(),
|
|
19
|
+
subtype: z.string().nullable(),
|
|
20
|
+
type: accountTypeSchema,
|
|
21
|
+
})
|
|
22
|
+
.strict()),
|
|
23
|
+
};
|
|
24
|
+
export const getTransactionsInputSchema = {
|
|
29
25
|
cursor: z
|
|
30
26
|
.string()
|
|
31
27
|
.min(1)
|
|
32
28
|
.optional()
|
|
33
|
-
.describe('Opaque pagination cursor from a
|
|
29
|
+
.describe('Opaque pagination cursor from a prior get_transactions response.'),
|
|
34
30
|
last_knowledge: dateTimeSchema
|
|
35
31
|
.optional()
|
|
36
|
-
.describe('Incremental sync checkpoint
|
|
32
|
+
.describe('Incremental sync checkpoint. Cannot be combined with cursor.'),
|
|
33
|
+
limit: z
|
|
34
|
+
.number()
|
|
35
|
+
.int()
|
|
36
|
+
.min(1)
|
|
37
|
+
.max(1000)
|
|
38
|
+
.default(500)
|
|
39
|
+
.describe('Maximum transactions to return (default 500, max 1000).'),
|
|
37
40
|
search: z
|
|
38
41
|
.string()
|
|
39
42
|
.min(1)
|
|
40
43
|
.max(120)
|
|
41
44
|
.optional()
|
|
42
|
-
.describe('Optional merchant or description search
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.superRefine((value, ctx) => {
|
|
46
|
-
if (value.cursor && value.last_knowledge) {
|
|
47
|
-
ctx.addIssue({
|
|
48
|
-
code: z.ZodIssueCode.custom,
|
|
49
|
-
path: ['cursor'],
|
|
50
|
-
message: '`cursor` cannot be combined with `last_knowledge`.',
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
export const getTransactionsInputSchema = getTransactionsInputObjectSchema;
|
|
55
|
-
export const transactionSchema = z
|
|
56
|
-
.object({
|
|
57
|
-
id: z.string().nullable(),
|
|
58
|
-
date: dateSchema.nullable(),
|
|
59
|
-
amount: z.number().finite(),
|
|
60
|
-
merchant: z.string().nullable(),
|
|
61
|
-
description: z.string().nullable(),
|
|
62
|
-
category: z.string().nullable(),
|
|
63
|
-
detailed_category: z.string().nullable(),
|
|
64
|
-
})
|
|
65
|
-
.strict();
|
|
66
|
-
export const getTransactionsOutputSchema = z
|
|
67
|
-
.object({
|
|
45
|
+
.describe('Optional merchant or description search query.'),
|
|
46
|
+
};
|
|
47
|
+
export const getTransactionsOutputSchema = {
|
|
68
48
|
generated_at: dateTimeSchema.nullable(),
|
|
69
49
|
next_knowledge: dateTimeSchema.nullable(),
|
|
70
|
-
txns: z.array(transactionSchema),
|
|
71
50
|
pagination: z
|
|
72
51
|
.object({
|
|
73
|
-
limit: z.int().min(1).max(1000),
|
|
74
|
-
next_cursor: z.string().nullable(),
|
|
75
52
|
has_more: z.boolean(),
|
|
53
|
+
limit: z.number().int().min(1).max(1000),
|
|
54
|
+
next_cursor: z.string().nullable(),
|
|
76
55
|
})
|
|
77
56
|
.strict(),
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.strict();
|
|
109
|
-
export const goalSchema = z
|
|
110
|
-
.object({
|
|
111
|
-
name: z.string(),
|
|
112
|
-
target: z.number().finite(),
|
|
113
|
-
current: z.number().finite(),
|
|
114
|
-
progress_percent: z.number().finite(),
|
|
115
|
-
target_date: dateSchema.nullable(),
|
|
116
|
-
status: z.string().nullable(),
|
|
117
|
-
category: z.string().nullable(),
|
|
118
|
-
monthly_contribution_needed: z.number().finite().nullable(),
|
|
119
|
-
})
|
|
120
|
-
.strict();
|
|
121
|
-
export const healthPillarsSchema = z
|
|
122
|
-
.object({
|
|
123
|
-
spend: z.number().finite().nullable(),
|
|
124
|
-
save: z.number().finite().nullable(),
|
|
125
|
-
borrow: z.number().finite().nullable(),
|
|
126
|
-
build: z.number().finite().nullable(),
|
|
127
|
-
})
|
|
128
|
-
.strict();
|
|
129
|
-
export const liabilitySchema = z
|
|
130
|
-
.object({
|
|
131
|
-
account_id: z.string(),
|
|
132
|
-
type: z.string(),
|
|
133
|
-
balance: z.number().finite().nullable(),
|
|
134
|
-
apr_percentage: z.number().finite().nullable(),
|
|
135
|
-
minimum_payment: z.number().finite().nullable(),
|
|
136
|
-
next_payment_due_date: dateSchema.nullable(),
|
|
137
|
-
is_overdue: z.boolean().nullable(),
|
|
138
|
-
})
|
|
139
|
-
.strict();
|
|
140
|
-
export const holdingSchema = z
|
|
141
|
-
.object({
|
|
142
|
-
account_id: z.string(),
|
|
143
|
-
security_name: z.string().nullable(),
|
|
144
|
-
symbol: z.string().nullable(),
|
|
145
|
-
type: z.string().nullable(),
|
|
146
|
-
quantity: z.number().finite(),
|
|
147
|
-
value: z.number().finite().nullable(),
|
|
148
|
-
cost_basis: z.number().finite().nullable(),
|
|
149
|
-
})
|
|
150
|
-
.strict();
|
|
151
|
-
export const categorySpendingSchema = z
|
|
152
|
-
.object({
|
|
153
|
-
category: z.string(),
|
|
154
|
-
amount: z.number().finite(),
|
|
155
|
-
})
|
|
156
|
-
.strict();
|
|
157
|
-
export const financialHealthSchema = z
|
|
158
|
-
.object({
|
|
159
|
-
score: z.number().finite().nullable(),
|
|
160
|
-
label: z.string().nullable(),
|
|
161
|
-
data_completeness: z.number().finite().nullable(),
|
|
162
|
-
pillars: healthPillarsSchema.nullable(),
|
|
163
|
-
message: z.string().nullable(),
|
|
164
|
-
})
|
|
165
|
-
.strict();
|
|
166
|
-
export const getFinancialStateOutputSchema = z
|
|
167
|
-
.object({
|
|
57
|
+
txns: z.array(z
|
|
58
|
+
.object({
|
|
59
|
+
amount: z.number().finite(),
|
|
60
|
+
category: z.string().nullable(),
|
|
61
|
+
date: dateSchema.nullable(),
|
|
62
|
+
description: z.string().nullable(),
|
|
63
|
+
detailed_category: z.string().nullable(),
|
|
64
|
+
id: z.string().nullable(),
|
|
65
|
+
merchant: z.string().nullable(),
|
|
66
|
+
})
|
|
67
|
+
.strict()),
|
|
68
|
+
};
|
|
69
|
+
export const getFinancialStateOutputSchema = {
|
|
70
|
+
budgets: z.array(z
|
|
71
|
+
.object({
|
|
72
|
+
amount: z.number().finite(),
|
|
73
|
+
name: z.string(),
|
|
74
|
+
percent_used: z.number().finite(),
|
|
75
|
+
period: z.string(),
|
|
76
|
+
remaining: z.number().finite(),
|
|
77
|
+
spent: z.number().finite(),
|
|
78
|
+
status: z.string().nullable(),
|
|
79
|
+
})
|
|
80
|
+
.strict()),
|
|
81
|
+
category_spending: z.array(z
|
|
82
|
+
.object({
|
|
83
|
+
amount: z.number().finite(),
|
|
84
|
+
category: z.string(),
|
|
85
|
+
})
|
|
86
|
+
.strict()),
|
|
168
87
|
generated_at: dateTimeSchema,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
88
|
+
goals: z.array(z
|
|
89
|
+
.object({
|
|
90
|
+
category: z.string().nullable(),
|
|
91
|
+
current: z.number().finite(),
|
|
92
|
+
monthly_contribution_needed: z.number().finite().nullable(),
|
|
93
|
+
name: z.string(),
|
|
94
|
+
progress_percent: z.number().finite(),
|
|
95
|
+
status: z.string().nullable(),
|
|
96
|
+
target: z.number().finite(),
|
|
97
|
+
target_date: dateSchema.nullable(),
|
|
98
|
+
})
|
|
99
|
+
.strict()),
|
|
100
|
+
health: z
|
|
101
|
+
.object({
|
|
102
|
+
data_completeness: z.number().finite().nullable(),
|
|
103
|
+
label: z.string().nullable(),
|
|
104
|
+
message: z.string().nullable(),
|
|
105
|
+
pillars: z
|
|
106
|
+
.object({
|
|
107
|
+
borrow: z.number().finite().nullable(),
|
|
108
|
+
build: z.number().finite().nullable(),
|
|
109
|
+
save: z.number().finite().nullable(),
|
|
110
|
+
spend: z.number().finite().nullable(),
|
|
111
|
+
})
|
|
112
|
+
.strict()
|
|
113
|
+
.nullable(),
|
|
114
|
+
score: z.number().finite().nullable(),
|
|
115
|
+
})
|
|
116
|
+
.strict(),
|
|
117
|
+
holdings: z.array(z
|
|
118
|
+
.object({
|
|
119
|
+
account_id: z.string(),
|
|
120
|
+
cost_basis: z.number().finite().nullable(),
|
|
121
|
+
quantity: z.number().finite(),
|
|
122
|
+
security_name: z.string().nullable(),
|
|
123
|
+
symbol: z.string().nullable(),
|
|
124
|
+
type: z.string().nullable(),
|
|
125
|
+
value: z.number().finite().nullable(),
|
|
126
|
+
})
|
|
127
|
+
.strict()),
|
|
128
|
+
liabilities: z.array(z
|
|
129
|
+
.object({
|
|
130
|
+
account_id: z.string(),
|
|
131
|
+
apr_percentage: z.number().finite().nullable(),
|
|
132
|
+
balance: z.number().finite().nullable(),
|
|
133
|
+
is_overdue: z.boolean().nullable(),
|
|
134
|
+
minimum_payment: z.number().finite().nullable(),
|
|
135
|
+
next_payment_due_date: dateSchema.nullable(),
|
|
136
|
+
type: z.string(),
|
|
137
|
+
})
|
|
138
|
+
.strict()),
|
|
139
|
+
recurring: z.array(z
|
|
140
|
+
.object({
|
|
141
|
+
active: z.boolean(),
|
|
142
|
+
amount: z.number().finite(),
|
|
143
|
+
frequency: z.string(),
|
|
144
|
+
merchant: z.string(),
|
|
145
|
+
next_date: dateSchema.nullable(),
|
|
146
|
+
type: z.string().nullable(),
|
|
147
|
+
})
|
|
148
|
+
.strict()),
|
|
149
|
+
snapshots: z.array(z
|
|
150
|
+
.object({
|
|
151
|
+
date: dateSchema,
|
|
152
|
+
net_worth: z.number().finite(),
|
|
153
|
+
total_assets: z.number().finite(),
|
|
154
|
+
total_liabilities: z.number().finite(),
|
|
155
|
+
})
|
|
156
|
+
.strict()),
|
|
157
|
+
};
|
|
158
|
+
export const verifyKeyOutputSchema = {
|
|
182
159
|
status: z.string(),
|
|
183
|
-
|
|
184
|
-
|
|
160
|
+
valid: z.boolean(),
|
|
161
|
+
};
|
package/dist/server.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
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 };
|
|
5
|
+
export type { WarmApiClientOptions } from './warm-api-client.js';
|
|
6
|
+
export interface WarmServerOptions extends WarmApiClientOptions {
|
|
7
|
+
serverInfo?: Implementation;
|
|
8
|
+
}
|
|
9
|
+
export declare const API_URL: string;
|
|
10
|
+
export declare const WARM_SERVER_INFO: {
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function registerWarmTools(server: McpServer, options?: WarmApiClientOptions): void;
|
|
15
|
+
export declare function createWarmServer(options?: WarmServerOptions): McpServer;
|
package/dist/server.js
CHANGED
|
@@ -1,14 +1,75 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { createWarmApiClient, getConfiguredApiKey, verifyWarmApiKey, apiRequest, } from './warm-api-client.js';
|
|
4
|
+
import { emptyInputSchema, getAccountsOutputSchema, getFinancialStateOutputSchema, getTransactionsInputSchema, getTransactionsOutputSchema, verifyKeyOutputSchema, } from './schemas.js';
|
|
5
|
+
export { apiRequest, createWarmApiClient, getConfiguredApiKey, verifyWarmApiKey };
|
|
6
|
+
export const API_URL = process.env.WARM_API_URL || 'https://warm.io';
|
|
7
|
+
function getPackageVersion() {
|
|
8
|
+
try {
|
|
9
|
+
const packageJson = fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8');
|
|
10
|
+
const parsed = JSON.parse(packageJson);
|
|
11
|
+
return parsed.version || '0.0.0';
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return '0.0.0';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export const WARM_SERVER_INFO = {
|
|
18
|
+
name: 'warm',
|
|
19
|
+
version: getPackageVersion(),
|
|
20
|
+
};
|
|
21
|
+
const READ_ONLY_TOOL_ANNOTATIONS = {
|
|
22
|
+
destructiveHint: false,
|
|
23
|
+
idempotentHint: true,
|
|
24
|
+
openWorldHint: false,
|
|
25
|
+
readOnlyHint: true,
|
|
26
|
+
};
|
|
27
|
+
function createStructuredToolResult(structuredContent) {
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: 'text',
|
|
32
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
structuredContent,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function registerWarmTools(server, options = {}) {
|
|
39
|
+
const client = createWarmApiClient(options);
|
|
40
|
+
server.registerTool('get_accounts', {
|
|
41
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
42
|
+
description: 'Read-only list of connected financial accounts with balances, subtypes, institutions, and masks when available.',
|
|
43
|
+
inputSchema: emptyInputSchema,
|
|
44
|
+
outputSchema: getAccountsOutputSchema,
|
|
45
|
+
}, async () => createStructuredToolResult(await client.getAccounts()));
|
|
46
|
+
server.registerTool('get_transactions', {
|
|
47
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
48
|
+
description: 'Read-only transaction export with strict opaque cursor pagination and optional last_knowledge incremental sync.',
|
|
49
|
+
inputSchema: getTransactionsInputSchema,
|
|
50
|
+
outputSchema: getTransactionsOutputSchema,
|
|
51
|
+
}, async (args) => {
|
|
52
|
+
if (args.cursor && args.last_knowledge) {
|
|
53
|
+
throw new Error('`cursor` cannot be combined with `last_knowledge`.');
|
|
54
|
+
}
|
|
55
|
+
return createStructuredToolResult(await client.getTransactions(args));
|
|
56
|
+
});
|
|
57
|
+
server.registerTool('get_financial_state', {
|
|
58
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
59
|
+
description: 'Read-only broad financial state bundle with snapshots, recurring payments, budgets, goals, financial health, liabilities, holdings, and category spending.',
|
|
60
|
+
inputSchema: emptyInputSchema,
|
|
61
|
+
outputSchema: getFinancialStateOutputSchema,
|
|
62
|
+
}, async () => createStructuredToolResult(await client.getFinancialState()));
|
|
63
|
+
server.registerTool('verify_key', {
|
|
64
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
65
|
+
description: 'Read-only API key validation for the configured Warm account.',
|
|
66
|
+
inputSchema: emptyInputSchema,
|
|
67
|
+
outputSchema: verifyKeyOutputSchema,
|
|
68
|
+
}, async () => createStructuredToolResult(await client.verifyKey()));
|
|
69
|
+
}
|
|
70
|
+
export function createWarmServer(options = {}) {
|
|
71
|
+
const { serverInfo, ...clientOptions } = options;
|
|
72
|
+
const server = new McpServer(serverInfo || WARM_SERVER_INFO);
|
|
73
|
+
registerWarmTools(server, clientOptions);
|
|
13
74
|
return server;
|
|
14
75
|
}
|
package/dist/stdio.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Readable, Writable } from 'node:stream';
|
|
2
|
+
import type { Transport, TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
3
|
+
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
export declare class AdaptiveStdioTransport implements Transport {
|
|
5
|
+
private readonly stdin;
|
|
6
|
+
private readonly stdout;
|
|
7
|
+
onclose?: () => void;
|
|
8
|
+
onerror?: (error: Error) => void;
|
|
9
|
+
onmessage?: <T extends JSONRPCMessage>(message: T) => void;
|
|
10
|
+
private buffer;
|
|
11
|
+
private clientFraming;
|
|
12
|
+
private closed;
|
|
13
|
+
private started;
|
|
14
|
+
constructor(stdin?: Readable, stdout?: Writable);
|
|
15
|
+
start(): Promise<void>;
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
send(message: JSONRPCMessage, _options?: TransportSendOptions): Promise<void>;
|
|
18
|
+
private readonly handleData;
|
|
19
|
+
private readonly handleError;
|
|
20
|
+
private readonly handleEnd;
|
|
21
|
+
private processBuffer;
|
|
22
|
+
private trimLeadingNewlines;
|
|
23
|
+
private readMessage;
|
|
24
|
+
private detectFraming;
|
|
25
|
+
private readJsonLineMessage;
|
|
26
|
+
private readContentLengthMessage;
|
|
27
|
+
private findHeaderBoundary;
|
|
28
|
+
private parseContentLength;
|
|
29
|
+
}
|
package/dist/stdio.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
const CONTENT_LENGTH_HEADER = 'content-length:';
|
|
3
|
+
export class AdaptiveStdioTransport {
|
|
4
|
+
stdin;
|
|
5
|
+
stdout;
|
|
6
|
+
onclose;
|
|
7
|
+
onerror;
|
|
8
|
+
onmessage;
|
|
9
|
+
buffer = Buffer.alloc(0);
|
|
10
|
+
clientFraming = null;
|
|
11
|
+
closed = false;
|
|
12
|
+
started = false;
|
|
13
|
+
constructor(stdin = process.stdin, stdout = process.stdout) {
|
|
14
|
+
this.stdin = stdin;
|
|
15
|
+
this.stdout = stdout;
|
|
16
|
+
}
|
|
17
|
+
async start() {
|
|
18
|
+
if (this.started) {
|
|
19
|
+
throw new Error('AdaptiveStdioTransport already started');
|
|
20
|
+
}
|
|
21
|
+
this.started = true;
|
|
22
|
+
this.stdin.on('data', this.handleData);
|
|
23
|
+
this.stdin.on('error', this.handleError);
|
|
24
|
+
this.stdin.on('end', this.handleEnd);
|
|
25
|
+
this.stdin.resume();
|
|
26
|
+
}
|
|
27
|
+
async close() {
|
|
28
|
+
if (this.closed) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.closed = true;
|
|
32
|
+
this.stdin.off('data', this.handleData);
|
|
33
|
+
this.stdin.off('error', this.handleError);
|
|
34
|
+
this.stdin.off('end', this.handleEnd);
|
|
35
|
+
this.stdin.pause();
|
|
36
|
+
this.buffer = Buffer.alloc(0);
|
|
37
|
+
this.onclose?.();
|
|
38
|
+
}
|
|
39
|
+
async send(message, _options) {
|
|
40
|
+
const json = JSON.stringify(message);
|
|
41
|
+
const framing = this.clientFraming ?? 'content-length';
|
|
42
|
+
const payload = framing === 'jsonl'
|
|
43
|
+
? `${json}\n`
|
|
44
|
+
: `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`;
|
|
45
|
+
await new Promise((resolve) => {
|
|
46
|
+
if (this.stdout.write(payload)) {
|
|
47
|
+
resolve();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.stdout.once('drain', resolve);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
handleData = (chunk) => {
|
|
54
|
+
const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
55
|
+
this.buffer = this.buffer.length === 0 ? incoming : Buffer.concat([this.buffer, incoming]);
|
|
56
|
+
this.processBuffer();
|
|
57
|
+
};
|
|
58
|
+
handleError = (error) => {
|
|
59
|
+
this.onerror?.(error);
|
|
60
|
+
};
|
|
61
|
+
handleEnd = () => {
|
|
62
|
+
void this.close();
|
|
63
|
+
};
|
|
64
|
+
processBuffer() {
|
|
65
|
+
while (true) {
|
|
66
|
+
try {
|
|
67
|
+
this.trimLeadingNewlines();
|
|
68
|
+
const parsed = this.readMessage();
|
|
69
|
+
if (parsed === null) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.clientFraming ??= parsed.framing;
|
|
73
|
+
this.onmessage?.(parsed.message);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
this.onerror?.(error instanceof Error ? error : new Error(String(error)));
|
|
77
|
+
this.buffer = Buffer.alloc(0);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
trimLeadingNewlines() {
|
|
83
|
+
while (this.buffer.length > 0) {
|
|
84
|
+
if (this.buffer[0] === 0x0a) {
|
|
85
|
+
this.buffer = this.buffer.subarray(1);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (this.buffer.length > 1 && this.buffer[0] === 0x0d && this.buffer[1] === 0x0a) {
|
|
89
|
+
this.buffer = this.buffer.subarray(2);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
readMessage() {
|
|
96
|
+
const framing = this.clientFraming ?? this.detectFraming();
|
|
97
|
+
if (framing === null) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return framing === 'content-length'
|
|
101
|
+
? this.readContentLengthMessage()
|
|
102
|
+
: this.readJsonLineMessage();
|
|
103
|
+
}
|
|
104
|
+
detectFraming() {
|
|
105
|
+
if (this.buffer.length === 0) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const preview = this.buffer
|
|
109
|
+
.subarray(0, Math.min(this.buffer.length, 32))
|
|
110
|
+
.toString('utf8')
|
|
111
|
+
.toLowerCase();
|
|
112
|
+
if (preview.startsWith(CONTENT_LENGTH_HEADER)) {
|
|
113
|
+
return 'content-length';
|
|
114
|
+
}
|
|
115
|
+
if (preview.startsWith('{') || preview.startsWith('[')) {
|
|
116
|
+
return 'jsonl';
|
|
117
|
+
}
|
|
118
|
+
const newlineIndex = this.buffer.indexOf(0x0a);
|
|
119
|
+
if (newlineIndex === -1) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const firstLine = this.buffer.toString('utf8', 0, newlineIndex).replace(/\r$/, '').trimStart();
|
|
123
|
+
if (firstLine.toLowerCase().startsWith(CONTENT_LENGTH_HEADER)) {
|
|
124
|
+
return 'content-length';
|
|
125
|
+
}
|
|
126
|
+
if (firstLine.startsWith('{') || firstLine.startsWith('[')) {
|
|
127
|
+
return 'jsonl';
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`Unsupported MCP stdio framing: ${JSON.stringify(firstLine.slice(0, 80))}`);
|
|
130
|
+
}
|
|
131
|
+
readJsonLineMessage() {
|
|
132
|
+
const newlineIndex = this.buffer.indexOf(0x0a);
|
|
133
|
+
if (newlineIndex === -1) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const rawLine = this.buffer.toString('utf8', 0, newlineIndex).replace(/\r$/, '');
|
|
137
|
+
this.buffer = this.buffer.subarray(newlineIndex + 1);
|
|
138
|
+
if (rawLine.trim() === '') {
|
|
139
|
+
return this.readMessage();
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
framing: 'jsonl',
|
|
143
|
+
message: JSON.parse(rawLine),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
readContentLengthMessage() {
|
|
147
|
+
const headerBoundary = this.findHeaderBoundary();
|
|
148
|
+
if (headerBoundary === null) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const { bodyStart, headerEnd } = headerBoundary;
|
|
152
|
+
const headerText = this.buffer.toString('utf8', 0, headerEnd);
|
|
153
|
+
const contentLength = this.parseContentLength(headerText);
|
|
154
|
+
if (this.buffer.length < bodyStart + contentLength) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const bodyText = this.buffer.toString('utf8', bodyStart, bodyStart + contentLength);
|
|
158
|
+
this.buffer = this.buffer.subarray(bodyStart + contentLength);
|
|
159
|
+
return {
|
|
160
|
+
framing: 'content-length',
|
|
161
|
+
message: JSON.parse(bodyText),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
findHeaderBoundary() {
|
|
165
|
+
const crlfBoundary = this.buffer.indexOf('\r\n\r\n');
|
|
166
|
+
if (crlfBoundary !== -1) {
|
|
167
|
+
return {
|
|
168
|
+
bodyStart: crlfBoundary + 4,
|
|
169
|
+
headerEnd: crlfBoundary,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const lfBoundary = this.buffer.indexOf('\n\n');
|
|
173
|
+
if (lfBoundary !== -1) {
|
|
174
|
+
return {
|
|
175
|
+
bodyStart: lfBoundary + 2,
|
|
176
|
+
headerEnd: lfBoundary,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
parseContentLength(headerText) {
|
|
182
|
+
const headers = headerText.split(/\r?\n/);
|
|
183
|
+
for (const header of headers) {
|
|
184
|
+
const separatorIndex = header.indexOf(':');
|
|
185
|
+
if (separatorIndex === -1) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const name = header.slice(0, separatorIndex).trim().toLowerCase();
|
|
189
|
+
if (name !== 'content-length') {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const value = Number.parseInt(header.slice(separatorIndex + 1).trim(), 10);
|
|
193
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
194
|
+
throw new Error(`Invalid Content-Length header: ${header}`);
|
|
195
|
+
}
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
throw new Error('Missing Content-Length header');
|
|
199
|
+
}
|
|
200
|
+
}
|