| title | Stdio MCP Proxy Guide |
|---|---|
| created | 2026-08-01 |
| updated | 2026-10-01 |
labby proxy runs a stdio MCP server in the foreground and exposes its tools
as a Streamable HTTP endpoint. With --mcp-json, it aggregates local stdio
and remote HTTP servers from a .mcp.json. Funnel makes the endpoint reachable
from the public internet; Labby permits Funnel only with OAuth. In Funnel mode, this
single proxy process also serves the OAuth routes, including the Google
callback. It needs durable Labby OAuth state but no separate labby serve.
You need: Node.js, signed-in Tailscale with Funnel enabled, and Apple
Silicon or Linux with KVM. Verify the selected release includes Funnel proxy support with proxy --help.
-
Get your Google callback first:
npx -y @dinglebear/labby proxy --funnel --port 8443Copy the printed callback into a Google Cloud Web application OAuth client's authorized redirect URIs. This preview publishes nothing.
-
Configure OAuth and save defaults. Replace the example origin with the printed origin; provide your Google client ID, secret, and admin email when setup requests them:
npx -y @dinglebear/labby setup --role server --auth oauth --oauth google --public-url https://YOUR-NODE.ts.net:8443 --no-desktop --yes npx -y @dinglebear/labby config proxy set --exposure funnel --auth oauth --port 8443 --yes
-
Start it and keep it running:
npx -y @dinglebear/labby proxy -- npx -y microsandbox-mcp -
Connect ChatGPT: enable Developer mode, create an app using the printed
/mcpURL and OAuth, then sign in with Google. -
Ask ChatGPT: “Check the Microsandbox runtime, install it if needed, then create, run, and remove a test sandbox.”
Keep ~/.labby across restarts. Subscriptions work; no OpenAI API integration
is required. See the detailed steps below for prerequisites and troubleshooting,
or use the private OpenAI tunnel.
Labby and the Microsandbox MCP adapter can both run through npx; there is no
separate manual Labby binary install for this path. You still need Node.js
22+ and Tailscale. The Microsandbox runtime can be installed through its MCP
tools after connecting ChatGPT. The Labby npm launcher downloads
the matching native Labby release binary on first use and verifies its release
checksum. It does not verify the release's build attestation; use the
verified installer if that provenance check is
required. The npm launcher supports macOS Apple Silicon and Linux x86-64.
Keep npm's cache and $LABBY_HOME available across runs.
The commands below require a Labby release containing Funnel proxy support.
Verify proxy --help lists --funnel and --mcp-json in the selected release.
If either is absent, replace npx -y @dinglebear/labby with a checkout binary
containing those features. Keep the child command npx -y microsandbox-mcp
as shown.
-
Install Tailscale on the host that will run Labby and the microVMs. On macOS, Funnel requires a Tailscale open source variant. Sign in, enable Funnel for the node in the tailnet admin console, and ensure its HTTPS certificate is available. Run this before creating Google OAuth credentials:
npx -y @dinglebear/labby proxy --funnel --port 8443Labby reads the node's public DNS name from
tailscale status --jsonand prints the exact Google redirect URI and MCP URL when provider credentials are absent. The preview starts no child and makes no public mapping. Record the printed port; use that same port on every run so the callback and ChatGPT connector URL stay stable. Funnel supports HTTPS ports 443, 8443, and 10000. -
In Google Cloud, configure the OAuth consent screen, then create a Web application OAuth client. Add the printed redirect URI to its authorized redirect URIs. Save the client ID and secret. Set up Labby Google OAuth using the printed origin as
--public-url, and select--auth oauth:npx -y @dinglebear/labby setup --role server --auth oauth --oauth google \ --public-url https://node.example.ts.net:8443 --no-desktop --yes
Provide
LABBY_GOOGLE_CLIENT_ID,LABBY_GOOGLE_CLIENT_SECRET, andLABBY_AUTH_ADMIN_EMAILthrough the setup's secret inputs. Server setup without--deploymentprepares authentication without installing or starting a daemon. The proxy initializes its durable OAuth state on first startup. Keep$LABBY_HOME, its.env, auth database, and signing key across restarts. The configured public origin must exactly match the Funnel origin, including its port. Save the proxy defaults once, using the same port:npx -y @dinglebear/labby config proxy set \ --exposure funnel --auth oauth --port 8443 --yes
-
Ensure the host supports local microVMs: Linux needs KVM access, and macOS requires Apple Silicon. The official microsandbox-mcp adapter starts through
npxeven before the runtime is installed. It exposesruntime_checkandruntime_installfor the next step; starting the adapter alone does not automatically install the runtime. -
Start the proxy. It reads Funnel, OAuth, and the stable port from the saved configuration, so subsequent runs need no exposure or port flags:
npx -y @dinglebear/labby proxy -- npx -y microsandbox-mcpPin the MCP package for a durable deployment. The proxy runs in the foreground and prints its exact public
/mcpURL. Keep it running.--funnelselects OAuth automatically. Explicit--auth none,bearer, ortailnetfails before publication. The longer--print-google-callbackoption remains available to reprint the callback later without starting the proxy. -
Enable ChatGPT web developer mode in Settings → Security and login. Open ChatGPT Plugins, select the plus button, and create a developer-mode app with the printed URL and OAuth authentication, then complete Labby's Google sign-in. On Business, a workspace admin/owner enables developer mode and creates the app; Enterprise/Edu admins grant developer access. Ask ChatGPT to call
runtime_check, useruntime_installif needed, and check again. Then create, run, and remove one disposable sandbox. If MCP installation fails, use the official manual runtime installer on the host, then repeatruntime_check:curl -fsSL https://install.microsandbox.dev | shDeveloper mode supports read and write tools on Plus, Pro, Business, Enterprise, and Education web accounts, subject to confirmation settings and workspace permissions. Select Developer mode and the app in the chat composer before asking it to control a sandbox.
The proxy exposes the Microsandbox MCP tools directly. It does not install a coding agent inside a VM, manage task worktrees, or turn a one-off command sandbox into a durable worker. Give each task an explicit source transfer, agent runtime, resource limit, and cleanup plan.
ChatGPT subscriptions can use custom MCP apps; a Responses API integration is not required.
Secure MCP Tunnel can connect ChatGPT to a private MCP endpoint. This alternative uses OpenAI organization/workspace permissions; the Google OAuth Funnel flow above remains available when Google sign-in is required.
-
In Platform tunnel settings, create a tunnel and associate its owning Platform organization and target ChatGPT workspace. Personal accounts use their personal Platform organization. Creating a tunnel requires Tunnels Read + Manage; running/selecting it requires Tunnels Read + Use.
-
Download
tunnel-clientfrom those settings or the official release. Start a loopback-only Labby proxy; record its URL:npx -y @dinglebear/labby proxy --local --auth none --port 18765 -- npx -y microsandbox-mcp -
Supply the runtime API key as
CONTROL_PLANE_API_KEY, then configure and run:tunnel-client init --sample sample_mcp_stdio_local --profile labby \ --tunnel-id tunnel_YOUR_ID --mcp-server-url http://127.0.0.1:18765/mcp tunnel-client doctor --profile labby --explain tunnel-client run --profile labby
-
Create the ChatGPT developer-mode app with Connection → Tunnel, select the tunnel, and choose No Authentication for this private endpoint. Keep both processes running. Never publish this unauthenticated listener.
Tunnel transport does not automatically make an OAuth authorization server reachable for browser sign-in. If you select OAuth, its issuer and callback must still be reachable. Consult the linked tunnel documentation for permission and workspace-association troubleshooting.
Run the one-time setup, check the host, then launch a JavaScript server:
labby config proxy set
labby doctor proxy
labby proxy /path/to/dist.jsThe built-in defaults are Tailscale Serve exposure, tailnet authentication,
/mcp, and a random port from 49152 through 65535. After setup, the normal run
needs no proxy flags. Startup prints the resolved child command, public URL,
exposure, and auth policy, then waits for Ctrl+C.
MCP proxy ready
Server node /path/to/dist.js
URL https://node.example.ts.net:53147/mcp
Exposure Tailscale Serve
Auth Tailnet
Press Ctrl+C to stop.
Use --json for one machine-readable readiness object. It contains url,
exposure, auth, external_port, local_addr, command, child_pid, and
protocol_version. It never contains a bearer token, OAuth token, lease ID,
authorization header, or child environment value.
Labby parses its options only before the first child token. Everything after
that token belongs to the child, including values beginning with -:
labby proxy /path/to/dist.js --workspace /srv/data --read-only
labby proxy --port 52177 /path/to/dist.js --child-flagAn explicit separator is accepted but is not normally required:
labby proxy -- npx -y @modelcontextprotocol/server-filesystem /srv/dataThe first child token resolves in this order:
- An existing executable file runs directly.
- An existing file with a valid shebang runs directly when executable; on Unix, a non-executable file may use a standards-compliant interpreter plus one optional shebang argument.
.js,.mjs, and.cjsfiles usenodefromPATH..pyfiles usepython3fromPATH.- A bare command is resolved through
PATH. - An unknown, non-executable file fails with an explicit-command suggestion.
Labby never invokes a shell. TypeScript has no inferred launcher; use a
shebang or an explicit command such as labby proxy -- npx tsx server.ts.
Arguments are retained as OsString, including non-UTF-8 Unix arguments.
--cwd changes the child working directory; otherwise the caller's current
directory is used.
| Setting | Behavior |
|---|---|
tailscale exposure |
Binds HTTP to loopback, publishes one HTTPS port with Tailscale Serve, and prints the tailnet URL. This is the default. |
funnel exposure |
Binds HTTP to loopback and publishes one public HTTPS port with Tailscale Funnel. Requires OAuth and port 443, 8443, or 10000. Random mode tries 8443, 10000, then 443. |
local exposure |
Binds and prints a loopback HTTP URL only. Use --local; it never binds a LAN wildcard. |
tailnet auth |
Adds no application token. Reachability and grants are owned by Tailscale. Valid only with Tailscale exposure. This is the default. |
bearer auth |
Requires the separate proxy bearer token on every MCP request and SSE stream. |
oauth auth |
Validates Labby-issued JWTs for the exact proxy resource URL and configured scopes. Funnel hosts its own OAuth router; Serve uses a live OAuth daemon. |
none auth |
Adds no application authentication. Intended for explicit loopback use such as --local --auth none. |
| random port | Chooses an unused external Serve port from the configured range, with collision retries. |
| fixed port | Uses the numeric proxy.port or one-run --port; startup fails rather than replacing an existing mapping. |
One-run examples:
labby proxy --port 52177 /path/to/dist.js
labby proxy --auth oauth /path/to/dist.js
labby proxy --funnel --port 8443 -- npx -y microsandbox-mcp
printf '%s\n' "$TOKEN" | labby proxy --auth bearer --bearer-token-stdin /path/to/dist.js
labby proxy --local --auth none /path/to/dist.js--bearer-token also implies bearer mode, but shell history and process
inspection can expose literal command-line secrets. Prefer setup-generated
storage, the environment, or --bearer-token-stdin.
There is no silent fallback. A Tailscale, bearer, OAuth, port, child, or publication failure stops startup; a runtime failure begins owned cleanup.
The effective order is:
- one-run CLI options;
- existing process environment, then values loaded from exactly
$LABBY_HOME/.env(normally~/.labby/.env) for unset names; - exactly
$LABBY_HOME/config.tomlwhen the absolute override is set, otherwise~/.labby/config.toml; - built-in defaults.
Only secret and executable/logging controls use environment variables. Proxy preference keys do not have implicit one-to-one environment aliases. The working directory does not participate in configuration discovery. See Runtime Configuration for the canonical path contract.
Complete [proxy] table:
[proxy]
exposure = "tailscale"
auth = "tailnet"
path = "/mcp"
port = "random"
port_range_start = 49152
port_range_end = 65535
bearer_token_env = "LABBY_PROXY_BEARER_TOKEN"
oauth_scopes = ["mcp:read", "mcp:write"]
inherit_env = []
shutdown_grace_ms = 3000| Key | Accepted values and validation |
|---|---|
proxy.exposure |
tailscale, funnel, or local; default tailscale. |
proxy.auth |
tailnet, bearer, oauth, or none; default tailnet. local plus tailnet is rejected. |
proxy.path |
Absolute, non-root path without query, fragment, . segments, or .. segments; default /mcp. |
proxy.port |
"random" or a nonzero integer. It is the external HTTPS port for Tailscale publication. |
proxy.port_range_start |
First random candidate; default 49152, minimum 1024. |
proxy.port_range_end |
Last random candidate; default 65535, and not below the start. |
proxy.bearer_token_env |
Valid environment-variable name containing the bearer secret; default LABBY_PROXY_BEARER_TOKEN. |
proxy.oauth_scopes |
Non-empty, whitespace-free scope tokens; default mcp:read and mcp:write. |
proxy.inherit_env |
Extra valid ambient variable names copied into the otherwise scrubbed child environment. |
proxy.shutdown_grace_ms |
Validated range 1..=60000; default 3000. |
Generated machine-readable and Markdown inventories are in
proxy-config-reference and the
environment reference.
Proxy-relevant environment variables:
| Variable | Purpose |
|---|---|
LABBY_PROXY_BEARER_TOKEN |
Default secret source. If bearer_token_env names another key, that configured key is used instead. |
LABBY_TAILSCALE_BIN |
Overrides the tailscale executable used by publication and proxy preflight. |
LABBY_GW_UPSTREAM_STDERR |
Controls the redacted child-stderr forwarding level; default debug, and off discards it. |
LABBY_HOME |
Absolute durable-state root. Relocates config/secrets and fixes the access store at $LABBY_HOME/access.db; relative values fail closed. |
LABBY_MCP_HTTP_HOST, LABBY_MCP_HTTP_PORT |
First live-daemon candidate for OAuth lease actions. |
LABBY_MCP_HTTP_TOKEN |
Authenticates the proxy CLI to the daemon's admin gateway action route; it is not accepted by the proxied OAuth resource. |
LABBY_PUBLIC_URL, LABBY_MCP_GATEWAY_URL |
Public live-daemon fallback candidates and stable OAuth issuer configuration, as described in the OAuth guide. |
LABBY_PROXY_TEST_RENEW_MS |
Test-only renewal interval available with proxy-testkit; never use it as production configuration. |
PATH is consulted for launcher inference. Runtime-essential variables are
copied by the stdio spawn policy; other ambient variables require
proxy.inherit_env, --inherit-env NAME, or an explicit --env NAME=VALUE.
The latter two are child-process inputs, not persisted proxy preferences.
labby config proxy set is interactive on a terminal. For automation use --yes
and explicit flags; --dry-run previews without mutation. The setup action
preserves unrelated TOML keys, comments, and .env entries and is byte-stable
on a second identical run.
Bearer setup has two safe paths:
# Generate a new 64-character random hex token when none exists.
labby config proxy set --yes --auth bearer
# Store a supplied token from stdin; the literal is never written to TOML.
printf '%s\n' "$TOKEN" | labby config proxy set --yes --bearer-token-stdinThe non-secret preferences go to $LABBY_HOME/config.toml; the bearer secret
goes to the environment key named by proxy.bearer_token_env in
$LABBY_HOME/.env. Existing secrets are reused unless stdin supplies a
replacement. On Unix, setup creates or repairs $LABBY_HOME to mode 0700
and .env to mode 0600. Output, debug formatting, and JSON report only that
a secret changed, never its value.
labby doctor proxy with no route arguments performs the zero-route local
preflight: proxy config validation, Node/Python launcher presence, selected
auth prerequisites, Tailscale version/connectivity/DNS/Serve capability, and
OAuth issuer/daemon/create-renew-release action checks where applicable.
The older routed reverse-proxy doctor remains available and unchanged:
labby doctor proxy \
--app-url https://lab.example.com \
--mcp-url https://mcp.example.com \
--route /telemetrySupplying route arguments selects that public Labby/protected-route check; it does not run the local stdio-proxy preflight.
Save a .mcp.json containing stdio entries in ~/.labby (or the configured
LABBY_HOME), then run npx -y @dinglebear/labby proxy with your saved proxy
defaults. If no file is present there, Labby looks beside its native executable.
For the npm launcher, that means the downloaded binary's directory, not the
Node wrapper's directory. The Labby home file wins when both exist.
An explicit child command bypasses discovery. --mcp-json PATH selects a
specific file instead. An unreadable or invalid selected file fails startup;
Labby does not silently try another configuration.
Example .mcp.json:
{
"mcpServers": {
"sandbox": { "command": "npx", "args": ["-y", "microsandbox-mcp"] },
"files": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"] }
}
}npx -y @dinglebear/labby proxy --funnel --port 8443 --mcp-json /path/to/.mcp.jsonThe file can contain up to 16 named servers, mixing local stdio commands and
remote Streamable HTTP endpoints. Stdio entries support command, args, and
an env object. HTTP entries use url, optionally type: "http" or
type: "streamable-http", and headers. Native gateway transport,
socket_path, lifecycle and exposure settings also pass through shared
validation. Paths resolve relative to the file's directory unless --cwd
overrides it. Selected --inherit-env values reach
all servers; each JSON entry overrides those values, and explicit --env values
have the highest priority. Unselected ambient secrets remain scrubbed.
Labby starts an isolated local MCP aggregator and connects to every
configured local or remote server before public publication. Tools are
qualified by upstream name, such as sandbox::runtime_check, so matching tool names can coexist.
Native tool names containing :: are rejected before startup publication.
Labby-owned control tools are excluded from this aggregate surface. The file
and any environment secrets must be readable only by the operator. Streamable
HTTP is supported; legacy standalone SSE transport is not.
{
"mcpServers": {
"sandbox": { "command": "npx", "args": ["-y", "microsandbox-mcp"] },
"remote": { "type": "http", "url": "https://mcp.example.com/mcp", "bearer_token_env": "REMOTE_MCP_TOKEN" }
}
}bearer_token_env selects a credential from the supplied entry environment,
CLI environment overrides, or the running Labby installation. Common
headers: {"Authorization": "Bearer ..."} entries are also accepted and
translated into private gateway credential references. HTTP credentials are
kept in the isolated aggregate home and are not implicitly copied to unrelated
stdio children; explicit --env and --inherit-env selections still apply to
child processes. Missing configured credentials fail startup; there is no
anonymous fallback. Other custom headers use the gateway validation rules.
The OAuth authorization server has a stable issuer such as
https://lab.example.com. The ephemeral proxy is a separate protected
resource. Its identity is the exact public URL, including port and path:
issuer: https://lab.example.com
resource: https://node.example.ts.net:53147/mcp
Changing either 53147 or /mcp changes the JWT audience. Each random-port
run therefore creates a distinct resource URL. Use a fixed proxy.port for a
long-lived connector configuration; otherwise update the connector to the URL
printed by every run.
With Funnel, OAuth startup requires LABBY_AUTH_MODE=oauth, Google provider
credentials, and persistent signing and token state in $LABBY_HOME. The
proxy serves its own authorization server at the Funnel origin and registers
its exact /mcp URL as a protected resource. No daemon or lease is needed.
With Tailscale Serve, OAuth requires a stable LABBY_PUBLIC_URL (or equivalent
[auth]/[public_urls] configuration), the same-host signing keys, and a
reachable labby serve daemon. The CLI verifies the daemon's metadata and
JWKS, checks the three lease actions, then dispatches through authenticated
POST /v1/gateway:
gateway.oauth.resource_lease.creategateway.oauth.resource_lease.renewgateway.oauth.resource_lease.release
These are gateway actions, not dedicated /v1/internal/* routes. They require
lab:admin; lease IDs are opaque secrets and are redacted from diagnostics.
The default lease TTL is 120 seconds. The proxy renews every 40 seconds plus up to four seconds of jitter. Renewal failure terminates the proxy. Normal shutdown releases the lease. Forced termination cannot perform an async release, so the daemon ignores the expired lease and prunes expired entries on its 30-second sweep. Configured protected resources and ephemeral leases are separate registries, so route refresh does not erase active proxy resources.
The proxy serves RFC 9728 metadata at the public origin root:
GET https://node.example.ts.net:53147/.well-known/oauth-protected-resource
That document advertises the exact resource with port and /mcp. The
WWW-Authenticate challenge points to the same root metadata URL. This direct
proxy does not use the path-suffixed metadata URLs used by configured Gateway
protected routes. Local HTTP OAuth is intentionally rejected because daemon
leases accept HTTPS resources only; there is no bearer or no-auth downgrade.
Before publication Labby checks tailscale version, connected status, the
node DNS name, and tailscale serve status --json. It treats ports present in
either Serve TCP or web maps as occupied. A fixed-port collision fails. Random
mode retries collision-shaped claim failures, up to 32 candidates.
The owned Serve command is exact:
tailscale serve --yes --https=<external-port> http://127.0.0.1:<local-port>Funnel uses the same command shape with funnel instead of serve.
Readiness and periodic checks require both the exact loopback backend and
AllowFunnel=true for the exact public authority. If a mapping switches
between Serve and Funnel, Labby stops and refuses to remove that changed
mapping during cleanup.
Readiness requires the exact DNS-name, port, root handler, and loopback backend to appear in Serve status. While running, Labby watches both the foreground Serve child and that exact mapping. A disappeared or changed mapping is a runtime failure.
On Ctrl+C or a supervised component failure, cleanup stops HTTP, terminates the Serve child, waits for the mapping to disappear, reaps the stdio child, and releases the OAuth lease. If the owned mapping remains, Labby re-reads status and runs only:
tailscale serve --yes --https=<external-port> offIt does that only while the mapping still points to its recorded loopback
backend. If ownership changed, cleanup refuses to remove it. Labby never calls
tailscale serve reset and never rewrites unrelated mappings.
After an uncatchable process crash or power loss, inspect status before manual
recovery. Remove only the printed port and only after confirming its backend is
the dead proxy's 127.0.0.1:<local-port> target. Never use serve reset as a
proxy cleanup shortcut. The OAuth lease recovers independently through TTL
expiry; restart the proxy only after resolving any surviving exact-port
mapping.
- The HTTP listener binds only to
127.0.0.1; remote exposure belongs to Tailscale Serve. There is no LAN wildcard or public Funnel fallback. - RMCP Host validation admits loopback authorities and, when applicable, only the exact public resource host plus port. Origin validation similarly admits loopback origins and the exact public origin. Unexpected Host or Origin values are rejected to resist DNS rebinding and browser cross-origin abuse.
- The child is launched as argv without a shell. Its environment starts with
env_clear, then a small cross-platform runtime allowlist is restored, followed by explicitly inherited and explicit values. Ambient Labby/OAuth and upstream secrets are not inherited by default. - Child stderr is continuously drained so a full pipe cannot deadlock the MCP server. Diagnostic tails pass through central stdio redaction before logs.
- Bearer tokens use constant-time comparison and are separate from
LABBY_MCP_HTTP_TOKEN. OAuth disables static-admin-token fallback. - Unix process groups and Windows Job Objects own descendants so normal and supervised shutdown reap package-runner trees, not only the immediate PID.
proxy command resolution failed
: Install node/python3, add the executable to PATH, use a valid shebang,
or provide the launcher explicitly after --. .ts is never guessed.
tailnet auth requires Tailscale exposure
: --local does not silently weaken tailnet. Select --auth bearer or
--auth none explicitly for loopback use.
Tailscale publication port ... is already configured
: Choose another fixed port or return to port = "random". Inspect
tailscale serve status --json; do not reset unrelated routes.
Tailscale ... offline, missing DNS name, or Serve capability failure
: Run labby doctor proxy, then repair Tailscale connectivity and HTTPS Serve
support. Labby does not fall back to local exposure.
bearer auth requires ...
: Run labby config proxy set --yes --auth bearer, export the configured key, or
pipe the secret to --bearer-token-stdin. Confirm the effective
$LABBY_HOME/config.toml sets the intended bearer_token_env.
proxy OAuth requires a stable Labby public issuer
: For Serve exposure, configure OAuth and LABBY_PUBLIC_URL, start labby serve,
and verify its authorization-server metadata. For Funnel, use the callback
preview, configure Google OAuth in the same Labby home, and reuse the
previewed port. Funnel uses its own public origin as issuer.
live Labby daemon does not support proxy OAuth leases
: The CLI reached an older daemon. Upgrade/restart the daemon and confirm all
three lease actions appear in GET /v1/gateway/actions.
OAuth 401 for a token that works elsewhere
: Obtain a token whose audience is the exact printed URL, including external
port and /mcp, and whose scopes cover proxy.oauth_scopes. A token for the
same host on another port is intentionally rejected.
Host or Origin rejected : Connect to the exact printed URL. Reverse proxies and browser clients must preserve its authority and origin; do not replace the port or MCP path.
Proxy exits after startup : Inspect redacted logs for child closure, HTTP exit, Serve/Funnel ownership drift, or (in Serve OAuth mode) lease renewal failure. Cleanup errors are attached to the primary failure rather than replacing it.