Skip to content

Add GitHub GraphQL capability and docs - #20

Merged
kentcdodds merged 4 commits into
mainfrom
cursor/github-graphql-support-cec2
Mar 20, 2026
Merged

kentcdodds merged 4 commits into
mainfrom
cursor/github-graphql-support-cec2

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Mar 20, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • add GitHub GraphQL client + coding capabilities (graphql + docs)
  • extend GitHub mock to serve GraphQL responses
  • document GraphQL support in setup/env docs

Testing

  • bun test ./mock-servers/github ./mcp/github ./mcp/capabilities/coding
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Added GitHub GraphQL support and a read-only GraphQL docs fetch capability plus a GraphQL client.
  • Documentation

    • Updated setup and environment docs and example: local dev now provides mock API URL+token for both GitHub REST and GraphQL; clarified GraphQL endpoint construction and token behavior.
  • Tests

    • Added tests and expanded mock coverage for GraphQL requests against the local mock server.
  • Refactor

    • Centralized GitHub docs path validation for REST/GraphQL doc fetches.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Mar 20, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds GitHub GraphQL support across the stack: new GraphQL client, two GraphQL domain capabilities (query + docs), mock server GraphQL endpoint and tests, documentation updates clarifying REST + GraphQL coverage, and minor export changes.

Changes

Cohort / File(s) Summary
Documentation
\.env.example, docs/agents/mock-api-servers.md, docs/agents/setup.md, docs/environment-variables.md, docs/setup-manifest.md
Clarified that local dev (bun run dev) provides mock URL/token for both github_rest and github_graphql; updated GITHUB_API_BASE_URL/GraphQL endpoint guidance and GITHUB_TOKEN behavior.
GraphQL Client
mcp/github/github-graphql-client.ts, mcp/github/github-graphql-client.test.ts
Added GitHubGraphqlClient with types, JSON parsing and error handling, createGitHubGraphqlClient, and integration tests against the local GraphQL mock.
Capabilities — GraphQL
mcp/capabilities/coding/github-graphql.ts, mcp/capabilities/coding/github-graphql-api-docs.ts, mcp/capabilities/coding/index.ts
Introduced github_graphql capability (query execution + response normalization) and github_graphql_api_docs capability (docs fetch with path validation); re-exported both.
Domain & Tests
mcp/capabilities/coding/domain.ts, mcp/capabilities/coding/coding-capabilities.test.ts
Added GraphQL capabilities to codingDomain and updated description; added tests validating successful GraphQL responses and validation failure on empty query.
Mock Server
mock-servers/github/worker.ts, mock-servers/github/github-mock.test.ts
Renamed mock metadata/title to "GitHub API", added supportsGraphql: true, implemented POST /graphql handling with fixture responses and error cases, and extended tests to assert GraphQL behavior.
Docs Path Validation
mcp/capabilities/coding/github-docs-path.ts, mcp/capabilities/coding/github-rest-api-docs.ts
Added shared assertGithubDocsPath validator and replaced local REST docs path checks to reuse it.
Misc — Rest Client Export
mcp/github/github-rest-client.ts
Exported previously internal formatRateLimitHint helper (no behavior change).

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Capability as GitHubGraphqlCapability
    participant GQLClient as GitHubGraphqlClient
    participant MockServer as Mock GitHub Server

    Client->>Capability: handler({ query, variables })
    Capability->>GQLClient: create via env (token, baseUrl)
    Capability->>GQLClient: query({ query, variables, operationName })
    GQLClient->>MockServer: POST /graphql (headers + body)
    MockServer->>MockServer: parse & validate request
    MockServer-->>GQLClient: 200 { data, errors?, extensions? } or 400/unsupported
    GQLClient->>GQLClient: normalizeGraphqlResponse()
    GQLClient-->>Capability: { status, data, errors, extensions }
    Capability-->>Client: return normalized response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I nibble on a GraphQL thread,

Queries hum and docs are read,
Mocks stand ready, tokens bright,
REST and Graph together light,
Tiny hops, big code ahead!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add GitHub GraphQL capability and docs' directly summarizes the main changes: adding GitHub GraphQL support (new client, capabilities, mock) and updating documentation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/github-graphql-support-cec2
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kentcdodds
kentcdodds marked this pull request as ready for review March 20, 2026 02:39
@github-actions

github-actions Bot commented Mar 20, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-20.kentcdodds.workers.dev

Worker: kody-pr-20
D1: kody-pr-20-db
KV: kody-pr-20-oauth-kv

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
mcp/github/github-graphql-client.test.ts (1)

15-32: Avoid buffering unused process output.

captureOutput stores all output in-memory, but callers discard the getter. This creates unnecessary unbounded memory growth risk in tests.

