- The MCP service serves tools to AI assistants over the Model Context Protocol.
- The API service, the “Gateway API”, is a flat REST contract for interfaces people click in. The Zendesk app runs on it.
addBff() is a deprecated alias of the Gateway’s api option, and mountAiMcpServer() a deprecated alias of its mcp option.
Three names, three things. The Agent API is your agent’s own
/forest routes, the ones the Forest UI calls (see the Agent SDK overview). The Forest API is Forest’s public reporting API (see its introduction). The Gateway API is neither: it is the REST surface the Gateway serves to your trusted UIs.Permissions
The Gateway forwards calls and your back-end decides them. Every call reaches your back-end as the person who made it, and comes back filtered exactly as it would for them in Forest. Roles, permissions and scopes apply unchanged, and a collection your schema no longer exposes is re-checked on every call.Audit trail
Every data and action operation through the API is written to your Forest activity logs, attributed to the signed-in user, exactly as if they had acted in the Forest UI. The MCP service logs the same way, with four tool exceptions listed on its page.Adding the Gateway
Embedded in the Node.js agent, the Gateway needs@forestadmin/agent, plus @forestadmin/agent-bff only for the API service. That one is an optional peer dependency the agent loads at runtime when api is set, at exactly the version the agent declares. MCP alone installs nothing extra.
1
Install
latest? Install the exact @forestadmin/agent-bff version your agent’s release declares instead. On a mismatch the agent refuses to start, and the error names the version it wants with the command to run, npm install @forestadmin/agent-bff@X.Y.Z.addGateway() ships with @forestadmin/agent 1.109.0. On an earlier agent the call throws agent.addGateway is not a function.2
Enable the services you need
addGateway() once, before start(): a second call throws addGateway can only be called once, and so does a call once startup has begun. Enable at least one service, checked when the agent starts: addGateway({ mcp: true }) for MCP only, addGateway({ api: { ... } }) for the API only, or both as above. An argument-less addGateway() is a TypeError on the spot, and an empty addGateway({}) throws addGateway() enables no service when the agent starts.3
Give the UI its URL
The URL a trusted UI calls is
{your-agent-url}{basePath}/api, which is /api with the default empty basePath.Sign-in needs api.tokenEncryptionKey, a Base64-encoded 32-byte key. Generate one with:4
Check the deployment
GET {your-agent-url}{basePath}/api/health must answer "status": "ok". The full probe contract: Checking a deployment.- The MCP service exposes the tools, their restrictions and their token lifetimes, all documented on the Forest MCP Server page.
- The API service is configured through the
apioptions.tokenEncryptionKeyenables OAuth sign-in (and with it the AI relay),allowedOriginsenables browser access,openapiEnabledserves the API docs. The secrets, the Forest URLs and the logger are inherited from the agent: embedded, the Gateway API never reads the environment.
Routes
With an emptybasePath (the default), the Gateway claims these routes at the root of whatever the agent is mounted on. Every route moves under the prefix when you set one; the table writes it {basePath}.
The two
.well-known documents only exist when the MCP service is on. OAuth discovery must stay at the origin root (RFC 8414/9728), so a basePath is carried as a suffix rather than a prefix. Every other /.well-known/* request reaches your host application.
The /oauth/* namespace is shared. Both services run their own OAuth, and GET/POST {basePath}/oauth/authorize and POST {basePath}/oauth/token dispatch on one query parameter. {basePath}/oauth/*?service=api goes to the API service, and the same paths without a service go to the MCP service. Other values answer 400 invalid_request while the MCP service is on; an API-only Gateway claims only ?service=api on /oauth, and every other /oauth request reaches your host. ?service=api on a Gateway without the API service answers 404 not_found. API OAuth needs api.tokenEncryptionKey: without it the API’s /oauth side stays off, sign-in included. Only API callers send service; an MCP client never does.
The data routes under /api/agent/v1 are POST, except GET for permissions and context, and the filter, projection, sort, search and paging all travel in the body, never in the query string. Errors use a type-first contract, { error: { type, status, message, details? } }, so branch on error.type and never on message text. The one exception is POST /api/agent/v1/ai/query, which relays the Forest server: its JSON answers below 500 keep the upstream’s own { errors: [{ detail }] } shape. To-one relations are not listable and answer 404 unknown_relation by design.
The route layout never depends on which services are on: the API is always under {basePath}/api, even when it is the only service.
Choosing a basePath
Leave basePath empty when your host application does not need the root paths. Set it when the host already serves /api, /mcp or /oauth, or, with the MCP service on, when it needs the root .well-known documents. A basePath must be a plain path prefix like /gateway (letters, digits, - and _ only) and may not overlap the agent’s own mount prefix.
Ingress and network rules
An ingress or WAF that allow-lists only{basePath}/api/* breaks sign-in. The browser sign-in runs on {basePath}/oauth/*, so that namespace must pass too, for the API (?service=api) as for the MCP service. With the MCP service on and a non-empty basePath, the two discovery paths /.well-known/oauth-authorization-server{basePath} and /.well-known/oauth-protected-resource{basePath}/mcp must also reach the agent, even though they sit outside {basePath}.
Restricting which OAuth clients can connect
By default, any OAuth client application registered at Forest can connect. The Gateway’s top-levelallowedOAuthClients restricts both services at once, MCP and API:
http(s) URI on a listed domain or one of its subdomains (dust.tt matches eu.dust.tt). Matching uses redirect URIs because they are the one piece of registration metadata an impostor cannot benefit from. Every other client is rejected with a standard OAuth invalid_client error, and the response does not reveal the allowed domains.
Keep forestadmin.com in the list as long as you use Forest’s Zendesk app. Its sign-in redirect runs through app.forestadmin.com, so without that entry the app itself gets rejected. Restrict which AI clients can connect covers native desktop clients, loopback redirect URIs, and the token lifetime of approvals granted before the restriction.
The deprecated aliases do not share this scope. mountAiMcpServer() keeps its own allowedOAuthClients (MCP only), and addBff() has no allowlist: its API accepts any client registered at Forest.
Deprecated aliases, and when to keep them
mountAiMcpServer() and addBff() still work on their current paths and semantics: mountAiMcpServer() at the host root or under its own basePath, addBff() under /bff. Both are deprecated, though. mountAiMcpServer() logs its deprecation warning at startup; addBff() logs its own when /bff/* is first called, at most once an hour, and every /bff/* response carries Deprecation and Link headers. The Link points at the retired /product/embed/bff URL, which this documentation redirects to this page: that redirect stays in place for good.
Two cases justify keeping both aliases instead of moving to addGateway():
- You want the MCP service restricted by
allowedOAuthClientswhile the API stays open to any client registered at Forest. The Gateway’s list covers both services, so onlymountAiMcpServer({ allowedOAuthClients })plusaddBff()gives you that split. - The MCP service must stay at the root while your host already serves
/api. With an emptybasePath, the Gateway API would claim/api/agent/*,/api/docsand/api/health, and you cannot give the API alone abasePaththe MCP does not use. KeepmountAiMcpServer()at the root andaddBff()on/bff.
Moving from addBff() to addGateway({ api })
The migration is mechanical:
- The
addBff()options move underapiunchanged:addBff({ allowedOrigins, tokenEncryptionKey })becomesaddGateway({ api: { allowedOrigins, tokenEncryptionKey } }). - The URL moves from
/bffto{basePath}/api, which is/apiby default. Update every application that called the old URL. In the Zendesk app, theforestBFFUrlsetting changes from{your-agent-url}/bffto{your-agent-url}/api. - Users sign in again. Sessions live in memory in the agent process, and the routes they authenticated against have moved.
Mount before your body parser
Cross-origin access
A browser UI is cross-origin by definition, so the API’sallowedOrigins decides what reaches your data.
Matching is exact: scheme, host and port, case-insensitive, default ports normalized away, no trailing slash. An origin outside the list never reaches your data, with one exemption: a request from the very host serving the Gateway is allowed through, compared on host, so an application serving its own UI beside the mount works with an empty allow-list. How a refused origin is answered depends on the preflight:
- A request without a preflight answers
403 origin_not_allowed. - A preflighted request is stopped one step earlier, at the
OPTIONS.
A browser rarely shows you that
403. Every data route is a POST with a JSON body, so the browser sends a preflight first. A rejected OPTIONS answers 204 with no CORS headers, and the browser reports a plain CORS error without ever issuing the request that would have carried the 403. The cause shows up in the Gateway’s own log line, BFF preflight origin rejected, with the origin it refused. Read it there, or replay the call with curl -H 'Origin: ...' to see the 403 body.https://*.example.com, which needs @forestadmin/agent-bff 1.31.0 or later. It stands for exactly one subdomain label, so it covers https://app.example.com but neither https://a.b.example.com nor the bare https://example.com. Scheme and port still match exactly. The wildcard is refused at boot, and named in the startup warning, when it appears anywhere else in the host, more than once, or over a host with fewer than two remaining labels.
One shape escapes that check. A * placed in the URL’s userinfo rather than the host, as in https://*@example.com, is stripped when the entry is normalized. The entry is then accepted as the exact origin https://example.com and no warning names it. Mistyping *. as *@ therefore allows the bare domain and none of its subdomains, which is narrower than intended and silent. Check that the startup log lists the entries you expect.
A request carrying no Origin at all is untouched, which covers every server-to-server call. The opaque Origin: null counts as a present origin and is refused. A sandboxed iframe sends it, so give yours allow-same-origin if it has to reach the Gateway.
Browsers enforce this against
localhost too, so add your dev origin (for example http://localhost:4200) while developing. There is no development bypass.Timezone
Every API route exceptcontext and ai/query requires an explicit timezone, resolved in this order: the X-Forest-Timezone header, a timezone field in the body, then the fallback set by api.defaultTimezone. The requirement covers permissions too, although that route never reaches your back-end. With none of the three, the answer is 400 missing_timezone, and a non-IANA value gives 400 invalid_timezone.
Checking a deployment
configured says which optional surfaces this deployment was set up to serve, not that they work. A 503 degraded means a required variable is missing. The body never names which keys are present, because that would leak your configuration to an unauthenticated probe. Missing keys are logged at startup for operators instead.
OpenAPI document
Withapi.openapiEnabled set, GET {basePath}/api/agent/openapi.json serves a document you can generate a typed client from. It sits behind the same credentials as the data routes, so an uncredentialed curl answers 401 unauthorized. Pass a session bearer or an X-Forest-Bff-Key.
Timeouts and failures
Embedded, your agent runs in the same process: there is no socket, and a dispatch that fails or does not answer withinapi.agentTimeoutMs (default 10s) maps to 503 agent_unavailable. The 504 agent_timeout and 502 network_error types belong to the standalone Gateway, which calls your agent over HTTP: 504 when the agent accepts the connection and does not answer in time, 502 when it refuses the connection, its host does not resolve, or it fails mid-flight. The two are distinct on purpose, so a slow back-end and an unreachable one are not diagnosed as the same incident. Branch on error.type with that split in mind: an embedded client never sees a 504 or a 502 from its agent. ai/query is the exception: it relays the Forest server over the network and answers 502 network_error when that server cannot be reached and 504 upstream_timeout when it does not respond in time, embedded and standalone alike.
The agentTimeoutMs bound cancels nothing. A write cut off at the deadline keeps running inside the agent and still commits, so a client retrying on the timeout can double the mutation. Raise the timeout rather than relying on a retry.
Over the rate limit, the API answers 429 too_many_requests with a Retry-After and a details.cause saying whether the caller exceeded its budget or the limiter is saturated. Either way the body is the Gateway’s own { error: { type, status, message } }, never your back-end’s JSON:API errors array, so branch on error.type rather than parsing an upstream shape.
Standalone deployment
The Gateway above is the embedded deployment, inside the Node.js agent. When the agent cannot host it, for a Ruby back-end or an API that must scale apart from the agent, run the standalone Gateway: the same routes and the same OAuth, from its own process and port, configured through environment variables. It ships as the@forestadmin/gateway package and the ghcr.io/forestadmin/gateway image.
forest-bff and forest-mcp-server, the standalone commands that came before the Gateway, keep working and are deprecated. forest-bff serves the API at its root rather than under /api. Moving to the Gateway covers what changes.What’s next
Next: Zendesk app →
The Forest app for Zendesk, which runs on the Gateway’s API service.
Forest MCP Server →
The MCP service’s tools, restrictions, token lifetimes and uploads.