MCP setup#3941
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the specification and implementation blueprint for the Accounter MCP Connector, establishing a clear, multi-phase plan to expose read-only capabilities to Claude clients. The review feedback highlights key areas for improvement, including propagating the user's bearer token to the upstream GraphQL client, reusing existing shared auth utilities for token verification, enforcing strict size and TTL limits on the in-process cache to prevent memory exhaustion, and aligning Auth0 environment variables with the existing server configuration.
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.
🚀 Snapshot Release (
|
| Package | Version | Info |
|---|---|---|
@accounter/client |
0.1.0-alpha-20260726170304-a8f28680bb8a1caff7ea293c8dd4b42714b2ae88 |
npm ↗︎ unpkg ↗︎ |
@accounter/gmail-listener |
0.1.3-alpha-20260726170304-a8f28680bb8a1caff7ea293c8dd4b42714b2ae88 |
npm ↗︎ unpkg ↗︎ |
@accounter/green-invoice-graphql |
0.8.7-alpha-20260726170304-a8f28680bb8a1caff7ea293c8dd4b42714b2ae88 |
npm ↗︎ unpkg ↗︎ |
@accounter/hashavshevet-mesh |
0.2.13-alpha-20260726170304-a8f28680bb8a1caff7ea293c8dd4b42714b2ae88 |
npm ↗︎ unpkg ↗︎ |
@accounter/israeli-vat-scraper |
0.1.13-alpha-20260726170304-a8f28680bb8a1caff7ea293c8dd4b42714b2ae88 |
npm ↗︎ unpkg ↗︎ |
@accounter/modern-poalim-scraper |
0.10.7-alpha-20260726170304-a8f28680bb8a1caff7ea293c8dd4b42714b2ae88 |
npm ↗︎ unpkg ↗︎ |
@accounter/payper-mesh |
0.2.13-alpha-20260726170304-a8f28680bb8a1caff7ea293c8dd4b42714b2ae88 |
npm ↗︎ unpkg ↗︎ |
@accounter/scraper-app |
0.0.3-alpha-20260726170304-a8f28680bb8a1caff7ea293c8dd4b42714b2ae88 |
npm ↗︎ unpkg ↗︎ |
@accounter/server |
0.2.0-alpha-20260726170304-a8f28680bb8a1caff7ea293c8dd4b42714b2ae88 |
npm ↗︎ unpkg ↗︎ |
@accounter/shaam-uniform-format-generator |
0.2.7-alpha-20260726170304-a8f28680bb8a1caff7ea293c8dd4b42714b2ae88 |
npm ↗︎ unpkg ↗︎ |
@accounter/shaam6111-generator |
0.1.9-alpha-20260726170304-a8f28680bb8a1caff7ea293c8dd4b42714b2ae88 |
npm ↗︎ unpkg ↗︎ |
… 06) (#3971) * feat(mcp-server): OAuth protected resource metadata route (Prompt 06) Serve RFC 9728 protected-resource metadata so Claude clients can discover the Auth0 authorization server (docs/mcp/spec.md §6.2). - src/oauth/metadata.ts: config-driven buildProtectedResourceMetadata() and protectedResourceMetadataUrl() helpers; well-known path constant - GET /.well-known/oauth-protected-resource returns { resource, authorization_servers, bearer_methods_supported: ["header"] } as JSON, with resource matching MCP_PUBLIC_BASE_URL exactly - unit tests for the document shape + route-level test through requestHandler Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo * test(mcp-server): use vi.stubEnv for the metadata route test Address review: stub env vars via vi.stubEnv/vi.unstubAllEnvs instead of mutating process.env directly, to avoid cross-test pollution. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo --------- Co-authored-by: Claude <noreply@anthropic.com>
… 07) (#3972) * feat(mcp-server): 401 challenge helper for auth discovery (Prompt 07) Add a standards-compliant unauthenticated response for MCP OAuth discovery (docs/mcp/spec.md §6.2, §10.1) and gate the MCP endpoint on token presence. - src/oauth/challenge.ts: buildWwwAuthenticateHeader() and sendUnauthorized() emitting 401 + WWW-Authenticate: Bearer with a resource_metadata pointer to the well-known document (transport-level, never a JSON-RPC/tool error) - handler.ts: hasBearerToken() presence check; POST /mcp without a bearer token now returns the 401 challenge (token validity verified in Prompt 08) - tests for the header builder, sendUnauthorized, the presence check, and the 401 path through mcpHttpHandler Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo * fix(mcp-server): RFC-compliant WWW-Authenticate header Address review: - only emit error_description alongside error (RFC 6750 §3) - escape " and \ in quoted-string parameter values (RFC 7235) - use vi.stubEnv in the 401 handler test instead of mutating process.env Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo --------- Co-authored-by: Claude <noreply@anthropic.com>
…rompt 08) (#3973) * feat(mcp-server): bearer token extraction and Auth0 verification (Prompt 08) Verify Auth0 access tokens on the MCP endpoint (docs/mcp/spec.md §6.4, §8/§10.1), reusing the server package's jose-based JWKS approach. - src/auth/token.ts: extractBearerToken(); verifyAccessTokenWithKey() enforcing issuer/audience/signature/expiry and mapping claims to a typed AuthPrincipal (subject, scopes from scope+permissions, email); TokenVerificationError carries no token material; per-request principal store - src/auth/verifier.ts: env-backed verifyAccessToken() using a cached remote JWKS (createRemoteJWKSet) keyed by JWKS URL - handler.ts: POST /mcp now extracts + verifies the token; invalid/expired -> 401 with error="invalid_token"; missing -> 401 challenge; failures logged by reason only (never the token) - add jose dependency; unit tests for extraction, claim mapping, and valid/wrong-issuer/wrong-audience/expired/wrong-key verification Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo * fix(mcp-server): don't mask JWKS/infra failures as 401; tidy principal types Address review: - verifyAccessTokenWithKey only wraps genuine token-validation errors (by jose error code) into TokenVerificationError; infrastructure failures (e.g. a JWKS fetch timeout) propagate so the request surfaces as a 5xx instead of a misleading 401 - handler.authenticate rethrows non-TokenVerificationError so those become 500 - use the AuthPrincipal type directly in the import and authenticate() return type instead of Awaited<ReturnType<...>> - tests: infra error propagation at both the verifier and handler layers Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo * prettier --------- Co-authored-by: Claude <noreply@anthropic.com>
…a) (#4050) * feat(server): expose caller's own business memberships (MCP Prompt 08a) Add a `myMemberships` GraphQL query to the auth module that returns the authenticated caller's own business memberships. Gated with `@requiresAuth` only (never `@requiresRole`, never dependent on a selected business scope) so the MCP connector can read a user's businesses before any scope is chosen. The resolver reuses the memberships already resolved on the request auth context (`AuthContextProvider.getAuthContext()`) — no new DB query. An authenticated user with no memberships returns an empty list, not an error. The `MyBusinessMembership` type is prefixed to avoid colliding with the existing `common` module `BusinessMembership` type, and uses `roleId` to map cleanly onto the MCP connector's internal membership shape (Prompt 08b). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo * fix(server): role-gate myMemberships and give MyBusinessMembership an id - Gate `myMemberships` with `@requiresAnyRole(["business_owner", "accountant"])` instead of leaving it authenticated-only. - Add a required `id: ID!` to `MyBusinessMembership` (composite of the caller's user id and the business id) and drop the strict-id-in-types eslint workaround. - Populate the composite `id` in the resolver and update the doc comment. - Strengthen the error-wrapping test to assert the wrapper's message, `extensions.code`, and original error, and add a test that GraphQLErrors from the auth context are rethrown unchanged (addresses PR review comment). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dkv1EjQ1ygKUFrmEWUhS5p --------- Co-authored-by: Claude <noreply@anthropic.com>
…aims (#4046) * docs(mcp): resolve business memberships from the server, not token claims An Auth0 access token carries identity only (sub/email); it does not contain a user's Accounter business memberships or roles. Those live in the server DB (business_users, keyed by auth0_user_id) and are resolved per request. The blueprint/spec previously assumed memberships arrive as a token claim, which would resolve to an empty scope for every real caller. Correct the docs to describe resolving memberships from the Accounter server by forwarding the caller's bearer token and reading a new `myMemberships` query — much simpler than email-ingestion-gateway's control-plane token, since the MCP holds a real end-user token. - spec.md: rewrite §6.4 (token = identity only), add the resolution mechanism to §7.1, clarify §7.3 narrows server-resolved memberships, and fix the §12.2 phase step wording. - implementation-blueprint.md: insert prerequisite Prompt 08a (server `myMemberships` query) and Prompt 08b (MCP upstream membership source) between prompts 08 and 09 (lettered to avoid renumbering already-branched prompts); reframe Prompt 09 to consume the upstream source; update the workstream/slice/ step-list references and the progress note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo * docs(mcp): address review comments on membership resolution spec (#4052) - spec.md §6.4: document email as an optional claim (sub plus optional email/email_verified) to match the server's JWT parsing, which coerces a missing email to null - blueprint Prompt 08b: describe token forwarding as a requirement of the new MembershipSource rather than existing behavior (the handler makes no upstream calls yet) - blueprint Prompt 08b: introduce the upstream GraphQL client + helpers (graphql-client.ts, getUpstreamClient, coerceMembership, resolveAuthContext) as part of the prompt instead of assuming they already exist Claude-Session: https://claude.ai/code/session_017MqCVWqb56h8vjw8uyZ4rD Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
…4010) * feat(mcp-server): map verified identity to business membership context (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 * fix(mcp-server): harden membership claim coercion 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 * fix(mcp-server): reject malformed membership roles; 401 on identity mapping failure Address PR review feedback on Prompt 09: - coerceMembership now rejects a membership entry whose role is present but of a non-primitive type (object/array/boolean) instead of coercing it to '' — a malformed role could otherwise still contribute its businessId to the derived read scope. A missing role is still allowed (empty role). - The MCP handler now treats IdentityMappingError (e.g. a verified token with no subject) as an invalid_token 401 rather than letting it surface as a 5xx; genuine infrastructure errors still propagate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo --------- Co-authored-by: Claude <noreply@anthropic.com>
…t 10) (#4011) * feat(mcp-server): curated tool registry and input validation (Prompt 10) Add the production-ready tool registry abstraction (docs/mcp/spec.md §8.3). - src/tools/registry.ts: ToolDefinition (name, description, strict zod input schema, auth policy, pure handler); ToolRegistry with register/get/has/list and duplicate-name rejection; describe() renders input schemas to JSON Schema (additionalProperties:false) for tools/list - validateToolInput(): strict parsing with unknown-field rejection and a deterministic VALIDATION_ERROR payload (code, message, field-path issues) - ToolAuthPolicy / DataClassification / ToolExecutionContext contracts for the policy evaluator and handlers in later steps - unit tests for registration, duplicates, ordering, describe(), and schema validation (valid, missing required, unknown field, type mismatch) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo * fix(mcp-server): honor MCP kill-switch; harden tools/call params & schema strictness Address PR review feedback on Prompt 10: - MCP_ENABLED=0 now actually disables the connector: the MCP transport (`/mcp`) and its OAuth protected-resource-metadata discovery route return 404 (health only), instead of being served while the logs claim "disabled mode". - `tools/call` returns a clear InvalidParams ("params must be an object") when `params` is an array or other non-object, instead of a misleading "Unknown tool: undefined". - `registry.describe()` renders JSON Schema from the same `.strict()` schema that `validateToolInput` enforces, so the advertised `additionalProperties: false` provably matches the runtime unknown-field rejection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo --------- Co-authored-by: Claude <noreply@anthropic.com>
…#4012) Enforce required roles and business-scope constraints before tool execution (docs/mcp/spec.md §7). - src/tools/policy.ts: evaluateToolPolicy() checks required roles (any-of) and resolves the request's read scope as a subset of the caller's memberships, denying out-of-scope requests and empty scope (when required) with a deterministic AUTHORIZATION_ERROR; authorizeToolCall() convenience for a registered tool - unit tests for allow, role deny, out-of-scope deny, empty-scope deny, subset narrowing, any-of roles, and no-role-gate Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo Co-authored-by: Claude <noreply@anthropic.com>
…4013) * feat(mcp-server): hardened upstream GraphQL client (Prompt 12) Add the shared read-only upstream GraphQL client used by tool handlers (docs/mcp/spec.md §5.3, §10.3, §10.4). - src/upstream/graphql-client.ts: UpstreamGraphQLClient with strict timeout + cancellation (AbortController), bounded retries for idempotent read failures only (network/timeout/5xx; never 4xx or GraphQL errors), correlation-id and Authorization header propagation (token never logged), and sanitized UpstreamError (UPSTREAM_ERROR/TIMEOUT_ERROR, no internal details) - read-only guard refuses mutations/subscriptions; createReadOperation exposes only typed read wrappers to tools (no generic execute surface) - src/upstream/default-client.ts: env-backed, memoized client factory - unit tests for success/header propagation, read-only guard, timeout, retry eligibility (503 retried, 400/401 not, exhaustion), and error sanitization Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo * fix(mcp-server): harden upstream client against malformed input Address review: - read-only guard rejects mutation/subscription anywhere in the document (not just leading), preventing a multi-operation + operationName bypass - sanitizeGraphQLErrors tolerates null/non-object entries in the errors array - wrap response.json() and guard the body shape so a non-JSON/invalid response becomes a sanitized UpstreamError instead of a raw SyntaxError/TypeError Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo * fix(mcp-server): cap upstream error strings; extend timeout over body read Address PR review feedback on Prompt 12: - sanitizeGraphQLErrors now normalizes whitespace and caps each message (200 chars) and the combined string (500 chars), so a verbose/multi-line upstream error can neither bloat logs nor smuggle internal detail into the summary. - The request timeout budget now spans the entire exchange — obtaining the response AND consuming its body — instead of being cleared right after headers arrive. An abort raised while reading the body maps to a retryable TIMEOUT_ERROR rather than being misclassified as a non-JSON response. - Clarify the UpstreamError doc: its message carries no upstream internals; the local JS stack is our own call site. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo --------- Co-authored-by: Claude <noreply@anthropic.com>
#4051) * feat(mcp-server): resolve memberships from the server (MCP Prompt 08b) 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 * fix(mcp-server): drop unused businessName; guard non-object upstream 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
No description provided.