1- """MCP server authorization status cache, for the unauthorized MCP server guardrail .
1+ """MCP server authorization status cache: session-start writes it, the pre- MCP-execution hook reads it .
22
3- session-start fetches the authorization status of every MCP server the platform ingested from
4- this user's devices (Inventory -> AI Governance) and writes it here; the pre-MCP-execution hook
5- only reads it. An absent or corrupt cache means no status is known, so the guardrail fails open.
6- The cache also keeps the "Unauthorized AI Entity" policy's strict-mode flag the platform returns
7- with the statuses (whether unreviewed servers count as unauthorized).
3+ An absent or corrupt cache means no status is known, so the guardrail fails open.
84"""
95
106import json
2420
2521MCP_SERVER_STATUSES_FILE_NAME = 'ai-guardrails-mcp-servers.json'
2622
27- # The platform's response field for the strict mode, kept under the same name in the cache.
2823TREAT_UNREVIEWED_AS_UNAUTHORIZED_KEY = 'should_treat_unreviewed_as_unauthorized'
2924
30- # Claude Code namespaces a plugin's servers in tool names as `plugin_<plugin>_<server>`, while the
31- # platform stores the server under its config key - the name the session sweep reported.
25+ # Claude Code names plugin servers `plugin_<plugin>_<server>` in tool names; the platform stores `<server>`.
3226_PLUGIN_ALIAS_PREFIX = 'plugin_'
3327
3428
@@ -38,8 +32,7 @@ class McpServerAuthorizationStatus(str, Enum):
3832 UNAUTHORIZED = 'Unauthorized'
3933
4034
41- # When one alias maps to several servers (e.g. the same name configured differently on two
42- # devices), the most restrictive status wins.
35+ # An alias shared by several servers (e.g. configured differently per device) gets the most restrictive status.
4336_RESTRICTIVENESS = {
4437 McpServerAuthorizationStatus .AUTHORIZED : 0 ,
4538 McpServerAuthorizationStatus .UNREVIEWED : 1 ,
@@ -52,7 +45,7 @@ def get_mcp_server_statuses_cache_path() -> Path:
5245
5346
5447def parse_status (raw_status : object ) -> McpServerAuthorizationStatus :
55- """Case-insensitive; an unknown status reads as Unreviewed ( no decision was made on the server) ."""
48+ """An unknown status reads as Unreviewed: no decision was made on the server."""
5649 lowered = str (raw_status or '' ).lower ()
5750 for status in McpServerAuthorizationStatus :
5851 if status .value .lower () == lowered :
@@ -61,16 +54,12 @@ def parse_status(raw_status: object) -> McpServerAuthorizationStatus:
6154
6255
6356def _normalize_alias (alias : str ) -> str :
64- """The form an IDE may turn a config name into inside a tool name (Claude Code keeps [A-Za-z0-9_-]) ."""
57+ """Mirrors how Claude Code sanitizes a config name inside a tool name."""
6558 return re .sub (r'[^a-z0-9_-]' , '_' , alias .lower ())
6659
6760
6861def is_enforced (status : Optional [McpServerAuthorizationStatus ], treat_unreviewed : bool ) -> bool :
69- """Whether a server with this status (None: the platform never saw it) is enforced.
70-
71- Unauthorized always is; Unreviewed or unknown only when the policy treats unreviewed servers as
72- unauthorized (``treat_unreviewed``); Authorized never is.
73- """
62+ """``status`` is None when the platform never saw the server."""
7463 if status == McpServerAuthorizationStatus .UNAUTHORIZED :
7564 return True
7665 return treat_unreviewed and status != McpServerAuthorizationStatus .AUTHORIZED
@@ -91,8 +80,7 @@ class McpServerStatuses:
9180 _by_alias : dict = field (init = False , repr = False )
9281
9382 def __post_init__ (self ) -> None :
94- # Lowered alias -> (stored alias, most restrictive status). The platform matches aliases
95- # case-insensitively, so the CLI does too.
83+ # The platform matches aliases case-insensitively, so the CLI does too.
9684 self ._by_alias = {}
9785 for server in self .servers :
9886 if not isinstance (server , dict ) or not server .get ('alias' ):
@@ -104,7 +92,6 @@ def __post_init__(self) -> None:
10492 self ._by_alias [alias .lower ()] = McpServerMatch (alias , status )
10593
10694 def match (self , alias : str ) -> Optional [McpServerMatch ]:
107- """The platform's entry for the alias a hook reported, or None when the platform has none."""
10895 exact = self ._by_alias .get (alias .lower ())
10996 if exact is not None :
11097 return exact
@@ -127,7 +114,6 @@ def is_expired(self) -> bool:
127114 return time .time () - self .fetched_at > self .ttl_seconds
128115
129116 def needs_refresh (self , tenant_id : Optional [str ]) -> bool :
130- """Expired, or fetched for another tenant (the user switched tenants since)."""
131117 return self .is_expired () or self .tenant_id != tenant_id
132118
133119
@@ -137,7 +123,6 @@ def save_mcp_server_statuses(
137123 ttl_seconds : float = DEFAULT_TTL_SECONDS ,
138124 treat_unreviewed_as_unauthorized : bool = False ,
139125) -> None :
140- """Persist fetched statuses; a failed write just leaves the previous cache in place."""
141126 path = get_mcp_server_statuses_cache_path ()
142127 content = {
143128 'fetched_at' : time .time (),
@@ -154,7 +139,6 @@ def save_mcp_server_statuses(
154139
155140
156141def load_mcp_server_statuses () -> Optional [McpServerStatuses ]:
157- """The cached statuses, or None when the cache is absent or corrupt (quarantined)."""
158142 path = get_mcp_server_statuses_cache_path ()
159143 if not path .exists ():
160144 return None
@@ -170,7 +154,6 @@ def load_mcp_server_statuses() -> Optional[McpServerStatuses]:
170154 fetched_at = float (content ['fetched_at' ]),
171155 tenant_id = content .get ('tenant_id' ),
172156 ttl_seconds = float (content .get ('ttl_seconds' ) or DEFAULT_TTL_SECONDS ),
173- # Missing in caches written before the flag existed: not strict.
174157 treat_unreviewed_as_unauthorized = content .get (TREAT_UNREVIEWED_AS_UNAUTHORIZED_KEY ) is True ,
175158 )
176159 except Exception as e :
0 commit comments