@chrischall/mcp-utils 0.3.0 → 0.4.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 +21 -1
- package/dist/fs/index.d.ts +25 -0
- package/dist/fs/index.d.ts.map +1 -0
- package/dist/fs/index.js +58 -0
- package/dist/fs/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ import light:
|
|
|
27
27
|
|
|
28
28
|
| Import | Contents |
|
|
29
29
|
| --- | --- |
|
|
30
|
-
| `@chrischall/mcp-utils` | core barrel: `server` + `response` + `errors` + `config` + `http` + `zod` + `auth` |
|
|
30
|
+
| `@chrischall/mcp-utils` | core barrel: `server` + `response` + `errors` + `config` + `fs` + `http` + `zod` + `auth` |
|
|
31
31
|
| `@chrischall/mcp-utils/session` | session registry, session store, token manager |
|
|
32
32
|
| `@chrischall/mcp-utils/fetchproxy` | fetchproxy transport adapter, bot-wall / retry / concurrency helpers |
|
|
33
33
|
| `@chrischall/mcp-utils/html` | opt-in HTML scraping helpers (needs `node-html-parser`) |
|
|
@@ -115,6 +115,26 @@ const home = expandPath('~/.config/my-mcp');
|
|
|
115
115
|
`loadDotenvSafely` is a no-throw `.env` loader (returns `false` instead of
|
|
116
116
|
failing when the file is absent).
|
|
117
117
|
|
|
118
|
+
### `fs` — streaming file helpers (uploads)
|
|
119
|
+
|
|
120
|
+
`fileBlob`, `readFileHead`.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { fileBlob, readFileHead } from '@chrischall/mcp-utils';
|
|
124
|
+
|
|
125
|
+
// A file-backed Blob: fetch streams it from disk, never buffered in memory.
|
|
126
|
+
const blob = await fileBlob(path, { type: 'image/jpeg', maxBytes: 20_000_000, label: 'Image' });
|
|
127
|
+
const form = new FormData();
|
|
128
|
+
form.append('file', blob, 'photo.jpg');
|
|
129
|
+
|
|
130
|
+
// Sniff a header (image dimensions, magic bytes) without reading the whole file.
|
|
131
|
+
const head = await readFileHead(path, 65_536);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Use `fileBlob` in place of `new Blob([readFileSync(path)])` for `FormData` uploads
|
|
135
|
+
— `fs.openAsBlob` backs the Blob with the file on disk, so a 20 MB upload uses
|
|
136
|
+
constant memory instead of a 20 MB Buffer.
|
|
137
|
+
|
|
118
138
|
### `http` — bearer API-client kit
|
|
119
139
|
|
|
120
140
|
`createApiClient` plus building blocks: `buildQueryString`, `buildOptionalBody`,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Options for {@link fileBlob}. */
|
|
2
|
+
export interface FileBlobOptions {
|
|
3
|
+
/** MIME type stamped on the Blob (becomes the multipart part's Content-Type). */
|
|
4
|
+
type?: string;
|
|
5
|
+
/** Reject (before any upload) when the file is larger than this many bytes. */
|
|
6
|
+
maxBytes?: number;
|
|
7
|
+
/** Friendly name for the size-limit error message (e.g. "Image"). */
|
|
8
|
+
label?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A **file-backed** `Blob` for streaming multipart uploads. Backed by the file
|
|
12
|
+
* on disk via `fs.openAsBlob`, so the bytes are NOT read into memory — `fetch`
|
|
13
|
+
* streams them from disk as it sends the request body. Use in place of
|
|
14
|
+
* `new Blob([readFileSync(path)])` when building `FormData` for an upload.
|
|
15
|
+
*
|
|
16
|
+
* @throws if the file can't be opened, or (when `maxBytes` is set) is too large.
|
|
17
|
+
*/
|
|
18
|
+
export declare function fileBlob(path: string, opts?: FileBlobOptions): Promise<Blob>;
|
|
19
|
+
/**
|
|
20
|
+
* Read the first `bytes` of a file (for magic-byte / header sniffing — image
|
|
21
|
+
* dimensions, file-type detection) WITHOUT loading the whole file. Returns only
|
|
22
|
+
* as many bytes as were actually read (a short file yields a short buffer).
|
|
23
|
+
*/
|
|
24
|
+
export declare function readFileHead(path: string, bytes: number): Promise<Buffer>;
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fs/index.ts"],"names":[],"mappings":"AAcA,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAatF;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB/E"}
|
package/dist/fs/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// Filesystem helpers for multipart uploads — stream files from disk instead of
|
|
3
|
+
// buffering them in memory.
|
|
4
|
+
//
|
|
5
|
+
// The fleet's upload tools (ofw attachments, evite photos, skylight avatars)
|
|
6
|
+
// repeated `new Blob([readFileSync(path)])` → which loads the WHOLE file into a
|
|
7
|
+
// Node Buffer before the request even starts. `fileBlob()` returns a Blob backed
|
|
8
|
+
// by the file on disk (`fs.openAsBlob`), so `fetch` streams the bytes straight
|
|
9
|
+
// off disk as it sends the multipart body — constant memory regardless of size.
|
|
10
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
import { openAsBlob } from 'node:fs';
|
|
12
|
+
import { open } from 'node:fs/promises';
|
|
13
|
+
/**
|
|
14
|
+
* A **file-backed** `Blob` for streaming multipart uploads. Backed by the file
|
|
15
|
+
* on disk via `fs.openAsBlob`, so the bytes are NOT read into memory — `fetch`
|
|
16
|
+
* streams them from disk as it sends the request body. Use in place of
|
|
17
|
+
* `new Blob([readFileSync(path)])` when building `FormData` for an upload.
|
|
18
|
+
*
|
|
19
|
+
* @throws if the file can't be opened, or (when `maxBytes` is set) is too large.
|
|
20
|
+
*/
|
|
21
|
+
export async function fileBlob(path, opts = {}) {
|
|
22
|
+
let blob;
|
|
23
|
+
try {
|
|
24
|
+
blob = await openAsBlob(path, opts.type !== undefined ? { type: opts.type } : undefined);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error(`Cannot read file for upload: ${path}`);
|
|
28
|
+
}
|
|
29
|
+
if (opts.maxBytes !== undefined && blob.size > opts.maxBytes) {
|
|
30
|
+
throw new Error(`${opts.label ?? 'File'} is ${blob.size} bytes, over the ${opts.maxBytes}-byte limit: ${path}`);
|
|
31
|
+
}
|
|
32
|
+
return blob;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read the first `bytes` of a file (for magic-byte / header sniffing — image
|
|
36
|
+
* dimensions, file-type detection) WITHOUT loading the whole file. Returns only
|
|
37
|
+
* as many bytes as were actually read (a short file yields a short buffer).
|
|
38
|
+
*/
|
|
39
|
+
export async function readFileHead(path, bytes) {
|
|
40
|
+
// Wrap the open like `fileBlob` does, so a missing file yields the same clean,
|
|
41
|
+
// non-leaking message instead of a raw Node ENOENT (path + stack).
|
|
42
|
+
let fh;
|
|
43
|
+
try {
|
|
44
|
+
fh = await open(path, 'r');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new Error(`Cannot read file: ${path}`);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const buf = Buffer.alloc(bytes);
|
|
51
|
+
const { bytesRead } = await fh.read(buf, 0, bytes, 0);
|
|
52
|
+
return buf.subarray(0, bytesRead);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
await fh.close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/fs/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,+EAA+E;AAC/E,4BAA4B;AAC5B,EAAE;AACF,6EAA6E;AAC7E,gFAAgF;AAChF,iFAAiF;AACjF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAE/E,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAmB,MAAM,kBAAkB,CAAC;AAYzD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY,EAAE,OAAwB,EAAE;IACrE,IAAI,IAAU,CAAC;IACf,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC3F,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,gCAAgC,IAAI,EAAE,CAAC,CAAC;IAC1D,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC7D,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,KAAK,IAAI,MAAM,OAAO,IAAI,CAAC,IAAI,oBAAoB,IAAI,CAAC,QAAQ,gBAAgB,IAAI,EAAE,CAC/F,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY,EAAE,KAAa;IAC5D,+EAA+E;IAC/E,mEAAmE;IACnE,IAAI,EAAc,CAAC;IACnB,IAAI,CAAC;QACH,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,qBAAqB,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAChC,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;QACtD,OAAO,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IACpC,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;IACnB,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export * from './server/index.js';
|
|
|
17
17
|
export * from './response/index.js';
|
|
18
18
|
export * from './errors/index.js';
|
|
19
19
|
export * from './config/index.js';
|
|
20
|
+
export * from './fs/index.js';
|
|
20
21
|
export * from './http/index.js';
|
|
21
22
|
export * from './zod/index.js';
|
|
22
23
|
export * from './auth/index.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export * from './server/index.js';
|
|
|
17
17
|
export * from './response/index.js';
|
|
18
18
|
export * from './errors/index.js';
|
|
19
19
|
export * from './config/index.js';
|
|
20
|
+
export * from './fs/index.js';
|
|
20
21
|
export * from './http/index.js';
|
|
21
22
|
export * from './zod/index.js';
|
|
22
23
|
export * from './auth/index.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/mcp-utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Shared scaffolding for the chrischall MCP fleet — server bootstrap, tool-result formatting, helpful errors, hardened env/config, a bearer API-client kit, zod atoms, session registries, a fetchproxy transport adapter, auth resolver skeletons, an in-memory test harness, and opt-in HTML helpers. The generic MCP glue hoisted out of ~19 sibling servers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|