diff --git a/src/api/handlers/mcp/auth.py b/src/api/handlers/mcp/auth.py
index c72e51d5b..fdfd7688f 100644
--- a/src/api/handlers/mcp/auth.py
+++ b/src/api/handlers/mcp/auth.py
@@ -107,6 +107,10 @@ def decorated(*args, **kwargs):
def check_project_access_mcp(project_id: str) -> bool:
"""Return True if the current request may access project_id.
+ enabled_projects semantics (intentional):
+ {} → allow all projects (user chose no restriction when creating the token)
+ {id: ...} → allow only the listed project IDs
+
MCP token path: project must be in g.mcp_enabled_projects and not past
its per-project expiry (if set).
Session path: delegates to OPA (already checked in before_request).
@@ -116,6 +120,10 @@ def check_project_access_mcp(project_id: str) -> bool:
return True
enabled = g.mcp_enabled_projects
+ if not enabled:
+ # empty dict → no project restriction, allow all
+ return True
+
if project_id not in enabled:
return False
diff --git a/src/api/handlers/mcp/routes/projects.py b/src/api/handlers/mcp/routes/projects.py
index e82fcd9b5..4df144aad 100644
--- a/src/api/handlers/mcp/routes/projects.py
+++ b/src/api/handlers/mcp/routes/projects.py
@@ -27,21 +27,24 @@ def get(self):
enabled = getattr(g, 'mcp_enabled_projects', None)
if enabled is not None:
- # MCP token path: return only scoped projects where the token owner
- # is still a collaborator — re-validate membership at query time so
- # a revoked collaborator cannot enumerate project metadata.
+ # MCP token path
if not enabled:
- audit_mcp('list_projects', outcome='success', details={'count': 0})
- return []
-
- project_ids = list(enabled.keys())
- rows = g.db.execute_many_dict('''
- SELECT p.id, p.name, p.type, p.public
- FROM project p
- INNER JOIN collaborator co ON co.project_id = p.id AND co.user_id = %s
- WHERE p.id = ANY(%s::uuid[])
- ORDER BY p.name
- ''', [user_id, project_ids])
+ # empty dict = all projects the user is a collaborator on
+ rows = g.db.execute_many_dict('''
+ SELECT p.id, p.name, p.type, p.public
+ FROM project p
+ INNER JOIN collaborator co ON co.project_id = p.id AND co.user_id = %s
+ ORDER BY p.name
+ ''', [user_id])
+ else:
+ project_ids = list(enabled.keys())
+ rows = g.db.execute_many_dict('''
+ SELECT p.id, p.name, p.type, p.public
+ FROM project p
+ INNER JOIN collaborator co ON co.project_id = p.id AND co.user_id = %s
+ WHERE p.id = ANY(%s::uuid[])
+ ORDER BY p.name
+ ''', [user_id, project_ids])
else:
# Session path: return all projects the user is a collaborator on
rows = g.db.execute_many_dict('''
@@ -58,3 +61,4 @@ def get(self):
except Exception as exc:
audit_mcp('list_projects', outcome='failure', error=str(exc))
raise
+
diff --git a/src/dashboard-client/src/components/user/UserGlobalTokens.vue b/src/dashboard-client/src/components/user/UserGlobalTokens.vue
index e71e5716f..20f92a254 100644
--- a/src/dashboard-client/src/components/user/UserGlobalTokens.vue
+++ b/src/dashboard-client/src/components/user/UserGlobalTokens.vue
@@ -186,6 +186,155 @@
md-cancel-text="Cancel"
@close="onRevokeClose">
+
+
+
+
+
+
+
+ MCP Tokens
+
+ For use with the InfraBox MCP server (INFRABOX_MCP_TOKEN)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name must be at least 3 characters
+
+
+
+
+ 1–365 days
+
+
+ add_circle
+ Create MCP token
+
+
+
+
+ Project scope
+ leave all unchecked to allow access to all projects
+