Skip to content

Architecture

Christian Presley edited this page Oct 12, 2026 · 7 revisions

Architecture

This page describes the modules of zig-bridge-sdk and the path of a request from the client to the upstream server and back. A bridge is a stdio server of MCP revision 2025-11-251 for its client and a client of revision 2026-07-282 for the upstream server. The core module bridge has the parts that all bridges share. The module vscode and the executable mcp-bridge-vscode add the settings and the command line for Visual Studio Code (VS Code). Milestones M0 to M6 are on main, and the release 0.1.0 has them. They add the stdio upstream, the input requests, the notifications, the HTTPS upstream with OAuth, the embedded bridge and the start URL of the sign-in.

Applies to: zig-bridge-sdk 0.1.2 on main at commit 3cbc34b, the commit of the tag v0.1.2, with the milestones M0 to M6. zig-sdk 0.5.1 at commit 52fc1b8, the commit of the tag v0.5.1. Zig 0.16.03.

Modules

The package bridge_sdk has one core module and one module for each product. The Package Model page tells how the modules, the executables and the profiles fit together.

Module File Content
bridge src/bridge.zig The root of the core. It exports the parts in the rows below, bridge.Profile, bridge.version and the mcp module of zig-sdk.
bridge.legacy src/bridge/legacy.zig The part of revision 2025-11-25 that the bridge receives from its client. It parses the params of initialize, logging/setLevel, resources/subscribe, resources/unsubscribe and notifications/cancelled. It gives the class of each method and of each notification.
bridge.translate src/bridge/translate.zig The translation without I/O. It has the capability masks, the initialize result and the params of a forwarded request. It also has the result rules, the icon rule, the listen events, the log messages and the error table with the errors of the sign-in.
bridge.input src/bridge/input.zig The input requests of the upstream server (M2). It has the checks before the client gets them, the rounds and the checks of the answers.
bridge.notify src/bridge/notify.zig The listen stream of the upstream server (M3): the owner task, the subscribed URIs, the new streams after a loss and the synthetic events. From M4, it also has the rules for the HTTP status of a lost stream.
bridge.Frontend src/bridge/Frontend.zig The legacy stdio server for one client connection. It has the lifecycle, the reader, the slots, the b- pending table, the watcher of the upstream server and the output lock. From M4, it gives the sign-in the consent of the client.
bridge.Upstream src/bridge/Upstream.zig The mcp.Client of zig-sdk and its transport. The transport is a child process on stdio, Streamable HTTP to a URL (M4), the memory link, or a transport of the caller. A tracer writes one debug line for each exchange. isRemote selects the icon rule, and loadCaBundle adds the CA certificates of a PEM file.
bridge.oauth src/bridge/oauth.zig The sign-in at an HTTP upstream server (M4): the check of the authorization URL, the browser opener, the loopback receiver, SignIn, Gate and Authorizer. It also has the token stores, the index of the stored sign-ins, logout and the OAuth identity. From M6, it makes the one-time start URL of the browser.
bridge.embed src/bridge/embed.zig serveStdioAuto (M5): one stdio connection in the process of a zig-sdk server. The first request of the client selects the legacy path or the modern path.
bridge.log src/bridge/log.zig The log function of the executables. Each stderr line has a fixed tag, the scope and the level. The level can change at run time.
vscode bridges/vscode/vscode.zig vscode.profile, vscode.identity, vscode.client_metadata_url, vscode.serve, vscode.serveStdio, vscode.upstreamConfig and vscode.frontendOptions. The module also exports mcp and bridge.
mcp-bridge-vscode bridges/vscode/main.zig and bridges/vscode/cli.zig The executable: the command line with the stdio form, the URL form and logout, the log function, the exit codes and the watchdog.

bridge.legacy and bridge.translate do no I/O. Their functions take an arena, and that arena owns each value that they return. bridge.input and bridge.notify speak to the client only through functions of the front end: input.Peer and notify.Host. bridge.oauth asks the client about a sign-in only through oauth.Consent, which the front end gives.

The path of a request

This diagram shows a forwarded request, for example tools/call, with a stdio upstream server.

VS Code ──stdin line──▶ Frontend.run: the reader task
                          │  LineReader.next, Message.parseMaxDepth, legacy.classify
                          │  ping, logging/setLevel, server/discover, bad lines: the reader answers
                          ▼
                        Frontend.admit ──▶ slot task, with its own arena and cancel token
                          │  translate.forwardParams
                          ▼
                        Upstream.request ──▶ mcp.Client.requestAs ──▶ Tracer ──▶ stdio.Client
                          ▲                                                        │ stdin
                          │ raw result                                             ▼
                          │                                                upstream server
                          │ InputRequiredResult: input.Session.round
                          │   ──▶ b- requests to VS Code, then the next round with inputResponses
                          ▼
                        translate.shapeResult ──▶ Frontend.respond ──▶ out_lock ──▶ stdout ──▶ VS Code
Step Part What occurs
1 Frontend.run The reader task reads one line from stdin with its own LineReader. It parses the line in the arena of a new slot with mcp.jsonrpc.Message.parseMaxDepth.
2 legacy.classify The reader finds the class of the method. It answers ping, logging/setLevel, server/discover and each error itself. The section Lifecycle gives the rules.
3 Frontend.admit For initialize, a forwarded method, resources/subscribe and resources/unsubscribe, the reader starts a slot task.
4 translate.forwardParams Missing params become {}. The function removes task and _meta. It keeps the progress token of the client and the _meta keys that the profile allows.
5 Upstream.request The function calls mcp.Client.requestAs with allow_input_required, the time limit of the method, the cancel token of the slot, log_level and meta.
6 mcp.transport.stdio.Client The client of zig-sdk writes the request to the stdin of the child process and routes the response to the request. The stderr lines of the child process go to the stderr of the bridge.
7 input.Session When the result is an InputRequiredResult4, the round checks the input requests and sends them to VS Code as b- requests. Then the slot sends the upstream request again with inputResponses and requestState.
8 onProgress and forwardLog The progress of each round goes to the progress token of VS Code5. A log message goes to VS Code as notifications/message.
9 translate.shapeResult The result rules change the complete result for revision 2025-11-25. For a remote upstream server, translate.filterIcons also keeps only the data: icons.
10 Frontend.respond The slot writes the response as one line under the output lock. A canceled request gets no response.

With an upstream server at a URL (M4), step 6 uses the HTTP client transport of zig-sdk in place of stdio.Client. The provider of oauth.Authorizer gives the token of each request, and it answers each challenge of the upstream server. In the embedded bridge (M5), step 6 uses mcp.transport.memory.ClientLink to the server of the same process.

The bridge forwards eight methods: tools/list, tools/call, prompts/list, prompts/get, resources/list, resources/templates/list, resources/read and completion/complete. The two revisions have these methods with the same names. An unknown method gets the error -32601.

The result rules of translate.shapeResult:

  • Each result loses resultType, ttlMs, cacheScope and the server information in _meta.
  • A list result loses a nextCursor that is not a string. VS Code asks for the next page while nextCursor has a value, also the value null.
  • A tools/call result always has a content array. When it has structuredContent and no text block, it gets a text block with the serialized structuredContent.
  • The quirks of the profile change the tool schemas of a tools/list result. The Package Model page lists them.

When VS Code sends notifications/cancelled for a request in flight, the reader fires the cancel token of its slot, and VS Code gets no response. When the slot waits for the upstream server, the client of zig-sdk sends notifications/cancelled to the upstream server6. When the slot waits for the answers of VS Code, it cancels its b- requests. VS Code never gets a notifications/cancelled of the upstream server. The ids of the upstream server can be equal to the ids of VS Code.

On stdio, a log message of the upstream server has no progress token and no subscription id. The on_notification spawn option of the stdio client gives it to Upstream.on_log, and Frontend.forwardLog writes it to VS Code. That function drops a message below the level of the last logging/setLevel. onStdioNotification, the function of that spawn option, drops each other notification that belongs to no request. A notifications/cancelled of the upstream server is an example.

Lifecycle

Frontend.lifecycle is an atomic value with four states. Only the first initialize7 starts the upstream server, because only one initialize can win the compare-and-swap from awaiting_initialize to initializing.

                       initialize wins the compare-and-swap
  awaiting_initialize ──────────────────────────────────────▶ initializing
          ▲                                                        │
          │ spawn failure, discover failure or cancellation        │ discover result
          └────────────────────────────────────────────────────────┤
                                                                   ▼
                                                                 ready
  end of the input in each state, or loss of the upstream server ──▶ closing
