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