@forestadmin/mcp-server 1.21.0 → 1.22.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 CHANGED
@@ -20,6 +20,7 @@ This MCP server provides HTTP REST API access to Forest Admin operations, enabli
20
20
  | `dissociate` | Dissociate records from a relation |
21
21
  | `getActionForm` | Get the form fields for a custom action |
22
22
  | `executeAction` | Execute a custom action |
23
+ | `requestActionFileUpload` | Get a destination to upload a file to, for an action `File` field (only with `fileUploads`) |
23
24
 
24
25
  ## Usage
25
26
 
@@ -67,6 +68,8 @@ yarn start:dev # Development (loads .env file automatically)
67
68
  | `FOREST_AGENT_URL` | No | your environment's back-end URL | URL the MCP server uses to reach the back-end's data layer. Set it when the server runs next to a self-hosted back-end at an internal address (e.g. `http://localhost:3310`), instead of the public URL registered in Forest |
68
69
  | `FOREST_MCP_ACCESS_TOKEN_TTL_SECONDS` | No | `3600` (1 hour) | Maximum lifetime of the OAuth access tokens the server issues (`tokenTtl.accessTokenSeconds`). Minimum `60` |
69
70
  | `FOREST_MCP_REFRESH_TOKEN_TTL_SECONDS` | No | unbounded | Maximum time between two interactive logins (`tokenTtl.refreshTokenSeconds`). Unset, a client that keeps refreshing never signs in again. Minimum `60` |
71
+ | `FOREST_MCP_FILE_UPLOADS` | No | - | `false` turns action file uploads off (they are **on** by default, in memory). Any other value than `true`/`false` fails at startup |
72
+ | `FOREST_MCP_UPLOAD_STORAGE_MODULE` | No | - | Path to a module providing the `fileUploads` options, for a real storage backend. `FOREST_MCP_FILE_UPLOADS=false` wins over it |
70
73
 
71
74
  #### Example Configuration
72
75
 
@@ -164,6 +167,250 @@ The two settings differ in what the user notices:
164
167
 
165
168
  The minimum for either value is 60 seconds; anything lower is raised to it. An invalid value (zero, negative, fractional) fails at startup rather than silently leaving the tokens uncapped.
166
169
 
