Repository navigation
Translation Rules
This page gives the rules of the core module bridge between a client of revision 2025-11-251 and an upstream server of revision 2026-07-282. The rules are in src/bridge/translate.zig, src/bridge/Frontend.zig, src/bridge/input.zig, src/bridge/notify.zig and src/bridge/embed.zig3. The vscode bridge for Visual Studio Code (VS Code) applies them with the settings of vscode.profile. Each section names the milestone that added its rules. The VS Code page tells why VS Code needs them.
Applies to: zig-bridge-sdk 0.1.2 on main at commit 3cbc34b (the tag v0.1.2), with the milestones M0 to M6. The vscode bridge, and zig-sdk 0.5.1 at commit 52fc1b8.
Each bridge declares one bridge.Profile. The core functions take it, and the product is a property of the executable. The core never selects a profile from the clientInfo of the client.
| Field | Value in vscode.profile
|
Effect |
|---|---|---|
name |
mcp-bridge-vscode |
The tag of the log lines. It is also the server name when no other name exists. |
reply_protocol_version |
2025-11-25 |
The protocolVersion of the initialize result. |
request_id_prefix |
b- |
The prefix of the ids of the requests of the bridge to the client. |
meta_passthrough |
The keys traceparent and tracestate, and the prefix vscode.
|
The _meta keys of the client that can go to the upstream server. |
quirks.normalize_array_items |
true | The rule for the items of array schemas (R6). |
quirks.drop_non_object_output_schema |
true | The bridge removes each tool outputSchema whose root does not have type: "object". |
quirks.strict_legacy_results |
false | When true, the results lose the values that revision 2025-11-25 does not allow. VS Code and the MCP Apps read these values. |
The front end has four states. Only the first initialize starts the upstream server.
| State | Meaning |
|---|---|
awaiting_initialize |
The client did not send initialize, or the last initialize failed. |
initializing |
The bridge starts the upstream server and waits for server/discover. |
ready |
The bridge sends the requests to the upstream server. |
closing |
The input ended, or the upstream server stopped. |
The front end processes the messages of the client with these rules:
| Message of the client | What the front end does |
|---|---|
ping |
It answers {} in each state. |
logging/setLevel |
It keeps the level and answers {} in each state. Params that are not valid give -32602. |
server/discover |
It answers -32601 in each state. The bridge never forwards this request of the client to the upstream server. |
initialize |
The first initialize starts the upstream server. In initializing and ready, a second initialize gets -32600. |
A forwarded request, or a method that the bridge does not know, before ready
|
The request gets -32601 when its _meta has the key io.modelcontextprotocol/protocolVersion. Else it gets -32600. |
A forwarded request in ready
|
The request goes to the upstream server. |
resources/subscribe and resources/unsubscribe in ready
|
They change the listen stream (M3). Without resources.subscribe in the initialize result, or without a listen stream, they get -32601. |
A method that the bridge does not know, in ready
|
The request gets -32601. |
A request in closing
|
After the loss of the upstream server, the request gets -32603. After the end of the input, it gets no response. ping, logging/setLevel and server/discover keep their rules. |
notifications/initialized |
In ready, it opens the listen stream one time, when the initialize result declares a list change or resources.subscribe (M3). In another state, the front end ignores it. From M4, it also lets a later sign-in ask the client with a URL elicitation, when the client declared elicitation.url. |
notifications/cancelled |
It cancels the request in flight with the requestId of the notification. |
notifications/roots/list_changed |
The front end ignores it. The upstream server gets no roots.listChanged capability. |
| Each other notification | The front end ignores it and writes a debug line. |
A response with an id of the bridge, such as b-1
|
It goes to the table of the pending requests of the bridge (M2). |
- The front end sets the state
initializing. Params that are not valid give -32602. - It translates the capabilities of the client for the upstream server.
- It starts the upstream command, or it makes the HTTP client for the URL (M4).
- A failure gives the error of the cause
spawn_failedorconnect_failed. - It sends
server/discover4 with the time limit of--discover-timeout, 60 s by default. - It makes the
initializeresult from the discover result. - It sets the state
ready, and then it writes the result.
The state is ready before the result goes out, because the next requests of the client can come at once. When a step fails, the front end stops the upstream server and sets the state awaiting_initialize. Then it sends the error. The bridge stays open, and the client can send initialize again. When the client cancels initialize, the front end also returns to awaiting_initialize, without a response.
For an upstream server at a URL, a sign-in can start during server/discover. It can start when the store has no access token for the account that is valid for more than one minute. Then the time limit of server/discover is --discover-timeout and --sign-in-timeout together. In the sandbox of VS Code (SANDBOX_RUNTIME=1), initialize then fails at once with the cause sandbox. When the time limit of server/discover stops a sign-in, the error has the cause sign_in_timeout. A challenge that the bridge did not answer gives the error of its cause, for example registration_unavailable.
The upstream client of zig-sdk gets the clientInfo of the client and these capabilities:
| Capability of the client | Capability for the upstream server |
|---|---|
sampling |
As the client declared it. The bridge never adds sampling.tools or sampling.context. |
elicitation |
The modes as the client declared them. |
roots |
{}, without listChanged. Revision 2026-07-28 does not have roots.listChanged. |
experimental and extensions
|
The members that are objects. extensions never has the Tasks extension io.modelcontextprotocol/tasks. |
tasks and each other member |
Nothing. |
The parser also drops a member that does not have the shape of the schema. Thus the upstream server always gets capabilities that are valid.
| Member | Value |
|---|---|
protocolVersion |
2025-11-25, the reply_protocol_version of the profile. |
capabilities |
The members tools, prompts, resources, completions, logging, experimental and extensions of the discover result, when they are objects. listChanged, subscribe and logging stay as the upstream server declared them. The result never has tasks, and extensions never has the Tasks extension. |
serverInfo |
The name, title, version, icons, description and websiteUrl of _meta["io.modelcontextprotocol/serverInfo"] of the discover result. The bridge uses them only when they are valid and the name is not empty. For a remote upstream server, icons keeps only the icons with a data: URI (M4). |
serverInfo without upstream information |
The fallback name and the version of the bridge. The fallback name is the value of --name, or the file name of the upstream command without its extension. |
instructions |
A copy, when the discover result has a string. |
The bridge forwards eight methods. Both revisions have them with the same name: tools/list, tools/call, prompts/list, prompts/get, resources/list, resources/templates/list, resources/read and completion/complete.
The bridge changes the params of a forwarded request with these rules:
- Missing params, or params that are not an object, become
{}. - The bridge removes
task, because it declares no tasks. - It removes
_meta. The client of zig-sdk adds its own_metawith the protocol version, the client information, the capabilities and the log level. - It keeps the
progressTokenof the client for the progress rule. - From M3, the
_metakeys of the Meta passthrough go to the upstream server. - Each other member goes to the upstream server without a change.
The client of zig-sdk does not use its cache for these requests. Each request has a finite time limit:
| Request | Time limit |
|---|---|
The list requests and completion/complete
|
120 s |
The first round of prompts/get and resources/read
|
120 s |
Each later round of prompts/get and resources/read
|
1 h |
Each round of tools/call
|
1 h |
| The answers of the client to the input requests of one round | 1 h |
server/discover |
60 s, or the value of --discover-timeout
|
A request without a response in its limit gets -32603 with the cause timeout. Its data.detail has the method and the limit. The client can cancel a request earlier.
These limits also apply:
- At most 256 requests are in flight, the
max_in_flight_requestsof zig-sdk. The reader never waits. A request above the limit gets -32603 at once. - A line has at most
--max-line-bytesbytes, 64 MiB by default. The limit applies to the client and to the upstream server. - A message of the client has at most 64 levels of nesting.
- For an upstream server at a URL,
--max-response-byteslimits one JSON response and one event of a stream, 64 MiB by default (M4). A larger message fails its request at once.
The bridge applies these rules to each complete result before the client gets it. An InputRequiredResult never goes to the client. The section Input requests gives its rules.
| Rule | Results | What the bridge does |
|---|---|---|
| Removal of modern members | Each result | It removes resultType, ttlMs and cacheScope5, and the key io.modelcontextprotocol/serverInfo of _meta. It also removes a _meta that is then empty. |
nextCursor |
tools/list, prompts/list, resources/list and resources/templates/list
|
It removes a nextCursor that is not a string6. VS Code asks for the next page while nextCursor is present, also when its value is null. |
items of array schemas (R6) |
tools/list, with normalize_array_items
|
The next section gives the rule. |
outputSchema |
tools/list, with drop_non_object_output_schema
|
It removes each outputSchema whose root does not have type: "object". VS Code does not read it, and the TypeScript SDK 1.x refuses the whole list for one such schema. |
content |
tools/call |
It adds an empty content array when the result has no content array. |
| Text mirror | tools/call |
When structuredContent is not null and content has no text block, it adds one text block with the serialized structuredContent. |
structuredContent |
tools/call |
It keeps structuredContent without a change, whatever its JSON type. The MCP Apps read it. |
| Strict results |
tools/call and tools/list, with strict_legacy_results
|
It removes a structuredContent that is not an object, and each outputSchema whose root is not an object schema. The vscode profile does not enable this rule. |
| Icons | The results of a remote upstream server (M4) | The section Icons of a remote upstream server gives the rule. |
Each other member of a result goes to the client without a change.
The Copilot extension of VS Code stops each chat request when a schema has type: "array" and no items value that is true in JavaScript7. Thus the bridge walks each inputSchema of a tools/list result8, and it changes each such array schema:
- It changes only an object whose
typeis the string"array". A type array such as["array","null"]stays as it is. - Without
items, or with anitemsvalue ofnull, a number with the value 0 or"", the schema getsitems: {}. - With
items: false, the schema getsitems: {}. WithoutmaxItems, it also getsmaxItemswith the length ofprefixItems, or 0 withoutprefixItems. Thus the schema accepts the same arrays as before. - The walk examines
properties,patternProperties,dependencies,dependentSchemas,$defs,definitions,additionalProperties,not,if,then,else,contains,anyOf,allOf,oneOf,prefixItemsanditems. - The walk stops at a depth of 64. It writes an info line for a deeper schema and does not change it.
- Each change gives one info line with the name of the tool and the JSON pointer (RFC 6901) of the schema.
VS Code drops the http: and https: icons of a stdio server, and it gives file: icons the trust of a local process. The icons of a remote server must not get that trust. Thus for a remote upstream server, the bridge applies the rule IconRule.data_only in translate.initializeResult and translate.filterIcons:
- The
serverInfoof theinitializeresult keeps only the icons with adata:URI. The scheme ignores the case. - The items of
tools/list,prompts/list,resources/listandresources/templates/listkeep only such icons. - The resource links in the
contentof atools/callresult and in themessagesof aprompts/getresult keep only such icons. - An item without an icon after the rule loses its
iconsmember.
Upstream.isRemote selects the rule. An upstream server at a URL is remote, and a transport of the caller over a network is also remote. A stdio upstream server and the memory link keep each icon (IconRule.all).
- The bridge sends progress only when the client gave
_meta.progressToken9. Each JSON value except null, an array and an object is a token, also the integer 0. - In each round, the upstream server sends the progress with the id of that round as its token. The client of zig-sdk gives the progress of all rounds of the request to the bridge.
- The bridge sends
notifications/progresswith the token of the client and theprogress,totalandmessageof the upstream server. - The bridge keys the progress by request, and never by token. The Copilot harness sends the token 0 in each request.
- The client of zig-sdk drops the progress above 50 notifications per second for each request.
- Progress goes out only before the response, and never after a cancellation.
- A
notifications/cancelledof the client with the id of a request in flight cancels that request10. The client of zig-sdk then sendsnotifications/cancelledto the upstream server with the reasonthe client canceled the request. - A canceled request gets no response.
- A
notifications/cancelledwithoutrequestIdcancels nothing, because the bridge declares no tasks. - The client never gets a
notifications/cancelledof the upstream server. On a stdio upstream, the bridge drops each notification that belongs to no request, exceptnotifications/message. A listen stream gives only four methods to the client. - The bridge cancels its own requests to the client with
notifications/cancelled(M2). The section Pending requests of the bridge gives the rule. - At the end of the input, the front end cancels each request in flight. These requests get no response.
Each error of the bridge has a JSON-RPC code11, a fixed message and data.cause, the name of the cause. Some errors also have data.detail, for example the method and the limit of a timeout. A message or a detail never contains a secret or the stderr output of the child process. VS Code shows the message after "MPC : ", and the model also reads it. The messages are field values in translate.zig, thus zig build lint-docs checks them against the STE profile.
| Cause | Code | Message | When |
|---|---|---|---|
rpc |
-32603 | The upstream server sent an error. See the Output channel of the server. |
The upstream server sent an error, but the bridge has no error object. Usually the error of the upstream server goes to the client without a change. |
canceled |
No response | The client canceled the request. |
The client canceled the request, thus the bridge sends no response. |
timeout |
-32603 | The upstream server did not answer in time. See the Output channel of the server. |
The request had no response in its time limit. data.detail has the method and the limit. |
closed |
-32603 | The upstream server process stopped. See the Output channel of the server. |
The upstream connection closed. When the child process stopped, a forwarded request gets upstream_exited instead. |
transport_failed |
-32603 | The connection to the upstream server failed. See the Output channel of the server. |
The transport to the upstream server failed. |
invalid_response |
-32603 | The upstream server sent a response that is not valid. See the Output channel of the server. |
zig-sdk cannot read the response, for example a line longer than --max-line-bytes. A listen stream of a change that ends before its acknowledgment also gives this cause. |
too_many_rounds |
-32603 | The upstream server asked for input too many times. See the Output channel of the server. |
The upstream server still asks for input after 10 rounds, the mrtr_max_rounds_client of zig-sdk. |
out_of_memory |
-32603 | The bridge does not have sufficient memory for the request. |
The bridge has no memory for the request. |
undeclared_input_request |
-32603 | The upstream server asked for input that the client did not declare. |
An input request of a kind that the client did not declare, or a sampling request with tools. data.detail names the key of the input request. |
hook_failed |
-32603 | The bridge cannot give the input that the upstream server asked for. |
The client of zig-sdk reports a hook that failed or that is missing. |
not_connected |
-32603 | The bridge has no connection to the upstream server. See the Output channel of the server. |
The upstream client has no connection. |
task_cancelled |
-32603 | The upstream server canceled the task of the request. |
The task of the request ended with the status cancelled. |
invalid_request |
-32602 | The bridge cannot send the parameters of the request to the upstream server. |
The transport cannot send an argument of the request. |
invalid_meta |
-32602 | The request has a _meta key that the bridge cannot send to the upstream server. |
zig-sdk refused a _meta key. The bridge removes such keys first, thus this error tells of a fault in the bridge. |
spawn_failed |
-32603 | The bridge cannot start the upstream server. Examine the command in the configuration of the server. See the Output channel of the server. |
The bridge cannot start the upstream command. |
connect_failed |
-32603 | The bridge cannot make the client for the URL of the upstream server. Examine the URL, the proxy variables and the CA certificates. See the Output channel of the server. |
The bridge cannot make the HTTP client for the URL of the upstream server (M4). |
discover_failed |
The upstream code, else -32603 | The upstream server did not answer server/discover. It is not an MCP server of revision 2026-07-28, or it does not respond. See the Output channel of the server. |
server/discover failed with an upstream error or had no response in its limit. data.detail has the upstream message, or the text no response in <limit>. |
upstream_exited |
-32603 | The upstream server process stopped. See the Output channel of the server. |
The child process stopped. Each request in flight and each later request gets this error. |
not_initialized |
-32600 | The client did not initialize the connection. Send initialize first. |
A request before ready, without the modern _meta. |
already_initialized |
-32600 | The client already sent initialize on this connection. |
A second initialize. |
too_many_requests |
-32603 | The connection has too many requests in flight |
A request above the limit of 256 requests in flight. The message comes from zig-sdk. |
line_too_long |
-32600 | The request is longer than the line limit of the bridge. |
A line longer than --max-line-bytes. data.detail has the text limit: <n> bytes. |
too_deep |
-32600 | The request has more levels of nesting than the limit of the bridge. |
A message with more than 64 levels of nesting. data.detail has the text limit: <n> levels. |
parse_error |
-32700 | The request is not valid JSON. |
A line that is not valid JSON, or that is not valid UTF-8. |
invalid_request_shape |
-32600 | The message is not a valid JSON-RPC 2.0 request. |
Valid JSON that is not a valid JSON-RPC 2.0 message, also a request whose id is not a string or an integer. |
method_not_found |
-32601 | The server does not have this method. |
server/discover, a method that the bridge does not know, and a modern request before ready. Also resources/subscribe and resources/unsubscribe without a listen stream. |
invalid_params |
-32602 | The parameters of the request are not valid. |
The params of initialize, logging/setLevel, resources/subscribe or resources/unsubscribe are not valid. |
invalid_input_request |
-32603 | The upstream server sent an input request that is not valid for the client. See the Output channel of the server. |
An input request without the shape of the schema, a form schema that does not compile, or sampling messages that break the tool result rules. Also an InputRequiredResult with members that are not valid. |
too_many_input_requests |
-32603 | The upstream server asked for more inputs at one time than the limit of the bridge. |
One round has more than 16 input requests. |
input_timeout |
-32603 | The client did not answer the input request of the upstream server in time. |
The client did not answer all input requests of a round in 1 h. |
invalid_client_answer |
-32603 | The client sent an answer that is not valid for the input request of the upstream server. |
An answer without the shape of the schema, form content that is not valid against the requestedSchema, or a line that is not valid. |
sign_in_failed |
-32603 | The sign-in at the authorization server of the upstream server failed. See the Output channel of the server. |
A different failure of the sign-in (M4). data.detail gives the cause, for example the step and the HTTP status of a failed token request. |
sign_in_timeout |
-32603 | The sign-in did not complete in {seconds} s. The URL of the sign-in is in the Output channel of the server. To get more time, set --sign-in-timeout. During initialize, also set --discover-timeout. When the authorization server shows no sign-in page, it possibly does not know the stored client. Then run "{name} logout <url>" with the URL of this server, and restart the server. |
No redirect came in the time limit of the sign-in (M4). The time limit of server/discover can also stop a sign-in. |
sign_in_denied |
-32603 | The authorization server refused the access. Restart the server to sign in again. |
The redirect has error=access_denied (M4). |
sign_in_error |
-32603 | The authorization server sent an error for the sign-in. See the Output channel of the server. |
The redirect has a different error (M4). data.detail has its error code. |
sign_in_invalid_url |
-32603 | The authorization server sent an authorization URL that is not valid. The bridge did not open it. See the Output channel of the server. |
The authorization URL failed a check of the bridge, or it has no state or a different redirect URI (M4). |
sign_in_start_reused |
-32603 | The start URL of the sign-in received a second request, thus the bridge stopped the sign-in. Another program on the host of the bridge possibly opened the URL. Restart the server to sign in again. When this occurs again on a host that other users share, use --no-browser. |
The one-time start URL of the browser got a second GET before the redirect (M6). Another program possibly sent one of the two requests. The error has no data.detail. |
browser_launch_failed |
-32603 | The bridge cannot open a browser for the sign-in. See the Output channel of the server. |
The browser did not open, and the sign-in has the setting opener_failure_fatal (M4). Without that setting, the sign-in waits, and the user opens the URL of the sign-in line. |
redirect_port_unavailable |
-32603 | The bridge cannot listen on the redirect port {port}. Stop the program that uses the port, or set a different port with --redirect-port. Each server with a sign-in needs its own port. |
Another program uses the redirect port, or the bridge cannot listen on it (M4). |
registration_unavailable |
-32603 | The authorization server has no client registration that the bridge can use. Register a client with the redirect URI {redirect_uri} at the authorization server. Then give its client ID with --client-id and its issuer with --client-issuer. |
The authorization server supports no client ID metadata document and no dynamic client registration (M4). |
registration_failed |
-32603 | The authorization server refused the registration of the client. Register a client with the redirect URI {redirect_uri} at the authorization server. Then give its client ID with --client-id and its issuer with --client-issuer. |
The dynamic client registration failed (M4). data.detail has the HTTP status and the error code. |
issuer_not_registered |
-32603 | The client ID of --client-id is not for the authorization server of the upstream server. Give a client ID of this authorization server with --client-id, and its issuer with --client-issuer. |
--client-issuer names a different authorization server (M4). |
invalid_client_metadata_url |
-32603 | The URL of --client-metadata-url is not valid for a client ID metadata document. Use an https URL with a path. |
zig-sdk refused the URL of the client ID metadata document (M4). |
access_refused |
-32603 | The upstream server refused the access after the sign-in. The account possibly does not have the necessary permissions. See the Output channel of the server. |
The upstream server sent a challenge again after the one step-up attempt of the request (M4). data.detail has the HTTP status. |
reauthorization_required |
-32603 | The upstream server needs a new sign-in, and the client did not open the URL of the sign-in. Restart the server to sign in again. |
A later sign-in did not reach the browser (M4). data.detail has declined, no_consent or cooldown. |
sandbox |
-32603 | The upstream server needs a sign-in, but the bridge runs in the sandbox of VS Code. The sandbox blocks the browser and the redirect port of the sign-in. Disable the sandbox for this server. |
SANDBOX_RUNTIME=1 is set, and a sign-in is necessary (M4). |
The messages of the sign-in have placeholders. The bridge writes the time limit, the name of the executable, the redirect port and the redirect URI in their place. A message never has the URL of the upstream server, because its path or its query can hold a key. After a failed request to an upstream server at a URL, data.detail also has the HTTP status, for example HTTP status 404.
An error of the upstream server goes to the client with its code, its message and its data. These are the exceptions:
- The code -32042 becomes -32603, with the upstream message and without
data. Revision 2026-07-28 reserves this code, and VS Code opens each URL in itsdata. - An error for
server/discovergives the causediscover_failed. The code stays, except -32042, which becomes -32603. - The code -32602 in a round with
requestStatefollows the request-state rule.
An error of the client for a sampling or roots request also goes to the original request without a change (M2).
The exit code of mcp-bridge-vscode is 0 at the end of stdin. It is 1 when the upstream command stops, or for an internal error. It is 2 for arguments that are not valid, and for a missing variable of --header-env. It is also 2 for a --ca-file that the bridge cannot use, and for a token store that does not open.
- The line reader keeps the first 256 bytes and the last 256 bytes of a line that is too long.
- The front end finds the
idmember of the top-level object in the text. It examines the bytes and does not parse the text. - For a line that is too long, the front end also examines the end of the line. The TypeScript SDK 1.x writes the id as the last member.
- An id of a request gives the error with that id.
- An id with the prefix
b-belongs to a request of the bridge. That request gets the line as an answer that is not valid, and the client gets nothing. - Without an id, the error has the id null.
- A response of the client with the id null, or with an id that is not a string or an integer, gets no answer. An error response without an id also gets no answer.
A server of revision 2026-07-28 asks for input with an InputRequiredResult12. Revision 2025-11-25 has requests from the server to the client instead. The bridge sends each upstream request with allow_input_required, thus the client of zig-sdk returns each InputRequiredResult to the bridge. The bridge then does the rounds itself.
- The bridge examines all input requests of the round. One failed check fails the original request.
- It sends all questions of the round to the client at one time. Each question has a new id, such as
b-1. - It waits for all answers, at most 1 h.
- It examines each answer and makes the
inputResponsesobject. - It sends the upstream request again with
inputResponsesand therequestStateof the upstream server.
The rounds continue until a complete result or an error. One request has at most 10 rounds, the mrtr_max_rounds_client of zig-sdk. When the result of the tenth round still asks for input, the request fails with the cause too_many_rounds.
When a check fails, the client gets no question of the round, and the original request gets -32603. The data.detail has the form input request '<key>': <text>.
| Check | Cause | Text |
|---|---|---|
| The input request has the shape of the schema. | invalid_input_request |
the input request does not have the shape of the schema |
The client declared elicitation.form for a form. |
undeclared_input_request |
the client did not declare elicitation.form |
The requestedSchema of a form compiles with the validator of zig-sdk. |
invalid_input_request |
the requestedSchema does not compile |
The client declared elicitation.url for a URL. |
undeclared_input_request |
the client did not declare elicitation.url |
The client declared sampling for a sampling request13. |
undeclared_input_request |
the client did not declare sampling |
A sampling request with tools, toolChoice, or a tool_use or tool_result block, needs sampling.tools. VS Code never declares it. |
undeclared_input_request |
sampling with tools needs sampling.tools, and the client did not declare it |
The sampling messages obey the tool result rules of checkSamplingMessages of zig-sdk. |
invalid_input_request |
the sampling messages do not obey the tool result rules |
The client declared roots for a roots request. |
undeclared_input_request |
the client did not declare roots |
These checks of the whole round have a data.detail without a key:
| Check | Cause | Detail |
|---|---|---|
| A round has at most 16 input requests. | too_many_input_requests |
<n> input requests in one round, limit: 16 |
| The result is an object. | invalid_input_request |
the result is not an object |
inputRequests is an object. |
invalid_input_request |
the inputRequests member is not an object |
requestState is a string. |
invalid_input_request |
the requestState member is not a string |
The bridge does not examine includeContext, because the client can ignore it.
The client gets a URL only when it is a valid absolute URL and one of these conditions is true:
- The scheme is
https. - The scheme is
http, the host is a loopback host, and the URL has no user information. A loopback host islocalhost, an address in127.0.0.0/8, or[::1].
The bridge also refuses a URL with a backslash. A web browser reads a URL with the WHATWG rules, and the bridge reads it with the rules of RFC 398614. With a backslash, the two parsers can find different hosts. For a URL that the bridge refuses, the upstream server gets {"action":"decline"}, and the bridge writes a warning. This is not a failure of the request.
- Each question has the method
elicitation/create,sampling/createMessageorroots/list, and the params of the input request withouttask. - A URL elicitation also gets an
elicitationId. - A form without a property gets the choice form. A URL that the user accepted gets the Continue form.
| Input request | Answer of the client | Value for the upstream server |
|---|---|---|
| Elicitation | A result | The action, and the content for the accept of a form. The content must be valid against the requestedSchema, else the request fails with invalid_client_answer. A URL result never has content. |
| Elicitation | An error, or a line that is not valid | {"action":"cancel"} |
| Sampling | A result | The result, after a check of its shape. |
| Sampling or roots | An error | The original request fails with the error of the client: its code, its message and its data. VS Code refuses a sampling request with -32000. |
| Sampling or roots | A line that is not valid | The original request fails with invalid_client_answer. |
| Roots | A result | Only the roots with a URI that starts with file://15. |
| Each kind | A result without the shape of the schema | The original request fails with invalid_client_answer. |
VS Code cannot show a form without a property. Also, a URL that the user accepted must not open again. For these two cases, the client gets a form with one required choice:
{
"mode": "form",
"message": "Complete the step in the web browser. Then select Continue.",
"requestedSchema": {
"type": "object",
"properties": {
"continue": { "type": "string", "title": "Continue", "enum": ["Continue"], "default": "Continue" }
},
"required": ["continue"]
}
}| Case | Message of the form | Value for the upstream server |
|---|---|---|
| A form of the upstream server without a property | The message of the upstream form | Accept gives {"action":"accept","content":{}}. Each other answer follows the rules of an elicitation. |
| A later round asks again for a URL that the user accepted | Complete the step in the web browser. Then select Continue. |
Accept gives {"action":"accept"} without content. Decline and cancel stay the same. An error, or a line that is not valid, gives {"action":"cancel"}. |
The Continue form for a URL needs elicitation.form. Without it, the client gets the URL again.
- Revision 2025-11-25 needs an
elicitationIdfor a URL elicitation16. The bridge gives each URL step the idelicitation-<n>, which is unique on the connection. - After the user accepts a URL, the client gets
notifications/elicitation/completewith theelicitationIdwhen the next upstream result does not ask for that URL. That result can be the complete result or the nextInputRequiredResult. - The notification goes out before the response of the original request.
- When the rounds stop without a result, the client also gets the notification for each URL that the user accepted. This occurs after an error, after the limit of the rounds and after a cancellation. Then VS Code hides the URL in the chat.
- A URL whose step is complete gets a new step and a new
elicitationIdwhen a later round asks for it.
The upstream server seals requestState with a time limit, 600 s for a zig-sdk server. A late answer, or a restart of the upstream server, then gives the error -32602. The rule applies when a round with requestState fails with the upstream error -32602. The bridge examines only the code, because a server can send this error without data.reason.
- For
tools/call, the client gets a result withisError: trueand one text block. - For another method, the client gets an error with the upstream code, the same text as the message, and the upstream
data. - The text is
The upstream server did not accept the saved state of the request. The answer possibly came after the time limit of the state, or the server started again. Upstream message: <message>. Run the tool again. - For a method other than
tools/call, the last sentence of the text isSend the request again. - The bridge does not send the request again from the first round. It writes a warning line with the time of the wait for the answers.
- The front end keeps each request of the bridge in a table until its answer. The entry goes into the table before the request goes out.
- The reader puts each answer into the table at once, also at the limit of requests in flight.
- The reader drops an answer whose id is unknown, numeric, null or already resolved, with only a debug line. The client gets no answer for it.
- On a cancellation, a timeout, the end of the input or the loss of the upstream server, the bridge removes the entries first. Then it sends
notifications/cancelledfor each request that went out and has no answer. - VS Code ignores this notification and can answer late. The reader drops the late answer.
The reason of the cancellation tells which event occurred:
| Event | Reason |
|---|---|
| The client canceled the original request. | the client canceled the request |
| The input ended. | the client closed the connection |
| The upstream server stopped. | the upstream server stopped |
| The client did not answer in 1 h. | no answer in time |
Revision 2025-11-25 sends the list changes on the connection, and the client subscribes to a resource with resources/subscribe. Revision 2026-07-28 sends these notifications on a subscriptions/listen stream that the client opens17. The bridge keeps one such stream open for the client.
- The front end opens the stream after
notifications/initialized, when theinitializeresult declares a list change orresources.subscribe. - Without these, the front end opens no stream, and
resources/subscribeandresources/unsubscribeget -32601. - The filter of the stream has
toolsListChanged,promptsListChangedandresourcesListChangedfor the declared lists, andresourceSubscriptionswith the subscribed URIs. - One owner task holds the streams, the URIs and the state. Each stream has its own task and arena. Each notification for the client has its own scratch memory.
- The client gets only
notifications/tools/list_changed,notifications/prompts/list_changed,notifications/resources/list_changedandnotifications/resources/updated. - Each event loses the key
io.modelcontextprotocol/subscriptionIdof its_meta, and the bridge removes a_metathat is then empty. - The bridge drops a
notifications/resources/updatedwithout a stringuri. - The bridge reads each
notifications/subscriptions/acknowledgedand keeps its subscription id, but the client does not get it. - The bridge drops each other method, also
notifications/cancelled. - Only the active stream writes events. A stream becomes active at its first acknowledgment, and a newer stream takes its place at its own first acknowledgment.
The upstream server does not send the events of a gap again. Thus the client gets one list change for each declared list, and one notifications/resources/updated for each subscribed URI, after these acknowledgments:
- The acknowledgment of the first stream. VS Code lists the tools at the same time as
notifications/initialized, and a change before the first stream reaches no stream. - The acknowledgment of each stream after a loss.
- Each second or later acknowledgment of the active stream. The client of zig-sdk opens a lost stream again at once, at most 3 times (
max_lost_stream_retries).
A change of the subscriptions without a gap gives no list change and no update.
| End of the stream | What the bridge does |
|---|---|
The error Closed, TransportFailed or Timeout. The client of zig-sdk returns Timeout when the acknowledgment does not come in 10 s. |
The bridge opens a new stream after a wait. |
| A result of the upstream server, for example at its stop | The bridge opens a new stream after a wait. |
| A cancellation, a JSON-RPC error of the upstream server, another error, or the end of the upstream server | The bridge opens no new stream. Only a change of the subscriptions opens a stream again. |
| Over HTTP (M4): the status 408, 429, or 500 to 599 except 501 | The bridge opens a new stream after a wait. |
| Over HTTP (M4): the status 401 or 403, with a sign-in | The bridge opens a new stream after the next sign-in of a different request, and never after a timer. |
| Over HTTP (M4): each other status | The bridge opens no new stream. |
The first wait is 0.5 s (listen_backoff). Each later wait is two times longer, up to 30 s (listen_max_backoff). Only a stream that lives for 10 s after its acknowledgment resets the wait to 0.5 s.
- The owner task does one change at a time. The next change waits.
- A URI that the bridge has, or a URI to remove that it does not have, gets
{}at once. - For each other change, the bridge opens a new stream with all subscribed URIs.
- The first acknowledgment of the new stream makes it active, and it writes the response
{}. - Then the bridge cancels the old stream.
Thus the response comes before the first update of the new stream. No event of the old stream comes after the response. When the new stream fails before its acknowledgment, the old stream and the old URIs stay. The client then gets the error of the upstream server, or an error of the bridge with its cause. After the last unsubscribe, when the client knows no list, the bridge stops the stream and opens no new stream.
On a stdio upstream, the stream uses inline_notifications of zig-sdk. The reader task of the upstream client then gives each event to the bridge before it reads the next frame. Thus a list change that the upstream server writes before the result of a call reaches the client before that result. A process test examines this order in 1000 calls.
On an upstream server at a URL (M4), the listen stream and a call are different HTTP requests. Thus a list change can reach the client after the result of the call. In the embedded bridge (M5), the server publishes each event on the task that changes the server. Thus a list change of a call reaches the client before the result of that call.
At the end of the input, the listener cancels each stream and waits for the tasks of the streams. The front end waits for the listener and the requests for at most shutdown_grace. Then it closes the upstream client.
-
logging/setLevelsets the level of the client in each state. Revision 2026-07-28 has no such request18. Thus each upstream request and each listen stream has this level in its_meta. - A change of the level applies from the next round of a request.
- Without
logging/setLevel, the requests have no level, and the upstream server sends no log messages. - On a stdio upstream, a log message has no progress token and no subscription id. It comes through the
on_notificationcallback of the stdio client of zig-sdk. That callback gives only validnotifications/messageto the bridge, and it drops each other notification. - On the memory link, each request gives its log messages to its
on_logcallback. The embedded bridge of M5 and the tests use the memory link. - The bridge sends each log message as
notifications/messagewithlevel,loggeranddata, without the_metaof the upstream server. - The bridge drops a message below the level of the client, because a request can start before a change of the level.
- The
initializeresult declaresloggingonly when the upstream server declares it.
VS Code puts keys into the _meta of a request. The progress token stays with the bridge for the progress rule. Each other key of the _meta of a forwarded request goes to the upstream server only when all these rules are true19:
- The profile allows the key. The
vscodeprofile allowstraceparent,tracestateand each key that starts withvscode.. - The key obeys the grammar of
_metakeys, asvalidateKeyof zig-sdk examines it. - The key is not a key of zig-sdk:
progressToken, orprotocolVersion,clientInfo,clientCapabilities,logLevelorsubscriptionIdwith the prefixio.modelcontextprotocol/. - The key does not have a reserved prefix. The protocol reserves each prefix whose second label is
modelcontextprotocolormcp, for examplevscode.mcp/.
A key that passes these rules goes to the upstream server with its value, without a change. The bridge drops each other key. Thus one key that is not valid does not make the request fail. In the other direction, results lose the server information in _meta, events lose the subscription id, and log messages lose their _meta.
bridge.embed.serveStdioAuto serves one stdio connection in the process of a zig-sdk server. vscode.serveStdio calls it with vscode.profile, and a server calls vscode.serveStdio in place of mcp.transport.stdio.serve. The first request of the client selects the path of the connection. The path then serves the connection until the end of the input.
- The function reads each first line with the line framer of zig-sdk and the line limit of the server (
limits.stdio.max_line_bytes). - The framer skips a blank line. The function drops a line that is too long without a response, and it reads the next line.
- It parses the line with
Message.parseMaxDepthand the depth limit of the server (limits.json_max_depth). - The path reads each byte after the first request from the same reader. Thus two messages in one chunk reach the path.
| First message | Path |
|---|---|
A request with the method initialize
|
The legacy path. The word initialize in a value does not select it. |
A request with the method server/discover
|
No path. The discover rule applies, and the next line selects the path. |
| Each other request, a notification or a response | The modern path. |
| A line that is not valid JSON, a line deeper than the limit, or JSON that is not a JSON-RPC message | The modern path. The stdio transport of zig-sdk answers the line with an error. |
| A line that is not valid UTF-8 | The modern path. The function answers the line with the parse error of zig-sdk (-32700). |
| The end of the input before a request | No path. The function writes nothing and returns. |
A server/discover request before the first other request selects no path. The reader answers it at once with one of these options:
-
Discover.answer, the default: the server answers it, as the stdio transport of zig-sdk does. A client of revision 2026-07-28 then sends its next request and takes the modern path. -
Discover.refuse: the bridge answers -32601 with the causemethod_not_found. A client of the two revisions, such as the Copilot harness, then sendsinitializeand takes the legacy path.
With Discover.refuse, a client of revision 2026-07-28 that needs a server/discover result cannot use the server. A client that sends its next request without that result still takes the modern path. The vscode bridge gives the option as vscode.StdioOptions.discover.
| Path | Client | Rules |
|---|---|---|
| Legacy | A client of revision 2025-11-25, for example the Local harness of VS Code | A Frontend with the profile of the product gets the first line and the next lines. Its upstream server is the same server through mcp.transport.memory.ClientLink. The rules of this page apply. The time limits of the requests do not apply over the memory link. The wait for the answers of the client keeps its limit of 1 h. The fallback server name is the name of the server. |
| Modern | A client of revision 2026-07-28 | The stdio transport of zig-sdk (mcp.transport.stdio.Server) gets the first line and the next lines. A request above the limit of requests in flight gets -32603 at once. Thus the reader always reads the cancellations and the end of the input. |
The two paths take their limits from server.options.limits: the line length, the depth, the requests in flight and shutdown_grace. On the legacy path, a line that is too long gets -32600. On the first line and on the modern path, the function drops such a line without a response. On the legacy path, a handler of the server sees ctx.kind == .memory, and on the modern path it sees .stdio.
- The legacy path stops as
Frontend.shutdowndoes. - The modern path ends the listen streams of the server (
Server.shutdownSubscriptions). It waits for the other requests for at mostshutdown_grace, and a request that ends in that time still writes its response. - Then the modern path fires the cancel tokens of the requests that are left, and it cancels their tasks. A canceled request gets no response.
- At the return, the server has no listen stream. The function arms no watchdog.
The Roadmap gives the milestone of each part.
- MCP specification 2025-11-25: Model Context Protocol Specification 2025-11-25.
- MCP-SPEC-2026-07-28: Model Context Protocol Specification 2026-07-28.
-
src/bridge/translate.zig: The capabilities, the results and the error table of the bridge. -
MCP-DISCOVER-2026-07-28:
server/discover2026-07-28. - MCP-CACHING-2026-07-28: Caching of list results 2026-07-28.
- MCP-PAGINATION-2026-07-28: Pagination 2026-07-28.
-
VS Code issue 339276: An array parameter without
itemsblocks the tools of a server. - JSONSCHEMA-CORE-2020-12: JSON Schema 2020-12, core.
- MCP-PROGRESS-2026-07-28: Progress 2026-07-28.
- MCP-CANCELLATION-2026-07-28: Cancellation and time limits 2026-07-28.
- JSONRPC2: JSON-RPC 2.0 Specification.
- MCP-MRTR-2026-07-28: Multi round-trip requests 2026-07-28.
- MCP-SAMPLING-2026-07-28: Sampling 2026-07-28, deprecated in that revision.
- RFC3986: Uniform Resource Identifier (URI): Generic Syntax.
- MCP-ROOTS-2026-07-28: Roots 2026-07-28, deprecated in that revision.
-
MCP elicitation 2025-11-25: Elicitation in revision 2025-11-25, with
elicitationIdfor the URL mode. - MCP-SUBSCRIPTIONS-2026-07-28: Subscriptions 2026-07-28.
- MCP-LOGGING-2026-07-28: Logging 2026-07-28, deprecated in that revision.
-
MCP-BASIC-2026-07-28: Base protocol 2026-07-28, with the rules of
_meta.
zig-bridge-sdk uses the Apache License 2.0. The wiki uses the project profile of ASD-STE100 Simplified Technical English. Visual Studio Code and VS Code are trademarks of Microsoft Corporation. This project has no affiliation with Microsoft, and Microsoft does not endorse it.
zig-bridge-sdk
zig-sdk