State Start of the state What the bridge does
awaiting_initialize The start of the connection, and the end of a failed initialize. It waits for initialize.
initializing The first initialize wins the compare-and-swap. The slot starts the upstream command or makes the HTTP client (Upstream.connect), and it sends server/discover8. The time limit is 60 s, or the value of --discover-timeout. When a sign-in can start, the time limit also has --sign-in-timeout.
ready The discover result arrives. The state changes before the initialize response goes out, because the next requests of VS Code can come at once. It forwards the requests.
closing The end of the input, or the loss of the upstream server. It starts no new request.
Message Before ready In ready
ping The result {}. The result {}.
logging/setLevel The bridge keeps the level and answers {}. The bridge keeps the level and answers {}.
server/discover -32601 at once. -32601 at once.
initialize In awaiting_initialize, it starts the upstream server. In initializing, it gets -32600. -32600, with the cause already_initialized.
Another request -32601 when its _meta has the protocol version key of revision 2026-07-28. Else -32600, with the cause not_initialized. The bridge forwards it, or answers -32601 for an unknown method.
notifications/initialized The bridge ignores it. The bridge starts the listen stream one time, when the result declares a list change or resources.subscribe. From then on, a sign-in asks VS Code with a URL elicitation (M4).

The Copilot harness of VS Code sends server/discover first. The answer -32601 makes it continue with initialize.

The initialize result has the protocolVersion of the profile and the capabilities of the discover result after translate.ReplyMask. Its serverInfo comes from the _meta of the discover result. Without server information, the result has the fallback name and the version of the package. The result never has tasks or the Tasks extension.

When server/discover fails, initialize gets an error from the error table, and the bridge stops the upstream server. The state goes back to awaiting_initialize, so a new initialize can start the upstream server again. The bridge stays alive until the end of its input.

In closing, the bridge starts no new request. After the loss of the upstream server, a new request gets -32603 until the output closes. After the end of the input, the reader reads no more lines.

The reader never blocks

The reader task reads the lines of the client and never waits for a slot. It answers the requests of the lifecycle and the errors itself. It starts each other request with Io.Group.concurrent under in_flight_lock. At the limit max_in_flight_requests (256 by default), a new request gets -32603 at once. Thus the reader always reads notifications/cancelled and the answers of the client to the b- requests.

A slot task owns its arena, which holds the line and the parsed message. It also owns a cancel token. The slot writes its response one time, and the check and the write are one step under the output lock. A canceled request gets no response.

Unlike mcp.util.line_framer, the LineReader of the front end keeps the first 256 bytes and the last 256 bytes of a line that is too long. VS Code writes the id near the start of a line, and the TypeScript SDK 1.x writes it as the last member. recoverId and recoverTrailingId find the id in these bytes without a parse.

Line Code9 data.cause
A line longer than --max-line-bytes (64 MiB by default) -32600 line_too_long, with the limit in data.detail
A line that is not valid UTF-8, or not valid JSON -32700 parse_error
A message with more than 64 levels of nesting -32600 too_deep, with the limit in data.detail
Valid JSON that is not a JSON-RPC 2.0 message -32600 invalid_request_shape
A response with an id that is null, or not a string or an integer No response The reader drops it.

The error has the recovered id, or the id null when the reader finds no id. A bad line with the id of a b- request gets no error response. It resolves that entry of the pending table as an answer that is not valid.

The b- pending table

In M2, the bridge sends the input requests of the upstream server to the client as its own requests. Their ids are strings: the request_id_prefix of the profile and a number, for example b-1. Thus an id of the bridge cannot be equal to a numeric id of VS Code. The pending table is the table of mcp.transport.stdio.Client for the other direction.

  • The slot task puts all entries of a round into the table before the first request goes out. Thus the reader never sees an answer without its entry.
  • The reader resolves an answer at once, under pending_lock, and wakes the slot. It drops an answer with a numeric id, a null id, an id without the prefix, an unknown id, or a second answer. It writes only a debug line for it.
  • The slot waits on its wake event. It also wakes every 100 ms to examine its cancel token.
  • On a cancellation, at the time limit and at the end of the input, the slot first removes its entries from the table. Then it sends notifications/cancelled for each request that went out and has no answer.
  • VS Code ignores this cancellation and can answer late. The reader drops the late answer and writes nothing to stdout.
  • A round has at most 16 input requests (max_input_requests_per_round). The client gets 1 h for the answers of one round (Timeouts.input).

