This is an automated email from the ASF dual-hosted git repository.
FreeOnePlus pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git
The following commit(s) were added to refs/heads/master by this push:
new b8d3143 fix(semantic): isolate MetricFlow sidecar environment (#210)
b8d3143 is described below
commit b8d3143f2dae73a573e8756d810c3bd52182d737
Author: Yijia Su <[email protected]>
AuthorDate: Thu Aug 13 02:25:43 2026 +0800
fix(semantic): isolate MetricFlow sidecar environment (#210)
---
CHANGELOG.md | 3 +++
docs/integrations/metricflow.md | 5 +++-
doris_mcp_server/semantic/metricflow.py | 9 +++++++
test/semantic/test_metricflow_runtime.py | 40 +++++++++++++++++++++++++++++---
4 files changed, 53 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 17f16f1..c0f8787 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -65,6 +65,9 @@ under **Unreleased** until a new version is selected and
published.
### Fixed
+- Prevented MetricFlow sidecar processes from inheriting Doris credentials,
+ bearer tokens, OAuth/JWT secrets, and unrelated MCP Server environment
+ configuration by launching each provider with a fixed minimal environment.
- Accepted `+` revision metadata in exact Ossie and MetricFlow model
references across binding loading, runtime lookup, and public Child schemas.
- Enforced the canonical read-only SQL allowlist again at the production
diff --git a/docs/integrations/metricflow.md b/docs/integrations/metricflow.md
index 81afe47..d98de5d 100644
--- a/docs/integrations/metricflow.md
+++ b/docs/integrations/metricflow.md
@@ -72,7 +72,10 @@ compiled SQL and send it through the same bounded runtime as
The Server starts the configured executable without a shell, sends one JSON
object on stdin, reads one bounded JSON object from stdout, and then waits for
-the process to exit. The protocol version is `doris-mcp-metricflow/v1`.
+the process to exit. The Server supplies only a fixed locale and unbuffered-I/O
+environment, so Doris credentials, bearer tokens, OAuth/JWT secrets, and
+unrelated server configuration are not inherited. The protocol version is
+`doris-mcp-metricflow/v1`.
Request:
diff --git a/doris_mcp_server/semantic/metricflow.py
b/doris_mcp_server/semantic/metricflow.py
index 49a5684..9feb042 100644
--- a/doris_mcp_server/semantic/metricflow.py
+++ b/doris_mcp_server/semantic/metricflow.py
@@ -24,6 +24,7 @@ import re
import uuid
from collections.abc import Mapping, Sequence
from pathlib import Path
+from types import MappingProxyType
from typing import Any, Protocol, cast
from ..utils.query_runtime import DorisQueryRuntime, ReadOnlySQLGuard
@@ -43,6 +44,13 @@ _PROVIDER_OPERATIONS = frozenset(
"compile_query",
}
)
+_SIDECAR_ENVIRONMENT: Mapping[str, str] = MappingProxyType(
+ {
+ "LANG": "C.UTF-8",
+ "LC_ALL": "C.UTF-8",
+ "PYTHONUNBUFFERED": "1",
+ }
+)
def _bool_config(section: Any, name: str, default: bool) -> bool:
@@ -145,6 +153,7 @@ class MetricFlowSidecarProvider:
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=self._project_directory,
+ env=dict(_SIDECAR_ENVIRONMENT),
)
except (OSError, ValueError) as exc:
raise MetricFlowProviderFailure(
diff --git a/test/semantic/test_metricflow_runtime.py
b/test/semantic/test_metricflow_runtime.py
index ec58146..c6cfc5b 100644
--- a/test/semantic/test_metricflow_runtime.py
+++ b/test/semantic/test_metricflow_runtime.py
@@ -226,11 +226,24 @@ async def
test_metricflow_disabled_and_provider_failures_are_sanitized() -> None
@pytest.mark.asyncio
-async def test_metricflow_sidecar_protocol_round_trip(tmp_path: Path) -> None:
+async def test_metricflow_sidecar_protocol_round_trip(
+ tmp_path: Path,
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ secret_names = (
+ "DORIS_PASSWORD",
+ "MCP_STATE_HANDLE_SECRET",
+ "TOKEN_ADMIN",
+ "OAUTH_CLIENT_SECRET",
+ "JWT_SECRET",
+ )
+ for name in secret_names:
+ monkeypatch.setenv(name, f"secret-{name.lower()}")
script = tmp_path / "provider.py"
script.write_text(
"""
import json
+import os
import sys
request = json.loads(sys.stdin.read())
@@ -238,7 +251,10 @@ response = {
"protocol_version": request["protocol_version"],
"request_id": request["request_id"],
"ok": True,
- "data": {"items": [{"model_ref": "sales/main"}]},
+ "data": {
+ "items": [{"model_ref": "sales/main"}],
+ "environment": dict(sorted(os.environ.items())),
+ },
}
sys.stdout.write(json.dumps(response))
""".strip(),
@@ -251,7 +267,25 @@ sys.stdout.write(json.dumps(response))
result = await provider.request("list_models", {})
- assert result == {"items": [{"model_ref": "sales/main"}]}
+ assert result["items"] == [{"model_ref": "sales/main"}]
+ environment = result["environment"]
+ assert {
+ "LANG": environment["LANG"],
+ "LC_ALL": environment["LC_ALL"],
+ "PYTHONUNBUFFERED": environment["PYTHONUNBUFFERED"],
+ } == {
+ "LANG": "C.UTF-8",
+ "LC_ALL": "C.UTF-8",
+ "PYTHONUNBUFFERED": "1",
+ }
+ assert set(environment) <= {
+ "LANG",
+ "LC_ALL",
+ "PYTHONUNBUFFERED",
+ "__CF_USER_TEXT_ENCODING",
+ }
+ for name in secret_names:
+ assert name not in environment
@pytest.mark.asyncio
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]