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
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/discoverwhen an upstream refusesinitialize. - The gateway's OAuth authorization responses include the RFC 9207
issparameter. - The configured server icons are also added to an
initializereply that the upstream sends as an event stream. - On Managed Edge and in local development,
request.signalaborts 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
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
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.
Code
Migrate to the following:
Code
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.