From M4, a sign-in after notifications/initialized also uses a b- request: a URL elicitation with the authorization URL.

The listen stream

After notifications/initialized, the front end starts one notify.Listener when the initialize result declares a list change or resources.subscribe. The listener has one owner task in the group of the front end. The owner task holds the streams, the subscribed URIs and the state. Each subscriptions/listen stream10 has its own task and its own arena. Each event for the client gets its own scratch memory.

Part Rule
Events VS Code gets only notifications/tools/list_changed, notifications/prompts/list_changed, notifications/resources/list_changed and notifications/resources/updated, without the subscription id. The acknowledgment and the other notifications stay in the bridge.
Changes resources/subscribe and resources/unsubscribe go to the owner task, one change at a time. For a new set of URIs, the owner task opens a new stream. The acknowledgment of the new stream writes the response {}, and then the owner task cancels the old stream.
Failed change When the new stream fails before its acknowledgment, the old stream and the old URIs stay, and VS Code gets the error.
Active stream Only the events of the active stream go to VS Code. A stream becomes active at its first acknowledgment, and an older stream never becomes active again. The callback compares the generation under the output lock (Host.writeIf).
Gaps The first acknowledgment of the first stream and of each stream after a loss ends a gap. Each later acknowledgment of the active stream also ends a gap. After a gap, VS Code gets one list change for each declared list. It also gets one notifications/resources/updated for each subscribed URI. A change of the URIs alone gives no such events.
New streams After a loss, the owner task waits 0.5 s, and the wait doubles up to 30 s. A stream that lives for 10 s after its acknowledgment resets the wait. A cancellation, a JSON-RPC error and the end of the upstream server stop the new attempts.
HTTP status (M4) Over HTTP, the status 408, 429 or 5xx except 501 gives a new stream after the wait. After 401 or 403, the owner task waits for the next sign-in of a different request (Options.sign_ins), and never for a timer. Each other status stops the new attempts.
Order Upstream.listen sets inline_notifications. Thus on stdio, a list change that the upstream server writes before the result of a call reaches VS Code before that result.

The client of zig-sdk opens a lost stream again at once (Retry.force), at most max_lost_stream_retries (3) times. On stdio, the acknowledgment must arrive in listen_ack_timeout (10 s).

The callback of a stream runs on the reader task of the upstream client. On the memory link, it runs on the task that publishes the event. It only translates the event and writes it under the output lock. It takes no other lock of the bridge, waits for nothing and sends no request. The lock order is: the subscription lock of the server, the lock of the stream, the lock of the memory link, then the output lock.

The output lock

Each frame to the client goes out under out_lock, as one line on stdout11. Thus the frames of concurrent tasks do not mix. The output lock is the last lock in each lock order. A task blocks its cancel during a write to the client, so that a cancel cannot stop a write in the middle. After the output closes, the front end writes no more frames.

Loss of the upstream server

On stdio, Upstream.connect starts the child process with max_restarts = 0, so the client of zig-sdk never starts it again. In the state ready, a watcher task examines Upstream.gone every 200 ms. When the child process closes its stdout, the watcher does these steps:

  1. It sets the state closing. It writes the exit code or the signal of the child process to stderr.
  2. It stops the admission of new requests.
  3. It answers each request in flight with -32603 and the cause upstream_exited. Then it cancels the request.
  4. It stops the listener. Each canceled slot sends notifications/cancelled for its b- requests without an answer.
  5. It waits for the slots for at most shutdown_grace (2 s). Then it closes the output.
  6. It calls Hooks.on_upstream_exit. mcp-bridge-vscode then exits with code 1.

VS Code then shows the server as stopped, and it starts the bridge again at the next request. A request that fails with error.Closed before the watcher runs also gets -32603 with the cause upstream_exited. An upstream server at a URL is different: Upstream.gone is always false. A failed request over HTTP gets an error, and the bridge stays alive. The memory link of the embedded bridge never loses its server.

End of the input

At the end of stdin, Frontend.run calls Frontend.shutdown. The function does these steps:

  1. It sets the state closing, and it stops the reader and the watcher.
  2. It stops a sign-in that waits for the redirect (Authorizer.abort). No new sign-in starts.
  3. It calls Hooks.on_eof. mcp-bridge-vscode starts its watchdog thread here.
  4. It stops the admission and cancels each request in flight. A canceled request gets no response.
  5. It stops the listener. The listener cancels each stream.
  6. It waits for the slots and the listener for at most shutdown_grace (2 s). Then it cancels the tasks that are left.
  7. It closes the output and the upstream. The stdio client closes the stdin of the child process and reaps it.
  8. Frontend.run returns .eof. mcp-bridge-vscode then exits with code 0.

A child process that does not exit in shutdown_grace gets a termination signal, and after one more shutdown_grace a kill signal. On POSIX, the signals go to the process group of the child process.

Only the executable starts the watchdog, and the library does not. The watchdog is a plain thread and not a task, so it runs also when all tasks wait. After 8 s, it sends a kill signal to the process group of the child process on POSIX, and it exits with code 0. On Windows, the job object of the child process stops the process tree when the bridge exits. VS Code sends a termination signal 10 s after it closes stdin (POSIX only), and it stops the process tree after 20 s. The watchdog stops the bridge before these limits.

The HTTP upstream (M4)

vscode.serve gives bridge.Upstream the configuration Config.Http: the URL, the headers of the command line, the provider of the sign-in, the trust and the proxy. The HTTP client transport of zig-sdk (mcp.transport.HttpClient) sends the requests over Streamable HTTP12. Each request has its own connection, and a failed request fails alone. max_response_bytes (64 MiB) limits each JSON response and each event of a stream. The error of a failed request has the HTTP status of Diagnostics.http_status in data.detail.

This diagram shows a sign-in13 after a challenge of the upstream server.

upstream server ──401 or 403──▶ HttpClient ──▶ Authorizer.provider ──▶ OAuthClient (zig-sdk)
                                                                            │ authorization URL
                                                                            ▼
                     SignIn: validateAuthorizationUrl, then Receiver.start on 127.0.0.1:<port>
                                                                            │ sign-in line on stderr
                                                                            ▼
                     Gate: before notifications/initialized ──▶ Opener: the start URL to
                                                                          ShellExecuteW, $BROWSER,
                                                                          open or xdg-open
                           after notifications/initialized ──▶ Consent: a URL elicitation to VS Code
                                                                          with the authorization URL
                                                                            │
                                                                            ▼
                     Receiver: GET /start/<token> from the browser ──▶ 303 to the authorization URL
                                                                            │
                                                                            ▼
                     Receiver.wait: GET /callback with the expected state ──▶ OAuthClient: code,
                                                                          tokens ──▶ TokenStore
Part Rule
validateAuthorizationUrl The authorization URL uses https, has a host, and has no user information and no fragment. It has only the bytes 0x21 to 0x7E without ``"<>^`{
Opener On Windows, the opener calls ShellExecuteW. On other systems, it starts $BROWSER, open or xdg-open with the URL as the only argument, without a shell. From M6, a browser gets only the one-time start URL of the receiver (Delivery.browser), and never the authorization URL. The stdin and the stdout of that program are on the null device. Its program gets no secret variable of the environment.
Receiver It listens on 127.0.0.1 at the redirect port, before the browser opens. A GET of the callback path with the expected state ends the wait. From M6, the first GET of the start path /start/<token> gets 303 to the authorization URL, and a second GET ends the wait with error.StartReused. Each connection has a limit of 16 KiB and 10 s for its request head. The accept loop uses mcp.util.wake.cancelAcceptLoop. On main after 0.1.0, the accept ignores a cancel, and only the wake connection of stop ends it.
SignIn The authorize callback of OAuthClient. It checks the URL, starts the receiver, writes the sign-in line once, calls the opener and waits until the time limit, a cancel or abort. From M6, it makes a new start token for a browser, and the sign-in line keeps the authorization URL. It records the cause of each failure.
Gate Before notifications/initialized, it opens the browser. After it, it asks the client through Consent, and the bridge never opens the browser. A declined sign-in starts a cooldown of 60 s for all scopes.
Authorizer It holds the OAuthClient of zig-sdk, the SignIn, the Gate and the indexed token storage. Its provider records the cause of each challenge that it did not answer. The front end reads it with problemSince and makes the error of the request.
TokenStore auto tries the keychain of the host, then encrypted files with a key from outside the token directory, then memory.
IndexedStorage It keeps an index of the stored sign-ins in the same store. logout and logoutAll read it.
Identity The client name mcp-bridge-vscode (zig-bridge-sdk), the keychain service zig-bridge-sdk/mcp-bridge-vscode and the storage identity.

OAuthClient runs one sign-in at a time. zig-sdk does the OAuth protocol: PKCE, the check of state and iss, and the refresh of the tokens. It also checks that the resource of the metadata covers the URL. Each request gets one step-up attempt (max_step_up_attempts).

The embedded bridge (M5)

bridge.embed.serveStdioAuto(io, gpa, server, in, out, options) serves one stdio connection in the process of a zig-sdk server. vscode.serveStdio calls it with vscode.profile and the stdin and stdout of the process.

client ──first line──▶ serveStdioAuto: line framer, Message.parseMaxDepth, classify
                          │ server/discover: the server answers (.answer), or -32601 (.refuse)
                          │                  and no path
                          ├── initialize ──▶ legacy path: Frontend ──▶ Upstream (memory)
                          │                     ──▶ ClientLink ──▶ mcp.Server of the process
                          └── each other line ──▶ modern path: mcp.transport.stdio.Server
                                                    ──▶ mcp.Server of the process
Part Rule
The first line The function reads the first lines with mcp.util.line_framer and the line limit of the server. It parses them with the depth limit of the server. Only a request with the method initialize selects the legacy path.
server/discover A server/discover before the first other request selects no path. With Discover.answer, the default, the server answers it on the reader task. With Discover.refuse, the bridge answers -32601.
Legacy path A Frontend with the profile of the product. Its Upstream has the configuration Config.memory, thus it reaches the server through mcp.transport.memory.ClientLink. The fallback name is the name of the server.
Modern path The stdio transport of zig-sdk, with when_full set to .reject. Thus the reader never waits for a free slot.
Limits The two paths take the line length, the depth, the requests in flight and shutdown_grace from server.options.limits.
End of the input The legacy path stops as Frontend.shutdown does. The modern path calls Server.shutdownSubscriptions, waits for at most shutdown_grace, fires the cancel tokens of the requests that are left and cancels their tasks. The function arms no watchdog.

On the legacy path, the server publishes an event on the task that changes the server. The callback of the stream only translates the event and writes it under the output lock. Thus a list change of a call reaches VS Code before the result of that call.

Limits and time limits

Limit Value Source
server/discover 60 s, or --discover-timeout Frontend.Options.discover_timeout
server/discover when a sign-in can start --discover-timeout and --sign-in-timeout together Authorizer.interactive and Authorizer.signInTimeout
The list requests and completion/complete 120 s Frontend.Timeouts.list
The first round of prompts/get and resources/read 120 s Frontend.Timeouts.read
Each round of tools/call 1 h. A cancellation of VS Code stops it earlier. Frontend.Timeouts.call
The answers of the client to one round 1 h Frontend.Timeouts.input
Rounds of one request 10 mrtr_max_rounds_client of zig-sdk
Input requests of one round 16 Frontend.Options.max_input_requests_per_round
Requests in flight 256 max_in_flight_requests of mcp.Limits
Progress notifications of one request 50 each second max_progress_rate_per_s of mcp.Limits
One line, on the side of VS Code and on the upstream side 64 MiB, or --max-line-bytes Upstream.default_max_line_bytes
One JSON response or one event of a stream over HTTP 64 MiB, or --max-response-bytes Upstream.default_max_response_bytes
Nesting of a message of the client 64 levels json_max_depth of mcp.Limits
Nesting of a message of the upstream server 200 levels mcp.json.max_message_depth
The wait for the requests in flight at the end 2 s shutdown_grace of mcp.Limits
The watchdog after the end of stdin 8 s bridges/vscode/main.zig
The interval of the watcher 200 ms Frontend.Options.watch_interval
The log lines about a long request After 10 s, then each 30 s The watcher
The wait before a new listen stream 0.5 s, doubles up to 30 s listen_backoff and listen_max_backoff
One sign-in 300 s, or --sign-in-timeout oauth.default_sign_in_timeout_s
The head of a request at the loopback receiver 16 KiB and 10 s oauth.Receiver.Limits
Connections at the loopback receiver at one time 16 oauth.Receiver.Limits
An authorization URL 8 KiB oauth.max_url_bytes
The wait after a declined sign-in through the client 60 s oauth.default_consent_cooldown_s
The limits of the embedded bridge The limits of the server server.options.limits

Each time limit is finite. The upstream server of tools/call can need a long time, so its limit is long, and the cancellation of VS Code is the outer limit. On the memory link of the embedded bridge, the time limits of the requests do not apply. Only the wait for the answers of the client keeps its limit.

Error table

Each error of the bridge has a code, a fixed message and data.cause with the name of the cause. Some causes also have data.detail, for example the method and the limit of a time limit. A message never contains a secret, the URL of the upstream server or the stderr output of the child process. translate.codeOf, translate.messageOf, translate.errorFor and translate.problemError hold the table.

Cause Code When
parse_error -32700 A line that is not valid JSON or not valid UTF-8.
invalid_request_shape -32600 A message that is not a valid JSON-RPC 2.0 request.
line_too_long -32600 A line over --max-line-bytes.
too_deep -32600 A message with more than 64 levels of nesting.
not_initialized -32600 A request before initialize.
already_initialized -32600 A second initialize.
method_not_found -32601 server/discover, an unknown method, a request of revision 2026-07-28 before initialize, and resources/subscribe without a listen stream.
invalid_params -32602 The params of initialize, logging/setLevel, resources/subscribe or resources/unsubscribe are not valid.
invalid_request and invalid_meta -32602 zig-sdk cannot send an argument of the request, or it refuses a _meta key.
spawn_failed -32603 The bridge cannot start the upstream command.
connect_failed -32603 The bridge cannot make the HTTP client for the URL (M4).
discover_failed -32603, or the code of the upstream error server/discover gave a JSON-RPC error or did not answer in time. data.detail has the upstream message or the time limit. Another failure of server/discover gives the cause of its zig-sdk error.
upstream_exited -32603 The upstream server process stopped.
too_many_requests -32603 The limit of requests in flight.
timeout -32603 The upstream server did not answer in the time limit of the method.
invalid_input_request, too_many_input_requests, input_timeout and invalid_client_answer -32603 A failure of the input requests of a round (M2). For a failed check, data.detail names the key of the input request.
sign_in_failed, sign_in_timeout, sign_in_denied, sign_in_error, sign_in_invalid_url, browser_launch_failed and redirect_port_unavailable -32603 A sign-in failed (M4). The messages of sign_in_timeout and redirect_port_unavailable name the option to use.
sign_in_start_reused -32603 A second GET of the start URL stopped the sign-in (M6). The message names the host of the bridge and --no-browser.
registration_unavailable, registration_failed, issuer_not_registered and invalid_client_metadata_url -32603 The bridge has no client registration that the authorization server accepts (M4). Each message names the option to use. The messages of registration_unavailable and registration_failed also name the redirect URI.
access_refused, reauthorization_required and sandbox -32603 The upstream server refused the access after the sign-in, or a later sign-in did not reach the browser. The cause sandbox tells that the bridge runs in the sandbox of VS Code (M4).
closed, transport_failed, invalid_response, too_many_rounds, out_of_memory, undeclared_input_request, hook_failed, not_connected and task_cancelled -32603 The other members of mcp.Client.RequestError.
rpc -32603 An upstream error without its JSON-RPC error object.
canceled No response VS Code canceled the request.

A JSON-RPC error of the upstream server goes to VS Code with its code, its message and its data. The error -32042 becomes -32603 without data, because VS Code opens each URL in its data. When the upstream server refuses the requestState of a round with -32602, a tools/call gets a result with isError: true and a text for the user. Another method gets an error with that text and the upstream data.

mcp-bridge-vscode exits with code 0 at the end of stdin. It exits with code 1 after the loss of the upstream command or an internal error. It exits with code 2 for arguments that are not valid and for a missing variable of --header-env. It also exits with code 2 for a --ca-file that it cannot use, or a token store that does not open.

Diagnostics

Each stderr line of the bridge has the form mcp-bridge-vscode: <scope>: <level>: <message>. VS Code shows these lines in the Output channel of the server, together with the stderr lines of the child process. bridge.log writes each line with one write under the stderr lock, and a control character becomes a space. The level comes from --log-level, and the default is info. The sign-in line has the form mcp-bridge-vscode: sign in at <url>.

The tracer of Upstream writes one debug line for each exchange. The line has the upstream id, the round, the id of the client request, the method, the outcome and the time. It has no content of a frame, no header value and no token. The watcher writes an info line about a request that waits for 10 s, and then each 30 s.

zig-sdk 0.4.0

