fix: substitute server variable defaults when building base URL from OpenAPI spec (#3770)

* fix: resolve OpenAPI 3.x server variables in _create_default_client

When an OpenAPI spec defines server variables (e.g. `https://{region}.api.example.com/v1`),
the default values are now substituted before constructing the httpx client base URL.
Previously, the URL was used as-is, causing all requests to fail for specs that use
server variable templating.

Fixes #1681

* fix: use str.replace instead of format_map for server variable substitution

format_map applies Python string formatting rules, so variable names
like {api.version} would be treated as attribute access and raise errors.
Literal token replacement handles all valid OpenAPI variable names safely.
This commit is contained in:
Rishav Mitra 2026-04-06 16:48:30 -07:00 committed by GitHub
commit 99eaeb8af4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 55 additions and 0 deletions

View file

@ -175,6 +175,9 @@ class OpenAPIProvider(Provider):
"entry to the spec or provide an httpx.AsyncClient explicitly."
)
base_url = servers[0]["url"]
variables = servers[0].get("variables", {})
for name, var in variables.items():
base_url = base_url.replace(f"{{{name}}}", var.get("default", ""))
return httpx.AsyncClient(base_url=base_url, timeout=DEFAULT_TIMEOUT)
@asynccontextmanager

View file

@ -9,6 +9,58 @@ from fastmcp.server.providers.openapi import OpenAPIProvider
from fastmcp.server.providers.openapi.provider import DEFAULT_TIMEOUT
class TestOpenAPIProviderServerVariables:
"""Test that OpenAPIProvider resolves OpenAPI 3.x server variables."""
def test_server_variables_substituted_with_defaults(self):
spec = {
"openapi": "3.0.0",
"info": {"title": "Test API", "version": "1.0.0"},
"servers": [
{
"url": "https://{region}.api.example.com/v1",
"variables": {
"region": {
"default": "us",
"enum": ["us", "eu", "apac"],
}
},
}
],
"paths": {},
}
client = OpenAPIProvider._create_default_client(spec)
assert str(client.base_url) == "https://us.api.example.com/v1/"
def test_multiple_server_variables_substituted(self):
spec = {
"openapi": "3.0.0",
"info": {"title": "Test API", "version": "1.0.0"},
"servers": [
{
"url": "{scheme}://{host}/v1",
"variables": {
"scheme": {"default": "https"},
"host": {"default": "api.example.com"},
},
}
],
"paths": {},
}
client = OpenAPIProvider._create_default_client(spec)
assert str(client.base_url) == "https://api.example.com/v1/"
def test_static_server_url_unaffected(self):
spec = {
"openapi": "3.0.0",
"info": {"title": "Test API", "version": "1.0.0"},
"servers": [{"url": "https://api.example.com"}],
"paths": {},
}
client = OpenAPIProvider._create_default_client(spec)
assert str(client.base_url) == "https://api.example.com"
class TestOpenAPIProviderBasicFunctionality:
"""Test basic OpenAPIProvider functionality."""