FastMCP 3.4 is about reaching servers that live somewhere else. The headline is fastmcp-remote, a standalone bridge that connects stdio-only MCP hosts to servers hosted over HTTP. Around it, this release hardens the proxy layer those remote connections depend on — making bridges fail loudly instead of silently, and keeping authenticated sessions alive across the long idle periods that remote clients are prone to.
fastmcp-remote
Some MCP hosts still insist on launching a local stdio command, even when the server you want is already running over HTTP. FastMCP could already proxy a remote URL through fastmcp run, but that pulls in the full server-runner surface. fastmcp-remote is the small, single-purpose version: one URL in, one local stdio proxy out.
{
"mcpServers": {
"linear": {
"command": "uvx",
"args": ["fastmcp-remote", "https://mcp.linear.app/mcp"]
}
}
}OAuth is enabled automatically for HTTPS servers, with support for explicit bearer tokens and custom headers when you need them. The implementation stays on FastMCP primitives — Client, OAuth, create_proxy, and stdio — and credits the original npm mcp-remote project for the command shape.
Bridges That Fail Loudly
Proxies are lazy bridges: they don't touch the upstream server during construction, but they do forward real MCP requests once a client connects. As of 3.4, initialize is part of that forwarded surface — so a proxy only reports a successful handshake after the upstream server initializes too. A missing backend, a wrong URL (the server root instead of /mcp), denied upstream auth, or a non-MCP upstream now fails the downstream initialize instead of producing a "connected" proxy whose capability fetches quietly come back empty. The proxy also forwards ping upstream now.
This is an intentional behavior change from 3.3, and the reason bridge callers like fastmcp-remote surface real upstream failures instead of degrading into empty tool lists.
Auth That Survives Idle Time
Remote sessions sit idle, and short-lived upstream tokens punish that. fastmcp_access_token_expiry_seconds decouples the FastMCP-issued token's lifetime from the upstream expires_in — the FastMCP token is just a reference into proxy storage, re-validated and transparently refreshed on every request, so it can safely outlive a 5-minute upstream token without forcing a full OAuth flow after every idle period. When the upstream issues no refresh token, the lifetime is capped to match.
from fastmcp.server.auth.providers.github import GitHubProvider
auth = GitHubProvider(
client_id="...",
client_secret="...",
base_url="https://your-server.com",
fastmcp_access_token_expiry_seconds=60 * 60 * 24, # 24h client-facing token
)Alongside it, token_expiry_threshold_seconds treats tokens as expired N seconds early to close refresh races, and WorkOSProvider gains valid_scopes and extra_authorize_params.
Returnable Tool Errors
A tool could previously only signal an error by raising, which flattens to a text-only result and discards structured content. ToolResult now accepts is_error, mapping to CallToolResult.isError so a tool can hand back a rich error the model can see and act on. The proxy uses this to forward upstream tool errors intact instead of collapsing them.
@mcp.tool
def lookup(id: str) -> ToolResult:
if not found(id):
return ToolResult(
content="not found",
structured_content={"code": 404, "id": id},
is_error=True,
)
...Code Mode: Safe by Default
MontySandboxProvider() now applies a conservative baseline when constructed without limits — 30s duration, 100 MB memory — and CodeMode caps tool calls at 50 per execute block. Both remain explicitly opt-out (limits=None, max_tool_calls=None), so the safe configuration is the default instead of something you had to remember to turn on.
Under the Hood
The auth stack migrated its JWT handling to joserfc. The fastmcp dev CLI gains --host and --log-panel/--no-log-panel. Resources created from templates now preserve annotations, meta, title, and icons; resource templates with query params work on proxied servers; OTEL spans cover the sampling step and tool execution; MCP config files are read as UTF-8; and the OAuth server metadata endpoint now answers at the /.well-known/openid-configuration alias.
8 new contributors this release.
What's Changed
New Features 🎉
Breaking Changes ⚠️
Enhancements ✨
- ci: require external PRs to link a tracked issue by @strawgate in #4173
- feat: new options --host and --no-log-panel | --log-panel to cli dev apps by @itaru2622 in #4123
- Add valid_scopes and extra_authorize_params to WorkOSProvider by @tiagoskaneta in #4135
- Add token_expiry_threshold_seconds for proactive token refresh by @mohankumarelec in #4142
- Add review-issue skill for triaging gated external contributions by @jlowin in #4212
- Add contract gate to review-issue skill by @jlowin in #4214
- Let ToolResult return an error result via is_error by @jlowin in #4217
- Update published docs after PyPI release by @jlowin in #4211
- Allow pre-bound HTTP sockets by @jlowin in #4222
- Add targeted coverage tests by @strawgate in #4230
- Upgrade ty to 0.0.39 by @jlowin in #4225
- Decouple FastMCP access token lifetime from upstream expires_in by @jlowin in #4254
Security 🔒
- feat(code-mode): default sandbox limits and per-execution tool-call cap by @strawgate in #4170
- Security: Fix 3 findings in GitHub Actions workflows by @jpr5 in #4183
- Add outbound comment guardrails by @jlowin in #4196
- Add uv dependency cooldown by @jlowin in #4213
Fixes 🐞
- fix: VersionSpec eq matching normalizes versions and selects deterministically by @strawgate in #4058
- fix(tests): hoist azure-identity import out of the OBO test timeout window by @strawgate in #4176
- fix(auth): disambiguate auth-denied vs missing component messages by @strawgate in #4165
- fix: preserve annotations, meta, title, icons when creating resources from templates by @strawgate in #4061
- fix: add OTEL spans to sampling step and tool execution by @strawgate in #4059
- fix(config): read MCP config files as UTF-8 by @pragnyanramtha in #4164
- fix(schema): preserve root metadata on fallback by @yuyua9 in #4178
- fix(proxy): restore _current_server in _restore_request_context by @strawgate in #4168
- fix(auth): add /.well-known/openid-configuration alias for OAuth server metadata by @shigechika in #4167
- fix(code-mode): cancel Monty sandbox future on task cancellation by @strawgate in #4169
- fix(auth): unprefix Azure scopes echoed back to MCP clients by @rgillinlz in #4130
- fix(cli): forward stateless flag in uv run path by @yuyua9 in #4177
- fix(ci): scope minimize-reviews concurrency by event name by @strawgate in #4174
- Fix docs app demo iframe assets by @jlowin in #4194
- Guard require-issue-link check job to pull_request_target events by @jlowin in #4209
- Migrate auth JWTs to joserfc by @jlowin in #4221
- Skip published docs update for prereleases by @jlowin in #4224
- Surface proxy upstream failures by @jlowin in #4227
- Close upstream OAuth clients by @jlowin in #4248
- Fix GitHub MCP resource integration test by @jlowin in #4253
- Fix resource templates with query params on proxied servers by @rene84 in #4251
- Fix MDX syntax error in changelog by @jlowin in #4270
Docs 📚
- Document pip upgrade recovery for the fastmcp-slim package split by @jlowin in #4215
- Move pip upgrade recovery into a Troubleshooting section by @jlowin in #4219
- Restore Horizon docs banner by @jlowin in #4240
- fix: Trendshift link and badge in README.md by @bhantos in #4236
- docs: add tool fingerprinting recipe by @dgenio in #4233
- Backfill changelog and updates through v3.4.0 by @jlowin in #4269
Dependencies 📦
- chore(deps): bump the uv group across 2 directories with 1 update by @dependabot[bot] in #4113
- chore(deps-dev): bump pydantic-monty from 0.0.16 to 0.0.17 by @dependabot[bot] in #4023
Other Changes 🦾
New Contributors
- @pragnyanramtha made their first contribution in #4164
- @yuyua9 made their first contribution in #4178
- @tiagoskaneta made their first contribution in #4135
- @mohankumarelec made their first contribution in #4142
- @rgillinlz made their first contribution in #4130
- @jpr5 made their first contribution in #4183
- @bhantos made their first contribution in #4236
- @rene84 made their first contribution in #4251
Full Changelog: v3.3.1...v3.4.0