Configuration

Compatibility Dates

Applies to
Gatewayruntime for API, AI, and MCP gateways
Hosting
Managed EdgeManaged DedicatedSelf-Hosted

Zuplo is constantly shipping updates to the underlying runtime of projects. Occasionally, these updates aren't backwards compatible. In order to ensure that your project continues to build, deploy, and operate as you expect, you can set a compatibility date to lock in the behavior and APIs of the runtime.

The compatibility date is set in the zuplo.jsonc file at the root of your project. The zuplo.jsonc file is created by default for new projects and contains the default configuration. For more information on the zuplo.jsonc file, see the Zuplo Project Configuration documentation.

2026-11-06

This compatibility date is the default for new projects. Existing projects keep the date set in their zuplo.jsonc. To adopt it in an existing project, set "compatibilityDate": "2026-11-06" in zuplo.jsonc, then test the change in a preview environment before you deploy to production. For examples and migration steps for each change, see the 2026-11-06 changelog entry.

This compatibility date makes request validation, the MCP Server handler, and the MCP Gateway follow the OpenAPI, JSON Schema, and MCP specifications more closely, and adds MCP protocol revision 2026-07-28 support to the MCP Gateway and the MCP Server handler. The date turns on every change below together. To keep the previous behavior for any of them, keep your project on 2026-03-01 or earlier.

Schema Validation Follows the Declared Specification

This compatibility date changes how the Request Validation policy and the MCP Server handler validate against your OpenAPI schemas.

Before this date: The MCP Server handler converts each tool and prompt schema at startup. It inserts default values into arguments, drops undeclared arguments, and keeps readOnly properties in the tool input. A tool whose toolInputStyle is flat but whose input can't be fully flattened stays nested, or keeps its request body under a body argument, and the MCP server logs why. The Request Validation policy can't validate path parameters declared on the path item instead of the operation, and fails those requests with Validator not found when it validates that location.

After this date: The build generates every tool and prompt schema from your OpenAPI document and validates it against the JSON Schema dialect and OpenAPI version the document declares. A schema problem, such as an invalid pattern or a $ref that doesn't resolve, fails the build and names the operation. Tools no longer insert default values, reject undeclared arguments, and leave readOnly properties out of the input. Patterns use Unicode (u flag) regular expressions. The Request Validation policy uses the same dialect, format, and pattern rules, keeps its parameter type coercion, and validates path item parameters. Flat tool input can also flatten some request bodies built from allOf and bodies that use dependentRequired. A tool whose toolInputStyle is flat but that can't be fully flattened, because two input channels declare the same name or its request body would stay under body, fails the build. The error names the tool, the operation, and the cause, and suggests both fixes: change the schema, or set "toolInputStyle": "nested" on that tool. See When a tool stays nested.

OpenAPI Path Parameters End at the Closing Brace

This compatibility date changes how routes match a path parameter that's followed by literal text in the same segment, such as /items/{id}_details.

Before this date: Letters, digits, _, or $ after the } become part of the parameter name. /x/{id}abc matches /x/<anything> and sets request.params.idabc, never request.params.id. The build warns about each such path.

After this date: The parameter ends at its }. /x/{id}abc matches /x/fooabc with id set to foo, and no longer matches /x/foo. Other paths match exactly as before.

Blank Policy Options Are Treated as Unset or Rejected

This compatibility date changes how built-in policies handle an empty or whitespace-only value, such as "$env(MY_VAR)" when MY_VAR is empty, for a string option.

Before this date: Many policies reject the empty value for an optional option. Some fail every request with a configuration error, for example an empty link on the HTTP Deprecation policy. Each policy handles an empty required option in its own way, so any error that follows doesn't name the option.

After this date: For an optional option, the policy behaves as if the option were left out and applies its default. Secrets, options where an empty value has a meaning, and options whose default would grant more access keep their current behavior.

For a required option, the policy isn't created. Requests to routes that use it fail with a configuration error that names each empty required option:

Code
Policy 'my-oauth-policy': the required option 'clientSecret' is empty. Set it to a value, or check that the environment variable it reads is set.

Required options where an empty value has a meaning keep their current behavior: match and replaceWith on the Replace String policy, and body on the Set Body policy. This check applies to policies configured in policies.json, not to built-in policies that custom code creates directly.

MCP Tool Parameters Use Their OpenAPI Serialization Style

This compatibility date changes how the MCP Server handler writes a tool's path, query, and header arguments into the request it sends to the route.

Before this date: The handler ignores style, explode, allowReserved, and content. Query arrays repeat the parameter name, query objects are sent as JSON text, and an object in a path or header becomes [object Object].

After this date: Each parameter is serialized as its OpenAPI definition says. For example, a deepObject query parameter becomes rgb[R]=100&rgb[G]=200, and a label path parameter becomes .blue.black.brown.

MCP Tool Names Follow the MCP Naming Rule

This compatibility date changes the names that the MCP Server handler advertises for tools.

Before this date: Each tool uses its configured name as is, including spaces, /, and names over 128 characters. When two tools share a name, the later one replaces the earlier one.

After this date: Any character other than A-Z, a-z, 0-9, _, -, and . becomes _, and names are cut to 128 characters. A name that's already taken gets a _2, _3, and so on suffix, so both tools stay available. Clients list and call tools by the new name. Names that already follow the rule don't change. Set x-zuplo-route.mcp.name to choose a stable name.

MCP Responses Match the Negotiated Protocol Version

This compatibility date changes which fields the MCP Server handler sends to clients on older MCP protocol versions.

Before this date: Every response includes every field, so a 2025-03-26 client receives outputSchema, structuredContent, and title, which that revision doesn't define.

After this date: The request's MCP-Protocol-Version header, or 2025-03-26 when it's missing, decides which fields the handler sends. Fields added in a later revision are left out. When a tools/call result has structuredContent but no text block, older clients receive its JSON as a text block.

MCP Resource Reads Fail on Error Responses

This compatibility date changes how the MCP Server handler answers resources/read when the resource route returns an error.

Before this date: The route's response body is returned as the resource's contents whatever the status code, so a 403 error body reaches the client as a successful read.

After this date: A status outside 200-299 returns a JSON-RPC error (-32603) that names the resource, its URI, and the status. The route's body isn't sent.

Invalid MCP Resource URIs Fail the Build

This compatibility date changes how the build handles a resource URI set in x-zuplo-route.mcp.uri that isn't an absolute RFC 3986 URI, such as audit doc with spaces.

Before this date: The build warns and the handler advertises the URI as written.

After this date: The build fails and names the operation and the URI. The default URI (mcp://resources/<name>) is always valid.

MCP Protocol 2026-07-28 in the MCP Gateway and MCP Server Handler

This compatibility date turns on support for MCP protocol revision 2026-07-28 in the MCP Gateway and the MCP Server handler.

Before this date: The MCP Gateway relays 2026-07-28 traffic without handling it as that revision. Upstream servers that answer only server/discover are reported as unavailable. The MCP Server handler answers a request with MCP-Protocol-Version: 2026-07-28 with HTTP 400 and JSON-RPC error -32000.

After this date: The MCP Gateway handles 2026-07-28 requests as the revision requires, including server/discover and subscriptions/listen. A caller that hasn't connected the upstream gets an input_required result with a connect link for a tool call, prompt, or resource read, and an empty list for a list method. Requests on 2025-11-25 and earlier also get the changes in MCP Gateway Answers Follow the MCP Specification, and these project-wide changes:

  • Upstream readiness checks fall back to server/discover when an upstream refuses initialize.
  • The gateway's OAuth authorization responses include the RFC 9207 iss parameter.
  • The configured server icons are also added to an initialize reply that the upstream sends as an event stream.
  • On Managed Edge and in local development, request.signal aborts when the client disconnects, for all code in the project.

The MCP Server handler serves a request whose MCP-Protocol-Version header is 2026-07-28 on that revision: it answers server/discover, returns HTTP 404 for the methods 2026-07-28 removed (such as initialize and ping), adds resultType and the server's name and version to its results, and adds ttlMs and cacheScope to discovery, list, and read results. A request that names 2026-07-28 only in its params._meta envelope, without the header, returns HTTP 400 with JSON-RPC error -32020, because 2026-07-28 requires the header. Requests on 2025-11-25 and earlier get the same answers as before, apart from the changes in MCP Server Handler Checks Prompt Messages and Batch Answers. The handler doesn't serve tools that ask the client for input (input_required results, elicitation, sampling, and roots), subscriptions/listen notifications, logging, or tasks.

See MCP Gateway compatibility dates and MCP protocol 2026-07-28 in the MCP Server handler for details.

MCP Gateway Answers Follow the MCP Specification

This compatibility date changes the answers the MCP Gateway builds itself in several error and edge cases, mostly for a caller that hasn't connected the upstream yet.

Before this date: A caller that hasn't connected the upstream gets the -32042 URL-elicitation error with HTTP 200 for every message, including notifications, ping, malformed bodies, and clients that never declared URL elicitation. When the upstream rejects the credential the gateway sent, the client gets the upstream's 401 or 403 and its WWW-Authenticate challenge.

After this date: Only an initialize on 2025-11-25 that declares URL elicitation gets -32042. Other requests get JSON-RPC error -32603 with the connect link in data.connectRequired. Notifications get HTTP 202 with no body, ping gets an empty result, malformed bodies and unsupported MCP-Protocol-Version values get HTTP 400, and a GET or DELETE gets HTTP 405. When the upstream rejects the gateway's credential on a route with the mcp-token-exchange-inbound policy, the client gets JSON-RPC error -32603 with the upstream's status in data.upstreamStatus. See Gateway answers follow the MCP specification for every change.

MCP Server Handler Checks Prompt Messages and Batch Answers

This compatibility date changes how the MCP Server handler answers prompts/get and JSON-RPC batches.

Before this date: prompts/get returns the prompt route's messages as the route sends them, including a role such as system or a content block MCP doesn't define. A batch whose answer holds one response gets a bare object. A tool from the deprecated files option with a blank x-zuplo-mcp-tool name is registered with an empty name.

After this date: prompts/get returns JSON-RPC error -32603 naming the prompt and the first invalid message when a message's role isn't user or assistant, or its content isn't a block the client's protocol version defines. The answer to a batch is always an array. A blank tool name falls back to the operation's operationId.

2026-03-01

This compatibility date was the default for projects created after March 1st, 2026, until 2026-11-06 became the default. Existing projects should update their zuplo.jsonc to take advantage of this change.

Chain Response Sending Hooks

This compatibility date changes how response sending hooks behave when multiple hooks are registered. With this flag enabled, response sending hooks (context.addResponseSendingHook and runtime.addResponseSendingHook) chain properly, where each hook receives the response from the previous hook instead of the original response.

Before this flag: All hooks received the original response, so only the last hook's output was used.

After this flag: Hooks are chained properly, each receiving the previous hook's response. For example, if you register three hooks A, B, and C in a policy in that order, hook A's response passes to hook B, then hook B's response passes to hook C.

This change enables more predictable behavior when composing multiple response transformations.

501 for Unsupported HTTP Methods

This compatibility date changes how unsupported HTTP methods are handled. Requests using non-standard HTTP methods such as WebDAV methods (PROPFIND, MKCOL, COPY, etc.) now return a proper 501 Not Implemented response instead of a 500 Internal Server Error.

Before this flag: Unsupported HTTP methods returned a 500 Internal Server Error.

After this flag: Unsupported HTTP methods return a 501 Not Implemented response with an RFC 7807 application/problem+json body.

2025-02-06

This compatibility date introduces a number of breaking changes to improve the overall behavior of Zuplo APIs. This compatibility date is the default for any projects created after 2025-03-27.

Special Characters in OpenAPI Format URLs

Previously, special characters that were included in URLs that used the open-api formatted URLs weren't escaped. This allowed an unintended behavior where the URL could include Regex patterns even though the OpenAPI format URLs doesn't allow regex. This has been fixed and now all special characters are escaped. This allows URLs with formats like:

Code
/accounts/open({id}) /accounts/:action

Removed legacy Log Initialization

Previously, several of the Zuplo log plugins could be enabled by setting undocumented environment variables. This was a legacy feature that was added before the current plugin system existed. This feature has been removed. Log plugins should be enabled using the documented plugin system.

If you are setting any of the following environment variables, you should migrate to the log plugin initialization.

Code
GCP_USER_LOG_NAME GCP_USER_LOG_SVC_ACCT_JSON ZUPLO_USER_LOGGER_DATA_DOG_URL

Removed legacy Log Context

Previously, several log plugins used special properties on context.custom to set global attributes on logs. This was a legacy feature that was added before the current plugin system existed. This feature has been removed. Log plugins should be enabled using the documented plugin system.

If you are setting any of the following in your code, you should migrate to the plugin configuration instead.

TypeScriptCode
context.custom["__ddtags"]; // Sets tags context.custom["__ddattr"]; // Sets fields

Migrate to the following:

TypeScriptCode
export function runtimeInit(runtime: RuntimeExtensions) { runtime.addPlugin( new DataDogLoggingPlugin({ url: "https://http-intake.logs.datadoghq.com/api/v2/logs", apiKey: environment.DATADOG_API_KEY, source: "MyAPI", // Optional, defaults to "Zuplo" tags: { tag: "hello", }, fields: { field1: "value1", field2: "value2", }, }), ); }

Improved Node.js Compatibility

The Zuplo runtime doesn't run Node.js, but is compatible with a number of Node.js APIs. This compatibility date adds some additional support for Node.js specific APIs.

URL Forward Handler Redirects

The UrlForwardHandler now supports the followRedirects option. Previously, this option wasn't supported on the forward handler. However, it was erroneously documented as supported. This property is behind a compatibility date in order to ensure that if existing projects were using the option that didn't work as expected, they wouldn't be broken by this change.

2024-09-02

The compatibility date allows the ability to call fetch to hosts with custom ports. Previously only the standard ports (80, 443) were allowed.

2024-03-14

This compatibility date doesn't include any breaking changes. However, Zuplo made a number of changes to the runtime build process. These changes will allow a number of future improvements. Out of an abundance of caution, these changes are only enabled for projects that have set their compatibility date to 2024-03-14. This compatibility date is the default for any projects created after March 14th, 2024.

Over time, the build changes will be enabled by default on all future deployments regardless of compatibility date. Existing customers are encouraged to update their compatibility date to 2024-03-14 and test their projects to ensure that they continue to operate as expected.

This new build process has rolled out to all customers regardless of compatibility date.

2024-01-15

This compatibility date includes several breaking changes to improve the overall behavior of Zuplo APIs. This compatibility date is the default for any projects created after Jan. 15th, 2024.

Run Outbound Policies on All Responses

Previously, outbound policies would only run on response status ranging from 200-299. Now outbound policies will always run, regardless of the response code.

No Hooks on System Routes

Previously runtime hooks such as OnRequest or OnResponseSending would run on system routes. For example, if you are using our Developer Portal and have it running on /docs, before this change you could write a hook that modified the output of the Developer Portal. This could result in unexpected behavior and is now disallowed.

Remove Cloudflare Location Headers

On SaaS deployments, Zuplo routes all requests through Cloudflare. Cloudflare adds a number of headers to requests. Previously, some Cloudflare location headers (for example cf-ipregion) could be passed through your Zuplo gateway. Now these headers are always removed from the outbound request if they have been set. If you need access to geo-location data use context.incomingRequestProperties instead.

Remove Internal Zuplo Headers

Zuplo uses several internal headers to send data between different layers of our systems. Previously, some of these headers where exposed in a way that they could be accessed directly. Examples were zp-ipcity and zp-ipcountry. These headers are now always removed from the outbound request if they have been set. If you need access this data use context.incomingRequestProperties instead.

Last modified on