From 99eaeb8af458d83b41246776b310e39e1d8bb38a Mon Sep 17 00:00:00 2001 From: Rishav Mitra Date: Mon, 6 Apr 2026 16:48:30 -0700 Subject: [PATCH] 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. --- .../server/providers/openapi/provider.py | 3 ++ tests/server/providers/openapi/test_server.py | 52 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/fastmcp/server/providers/openapi/provider.py b/src/fastmcp/server/providers/openapi/provider.py index bc826d1df..dd9a73559 100644 --- a/src/fastmcp/server/providers/openapi/provider.py +++ b/src/fastmcp/server/providers/openapi/provider.py @@ -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 diff --git a/tests/server/providers/openapi/test_server.py b/tests/server/providers/openapi/test_server.py index 0d7447eab..af3641ca0 100644 --- a/tests/server/providers/openapi/test_server.py +++ b/tests/server/providers/openapi/test_server.py @@ -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."""