@hunterzhu/pulse-adapters 0.1.10 → 0.2.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.
@@ -0,0 +1,67 @@
1
+ import { z } from 'zod';
2
+ import { type ToolDefinition } from '@hunterzhu/pulse-tool-sdk';
3
+ declare const pdfInput: z.ZodObject<{
4
+ path: z.ZodString;
5
+ startPage: z.ZodOptional<z.ZodNumber>;
6
+ endPage: z.ZodOptional<z.ZodNumber>;
7
+ maxChars: z.ZodDefault<z.ZodNumber>;
8
+ }, "strict", z.ZodTypeAny, {
9
+ path: string;
10
+ maxChars: number;
11
+ startPage?: number | undefined;
12
+ endPage?: number | undefined;
13
+ }, {
14
+ path: string;
15
+ startPage?: number | undefined;
16
+ endPage?: number | undefined;
17
+ maxChars?: number | undefined;
18
+ }>;
19
+ declare const xlsxInput: z.ZodObject<{
20
+ path: z.ZodString;
21
+ sheet: z.ZodOptional<z.ZodString>;
22
+ maxRows: z.ZodDefault<z.ZodNumber>;
23
+ maxColumns: z.ZodDefault<z.ZodNumber>;
24
+ }, "strict", z.ZodTypeAny, {
25
+ path: string;
26
+ maxRows: number;
27
+ maxColumns: number;
28
+ sheet?: string | undefined;
29
+ }, {
30
+ path: string;
31
+ sheet?: string | undefined;
32
+ maxRows?: number | undefined;
33
+ maxColumns?: number | undefined;
34
+ }>;
35
+ type PdfInput = z.infer<typeof pdfInput>;
36
+ type XlsxInput = z.infer<typeof xlsxInput>;
37
+ export interface PdfReadOutput {
38
+ path: string;
39
+ totalPages: number;
40
+ firstPage: number;
41
+ lastPage: number;
42
+ title?: string;
43
+ author?: string;
44
+ text: string;
45
+ truncated: boolean;
46
+ }
47
+ export interface XlsxInspectOutput {
48
+ path: string;
49
+ creator?: string;
50
+ title?: string;
51
+ subject?: string;
52
+ sheetCount: number;
53
+ worksheets: Array<{
54
+ id: number;
55
+ name: string;
56
+ state: string;
57
+ rowCount: number;
58
+ columnCount: number;
59
+ preview?: Array<{
60
+ row: number;
61
+ cells: Array<string | number | boolean | null>;
62
+ }>;
63
+ }>;
64
+ truncated: boolean;
65
+ }
66
+ export declare function createDocumentTools(workspaceRoot: string): [ToolDefinition<PdfInput, PdfReadOutput>, ToolDefinition<XlsxInput, XlsxInspectOutput>];
67
+ export {};
@@ -0,0 +1,338 @@
1
+ import { lstat, readFile, realpath } from 'node:fs/promises';
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
3
+ import { inflateRawSync } from 'node:zlib';
4
+ import { getDocument } from 'pdfjs-dist/legacy/build/pdf.mjs';
5
+ import ExcelJS from 'exceljs';
6
+ import { z } from 'zod';
7
+ import { ToolError } from '@hunterzhu/pulse-tool-sdk';
8
+ const MAX_INPUT_BYTES = 20 * 1024 * 1024;
9
+ const MAX_PDF_PAGES = 2_000;
10
+ const MAX_PDF_PAGE_RANGE = 50;
11
+ const MAX_OUTPUT_CHARS = 50_000;
12
+ const MAX_XLSX_ENTRIES = 1_000;
13
+ const MAX_XLSX_UNCOMPRESSED_BYTES = 32 * 1024 * 1024;
14
+ const MAX_XLSX_ENTRY_BYTES = 16 * 1024 * 1024;
15
+ const MAX_XLSX_SHEETS = 100;
16
+ const pdfInput = z.object({
17
+ path: z.string().min(1).max(1_024),
18
+ startPage: z.number().int().positive().optional(),
19
+ endPage: z.number().int().positive().optional(),
20
+ maxChars: z.number().int().positive().max(MAX_OUTPUT_CHARS).default(20_000),
21
+ }).strict();
22
+ const xlsxInput = z.object({
23
+ path: z.string().min(1).max(1_024),
24
+ sheet: z.string().min(1).max(128).optional(),
25
+ maxRows: z.number().int().positive().max(100).default(20),
26
+ maxColumns: z.number().int().positive().max(50).default(20),
27
+ }).strict();
28
+ function docError(code, retryable = false, cause) {
29
+ return new ToolError(code, code, { retryable: retryable || isRetryable(cause) });
30
+ }
31
+ function isRetryable(cause) {
32
+ if (cause === null || typeof cause !== 'object')
33
+ return false;
34
+ const code = cause.code;
35
+ return code === 'EAGAIN' || code === 'EBUSY' || code === 'EMFILE' || code === 'ENFILE' || code === 'ETIMEDOUT';
36
+ }
37
+ function within(root, target) {
38
+ const path = relative(root, target);
39
+ return path === '' || (path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path));
40
+ }
41
+ async function readWorkspaceFile(rootInput, path, signal) {
42
+ if (signal.aborted)
43
+ throw docError('ABORTED');
44
+ if (isAbsolute(path) || path.includes('\0'))
45
+ throw docError('PATH_OUTSIDE_WORKSPACE');
46
+ const root = await realpath(rootInput).catch((cause) => { throw docError('WORKSPACE_UNAVAILABLE', false, cause); });
47
+ const candidate = resolve(root, path);
48
+ if (!within(root, candidate))
49
+ throw docError('PATH_OUTSIDE_WORKSPACE');
50
+ const candidateEntry = await lstat(candidate).catch((cause) => { throw docError(cause?.code === 'ENOENT' ? 'DOCUMENT_NOT_FOUND' : 'DOCUMENT_READ_FAILED', false, cause); });
51
+ if (candidateEntry.isSymbolicLink())
52
+ throw docError('PATH_OUTSIDE_WORKSPACE');
53
+ const absolutePath = await realpath(candidate).catch((cause) => { throw docError(cause?.code === 'ENOENT' ? 'DOCUMENT_NOT_FOUND' : 'DOCUMENT_READ_FAILED', false, cause); });
54
+ if (!within(root, absolutePath))
55
+ throw docError('PATH_OUTSIDE_WORKSPACE');
56
+ const entry = await lstat(absolutePath).catch((cause) => { throw docError('DOCUMENT_READ_FAILED', false, cause); });
57
+ if (!entry.isFile())
58
+ throw docError('DOCUMENT_NOT_A_FILE');
59
+ if (entry.size > MAX_INPUT_BYTES)
60
+ throw docError('DOCUMENT_TOO_LARGE');
61
+ const data = await readFile(absolutePath, { signal }).catch((cause) => {
62
+ if (cause.name === 'AbortError')
63
+ throw docError('ABORTED');
64
+ throw docError('DOCUMENT_READ_FAILED', false, cause);
65
+ });
66
+ if (signal.aborted)
67
+ throw docError('ABORTED');
68
+ if (data.byteLength > MAX_INPUT_BYTES)
69
+ throw docError('DOCUMENT_TOO_LARGE');
70
+ return { absolutePath, data };
71
+ }
72
+ function pdfText(items) {
73
+ return items.map((item) => item !== null && typeof item === 'object' && 'str' in item && typeof item.str === 'string' ? item.str : '').join(' ');
74
+ }
75
+ async function readPdf(root, input, signal) {
76
+ const { absolutePath, data } = await readWorkspaceFile(root, input.path, signal);
77
+ let destroyDocument;
78
+ try {
79
+ const loading = getDocument({ data: new Uint8Array(data), useSystemFonts: true, isEvalSupported: false, stopAtErrors: true });
80
+ const pdf = await loading.promise;
81
+ destroyDocument = () => pdf.destroy();
82
+ if (pdf.numPages > MAX_PDF_PAGES)
83
+ throw docError('PDF_PAGE_COUNT_TOO_LARGE');
84
+ const firstPage = input.startPage ?? 1;
85
+ const lastPage = input.endPage ?? Math.min(pdf.numPages, firstPage + MAX_PDF_PAGE_RANGE - 1);
86
+ if (firstPage > pdf.numPages || lastPage > pdf.numPages || lastPage < firstPage)
87
+ throw docError('INVALID_PDF_PAGE_RANGE');
88
+ if (lastPage - firstPage + 1 > MAX_PDF_PAGE_RANGE)
89
+ throw docError('PDF_PAGE_RANGE_TOO_LARGE');
90
+ const metadata = await pdf.getMetadata();
91
+ const info = metadata.info;
92
+ const chunks = [];
93
+ let used = 0;
94
+ let truncated = false;
95
+ for (let pageNumber = firstPage; pageNumber <= lastPage; pageNumber++) {
96
+ if (signal.aborted)
97
+ throw docError('ABORTED');
98
+ const page = await pdf.getPage(pageNumber);
99
+ try {
100
+ const content = await page.getTextContent();
101
+ const text = pdfText(content.items);
102
+ const remaining = input.maxChars - used;
103
+ if (remaining <= 0) {
104
+ truncated = true;
105
+ break;
106
+ }
107
+ const bounded = text.slice(0, remaining);
108
+ if (bounded.length < text.length)
109
+ truncated = true;
110
+ chunks.push(`--- Page ${pageNumber} ---\n${bounded}`);
111
+ used += bounded.length;
112
+ }
113
+ finally {
114
+ page.cleanup();
115
+ }
116
+ if (truncated)
117
+ break;
118
+ }
119
+ return {
120
+ path: input.path,
121
+ totalPages: pdf.numPages,
122
+ firstPage,
123
+ lastPage: Math.min(lastPage, firstPage + chunks.length - 1),
124
+ ...(typeof info.Title === 'string' && info.Title ? { title: info.Title.slice(0, 500) } : {}),
125
+ ...(typeof info.Author === 'string' && info.Author ? { author: info.Author.slice(0, 500) } : {}),
126
+ text: chunks.join('\n\n').slice(0, input.maxChars),
127
+ truncated,
128
+ };
129
+ }
130
+ catch (cause) {
131
+ if (cause instanceof ToolError)
132
+ throw cause;
133
+ if (signal.aborted)
134
+ throw docError('ABORTED');
135
+ throw docError('INVALID_PDF', false, cause);
136
+ }
137
+ finally {
138
+ if (destroyDocument)
139
+ await destroyDocument().catch(() => undefined);
140
+ }
141
+ }
142
+ function checkXlsxArchive(data) {
143
+ if (data.length < 22 || data.readUInt32LE(0) !== 0x04034b50)
144
+ throw docError('INVALID_XLSX_ARCHIVE');
145
+ const lowerBound = Math.max(0, data.length - 65_557);
146
+ let eocd = -1;
147
+ for (let offset = data.length - 22; offset >= lowerBound; offset--) {
148
+ if (data.readUInt32LE(offset) === 0x06054b50) {
149
+ eocd = offset;
150
+ break;
151
+ }
152
+ }
153
+ if (eocd < 0)
154
+ throw docError('INVALID_XLSX_ARCHIVE');
155
+ const disk = data.readUInt16LE(eocd + 4);
156
+ const centralDisk = data.readUInt16LE(eocd + 6);
157
+ const entriesOnDisk = data.readUInt16LE(eocd + 8);
158
+ const entryCount = data.readUInt16LE(eocd + 10);
159
+ const centralSize = data.readUInt32LE(eocd + 12);
160
+ const centralOffset = data.readUInt32LE(eocd + 16);
161
+ const commentLength = data.readUInt16LE(eocd + 20);
162
+ if (disk !== 0 || centralDisk !== 0 || entriesOnDisk !== entryCount || entryCount === 0xffff || entryCount > MAX_XLSX_ENTRIES || eocd + 22 + commentLength > data.length || centralOffset + centralSize > eocd)
163
+ throw docError('INVALID_XLSX_ARCHIVE');
164
+ let offset = centralOffset;
165
+ let totalUncompressed = 0;
166
+ for (let index = 0; index < entryCount; index++) {
167
+ if (offset + 46 > data.length || data.readUInt32LE(offset) !== 0x02014b50)
168
+ throw docError('INVALID_XLSX_ARCHIVE');
169
+ const flags = data.readUInt16LE(offset + 8);
170
+ const compressionMethod = data.readUInt16LE(offset + 10);
171
+ const compressed = data.readUInt32LE(offset + 20);
172
+ const uncompressed = data.readUInt32LE(offset + 24);
173
+ const nameLength = data.readUInt16LE(offset + 28);
174
+ const extraLength = data.readUInt16LE(offset + 30);
175
+ const entryCommentLength = data.readUInt16LE(offset + 32);
176
+ const localOffset = data.readUInt32LE(offset + 42);
177
+ const entrySize = 46 + nameLength + extraLength + entryCommentLength;
178
+ if ((flags & 0x1) !== 0 || compressed === 0xffff_ffff || uncompressed === 0xffff_ffff || uncompressed > MAX_XLSX_ENTRY_BYTES)
179
+ throw docError('UNSUPPORTED_XLSX_ENTRY');
180
+ if (offset + entrySize > data.length || compressed > 0 && uncompressed / compressed > 10_000 || localOffset + 30 > centralOffset || data.readUInt32LE(localOffset) !== 0x04034b50)
181
+ throw docError('INVALID_XLSX_ARCHIVE');
182
+ const localNameLength = data.readUInt16LE(localOffset + 26);
183
+ const localExtraLength = data.readUInt16LE(localOffset + 28);
184
+ const contentOffset = localOffset + 30 + localNameLength + localExtraLength;
185
+ if (contentOffset + compressed > centralOffset || data.readUInt16LE(localOffset + 8) !== compressionMethod)
186
+ throw docError('INVALID_XLSX_ARCHIVE');
187
+ const compressedData = data.subarray(contentOffset, contentOffset + compressed);
188
+ let actualUncompressed;
189
+ try {
190
+ if (compressionMethod === 0)
191
+ actualUncompressed = compressedData.length;
192
+ else if (compressionMethod === 8)
193
+ actualUncompressed = inflateRawSync(compressedData, { maxOutputLength: MAX_XLSX_ENTRY_BYTES + 1 }).byteLength;
194
+ else
195
+ throw docError('UNSUPPORTED_XLSX_COMPRESSION');
196
+ }
197
+ catch (cause) {
198
+ if (cause instanceof ToolError)
199
+ throw cause;
200
+ throw docError('INVALID_XLSX_ARCHIVE', false, cause);
201
+ }
202
+ if (actualUncompressed !== uncompressed)
203
+ throw docError('INVALID_XLSX_ARCHIVE');
204
+ totalUncompressed += actualUncompressed;
205
+ if (totalUncompressed > MAX_XLSX_UNCOMPRESSED_BYTES)
206
+ throw docError('XLSX_CONTENT_TOO_LARGE');
207
+ offset += entrySize;
208
+ }
209
+ if (offset !== centralOffset + centralSize)
210
+ throw docError('INVALID_XLSX_ARCHIVE');
211
+ }
212
+ function cellValue(value, maxChars) {
213
+ if (value === null || value === undefined)
214
+ return null;
215
+ if (typeof value === 'string')
216
+ return value.slice(0, maxChars);
217
+ if (typeof value === 'number' || typeof value === 'boolean')
218
+ return value;
219
+ if (value instanceof Date)
220
+ return value.toISOString();
221
+ if (typeof value === 'object') {
222
+ const record = value;
223
+ if (Array.isArray(record.richText))
224
+ return record.richText.map((part) => part !== null && typeof part === 'object' && 'text' in part ? String(part.text) : '').join('').slice(0, maxChars);
225
+ if (typeof record.text === 'string')
226
+ return record.text.slice(0, maxChars);
227
+ if (typeof record.formula === 'string')
228
+ return `=${record.formula}`.slice(0, maxChars);
229
+ if (typeof record.error === 'string')
230
+ return `#${record.error}`.slice(0, maxChars);
231
+ return '[complex value]';
232
+ }
233
+ return String(value).slice(0, maxChars);
234
+ }
235
+ async function inspectXlsx(root, input, signal) {
236
+ const { data } = await readWorkspaceFile(root, input.path, signal);
237
+ checkXlsxArchive(Buffer.from(data));
238
+ const workbook = new ExcelJS.Workbook();
239
+ try {
240
+ await workbook.xlsx.load(Buffer.from(data));
241
+ if (signal.aborted)
242
+ throw docError('ABORTED');
243
+ const selected = input.sheet === undefined ? undefined : workbook.getWorksheet(input.sheet);
244
+ if (input.sheet !== undefined && !selected)
245
+ throw docError('XLSX_SHEET_NOT_FOUND');
246
+ if (workbook.worksheets.length > MAX_XLSX_SHEETS)
247
+ throw docError('XLSX_SHEET_COUNT_TOO_LARGE');
248
+ const sheets = selected ? [selected] : workbook.worksheets;
249
+ let remaining = MAX_OUTPUT_CHARS - 20_000;
250
+ let truncated = false;
251
+ const worksheets = [];
252
+ for (const sheet of sheets) {
253
+ if (signal.aborted)
254
+ throw docError('ABORTED');
255
+ const rowCount = sheet.actualRowCount;
256
+ const columnCount = sheet.actualColumnCount;
257
+ let preview;
258
+ if (selected) {
259
+ preview = [];
260
+ if (sheet.columnCount > input.maxColumns)
261
+ truncated = true;
262
+ const previewLimit = Symbol('preview-limit');
263
+ try {
264
+ sheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
265
+ if (preview.length >= input.maxRows || remaining <= 0) {
266
+ truncated = true;
267
+ throw previewLimit;
268
+ }
269
+ const cells = [];
270
+ const columnLimit = Math.min(input.maxColumns, Math.max(1, sheet.columnCount));
271
+ for (let column = 1; column <= columnLimit; column++) {
272
+ const cell = cellValue(row.getCell(column).value, Math.min(2_000, remaining));
273
+ const size = Buffer.byteLength(JSON.stringify(cell), 'utf8');
274
+ if (size > remaining) {
275
+ truncated = true;
276
+ remaining = 0;
277
+ break;
278
+ }
279
+ cells.push(cell);
280
+ remaining -= size;
281
+ }
282
+ preview.push({ row: rowNumber, cells });
283
+ if (remaining <= 0)
284
+ throw previewLimit;
285
+ });
286
+ }
287
+ catch (cause) {
288
+ if (cause !== previewLimit)
289
+ throw cause;
290
+ }
291
+ if (rowCount > input.maxRows)
292
+ truncated = true;
293
+ }
294
+ worksheets.push({ id: sheet.id, name: sheet.name.slice(0, 128), state: String(sheet.state), rowCount, columnCount, ...(preview === undefined ? {} : { preview }) });
295
+ }
296
+ const result = {
297
+ path: input.path,
298
+ ...(typeof workbook.creator === 'string' ? { creator: workbook.creator.slice(0, 500) } : {}),
299
+ ...(typeof workbook.title === 'string' ? { title: workbook.title.slice(0, 500) } : {}),
300
+ ...(typeof workbook.subject === 'string' ? { subject: workbook.subject.slice(0, 500) } : {}),
301
+ sheetCount: workbook.worksheets.length,
302
+ worksheets,
303
+ truncated,
304
+ };
305
+ if (Buffer.byteLength(JSON.stringify(result), 'utf8') > MAX_OUTPUT_CHARS)
306
+ throw docError('DOCUMENT_OUTPUT_TOO_LARGE');
307
+ return result;
308
+ }
309
+ catch (cause) {
310
+ if (cause instanceof ToolError)
311
+ throw cause;
312
+ if (signal.aborted)
313
+ throw docError('ABORTED');
314
+ throw docError('INVALID_XLSX', false, cause);
315
+ }
316
+ }
317
+ function manifest(name, description, inputSchema, outputSchema) {
318
+ return { name, version: '1.0.0', description, tags: ['documents', 'read'], inputSchema, outputSchema, concurrencyClass: 'tool', locks: [], supportsAbortSignal: true, sideEffectPolicy: 'read', retrySafety: 'read_only', defaultTimeoutMs: 60_000, maxResultSummaryBytes: 4_096 };
319
+ }
320
+ const pdfManifest = manifest('document.pdf.read', 'Extract bounded text and metadata from a PDF in the workspace.', { type: 'object', required: ['path'], properties: { path: { type: 'string' }, startPage: { type: 'integer', minimum: 1 }, endPage: { type: 'integer', minimum: 1 }, maxChars: { type: 'integer', minimum: 1, maximum: MAX_OUTPUT_CHARS, default: 20_000 } }, additionalProperties: false }, { type: 'object', required: ['path', 'totalPages', 'firstPage', 'lastPage', 'text', 'truncated'], properties: { path: { type: 'string' }, totalPages: { type: 'integer' }, firstPage: { type: 'integer' }, lastPage: { type: 'integer' }, title: { type: 'string' }, author: { type: 'string' }, text: { type: 'string' }, truncated: { type: 'boolean' } }, additionalProperties: false });
321
+ const xlsxManifest = manifest('document.xlsx.inspect', 'List workbook metadata and worksheet dimensions; optionally return a bounded cell preview.', { type: 'object', required: ['path'], properties: { path: { type: 'string' }, sheet: { type: 'string' }, maxRows: { type: 'integer', minimum: 1, maximum: 100, default: 20 }, maxColumns: { type: 'integer', minimum: 1, maximum: 50, default: 20 } }, additionalProperties: false }, { type: 'object', required: ['path', 'sheetCount', 'worksheets', 'truncated'], properties: { path: { type: 'string' }, creator: { type: 'string' }, title: { type: 'string' }, subject: { type: 'string' }, sheetCount: { type: 'integer' }, worksheets: { type: 'array', items: { type: 'object', required: ['id', 'name', 'state', 'rowCount', 'columnCount'], properties: { id: { type: 'integer' }, name: { type: 'string' }, state: { type: 'string' }, rowCount: { type: 'integer' }, columnCount: { type: 'integer' }, preview: { type: 'array', items: { type: 'object', required: ['row', 'cells'], properties: { row: { type: 'integer' }, cells: { type: 'array', items: { anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }, { type: 'null' }] } } }, additionalProperties: false } } }, additionalProperties: false } }, truncated: { type: 'boolean' } }, additionalProperties: false });
322
+ export function createDocumentTools(workspaceRoot) {
323
+ const root = resolve(workspaceRoot);
324
+ return [
325
+ {
326
+ manifest: { ...pdfManifest, locks: [{ resource: `workspace:${root}`, mode: 'shared' }], permissions: { workspaceRoots: [root] } },
327
+ validateInput: (input) => pdfInput.parse(input),
328
+ execute: (input, context) => readPdf(root, input, context.signal),
329
+ summarize: (output) => ({ path: output.path, totalPages: output.totalPages, firstPage: output.firstPage, lastPage: output.lastPage, truncated: output.truncated, textPreview: output.text.slice(0, 1_000) }),
330
+ },
331
+ {
332
+ manifest: { ...xlsxManifest, locks: [{ resource: `workspace:${root}`, mode: 'shared' }], permissions: { workspaceRoots: [root] } },
333
+ validateInput: (input) => xlsxInput.parse(input),
334
+ execute: (input, context) => inspectXlsx(root, input, context.signal),
335
+ summarize: (output) => ({ path: output.path, sheetCount: output.sheetCount, worksheets: output.worksheets.map(({ name, rowCount, columnCount, state }) => ({ name, rowCount, columnCount, state })), truncated: output.truncated }),
336
+ },
337
+ ];
338
+ }
package/dist/index.d.ts CHANGED
@@ -7,5 +7,7 @@ export * from './providers/factory.js';
7
7
  export * from './providers/runtime-executor.js';
8
8
  export * from './tools/filesystem.js';
9
9
  export * from './tools/shell.js';
10
+ export * from './documents/tools.js';
10
11
  export * from './tools/registry.js';
11
12
  export * from './workers/http.js';
13
+ export * from './mcp/index.js';
package/dist/index.js CHANGED
@@ -7,5 +7,7 @@ export * from './providers/factory.js';
7
7
  export * from './providers/runtime-executor.js';
8
8
  export * from './tools/filesystem.js';
9
9
  export * from './tools/shell.js';
10
+ export * from './documents/tools.js';
10
11
  export * from './tools/registry.js';
11
12
  export * from './workers/http.js';
13
+ export * from './mcp/index.js';
@@ -0,0 +1 @@
1
+ export * from './stdio.js';
@@ -0,0 +1 @@
1
+ export * from './stdio.js';
@@ -0,0 +1,78 @@
1
+ import { type JsonValue, type ToolDefinition } from '@hunterzhu/pulse-tool-sdk';
2
+ export interface McpStdioClientOptions {
3
+ command: string;
4
+ args?: string[];
5
+ cwd?: string;
6
+ /** Environment passed to the MCP server. Omit to inherit the current process environment. */
7
+ env?: NodeJS.ProcessEnv;
8
+ clientInfo?: {
9
+ name: string;
10
+ version: string;
11
+ };
12
+ protocolVersion?: string;
13
+ timeoutMs?: number;
14
+ shutdownTimeoutMs?: number;
15
+ maxLineBytes?: number;
16
+ /** Maximum number of tools/list pages accepted from one server. */
17
+ maxToolPages?: number;
18
+ /** Prefix applied to exported Pulse tool names to avoid collisions between servers. */
19
+ namespace?: string;
20
+ }
21
+ export interface McpTool {
22
+ name: string;
23
+ description?: string;
24
+ inputSchema: Record<string, unknown>;
25
+ [key: string]: unknown;
26
+ }
27
+ export interface McpToolCallResult {
28
+ content?: unknown[];
29
+ structuredContent?: unknown;
30
+ isError?: boolean;
31
+ [key: string]: unknown;
32
+ }
33
+ /**
34
+ * Minimal MCP stdio client for the initialize lifecycle and tools capability.
35
+ * The subprocess uses newline-delimited JSON-RPC on stdout; stderr is never parsed as protocol.
36
+ */
37
+ export declare class McpStdioClient {
38
+ private readonly options;
39
+ private child;
40
+ private readonly pending;
41
+ private readonly requestIds;
42
+ private readonly remoteTools;
43
+ private nextId;
44
+ private buffer;
45
+ private closed;
46
+ private started;
47
+ private readonly decoder;
48
+ constructor(options: McpStdioClientOptions);
49
+ /** Starts the child, negotiates the protocol, then discovers the remote tools. */
50
+ connect(): Promise<ToolDefinition<Record<string, JsonValue>, JsonValue>[]>;
51
+ /** Refreshes and returns the server's complete paginated tools/list result. */
52
+ refreshTools(): Promise<McpTool[]>;
53
+ /** Calls one discovered remote tool and returns the JSON-compatible MCP result envelope. */
54
+ callTool(name: string, arguments_: Record<string, JsonValue>, options?: {
55
+ signal?: AbortSignal;
56
+ timeoutMs?: number;
57
+ }): Promise<JsonValue>;
58
+ /** Closes stdin, waits briefly for exit, then terminates a server that stays alive. */
59
+ close(): Promise<void>;
60
+ /** Exposes the discovered tools as Pulse ToolDefinitions, preserving each remote input schema. */
61
+ toToolDefinitions(): ToolDefinition<Record<string, JsonValue>, JsonValue>[];
62
+ private consume;
63
+ private receive;
64
+ private request;
65
+ private cancelRequest;
66
+ private notify;
67
+ private write;
68
+ private ensureConnected;
69
+ private rejectPending;
70
+ private fail;
71
+ }
72
+ /** Starts one MCP server and returns a client plus Pulse-compatible tool definitions. */
73
+ export declare function createMcpStdioAdapter(options: McpStdioClientOptions): Promise<{
74
+ client: McpStdioClient;
75
+ tools: ToolDefinition<Record<string, JsonValue>, JsonValue>[];
76
+ }>;
77
+ /** Creates an opaque namespace suitable for using the server name in Pulse tool names. */
78
+ export declare function mcpToolNamespace(serverName: string): string;