Skip to content

feat(mcp-server): hardened upstream GraphQL client (MCP Prompt 12)#4013

Open
gilgardosh wants to merge 2 commits into
claude/mcp-prompt-11-policy-evaluatorfrom
claude/mcp-prompt-12-graphql-client
Open

feat(mcp-server): hardened upstream GraphQL client (MCP Prompt 12)#4013
gilgardosh wants to merge 2 commits into
claude/mcp-prompt-11-policy-evaluatorfrom
claude/mcp-prompt-12-graphql-client

Conversation

@gilgardosh

Copy link
Copy Markdown
Collaborator

Summary

Implements Prompt 12 – GraphQL upstream client with timeout/retry guardrails
(see docs/mcp/spec.md §5.3, §10.3, §10.4). Adds
the single shared client that tool handlers use to reach the Accounter GraphQL
server.

Stacked PR: targets claude/mcp-prompt-11-policy-evaluator (Prompt 11, #4012).
Review/merge Prompts 05→11 first; this diff shows only the Prompt 12 changes.

Changes

  • src/upstream/graphql-client.tsUpstreamGraphQLClient:
    • Timeout + cancellation via AbortController (a per-call budget); an aborted request maps to a retryable TIMEOUT_ERROR.
    • Bounded retries for idempotent reads only — network errors, timeouts, and 5xx are retried up to maxRetries (default 2); 4xx (auth/validation) and GraphQL-level errors are never retried.
    • Header propagationX-Correlation-Id and the caller's Authorization bearer token are forwarded; the raw token is never logged.
    • Sanitized errorsUpstreamError (UPSTREAM_ERROR / TIMEOUT_ERROR, retryable flag) with business-safe messages only (GraphQL error messages collapsed, capped; no stack traces / SQL).
    • Read-only guard — mutations/subscriptions are refused (phase-1 non-goal). createReadOperation is the only surface tools use — there is no generic execute-anything API.
  • src/upstream/default-client.ts — env-backed, memoized client factory (endpoint + timeout from config).
  • Tests — success + header propagation (correlation id & Authorization, omitted when absent), read-only guard, timeout mapping, retry eligibility (503 retried then success, 400/401 not retried, exhaustion on persistent 5xx), GraphQL-error sanitization, and a createReadOperation round-trip. 140 tests total.

Validation

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

Notes / open decisions

  • Authorization forwarding. The prompt asks to forward the caller's bearer token upstream. The client accepts it via UpstreamRequestContext.authorization; populating it from the authenticated request (the token captured at verification time) is wired when the first real tool executes (Prompt 13). Forwarding the user's own token means upstream re-applies the exact same identity/tenant checks — defense in depth over the connector's own policy evaluator.
  • Retry backoff. Retries are immediate (no delay) to stay within the tight tool-latency budget; if we want jittered backoff, that's a small follow-up. Retries are capped and only for idempotent reads, so this is safe.
  • fetch-based (Node global) with an injectable fetchImpl for hermetic tests — no graphql-request dependency, keeping full control over timeout/retry/headers.

🤖 Generated with Claude Code


Generated by Claude Code

@gilgardosh
gilgardosh temporarily deployed to accounter-fullstack July 21, 2026 15:24 — with GitHub Actions Inactive
@gilgardosh
gilgardosh temporarily deployed to accounter-fullstack July 21, 2026 15:24 — with GitHub Actions Inactive

@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 a hardened, read-only upstream GraphQL client (UpstreamGraphQLClient) with built-in timeouts, bounded retries, header propagation, and error sanitization, along with comprehensive unit tests and a memoized default client instance. The review feedback highlights critical security and robustness improvements: addressing a potential bypass in the read-only query validation regex, adding defensive checks in sanitizeGraphQLErrors to prevent TypeErrors on malformed error arrays, and wrapping response.json() in a try-catch block to handle non-JSON responses gracefully.

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 thread packages/mcp-server/src/upstream/graphql-client.ts Outdated
Comment on lines +70 to +77
function sanitizeGraphQLErrors(errors: Array<{ message?: unknown }>): string {
const messages = errors
.map(error => (typeof error.message === 'string' ? error.message : ''))
.map(message => message.trim())
.filter(Boolean)
.slice(0, 3);
return messages.length > 0 ? messages.join('; ') : 'Upstream GraphQL error';
}

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

If any element in the errors array is null or not an object, accessing error.message will throw a raw TypeError, bypassing the error sanitization layer. Adding a safety check ensures robustness against malformed upstream error payloads.

Suggested change
function sanitizeGraphQLErrors(errors: Array<{ message?: unknown }>): string {
const messages = errors
.map(error => (typeof error.message === 'string' ? error.message : ''))
.map(message => message.trim())
.filter(Boolean)
.slice(0, 3);
return messages.length > 0 ? messages.join('; ') : 'Upstream GraphQL error';
}
function sanitizeGraphQLErrors(errors: Array<{ message?: unknown }>): string {
const messages = errors
.map(error => (error && typeof error === 'object' && typeof error.message === 'string' ? error.message : ''))
.map(message => message.trim())
.filter(Boolean)
.slice(0, 3);
return messages.length > 0 ? messages.join('; ') : 'Upstream GraphQL error';
}

Comment thread packages/mcp-server/src/upstream/graphql-client.ts Outdated
claude added 2 commits July 21, 2026 16:13
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
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
@gilgardosh
gilgardosh force-pushed the claude/mcp-prompt-12-graphql-client branch from e95d30d to 2806ce8 Compare July 21, 2026 16:14
@gilgardosh
gilgardosh temporarily deployed to accounter-fullstack July 21, 2026 16:14 — with GitHub Actions Inactive
@gilgardosh
gilgardosh temporarily deployed to accounter-fullstack July 21, 2026 16:15 — 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