This is an automated email from the ASF dual-hosted git repository.

yzheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris-tools.git


The following commit(s) were added to refs/heads/main by this push:
     new 267281a  Enforce mypy for MCP (#117)
267281a is described below

commit 267281a084aa1bc5575ad16951f083a11d3caa54
Author: Yong Zheng <[email protected]>
AuthorDate: Tue Dec 30 01:04:30 2025 -0600

    Enforce mypy for MCP (#117)
---
 mcp-server/.pre-commit-config.yaml      |  7 +++++
 mcp-server/int_test/client.py           |  5 ++--
 mcp-server/polaris_mcp/authorization.py | 40 ++++++++++++++--------------
 mcp-server/polaris_mcp/server.py        |  4 +--
 mcp-server/polaris_mcp/tools/policy.py  | 46 +++++++++++++++------------------
 mcp-server/tests/test_config.py         |  3 ++-
 mcp-server/tests/test_rest_tool.py      |  2 ++
 mcp-server/tests/test_table_tool.py     |  3 ++-
 8 files changed, 59 insertions(+), 51 deletions(-)

diff --git a/mcp-server/.pre-commit-config.yaml 
b/mcp-server/.pre-commit-config.yaml
index 8e360cd..7926b76 100644
--- a/mcp-server/.pre-commit-config.yaml
+++ b/mcp-server/.pre-commit-config.yaml
@@ -37,3 +37,10 @@ repos:
       # Run the formatter.
       - id: ruff-format
         files: ^mcp-server/
+  - repo: https://github.com/pre-commit/mirrors-mypy
+    rev: v1.16.0
+    hooks:
+      - id: mypy
+        args:
+          [--disallow-untyped-defs, --ignore-missing-imports, --install-types, 
--non-interactive, --follow-imports=skip]
+        files: 
'(mcp-server/polaris_mcp/.*\.py)|(mcp-server/polaris_mcp/tools/.*\.py)|(mcp-server/tests/.*\.py)|(mcp-server/int_test/.*\.py)'
diff --git a/mcp-server/int_test/client.py b/mcp-server/int_test/client.py
index 9665e74..3efb27c 100644
--- a/mcp-server/int_test/client.py
+++ b/mcp-server/int_test/client.py
@@ -25,6 +25,7 @@ import argparse
 import json
 import sys
 from typing import Any, Optional
+import mcp.types as types
 
 
 class McpClientError(Exception):
@@ -115,7 +116,7 @@ async def _display(result: Any) -> None:
 
 
 async def _run_session(session: Any, args: argparse.Namespace) -> None:
-    def list_tools(tools):
+    def list_tools(tools: types.ListToolsResult) -> None:
         print("Available Tools:")
         for tool in tools:
             print(f"- {tool.name}: {tool.description}")
@@ -184,7 +185,7 @@ async def _run_session(session: Any, args: 
argparse.Namespace) -> None:
             print(f"Error running tool '{selected_tool.name}': {e}")
 
 
-async def run():
+async def run() -> None:
     parser = argparse.ArgumentParser(description="Polaris MCP Client")
     parser.add_argument(
         "server", help="MCP server. Can be a local .py file, or an HTTP/SSE 
URL."
diff --git a/mcp-server/polaris_mcp/authorization.py 
b/mcp-server/polaris_mcp/authorization.py
index 6f38076..f19741f 100644
--- a/mcp-server/polaris_mcp/authorization.py
+++ b/mcp-server/polaris_mcp/authorization.py
@@ -73,7 +73,7 @@ class 
ClientCredentialsAuthorizationProvider(AuthorizationProvider):
         return f"Bearer {token}" if token else None
 
     def _get_token_from_realm(self, realm: Optional[str]) -> Optional[str]:
-        def needs_refresh(cached):
+        def needs_refresh(cached: Optional[tuple[str, float]]) -> bool:
             return (
                 cached is None
                 or cached[1] - self._refresh_buffer_seconds <= time.time()
@@ -82,7 +82,7 @@ class 
ClientCredentialsAuthorizationProvider(AuthorizationProvider):
         cache_key = realm or ""
         token = self._cached.get(cache_key)
         # Token not expired
-        if not needs_refresh(token):
+        if token and not needs_refresh(token):
             return token[0]
         # Acquire lock and verify again if token expired
         with self._lock:
@@ -102,26 +102,26 @@ class 
ClientCredentialsAuthorizationProvider(AuthorizationProvider):
             val = os.getenv(key)
             return val.strip() or None if val else None
 
-        def load_creds(realm: Optional[str] = None) -> dict[str, 
Optional[str]]:
-            prefix = f"POLARIS_REALM_{realm}_" if realm else "POLARIS_"
-            return {
-                "client_id": get_env(f"{prefix}CLIENT_ID"),
-                "client_secret": get_env(f"{prefix}CLIENT_SECRET"),
-                "scope": get_env(f"{prefix}TOKEN_SCOPE"),
-                "token_url": get_env(f"{prefix}TOKEN_URL"),
+        def load_creds(creds_realm: Optional[str] = None) -> 
Optional[dict[str, str]]:
+            prefix = f"POLARIS_REALM_{creds_realm}_" if creds_realm else 
"POLARIS_"
+            client_id = get_env(f"{prefix}CLIENT_ID")
+            client_secret = get_env(f"{prefix}CLIENT_SECRET")
+            if not client_id or not client_secret:
+                return None
+            creds: dict[str, str] = {
+                "client_id": client_id,
+                "client_secret": client_secret,
             }
-
-        # Only use realm-specific credentials
-        if realm:
-            creds = load_creds(realm)
-            if creds["client_id"] and creds["client_secret"]:
-                return creds
-            return None
-        # No realm specified, use global credentials
-        creds = load_creds()
-        if creds["client_id"] and creds["client_secret"]:
+            scope = get_env(f"{prefix}TOKEN_SCOPE")
+            if scope:
+                creds["scope"] = scope
+            token_url = get_env(f"{prefix}TOKEN_URL")
+            if token_url:
+                creds["token_url"] = token_url
             return creds
-        return None
+
+        # Use global credentials if realm not specified
+        return load_creds(realm) if realm else load_creds()
 
     def _fetch_token(
         self, realm: Optional[str], credentials: dict[str, str]
diff --git a/mcp-server/polaris_mcp/server.py b/mcp-server/polaris_mcp/server.py
index 7ebb812..1f458d2 100644
--- a/mcp-server/polaris_mcp/server.py
+++ b/mcp-server/polaris_mcp/server.py
@@ -413,7 +413,7 @@ def _call_tool(
 
 
 def _to_tool_result(result: ToolExecutionResult) -> FastMcpToolResult:
-    structured = {"isError": result.is_error}
+    structured: dict[str, Any] = {"isError": result.is_error}
     if result.metadata is not None:
         structured["meta"] = result.metadata
 
@@ -448,7 +448,7 @@ def _coerce_body(body: Any) -> Any:
     return body
 
 
-def _normalize_namespace(namespace: str | Sequence[str]) -> str | list[str]:
+def _normalize_namespace(namespace: str | Sequence) -> str | list[str]:
     if isinstance(namespace, str):
         return namespace
     return [str(part) for part in namespace]
diff --git a/mcp-server/polaris_mcp/tools/policy.py 
b/mcp-server/polaris_mcp/tools/policy.py
index 1961044..b5b12d9 100644
--- a/mcp-server/polaris_mcp/tools/policy.py
+++ b/mcp-server/polaris_mcp/tools/policy.py
@@ -147,31 +147,26 @@ class PolarisPolicyTool(McpTool):
         if isinstance(realm, str) and realm.strip():
             delegate_args["realm"] = realm
 
-        if normalized == "list":
-            self._require_namespace(namespace, "list")
-            self._handle_list(delegate_args, catalog, namespace)
-        elif normalized == "get":
-            self._require_namespace(namespace, "get")
-            self._handle_get(arguments, delegate_args, catalog, namespace)
-        elif normalized == "create":
-            self._require_namespace(namespace, "create")
-            self._handle_create(arguments, delegate_args, catalog, namespace)
-        elif normalized == "update":
-            self._require_namespace(namespace, "update")
-            self._handle_update(arguments, delegate_args, catalog, namespace)
-        elif normalized == "delete":
-            self._require_namespace(namespace, "delete")
-            self._handle_delete(arguments, delegate_args, catalog, namespace)
-        elif normalized == "attach":
-            self._require_namespace(namespace, "attach")
-            self._handle_attach(arguments, delegate_args, catalog, namespace)
-        elif normalized == "detach":
-            self._require_namespace(namespace, "detach")
-            self._handle_detach(arguments, delegate_args, catalog, namespace)
-        elif normalized == "applicable":
+        if normalized == "applicable":
             self._handle_applicable(delegate_args, catalog)
-        else:  # pragma: no cover
-            raise ValueError(f"Unsupported operation: {operation}")
+        else:
+            str_namespace = self._require_namespace(namespace, normalized)
+            if normalized == "list":
+                self._handle_list(delegate_args, catalog, str_namespace)
+            elif normalized == "get":
+                self._handle_get(arguments, delegate_args, catalog, 
str_namespace)
+            elif normalized == "create":
+                self._handle_create(arguments, delegate_args, catalog, 
str_namespace)
+            elif normalized == "update":
+                self._handle_update(arguments, delegate_args, catalog, 
str_namespace)
+            elif normalized == "delete":
+                self._handle_delete(arguments, delegate_args, catalog, 
str_namespace)
+            elif normalized == "attach":
+                self._handle_attach(arguments, delegate_args, catalog, 
str_namespace)
+            elif normalized == "detach":
+                self._handle_detach(arguments, delegate_args, catalog, 
str_namespace)
+            else:  # pragma: no cover
+                raise ValueError(f"Unsupported operation: {operation}")
 
         raw = self._rest_client.call(delegate_args)
         return self._maybe_augment_error(raw, normalized)
@@ -360,11 +355,12 @@ class PolarisPolicyTool(McpTool):
             return "applicable"
         raise ValueError(f"Unsupported operation: {operation}")
 
-    def _require_namespace(self, namespace: Optional[str], operation: str) -> 
None:
+    def _require_namespace(self, namespace: Optional[str], operation: str) -> 
str:
         if not namespace:
             raise ValueError(
                 f"Namespace is required for {operation} operations. Provide 
`namespace` as a string or array."
             )
+        return namespace
 
     def _resolve_namespace(self, namespace: Any) -> str:
         if namespace is None:
diff --git a/mcp-server/tests/test_config.py b/mcp-server/tests/test_config.py
index 09b998a..acf24df 100644
--- a/mcp-server/tests/test_config.py
+++ b/mcp-server/tests/test_config.py
@@ -23,6 +23,7 @@ import os
 import textwrap
 from unittest import mock
 from polaris_mcp import server
+from pathlib import Path
 
 
 def test_main_loads_default_config_file() -> None:
@@ -64,7 +65,7 @@ def test_main_loads_custom_config_file() -> None:
         mock_server_instance.run.assert_called_once()
 
 
-def test_config_loading_precedence(tmp_path) -> None:
+def test_config_loading_precedence(tmp_path: Path) -> None:
     config_file = tmp_path / "test_config.env"
     config_file.write_text(
         textwrap.dedent("""
diff --git a/mcp-server/tests/test_rest_tool.py 
b/mcp-server/tests/test_rest_tool.py
index 07de663..e3ca8ff 100644
--- a/mcp-server/tests/test_rest_tool.py
+++ b/mcp-server/tests/test_rest_tool.py
@@ -105,6 +105,7 @@ def test_call_builds_request_and_metadata_with_json_body() 
-> None:
     assert not result.is_error
     assert f"POST {expected_url}" in result.text
     assert '"result": "ok"' in result.text
+    assert result.metadata is not None
     assert result.metadata["method"] == "POST"
     assert result.metadata["url"] == expected_url
     assert result.metadata["status"] == 201
@@ -155,6 +156,7 @@ def 
test_call_uses_authorization_provider_and_handles_plain_text() -> None:
 
     assert result.is_error
     assert "Status: 404" in result.text
+    assert result.metadata is not None
     assert result.metadata["url"] == expected_url
     assert result.metadata["request"]["bodyText"] == "payload"
     assert result.metadata["response"]["bodyText"] == "failure"
diff --git a/mcp-server/tests/test_table_tool.py 
b/mcp-server/tests/test_table_tool.py
index e3d5d9b..9a87154 100644
--- a/mcp-server/tests/test_table_tool.py
+++ b/mcp-server/tests/test_table_tool.py
@@ -23,6 +23,7 @@ from __future__ import annotations
 
 import pytest
 from unittest import mock
+from typing import Any
 
 from polaris_mcp.base import ToolExecutionResult
 from polaris_mcp.tools.table import PolarisTableTool
@@ -87,7 +88,7 @@ def test_get_operation_requires_table_argument() -> None:
 
 def test_create_operation_deep_copies_request_body() -> None:
     tool, delegate = _build_tool()
-    body = {"table": "t1", "properties": {"schema-id": 1}}
+    body: dict[str, Any] = {"table": "t1", "properties": {"schema-id": 1}}
     tool.call(
         {
             "operation": "create",

Reply via email to