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
- Add an upstream server whose name has uppercase letters, e.g.
MyTeamHub.
- In the dashboard, create a Tool Group selecting some of that server's tools.
- Assign the group to an agent (this writes a
default-block consumer with allow: [<group>]).
- 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 MyTeamHub → myteamhub (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.
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'sserviceskey 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
v0.2.9-fix2(self-hosted, personal/non-enterprise mode), all-in-one image.clientName: "Cursor", no consumer tag).Steps to reproduce
MyTeamHub.default-blockconsumer withallow: [<group>]).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:
So the capability resolver reports
serviceName = "myteamhub"(advertised tools aremyteamhub__<tool>), and calls into permission checking with that normalized name:But the permission manager builds its per-service map straight from the tool group's
serviceskeys without normalization:Then
evaluatePermissiondoesservices.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
MyTeamHub→myteamhub(viaPUT /config/tool-groups/:name) immediately restores all 17 tools for the same consumer. Confirmed via a realinitialize+tools/listagainst/mcpasclientName: "Cursor":services: { MyTeamHub: [...] }→tools/listreturns 0services: { myteamhub: [...] }→tools/listreturns 17Suggested fix
Normalize the server (service) key in the permission layer so it matches the already-normalized
serverNameused at request time — e.g. applynormalizeServerName()to the keys inPermissionManagerState.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.tscase asserting that a capitalized groupserviceskey still resolves for a lowercased server would guard against regressions.