Skip to content

feat(mcp-server): resolve memberships from the server (MCP Prompt 08b)#4051

Merged
gilgardosh merged 2 commits into
mcp-setupfrom
claude/mcp-prompt-08b-upstream-memberships
Jul 26, 2026
Merged

feat(mcp-server): resolve memberships from the server (MCP Prompt 08b)#4051
gilgardosh merged 2 commits into
mcp-setupfrom
claude/mcp-prompt-08b-upstream-memberships

Conversation

@gilgardosh

Copy link
Copy Markdown
Collaborator

Summary

Implements Prompt 08b — MCP: resolve memberships from the server
(see docs/mcp/implementation-blueprint.md and docs/mcp/spec.md §6.4, §7.1).

The connector never derives business memberships from token claims. It resolves the caller's businesses by forwarding the user's bearer token to the Accounter GraphQL server and reading them back from the dedicated myMemberships query (added server-side in Prompt 08a, #4050). Because the connector holds a real end-user token, "authenticating to the server" is plain token pass-through — no Auth0 custom claim and no shared service secret.

Baseline note. 08b reuses the upstream client (createReadOperation / UpstreamGraphQLClient / getUpstreamClient) and the identity seam (resolveAuthContext, coerceMembership, membershipsFromClaims), none of which exist on mcp-setup/08a. This PR therefore targets claude/mcp-prompt-12-graphql-client (which stacks Prompts 09→12 on mcp-setup) — the earliest branch containing every seam it wires into. The diff below is only the 08b change.

Changes

  • upstream/memberships.ts (new) — an upstream MembershipSource:
    • MY_MEMBERSHIPS_QUERY + a typed, read-only createReadOperation wrapper that maps myMemberships rows to the internal BusinessMembership shape via the shared coerceMembership.
    • createUpstreamMembershipSource({ client, authorization, correlationId }) returns a MembershipSource that forwards the caller's Authorization header and correlation id. Identity is carried by the forwarded token, so the principal isn't consulted.
    • Error semantics: any upstream/auth/infra failure throws (surfaces as 401/5xx) so a failed lookup never degrades into a silent empty scope. A genuinely empty server result maps to []. A non-list payload violates the server's non-null [MyBusinessMembership!]! contract and is treated as an upstream error.
  • auth/identity.ts — export coerceMembership so the upstream source maps rows the same way the claims source does. The MembershipSource seam stays pluggable and membershipsFromClaims remains exported for tests.
  • mcp/handler.tsauthenticate now builds the upstream source (getUpstreamClient() + the request's Authorization header + correlation id) and passes it to resolveAuthContext, replacing the default claims source. The token is never logged. Scoped the route's header comment now that the auth path makes one upstream call.
  • Tests
    • upstream/__tests__/memberships.test.ts (new) — success maps rows to the internal shape (dropping businessName); forwards auth header + correlation id and issues myMemberships; empty result ⇒ []; malformed rows dropped; non-list ⇒ throws; upstream failure propagates.
    • mcp/__tests__/handler.test.ts — stub the membership source + upstream client accessor so the existing transport tests stay hermetic (no network / no upstream env required).

Precheck (per the prompt)

The forwarded token authenticates to the server only if the server accepts the MCP's Auth0 audience. This can't be exercised in CI (no live Auth0). Before enabling in a live environment, verify a real user token against an existing @requiresAuth server query; if the audiences differ, add a token-acquisition step (shared audience or Auth0 token-exchange) before wiring. Called out in the source docs.

Validation

  • yarn workspace @accounter/mcp-server build (tsc) — passes.
  • vitest --project unit packages/mcp-server150/150 pass.
  • eslint clean on the changed source files; prettier formatted.

🤖 Generated with Claude Code


Generated by Claude Code

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

Copilot AI 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.

⚠️ Not ready to approve

The new upstream membership mapper can throw an unsanitized TypeError if the upstream returns data: null, bypassing the intended UpstreamError semantics.

Pull request overview

Adds an upstream-backed membership resolution path to the MCP server so business memberships are resolved from the Accounter GraphQL server (myMemberships) via bearer-token pass-through, rather than derived from token claims, and wires that source into request authentication.

Changes:

  • Introduces an upstream MembershipSource (createUpstreamMembershipSource) that calls myMemberships and maps rows via the shared coerceMembership.
  • Updates MCP auth flow to use the upstream membership source during authenticate (forwarding Authorization + correlation id).
  • Adds unit tests for the upstream membership source and stubs upstream access in existing MCP transport tests to keep them hermetic.
File summaries
File Description
packages/mcp-server/src/upstream/memberships.ts New upstream membership source + myMemberships read operation wrapper and mapping logic.
packages/mcp-server/src/upstream/tests/memberships.test.ts New unit coverage for mapping, forwarding context, empty/malformed payloads, and error propagation.
packages/mcp-server/src/mcp/handler.ts Auth now builds and passes an upstream membership source to resolveAuthContext.
packages/mcp-server/src/mcp/tests/handler.test.ts Mocks upstream membership source/client accessor to avoid network/env coupling in transport tests.
packages/mcp-server/src/auth/identity.ts Exports coerceMembership for reuse by the upstream membership source.

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • Review effort level: Low

Note

Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/mcp-server/src/upstream/memberships.ts
Comment thread packages/mcp-server/src/upstream/memberships.ts
@gilgardosh
gilgardosh force-pushed the claude/mcp-prompt-12-graphql-client branch from 2806ce8 to f4dc13c Compare July 26, 2026 16:09
@gilgardosh
gilgardosh force-pushed the claude/mcp-prompt-08b-upstream-memberships branch from f12202f to 2dfb62b Compare July 26, 2026 16:11
@gilgardosh
gilgardosh temporarily deployed to accounter-fullstack July 26, 2026 16:11 — with GitHub Actions Inactive
@gilgardosh
gilgardosh temporarily deployed to accounter-fullstack July 26, 2026 16:11 — with GitHub Actions Inactive
@gilgardosh
gilgardosh force-pushed the claude/mcp-prompt-12-graphql-client branch from f4dc13c to 1068e23 Compare July 26, 2026 16:44
Base automatically changed from claude/mcp-prompt-12-graphql-client to mcp-setup July 26, 2026 16:48
claude added 2 commits July 26, 2026 19:49
Resolve the caller's business memberships by forwarding their bearer token to
the Accounter server's `myMemberships` query, and make that the wired
membership source — the connector no longer derives memberships from token
claims (spec §6.4, §7.1).

- Add an upstream `MembershipSource` (`createUpstreamMembershipSource`) that
  issues `myMemberships` via the existing `createReadOperation` +
  `UpstreamGraphQLClient`, threading the caller's Authorization header and
  correlation id, and maps rows through the shared `coerceMembership`.
- Wire it into `resolveAuthContext` in the MCP handler, replacing the default
  claims source. The `MembershipSource` seam stays pluggable and
  `membershipsFromClaims` remains exported for tests.
- Error semantics: any upstream/auth/infra failure throws (surfaces as 401/5xx)
  so a failed lookup never degrades into a silent empty scope; a genuinely
  empty server result maps to []. A non-list payload is treated as an upstream
  contract violation. The token is never logged.

Precheck note: the forwarded token authenticates to the server only if the
server accepts the MCP's Auth0 audience. If the audiences differ, a
token-acquisition step (shared audience or Auth0 token-exchange) is needed
before this is enabled in a live environment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo
…data (08b)

Address PR review feedback on Prompt 08b:

- The myMemberships query no longer selects businessName: the internal
  BusinessMembership shape keeps only { businessId, roleId }, so the field was
  fetched and immediately dropped.
- Guard against a non-object `data` (e.g. a misbehaving upstream returning
  `data: null`, which the client passes through since only `undefined` is "no
  data") before reading `data.myMemberships`, raising a sanitized UpstreamError
  instead of a raw TypeError.

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-08b-upstream-memberships branch from 2dfb62b to e1791c5 Compare July 26, 2026 16:49
@gilgardosh
gilgardosh temporarily deployed to accounter-fullstack July 26, 2026 16:50 — with GitHub Actions Inactive
@gilgardosh
gilgardosh temporarily deployed to accounter-fullstack July 26, 2026 16:50 — with GitHub Actions Inactive
@gilgardosh
gilgardosh merged commit 6a54f20 into mcp-setup Jul 26, 2026
8 checks passed
@gilgardosh
gilgardosh deleted the claude/mcp-prompt-08b-upstream-memberships branch July 26, 2026 17:01
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.

3 participants