From cc16774db513ea8b2181524bd32747646ebec093 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Mon, 17 Aug 2026 15:08:27 +0200 Subject: [PATCH 1/9] =?UTF-8?q?docs(mcp):=20action=20file=20uploads=20?= =?UTF-8?q?=E2=80=94=20flow,=20client=20prerequisites,=20integrity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- product/embed/mcp-server.mdx | 77 ++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/product/embed/mcp-server.mdx b/product/embed/mcp-server.mdx index 7676040..63631ce 100644 --- a/product/embed/mcp-server.mdx +++ b/product/embed/mcp-server.mdx @@ -145,6 +145,7 @@ The Forest MCP server exposes the following capabilities: | --------------- | ---------------------------------- | | `getActionForm` | Get form fields for a smart action | | `executeAction` | Execute a smart action | +| `requestActionFileUpload` | Get an upload destination for an action's File field | ## Restrict tools @@ -242,6 +243,82 @@ The two settings differ in what your users notice: The minimum for either value is 60 seconds; a lower value is raised to it. An invalid value (zero, negative or fractional) stops the server at startup rather than silently leaving your tokens uncapped. +## Action file uploads + +Actions with **File fields** work over MCP out of the box. The file never travels through the AI's +context window: the model asks for an upload destination, sends the bytes there directly, and +passes a signed reference — a *handle* — as the field value. + +``` +1. requestActionFileUpload {filename, mimeType, sha256} → uploadUrl + fileHandle +2. PUT the raw bytes to uploadUrl → outside the MCP protocol +3. executeAction {Document: "$uploadedFile:"} → the action receives the real file +``` + +Nothing to provision: by default the back-end holds uploaded files in memory and serves its own +upload endpoint. That is correct for a **single back-end instance only** — with several replicas or +on a serverless runtime, the upload and the action can land on different instances. Plug a storage +backend (S3 presigned URLs, GCS, Azure SAS) for those deployments, or turn the feature off: + + + +```javascript Storage backend +agent.mountAiMcpServer({ + // any object implementing createUploadUrl / download / getSize — + // see the @forestadmin/mcp-server README for the contract and an S3 example + fileUploads: { storage: myUploadStorage }, +}); +``` + +```javascript Turn it off +agent.mountAiMcpServer({ fileUploads: false }); +``` + +```bash Standalone +FOREST_MCP_FILE_UPLOADS=false npx forest-mcp-server +# or point it at a storage module: +FOREST_MCP_UPLOAD_STORAGE_MODULE=./my-storage.js npx forest-mcp-server +``` + + + +### Client prerequisites + +The upload itself is an ordinary HTTPS request made by the AI client, outside the MCP protocol. +Whether the client can make it depends on where it runs: + +| Client | Works when | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| Claude Code | Always — its shell runs on the developer's machine, so even a `localhost` back-end is reachable | +| Claude Desktop, Claude.ai, Cowork | The upload host is **publicly reachable** (never `localhost`) and its domain is **allowed for outbound traffic** in the client's code-execution sandbox | + + + On a managed (Team/Enterprise) Claude workspace, **two settings belong to the workspace admin, + not the end user**: the right to add a custom connector at all, and the sandbox's outbound domain + allowlist. Ask for both in the same request — one per Forest back-end (or storage) domain. + + +### Integrity + +- The upload URL is pre-authorized, expires after 15 minutes, and against the built-in in-memory + store accepts a **single** upload. +- The handle is a signed token bound to the user who requested it; it expires after 45 minutes. +- The AI is instructed to **pin the file's sha256**: the digest is re-verified when the action + runs, so content substituted after the upload is rejected. +- Files are capped at 20 MiB each by default (`fileUploads.maxBytes`). + + + The filename is whatever the AI client reports, and sandboxes have been observed normalizing it + (a dropped hyphen) while the bytes stay exact. In your action code, treat `file.name` as a label, + not an identifier. + + + + This capability is **experimental**: the MCP specification is designing its own file transfer + story ([SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631)), and + the tool may change to follow it once it lands. + + ## Connect your AI assistant Your MCP endpoint is available at `/mcp` (`/mcp` when mounted, `/mcp` when standalone). On first connection, a browser window opens for you to log in with your Forest credentials; the assistant then operates with that user's permissions. From a45cfad702443990aa7d79547c7a3b6f981ac470 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Mon, 17 Aug 2026 15:48:54 +0200 Subject: [PATCH 2/9] docs(mcp): FOREST_MCP_SERVER_URL for deployed standalone servers --- product/embed/mcp-server.mdx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/product/embed/mcp-server.mdx b/product/embed/mcp-server.mdx index 63631ce..670c975 100644 --- a/product/embed/mcp-server.mdx +++ b/product/embed/mcp-server.mdx @@ -46,6 +46,13 @@ FOREST_ENV_SECRET=xxx FOREST_AUTH_SECRET=xxx npx forest-mcp-server ### Standalone configuration + + Deploying the standalone server behind a public URL? Set `FOREST_MCP_SERVER_URL` to that URL + (an http(s) origin, no path). Without it the server advertises `localhost` in its OAuth + metadata and upload URLs — which works on the developer's machine and breaks for every remote + user. Available from `@forestadmin/mcp-server` 1.22.1. + + The standalone Forest MCP Server is configured entirely through environment variables: | Variable | Required | Default | Description | From ce3bfa03ce60e4f1cfd8b7b97877d6878b6859a4 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Mon, 17 Aug 2026 15:49:16 +0200 Subject: [PATCH 3/9] docs(mcp): drop the guessed version number --- product/embed/mcp-server.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/product/embed/mcp-server.mdx b/product/embed/mcp-server.mdx index 670c975..a96f55d 100644 --- a/product/embed/mcp-server.mdx +++ b/product/embed/mcp-server.mdx @@ -50,7 +50,7 @@ FOREST_ENV_SECRET=xxx FOREST_AUTH_SECRET=xxx npx forest-mcp-server Deploying the standalone server behind a public URL? Set `FOREST_MCP_SERVER_URL` to that URL (an http(s) origin, no path). Without it the server advertises `localhost` in its OAuth metadata and upload URLs — which works on the developer's machine and breaks for every remote - user. Available from `@forestadmin/mcp-server` 1.22.1. + user. The standalone Forest MCP Server is configured entirely through environment variables: From 9f21f55e7ae1e1e2a75e3f0bcd7de44815fffe64 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Mon, 17 Aug 2026 15:55:08 +0200 Subject: [PATCH 4/9] docs(mcp): FOREST_MCP_SERVER_URL in the standalone table and the deploy command --- product/embed/mcp-server.mdx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/product/embed/mcp-server.mdx b/product/embed/mcp-server.mdx index a96f55d..779885c 100644 --- a/product/embed/mcp-server.mdx +++ b/product/embed/mcp-server.mdx @@ -37,7 +37,11 @@ npm install @forestadmin/mcp-server You will then need to provide your FOREST\_ENV\_SECRET and FOREST\_AUTH\_SECRET variables to start the Forest MCP Server, to ensure it can authenticate and access the right back-end, corresponding to your project and environment of choice: ```text +# local FOREST_ENV_SECRET=xxx FOREST_AUTH_SECRET=xxx npx forest-mcp-server + +# deployed behind a public URL +FOREST_MCP_SERVER_URL=https://mcp.example.com FOREST_ENV_SECRET=xxx FOREST_AUTH_SECRET=xxx npx forest-mcp-server ``` @@ -60,11 +64,14 @@ The standalone Forest MCP Server is configured entirely through environment vari | `FOREST_ENV_SECRET` | Yes | — | Your environment secret, used to authenticate and reach the right back-end. | | `FOREST_AUTH_SECRET` | Yes | — | Your authentication secret. Must match the one of the corresponding back-end. | | `MCP_SERVER_PORT` | No | `3931` | Port the standalone server listens on. | +| `FOREST_MCP_SERVER_URL` | **Yes when deployed** | `http://localhost:` | Public URL the server is reachable at — an http(s) origin, no path. Everything the server advertises to clients (OAuth endpoints, upload URLs) derives from it; left unset on a deployed server, clients are sent to their own `localhost`. | | `FOREST_MCP_ENABLED_TOOLS` | No | all tools | Comma-separated allowlist of tools to expose (see [Restrict tools](#restrict-tools)). | | `FOREST_AGENT_URL` | No | your environment's back-end URL | URL the MCP Server uses to reach your back-end's data layer. | | `FOREST_MCP_ACCESS_TOKEN_TTL_SECONDS` | No | `3600` (1 hour) | Shortens the OAuth access token lifetime (see [Token lifetimes](#token-lifetimes)). Minimum `60`. | | `FOREST_MCP_REFRESH_TOKEN_TTL_SECONDS` | No | unbounded | Shortens the time between two interactive logins (see [Token lifetimes](#token-lifetimes)). Minimum `60`. | | `FOREST_MCP_ALLOWED_OAUTH_CLIENTS` | No | any registered client | Comma-separated domains of the OAuth clients allowed to connect (see [Restrict which AI clients can connect](#restrict-which-ai-clients-can-connect)). | +| `FOREST_MCP_FILE_UPLOADS` | No | on | `false` turns action file uploads off (see [Action file uploads](#action-file-uploads)). | +| `FOREST_MCP_UPLOAD_STORAGE_MODULE` | No | in-memory store | Path to a module exporting the `fileUploads` options, for a real storage backend (see [Action file uploads](#action-file-uploads)). | Set `FOREST_AGENT_URL` when the MCP Server runs next to a self-hosted back-end reachable at an internal address (e.g. `http://localhost:3310`), so tool calls hit it directly instead of the public back-end URL registered in Forest. From 2f4df262480cccc380ce999e9bbed9224459e59d Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Mon, 17 Aug 2026 16:45:58 +0200 Subject: [PATCH 5/9] docs(mcp): fix the double-prefix trap, name the limits, drop the unreleased var --- product/embed/mcp-server.mdx | 39 ++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/product/embed/mcp-server.mdx b/product/embed/mcp-server.mdx index 779885c..f196737 100644 --- a/product/embed/mcp-server.mdx +++ b/product/embed/mcp-server.mdx @@ -37,11 +37,7 @@ npm install @forestadmin/mcp-server You will then need to provide your FOREST\_ENV\_SECRET and FOREST\_AUTH\_SECRET variables to start the Forest MCP Server, to ensure it can authenticate and access the right back-end, corresponding to your project and environment of choice: ```text -# local FOREST_ENV_SECRET=xxx FOREST_AUTH_SECRET=xxx npx forest-mcp-server - -# deployed behind a public URL -FOREST_MCP_SERVER_URL=https://mcp.example.com FOREST_ENV_SECRET=xxx FOREST_AUTH_SECRET=xxx npx forest-mcp-server ``` @@ -50,13 +46,6 @@ FOREST_MCP_SERVER_URL=https://mcp.example.com FOREST_ENV_SECRET=xxx FOREST_AUTH_ ### Standalone configuration - - Deploying the standalone server behind a public URL? Set `FOREST_MCP_SERVER_URL` to that URL - (an http(s) origin, no path). Without it the server advertises `localhost` in its OAuth - metadata and upload URLs — which works on the developer's machine and breaks for every remote - user. - - The standalone Forest MCP Server is configured entirely through environment variables: | Variable | Required | Default | Description | @@ -64,7 +53,6 @@ The standalone Forest MCP Server is configured entirely through environment vari | `FOREST_ENV_SECRET` | Yes | — | Your environment secret, used to authenticate and reach the right back-end. | | `FOREST_AUTH_SECRET` | Yes | — | Your authentication secret. Must match the one of the corresponding back-end. | | `MCP_SERVER_PORT` | No | `3931` | Port the standalone server listens on. | -| `FOREST_MCP_SERVER_URL` | **Yes when deployed** | `http://localhost:` | Public URL the server is reachable at — an http(s) origin, no path. Everything the server advertises to clients (OAuth endpoints, upload URLs) derives from it; left unset on a deployed server, clients are sent to their own `localhost`. | | `FOREST_MCP_ENABLED_TOOLS` | No | all tools | Comma-separated allowlist of tools to expose (see [Restrict tools](#restrict-tools)). | | `FOREST_AGENT_URL` | No | your environment's back-end URL | URL the MCP Server uses to reach your back-end's data layer. | | `FOREST_MCP_ACCESS_TOKEN_TTL_SECONDS` | No | `3600` (1 hour) | Shortens the OAuth access token lifetime (see [Token lifetimes](#token-lifetimes)). Minimum `60`. | @@ -159,7 +147,7 @@ The Forest MCP server exposes the following capabilities: | --------------- | ---------------------------------- | | `getActionForm` | Get form fields for a smart action | | `executeAction` | Execute a smart action | -| `requestActionFileUpload` | Get an upload destination for an action's File field | +| `requestActionFileUpload` | Get an upload destination for an action's `File` or `FileList` field | ## Restrict tools @@ -266,12 +254,17 @@ passes a signed reference — a *handle* — as the field value. ``` 1. requestActionFileUpload {filename, mimeType, sha256} → uploadUrl + fileHandle 2. PUT the raw bytes to uploadUrl → outside the MCP protocol -3. executeAction {Document: "$uploadedFile:"} → the action receives the real file +3. executeAction {Document: fileHandle} → the action receives the real file ``` +`fileHandle` is a string of the form `$uploadedFile:` — pass it through unchanged, +the prefix is already there. + Nothing to provision: by default the back-end holds uploaded files in memory and serves its own -upload endpoint. That is correct for a **single back-end instance only** — with several replicas or -on a serverless runtime, the upload and the action can land on different instances. Plug a storage +upload endpoint at `/mcp/uploads` — that host is the one to get allowed in the +next section. Objects are lost on restart, and it is correct for a **single back-end instance +only**: with several replicas or on a serverless runtime, the upload and the action can land on +different instances. Plug a storage backend (S3 presigned URLs, GCS, Azure SAS) for those deployments, or turn the feature off: @@ -289,8 +282,11 @@ agent.mountAiMcpServer({ fileUploads: false }); ``` ```bash Standalone +# only 'true' or 'false' — any other value fails at startup FOREST_MCP_FILE_UPLOADS=false npx forest-mcp-server -# or point it at a storage module: + +# or point it at a storage module. FOREST_MCP_FILE_UPLOADS=false wins over it, +# so do not set both unless you mean to turn uploads off. FOREST_MCP_UPLOAD_STORAGE_MODULE=./my-storage.js npx forest-mcp-server ``` @@ -320,6 +316,10 @@ Whether the client can make it depends on where it runs: - The AI is instructed to **pin the file's sha256**: the digest is re-verified when the action runs, so content substituted after the upload is rejected. - Files are capped at 20 MiB each by default (`fileUploads.maxBytes`). +- The in-memory store holds 64 MiB across all pending uploads (`fileUploads.ephemeralMaxTotalBytes`). + Redeeming a file does not free it — it lives until the handle expires — so on the defaults that is + about **three max-size files per 45-minute window**, not a rolling 64 MiB. Past that, uploads are + refused with a `507`. The filename is whatever the AI client reports, and sandboxes have been observed normalizing it @@ -329,8 +329,9 @@ Whether the client can make it depends on where it runs: This capability is **experimental**: the MCP specification is designing its own file transfer - story ([SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631)), and - the tool may change to follow it once it lands. + story ([SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631)). The + `UploadStorage` contract is expected to survive — safe to write an adapter against — but the + `requestActionFileUpload` tool and the handle format may change to follow the specification. ## Connect your AI assistant From 1975ef50a5a7cf6f561eecad75bee8b15fd63c6e Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Mon, 17 Aug 2026 18:38:53 +0200 Subject: [PATCH 6/9] docs(mcp): apply the returned method and headers, and mark the ttls as defaults --- product/embed/mcp-server.mdx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/product/embed/mcp-server.mdx b/product/embed/mcp-server.mdx index f196737..f5e16e5 100644 --- a/product/embed/mcp-server.mdx +++ b/product/embed/mcp-server.mdx @@ -252,11 +252,15 @@ context window: the model asks for an upload destination, sends the bytes there passes a signed reference — a *handle* — as the field value. ``` -1. requestActionFileUpload {filename, mimeType, sha256} → uploadUrl + fileHandle -2. PUT the raw bytes to uploadUrl → outside the MCP protocol +1. requestActionFileUpload {filename, mimeType, sha256} → uploadUrl + method + headers + fileHandle +2. send the bytes to uploadUrl, with that method and every returned header 3. executeAction {Document: fileHandle} → the action receives the real file ``` +Step 2 happens outside the MCP protocol, and the returned `method` and `headers` are not +decoration: a pinned `sha256` is signed into a checksum header on S3, and the upload is rejected +without it. Apply them as returned rather than assuming `PUT` with no headers. + `fileHandle` is a string of the form `$uploadedFile:` — pass it through unchanged, the prefix is already there. @@ -310,9 +314,11 @@ Whether the client can make it depends on where it runs: ### Integrity -- The upload URL is pre-authorized, expires after 15 minutes, and against the built-in in-memory - store accepts a **single** upload. -- The handle is a signed token bound to the user who requested it; it expires after 45 minutes. +- The upload URL is pre-authorized and expires after 15 minutes by default + (`fileUploads.uploadUrlTtlSeconds`); against the built-in in-memory store it accepts a **single** + upload. +- The handle is a signed token bound to the user who requested it, expiring after 45 minutes by + default (`fileUploads.handleTtlSeconds`). - The AI is instructed to **pin the file's sha256**: the digest is re-verified when the action runs, so content substituted after the upload is rejected. - Files are capped at 20 MiB each by default (`fileUploads.maxBytes`). From 1259692953d79d29c2a218f9a9e25d12910009fb Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Tue, 18 Aug 2026 09:54:06 +0200 Subject: [PATCH 7/9] docs(mcp): name both store-full statuses, and the standalone upload-url gap --- product/embed/mcp-server.mdx | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/product/embed/mcp-server.mdx b/product/embed/mcp-server.mdx index f5e16e5..9433d3f 100644 --- a/product/embed/mcp-server.mdx +++ b/product/embed/mcp-server.mdx @@ -265,7 +265,7 @@ without it. Apply them as returned rather than assuming `PUT` with no headers. the prefix is already there. Nothing to provision: by default the back-end holds uploaded files in memory and serves its own -upload endpoint at `/mcp/uploads` — that host is the one to get allowed in the +upload endpoint at `/mcp/uploads` — that host is the one to get allowed in the next section. Objects are lost on restart, and it is correct for a **single back-end instance only**: with several replicas or on a serverless runtime, the upload and the action can land on different instances. Plug a storage @@ -296,6 +296,14 @@ FOREST_MCP_UPLOAD_STORAGE_MODULE=./my-storage.js npx forest-mcp-server + + **On a deployed standalone server, configure a storage backend.** The in-memory store builds its + upload URLs from `http://localhost:`, which is what the standalone server knows about + itself — so remote clients receive URLs pointing at their own machine. With a storage backend the + URL comes from that backend instead and the problem disappears. Mounted deployments are + unaffected: their upload URLs derive from the back-end URL registered in Forest. + + ### Client prerequisites The upload itself is an ordinary HTTPS request made by the AI client, outside the MCP protocol. @@ -324,8 +332,17 @@ Whether the client can make it depends on where it runs: - Files are capped at 20 MiB each by default (`fileUploads.maxBytes`). - The in-memory store holds 64 MiB across all pending uploads (`fileUploads.ephemeralMaxTotalBytes`). Redeeming a file does not free it — it lives until the handle expires — so on the defaults that is - about **three max-size files per 45-minute window**, not a rolling 64 MiB. Past that, uploads are - refused with a `507`. + about **three max-size files per 45-minute window**, not a rolling 64 MiB. Past that an upload is + refused with a `413` when it is what exceeds the total, or a `507` when the store was already + full — in both cases the response body names the store. A `413` alone does not distinguish this + from a file over `maxBytes`, so branch on the body, not the status. + + + The four `fileUploads.*` settings above are code-only — they are passed to `mountAiMcpServer`, + and there is no environment variable for any of them. On a standalone server they are set in the + module `FOREST_MCP_UPLOAD_STORAGE_MODULE` points at, which carries the whole `fileUploads` object + and not just the storage. + The filename is whatever the AI client reports, and sandboxes have been observed normalizing it From e311348d9b82a0a63a26a5bb96bcfa9bdebdf8e3 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Tue, 18 Aug 2026 10:10:56 +0200 Subject: [PATCH 8/9] docs(mcp): a storage backend fixes the upload urls, not oauth discovery --- product/embed/mcp-server.mdx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/product/embed/mcp-server.mdx b/product/embed/mcp-server.mdx index 9433d3f..af2ff61 100644 --- a/product/embed/mcp-server.mdx +++ b/product/embed/mcp-server.mdx @@ -297,11 +297,12 @@ FOREST_MCP_UPLOAD_STORAGE_MODULE=./my-storage.js npx forest-mcp-server - **On a deployed standalone server, configure a storage backend.** The in-memory store builds its - upload URLs from `http://localhost:`, which is what the standalone server knows about - itself — so remote clients receive URLs pointing at their own machine. With a storage backend the - URL comes from that backend instead and the problem disappears. Mounted deployments are - unaffected: their upload URLs derive from the back-end URL registered in Forest. + **A deployed standalone server cannot serve remote clients today.** Everything it advertises + derives from `http://localhost:`, which is all the standalone server knows about itself: + the OAuth endpoints a client discovers, and the upload URLs of the in-memory store. Configuring a + storage backend fixes the upload URLs — they then come from the backend — but not OAuth + discovery, so remote clients still cannot connect. Mounted deployments are unaffected: their URLs + derive from the back-end URL registered in Forest. ### Client prerequisites From 7f840bc9286a492aed6ac35d1b0132824175eeb3 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Tue, 18 Aug 2026 10:16:44 +0200 Subject: [PATCH 9/9] docs(mcp): prefix the upload path with basePath, and scope the Claude Code row --- product/embed/mcp-server.mdx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/product/embed/mcp-server.mdx b/product/embed/mcp-server.mdx index af2ff61..aac6761 100644 --- a/product/embed/mcp-server.mdx +++ b/product/embed/mcp-server.mdx @@ -265,8 +265,9 @@ without it. Apply them as returned rather than assuming `PUT` with no headers. the prefix is already there. Nothing to provision: by default the back-end holds uploaded files in memory and serves its own -upload endpoint at `/mcp/uploads` — that host is the one to get allowed in the -next section. Objects are lost on restart, and it is correct for a **single back-end instance +upload endpoint at `/mcp/uploads`, or `//mcp/uploads` if +you passed `basePath` to `mountAiMcpServer` — that host is the one to get allowed in the next +section. Objects are lost on restart, and it is correct for a **single back-end instance only**: with several replicas or on a serverless runtime, the upload and the action can land on different instances. Plug a storage backend (S3 presigned URLs, GCS, Azure SAS) for those deployments, or turn the feature off: @@ -312,7 +313,7 @@ Whether the client can make it depends on where it runs: | Client | Works when | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -| Claude Code | Always — its shell runs on the developer's machine, so even a `localhost` back-end is reachable | +| Claude Code | The upload host is reachable from the machine running Claude Code — so a `localhost` back-end works, as long as it runs on that same machine | | Claude Desktop, Claude.ai, Cowork | The upload host is **publicly reachable** (never `localhost`) and its domain is **allowed for outbound traffic** in the client's code-execution sandbox |