@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/dist/schemas.js CHANGED
@@ -1,184 +1,161 @@
1
- import * as z from 'zod/v4';
2
- const dateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Expected date in YYYY-MM-DD format.');
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 = z.object({}).strict().default({});
5
- export const accountTypeSchema = z.enum(['depository', 'credit', 'loan', 'investment', 'other']);
6
- export const accountSchema = z
7
- .object({
8
- name: z.string(),
9
- type: accountTypeSchema,
10
- subtype: z.string().nullable(),
11
- balance: z.number().finite(),
12
- institution: z.string().nullable(),
13
- mask: z.string().nullable(),
14
- })
15
- .strict();
16
- export const getAccountsOutputSchema = z
17
- .object({
18
- accounts: z.array(accountSchema),
19
- })
20
- .strict();
21
- const getTransactionsInputObjectSchema = z
22
- .object({
23
- limit: z
24
- .int()
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 previous get_transactions response.'),
29
+ .describe('Opaque pagination cursor from a prior get_transactions response.'),
34
30
  last_knowledge: dateTimeSchema
35
31
  .optional()
36
- .describe('Incremental sync checkpoint from a prior get_transactions response. Cannot be combined with cursor.'),
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 string. Keep it unchanged while following a cursor chain.'),
43
- })
44
- .strict()
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
- .strict();
80
- export const snapshotSchema = z
81
- .object({
82
- date: dateSchema,
83
- net_worth: z.number().finite(),
84
- total_assets: z.number().finite(),
85
- total_liabilities: z.number().finite(),
86
- })
87
- .strict();
88
- export const recurringSchema = z
89
- .object({
90
- merchant: z.string(),
91
- amount: z.number().finite(),
92
- frequency: z.string(),
93
- next_date: dateSchema.nullable(),
94
- type: z.string().nullable(),
95
- active: z.boolean(),
96
- })
97
- .strict();
98
- export const budgetSchema = z
99
- .object({
100
- name: z.string(),
101
- amount: z.number().finite(),
102
- spent: z.number().finite(),
103
- remaining: z.number().finite(),
104
- percent_used: z.number().finite(),
105
- period: z.string(),
106
- status: z.string().nullable(),
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
- snapshots: z.array(snapshotSchema),
170
- recurring: z.array(recurringSchema),
171
- budgets: z.array(budgetSchema),
172
- goals: z.array(goalSchema),
173
- health: financialHealthSchema,
174
- liabilities: z.array(liabilitySchema),
175
- holdings: z.array(holdingSchema),
176
- category_spending: z.array(categorySpendingSchema),
177
- })
178
- .strict();
179
- export const verifyKeyOutputSchema = z
180
- .object({
181
- valid: z.boolean(),
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
- .strict();
160
+ valid: z.boolean(),
161
+ };
package/dist/server.d.ts CHANGED
@@ -1,8 +1,15 @@
1
- /**
2
- * Warm MCP stdio entrypoint and shared exports.
3
- *
4
- * The typed MCP contract and Warm API helpers live in `warm-server.ts`.
5
- */
6
- import { API_URL, WARM_SERVER_INFO, apiRequest, createWarmApiClient, createWarmServer, getConfiguredApiKey, verifyWarmApiKey } from './warm-server.js';
7
- export { API_URL, WARM_SERVER_INFO, apiRequest, createWarmApiClient, createWarmServer, getConfiguredApiKey, verifyWarmApiKey, };
8
- export declare function startStdioServer(): Promise<import("@modelcontextprotocol/sdk/server/mcp.js").McpServer>;
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
- * Warm MCP stdio entrypoint and shared exports.
3
- *
4
- * The typed MCP contract and Warm API helpers live in `warm-server.ts`.
5
- */
6
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
- import { API_URL, WARM_SERVER_INFO, apiRequest, createWarmApiClient, createWarmServer, getConfiguredApiKey, verifyWarmApiKey, } from './warm-server.js';
8
- export { API_URL, WARM_SERVER_INFO, apiRequest, createWarmApiClient, createWarmServer, getConfiguredApiKey, verifyWarmApiKey, };
9
- export async function startStdioServer() {
10
- const server = createWarmServer();
11
- const transport = new StdioServerTransport();
12
- await server.connect(transport);
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
  }
@@ -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
+ }