Skip to content

feat: upload files through the MCP server for action File fields - #1815

Merged
Scra3 merged 39 commits into
mainfrom
feature/prd-913-upload-files-through-the-mcp-server
Aug 17, 2026
Merged

feat: upload files through the MCP server for action File fields#1815
Scra3 merged 39 commits into
mainfrom
feature/prd-913-upload-files-through-the-mcp-server

Conversation

@Scra3

@Scra3 Scra3 commented Aug 10, 2026

Copy link
Copy Markdown
Member

Action forms with File fields could not run over MCP. The agent expects a file value as a data uri, so the bytes had to travel through the model's context window — a 2-5 MB document costs hundreds of thousands of tokens and exceeds most MCP clients' payload limits. Two customers hit this: Roundtable in April (document-extraction flow blocked on the upload step) and Qonto, who opened #1814 to implement it themselves.

This PR builds on that contribution. Reviewing it surfaced a deeper gap, so the work splits in three.

fixes PRD-913

1. agent-client owns the encoding

setFields was a pass-through with no support for File / ['File'] fields, so every caller had to hand-craft the data uri. It now accepts a File object ({ buffer, mimeType, name }) and encodes it; strings pass through untouched, so already-encoded data uris and opaque references still work.

The codec itself was duplicated in the agent and in plugin-aws-s3 (and the File type redeclared), so it moves to datasource-toolkit and both reuse it — -90 lines. Running the agent's and the plugin's existing suites against the shared implementation is what proves encode/decode symmetry.

Three fixes come along for free:

  • The name is now percent-encoded. feat(mcp-server): enable action file fields via an upload side-channel #1814 built the data uri by hand without it, so a filename like rapport final;v2.pdf broke the agent's header parsing (it splits on ;).
  • parseDataUri fails readably. No comma raised a TypeError, a lone % a URIError, and both surfaced as a generic 500 — from a value a model populates freely. They are now a ValidationError carrying the message, which the agent renders as a 400. Only name and charset are read back from the media types, so a uri carrying buffer=oops can no longer replace the decoded bytes with a string.
  • makeDataUri no longer emits charset=undefined.

No agent change is needed, on any generation. agent-client hits the standard /forest/... routes, so what we send is byte-for-byte what the browser already sends for a File field.

⚠️ One observable change for agent-client consumers: getType() now answers 'FileList' where it used to leak the wire array ['File']. The array form is kept in the payload echoed back to the agent, whose change-hook parser matches only that shape. This is what keeps getType(): string and unbreaks agent-bff and workflow-executor.

2. The MCP upload side-channel

The requestActionFileUpload tool returns a pre-authorized upload URL plus a handle bound to the requesting user; the client uploads straight to the storage; executeAction swaps "$uploadedFile:<handle>" for the downloaded file. The model only ever exchanges the small handle.

It is a tool, not the HTTP route #1814 proposed: a route is invisible to clients, the only thing telling a model it existed was a sentence in the executeAction description. A tool is listed by tools/list with its schema, and the call goes through the same logging as the others. It is registered only when fileUploads is set, and it checks the mcp:action scope itself — /mcp only requires mcp:read, and the route it replaced required mcp:action, so moving to a tool would otherwise have dropped an authorization check.

Nothing to provision. fileUploads: {} is enough: the server holds the objects in memory and serves its own PUT endpoint under <origin>/mcp/uploads. Provide a storage for anything beyond a single instance — the upload and the redemption are two separate requests, so with several replicas or on a serverless runtime one lands where the other did not. That is announced at startup and named again in the failure, rather than surfacing as a flaky feature.

It is not free, and not forced either way. #1814 required a storage, so the upload always
went to a bucket — one stable domain, shared by every customer on S3. The in-memory default sends
it to the customer's own agent origin instead: a different domain per customer, and one that no
client pre-allows. So this removes the provisioning friction and moves it onto the allowlist a
hosted sandbox enforces. Deliberate: a developer trying the feature with Claude Code needs no cloud
account, and anyone deploying to a walled client can still set a storage whose domain is easier
to get allowed. Requiring a bucket to make one client's allowlist simpler would tax everyone else.

Changes from #1814, all from review:

  • resolve.ts returns a File object instead of building the data uri — that is what removes the percent-encoding bug above.
  • getSize is required on UploadStorage. It was optional, and without it download() buffers the whole object before the maxBytes check, so the README's memory bound was not true.
  • Storage reads are bounded by downloadTimeoutSeconds (15 s). Unbounded, a backend that stopped answering held its concurrency slot while the caller's own timeout fired — a real client cuts at 30 s — so the slots drained away invisibly.
  • The numeric options are validated at startup. maxConcurrentDownloads: 0 used to queue every redemption with nothing left to release it, hanging instead of failing.
  • parseFileReference is isolated as the single place recognizing a reference, so SEP-2631 file URIs can be added without touching the resolution path.
  • The option is @experimental, and the standalone binary can enable it (FOREST_MCP_FILE_UPLOADS, or FOREST_MCP_UPLOAD_STORAGE_MODULE for a real backend — a storage is an object, so it cannot travel through an env var).

Kept from the original: the stateless JWT handle, the user binding, the optional sha256 pin re-verified on download, the concurrency bound, and the deliberate choice not to resolve handles in getActionForm (it echoes values back into the model's context).

3. A runnable example

packages/_example enables it with fileUploads: {} — no storage code, no cloud account. Its review collection carries an Attach a document action with a File and a FileList field, reporting the name, mime type and byte count it received.

Verification

Tested end to end against a real agent over MCP, not only in unit tests:

Happy path Received rapport final_v2.pdf (application/pdf, 51 bytes) — the exact source size
FileList two files resolved, 9 and 23 bytes
Handle replay the same handle reused within its TTL works, as documented
sha256 pin violated content substituted after the URL was issued is rejected
Handle on a String field rejected, instead of serializing {"buffer":{"type":"Buffer",...}} into the column
Bare filename on a File field 400 with a readable message, not an opaque 500

Cross-language contract verified by execution, not by reading: agent-ruby's own ForestValueConverter.parse_data_uri decodes what this encoder produces, names with ; , = space and % included, 50/50 bytes. And the bug it fixes is real there too — un-encoded, rapport final;v2.pdf arrived as "rapport final", and a,b=c.txt raised ArgumentError: invalid base64.

~5 700 tests green across the touched packages, 0 lint errors, 100% coverage on file-uploads/.

Known limitations

  • Client compatibility, tested against the running _example:

    Client Result
    Claude Code Works, end to end. Its shell runs on the developer's machine, so it reaches a localhost agent — the only client that can be tried without deploying anything.
    Claude Desktop (chat) Works, verified end to end. MCP itself is fine — mcp-remote runs locally. The PUT leaves a hosted sandbox whose egress allowlist was measured: before the host was allowed, PUT https://httpbin.org/put from there answered Host not in allowlist and plain https://example.com came back 403. With the agent behind a public HTTPS URL and that host added to the allowed domains, the upload goes through. Never works against localhost.
    Cowork (cloud mode) Works, verified end to end — same topology and the same two conditions as Desktop (public agent URL, host allowed for egress). The earlier failure was an artifact of testing against localhost. Verified server-side: the run started from one natural sentence, the model pinned the sha256 unprompted, and no POST /mcp body carried base64.

    Both conditions are client-side and outside this server's control. What the server can do, it
    does: the tool states the prerequisite in its description and repeats it in its response, so a
    model whose upload was blocked reports that rather than guessing at an expired handle.

    The allowlist entry is the whole of it. Satisfying it is enough — no other client-side
    condition surfaced. Where that setting lives is deliberately not documented: it differs between
    clients and versions, and naming a path sends a blocked user to a panel that may not have it,
    which is what happened while testing this.

    The product consequence is a support path, not a bug. A user whose upload is blocked has to
    ask an administrator of their own Anthropic organization to allow the Forest agent's domain —
    someone with no reason to have heard of Forest Admin — once per agent domain. There is no
    single-domain shortcut to offer them: serving the bytes from one Forest-owned host would mean
    Forest infrastructure holding customer files, which is precisely what a self-hosted agent exists
    to avoid.

    So Claude Desktop chat is not a channel this feature can carry on its own. Claude Code and
    programmatic callers, including the integration this started from, are unaffected — and being
    developer-side, they are also where action file uploads are actually driven from.

  • The in-memory store is single-instance and lost on restart. Multiple replicas, cluster mode and serverless runtimes need a storage.

  • A File field that declares a change hook receives the unresolved handle in that hook.

  • workflow-executor gains the plumbing but has no source of bytes, so this does not yet let it send files.

  • agent-python was not verified (not checked out locally). agent-php is archived.


Co-authored with @stefanoamorelli, whose #1814 this supersedes.

Note

Add file upload support to the MCP server via a new requestActionFileUpload tool

  • Introduces a requestActionFileUpload MCP tool that issues a pre-signed upload URL and a JWT-bound file handle for use in action form fields; enforces mcp:action scope and input validation (mime type, sha256 digest, filename sanitization).
  • Adds an /mcp/uploads HTTP endpoint backed by in-memory EphemeralStorage when no external storage backend is provided; supports per-file and total store size limits, single-use upload URLs, and TTL-based expiry.
  • When executeAction receives file handle values, it now resolves them to in-memory File objects with concurrency limiting, per-file size enforcement, optional digest verification, and timeout handling before invoking the action.
  • Adds FOREST_MCP_UPLOAD_STORAGE_MODULE environment variable to load a custom UploadStorage backend (e.g. S3) at startup; the module can export options directly or as a sync/async factory.
  • Centralizes data URI serialization/parsing in @forestadmin/datasource-toolkit and updates agent, agent-client, and plugin-aws-s3 to use these shared utilities; file-type field values that are not data URIs are now passed through unchanged rather than parsed.
  • Risk: EphemeralStorage is single-process and in-memory only — data is lost on restart and is unsuitable for multi-instance deployments; a warning is logged on first use.

Changes since #1815 opened

  • Changed ActionFormField type property and ActionField.getType() return type from string to string | [string] across agent-bff, agent-client, and workflow-executor packages to preserve list types as arrays in their wire format, and added ActionField.getTypeName() and FieldGetter.getTypeName() methods that return collapsed type names converting array forms like ['File'] to 'FileList' [a961a95]
  • Modified EphemeralStorage.download() method in mcp-server to no longer delete stored objects after retrieval, allowing repeated downloads of the same key until expiry or memory reclamation [a961a95]
  • Updated declareGetActionFormTool handler in mcp-server to filter out file reference placeholders from provided values before calling tryToSetFields, preventing file upload handles from being posted to change hooks [a961a95]
  • Added validation to parseDataUri() function in datasource-toolkit to reject data URIs that do not include base64 encoding in their media type, throwing a ValidationError instead of decoding to invalid data [a961a95]
  • Enhanced error handling in mcp-server file upload resolution to provide field-scoped diagnostic messages when getSize() fails or upload handles are invalid, instructing operators to check the upload step or request new uploads [a961a95]
  • Strengthened validation in loadFileUploads() function in mcp-server to verify custom storage modules export required methods (createUploadUrl, download, getSize) and reject misconfigured modules early instead of falling back to in-memory storage [a961a95]
  • Added initialization guard and configuration warning to ForestMCPServer.buildExpressApp() method in mcp-server to prevent reinitialization of file upload storage on router rebuilds and log a warning when maxBytes exceeds ephemeralMaxTotalBytes for in-memory store [a961a95]
  • Updated sanitizeFilename() function in request-action-file-upload tool to use Unicode-aware regex pattern with \p{L} and \p{N} for preserving letters and numbers across all locales while sanitizing other characters to underscores [a961a95]
  • Removed normalization logic from createSemaphore() function in mcp-server that previously coerced limits below 1 to 1, now using the provided limit value as-is [a961a95]
  • Added documentation clarifying storage quota checking logic and error handling behavior in file upload storage implementation [49bd1d1]
  • Updated test assertion in @forestadmin/agent-testing package to expect Field.getType() to return ['String'] instead of 'StringList' for a list field [f720ae5]
  • Updated documentation in mcp-server package README to confirm end-to-end verification of sandbox outbound allowlist requirement, added concrete example demonstrating that PUT requests succeed when agent host is added to sandbox allowed domains and fail with 'Host not in allowlist' error when not allowlisted, and restructured content to emphasize documenting upload host configuration [cee8109]
  • Changed getActionForm tool handler to withhold file handle values from tryToSetFields while treating them as satisfying required file fields [2190fd5]
  • Added support for disabling file upload functionality by passing fileUploads: false to both Agent and ForestMCPServer [2190fd5]
  • Added test coverage for file upload disablement and withheld file handle behavior [2190fd5]
  • Updated documentation to reflect file upload disablement via fileUploads: false option [2190fd5]
  • Documented upload URL semantics and sha256 pinning for file uploads in the mcp-server package [b6f30be]
  • Added FOREST_MCP_FILE_UPLOADS environment variable to control action file upload functionality [cb63a04]
  • Fixed prototype pollution vulnerability in field value checking for withheld action form values [cb63a04]
  • Updated documentation for file upload configuration and behavior [cb63a04]
  • Refined security warning in requestActionFileUpload tool description [cb63a04]
  • Added test coverage for file upload configuration and prototype pollution fix [cb63a04]
  • Updated client compatibility list and infrastructure requirements for file uploads through the MCP server [d6913ab]
  • Expanded verification section with successful end-to-end upload evidence from both Claude Desktop and Cowork cloud sessions [d6913ab]
  • Added documentation note regarding sandbox filename normalization behavior [d6913ab]

Macroscope summarized bb62c80.

@linear-code

linear-code Bot commented Aug 10, 2026

Copy link
Copy Markdown

PRD-913

@qltysh

qltysh Bot commented Aug 10, 2026

Copy link
Copy Markdown

12 new issues

Tool Category Rule Count
qlty Structure Function with high complexity (count = 19): makeFormDataFromFields 4
qlty Structure Function with many returns (count = 4): encodeFileFieldValue 3
qlty Structure Deeply nested control flow (level = 4) 2
qlty Structure Complex binary expression 1
qlty Structure Function with many parameters (count = 4): refuse 1
qlty Structure High total complexity (count = 60) 1

Comment thread packages/mcp-server/src/file-uploads/types.ts Outdated

// Swap the upload handles for the uploaded files before they reach the form. agent-client
// encodes them for the agent. No-op when no value carries a handle.
const values = options.values

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High tools/execute-action.ts:66

resolveUploadedFileValues inspects every field value, so a legitimate string or text field value that happens to start with $uploadedFile: is treated as an upload handle. The value is then replaced or rejected during handle verification, making it impossible to submit that literal text through the action. There is no field-type check or escaping mechanism to distinguish real upload handles from ordinary string values with the same prefix. Consider only resolving handles for fields whose schema type is a file/upload, or providing an escaping mechanism so literal values with this prefix can pass through unchanged.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/mcp-server/src/tools/execute-action.ts around line 66:

`resolveUploadedFileValues` inspects every field value, so a legitimate string or text field value that happens to start with `$uploadedFile:` is treated as an upload handle. The value is then replaced or rejected during handle verification, making it impossible to submit that literal text through the action. There is no field-type check or escaping mechanism to distinguish real upload handles from ordinary string values with the same prefix. Consider only resolving handles for fields whose schema type is a file/upload, or providing an escaping mechanism so literal values with this prefix can pass through unchanged.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real, but not fixing it here — the tradeoff is deliberate.

A field-type check is not available at this point: resolution runs before the form is loaded, so field types are unknown. Resolving after action() loads the form would give us types, but it moves the download inside the form lifecycle for a collision that requires a user to submit the literal text $uploadedFile:<something> into a text field.

An escape hatch has the opposite problem: an unverifiable reference would have to pass through silently, and then a genuinely expired or tampered handle reaches the agent as a raw string instead of producing "handle expired". That trades a readable error for silent corruption, which is the failure mode this PR spent most of its effort removing.

The related and more likely mistake — a resolved file landing on a field that is not a file — is now rejected in agent-client (encodeFileFieldValue throws instead of serializing {"buffer":{"type":"Buffer",...}} into the column).

This becomes clean once SEP-2631 lands: the file input is declared in the schema (x-mcp-file), so resolution can key off the declaration instead of a string prefix. Noted as a known limitation until then.

Comment thread packages/mcp-server/src/file-uploads/types.ts
Comment thread packages/agent-client/src/action-fields/file-value.ts
Comment thread packages/agent/src/agent.ts
Comment thread packages/mcp-server/src/file-uploads/types.ts Outdated
@qltysh

qltysh Bot commented Aug 10, 2026

Copy link
Copy Markdown

Qlty


Coverage Impact

⬆️ Merging this pull request will increase total coverage on main by 0.1%.

Modified Files with Diff Coverage (20)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
packages/agent-client/src/action-fields/field-getter.ts100.0%
Coverage rating: A Coverage rating: A
packages/mcp-server/src/tools/execute-action.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent-client/src/action-fields/field-form-states.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent/src/utils/forest-schema/action-values.ts100.0%
Coverage rating: A Coverage rating: A
packages/mcp-server/src/tools/get-action-form.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent/src/agent.ts100.0%
Coverage rating: A Coverage rating: A
packages/plugin-aws-s3/src/utils/data-uri.ts100.0%
Coverage rating: A Coverage rating: A
packages/datasource-toolkit/src/index.ts100.0%
Coverage rating: A Coverage rating: A
packages/mcp-server/src/server.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent-client/src/action-fields/action-field.ts0.0%26
New Coverage rating: A
packages/mcp-server/src/file-uploads/handles.ts100.0%
New Coverage rating: A
packages/mcp-server/src/utils/load-file-uploads.ts100.0%
New Coverage rating: A
packages/mcp-server/src/file-uploads/resolve.ts100.0%
New Coverage rating: A
packages/datasource-toolkit/src/utils/data-uri.ts100.0%
New Coverage rating: A
packages/mcp-server/src/file-uploads/ephemeral-storage.ts100.0%
New Coverage rating: A
packages/mcp-server/src/file-uploads/semaphore.ts100.0%
New Coverage rating: A
packages/mcp-server/src/file-uploads/file-reference.ts100.0%
New Coverage rating: A
packages/mcp-server/src/tools/request-action-file-upload.ts100.0%
New Coverage rating: A
packages/mcp-server/src/file-uploads/types.ts100.0%
New Coverage rating: A
packages/agent-client/src/action-fields/file-value.ts100.0%
Total99.7%
🤖 Increase coverage with AI coding...
In the `feature/prd-913-upload-files-through-the-mcp-server` branch, add test coverage for this new code:

- `packages/agent-client/src/action-fields/action-field.ts` -- Line 26

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

@Scra3
Scra3 force-pushed the feature/prd-913-upload-files-through-the-mcp-server branch from 9485a7d to fd8652d Compare August 11, 2026 09:25
Comment thread packages/mcp-server/src/tools/execute-action.ts Outdated
Comment thread packages/mcp-server/src/server.ts
Comment thread packages/mcp-server/src/server.ts
Comment thread packages/mcp-server/src/tools/request-action-file-upload.ts
Comment thread packages/datasource-toolkit/src/utils/data-uri.ts
Comment thread packages/_example/src/forest/local-upload-storage.ts Outdated
Comment thread packages/_example/src/forest/local-upload-storage.ts Outdated
Comment thread packages/_example/src/forest/local-upload-storage.ts Outdated
Comment thread packages/mcp-server/src/file-uploads/resolve.ts
Comment thread packages/mcp-server/src/file-uploads/resolve.ts Outdated
alban bertolini and others added 15 commits August 12, 2026 16:01
The data uri codec used for action File fields was duplicated in the agent
and in plugin-aws-s3. Expose it here so both, and agent-client, share one
implementation. Empty media types are now filtered out, which the agent copy
did not do (it emitted charset=undefined).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
setFields was a pass-through, so every caller had to hand-craft the
data uri the agent expects for a File field. It now accepts a File object
({ buffer, mimeType, name }) and encodes it, while strings pass through
untouched so already encoded data uris and opaque references still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The agent and plugin-aws-s3 each carried their own copy of the same
parser and encoder, and plugin-aws-s3 redeclared the File type. Both now
use the datasource-toolkit implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Action forms with File fields could not run over MCP: the agent expects a
data uri, so the bytes had to travel through the model's context window and
exceeded most clients' payload limits.

POST /files returns a pre-authorized upload URL plus a signed handle bound to
the requesting user, the client uploads straight to the storage backend, and
executeAction swaps the handle for the uploaded file before calling the agent.
The model only ever exchanges the small handle. Redemption enforces a size
cap, an optional sha256 pin, and a download concurrency bound.

The storage backend is pluggable and adds no dependency to the package.

Co-authored-by: Stefano Amorelli <stefano@amorelli.tech>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The PlainField.type widening broke agent-bff and workflow-executor; the wire
array form is now normalized in FieldGetter.getType instead, which keeps the
public signature a string. loadChanges still echoes the wire shape verbatim,
which the agent's change-hook parser requires.

Silent failures found in review:
- a maxConcurrentDownloads below 1 queued every redemption with nothing left
  to release it, hanging instead of failing; the numeric options are now
  validated at startup
- getSize answering null or NaN skipped the size cap, since strictNullChecks
  is off
- a single file on a FileList field, and a file on a field that is not one,
  were shipped unencoded and serialized into the column
- parseDataUri raised an opaque TypeError on anything that was not a data uri
- resolution ran outside withActivityLog, so a cross-user redemption attempt
  left no audit trail

Also: verify handles before taking a download slot, validate the claims that
feed storage.download, pin the JWT algorithm, name the field in redemption
errors, reject a filename made only of dots, and plumb fileUploads through
mountAiMcpServer so the option is reachable on the embedded mount.

The README claim that maxConcurrentDownloads bounds memory was wrong: every
file a call references is held until it completes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- the embedded mount never dispatched /files: makeIsMcpRoute was built
  without the fileUploads flag, so the option was plumbed but the route
  still 404ed, leaving the feature unusable on agent.mountAiMcpServer
- isFile accepted { buffer, mimeType } with no name, encoding a data uri
  without name= and producing a File whose required name was missing
- warn when handleTtlSeconds equals uploadUrlTtlSeconds, which leaves zero
  margin to redeem, not only when it is shorter

getType() now answers 'StringList' where it used to leak the wire array
['String'], so the agent-testing expectation follows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
POST /files was invisible to clients: the only thing telling a model it
existed was a sentence in the executeAction description. A tool is listed by
tools/list with its schema, so the client discovers it, and the call goes
through the activity log like every other tool.

The tool is registered only when a storage backend is configured, so a server
without one never advertises a capability every call would reject.

It also carries the prerequisite the server cannot check: the upload is an
outbound HTTPS request made by the client. In a code execution sandbox the
host of uploadUrl must be allowed for outbound traffic — on Claude Desktop
under Additional allowed domains. Stated in the description and repeated in
the response, so a model whose upload was blocked can tell that apart from an
expired handle.

Removes the express router, the /files route matching and its plumbing in the
agent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
resolveFileUploads validates five options and warns on a handle that cannot
outlive its upload window, none of which was tested. The upload destination
had no coverage of the method/headers fallbacks either.

Also drops a dead branch in resolve: download never used the parsed
reference, so collectReferences now carries the handle instead of the value
being parsed twice, and the unreachable kind guard is gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Manual end-to-end testing against a real agent surfaced two errors the
caller could not act on.

A non-data-uri on a File field threw a bare Error, which the agent's error
middleware renders as a generic 500 — so a model that passed a filename
instead of a handle learned nothing. It is now a ValidationError, which maps
to a 400 carrying the message.

And the friendly empty-file hint was unreachable: a backend honouring the
contract rejects a missing object rather than returning zero bytes, so the
most likely mistake — redeeming a handle that was never uploaded to —
surfaced as a raw NoSuchKey. The download failure now names the field and
keeps the cause.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The flow had no runnable demonstration: exercising it required a cloud
bucket and a smart action with a File field, so nobody could try it without
building both.

_example now carries a disk-backed UploadStorage that serves its own PUT
endpoint, and the review collection gets an 'Attach a document' action with a
File and a FileList field, reporting the name, mime type and byte count it
received — which is what tells you the bytes crossed the chain intact.

The storage authenticates nothing and is for localhost only; its objects are
gitignored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The PUT key comes from the request URL, so it is caller-controlled, and
sanitizing the charset was not enough: '.' and '/' are legal in a key, so
'../../etc/passwd' resolved outside the storage root and the unauthenticated
endpoint wrote there. Reproduced before the fix, rejected with a 400 after.

pathOf now resolves then requires containment, and it runs inside the promise
chain: throwing from the 'end' listener would have taken the process down
instead of answering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… every object

Moving POST /files to a tool silently dropped an authorization check: the
route was mounted behind mcp:action, while /mcp only requires mcp:read. A
read-only token could therefore mint a pre-authorized write into the host's
storage. The tool now checks the scope itself.

A whitespace-only filename also sanitized down to an empty string, producing
a key ending in '/' — a folder marker on S3-style backends — and a File whose
name was empty. It is trimmed at the schema and falls back to 'file'.

The mime type error said 'required' for a value that was supplied but
malformed, sending the model down the wrong path. And CLAUDE.md claimed the
tool goes through the activity log, which it does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing authenticates the PUT endpoint and the body was buffered to the end
with no limit, so an arbitrarily large request could run the example agent out
of memory. It now refuses as soon as 25 MiB is crossed — above the 20 MiB
fileUploads default, so an oversized upload is still reported by the server's
own maxBytes check rather than masked here.

Verified with a 30 MB body: 413, nothing written, the process survives and the
heap stays flat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prefix check was not enough. A data uri with no comma made Buffer.from
raise a TypeError, and a lone '%' in a media type made decodeURIComponent
raise a URIError — both surface as a generic 500, so a model that sent a
malformed value never learned what to send instead. Both now produce the same
ValidationError as a value that is not a data uri at all.

A properly percent-encoded name still decodes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The UploadStorage contract puts no bound on a read, so a backend that stopped
answering held its concurrency slot indefinitely. The caller's own request
timeout fires meanwhile — a real client cuts at 30s — so the failure was
invisible here while the slots drained away one by one, and every later
redemption queued behind them forever.

Reads are now bounded by downloadTimeoutSeconds, 15s by default, which leaves
half of a 30s caller budget for encoding and the action itself. The waiting
queue is capped too: past that point the last waiter would be served around
the time the caller has given up anyway, so failing tells the model to retry
instead of holding it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Scra3
Scra3 force-pushed the feature/prd-913-upload-files-through-the-mcp-server branch from 36d743d to 49d688a Compare August 12, 2026 14:03
Comment thread packages/datasource-toolkit/src/utils/data-uri.ts
claims: verifyUploadHandle(handle, userId, uploads.authSecret),
}));

const files = new Map(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High file-uploads/resolve.ts:145

Promise.all(verified.map(...)) submits every file reference to the bounded semaphore simultaneously. The semaphore only allows limit active plus 2 * limit queued tasks, so any action with more than 3 * maxConcurrentDownloads distinct file references is deterministically rejected with "Too many uploads" — even when the server has no other load. Retrying the same action cannot succeed. Consider feeding the downloads through the concurrency limit incrementally instead of enqueuing them all at once, or otherwise exempt a single accepted action from the overload queue cap.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/mcp-server/src/file-uploads/resolve.ts around line 145:

`Promise.all(verified.map(...))` submits every file reference to the bounded semaphore simultaneously. The semaphore only allows `limit` active plus `2 * limit` queued tasks, so any action with more than `3 * maxConcurrentDownloads` distinct file references is deterministically rejected with "Too many uploads" — even when the server has no other load. Retrying the same action cannot succeed. Consider feeding the downloads through the concurrency limit incrementally instead of enqueuing them all at once, or otherwise exempt a single accepted action from the overload queue cap.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and this was a regression I introduced two commits earlier: the queue cap was meant to shed cross-request overload, but one action submits all its references at once, so it also capped a single action at 3 × maxConcurrentDownloads references — 15 by default, deterministically, with retrying unable to help.

Fixed in 914548c by removing the cap rather than exempting one action. The cap was defending against a hanging backend holding slots forever, and downloadTimeoutSeconds (added in the previous commit) already bounds that — so it was a second mechanism for a problem already solved, whose only distinct effect was rejecting valid work. A test now asserts a burst of 25 references is served with a limit of 2.

A storage backend is an object with methods, so unlike every other standalone
option it cannot travel through an environment variable. File uploads were
therefore reachable only by embedding the server in an agent or by writing a
custom entry point — which left the standalone CLI, the way this is actually
deployed, unable to turn the feature on at all.

FOREST_MCP_UPLOAD_STORAGE_MODULE points at a module default-exporting the
options, or a function returning them. A missing path or a module without a
storage fails at startup, like the other options, rather than running with
uploads silently disabled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing had to be provisioned to use them, yet they still had to be switched
on — so every agent shipped with File-field actions that could not run over MCP
until someone found the option. `fileUploads` now only configures the feature:
a storage backend, size limits, ttls.

`enabledTools` is the off switch, and it takes everything with it — leaving
`requestActionFileUpload` out skips the tool, the upload endpoint and the
`executeAction` instructions together, so the server never advertises a tool it
did not register.

The single-instance warning moves from startup to the first createUploadUrl.
Announcing at boot would now reach every agent, including those whose actions
have no file field, which is noise rather than a warning.

Note what this widens: every agent that mounts the MCP server now serves an
unauthenticated PUT endpoint on its own origin. It only accepts keys this
server issued, each single-use and expiring, and holds at most
ephemeralMaxTotalBytes.
// refuse a replacement that fits, and keeping them until 'end' would hold both at once.
this.forget(key);

if (this.storedBytes + this.inFlightBytes >= maxTotalBytes) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High file-uploads/ephemeral-storage.ts:165

A zero-byte upload receives HTTP 507 when storedBytes + inFlightBytes exactly equals maxTotalBytes, even though it requires no additional capacity and fits within the limit. Use a strict greater-than check so empty uploads are admitted while non-empty uploads are rejected as they exceed the limit.

Suggested change
if (this.storedBytes + this.inFlightBytes >= maxTotalBytes) {
if (this.storedBytes + this.inFlightBytes > maxTotalBytes) {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/mcp-server/src/file-uploads/ephemeral-storage.ts around line 165:

A zero-byte upload receives HTTP `507` when `storedBytes + inFlightBytes` exactly equals `maxTotalBytes`, even though it requires no additional capacity and fits within the limit. Use a strict greater-than check so empty uploads are admitted while non-empty uploads are rejected as they exceed the limit.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not taken, deliberately — with one correction to what the code claims.

You are right that a zero-byte upload needs no capacity and that >= refuses it. But > moves the common case from a 507 answered before a byte is read to a 413 decided on the first chunk, and 507 is the more accurate signal for "this server is out of room" — it is the one status that tells an operator the store is the problem rather than the file. Trading that for an empty file that arrives exactly when the store is full is not a trade I would make; an empty upload on its own is already accepted and tested.

What was wrong is the comment, which read as if the check were about the declared body size. It now says it refuses when the store has no room at all, whatever the body.

Comment thread packages/mcp-server/src/server.ts
alban bertolini added 2 commits August 13, 2026 15:56
The prerequisite sent the model to "Settings > Capabilities > Code execution
and file creation > Additional allowed domains". That panel does not exist in
every Claude Desktop build — on a managed workspace the code execution
capability runs while its network setting is not exposed to the user at all, so
the text pointed at a menu they cannot open.

It now names the requirement — the host must be publicly reachable and allowed
for outbound traffic in that environment — and says the setting may be an
administrator one. The README tells integrators to give their users the host to
get allowed rather than a menu path.
Named as a motivating case for the storage backend, and it is no longer one:
Forest Cloud is being wound down. The warning still names serverless runtimes
and multiple replicas, which is what remains — and both are the deployments a
self-hosted agent chooses deliberately rather than inherits.
Comment thread packages/mcp-server/src/file-uploads/ephemeral-storage.ts Outdated
Comment thread packages/agent-client/src/action-fields/field-getter.ts
Comment thread packages/mcp-server/src/file-uploads/resolve.ts
Comment thread packages/mcp-server/src/utils/load-file-uploads.ts
Comment thread packages/mcp-server/src/file-uploads/resolve.ts
Comment thread packages/mcp-server/src/tools/request-action-file-upload.ts Outdated
Comment thread packages/mcp-server/src/file-uploads/resolve.ts Outdated
Comment thread packages/mcp-server/src/cli.ts Outdated
Comment thread packages/mcp-server/src/file-uploads/resolve.ts
Comment thread packages/mcp-server/src/file-uploads/semaphore.ts Outdated
alban bertolini added 5 commits August 13, 2026 17:18
Normalizing `['String']` to `'StringList'` inside `getType()` changed what two
API responses publish, not just a type: agent-bff (read by the frontend) and
workflow-executor (read by the workflow editor) both put `field.getType()`
straight into their form-field payload. The normalization existed only to
satisfy their declared `getType(): string`, which was the part that was wrong —
the runtime had always emitted the array for list fields.

So `getType()` returns the wire value again and `getTypeName()` carries the
collapsed name, used where a name is what is wanted: dispatching to the file
encoder, and the type this server reports to a model — `getActionForm` has to
say `FileList`, since the upload tool's own description tells the model to look
for a field of that type.

Both consumers' declared types now say what they always emitted.
…module

From review, in order of what a user would hit.

**A retry after any post-download failure lost every file.** `download()`
deleted the object on read, while `resolveUploadedFileValues` fetches every
reference *before* `setFields` and `execute` — the sequence the tool's own
description tells the model to retry. A mistyped field name or a throwing hook
therefore left the second attempt reporting that uploads had failed when they
had succeeded. Nothing is consumed on read now; `expire()` and
`ephemeralMaxTotalBytes` reclaim instead. The upload url stays single-use, which
is a different property, enforced on the issued key.

**A change hook on an action with a File field returned a 500.** On the
`getActionForm` path the handle reached the hook, which read `.buffer` off a
string. File references are withheld from `tryToSetFields` there, so the field
reads as unset — what it was before the model chose a destination.

**A storage module that exports a broken storage booted as if configured** and
silently ran the in-memory store, putting a deliberately replicated deployment
on the one backend that cannot serve it. The three methods are duck-typed when
the key is present, naming the missing one; omitting `storage` entirely stays
the documented way to ask for the in-memory store.

**A data uri without `;base64` decoded to garbage.** `Buffer.from(x, 'base64')`
skips what it cannot read rather than throwing, so `data:text/plain,hello`
became 3 bytes of nonsense and reached the end user as a corrupt file, reported
as a success.

Smaller, same review: `getSize` failures are wrapped like `download`'s (a
missing object is the likeliest failure of the flow, and it was reaching the
model as a bare SDK string); an unusable handle names its field instead of
`jwt expired`; the upload 404 admits another instance may have issued the key;
`maxBytes` above `ephemeralMaxTotalBytes` warns instead of advertising a size
every upload of which is refused; accented filenames survive sanitizing;
building a second app no longer swaps the store under the first one's urls; and
the semaphore's unreachable `Math.max(1, …)` is gone, with the test that only
covered it.

The README's claims about consume-on-read, change hooks and what
`maxConcurrentDownloads` bounds after a timeout are corrected rather than left
describing the old behaviour.
…ay reject

Two comments that described the code less precisely than the review of them
did: the total-bytes check reads as being about the declared body size when it
is about the store having no room at all, and the `getSize` contract said what
to return when a size is unavailable but nothing about an absent object — which
is the likeliest outcome of the whole flow.
Changed to 'StringList' earlier in this PR to match a getType() that has since
been reverted, so it went back to failing. `['String']` is what it asserted from
the day the package landed — the oldest evidence in the repo that the agent
emits the array, and the reason the wire had to stay as it was.
Tested from a Claude Desktop chat against an agent behind a public HTTPS URL,
with that host added to the sandbox's allowed domains: the model's PUT goes
through. The same sandbox had answered `Host not in allowlist` for an ordinary
public domain beforehand, so the allowlist is the whole of the second condition
and satisfying it is enough.

Still no menu path in either the tool description or here. Where the setting
lives differs between clients and versions, and naming one sends a blocked user
to a panel that may not have it — which is exactly what happened while testing
this.
Comment thread packages/mcp-server/src/server.ts
alban bertolini added 3 commits August 14, 2026 15:06
…y off

Withholding the handle from the change hooks fixed a 500 but made the field
unfillable: the agent never saw it, so `getValue()` stayed undefined, the field
stayed in `requiredFields`, and `canExecute` could never become true. The tool's
own description tells the model to call `getActionForm` until it does — with no
value that gets there, since a data uri is what it is told never to send and the
handle was being dropped. The handle is now echoed back as that field's value
and counted as filling it, while still never reaching the agent.

`fileUploads: false` turns the whole feature off. `enabledTools` could do it,
but it is an allowlist: declining one experimental feature that way meant naming
the ten other tools and opting out of everything shipped later — a poor trade
for something on by default. It drops the tool from the enabled set, so
registration, the upload endpoint and the executeAction paragraph all follow.

Also states the store's real capacity: an object survives redemption now, so
64 MiB of defaults holds about three max-size files per 45-minute window rather
than a rolling 64 MiB.
Threat-modeling the leaked-url case per backend showed the two defenses do not
live where they seemed to. The in-memory store is single-use, so a leaked url
cannot replace bytes that already landed. A presigned backend url is the
opposite: S3 accepts as many PUTs as fit in expiresInSeconds, so on the
production backend a url leaked to an access log can overwrite the upload after
it happened, until the action runs.

The sha256 pin is the one defense that covers both — S3 signs it into the url
so a different payload is rejected at upload, and redemption re-verifies it
regardless. Requiring the signed handle on the PUT instead would not even be
implementable there: a presigned request carries its signature in the query,
and S3 rejects a request presenting a second authorization mechanism.

So the tool now tells the model to compute and pass the digest as the normal
course, skipping it only when it cannot, and the README states the replayable
window an unpinned upload accepts.
Three agents over the last unreviewed 121 lines. What they caught:

- `in` on the withheld map walked the prototype chain, so a required field
  literally named toString read as filled by Object.prototype.toString and
  canExecute came back true on an empty form. Own-property check instead.
- fileUploads: false silently deleted a tool the caller had explicitly listed
  in enabledTools — in a resolver whose convention is to log every enablement
  surprise. It still wins, and now says so.
- The standalone server had no way to express fileUploads: false, while the
  README discouraged the one route it had. FOREST_MCP_FILE_UPLOADS returns with
  the only meaning left to it: 'false' turns the feature off, wins over a
  configured storage module, and any other value than true/false fails at
  startup instead of silently leaving the feature on.
- Two comments this PR itself falsified still named enabledTools as "the one
  way" to turn uploads off (server.ts and CLAUDE.md); the tool description
  claimed url-leak substitution unconditionally when the in-memory store's
  single-use url makes it impossible after the upload; the README said the
  single-instance warning fires at startup when it fires on first use; and the
  S3 example never said unhoistableHeaders is what keeps the checksum enforced.

New tests: the FileList half of the withheld-handle behavior (array echoed
back, required field counted as filled), the prototype-chain case, and the
fileUploads-false-vs-enabledTools precedence with its warning.
maxTotalBytes: this.fileUploads.ephemeralMaxTotalBytes,
ttlSeconds: this.fileUploads.handleTtlSeconds,
issuedTtlSeconds: this.fileUploads.uploadUrlTtlSeconds,
publicBaseUrl: new URL(uploadsPath, effectiveBaseUrl).href,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/server.ts:606

When effectiveBaseUrl includes a pathname such as /agent/, generated in-memory upload URLs point to /mcp/uploads, while the router is mounted at /agent/mcp/uploads, so uploads consistently fail with 404/405. new URL(uploadsPath, effectiveBaseUrl) treats the leading slash as an origin-absolute path; resolve mcp/uploads relative to mountBase instead, as for mcpResourceUrl.

-        publicBaseUrl: new URL(uploadsPath, effectiveBaseUrl).href,
+        publicBaseUrl: new URL('mcp/uploads', mountBase).href,
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/mcp-server/src/server.ts around line 606:

When `effectiveBaseUrl` includes a pathname such as `/agent/`, generated in-memory upload URLs point to `/mcp/uploads`, while the router is mounted at `/agent/mcp/uploads`, so uploads consistently fail with 404/405. `new URL(uploadsPath, effectiveBaseUrl)` treats the leading slash as an origin-absolute path; resolve `mcp/uploads` relative to `mountBase` instead, as for `mcpResourceUrl`.

Comment thread packages/mcp-server/README.md Outdated
**Verified end to end** from a Claude Desktop chat: an agent behind a public HTTPS URL, that host
added to the sandbox's allowed domains, and the model's `PUT` goes through. Before the host was
allowed, the same sandbox answered `Host not in allowlist` even for an ordinary public domain —
so the allowlist is the whole of condition 2, and satisfying it is enough.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Opus 5 (claude-opus-5): Docs only — nothing to fix here. Two results from a manual pass, both for this section and the limitations table in the PR body.

Cowork works, end to end

The PR body still says "Does not work as tested: the session runs remotely, so even the MCP transport cannot reach a local agent. Untested against a public agent." That has now been tested against a public agent, and it works.

Setup was an agent behind a public HTTPS URL with that host in the sandbox's egress allowlist — the same two conditions this section already states for Claude Desktop. Verified from the server side rather than from the model's account of it:

14:33:40  getActionForm                                   → discovers the File field
14:33:44  requestActionFileUpload {filename, mimeType, sha256}
14:33:51  PUT 15095 bytes from 160.79.106.138 → 200      → a cloud IP, not the dev machine
14:33:57  getActionForm {Document: "$uploadedFile:…"}     → canExecute true
14:34:03  executeAction {Document: "$uploadedFile:…"}

No base64 in any tool argument — checked against every POST /mcp body captured at the tunnel, not the transcript. The pinned digest matched the source file byte for byte and the server re-verified it on download.

So the original verdict was an artifact of testing against a localhost agent, which is condition 1 of this very section. Worth folding Cowork into the same bullet as Claude Desktop rather than leaving it as a separate failure row — same topology, same two conditions, same outcome.

The affordance holds without a scripted prompt

That run came from one sentence — "Can you attach this invoice to review 1 in Forest please?" — with no tool named, no instruction against inlining, and no mention of a digest. The model still found getActionForm → File field → requestActionFileUpload, and pinned the sha256 unprompted. That chain only exists in prose in the tool descriptions, so it is worth knowing it survives a realistic operator prompt and not just a test script.

One wrinkle for the docs

The filename the action stored was rapport1815.pdf; the file on disk was rapport-1815.pdf. Not sanitizeFilename — it preserves hyphens, and the same run kept the parentheses and accents in rapport final;v2 (été).pdf. So either the client normalised the attachment's filename in the sandbox or the model retyped it. Bytes and mime type were correct.

Harmless, but it means the name a customer's action persists can differ from the name the user recognises, and that is invisible from this side. Probably one sentence here: the filename is whatever the client reports, so treat it as a label rather than an identifier.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Folded in, d6913ab — thank you for closing this with server-side evidence rather than the transcript.

  • The README's client section now carries Cowork in the same bullet as Desktop and Claude.ai (same topology, same two conditions), with the verification described: single natural sentence, sha256 pinned unprompted, no base64 in any POST /mcp body. The PR body's compatibility row says the same and names the earlier verdict for what it was — an artifact of testing against localhost.
  • The filename wrinkle is documented where the conditions are: a sandbox may normalize the attachment's name before the model sees it, so the name a customer's action stores is a label, not an identifier. Your hyphen case is the example.

The unprompted-affordance observation is the most valuable line of the three for the release notes: the whole chain lives only in tool-description prose, and it survived a realistic operator prompt.

Cowork's "does not work" verdict was an artifact of testing against a
localhost agent — condition 1 of this very section. Against a public agent
with the host allowed it completes end to end, from a single natural sentence,
with the sha256 pinned unprompted and no base64 in any tool argument, verified
against the request bodies at the tunnel rather than the transcript.

Also observed there: a sandbox may normalize the attachment's filename before
the model ever sees it, so the name a customer's action stores can differ from
the name the user recognises. Stated as: a label, not an identifier.
@Scra3
Scra3 merged commit f09e88c into main Aug 17, 2026
37 checks passed
@Scra3
Scra3 deleted the feature/prd-913-upload-files-through-the-mcp-server branch August 17, 2026 13:03
Scra3 pushed a commit to ForestAdmin/docs that referenced this pull request Aug 17, 2026
The reference page gains the requestActionFileUpload tool and a section on
action file uploads: the flow that keeps bytes out of the model's context, the
in-memory default and its single-instance limit, the storage backend, both off
switches, the client prerequisites with the two admin-owned settings on managed
workspaces, and the integrity model including the sha256 pin and the observed
filename normalization.

Ships with ForestAdmin/agent-nodejs#1815.
forest-bot added a commit that referenced this pull request Aug 17, 2026
forest-bot added a commit that referenced this pull request Aug 17, 2026
# @forestadmin/plugin-aws-s3 [1.6.0](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/plugin-aws-s3@1.5.14...@forestadmin/plugin-aws-s3@1.6.0) (2026-08-17)

### Features

* upload files through the MCP server for action File fields ([#1815](#1815)) ([f09e88c](f09e88c))

### Dependencies

* **@forestadmin/datasource-toolkit:** upgraded to 1.55.0
* **@forestadmin/datasource-customizer:** upgraded to 1.70.1
forest-bot added a commit that referenced this pull request Aug 17, 2026
# @forestadmin/agent-client [1.14.0](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent-client@1.13.1...@forestadmin/agent-client@1.14.0) (2026-08-17)

### Features

* upload files through the MCP server for action File fields ([#1815](#1815)) ([f09e88c](f09e88c))

### Dependencies

* **@forestadmin/datasource-toolkit:** upgraded to 1.55.0
* **@forestadmin/forestadmin-client:** upgraded to 1.42.1
forest-bot added a commit that referenced this pull request Aug 17, 2026
# @forestadmin/mcp-server [1.22.0](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/mcp-server@1.21.1...@forestadmin/mcp-server@1.22.0) (2026-08-17)

### Features

* upload files through the MCP server for action File fields ([#1815](#1815)) ([f09e88c](f09e88c))

### Dependencies

* **@forestadmin/agent-client:** upgraded to 1.14.0
* **@forestadmin/forestadmin-client:** upgraded to 1.42.1
forest-bot added a commit that referenced this pull request Aug 17, 2026
# @forestadmin/workflow-executor [1.24.0](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/workflow-executor@1.23.9...@forestadmin/workflow-executor@1.24.0) (2026-08-17)

### Features

* upload files through the MCP server for action File fields ([#1815](#1815)) ([f09e88c](f09e88c))

### Dependencies

* **@forestadmin/agent-client:** upgraded to 1.14.0
* **@forestadmin/ai-proxy:** upgraded to 1.12.6
* **@forestadmin/forestadmin-client:** upgraded to 1.42.1
forest-bot added a commit that referenced this pull request Aug 17, 2026
# @forestadmin/agent-bff [1.18.0](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent-bff@1.17.0...@forestadmin/agent-bff@1.18.0) (2026-08-17)

### Features

* upload files through the MCP server for action File fields ([#1815](#1815)) ([f09e88c](f09e88c))

### Dependencies

* **@forestadmin/agent-client:** upgraded to 1.14.0
* **@forestadmin/datasource-toolkit:** upgraded to 1.55.0
* **@forestadmin/forestadmin-client:** upgraded to 1.42.1
forest-bot added a commit that referenced this pull request Aug 17, 2026
# @forestadmin/agent [1.95.0](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent@1.94.1...@forestadmin/agent@1.95.0) (2026-08-17)

### Features

* upload files through the MCP server for action File fields ([#1815](#1815)) ([f09e88c](f09e88c))

### Dependencies

* **@forestadmin/datasource-customizer:** upgraded to 1.70.1
* **@forestadmin/datasource-toolkit:** upgraded to 1.55.0
* **@forestadmin/forestadmin-client:** upgraded to 1.42.1
* **@forestadmin/mcp-server:** upgraded to 1.22.0
* **@forestadmin/datasource-sql:** upgraded to 1.17.12
* **@forestadmin/workflow-executor:** upgraded to 1.24.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants