From 3d5e03485af343d77cc51a673d1ceedf6472b3f6 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Wed, 27 May 2026 21:17:26 +0200 Subject: [PATCH] feat(mcp): get_usage_metrics works for Tenant Admins (role-based routing) get_usage_metrics always called /api/global/metrics/usage (GlobalAdminOnly), so a Tenant Admin got 403 even though a MemberRead tenant endpoint (/api/metrics/usage) exists and returns the same shape scoped to their tenant. Route via pickGlobalOrTenantPath: GA -> global cross-tenant overview; Tenant Admin/Member -> /api/metrics/usage (JWT-scoped, tenantId ignored). Description updated for both roles. Verified live with a Tenant-Admin token: 200, own-tenant usage only (tenants.total=1), platformStats correctly omitted (GA-only) - no cross-tenant leak. tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../autopilot-monitor-mcp/src/tools/admin.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/McpServer/autopilot-monitor-mcp/src/tools/admin.ts b/src/McpServer/autopilot-monitor-mcp/src/tools/admin.ts index dfa721b5..5eee5c78 100644 --- a/src/McpServer/autopilot-monitor-mcp/src/tools/admin.ts +++ b/src/McpServer/autopilot-monitor-mcp/src/tools/admin.ts @@ -257,13 +257,13 @@ export function registerAdminTools(server: McpServer): void { // Tool 15: get_usage_metrics server.tool( 'get_usage_metrics', - 'Get platform usage statistics: active tenants, session volumes, feature adoption. ' + - 'Global Admin only — omit tenantId for the platform-wide overview, or pass tenantId to filter that ' + - 'cross-tenant view down to a single tenant. The call always uses the global endpoint, so a Tenant Admin ' + - 'cannot use this tool (they receive 403) even when passing their own tenantId. ' + + 'Get usage statistics: session volumes, feature adoption, active tenants/users. ' + + 'Global Admin: cross-tenant platform overview — omit tenantId for the whole platform, or pass ' + + 'tenantId to filter that view to one tenant. Tenant Admin / Member: returns usage scoped to your ' + + 'own tenant (the tenantId argument is ignored — your token determines the tenant). ' + 'days accepts any value 1-365 (e.g. 5, 7, 12, 30, 90).', { - tenantId: z.string().optional().describe('Filter the global view to a single tenant. Omit for the platform-wide overview. This tool is Global Admin only either way.'), + tenantId: z.string().optional().describe('Global Admin only: filter the platform-wide view to a single tenant. Ignored for Tenant Admins (your token scopes the result).'), days: z.coerce.number().int().min(1).max(365).optional().default(30) .describe('Time window in days (1-365). Defaults to 30. Sessions.Total / Tenants.Total reflect this window.'), }, @@ -271,7 +271,11 @@ export function registerAdminTools(server: McpServer): void { async (args) => withToolTelemetry('get_usage_metrics', async () => { try { const { tenantId, days } = args; - const data = await apiFetch(`/api/global/metrics/usage${buildQuery({ tenantId, days })}`); + // GA → /api/global/metrics/usage (tenantId is a filter); Tenant-Admin → /api/metrics/usage + // (JWT-scoped; tenantId ignored). Routing by role unlocks the MemberRead tenant endpoint + // for non-GA callers instead of a blanket 403. + const path = pickGlobalOrTenantPath('/api/global/metrics/usage', '/api/metrics/usage'); + const data = await apiFetch(`${path}${buildQuery({ tenantId, days })}`); return toolResultText(data, MAX_RESULT_SIZE_CHARS.small); } catch (error: unknown) { return toolError('get_usage_metrics', args, error);