Skip to content

Tool group permissions are case-sensitive on server name → default-block consumers get 0 tools #68

Description

@rb-ntnx

Summary

Tool group permission matching in MCPX is case-sensitive on the server (service) name, while upstream server names are normalized to lowercase everywhere else. As a result, a consumer with default-block + an allowed tool group is granted zero tools whenever the tool group's services key isn't already lowercase — which is exactly what the dashboard "Create Tool Group" flow produces (it uses the server's display name, e.g. MyTeamHub).

This makes selective tool-group access effectively "all or nothing": "All Server Tools" works (default allow), but selecting a group yields 0 tools.

Environment

  • MCPX v0.2.9-fix2 (self-hosted, personal/non-enterprise mode), all-in-one image.
  • Downstream client: Cursor (connects as clientName: "Cursor", no consumer tag).

Steps to reproduce

  1. Add an upstream server whose name has uppercase letters, e.g. MyTeamHub.
  2. In the dashboard, create a Tool Group selecting some of that server's tools.
  3. Assign the group to an agent (this writes a default-block consumer with allow: [<group>]).
  4. Have the agent list tools.

Expected: the agent sees the group's tools (e.g. 17).
Actual: the agent sees 0 tools.

Root cause

Upstream servers are registered under a normalized (lowercased) name:

// packages/toolkit-core/src/data/string-sanitation.ts (and toolkit-ui/.../server-helpers.ts)
export function normalizeServerName(name: string): string {
  return name.toLowerCase().trim();
}

So the capability resolver reports serviceName = "myteamhub" (advertised tools are myteamhub__<tool>), and calls into permission checking with that normalized name:

// packages/mcpx-server/src/services/capability-resolver.ts (allows())
this.permissions.hasPermission({
  serviceName: cap.serverName,        // "myteamhub"
  capabilityName: cap.capabilityName, // real tool name
  ...
});

But the permission manager builds its per-service map straight from the tool group's services keys without normalization:

// packages/mcpx-server/src/services/permissions.ts (addServices())
Object.entries(toolGroup).forEach(([serviceName, serviceToolGroup]) => {
  const current = services.get(serviceName); // key = "MyTeamHub" (as stored)
  ...
});

Then evaluatePermission does services.get("myteamhub"), which misses the "MyTeamHub" entry, falls through to the consumer default (block), and denies every tool.

Reproduction of the fix (verified)

Rewriting the group's service key from MyTeamHubmyteamhub (via PUT /config/tool-groups/:name) immediately restores all 17 tools for the same consumer. Confirmed via a real initialize + tools/list against /mcp as clientName: "Cursor":

  • services: { MyTeamHub: [...] }tools/list returns 0
  • services: { myteamhub: [...] }tools/list returns 17

Suggested fix

Normalize the server (service) key in the permission layer so it matches the already-normalized serverName used at request time — e.g. apply normalizeServerName() to the keys in PermissionManagerState.addServices() (and/or when a tool group is created/updated). Optionally normalize the key in the UI's Create Tool Group flow as well so persisted config stays clean.

Adding a permissions.test.ts case asserting that a capitalized group services key still resolves for a lowercased server would guard against regressions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions