Skip to content

feat(mcp-server): identity mapping to business scope (MCP Prompt 09)#4010

Open
gilgardosh wants to merge 2 commits into
mcp-setupfrom
claude/mcp-prompt-09-identity-mapping
Open

feat(mcp-server): identity mapping to business scope (MCP Prompt 09)#4010
gilgardosh wants to merge 2 commits into
mcp-setupfrom
claude/mcp-prompt-09-identity-mapping

Conversation

@gilgardosh

Copy link
Copy Markdown
Collaborator

Summary

Implements Prompt 09 – Identity mapping to business scope (see
docs/mcp/spec.md §7). Maps a verified token
to an internal user + business-membership context, mirroring the server's
tenant-isolation model.

Stacked PR: targets claude/mcp-prompt-08-token-verify (Prompt 08, #3973).
Review/merge Prompts 05→08 first; this diff shows only the Prompt 09 changes.

Changes

  • src/auth/identity.ts
    • McpAuthContextuserId, auth0UserId, email, roles (token scopes), memberships, defaultReadScope, and the source principal.
    • Scope helpers mirroring packages/server/.../auth-scope.ts: readScopeFromMemberships (default = all memberships), narrowReadScope (subset-only; returns null if any requested id is outside memberships), and resolveRequestedReadScope (no request ⇒ default; otherwise the narrowed subset or null).
    • MembershipSource — pluggable; default membershipsFromClaims reads the token's memberships custom claim (camelCase or snake_case entries, de-duplicated). A later step can swap in a GraphQL-backed source.
    • IdentityMappingError — the clear failure mode when a verified token has no subject. An empty membership set is a valid user with no access (authorization decides what that permits).
    • Per-request context store (setAuthContext/getAuthContext).
  • src/mcp/handler.ts — after token verification, resolve and store the auth context on the request.
  • Tests — multi-business mapping, default/narrowed/out-of-scope behavior, claim parsing (valid/malformed/non-array), an injected async source, and the missing-subject failure. 112 tests total.
  • README.md — Identity & tenant scope section.

Validation

  • yarn workspace @accounter/mcp-server test → 112 passed
  • yarn workspace @accounter/mcp-server lint / typecheck / build → pass

Notes / open decisions

  • Reuse vs. mirror (same call as Prompt 08). The server's membership/scope types and helpers are pure, but live inside @accounter/server, which is a graphql-modules/Postgres app rather than a library. I mirrored the shapes and rules (BusinessMembership, AuthorizedReadScope, narrowReadScope semantics) here and flagged extracting a shared @accounter/auth package as a follow-up.
  • Where memberships come from. The GraphQL upstream client is Prompt 12, so Prompt 09 defaults to reading a memberships token claim (Auth0 configured to emit it) and keeps the source pluggable. This keeps the step self-contained and lets Prompt 11/12 wire a DB/GraphQL-backed resolver without another refactor.
  • Non-blocking wiring. The context is resolved and stored but not yet enforced — the smoke path is unchanged. Enforcement (deny with AUTHORIZATION_ERROR) is Prompt 11.

🤖 Generated with Claude Code


Generated by Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces identity mapping and tenant scoping to the MCP server package, mapping verified tokens to an internal user and business membership context (McpAuthContext) to enforce tenant isolation. The changes include parsing memberships from token claims, narrowing read scopes, and integrating this context resolution into the request handler. Feedback is provided to improve the robustness of the coerceMembership function by explicitly excluding arrays and refining roleId type coercion.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +117 to +128
function coerceMembership(entry: unknown): BusinessMembership | null {
if (!entry || typeof entry !== 'object') {
return null;
}
const record = entry as Record<string, unknown>;
const businessId = record.businessId ?? record.business_id;
if (typeof businessId !== 'string' || businessId.length === 0) {
return null;
}
const roleId = record.roleId ?? record.role_id ?? '';
return { businessId, roleId: typeof roleId === 'string' ? roleId : String(roleId) };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To improve robustness when parsing untrusted token claims, we should explicitly exclude arrays (since typeof [] === 'object') and avoid converting arbitrary objects or arrays to strings for roleId (only allow string or number, otherwise default to an empty string).

function coerceMembership(entry: unknown): BusinessMembership | null {
  if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
    return null;
  }
  const record = entry as Record<string, unknown>;
  const businessId = record.businessId ?? record.business_id;
  if (typeof businessId !== 'string' || businessId.length === 0) {
    return null;
  }
  const rawRoleId = record.roleId ?? record.role_id;
  const roleId =
    typeof rawRoleId === 'string'
      ? rawRoleId
      : typeof rawRoleId === 'number'
        ? String(rawRoleId)
        : '';
  return { businessId, roleId };
}

@gilgardosh
gilgardosh force-pushed the claude/mcp-prompt-08-token-verify branch from 8956f31 to e5706a6 Compare July 21, 2026 15:51
Base automatically changed from claude/mcp-prompt-08-token-verify to mcp-setup July 21, 2026 16:03
claude added 2 commits July 21, 2026 16:11
…t (Prompt 09)

Build an internal auth context from a verified token, mirroring the server's
tenant-isolation model (docs/mcp/spec.md §7).

- src/auth/identity.ts: McpAuthContext (userId, roles, memberships,
  defaultReadScope); readScopeFromMemberships + narrowReadScope +
  resolveRequestedReadScope (subset-only narrowing, reject out-of-scope);
  pluggable MembershipSource defaulting to a `memberships` token claim;
  IdentityMappingError for an unresolvable user; per-request context store
- handler: resolve and store the auth context after token verification
- unit tests for multi-business mapping, default/narrowed scope, claim parsing,
  and the missing-subject failure mode

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo
Address review: exclude arrays from object membership entries and only accept a
string/number roleId (never stringify arbitrary objects/arrays).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo
@gilgardosh
gilgardosh force-pushed the claude/mcp-prompt-09-identity-mapping branch from 743418f to 9080929 Compare July 21, 2026 16:12
@gilgardosh
gilgardosh temporarily deployed to accounter-fullstack July 21, 2026 16:12 — with GitHub Actions Inactive
@gilgardosh
gilgardosh temporarily deployed to accounter-fullstack July 21, 2026 16:12 — with GitHub Actions Inactive
@gilgardosh gilgardosh self-assigned this Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants