adam-christian-software commented on code in PR #39:
URL: https://github.com/apache/polaris-tools/pull/39#discussion_r2514965451


##########
mcp-server/README.md:
##########
@@ -0,0 +1,112 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+# Apache Polaris MCP Server (Python)
+
+This package provides a Python implementation of the [Model Context Protocol 
(MCP)](https://modelcontextprotocol.io) server for Apache Polaris. It wraps the 
Polaris REST APIs so MCP-compatible clients (IDEs, agents, chat applications) 
can issue structured requests via JSON-RPC on stdin/stdout.
+
+The implementation is built on top of [FastMCP](https://gofastmcp.com) for 
streamlined server registration and transport handling.

Review Comment:
   Celebration (you can resolve): I like the idea of using FastMCP since it's 
been incorporated into the official Python MCP SDK 
(https://github.com/modelcontextprotocol/python-sdk).



##########
mcp-server/README.md:
##########
@@ -0,0 +1,112 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+# Apache Polaris MCP Server (Python)
+
+This package provides a Python implementation of the [Model Context Protocol 
(MCP)](https://modelcontextprotocol.io) server for Apache Polaris. It wraps the 
Polaris REST APIs so MCP-compatible clients (IDEs, agents, chat applications) 
can issue structured requests via JSON-RPC on stdin/stdout.
+
+The implementation is built on top of [FastMCP](https://gofastmcp.com) for 
streamlined server registration and transport handling.
+

Review Comment:
   Nit: You might want to list out the system dependencies like uv, the python 
versions.



##########
mcp-server/pyproject.toml:
##########
@@ -0,0 +1,48 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+[project]
+name = "polaris-mcp"
+version = "1.2.0"
+description = "Apache Polaris Model Context Protocol server"
+authors = [
+    {name = "Apache Software Foundation", email = "[email protected]"}

Review Comment:
   For the authors, I would put the Apache Polaris Community rather than the 
Apache Software Foundation.



##########
mcp-server/README.md:
##########
@@ -0,0 +1,112 @@
+<!--

Review Comment:
   I'm wondering how we are tracking the next steps. Like, I could see the 
following might be necessary:
   
   1. The release items like LICENSE, NOTICE, etc
   2. CI/CD workflow for Python tests
   
   But, these don't have to be done in this PR. Maybe, we could put them as git 
Issues in Polaris-Tools?



##########
mcp-server/pyproject.toml:
##########
@@ -0,0 +1,48 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+[project]
+name = "polaris-mcp"
+version = "1.2.0"
+description = "Apache Polaris Model Context Protocol server"
+authors = [
+    {name = "Apache Software Foundation", email = "[email protected]"}
+]
+readme = "README.md"
+requires-python = ">=3.10,<4.0"
+license = "Apache-2.0"
+keywords = ["Apache Polaris", "Polaris", "Model Context Protocol"]
+dependencies = [
+    "fastmcp>=2.13.0.2",
+    "urllib3>=1.25.3,<3.0.0",
+]
+
+[project.scripts]
+polaris-mcp = "polaris_mcp.server:main"
+
+[project.urls]
+homepage = "https://polaris.apache.org/";
+repository = "https://github.com/apache/polaris/";

Review Comment:
   We should update this to be the Polaris-Tools repo.



##########
mcp-server/polaris_mcp/tools/table.py:
##########
@@ -0,0 +1,245 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""Iceberg table MCP tool."""
+
+from __future__ import annotations
+
+import copy
+from typing import Any, Dict, Optional, Set
+
+import urllib3
+
+from ..authorization import AuthorizationProvider
+from ..base import JSONDict, McpTool, ToolExecutionResult, copy_if_object, 
require_text
+from ..rest import PolarisRestTool, encode_path_segment
+
+
+class PolarisTableTool(McpTool):
+    """Expose Polaris table REST endpoints through MCP."""
+
+    TOOL_NAME = "polaris-iceberg-table"
+    TOOL_DESCRIPTION = (
+        "Perform table-centric operations (list, get, create, commit, delete) 
using the Polaris REST API."
+    )
+    NAMESPACE_DELIMITER = "\x1f"
+
+    LIST_ALIASES: Set[str] = {"list", "ls"}
+    GET_ALIASES: Set[str] = {"get", "load", "fetch"}
+    CREATE_ALIASES: Set[str] = {"create"}
+    COMMIT_ALIASES: Set[str] = {"commit", "update"}
+    DELETE_ALIASES: Set[str] = {"delete", "drop"}
+
+    def __init__(
+        self,
+        base_url: str,
+        http: urllib3.PoolManager,
+        authorization_provider: AuthorizationProvider,
+    ) -> None:
+        self._delegate = PolarisRestTool(
+            name="polaris.table.delegate",
+            description="Internal delegate for table operations",
+            base_url=base_url,
+            default_path_prefix="api/catalog/v1/",
+            http=http,
+            authorization_provider=authorization_provider,
+        )
+
+    @property
+    def name(self) -> str:
+        return self.TOOL_NAME
+
+    @property
+    def description(self) -> str:
+        return self.TOOL_DESCRIPTION
+
+    def input_schema(self) -> JSONDict:
+        return {
+            "type": "object",
+            "properties": {
+                "operation": {
+                    "type": "string",
+                    "enum": ["list", "get", "create", "commit", "delete"],
+                    "description": (
+                        "Table operation to execute. Supported values: list, 
get (synonyms: load, fetch), "
+                        "create, commit (synonym: update), delete (synonym: 
drop)."
+                    ),
+                },
+                "catalog": {
+                    "type": "string",
+                    "description": "Polaris catalog identifier (maps to the 
{prefix} path segment).",
+                },
+                "namespace": {
+                    "anyOf": [
+                        {"type": "string"},
+                        {"type": "array", "items": {"type": "string"}},
+                    ],
+                    "description": (
+                        "Namespace that contains the target tables. Provide as 
a string that uses the ASCII Unit "
+                        'Separator (0x1F) between hierarchy levels (e.g. 
"analytics\\u001Fdaily") or as an array of '
+                        "strings."
+                    ),
+                },
+                "table": {
+                    "type": "string",
+                    "description": (
+                        "Table identifier for operations that target a 
specific table (get, commit, delete)."
+                    ),
+                },
+                "query": {
+                    "type": "object",
+                    "description": "Optional query string parameters (for 
example page-size, page-token, include-drop).",
+                    "additionalProperties": {"type": "string"},
+                },
+                "headers": {
+                    "type": "object",
+                    "description": "Optional additional HTTP headers to 
include with the request.",
+                    "additionalProperties": {"type": "string"},
+                },
+                "body": {
+                    "type": "object",
+                    "description": "Optional request body payload for create 
or commit operations.",
+                },
+            },
+            "required": ["operation", "catalog", "namespace"],
+        }
+
+    def call(self, arguments: Any) -> ToolExecutionResult:
+        if not isinstance(arguments, dict):
+            raise ValueError("Tool arguments must be a JSON object.")
+
+        operation = require_text(arguments, "operation").lower().strip()
+        normalized = self._normalize_operation(operation)
+
+        catalog = encode_path_segment(require_text(arguments, "catalog"))
+        namespace = 
encode_path_segment(self._resolve_namespace(arguments.get("namespace")))
+
+        delegate_args: JSONDict = {}
+        copy_if_object(arguments.get("query"), delegate_args, "query")
+        copy_if_object(arguments.get("headers"), delegate_args, "headers")
+
+        if normalized == "list":
+            self._handle_list(delegate_args, catalog, namespace)
+        elif normalized == "get":
+            self._handle_get(arguments, delegate_args, catalog, namespace)
+        elif normalized == "create":
+            self._handle_create(arguments, delegate_args, catalog, namespace)
+        elif normalized == "commit":
+            self._handle_commit(arguments, delegate_args, catalog, namespace)
+        elif normalized == "delete":
+            self._handle_delete(arguments, delegate_args, catalog, namespace)
+        else:  # pragma: no cover - defensive, normalize guarantees handled 
cases
+            raise ValueError(f"Unsupported operation: {operation}")
+
+        return self._delegate.call(delegate_args)
+
+    def _handle_list(self, delegate_args: JSONDict, catalog: str, namespace: 
str) -> None:
+        delegate_args["method"] = "GET"
+        delegate_args["path"] = f"{catalog}/namespaces/{namespace}/tables"
+
+    def _handle_get(
+        self,
+        arguments: Dict[str, Any],
+        delegate_args: JSONDict,
+        catalog: str,
+        namespace: str,
+    ) -> None:
+        table = encode_path_segment(
+            require_text(arguments, "table", "Table name is required for get 
operations.")
+        )
+        delegate_args["method"] = "GET"
+        delegate_args["path"] = 
f"{catalog}/namespaces/{namespace}/tables/{table}"
+
+    def _handle_create(
+        self,
+        arguments: Dict[str, Any],
+        delegate_args: JSONDict,
+        catalog: str,
+        namespace: str,
+    ) -> None:
+        body = arguments.get("body")
+        if not isinstance(body, dict):
+            raise ValueError(
+                "Create operations require a request body that matches the 
CreateTableRequest schema. See CreateTableRequest in "
+                
"https://raw.githubusercontent.com/apache/polaris/apache-polaris-1.2.0-incubating/spec/generated/bundled-polaris-catalog-service.yaml";

Review Comment:
   We can file a GitHub issue here, but we are linking a specific version of 
the Apache Polaris interface. We could:
   
   1. Modify this to a redirect to the most recent version
   2. Have the user pass in an Polaris version
   3. Have some way to communicate which version Polaris is and have the MCP 
Server pull in the right version of the entities



##########
mcp-server/README.md:
##########
@@ -0,0 +1,112 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+# Apache Polaris MCP Server (Python)
+
+This package provides a Python implementation of the [Model Context Protocol 
(MCP)](https://modelcontextprotocol.io) server for Apache Polaris. It wraps the 
Polaris REST APIs so MCP-compatible clients (IDEs, agents, chat applications) 
can issue structured requests via JSON-RPC on stdin/stdout.
+
+The implementation is built on top of [FastMCP](https://gofastmcp.com) for 
streamlined server registration and transport handling.
+

Review Comment:
   Nit: It might be helpful to have a table of contents here.



##########
mcp-server/README.md:
##########
@@ -0,0 +1,112 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+# Apache Polaris MCP Server (Python)

Review Comment:
   Nit: I'd leave out (Python) here. This is going to be the Polaris MCP 
Server, so I don't think it matters what language it is in.



##########
mcp-server/README.md:
##########
@@ -0,0 +1,112 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+# Apache Polaris MCP Server (Python)
+
+This package provides a Python implementation of the [Model Context Protocol 
(MCP)](https://modelcontextprotocol.io) server for Apache Polaris. It wraps the 
Polaris REST APIs so MCP-compatible clients (IDEs, agents, chat applications) 
can issue structured requests via JSON-RPC on stdin/stdout.
+
+The implementation is built on top of [FastMCP](https://gofastmcp.com) for 
streamlined server registration and transport handling.
+
+## Installation
+
+From the repository root:
+
+```bash
+cd client/python-mcp
+uv sync
+```
+
+## Running
+
+Launch the MCP server (which reads from stdin and writes to stdout):
+
+```bash
+uv run polaris-mcp
+```
+
+## Testing
+
+Install dependencies (one-time) and run the Python unit tests with `uv` so the 
locked environment is used:

Review Comment:
   Nit: I don't think that you run need to mention this line since it should be 
self-explanatory.



##########
mcp-server/polaris_mcp/server.py:
##########
@@ -0,0 +1,439 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""Entry point for the Polaris Model Context Protocol server."""
+
+from __future__ import annotations
+
+import os
+from typing import Any, Mapping, MutableMapping, Sequence
+from urllib.parse import urljoin
+
+import urllib3
+from fastmcp import FastMCP
+from fastmcp.tools.tool import ToolResult as FastMcpToolResult
+from importlib import metadata
+from mcp.types import TextContent
+
+from .authorization import (
+    AuthorizationProvider,
+    ClientCredentialsAuthorizationProvider,
+    StaticAuthorizationProvider,
+    none,
+)
+from .base import ToolExecutionResult
+from .tools import (
+    PolarisCatalogRoleTool,
+    PolarisCatalogTool,
+    PolarisNamespaceTool,
+    PolarisPolicyTool,
+    PolarisPrincipalRoleTool,
+    PolarisPrincipalTool,
+    PolarisTableTool,
+)
+
+DEFAULT_BASE_URL = "http://localhost:8181/";
+OUTPUT_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "isError": {"type": "boolean"},
+        "meta": {"type": "object"},
+    },
+    "required": ["isError"],
+    "additionalProperties": True,
+}
+
+
+def create_server() -> FastMCP:

Review Comment:
   Note (feel free to resolve): Depending on how many tools we are expecting to 
add the MCP Server, we might want to provide some sort of IOC to be able to 
register our tools rather than having to iterate here. Obviously, this is not 
for this stage.



##########
mcp-server/pyproject.toml:
##########
@@ -0,0 +1,48 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+[project]
+name = "polaris-mcp"
+version = "1.2.0"

Review Comment:
   Is this the version of the mcp server? I would say we should put it as 0.9.0 
per our offline discussions.



##########
mcp-server/README.md:
##########
@@ -0,0 +1,112 @@
+<!--

Review Comment:
   You should add some python-specific items to the .gitignore. Here are some 
after playing around with this:
   
   1. mcp-server/polaris_mcp.egg-info/
   2. mcp-server/polaris_mcp/__pycache__/
   3. mcp-server/polaris_mcp/tools/__pycache__/
   4. mcp-server/tests/__pycache__/



##########
mcp-server/README.md:
##########
@@ -0,0 +1,112 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+# Apache Polaris MCP Server (Python)
+
+This package provides a Python implementation of the [Model Context Protocol 
(MCP)](https://modelcontextprotocol.io) server for Apache Polaris. It wraps the 
Polaris REST APIs so MCP-compatible clients (IDEs, agents, chat applications) 
can issue structured requests via JSON-RPC on stdin/stdout.
+
+The implementation is built on top of [FastMCP](https://gofastmcp.com) for 
streamlined server registration and transport handling.
+
+## Installation
+
+From the repository root:
+
+```bash
+cd client/python-mcp

Review Comment:
   We should update this to cd mcp-server/



##########
mcp-server/README.md:
##########
@@ -0,0 +1,112 @@
+<!--

Review Comment:
   Right now, I'm not seeing logging. Can you file a GitHub issue to add 
structured logging?



##########
mcp-server/tests/test_namespace_tool.py:
##########
@@ -0,0 +1,84 @@
+"""Unit tests for ``polaris_mcp.tools.namespace``."""

Review Comment:
   Right now, we only have tests to cover server helpers and two tools. Can you 
file a GitHub issue to test:
   1. All of the other tools
   2. Authorization flows
   3. Integration tests
   



##########
mcp-server/polaris_mcp/authorization.py:
##########
@@ -0,0 +1,136 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""Authorization helpers for the Polaris MCP server."""
+
+from __future__ import annotations
+
+import json
+import threading
+import time
+from abc import ABC, abstractmethod
+from typing import Optional
+from urllib.parse import urlencode
+
+import urllib3
+
+
+class AuthorizationProvider(ABC):
+    """Return Authorization header values for outgoing requests."""
+
+    @abstractmethod
+    def authorization_header(self) -> Optional[str]:
+        ...
+
+
+class StaticAuthorizationProvider(AuthorizationProvider):
+    """Wrap a static bearer token."""
+
+    def __init__(self, token: Optional[str]) -> None:
+        value = (token or "").strip()
+        self._header = f"Bearer {value}" if value else None
+
+    def authorization_header(self) -> Optional[str]:
+        return self._header
+
+
+class ClientCredentialsAuthorizationProvider(AuthorizationProvider):
+    """Implements the OAuth client-credentials flow with caching."""
+
+    def __init__(
+        self,
+        token_endpoint: str,
+        client_id: str,
+        client_secret: str,
+        scope: Optional[str],
+        http: urllib3.PoolManager,
+    ) -> None:
+        self._token_endpoint = token_endpoint
+        self._client_id = client_id
+        self._client_secret = client_secret
+        self._scope = scope
+        self._http = http
+        self._lock = threading.Lock()
+        self._cached: Optional[tuple[str, float]] = None  # (token, 
expires_at_epoch)
+
+    def authorization_header(self) -> Optional[str]:
+        token = self._current_token()
+        return f"Bearer {token}" if token else None
+
+    def _current_token(self) -> Optional[str]:
+        now = time.time()
+        cached = self._cached
+        if not cached or cached[1] - 60 <= now:

Review Comment:
   Feel free to file a GitHub issue or fix it in this PR, we should make the 
60-second buffer configurable.



##########
mcp-server/polaris_mcp/rest.py:
##########
@@ -0,0 +1,310 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""HTTP helper used by the Polaris MCP tools."""
+
+from __future__ import annotations
+
+import json
+from dataclasses import dataclass
+from typing import Any, Dict, List, Optional, Tuple
+from urllib.parse import urlencode, urljoin, urlsplit, urlunsplit, quote
+
+import urllib3
+
+from .authorization import AuthorizationProvider, none
+from .base import JSONDict, ToolExecutionResult
+
+
+DEFAULT_TIMEOUT = urllib3.Timeout(connect=30.0, read=30.0)

Review Comment:
   Enhancement: We can file a GitHub issue for this, but it probably should be 
configurable.



##########
mcp-server/polaris_mcp/rest.py:
##########
@@ -0,0 +1,310 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""HTTP helper used by the Polaris MCP tools."""
+
+from __future__ import annotations
+
+import json
+from dataclasses import dataclass
+from typing import Any, Dict, List, Optional, Tuple
+from urllib.parse import urlencode, urljoin, urlsplit, urlunsplit, quote
+
+import urllib3
+
+from .authorization import AuthorizationProvider, none
+from .base import JSONDict, ToolExecutionResult
+
+
+DEFAULT_TIMEOUT = urllib3.Timeout(connect=30.0, read=30.0)
+
+
+def encode_path_segment(value: str) -> str:
+    """URL-encode a string for safe use as an HTTP path component."""
+
+    # urllib encodes spaces as "+" by default; convert to %20 to keep literal 
path semantics.
+    return quote(value, safe="").replace("+", "%20")
+
+
+def _ensure_trailing_slash(url: str) -> str:
+    return url if url.endswith("/") else f"{url}/"
+
+
+def _normalize_prefix(prefix: Optional[str]) -> str:
+    if not prefix:
+        return ""
+    trimmed = prefix.strip()
+    if trimmed.startswith("/"):
+        trimmed = trimmed[1:]
+    if trimmed and not trimmed.endswith("/"):
+        trimmed = f"{trimmed}/"
+    return trimmed
+
+
+def _merge_headers(values: Optional[Dict[str, Any]]) -> Dict[str, str]:
+    headers: Dict[str, str] = {"Accept": "application/json"}
+    if not values:
+        return headers
+
+    for name, raw_value in values.items():
+        if not name or raw_value is None:
+            continue
+        if isinstance(raw_value, list):
+            flattened = [str(item) for item in raw_value if item is not None]
+            if not flattened:
+                continue
+            headers[name] = ", ".join(flattened)
+        else:
+            headers[name] = str(raw_value)
+    return headers
+
+
+def _serialize_body(node: Any) -> Optional[str]:
+    if node is None:
+        return None
+    if isinstance(node, (str, bytes)):
+        return node.decode("utf-8") if isinstance(node, bytes) else node
+    return json.dumps(node)
+
+
+def _pretty_body(raw: str) -> str:
+    if not raw.strip():
+        return ""
+    try:
+        parsed = json.loads(raw)
+    except json.JSONDecodeError:
+        return raw
+    return json.dumps(parsed, indent=2)
+
+
+def _headers_to_dict(headers: urllib3.response.HTTPHeaderDict) -> Dict[str, 
str]:
+    flattened: Dict[str, str] = {}
+    for key in headers:
+        values = headers.getlist(key)
+        flattened[key] = ", ".join(values)
+    return flattened
+
+
+def _append_query(url: str, params: List[Tuple[str, str]]) -> str:
+    if not params:
+        return url
+    parsed = urlsplit(url)
+    extra_parts = [urlencode({k: v}) for k, v in params if v is not None]
+    existing = parsed.query
+    if existing:
+        query = "&".join([existing] + extra_parts) if extra_parts else existing
+    else:
+        query = "&".join(extra_parts)
+    return urlunsplit((parsed.scheme, parsed.netloc, parsed.path, query, 
parsed.fragment))
+
+
+def _build_query(parameters: Optional[Dict[str, Any]]) -> List[Tuple[str, 
str]]:
+    if not parameters:
+        return []
+    entries: List[Tuple[str, str]] = []
+    for key, value in parameters.items():
+        if not key or value is None:
+            continue
+        if isinstance(value, list):
+            for item in value:
+                if item is not None:
+                    entries.append((key, str(item)))
+        else:
+            entries.append((key, str(value)))
+    return entries
+
+
+def _maybe_parse_json(text: Optional[str]) -> Tuple[Optional[Any], 
Optional[str]]:
+    if text is None:
+        return None, None
+    try:
+        return json.loads(text), None
+    except json.JSONDecodeError:
+        return None, text
+
+
+class PolarisRestTool:
+    """Issues HTTP requests against the Polaris REST API and packages the 
response."""
+
+    def __init__(
+        self,
+        name: str,
+        description: str,
+        base_url: str,
+        default_path_prefix: str,
+        http: urllib3.PoolManager,
+        authorization_provider: Optional[AuthorizationProvider] = None,
+    ) -> None:
+        self._name = name
+        self._description = description
+        self._base_url = _ensure_trailing_slash(base_url)
+        self._path_prefix = _normalize_prefix(default_path_prefix)
+        self._http = http
+        self._authorization = authorization_provider or none()
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    def input_schema(self) -> JSONDict:
+        """Return the generic JSON schema shared by delegated tools."""
+        return {
+            "type": "object",
+            "properties": {
+                "method": {
+                    "type": "string",
+                    "description": (
+                        "HTTP method, e.g. GET, POST, PUT, DELETE, PATCH, HEAD 
or OPTIONS. "
+                        "Defaults to GET."
+                    ),
+                },
+                "path": {
+                    "type": "string",
+                    "description": (
+                        "Relative path under the Polaris base URL, such as "
+                        "/api/management/v1/catalogs. Absolute URLs are also 
accepted."
+                    ),
+                },
+                "query": {
+                    "type": "object",
+                    "description": (
+                        "Optional query string parameters. Values can be 
strings or arrays of strings."
+                    ),
+                    "additionalProperties": {
+                        "anyOf": [{"type": "string"}, {"type": "array", 
"items": {"type": "string"}}]
+                    },
+                },
+                "headers": {
+                    "type": "object",
+                    "description": (
+                        "Optional request headers. Accept and Authorization 
headers are supplied "
+                        "automatically when omitted."
+                    ),
+                    "additionalProperties": {
+                        "anyOf": [{"type": "string"}, {"type": "array", 
"items": {"type": "string"}}]
+                    },
+                },
+                "body": {
+                    "type": ["object", "array", "string", "number", "boolean", 
"null"],
+                    "description": (
+                        "Optional request body. Objects and arrays are 
serialized as JSON, strings "
+                        "are sent as-is."
+                    ),
+                },
+            },
+            "required": ["path"],
+        }
+
+    def call(self, arguments: Any) -> ToolExecutionResult:
+        if not isinstance(arguments, dict):
+            raise ValueError("Tool arguments must be a JSON object.")
+
+        method = str(arguments.get("method", "GET") or "GET").strip().upper() 
or "GET"
+        path = self._require_path(arguments)
+        query_params = arguments.get("query")
+        headers_param = arguments.get("headers")
+        body_node = arguments.get("body")
+
+        query = query_params if isinstance(query_params, dict) else None
+        headers = headers_param if isinstance(headers_param, dict) else None
+
+        target_uri = self._resolve_target_uri(path, query)
+
+        header_values = _merge_headers(headers)
+        if not any(name.lower() == "authorization" for name in header_values):
+            token = self._authorization.authorization_header()
+            if token:
+                header_values["Authorization"] = token
+
+        body_text = _serialize_body(body_node)
+        if body_text is not None and not any(name.lower() == "content-type" 
for name in header_values):
+            header_values["Content-Type"] = "application/json"
+
+        response = self._http.request(
+            method,
+            target_uri,
+            body=body_text.encode("utf-8") if body_text is not None else None,
+            headers=header_values,
+            timeout=DEFAULT_TIMEOUT,
+        )
+
+        response_body = response.data.decode("utf-8") if response.data else ""
+        rendered_body = _pretty_body(response_body)
+
+        lines = [f"{method} {target_uri}", f"Status: {response.status}"]
+        for key, value in _headers_to_dict(response.headers).items():
+            lines.append(f"{key}: {value}")
+        if rendered_body:
+            lines.append("")
+            lines.append(rendered_body)
+        message = "\n".join(lines)
+
+        metadata: JSONDict = {
+            "method": method,
+            "url": target_uri,
+            "status": response.status,
+            "request": {
+                "method": method,
+                "url": target_uri,
+                "headers": dict(header_values),

Review Comment:
   So, I think there might be a security vulnerability here. The authorization 
header could include the bearer token. I think this is then sent to the MCP 
Client. I don't believe that we want to do that because that could get stored 
in conversation history.
   
   ```
   def _sanitize_headers(headers: Dict[str, str]) -> Dict[str, str]:
       sanitized = {}
       sensitive_headers = {"authorization", "x-api-key", "cookie", 
"set-cookie"}
       
       for key, value in headers.items():
           if key.lower() in sensitive_headers:
               sanitized[key] = "[REDACTED]"
           else:
               sanitized[key] = value
       
       return sanitized
   ```
   
   
   In this function:
   
   ```
   metadata: JSONDict = {
       ...
       "request": {
           "method": method,
           "url": target_uri,
           "headers": _sanitize_headers(header_values),
       },
       ...
   }
   ```



##########
mcp-server/polaris_mcp/server.py:
##########
@@ -0,0 +1,439 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""Entry point for the Polaris Model Context Protocol server."""
+
+from __future__ import annotations
+
+import os
+from typing import Any, Mapping, MutableMapping, Sequence
+from urllib.parse import urljoin
+
+import urllib3
+from fastmcp import FastMCP
+from fastmcp.tools.tool import ToolResult as FastMcpToolResult
+from importlib import metadata
+from mcp.types import TextContent
+
+from .authorization import (
+    AuthorizationProvider,
+    ClientCredentialsAuthorizationProvider,
+    StaticAuthorizationProvider,
+    none,
+)
+from .base import ToolExecutionResult
+from .tools import (
+    PolarisCatalogRoleTool,
+    PolarisCatalogTool,
+    PolarisNamespaceTool,
+    PolarisPolicyTool,
+    PolarisPrincipalRoleTool,
+    PolarisPrincipalTool,
+    PolarisTableTool,
+)
+
+DEFAULT_BASE_URL = "http://localhost:8181/";
+OUTPUT_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "isError": {"type": "boolean"},
+        "meta": {"type": "object"},
+    },
+    "required": ["isError"],
+    "additionalProperties": True,
+}
+
+
+def create_server() -> FastMCP:
+    """Construct a FastMCP server with Polaris tools."""
+
+    base_url = _resolve_base_url()
+    http = urllib3.PoolManager()
+    authorization_provider = _resolve_authorization_provider(base_url, http)
+
+    table_tool = PolarisTableTool(base_url, http, authorization_provider)
+    namespace_tool = PolarisNamespaceTool(base_url, http, 
authorization_provider)
+    principal_tool = PolarisPrincipalTool(base_url, http, 
authorization_provider)
+    principal_role_tool = PolarisPrincipalRoleTool(base_url, http, 
authorization_provider)
+    catalog_role_tool = PolarisCatalogRoleTool(base_url, http, 
authorization_provider)
+    policy_tool = PolarisPolicyTool(base_url, http, authorization_provider)
+    catalog_tool = PolarisCatalogTool(base_url, http, authorization_provider)
+
+    server_version = _resolve_package_version()
+    mcp = FastMCP(
+        name="polaris-mcp",
+        version=server_version,
+    )
+
+    @mcp.tool(
+        name=table_tool.name,
+        description=table_tool.description,
+        output_schema=OUTPUT_SCHEMA,
+    )
+    def polaris_iceberg_table(
+        operation: str,
+        catalog: str,
+        namespace: str | Sequence[str],
+        table: str | None = None,
+        query: Mapping[str, str | Sequence[str]] | None = None,
+        headers: Mapping[str, str | Sequence[str]] | None = None,
+        body: Any | None = None,
+    ) -> FastMcpToolResult:
+        return _call_tool(
+            table_tool,
+            required={
+                "operation": operation,
+                "catalog": catalog,
+                "namespace": namespace,
+            },
+            optional={
+                "table": table,
+                "query": query,
+                "headers": headers,
+                "body": body,
+            },
+            transforms={
+                "namespace": _normalize_namespace,
+                "query": _copy_mapping,
+                "headers": _copy_mapping,
+                "body": _coerce_body,
+            },
+        )
+
+    @mcp.tool(
+        name=namespace_tool.name,
+        description=namespace_tool.description,
+        output_schema=OUTPUT_SCHEMA,
+    )
+    def polaris_namespace_request(
+        operation: str,
+        catalog: str,
+        namespace: str | Sequence[str] | None = None,
+        query: Mapping[str, str | Sequence[str]] | None = None,
+        headers: Mapping[str, str | Sequence[str]] | None = None,
+        body: Any | None = None,
+    ) -> FastMcpToolResult:
+        return _call_tool(
+            namespace_tool,
+            required={
+                "operation": operation,
+                "catalog": catalog,
+            },
+            optional={
+                "namespace": namespace,
+                "query": query,
+                "headers": headers,
+                "body": body,
+            },
+            transforms={
+                "namespace": _normalize_namespace,
+                "query": _copy_mapping,
+                "headers": _copy_mapping,
+                "body": _coerce_body,
+            },
+        )
+
+    @mcp.tool(
+        name=principal_tool.name,
+        description=principal_tool.description,
+        output_schema=OUTPUT_SCHEMA,
+    )
+    def polaris_principal_request(
+        operation: str,
+        principal: str | None = None,
+        principalRole: str | None = None,
+        query: Mapping[str, str | Sequence[str]] | None = None,
+        headers: Mapping[str, str | Sequence[str]] | None = None,
+        body: Any | None = None,
+    ) -> FastMcpToolResult:
+        return _call_tool(
+            principal_tool,
+            required={"operation": operation},
+            optional={
+                "principal": principal,
+                "principalRole": principalRole,
+                "query": query,
+                "headers": headers,
+                "body": body,
+            },
+            transforms={
+                "query": _copy_mapping,
+                "headers": _copy_mapping,
+                "body": _coerce_body,
+            },
+        )
+
+    @mcp.tool(
+        name=principal_role_tool.name,
+        description=principal_role_tool.description,
+        output_schema=OUTPUT_SCHEMA,
+    )
+    def polaris_principal_role_request(
+        operation: str,
+        principalRole: str | None = None,
+        catalog: str | None = None,
+        catalogRole: str | None = None,
+        query: Mapping[str, str | Sequence[str]] | None = None,
+        headers: Mapping[str, str | Sequence[str]] | None = None,
+        body: Any | None = None,
+    ) -> FastMcpToolResult:
+        return _call_tool(
+            principal_role_tool,
+            required={"operation": operation},
+            optional={
+                "principalRole": principalRole,
+                "catalog": catalog,
+                "catalogRole": catalogRole,
+                "query": query,
+                "headers": headers,
+                "body": body,
+            },
+            transforms={
+                "query": _copy_mapping,
+                "headers": _copy_mapping,
+                "body": _coerce_body,
+            },
+        )
+
+    @mcp.tool(
+        name=catalog_role_tool.name,
+        description=catalog_role_tool.description,
+        output_schema=OUTPUT_SCHEMA,
+    )
+    def polaris_catalog_role_request(
+        operation: str,
+        catalog: str,
+        catalogRole: str | None = None,
+        query: Mapping[str, str | Sequence[str]] | None = None,
+        headers: Mapping[str, str | Sequence[str]] | None = None,
+        body: Any | None = None,
+    ) -> FastMcpToolResult:
+        return _call_tool(
+            catalog_role_tool,
+            required={
+                "operation": operation,
+                "catalog": catalog,
+            },
+            optional={
+                "catalogRole": catalogRole,
+                "query": query,
+                "headers": headers,
+                "body": body,
+            },
+            transforms={
+                "query": _copy_mapping,
+                "headers": _copy_mapping,
+                "body": _coerce_body,
+            },
+        )
+
+    @mcp.tool(
+        name=policy_tool.name,
+        description=policy_tool.description,
+        output_schema=OUTPUT_SCHEMA,
+    )
+    def polaris_policy_request(
+        operation: str,
+        catalog: str,
+        namespace: str | Sequence[str] | None = None,
+        policy: str | None = None,
+        query: Mapping[str, str | Sequence[str]] | None = None,
+        headers: Mapping[str, str | Sequence[str]] | None = None,
+        body: Any | None = None,
+    ) -> FastMcpToolResult:
+        return _call_tool(
+            policy_tool,
+            required={
+                "operation": operation,
+                "catalog": catalog,
+            },
+            optional={
+                "namespace": namespace,
+                "policy": policy,
+                "query": query,
+                "headers": headers,
+                "body": body,
+            },
+            transforms={
+                "namespace": _normalize_namespace,
+                "query": _copy_mapping,
+                "headers": _copy_mapping,
+                "body": _coerce_body,
+            },
+        )
+
+    @mcp.tool(
+        name=catalog_tool.name,
+        description=catalog_tool.description,
+        output_schema=OUTPUT_SCHEMA,
+    )
+    def polaris_catalog_request(
+        operation: str,
+        catalog: str | None = None,
+        query: Mapping[str, str | Sequence[str]] | None = None,
+        headers: Mapping[str, str | Sequence[str]] | None = None,
+        body: Any | None = None,
+    ) -> FastMcpToolResult:
+        return _call_tool(
+            catalog_tool,
+            required={"operation": operation},
+            optional={
+                "catalog": catalog,
+                "query": query,
+                "headers": headers,
+                "body": body,
+            },
+            transforms={
+                "query": _copy_mapping,
+                "headers": _copy_mapping,
+                "body": _coerce_body,
+            },
+        )
+
+    return mcp
+
+
+def _call_tool(
+    tool: Any,
+    *,
+    required: Mapping[str, Any],
+    optional: Mapping[str, Any | None] | None = None,
+    transforms: Mapping[str, Any] | None = None,
+) -> FastMcpToolResult:
+    arguments: MutableMapping[str, Any] = dict(required)
+    if optional:
+        for key, value in optional.items():
+            if value is not None:
+                arguments[key] = value
+    if transforms:
+        for key, transform in transforms.items():
+            if key in arguments and arguments[key] is not None:
+                arguments[key] = transform(arguments[key])
+    return _to_tool_result(tool.call(arguments))
+
+
+def _to_tool_result(result: ToolExecutionResult) -> FastMcpToolResult:
+    structured = {"isError": result.is_error}
+    if result.metadata is not None:
+        structured["meta"] = result.metadata
+    return FastMcpToolResult(
+        content=[TextContent(type="text", text=result.text)],
+        structured_content=structured,
+    )
+
+
+def _copy_mapping(
+    mapping: Mapping[str, Any] | None,
+) -> MutableMapping[str, Any] | None:
+    if mapping is None:
+        return None
+    copied: MutableMapping[str, Any] = {}
+    for key, value in mapping.items():
+        if value is None:
+            continue
+        if isinstance(value, (list, tuple)):
+            copied[key] = [str(item) for item in value]
+        else:
+            copied[key] = value
+    return copied
+
+
+def _coerce_body(body: Any) -> Any:
+    """Return plain dicts for mapping objects so downstream JSON encoding 
succeeds."""
+    if isinstance(body, Mapping):
+        return dict(body)
+    return body
+
+
+def _normalize_namespace(namespace: str | Sequence[str]) -> str | list[str]:
+    if isinstance(namespace, str):
+        return namespace
+    return [str(part) for part in namespace]
+
+
+def _resolve_base_url() -> str:
+    for candidate in (
+        os.getenv("POLARIS_BASE_URL"),
+        os.getenv("POLARIS_REST_BASE_URL"),
+    ):
+        if candidate and candidate.strip():

Review Comment:
   We might want to some basic validation here.



##########
mcp-server/polaris_mcp/authorization.py:
##########
@@ -0,0 +1,136 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""Authorization helpers for the Polaris MCP server."""
+
+from __future__ import annotations
+
+import json
+import threading
+import time
+from abc import ABC, abstractmethod
+from typing import Optional
+from urllib.parse import urlencode
+
+import urllib3
+
+
+class AuthorizationProvider(ABC):
+    """Return Authorization header values for outgoing requests."""
+
+    @abstractmethod
+    def authorization_header(self) -> Optional[str]:
+        ...
+
+
+class StaticAuthorizationProvider(AuthorizationProvider):
+    """Wrap a static bearer token."""
+
+    def __init__(self, token: Optional[str]) -> None:
+        value = (token or "").strip()
+        self._header = f"Bearer {value}" if value else None
+
+    def authorization_header(self) -> Optional[str]:
+        return self._header
+
+
+class ClientCredentialsAuthorizationProvider(AuthorizationProvider):
+    """Implements the OAuth client-credentials flow with caching."""
+
+    def __init__(
+        self,
+        token_endpoint: str,
+        client_id: str,
+        client_secret: str,
+        scope: Optional[str],
+        http: urllib3.PoolManager,
+    ) -> None:
+        self._token_endpoint = token_endpoint
+        self._client_id = client_id
+        self._client_secret = client_secret
+        self._scope = scope
+        self._http = http
+        self._lock = threading.Lock()
+        self._cached: Optional[tuple[str, float]] = None  # (token, 
expires_at_epoch)
+
+    def authorization_header(self) -> Optional[str]:
+        token = self._current_token()
+        return f"Bearer {token}" if token else None
+
+    def _current_token(self) -> Optional[str]:
+        now = time.time()
+        cached = self._cached
+        if not cached or cached[1] - 60 <= now:
+            with self._lock:
+                cached = self._cached
+                if not cached or cached[1] - 60 <= time.time():
+                    self._cached = cached = self._fetch_token()
+        return cached[0] if cached else None
+
+    def _fetch_token(self) -> tuple[str, float]:
+        payload = {
+            "grant_type": "client_credentials",
+            "client_id": self._client_id,
+            "client_secret": self._client_secret,
+        }
+        if self._scope:
+            payload["scope"] = self._scope
+
+        encoded = urlencode(payload)
+        response = self._http.request(

Review Comment:
   Enhancement: We can file an issue on this, but it might be nice to have 
throttling & retry logic for transient failures.



##########
mcp-server/polaris_mcp/tools/namespace.py:
##########
@@ -0,0 +1,304 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""Namespace MCP tool."""
+
+from __future__ import annotations
+
+import copy
+import string
+from typing import Any, Dict, List, Optional, Set
+
+import urllib3
+
+from ..authorization import AuthorizationProvider
+from ..base import JSONDict, McpTool, ToolExecutionResult, copy_if_object, 
require_text
+from ..rest import PolarisRestTool, encode_path_segment
+
+
+class PolarisNamespaceTool(McpTool):
+    """Manage namespaces through the Polaris REST API."""
+
+    TOOL_NAME = "polaris-namespace-request"
+    TOOL_DESCRIPTION = (
+        "Manage namespaces in an Iceberg catalog (list, get, create, update 
properties, delete)."
+    )
+    NAMESPACE_DELIMITER = "\x1f"

Review Comment:
   Nit: It seems like this is defined multiple times. Maybe, we could push to a 
common area?



-- 
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