♻️ Proposed fix
-function captureOutput(stream: ReadableStream<Uint8Array> | null) {
-	let output = ''
-	if (!stream) return () => output
+function captureOutput(stream: ReadableStream<Uint8Array> | null) {
+	if (!stream) return
 	const reader = stream.getReader()
 	const decoder = new TextDecoder()
 	void (async () => {
 		try {
 			while (true) {
 				const { value, done } = await reader.read()
 				if (done) break
-				if (value) output += decoder.decode(value)
+				if (value) void decoder.decode(value)
 			}
 		} catch {
 			// ignore
 		}
 	})()
-	return () => output
 }
@@
-	void captureOutput(proc.stdout)
-	void captureOutput(proc.stderr)
+	captureOutput(proc.stdout)
+	captureOutput(proc.stderr)

Also applies to: 80-81

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/github/github-graphql-client.test.ts` around lines 15 - 32, captureOutput
currently accumulates all stream data into memory even when callers ignore the
returned getter, causing unbounded buffering; update captureOutput (and the
similar code at lines noted) so it only buffers when explicitly requested: add a
boolean parameter like shouldBuffer (default false) or change the function to
return a no-op getter by default and only create the reader/decoder/async loop
when shouldBuffer is true or a buffering mode is requested; ensure symbols
referenced are captureOutput, reader.getReader(), and decoder.decode() so you
only instantiate the reader/decoder and start the async reader loop when
buffering is enabled to avoid unnecessary memory growth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mcp/capabilities/coding/github-graphql.ts`:
- Around line 76-85: The normalizer currently drops non-GraphQL JSON error
payloads by returning errors: null when response.body contains a top-level
message (e.g., { "message": "Bad credentials" }); update the logic around the
destructured response.body (data, errors, extensions) to detect when errors is
null/undefined and response.body.message is present and, in that case, populate
errors with a single-element array like [{ message: response.body.message }] (or
otherwise surface the message in the returned object), while preserving status,
data (null if absent), and extensions; modify the block that throws on non-array
errors to still handle non-GraphQL payloads and return the constructed errors
array instead of dropping the message.
- Around line 8-10: The Zod schema for the GraphQL request allows
whitespace-only values because .min(1) runs before trimming; update the schema
fields named query and operationName in github-graphql.ts to call .trim() before
.min(1) (e.g., replace z.string().min(1) with z.string().trim().min(1)) so
whitespace-only inputs fail validation rather than being trimmed to empty at
runtime.

---

Nitpick comments:
In `@mcp/github/github-graphql-client.test.ts`:
- Around line 15-32: captureOutput currently accumulates all stream data into
memory even when callers ignore the returned getter, causing unbounded
buffering; update captureOutput (and the similar code at lines noted) so it only
buffers when explicitly requested: add a boolean parameter like shouldBuffer
(default false) or change the function to return a no-op getter by default and
only create the reader/decoder/async loop when shouldBuffer is true or a
buffering mode is requested; ensure symbols referenced are captureOutput,
reader.getReader(), and decoder.decode() so you only instantiate the
reader/decoder and start the async reader loop when buffering is enabled to
avoid unnecessary memory growth.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8ba33628-94bc-4cc2-a8de-df90dbeae19e

📥 Commits

Reviewing files that changed from the base of the PR and between d50417b and e61d912.

📒 Files selected for processing (14)
  • .env.example
  • docs/agents/mock-api-servers.md
  • docs/agents/setup.md
  • docs/environment-variables.md
  • docs/setup-manifest.md
  • mcp/capabilities/coding/coding-capabilities.test.ts
  • mcp/capabilities/coding/domain.ts
  • mcp/capabilities/coding/github-graphql-api-docs.ts
  • mcp/capabilities/coding/github-graphql.ts
  • mcp/capabilities/coding/index.ts
  • mcp/github/github-graphql-client.test.ts
  • mcp/github/github-graphql-client.ts
  • mock-servers/github/github-mock.test.ts
  • mock-servers/github/worker.ts

Comment on lines +8 to +10
query: z
.string()
.min(1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In Zod 4, does z.string().min(1)accept whitespace-only strings unless.trim()is applied before.min(1)?

💡 Result:

Yes, in Zod 4, z.string.min(1) accepts whitespace-only strings like " " because it checks the string length after any prior transforms, and without .trim, the length of " " is 1, which meets the minimum. To reject whitespace-only strings, apply .trim before .min(1), as in z.string.trim.min(1), which trims whitespace first, making " ".length become 0 and fail the check.

Citations:


🏁 Script executed:

cat -n mcp/capabilities/coding/github-graphql.ts | sed -n '1,70p'

Repository: kentcdodds/kody

Length of output: 2554


Whitespace-only query and operationName bypass schema validation.

query: ' ' and operationName: ' ' pass .min(1) checks because they have length ≥ 1, then get trimmed to '' at runtime (lines 56, 58) instead of failing fast during validation.

Apply .trim() before .min(1) in the schema:

Proposed fix
 const inputSchema = z.object({
 	query: z
 		.string()
+		.trim()
 		.min(1)
 		.describe(
 			'GraphQL query or mutation string to send to https://api.github.com/graphql.',
 		),
 	variables: z
 		.record(z.string(), z.unknown())
 		.optional()
 		.describe('Optional GraphQL variables object (JSON).'),
 	operationName: z
 		.string()
+		.trim()
 		.min(1)
 		.optional()
 		.describe('Optional GraphQL operation name.'),
 })
 const response = await client.query({
-	query: args.query.trim(),
+	query: args.query,
 	variables: args.variables,
-	operationName: args.operationName?.trim() || undefined,
+	operationName: args.operationName || undefined,
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/capabilities/coding/github-graphql.ts` around lines 8 - 10, The Zod
schema for the GraphQL request allows whitespace-only values because .min(1)
runs before trimming; update the schema fields named query and operationName in
github-graphql.ts to call .trim() before .min(1) (e.g., replace
z.string().min(1) with z.string().trim().min(1)) so whitespace-only inputs fail
validation rather than being trimmed to empty at runtime.

Comment on lines +76 to +85
const { data, errors, extensions } = response.body
if (errors != null && !Array.isArray(errors)) {
throw new Error('GitHub GraphQL response "errors" is not an array.')
}
return {
status: response.status,
data: data ?? null,
errors: errors ?? null,
extensions: extensions ?? null,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For GitHub's GraphQL endpoint, what JSON body is returned on authentication failures (e.g., bad token)? Does it return messagewithout a GraphQLerrors array?

💡 Result:

For GitHub's GraphQL endpoint (https://api.github.com/graphql), authentication failures (e.g., bad or missing token) return an HTTP 401 Unauthorized response with a JSON body containing a top-level message field (e.g., "This endpoint requires you to be authenticated." or "Bad credentials") and a documentation_url, but no GraphQL errors array or data field. This is a standard GitHub API error format used before GraphQL processing, causing GraphQL clients to reject it as invalid (lacking data or errors). Examples from sources: - {"message": "This endpoint requires you to be authenticated.", "documentation_url": "https://docs.github.com/v3/#authentication"} - {"message": "Bad credentials", "documentation_url": "https://docs.github.com/rest"}

Citations:


🏁 Script executed:

# First, let's locate and read the GitHub GraphQL file to understand the full context
find . -name "github-graphql.ts" -type f

Repository: kentcdodds/kody

Length of output: 102


🏁 Script executed:

# Read the file around the specified lines (76-85) to see the full context
if [ -f "./mcp/capabilities/coding/github-graphql.ts" ]; then
  echo "=== File found, reading context (lines 60-100) ==="
  cat -n ./mcp/capabilities/coding/github-graphql.ts | sed -n '60,100p'
else
  echo "File not found at expected path, searching for it..."
  git ls-files | grep -i "github-graphql"
fi

Repository: kentcdodds/kody

Length of output: 986


Non-GraphQL JSON error payloads are being dropped.

If GitHub responds with a JSON body like { "message": "Bad credentials" } (which it does on authentication failures), the normalizer returns errors: null, losing the actionable error message. GitHub's GraphQL endpoint returns HTTP 401 responses with a top-level message field when authentication fails, but no GraphQL errors array.

🛠️ Proposed fix
 	const { data, errors, extensions } = response.body
 	if (errors != null && !Array.isArray(errors)) {
 		throw new Error('GitHub GraphQL response "errors" is not an array.')
 	}
+	const fallbackMessage =
+		typeof (response.body as { message?: unknown }).message === 'string'
+			? (response.body as { message: string }).message
+			: null
 	return {
 		status: response.status,
 		data: data ?? null,
-		errors: errors ?? null,
+		errors: errors ?? (fallbackMessage ? [{ message: fallbackMessage }] : null),
 		extensions: extensions ?? null,
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data, errors, extensions } = response.body
if (errors != null && !Array.isArray(errors)) {
throw new Error('GitHub GraphQL response "errors" is not an array.')
}
return {
status: response.status,
data: data ?? null,
errors: errors ?? null,
extensions: extensions ?? null,
}
const { data, errors, extensions } = response.body
if (errors != null && !Array.isArray(errors)) {
throw new Error('GitHub GraphQL response "errors" is not an array.')
}
const fallbackMessage =
typeof (response.body as { message?: unknown }).message === 'string'
? (response.body as { message: string }).message
: null
return {
status: response.status,
data: data ?? null,
errors: errors ?? (fallbackMessage ? [{ message: fallbackMessage }] : null),
extensions: extensions ?? null,
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/capabilities/coding/github-graphql.ts` around lines 76 - 85, The
normalizer currently drops non-GraphQL JSON error payloads by returning errors:
null when response.body contains a top-level message (e.g., { "message": "Bad
credentials" }); update the logic around the destructured response.body (data,
errors, extensions) to detect when errors is null/undefined and
response.body.message is present and, in that case, populate errors with a
single-element array like [{ message: response.body.message }] (or otherwise
surface the message in the returned object), while preserving status, data (null
if absent), and extensions; modify the block that throws on non-array errors to
still handle non-GraphQL payloads and return the constructed errors array
instead of dropping the message.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Empty base URL falls through to broken /graphql endpoint
    • Added the same fallback default as the REST client so empty base URLs resolve to the GitHub API host.
  • ✅ Fixed: Duplicated formatRateLimitHint across GitHub client files
    • Exported the shared helper from the REST client and reused it in the GraphQL client to remove duplication.

Comment thread mcp/github/github-graphql-client.ts
Comment thread mcp/github/github-graphql-client.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Duplicated docs-path validation logic across capabilities
    • Extracted shared assertGithubDocsPath helper and reused it for REST and GraphQL validations to remove duplicated logic.

Comment thread mcp/capabilities/coding/github-graphql-api-docs.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
mcp/capabilities/coding/github-graphql-api-docs.ts (1)

8-8: Consider extracting shared constants and schemas.

GITHUB_DOCS_ORIGIN, inputSchema, and outputSchema are duplicated between this file and github-rest-api-docs.ts. You could extract these to a shared module (e.g., github-docs-shared.ts) to reduce duplication.

♻️ Example shared module structure
// github-docs-shared.ts
import { z } from 'zod'

export const GITHUB_DOCS_ORIGIN = 'https://docs.github.com'

export const createDocsInputSchema = (apiType: 'REST' | 'GraphQL', example: string) =>
  z.object({
    path: z
      .string()
      .min(1)
      .describe(
        `Path on docs.github.com for ${apiType} reference pages, starting with \`/{locale}/${apiType.toLowerCase()}/\` (no host). Example: \`${example}\`.`,
      ),
  })

export const docsOutputSchema = z.object({
  status: z.number().describe('HTTP status from docs.github.com.'),
  contentType: z.string().nullable().describe('Response Content-Type header, if present.'),
  markdownTokenEstimate: z.string().nullable().describe('Optional x-markdown-tokens header when the response is markdown.'),
  body: z.string().describe('Response body (typically markdown when Content-Type is text/markdown).'),
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/capabilities/coding/github-graphql-api-docs.ts` at line 8, Extract the
duplicated GITHUB_DOCS_ORIGIN, inputSchema, and outputSchema into a new shared
module (e.g., github-docs-shared) and import them from both
github-graphql-api-docs.ts and github-rest-api-docs.ts; specifically move the
constant GITHUB_DOCS_ORIGIN and the two Zod schemas (currently named inputSchema
and outputSchema) into the shared file, expose a helper like
createDocsInputSchema(apiType, example) if you need API-specific descriptions,
update both files to import GITHUB_DOCS_ORIGIN, the shared input schema (or
factory) and docsOutputSchema, and remove the duplicated definitions from each
file to eliminate redundancy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@mcp/capabilities/coding/github-graphql-api-docs.ts`:
- Line 8: Extract the duplicated GITHUB_DOCS_ORIGIN, inputSchema, and
outputSchema into a new shared module (e.g., github-docs-shared) and import them
from both github-graphql-api-docs.ts and github-rest-api-docs.ts; specifically
move the constant GITHUB_DOCS_ORIGIN and the two Zod schemas (currently named
inputSchema and outputSchema) into the shared file, expose a helper like
createDocsInputSchema(apiType, example) if you need API-specific descriptions,
update both files to import GITHUB_DOCS_ORIGIN, the shared input schema (or
factory) and docsOutputSchema, and remove the duplicated definitions from each
file to eliminate redundancy.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3ee3ae66-9277-4727-838c-9eb25e59ad44

📥 Commits

Reviewing files that changed from the base of the PR and between cdfea67 and f3dd5e9.

📒 Files selected for processing (3)
  • mcp/capabilities/coding/github-docs-path.ts
  • mcp/capabilities/coding/github-graphql-api-docs.ts
  • mcp/capabilities/coding/github-rest-api-docs.ts

@kentcdodds
kentcdodds merged commit 589938f into main Mar 20, 2026
9 checks passed
@kentcdodds
kentcdodds deleted the cursor/github-graphql-support-cec2 branch March 20, 2026 13:29
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