170
+ ## Action File Uploads
171
+
172
+ > **Experimental.** The MCP specification is still designing its own file transfer story
173
+ > ([SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631)). The
174
+ > `UploadStorage` contract is expected to survive, but the `requestActionFileUpload` tool and the handle
175
+ > format may change to follow the specification once it lands.
176
+
177
+ Actions with **File fields** cannot normally run over MCP. The agent expects file values as data
178
+ uris, which would transit the model's context window and exceed most MCP clients' payload limits.
179
+ The `fileUploads` option enables them through an upload side-channel that keeps the bytes out of
180
+ the conversation:
181
+
182
+ 1. The client calls the `requestActionFileUpload` tool with `{ "filename", "mimeType", "sha256"? }` and receives a pre-authorized upload URL plus a signed `fileHandle` string.
183
+ 2. The client uploads the raw bytes directly to the storage backend, so they never pass through the MCP server or the model.
184
+ 3. The client passes the handle (`"$uploadedFile:<...>"`) as the field value in `executeAction`. The server downloads the object and hands it to the agent. The model only ever exchanges the small handle.
185
+
186
+ `requestActionFileUpload` follows `enabledTools` like every other tool, so a server that leaves it out never advertises it and never serves the upload endpoint.
187
+
188
+ The upload URL is unauthenticated — the model holds no agent credential, and must not — so the URL
189
+ itself is the authorization, as with an S3 presigned PUT. It carries a random uuid, is refused
190
+ before a byte is read unless this server issued it, expires with `uploadUrlTtlSeconds`, and serves
191
+ nothing but the `PUT`. Against the in-memory store it also **accepts a single upload**: once the
192
+ bytes land, a leaked URL can no longer replace them. Writing is not consuming either — redemption
193
+ needs the signed handle, which is bound to the user who requested it.
194
+
195
+ A presigned backend URL is a different animal: it is typically **replayable** until it expires — S3
196
+ accepts as many `PUT`s as fit in `expiresInSeconds` — so there, a URL leaked to an access log can
197
+ overwrite the bytes *after* the legitimate upload and before the action runs. The `sha256` pin is
198
+ the defense that covers every backend at once: S3 signs it into the URL, so a different payload is
199
+ rejected at upload time, and redemption re-verifies the digest regardless of what the backend
200
+ checked. The tool instructs the model to pin by default; treat an unpinned upload as accepting that
201
+ window.
202
+
203
+ ### Nothing to provision, and nothing to switch on
204
+
205
+ It is on by default. With no `storage`, the server holds the objects in memory and serves its own
206
+ upload endpoint under `<origin>/mcp/uploads`. The `fileUploads` option only *configures* that — a
207
+ backend, size limits, ttls:
208
+
209
+ ```typescript
210
+ agent.mountAiMcpServer(); // in memory, single instance
211
+ agent.mountAiMcpServer({ fileUploads: { storage } }); // a real backend
212
+ ```
213
+
214
+ To turn the feature off, pass `fileUploads: false` — on the standalone server,
215
+ `FOREST_MCP_FILE_UPLOADS=false`. The tool is not registered, the upload endpoint is never mounted,
216
+ and `executeAction` stops mentioning either. Going through `enabledTools` would work too, but it is
217
+ an allowlist — declining this one feature that way means naming every other tool and opting out of
218
+ everything shipped after.
219
+
220
+ > **Single instance only.** The upload and the redemption are two separate requests. With several
221
+ > replicas, in cluster mode, or on a serverless runtime, one of them lands
222
+ > on an instance that never saw the other and the action fails — intermittently, which reads as a
223
+ > flaky feature rather than a misconfiguration. Objects are also lost on restart. The server warns
224
+ > the first time an upload destination is asked for, and the failure names this cause. **Those deployments need a `storage`.**
225
+
226
+ `ephemeralMaxTotalBytes` bounds what the in-memory store holds across all pending uploads, 64 MiB by
227
+ default. Redeeming a file does not free it — the object lives until `handleTtlSeconds` so a retry
228
+ after a failed action still finds it — so on the defaults the store holds about three max-size
229
+ files per 45-minute window rather than a rolling 64 MiB. Size it against that, or shorten
230
+ `handleTtlSeconds`. It is deliberately absolute rather than a multiple of `maxBytes`: derived, raising the
231
+ per-file limit would multiply what the process can hold.
232
+
233
+ ### With a storage backend
234
+
235
+ Provide `storage` for anything beyond a single instance. Any backend that can pre-authorize an
236
+ upload and read the object back works — S3 presigned URLs (below), GCS signed URLs, Azure SAS. The
237
+ package has no storage dependency of its own.
238
+
239
+ ### On the standalone server
240
+
241
+ Uploads are on with the in-memory store, with the same single-instance caveat.
242
+
243
+ For a real backend, a storage is an object with methods, so unlike every other standalone option it
244
+ cannot travel through an environment variable. Point `FOREST_MCP_UPLOAD_STORAGE_MODULE` at a module
245
+ that default-exports the options instead — a bad path, or a module that exports nothing, fails at
246
+ startup rather than running with uploads silently disabled:
247
+
248
+ ```javascript
249
+ // forest-upload-storage.js
250
+ module.exports = {
251
+ storage: {
252
+ /* createUploadUrl / download / getSize, as below */
253
+ },
254
+ maxBytes: 50 * 1024 * 1024,
255
+ downloadTimeoutSeconds: 10,
256
+ };
257
+ ```
258
+
259
+ ```bash
260
+ FOREST_MCP_UPLOAD_STORAGE_MODULE=./forest-upload-storage.js npx forest-mcp-server
261
+ ```
262
+
263
+ The module may also export a function, sync or async, returning the same options — useful when the
264
+ backend needs credentials fetched at boot.
265
+
266
+ ### The client must be able to upload
267
+
268
+ Step 2 is an ordinary HTTPS request, made by the client, outside the MCP protocol. The client has to
269
+ be able to make it:
270
+
271
+ - **Claude Code** and custom agents: works. The shell runs on the same machine as the developer, so
272
+ it reaches a `localhost` agent too — this is the one place the whole flow can be tried end to end
273
+ against a local agent. Verified.
274
+ - **Claude Desktop, Claude.ai and Cowork**: the attached file lands in the code execution sandbox
275
+ and the model can `curl -X PUT -T <path> <uploadUrl>` — applying every header the tool returned,
276
+ since a pinned `sha256` is signed into `x-amz-checksum-sha256` on S3 and the PUT is rejected
277
+ without it. Two conditions, and both are needed:
278
+ 1. **`uploadUrl` must be publicly reachable.** That sandbox is hosted and runs on its own
279
+ network, so a `localhost` or private address is never reachable from it, whatever else is
280
+ configured. An agent running on a developer's machine cannot be tested this way.
281
+ 2. **Its host must be allowed for outbound traffic** in that sandbox. Where this is configured
282
+ differs between clients and versions — and on a managed workspace the setting may not be
283
+ exposed to the end user at all, in which case only an administrator can unblock the upload.
284
+ Do not promise your users a menu path; give them the host to get allowed.
285
+
286
+ Both are client-side and outside this server's control, so document your upload host for your
287
+ users.
288
+
289
+ **Verified end to end** from a Claude Desktop chat and from a Cowork cloud session: an agent
290
+ behind a public HTTPS URL, that host added to the sandbox's allowed domains, and the model's
291
+ `PUT` goes through. Before the host was allowed, the same sandbox answered `Host not in
292
+ allowlist` even for an ordinary public domain — so the allowlist is the whole of condition 2,
293
+ and satisfying it is enough. The Cowork run started from a single natural sentence, with no tool
294
+ named: the model found the form, requested a destination and pinned the `sha256` unprompted, and
295
+ no tool argument carried base64 — checked against the request bodies at the tunnel, not the
296
+ transcript.
297
+
298
+ One wrinkle observed there: the filename the action stores is whatever the client reports, and a
299
+ sandbox may normalize it (`rapport-1815.pdf` arrived as `rapport1815.pdf` while the bytes and
300
+ mime type were exact). Treat it as a label, not an identifier.
301
+
302
+ The tool states this prerequisite in its description and repeats it in its response, so a model
303
+ whose upload was blocked has the diagnosis in context.
304
+
305
+ ### Trying it locally
306
+
307
+ `packages/_example` needs no configuration for this — no cloud account, no storage code.
308
+ Its `review` collection carries an `Attach a document` action with a `File` and a `FileList` field.
309
+ Start the example agent, connect an MCP client to it, and ask for that action with a file — the
310
+ action reports the name, mime type and byte count it received.
311
+
312
+ ```mermaid
313
+ sequenceDiagram
314
+ participant Client as MCP client
315
+ participant Server as MCP server
316
+ participant Storage as Storage backend
317
+ participant Agent as Forest Admin agent
318
+
319
+ Client->>Server: requestActionFileUpload {filename, mimeType, sha256?}
320
+ Server-->>Client: uploadUrl + fileHandle (user-bound JWT)
321
+ Client->>Storage: PUT raw bytes to uploadUrl
322
+ Note over Client,Storage: bytes bypass the server and the model
323
+ Client->>Server: executeAction {values: {field: "$uploadedFile:..."}}
324
+ Server->>Storage: download object
325
+ Note over Server: verify user, TTL, maxBytes, sha256 pin
326
+ Server->>Agent: executeAction with the file
327
+ Agent-->>Server: action result
328
+ Server-->>Client: result (the model only saw the handle)
329
+ ```
330
+
331
+ The storage backend is pluggable, and this package has no storage dependency. Provide an
332
+ implementation of `UploadStorage`; any backend that can pre-authorize an upload and read the object
333
+ back works, such as S3 presigned URLs (below), GCS signed URLs, Azure SAS, or an endpoint you serve
334
+ yourself. The only hard requirement is that the URL be reachable from the MCP client, since that is
335
+ what uploads the bytes.
336
+
337
+ ```typescript
338
+ import {
339
+ S3Client,
340
+ GetObjectCommand,
341
+ HeadObjectCommand,
342
+ PutObjectCommand,
343
+ } from '@aws-sdk/client-s3';
344
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
345
+ import type { UploadStorage } from '@forestadmin/mcp-server';
346
+
347
+ const s3 = new S3Client({});
348
+ const bucket = 'my-uploads-bucket';
349
+
350
+ const storage: UploadStorage = {
351
+ async createUploadUrl({ key, mimeType, sha256, expiresInSeconds }) {
352
+ const command = new PutObjectCommand({
353
+ Bucket: bucket,
354
+ Key: key,
355
+ ContentType: mimeType,
356
+ ...(sha256 && { ChecksumSHA256: sha256 }),
357
+ });
358
+ const url = await getSignedUrl(s3, command, {
359
+ expiresIn: expiresInSeconds,
360
+ // Load-bearing: without it the checksum is hoisted to the query string, which S3 does not
361
+ // enforce — the pin would silently stop protecting the upload.
362
+ ...(sha256 && { unhoistableHeaders: new Set(['x-amz-checksum-sha256']) }),
363
+ });
364
+ return {
365
+ url,
366
+ headers: { 'Content-Type': mimeType, ...(sha256 && { 'x-amz-checksum-sha256': sha256 }) },
367
+ };
368
+ },
369
+ async getSize(key) {
370
+ const head = await s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
371
+ return head.ContentLength;
372
+ },
373
+ async download(key) {
374
+ const object = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
375
+ return Buffer.from(await object.Body.transformToByteArray());
376
+ },
377
+ };
378
+
379
+ const server = new ForestMCPServer({
380
+ // ...
381
+ fileUploads: { storage },
382
+ });
383
+ ```
384
+
385
+ The other options are `keyPrefix` (default `mcp-uploads/`), `uploadUrlTtlSeconds` (default 15 min),
386
+ `handleTtlSeconds` (default 45 min, longer than the upload URL so a slow upload still leaves time to
387
+ run the action), `maxBytes` (default 20 MiB), `maxConcurrentDownloads` (default 5), and
388
+ `downloadTimeoutSeconds` (default 15 s), and `ephemeralMaxTotalBytes` (default 64 MiB, in-memory store only).
389
+
390
+ Lower `downloadTimeoutSeconds` if the clients calling your agent cut requests sooner than that. The
391
+ whole `executeAction` has to fit inside their timeout: reading the object, encoding it, and running
392
+ your action. A read that outlives the caller is wasted work — and left unbounded it would hold its
393
+ concurrency slot after the caller gave up.
394
+
395
+ A few properties matter in production.
396
+
397
+ - The server stays stateless. The handle is a JWT signed with `authSecret`, so there is no database and no session affinity, and any replica can redeem a handle issued by another.
398
+ - A handle is bound to the user it was issued to. Only that user's Bearer token can redeem it, and it expires with `handleTtlSeconds`. It stays redeemable until then, so keep the TTL short.
399
+ - When the client sends `sha256` (hex or base64), the upload URL is pinned to that digest and the digest is checked again on the downloaded bytes at redemption. Content substituted after an upload URL leak cannot be redeemed.
400
+ - **`maxBytes` does not bound memory on its own.** A pre-authorized upload URL cannot always cap the object size, so the limit is enforced at redemption: before downloading when `getSize` reports a size, and only after the bytes are in memory when it returns `undefined`. Implement `getSize` whenever the backend can answer it cheaply.
401
+ - **`maxConcurrentDownloads` bounds concurrent downloads, not peak memory.** All the files one `executeAction` call references are held together until the call completes, so a form with N file fields holds up to N × `maxBytes` whatever the concurrency limit is. Size `maxBytes` against the number of file fields your actions declare.
402
+ - **A timed-out read is abandoned, not cancelled.** `UploadStorage.download` takes no `AbortSignal`, so when `downloadTimeoutSeconds` fires the concurrency slot is freed while the underlying read keeps running. Against a backend slower than that timeout the number of reads in flight can therefore exceed `maxConcurrentDownloads`. Keep `downloadTimeoutSeconds` low so a slow backend fails fast instead of accumulating.
403
+ - The server never deletes objects, and does not delete them once redeemed either: `executeAction` downloads every reference before it sets fields or runs, so consuming on read would leave a model retrying after any later failure with handles whose objects are gone, told the upload failed when it had not. Configure a lifecycle rule on the storage backend, for example deleting objects under `keyPrefix` after one day; the in-memory store reclaims on `handleTtlSeconds` and on its own total.
404
+
405
+ Only `executeAction` resolves handles. `getActionForm` echoes field values back to the model, so a
406
+ handle stays a handle there: resolving it would put the file content back into the model's context.
407
+
408
+ **Change hooks never see a handle.** On the `getActionForm` path, file references are withheld from
409
+ `tryToSetFields`, so a hook fired by another field reads the file field as unset rather than as a
410
+ string it would call `.buffer` on. On the `executeAction` path the handles are already resolved, so
411
+ a hook receives the file as the data uri it expects. Either way a hook never has to know this
412
+ side-channel exists.
413
+
167
414
  ## API Endpoints
168
415
 
169
416
  Once running, the MCP server exposes the following endpoints:
package/dist/cli.js CHANGED
@@ -5,26 +5,45 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const server_1 = __importDefault(require("./server"));
8
+ const load_file_uploads_1 = __importDefault(require("./utils/load-file-uploads"));
8
9
  const parse_domain_list_1 = __importDefault(require("./utils/parse-domain-list"));
9
10
  const parse_tool_list_1 = __importDefault(require("./utils/parse-tool-list"));
10
11
  const toSeconds = (value) => (value === undefined ? undefined : Number(value));
11
- // Start the server when run directly as CLI
12
- const server = new server_1.default({
13
- forestServerUrl: process.env.FOREST_SERVER_URL || 'https://api.forestadmin.com',
14
- forestAppUrl: process.env.FOREST_APP_URL || 'https://app.forestadmin.com',
15
- envSecret: process.env.FOREST_ENV_SECRET,
16
- authSecret: process.env.FOREST_AUTH_SECRET,
17
- enabledTools: (0, parse_tool_list_1.default)(process.env.FOREST_MCP_ENABLED_TOOLS),
18
- allowedOAuthClients: (0, parse_domain_list_1.default)(process.env.FOREST_MCP_ALLOWED_OAUTH_CLIENTS),
19
- agentUrl: process.env.FOREST_AGENT_URL,
20
- // normalizeTokenTtl rejects NaN and non-positive values, so a bad variable fails at startup.
21
- tokenTtl: {
22
- accessTokenSeconds: toSeconds(process.env.FOREST_MCP_ACCESS_TOKEN_TTL_SECONDS),
23
- refreshTokenSeconds: toSeconds(process.env.FOREST_MCP_REFRESH_TOKEN_TTL_SECONDS),
24
- },
25
- });
26
- server.run().catch(error => {
12
+ async function main() {
13
+ // Uploads are on by default, so this variable only means one thing: 'false' turns them off.
14
+ // Anything else fails at startup like every other option — 'FALSE' or '0' silently leaving the
15
+ // feature on is the kind of surprise an operator meets in production.
16
+ const rawFileUploadsFlag = process.env.FOREST_MCP_FILE_UPLOADS;
17
+ if (rawFileUploadsFlag !== undefined && !['true', 'false'].includes(rawFileUploadsFlag)) {
18
+ throw new Error(`Invalid FOREST_MCP_FILE_UPLOADS "${rawFileUploadsFlag}": use 'false' to turn action file ` +
19
+ 'uploads off. They are on by default.');
20
+ }
21
+ // Loaded before constructing, so a bad module fails at startup like every other option. Uploads
22
+ // are on without it, held in memory; this only points them at a real backend. 'false' wins over
23
+ // a configured module — an operator setting both is turning the feature off.
24
+ const fileUploads = rawFileUploadsFlag === 'false'
25
+ ? false
26
+ : await (0, load_file_uploads_1.default)(process.env.FOREST_MCP_UPLOAD_STORAGE_MODULE);
27
+ const server = new server_1.default({
28
+ forestServerUrl: process.env.FOREST_SERVER_URL || 'https://api.forestadmin.com',
29
+ forestAppUrl: process.env.FOREST_APP_URL || 'https://app.forestadmin.com',
30
+ envSecret: process.env.FOREST_ENV_SECRET,
31
+ authSecret: process.env.FOREST_AUTH_SECRET,
32
+ enabledTools: (0, parse_tool_list_1.default)(process.env.FOREST_MCP_ENABLED_TOOLS),
33
+ allowedOAuthClients: (0, parse_domain_list_1.default)(process.env.FOREST_MCP_ALLOWED_OAUTH_CLIENTS),
34
+ agentUrl: process.env.FOREST_AGENT_URL,
35
+ // normalizeTokenTtl rejects NaN and non-positive values, so a bad variable fails at startup.
36
+ tokenTtl: {
37
+ accessTokenSeconds: toSeconds(process.env.FOREST_MCP_ACCESS_TOKEN_TTL_SECONDS),
38
+ refreshTokenSeconds: toSeconds(process.env.FOREST_MCP_REFRESH_TOKEN_TTL_SECONDS),
39
+ },
40
+ // Not a truthy spread: `false` is a meaningful value and has to reach the constructor.
41
+ ...(fileUploads !== undefined && { fileUploads }),
42
+ });
43
+ await server.run();
44
+ }
45
+ main().catch(error => {
27
46
  console.error('[FATAL] Server crashed:', error);
28
47
  process.exit(1);
29
48
  });
30
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2NsaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFFQSxzREFBdUM7QUFDdkMsa0ZBQXdEO0FBQ3hELDhFQUFvRDtBQUVwRCxNQUFNLFNBQVMsR0FBRyxDQUFDLEtBQWMsRUFBRSxFQUFFLENBQUMsQ0FBQyxLQUFLLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDO0FBRXhGLDRDQUE0QztBQUM1QyxNQUFNLE1BQU0sR0FBRyxJQUFJLGdCQUFlLENBQUM7SUFDakMsZUFBZSxFQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsaUJBQWlCLElBQUksNkJBQTZCO0lBQy9FLFlBQVksRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsSUFBSSw2QkFBNkI7SUFDekUsU0FBUyxFQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsaUJBQWlCO0lBQ3hDLFVBQVUsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLGtCQUFrQjtJQUMxQyxZQUFZLEVBQUUsSUFBQSx5QkFBYSxFQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsd0JBQXdCLENBQUM7SUFDakUsbUJBQW1CLEVBQUUsSUFBQSwyQkFBZSxFQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsZ0NBQWdDLENBQUM7SUFDbEYsUUFBUSxFQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsZ0JBQWdCO0lBQ3RDLDZGQUE2RjtJQUM3RixRQUFRLEVBQUU7UUFDUixrQkFBa0IsRUFBRSxTQUFTLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxtQ0FBbUMsQ0FBQztRQUM5RSxtQkFBbUIsRUFBRSxTQUFTLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxvQ0FBb0MsQ0FBQztLQUNqRjtDQUNGLENBQUMsQ0FBQztBQUVILE1BQU0sQ0FBQyxHQUFHLEVBQUUsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLEVBQUU7SUFDekIsT0FBTyxDQUFDLEtBQUssQ0FBQyx5QkFBeUIsRUFBRSxLQUFLLENBQUMsQ0FBQztJQUNoRCxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQ2xCLENBQUMsQ0FBQyxDQUFDIn0=
49
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2NsaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFFQSxzREFBdUM7QUFDdkMsa0ZBQXdEO0FBQ3hELGtGQUF3RDtBQUN4RCw4RUFBb0Q7QUFFcEQsTUFBTSxTQUFTLEdBQUcsQ0FBQyxLQUFjLEVBQUUsRUFBRSxDQUFDLENBQUMsS0FBSyxLQUFLLFNBQVMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQztBQUV4RixLQUFLLFVBQVUsSUFBSTtJQUNqQiw0RkFBNEY7SUFDNUYsK0ZBQStGO0lBQy9GLHNFQUFzRTtJQUN0RSxNQUFNLGtCQUFrQixHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsdUJBQXVCLENBQUM7SUFFL0QsSUFBSSxrQkFBa0IsS0FBSyxTQUFTLElBQUksQ0FBQyxDQUFDLE1BQU0sRUFBRSxPQUFPLENBQUMsQ0FBQyxRQUFRLENBQUMsa0JBQWtCLENBQUMsRUFBRSxDQUFDO1FBQ3hGLE1BQU0sSUFBSSxLQUFLLENBQ2Isb0NBQW9DLGtCQUFrQixxQ0FBcUM7WUFDekYsc0NBQXNDLENBQ3pDLENBQUM7SUFDSixDQUFDO0lBRUQsZ0dBQWdHO0lBQ2hHLGdHQUFnRztJQUNoRyw2RUFBNkU7SUFDN0UsTUFBTSxXQUFXLEdBQ2Ysa0JBQWtCLEtBQUssT0FBTztRQUM1QixDQUFDLENBQUUsS0FBZTtRQUNsQixDQUFDLENBQUMsTUFBTSxJQUFBLDJCQUFlLEVBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxnQ0FBZ0MsQ0FBQyxDQUFDO0lBRTFFLE1BQU0sTUFBTSxHQUFHLElBQUksZ0JBQWUsQ0FBQztRQUNqQyxlQUFlLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxpQkFBaUIsSUFBSSw2QkFBNkI7UUFDL0UsWUFBWSxFQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxJQUFJLDZCQUE2QjtRQUN6RSxTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxpQkFBaUI7UUFDeEMsVUFBVSxFQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsa0JBQWtCO1FBQzFDLFlBQVksRUFBRSxJQUFBLHlCQUFhLEVBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyx3QkFBd0IsQ0FBQztRQUNqRSxtQkFBbUIsRUFBRSxJQUFBLDJCQUFlLEVBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxnQ0FBZ0MsQ0FBQztRQUNsRixRQUFRLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxnQkFBZ0I7UUFDdEMsNkZBQTZGO1FBQzdGLFFBQVEsRUFBRTtZQUNSLGtCQUFrQixFQUFFLFNBQVMsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLG1DQUFtQyxDQUFDO1lBQzlFLG1CQUFtQixFQUFFLFNBQVMsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLG9DQUFvQyxDQUFDO1NBQ2pGO1FBQ0QsdUZBQXVGO1FBQ3ZGLEdBQUcsQ0FBQyxXQUFXLEtBQUssU0FBUyxJQUFJLEVBQUUsV0FBVyxFQUFFLENBQUM7S0FDbEQsQ0FBQyxDQUFDO0lBRUgsTUFBTSxNQUFNLENBQUMsR0FBRyxFQUFFLENBQUM7QUFDckIsQ0FBQztBQUVELElBQUksRUFBRSxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUMsRUFBRTtJQUNuQixPQUFPLENBQUMsS0FBSyxDQUFDLHlCQUF5QixFQUFFLEtBQUssQ0FBQyxDQUFDO0lBQ2hELE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDbEIsQ0FBQyxDQUFDLENBQUMifQ==
@@ -0,0 +1,46 @@
1
+ import type { UploadStorage } from './types';
2
+ import type { Logger } from '../server';
3
+ import type { Router } from 'express';
4
+ interface EphemeralOptions {
5
+ maxBytes: number;
6
+ maxTotalBytes: number;
7
+ ttlSeconds: number;
8
+ issuedTtlSeconds: number;
9
+ publicBaseUrl: string;
10
+ }
11
+ /**
12
+ * In-memory UploadStorage serving its own PUT endpoint on the MCP server's own origin, used when
13
+ * `fileUploads` is enabled without a storage backend. Nothing to provision — and nothing shared:
14
+ * objects live in this process only.
15
+ *
16
+ * The upload and the redemption are two separate requests, so behind several replicas, in cluster
17
+ * mode, or on a serverless runtime one lands on an instance that never saw the other. Those
18
+ * deployments need a real backend; this one is for a single instance.
19
+ */
20
+ export default class EphemeralStorage implements UploadStorage {
21
+ private readonly logger;
22
+ private readonly objects;
23
+ private readonly issued;
24
+ private storedBytes;
25
+ private inFlightBytes;
26
+ private options;
27
+ private announced;
28
+ constructor(logger: Logger);
29
+ /** Separate from the constructor because the server only knows its own base url later. */
30
+ configure(options: EphemeralOptions): void;
31
+ createUploadUrl({ key }: {
32
+ key: string;
33
+ }): Promise<{
34
+ url: string;
35
+ method: string;
36
+ }>;
37
+ download(key: string): Promise<Buffer>;
38
+ getSize(key: string): Promise<number | undefined>;
39
+ createRouter(): Router;
40
+ private write;
41
+ private read;
42
+ private forget;
43
+ private expire;
44
+ }
45
+ export {};
46
+ //# sourceMappingURL=ephemeral-storage.d.ts.map
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const express_1 = __importDefault(require("express"));
7
+ // Issued keys are cheap next to a body — a few hundred bytes against up to maxBytes — but they
8
+ // outlive the request that asked for one, and nothing forces the caller to ever upload. This bounds
9
+ // them at about 2 MB, orders of magnitude beyond any real number of pending uploads.
10
+ const MAX_OUTSTANDING_KEYS = 10000;
11
+ /**
12
+ * In-memory UploadStorage serving its own PUT endpoint on the MCP server's own origin, used when
13
+ * `fileUploads` is enabled without a storage backend. Nothing to provision — and nothing shared:
14
+ * objects live in this process only.
15
+ *
16
+ * The upload and the redemption are two separate requests, so behind several replicas, in cluster
17
+ * mode, or on a serverless runtime one lands on an instance that never saw the other. Those
18
+ * deployments need a real backend; this one is for a single instance.
19
+ */
20
+ class EphemeralStorage {
21
+ constructor(logger) {
22
+ this.logger = logger;
23
+ this.objects = new Map();
24
+ this.issued = new Map();
25
+ this.storedBytes = 0;
26
+ this.inFlightBytes = 0;
27
+ this.announced = false;
28
+ }
29
+ /** Separate from the constructor because the server only knows its own base url later. */
30
+ configure(options) {
31
+ this.options = options;
32
+ }
33
+ async createUploadUrl({ key }) {
34
+ const { publicBaseUrl, issuedTtlSeconds } = this.options;
35
+ // Said here rather than at startup: uploads are on by default, so a boot-time warning would
36
+ // reach every agent including those whose actions have no file field. This fires when the
37
+ // feature is actually used, which is when the limitation starts to matter.
38
+ if (!this.announced) {
39
+ this.announced = true;
40
+ this.logger('Warn', '[fileUploads] no storage backend: uploaded files are held in memory, on this instance ' +
41
+ 'only. They are lost on restart, and a deployment with several replicas or a serverless ' +
42
+ 'runtime needs a real backend on the fileUploads option.');
43
+ }
44
+ // Recorded so the endpoint only accepts keys it handed out. Without this, anything reaching the
45
+ // origin could fill the store under keys of its own and deny the feature to everyone else.
46
+ this.expire();
47
+ // Insertion order, so this drops the least recent pending upload. Refusing to issue instead
48
+ // would let one caller deny the feature to everyone; whoever is evicted gets a 404 telling
49
+ // them to ask again.
50
+ if (this.issued.size >= MAX_OUTSTANDING_KEYS) {
51
+ const [oldest] = this.issued.keys();
52
+ this.issued.delete(oldest);
53
+ this.logger('Warn', `[fileUploads] ${MAX_OUTSTANDING_KEYS} upload urls are outstanding: dropped ${oldest}, ` +
54
+ 'whose upload never came. Something is requesting destinations without uploading.');
55
+ }
56
+ this.issued.set(key, Date.now() + issuedTtlSeconds * 1000);
57
+ return {
58
+ url: `${publicBaseUrl.replace(/\/+$/, '')}/${encodeURIComponent(key)}`,
59
+ method: 'PUT',
60
+ };
61
+ }
62
+ async download(key) {
63
+ const stored = this.read(key);
64
+ if (!stored) {
65
+ throw new Error('not found in the in-memory store. Either the upload never completed — a refused one is ' +
66
+ 'answered with a 413 — or it expired after handleTtlSeconds, or it reached another ' +
67
+ 'instance: this store only holds what this instance received, so several replicas or a ' +
68
+ 'serverless runtime need a storage backend on the fileUploads option.');
69
+ }
70
+ // Deliberately NOT dropped on read. executeAction downloads every reference before it sets
71
+ // fields or runs, so any later failure — a mistyped field name, a throwing hook — would leave
72
+ // the model retrying with handles whose objects are gone, told the upload failed when it did
73
+ // not. expire() and maxTotalBytes reclaim instead. The upload url stays single-use; that is a
74
+ // different property, enforced on the issued key.
75
+ return stored.body;
76
+ }
77
+ async getSize(key) {
78
+ return this.read(key)?.body.length;
79
+ }
80
+ createRouter() {
81
+ const router = express_1.default.Router();
82
+ const { logger } = this;
83
+ // The uploads route is mounted ahead of the request logger, which needs the body parsers this
84
+ // one must precede, so each outcome is reported here instead.
85
+ const refuse = (res, key, status, reason) => {
86
+ logger('Warn', `[fileUploads] refused ${key}: ${reason}`);
87
+ res.status(status).json({ error: reason });
88
+ };
89
+ router.put('/:key', (req, res) => {
90
+ const { key } = req.params;
91
+ const { maxBytes, maxTotalBytes } = this.options;
92
+ this.expire();
93
+ // Consumed here rather than in write(): two concurrent PUTs for one key would both pass a
94
+ // has() check and the later would silently overwrite the earlier. A failed attempt burns the
95
+ // url too, which is the honest reading of single-use — retrying needs a fresh destination.
96
+ if (!this.issued.delete(key)) {
97
+ // An object still sitting here means the url was already used, which is the case an
98
+ // integrator actually hits. Reported apart because "or it has expired" sends them looking
99
+ // at ttls instead.
100
+ if (this.objects.has(key)) {
101
+ refuse(res, key, 409, 'this upload url was already used; request a fresh one');
102
+ }
103
+ else {
104
+ refuse(res, key, 404, 'no upload was authorized for this key: it was never issued, already used, or ' +
105
+ 'expired — or it was issued by another instance, which cannot be seen from here');
106
+ }
107
+ return;
108
+ }
109
+ // Answered before reading anything when the client declares a body that cannot fit.
110
+ const declared = Number(req.headers['content-length']);
111
+ if (Number.isFinite(declared) && declared > maxBytes) {
112
+ refuse(res, key, 413, `the file is larger than the ${maxBytes} byte limit`);
113
+ return;
114
+ }
115
+ // Dropped now rather than in write(): counting bytes this upload is about to replace would
116
+ // refuse a replacement that fits, and keeping them until 'end' would hold both at once.
117
+ this.forget(key);
118
+ // Refused before a byte is read whenever the store has no room at all, whatever this body
119
+ // turns out to be. `>=` rather than `>` so the answer is a 507 naming the store, instead of a
120
+ // 413 decided on the first chunk that reads as a problem with the file.
121
+ if (this.storedBytes + this.inFlightBytes >= maxTotalBytes) {
122
+ refuse(res, key, 507, `the in-memory store is full (${maxTotalBytes} bytes)`);
123
+ return;
124
+ }
125
+ const chunks = [];
126
+ let reserved = 0;
127
+ let refused = '';
128
+ // Reserved as the bytes arrive, not once the body is complete: concurrent uploads would
129
+ // otherwise each weigh their own size against the same stale total and all be admitted.
130
+ const release = () => {
131
+ this.inFlightBytes -= reserved;
132
+ reserved = 0;
133
+ };
134
+ req.on('data', chunk => {
135
+ if (refused)
136
+ return;
137
+ const { length } = chunk;
138
+ if (reserved + length > maxBytes) {
139
+ refused = `the file is larger than the ${maxBytes} byte limit`;
140
+ }
141
+ else if (this.storedBytes + this.inFlightBytes + length > maxTotalBytes) {
142
+ refused = `the in-memory store is full (${maxTotalBytes} bytes)`;
143
+ }
144
+ if (refused) {
145
+ // Kept reading, but no longer kept: destroying the socket or answering now would reach a
146
+ // client that is still writing as a connection reset instead of a 413.
147
+ chunks.length = 0;
148
+ release();
149
+ return;
150
+ }
151
+ reserved += length;
152
+ this.inFlightBytes += length;
153
+ chunks.push(chunk);
154
+ });
155
+ req.on('end', () => {
156
+ release();
157
+ if (refused) {
158
+ refuse(res, key, 413, refused);
159
+ return;
160
+ }
161
+ // A throw here would be an uncaughtException and take the whole server down, not just this
162
+ // request: Buffer.concat allocates and can fail under memory pressure.
163
+ try {
164
+ const body = Buffer.concat(chunks);
165
+ this.write(key, body);
166
+ logger('Debug', `[fileUploads] stored ${key} (${body.length} bytes)`);
167
+ res.status(200).end();
168
+ }
169
+ catch (error) {
170
+ logger('Error', `[fileUploads] could not store ${key}: ${error?.message}`);
171
+ res.status(500).json({ error: 'could not store the upload' });
172
+ }
173
+ });
174
+ // An aborted upload never reaches 'end', so its reservation would leak and shrink the store
175
+ // for good. Idempotent with the release above.
176
+ req.on('close', release);
177
+ // The path a client abort takes, which is the most common real failure here — silence would
178
+ // leave a 20 MiB upload that vanished with no trace anywhere.
179
+ req.on('error', (error) => {
180
+ logger('Warn', `[fileUploads] upload of ${key} failed after ${reserved} bytes: ${error.message}`);
181
+ release();
182
+ res.destroy();
183
+ });
184
+ });
185
+ return router;
186
+ }
187
+ write(key, body) {
188
+ this.forget(key);
189
+ this.objects.set(key, { body, expiresAt: Date.now() + this.options.ttlSeconds * 1000 });
190
+ this.storedBytes += body.length;
191
+ }
192
+ read(key) {
193
+ this.expire();
194
+ return this.objects.get(key);
195
+ }
196
+ forget(key) {
197
+ const stored = this.objects.get(key);
198
+ if (stored) {
199
+ this.objects.delete(key);
200
+ this.storedBytes -= stored.body.length;
201
+ }
202
+ }
203
+ expire() {
204
+ const now = Date.now();
205
+ this.objects.forEach((stored, key) => {
206
+ if (stored.expiresAt <= now)
207
+ this.forget(key);
208
+ });
209
+ this.issued.forEach((expiresAt, key) => {
210
+ if (expiresAt <= now)
211
+ this.issued.delete(key);
212
+ });
213
+ }
214
+ }
215
+ exports.default = EphemeralStorage;
216
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXBoZW1lcmFsLXN0b3JhZ2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvZmlsZS11cGxvYWRzL2VwaGVtZXJhbC1zdG9yYWdlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBSUEsc0RBQThCO0FBTzlCLCtGQUErRjtBQUMvRixvR0FBb0c7QUFDcEcscUZBQXFGO0FBQ3JGLE1BQU0sb0JBQW9CLEdBQUcsS0FBTSxDQUFDO0FBVXBDOzs7Ozs7OztHQVFHO0FBQ0gsTUFBcUIsZ0JBQWdCO0lBUW5DLFlBQTZCLE1BQWM7UUFBZCxXQUFNLEdBQU4sTUFBTSxDQUFRO1FBUDFCLFlBQU8sR0FBRyxJQUFJLEdBQUcsRUFBd0IsQ0FBQztRQUMxQyxXQUFNLEdBQUcsSUFBSSxHQUFHLEVBQWtCLENBQUM7UUFDNUMsZ0JBQVcsR0FBRyxDQUFDLENBQUM7UUFDaEIsa0JBQWEsR0FBRyxDQUFDLENBQUM7UUFFbEIsY0FBUyxHQUFHLEtBQUssQ0FBQztJQUVvQixDQUFDO0lBRS9DLDBGQUEwRjtJQUMxRixTQUFTLENBQUMsT0FBeUI7UUFDakMsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUM7SUFDekIsQ0FBQztJQUVELEtBQUssQ0FBQyxlQUFlLENBQUMsRUFBRSxHQUFHLEVBQW1CO1FBQzVDLE1BQU0sRUFBRSxhQUFhLEVBQUUsZ0JBQWdCLEVBQUUsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDO1FBRXpELDRGQUE0RjtRQUM1RiwwRkFBMEY7UUFDMUYsMkVBQTJFO1FBQzNFLElBQUksQ0FBQyxJQUFJLENBQUMsU0FBUyxFQUFFLENBQUM7WUFDcEIsSUFBSSxDQUFDLFNBQVMsR0FBRyxJQUFJLENBQUM7WUFDdEIsSUFBSSxDQUFDLE1BQU0sQ0FDVCxNQUFNLEVBQ04sd0ZBQXdGO2dCQUN0Rix5RkFBeUY7Z0JBQ3pGLHlEQUF5RCxDQUM1RCxDQUFDO1FBQ0osQ0FBQztRQUVELGdHQUFnRztRQUNoRywyRkFBMkY7UUFDM0YsSUFB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
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Sentinel prefix used inside action form values, e.g.
3
+ * { "document": "$uploadedFile:<jwt>" }
4
+ * A string rather than an object, so it passes the agent-client field validation and stays
5
+ * cheap when getActionForm echoes it back into the model's context.
6
+ */
7
+ export declare const UPLOADED_FILE_PREFIX = "$uploadedFile:";
8
+ /**
9
+ * Isolated so that the file URIs the MCP specification is designing (SEP-2631) can be recognized
10
+ * here without touching the resolution path.
11
+ */
12
+ export default function parseFileReference(value: unknown): string | null;
13
+ //# sourceMappingURL=file-reference.d.ts.map