Skip to content

Preserve versioned STDIO API user agents across MCP protocols - #3323

Merged
SamMorrowDrums merged 3 commits into
mainfrom
jidicula/graphql-user-agent-mcp-stdio
Sep 30, 2026
Merged

SamMorrowDrums merged 3 commits into
mainfrom
jidicula/graphql-user-agent-mcp-stdio

Conversation

@jidicula

@jidicula jidicula commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Give STDIO GitHub API requests a server/version user agent before any MCP handshake, retaining upstream client metadata per request across legacy and modern MCP protocols. Resolve usable build metadata for version-specific attribution without making missing or malformed metadata prevent startup.

Why

The pinned MCP SDK permits discovery and direct metadata-bearing tool calls without initialize, so initialisation-only middleware misses those requests. Reading ClientInfo() per request also avoids mutating a shared transport when client identity changes.

What changed

  • Set a default versioned GraphQL user agent and apply escaped upstream client metadata through request context. Preserve existing remote-server defaults, insiders markers, authentication and GraphQL feature headers.
  • Select valid version metadata in order: explicit release, explicit source revision, embedded VCS revision, then installed main-module version. Use vcs-<full-sha> for revisions, applying -dirty only to embedded VCS metadata. If nothing is usable, start with dev and warn on stderr, keeping stdout available for MCP.
  • Cover protocol negotiation, direct calls, concurrent transport isolation and version precedence/fallbacks. Document the version contract and whole-package build command.

MCP impact

  • No tool or API changes: schemas, tool results and permissions are unchanged. The additive exported transport-context helper preserves existing defaults.
  • Tool schema or behaviour changed
  • New tool added

Prompts tested (tool changes only)

Not applicable; tool definitions and schemas are unchanged.

Security / limits

  • No security or limits impact
  • Auth / permissions considered: existing bearer authentication and host restrictions remain intact.
  • Data exposure, filtering, or token/size limits considered: client metadata is escaped for the header; no additional request payload or credentials are emitted. All test authentication values are non-functional fixtures.

Tool renaming

  • I am not renaming tools as part of this PR.

Lint & tests

  • script/lint passed in the Codespace.
  • script/test passed, including the full race-enabled suite.

Offline compiled-binary probes exercised actual MCP calls and outgoing GraphQL headers for metadata-free builds, malformed metadata, release versions, explicit revisions and embedded VCS metadata. Fallback warnings stayed on stderr; authentication and feature headers were preserved. The tagged live-service E2E suite, full container build and production rollout were not run.

Docs

  • Not needed
  • Updated README and installation guidance. Tool documentation generation is unnecessary because schemas and toolsets are unchanged.

Preserve release identities, resolve actual source revisions, and attach upstream client metadata per request without mutating shared transports.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6dde41da-ec25-4370-b26c-b036d0a53b3c

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.

Copilot review overview

🟡 Changes recommended

Client metadata can invalidate HTTP headers, and default Docker builds can be incorrectly marked dirty.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Adds versioned, request-scoped user agents for STDIO GitHub API requests and resolves source-build versions from VCS metadata.

Changes:

  • Adds request-scoped user-agent transport handling.
  • Resolves release and source revision identifiers.
  • Adds protocol, transport, and version tests plus documentation.
File Description
README.md Documents source builds and user agents.
pkg/​http/​transport/​user_agent.go Adds request-scoped user-agent overrides.
pkg/​http/​transport/​user_agent_test.go Tests concurrent override isolation.
internal/​ghmcp/​server.go Propagates MCP client identity to API requests.
internal/​ghmcp/​server_test.go Adds wire-level protocol coverage.
docs/​installation-guides/​README.md Corrects the package build command.
cmd/​github-mcp-server/​version.go Resolves and validates build versions.
cmd/​github-mcp-server/​version_test.go Tests version resolution sources.
cmd/​github-mcp-server/​main.go Applies resolved versions to STDIO startup.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/ghmcp/server.go Outdated
Comment thread cmd/github-mcp-server/version.go Outdated
Encode upstream client comments as valid HTTP header data, keep explicit build revisions independent of embedded dirty state, and reject null source hashes. Cover protocol wire behaviour and document the version-source contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6dde41da-ec25-4370-b26c-b036d0a53b3c
@jidicula

Copy link
Copy Markdown
Contributor Author

Addressed both review findings in 56d11f5.

  • Client name/version metadata is escaped before it becomes an HTTP comment. Wire regressions exercise malformed metadata across the supported MCP request paths, retaining authentication, feature headers and request isolation.
  • Explicit linked revisions no longer inherit an unrelated embedded dirty bit. Embedded source revisions still retain their real dirty state, releases remain unchanged, and null revision hashes fail explicitly.

Before the implementation fix, the new tests reproduced five header-encoding failures, two explicit-revision/dirty-state failures and eight null-revision failures. After the fix, the complete repository race suite passed. A fresh targeted race run and the pinned analyser also passed after the unrelated formatter-only change had been removed.

GOTOOLCHAIN=go1.25.12 script/lint
script/test
go test -race -count=1 -timeout 5m ./cmd/github-mcp-server ./internal/ghmcp ./pkg/http/transport
bin/golangci-lint run

Validation selected the Go 1.25.12 toolchain root and executable path together, matching the repository's lint setup rather than mixing the image's Go root with a different compiler.

Actual compiled STDIO executables successfully sent list_issues requests to a loopback fixture for modified-source, explicit-revision and release builds. The clean post-commit binary emitted:

github-mcp-server/vcs-56d11f547d37a4c8b52a6a8021d0ca59451dd35a (binary-probe/1.0.0)

Its embedded source revision matches the published commit and reports vcs.modified=false. Authentication values in these probes are non-functional fixtures. No live-service E2E suite, new full container build or deployment was run.

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.

Copilot review overview

🟢 Approval recommended

The implementation preserves transport isolation and existing headers with comprehensive protocol and version-resolution coverage.

Review effort: Balanced
Findings: None

Resolved since last review (2)

@jidicula
jidicula force-pushed the jidicula/graphql-user-agent-mcp-stdio branch from 56d11f5 to 1e16b58 Compare September 28, 2026 09:47
@jidicula
jidicula marked this pull request as ready for review September 28, 2026 09:52
@jidicula
jidicula requested a review from a team as a code owner September 28, 2026 09:52

@SamMorrowDrums SamMorrowDrums left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The user-agent part of this looks good to me. Resolving ClientInfo() per request and passing it through the request context is the right approach for 2026-07-28, where initialize/server/discover are optional and requests have to stand alone. It still works for legacy sessions, it doesn't mutate the shared transport, and the remote server keeps its default behaviour.

One thing needs changing before this merges: resolveServerVersion now makes stdio fail to start when there's no version metadata. On main, version defaults to the literal "version" and gets passed straight through, so a server with no version metadata has always started. I confirmed that a go build -buildvcs=false build now exits with server build has no release or revision. The same would happen with source tarballs or vendored copies that have no .git, and with Docker builds where .git isn't in the build context (git rev-parse HEAD returns nothing and VERSION defaults to dev). Stopping startup over what is only a header label is a regression.

Could you:

  • Keep the precedence: release version → explicit main.commit → embedded VCS revision (vcs-<sha>[-dirty]). That part is a nice improvement.
  • Fall back instead of erroring: if nothing resolves, use the previous placeholder (or dev), optionally log a warning, and don't return an error.
  • Treat malformed values the same way: a short or invalid SHA should fall back or pass through as-is, not block startup.
  • Update the README: remove the "STDIO startup reports an error…" wording.

The user-agent fix doesn't depend on the version-resolution work, so splitting that into its own PR would also be fine if that's easier.

@jidicula
jidicula marked this pull request as draft September 30, 2026 15:52
Skip invalid build metadata while retaining release and revision precedence. Fall back to dev with a stderr warning rather than preventing startup, and cover the compatibility contract in tests and documentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6dde41da-ec25-4370-b26c-b036d0a53b3c
@jidicula

Copy link
Copy Markdown
Contributor Author

@SamMorrowDrums, addressed the startup regression from #3323 (review). Version resolution now skips missing or malformed candidates while preserving release -> explicit commit -> embedded VCS -> installed module precedence. If none is usable, STDIO starts with dev and a stderr warning; stdout remains reserved for MCP. The README now documents that fallback, and the request-scoped user-agent implementation is unchanged.

Added precedence and fallback regressions. The full race-enabled suite and lint passed in the Codespace, and offline probes of the compiled executable confirmed successful MCP/GraphQL requests for -buildvcs=false, short/invalid revisions, releases and source builds, with authentication and feature headers preserved.

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.

Copilot review overview

🟢 Approval recommended

The implementation addresses the stated behavior with comprehensive regression coverage and no unresolved correctness issues.

Review effort: Balanced
Findings: None

@SamMorrowDrums SamMorrowDrums left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the changes!

@SamMorrowDrums
SamMorrowDrums marked this pull request as ready for review September 30, 2026 21:00
@SamMorrowDrums
SamMorrowDrums merged commit 5a1a386 into main Sep 30, 2026
21 checks passed
@SamMorrowDrums
SamMorrowDrums deleted the jidicula/graphql-user-agent-mcp-stdio branch September 30, 2026 21: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