@forestadmin/mcp-server 1.21.1 → 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 +247 -0
- package/dist/cli.js +36 -17
- package/dist/file-uploads/ephemeral-storage.d.ts +46 -0
- package/dist/file-uploads/ephemeral-storage.js +216 -0
- package/dist/file-uploads/file-reference.d.ts +13 -0
- package/dist/file-uploads/file-reference.js +21 -0
- package/dist/file-uploads/handles.d.ts +11 -0
- package/dist/file-uploads/handles.js +41 -0
- package/dist/file-uploads/resolve.d.ts +8 -0
- package/dist/file-uploads/resolve.js +145 -0
- package/dist/file-uploads/semaphore.d.ts +3 -0
- package/dist/file-uploads/semaphore.js +37 -0
- package/dist/file-uploads/types.d.ts +74 -0
- package/dist/file-uploads/types.js +58 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/server.d.ts +15 -1
- package/dist/server.js +73 -2
- package/dist/tool-context.d.ts +2 -0
- package/dist/tools/execute-action.js +12 -4
- package/dist/tools/get-action-form.js +37 -6
- package/dist/tools/request-action-file-upload.d.ts +4 -0
- package/dist/tools/request-action-file-upload.js +162 -0
- package/dist/utils/load-file-uploads.d.ts +14 -0
- package/dist/utils/load-file-uploads.js +105 -0
- package/package.json +3 -3
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
server.
|
|
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,
|
|
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
|