build.zig.zon pins the commit 52fc1b83c5f70f9e6a1fc2f1e2e71250656672cc of zig-sdk, the commit of the tag v0.5.114. Release 0.1.0 pinned the commit 8334b15 of the tag v0.4.0. Milestones M3 and M4 pinned the commit cda66dc of the 0.4.0 stack, and milestones M0 to M2 used zig-sdk 0.3.0. The bridge uses these parts that are new in zig-sdk 0.4.0. The letters are the changes on the Roadmap.

zig-sdk part Change Use in the bridge
RequestOptions.log_level A Each upstream request and each listen stream has the level of the last logging/setLevel of VS Code. Revision 2026-07-28 sends the level with each request15.
RequestOptions.meta, meta.isSdkOwnedRequestKey and error.InvalidMeta B translate.passMeta keeps the keys that the profile allows and that zig-sdk accepts. Upstream sends them as meta.
The acknowledgment of a listen stream before its first event C A fix in the server of zig-sdk. It applies to the fixture server, to each upstream server on zig-sdk 0.4.0 and to the server of the embedded bridge.
Notifications in scratch memory for each frame E The method and the params of a callback are valid only during the call. A long listen stream does not make the request arena larger.
A size limit for each event of an SSE response G --max-response-bytes limits each event of a stream over HTTP, and the stream itself has no limit.
stdio.Client.exitStatus, Diagnostics.http_status, and a failed request for a frame that the router cannot read H Upstream.reap gives the exit code or the signal for the log line after a loss. A failed request over HTTP names its HTTP status. A bad upstream frame gives invalid_response at once, and not a time limit error.
RequestOptions.inline_notifications I Upstream.listen sets it. A list change reaches VS Code before the result of the call that caused it.
OAuthClient.Options.ca_bundle, and the reload of the stored token record J --ca-file applies to the requests of the sign-in. The client of zig-sdk reads the stored token record again before a refresh, and never writes cleared tokens over a newer record.
Proxies of HTTPS_PROXY, ALL_PROXY and NO_PROXY K The URL form gives the environment to Config.Http.proxy and to OAuthClient.
The spawn options on_notification and userdata of stdio.Client A fix of the 0.4.0 stack Upstream.connect sets them. onStdioNotification gives only notifications/message to Upstream.on_log.

The bridge also uses other parts of zig-sdk: mcp.Client with allow_input_required and Retry.force, and mcp.transport.stdio.Client.spawn with max_restarts. M4 uses mcp.transport.HttpClient, mcp.auth.OAuthClient, the token storages of mcp.auth and mcp.util.wake.cancelAcceptLoop. M5 uses mcp.transport.memory.ClientLink, mcp.transport.stdio.Server with when_full and Server.shutdownSubscriptions. Others are mcp.jsonrpc.Message.parseMaxDepth, mcp.util.line_framer, mcp.protocol.meta.validateKey, mcp.types.checkSamplingMessages, mcp.types.isValidUrl and the schema validator.

The Roadmap gives the state of each milestone, and the Threat Model gives the security requirements of each module.

References

  1. MCP-CHANGELOG-2025-11-25: Key changes of revision 2025-11-25.
  2. MCP-SPEC-2026-07-28: Model Context Protocol Specification 2026-07-28.
  3. ZIG-0.16-RELNOTES: Zig 0.16.0 release notes.
  4. MCP-MRTR-2026-07-28: Multi round-trip requests 2026-07-28.
  5. MCP-PROGRESS-2026-07-28: Progress 2026-07-28.
  6. MCP-CANCELLATION-2026-07-28: Cancellation and time limits 2026-07-28.
  7. Lifecycle 2025-11-25: The initialize request and the lifecycle of revision 2025-11-25.
  8. MCP-DISCOVER-2026-07-28: server/discover 2026-07-28.
  9. JSONRPC2: JSON-RPC 2.0 Specification.
  10. MCP-SUBSCRIPTIONS-2026-07-28: Subscriptions 2026-07-28.
  11. MCP-STDIO-2026-07-28: The stdio transport 2026-07-28.
  12. MCP-HTTP-2026-07-28: Streamable HTTP transport 2026-07-28.
  13. MCP-AUTH-2026-07-28: Authorization overview 2026-07-28.
  14. zig-sdk CHANGELOG.md at v0.5.1: The changes of each release of zig-sdk up to 0.5.1.
  15. MCP-LOGGING-2026-07-28: Logging 2026-07-28.