Copilot commented on code in PR #11622:
URL: https://github.com/apache/gravitino/pull/11622#discussion_r3401197399


##########
mcp-server/mcp_server/server.py:
##########
@@ -73,14 +119,17 @@ def _create_gravitino_mcp(setting: Setting) -> FastMCP:
 def _parse_mcp_url(url: str) -> ():

Review Comment:
   The return type annotation `-> ()` is not meaningful and defeats 
type-checking; this function always returns a 3-tuple (host, port, path).



##########
mcp-server/mcp_server/core/context.py:
##########
@@ -19,11 +19,52 @@
 from mcp_server.core.setting import Setting
 
 
+def _get_request_authorization() -> str:
+    """Return the raw ``Authorization`` header of the current HTTP request.
+
+    The header is forwarded to Gravitino verbatim so the auth scheme chosen by
+    the agent (``Basic`` for simple auth, ``Bearer`` for OAuth2, ``Negotiate``
+    for Kerberos) is preserved. Returns an empty string in stdio mode or when
+    the header is absent.
+    """
+    try:
+        from fastmcp.server.dependencies import get_http_request
+
+        return get_http_request().headers.get("authorization", "")
+    except Exception:  # noqa: BLE001 – stdio mode or missing request context
+        return ""

Review Comment:
   `_get_request_authorization()` currently swallows all exceptions and turns 
them into "no auth". That can mask real runtime errors; only `LookupError` is 
expected when there is no HTTP request context (stdio mode).



##########
mcp-server/mcp_server/server.py:
##########
@@ -21,18 +21,64 @@
 from typing import AsyncIterator
 from urllib.parse import urlparse
 
+import mcp.types as mt
 from fastmcp import FastMCP
 from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
 from fastmcp.server.middleware.logging import (
     LoggingMiddleware,
 )
+from fastmcp.server.middleware.middleware import (
+    CallNext,
+    Middleware,
+    MiddlewareContext,
+)
 from fastmcp.server.middleware.timing import TimingMiddleware
+from fastmcp.tools.base import ToolResult
 
+from mcp_server.core import audit
 from mcp_server.core.context import GravitinoContext
 from mcp_server.core.setting import Setting
 from mcp_server.tools import load_tools
 
 
+def _get_principal_from_request() -> str:
+    """Derive a display principal from the current HTTP request's 
Authorization header.
+
+    Returns "anonymous" in stdio mode or when no token is present.
+    """
+    try:
+        from fastmcp.server.dependencies import get_http_request
+
+        authorization = get_http_request().headers.get("authorization", "")
+        return audit._extract_principal(authorization)
+    except Exception:  # noqa: BLE001 – stdio mode or no request context
+        return "anonymous"

Review Comment:
   Catching `Exception` here will silently hide unexpected bugs (e.g., 
attribute errors) and turn them into an "anonymous" principal. Since 
stdio/no-request context is expected to raise `LookupError`, it’s safer to only 
handle that case.



##########
mcp-server/mcp_server/core/context.py:
##########
@@ -19,11 +19,52 @@
 from mcp_server.core.setting import Setting
 
 
+def _get_request_authorization() -> str:
+    """Return the raw ``Authorization`` header of the current HTTP request.
+
+    The header is forwarded to Gravitino verbatim so the auth scheme chosen by
+    the agent (``Basic`` for simple auth, ``Bearer`` for OAuth2, ``Negotiate``
+    for Kerberos) is preserved. Returns an empty string in stdio mode or when
+    the header is absent.
+    """
+    try:
+        from fastmcp.server.dependencies import get_http_request
+
+        return get_http_request().headers.get("authorization", "")
+    except Exception:  # noqa: BLE001 – stdio mode or missing request context
+        return ""
+
+
 class GravitinoContext:
     def __init__(self, setting: Setting):
-        self.gravitino_client = RESTClientFactory.create_rest_client(
-            setting.metalake, setting.gravitino_uri
+        self._setting = setting
+        # Static startup identity: the --token CLI value is treated as a Bearer
+        # token (OAuth2). Used in stdio mode or as the fallback when an HTTP
+        # request carries no Authorization header.
+        default_authorization = (
+            f"Bearer {setting.token}" if setting.token else ""
+        )
+        self._default_client = RESTClientFactory.create_rest_client(
+            setting.metalake, setting.gravitino_uri, default_authorization
         )
 
     def rest_client(self):
-        return self.gravitino_client
+        """Return a REST client carrying the correct identity for this request.
+
+        In HTTP transport mode the incoming request's ``Authorization`` header 
is
+        forwarded verbatim to Gravitino, taking priority over the static 
startup
+        token. This keeps concurrent sessions with different principals fully
+        isolated — one principal's identity never leaks into another's calls.
+
+        Falls back to the shared default client (static startup token) when:
+        - running in stdio mode (no HTTP request context), or
+        - the incoming request carries no Authorization header.
+        """
+        authorization = _get_request_authorization()
+        if authorization:
+            return RESTClientFactory.create_rest_client(
+                self._setting.metalake,
+                self._setting.gravitino_uri,
+                authorization,
+            )
+        return self._default_client

Review Comment:
   In HTTP mode, `rest_client()` creates a brand-new REST client (and thus a 
new `httpx.AsyncClient` connection pool) on every tool call whenever an 
Authorization header is present. `PlainRESTClientOperation` does not expose any 
close/cleanup hook, so this can leak sockets and grow resource usage under load.



##########
mcp-server/mcp_server/core/setting.py:
##########
@@ -33,3 +33,19 @@ class Setting:
     tags: Set[str] = field(default_factory=set)
     transport: str = DefaultSetting.default_transport
     mcp_url: str = DefaultSetting.default_mcp_url
+    # Bearer token forwarded to Gravitino on every request.
+    # Empty string means anonymous (no Authorization header sent).
+    token: str = ""

Review Comment:
   The comment on `Setting.token` says it is forwarded to Gravitino "on every 
request", but with per-request identity forwarding it is only the 
default/fallback when a request does not carry its own Authorization header 
(and in stdio mode). Keeping this comment accurate avoids future misuse.



##########
mcp-server/mcp_server/main.py:
##########
@@ -83,17 +93,44 @@ def _parse_args():
     parser.add_argument(
         "--transport",
         type=str,
-        choices=["stdio", "http"],
+        choices=["stdio", "http", "streamable-http"],
         default=DefaultSetting.default_transport,
-        help=f"Transport protocol type: stdio (local), http (Streamable HTTP). 
"
+        help="Transport protocol type: stdio (local), http / streamable-http "
+        "(networked Streamable HTTP; the two names are equivalent). "
         f"(default: {DefaultSetting.default_transport})",
     )
 
     parser.add_argument(
         "--mcp-url",
         type=str,
         default=DefaultSetting.default_mcp_url,
-        help=f"The url of MCP server if using http transport. (default: 
{DefaultSetting.default_mcp_url})",
+        help="The url of MCP server if using http transport, http:// or 
https://. "
+        f"(default: {DefaultSetting.default_mcp_url})",
+    )
+
+    parser.add_argument(
+        "--token",
+        type=str,
+        default=os.environ.get("GRAVITINO_TOKEN", ""),
+        help="Bearer token forwarded as Authorization header to Gravitino on 
every request. "
+        "Can also be set via the GRAVITINO_TOKEN environment variable. "
+        "When omitted, requests are sent without authentication.",
+    )

Review Comment:
   The `--token` help text says the token is forwarded "on every request", but 
in HTTP transport it is only used as a fallback when the incoming request has 
no Authorization header (per-request identity takes priority). This is 
user-facing CLI documentation and should reflect the actual behavior.



##########
mcp-server/tests/unit/tools/test_tag.py:
##########
@@ -99,14 +99,13 @@ async def _test_disassociate_tag_from_metadata(mcp_server):
 
         asyncio.run(_test_disassociate_tag_from_metadata(self.mcp))
 
-    def test_destructive_tag_tools_disabled_by_default(self):
-        async def _test_destructive_tag_tools_disabled_by_default(mcp_server):
-            tool_names = {tool.name for tool in await mcp_server.list_tools()}
+    def test_write_tag_tools_enabled_and_protected_by_authz(self):
+        """Write tools are exposed; authorization is enforced by Gravitino, 
not by hiding."""
 

Review Comment:
   This test/docstring asserts that authorization is enforced, but the test 
only verifies tool exposure (presence in `list_tools`). Consider narrowing the 
docstring (or extending the test) so it matches what is actually validated.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to