When deploying the MCP server behind an OAuth proxy (e.g. for GHES,
which does not natively support RFC 8414, RFC 7591, or PKCE), the
/.well-known/oauth-protected-resource endpoint currently always derives
the authorization_servers URL from GITHUB_HOST. There is no way to
point clients at a different authorization server without intercepting
that endpoint at the ingress/proxy layer.
The oauth.Config struct already has an AuthorizationServer field with
the conditional logic in place (pkg/http/oauth/oauth.go), but it was
never wired to any configuration surface.
This commit exposes it as:
- --authorization-server CLI flag on the http subcommand
- GITHUB_AUTHORIZATION_SERVER environment variable (via viper's
existing GITHUB_ prefix + automatic env mapping)
When set, the value is passed through ServerConfig into oauth.Config,
and the protected resource metadata advertises it directly instead of
calling apiHost.AuthorizationServerURL().
Bounds the total HTTP request, so allow modest headroom over the MCP
SDK's 4 MiB default for JSON-RPC and tool-call envelope overhead rather
than spending the whole budget on tool content.
Because the limit now exceeds the SDK default, passing it to
StreamableHTTPOptions is load-bearing: without it the SDK would cap
requests at 4 MiB and the headroom would not exist. Covered by a test
that sends a request between the two limits.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The middleware default was an arbitrary 10 MiB, above the 4 MiB the SDK
already enforces, so it never changed which requests were accepted.
Alias mcp.DefaultMaxRequestBodyBytes instead, making the earlier
enforcement point behaviour-preserving by construction.
Also pass the effective limit to StreamableHTTPOptions. Previously the
SDK kept its own 4 MiB default, so a larger configured
MaxRequestBodyBytes was silently capped; both layers now agree.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
WithMCPParse and WithScopeChallenge tests for oversized requests were
using strings.NewReader, which gives httptest.NewRequest a known
Content-Length. That let WithMaxBodySize reject the request in its
fast path before the request ever reached the middleware's own
io.ReadAll/isMaxBytesError handling, leaving those branches untested.
Reuse the existing unknownLengthBody helper (body_limit_test.go) so
these tests actually reach the fallback read path and cover the
*http.MaxBytesError handling added in WithMCPParse and
WithScopeChallenge.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add WithMaxBodySize middleware that bounds the request body via
http.MaxBytesReader (with a fast Content-Length rejection when known),
registered first in RegisterMiddleware so it runs before any other
middleware or the MCP SDK reads or buffers the body.
WithMCPParse and WithScopeChallenge now return a clear 413 "request
body too large" response when their body read hits the limit, instead
of silently continuing.
Defaults to 10 MiB, overridable via ServerConfig.MaxRequestBodyBytes.
Fixes#3102
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use exact configured host authorities for every REST, GraphQL, and raw client so redirects cannot reattach credentials to foreign hosts or ports. Add adversarial redirect and lookalike coverage.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
BearerAuthTransport re-adds the Authorization header on every hop, which
defeats net/http's cross-host redirect stripping. Scope the credential to
the configured hosts so a redirect off them travels without the token.
An empty AllowedHosts preserves prior behavior; the three production
construction sites populate it from the configured REST, upload, GraphQL
and raw hosts.
* feat(repos): add confirmed repository deletion
Add a destructive delete_repository tool that requires an exact owner/repo confirmation through multi-round-trip elicitation. Gate the tool to MCP protocol 2026-07-28 and newer across local and remote transports.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
* refactor(inventory): generalize tool availability guards
Gate protocol-restricted tools on required elicitation capabilities and enforce direct calls inside the registered handler so SDK result finalization remains intact.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
* feat(http): protect MRTR request state
Seal repository deletion targets for self-hosted HTTP with a stable AES-256-GCM key. Hide only delete_repository when no key is configured and expose an optional sealer interface for remote integrators.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
* fix(repos): expire deletion confirmations
Bind sealed repository deletion state to the immutable repository ID and a ten-minute expiry. Re-check identity before deletion so replay cannot affect a recreated repository.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
* fix(http): preserve tool and scope restrictions
Apply static allowlists before removing unavailable tools and fail closed on invalid configured tool names. Model independent OAuth requirements as conjunctive groups so repository deletion requires both delete_repo and repo.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
* fix(repos): require protected confirmation state
Give stdio a process-local request-state sealer and make deletion fail closed without one. Preserve legacy any-of OAuth behavior globally while documenting and enforcing delete_repository's conjunctive delete_repo and repo requirements.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
* fix(oauth): request repository deletion scope
Include delete_repo in the supported OAuth scope set used by stdio login, HTTP protected-resource metadata, and tool filtering.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
* fix(oauth): require deletion scope opt-in
Keep delete_repo in protected-resource discovery for step-up authorization while excluding it from the default stdio OAuth grant.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
* refactor(oauth): derive scope sets from catalog
Generate protected-resource supported scopes and the lower-risk default OAuth grant from one canonical scope definition list.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
* refactor(scopes): own OAuth scope catalog
Move supported and default OAuth scope policy into pkg/scopes so protected-resource metadata and stdio grants derive from the scope domain package.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
* fix(scopes): require workflow scope opt-in
Keep workflow and codespace in protected-resource discovery while excluding both from the default OAuth grant alongside delete_repo.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
---------
Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
* fix(http): reject unsupported subscription streams
Use the Mcp-Method header to reject subscriptions/listen with the spec-defined 404 Method Not Found response instead of opening an idle SSE stream. Preserve SDK validation for missing or mismatched headers.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06d5dda1-4086-4996-8d18-152e45e611b0
* refactor(http): clarify subscription rejection
Document why header validation precedes the unsupported-method rejection and use named SDK error constants in tests.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06d5dda1-4086-4996-8d18-152e45e611b0
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06d5dda1-4086-4996-8d18-152e45e611b0
GHES hosts accepted an http:// scheme, which was interpolated into every
REST/GraphQL/upload/raw/authorization URL. Authenticated requests would then
carry the bearer token/PAT over cleartext http, exposing it to network
interception and replay.
Add a central HTTPS check in parseAPIHost so no deployment can build
authenticated URLs over http, mirroring the existing GHEC behaviour. Permit
http only for loopback hosts (localhost, 127.0.0.1, ::1) so local development
against a dev server still works.
Closesgithub/copilot-mcp-core#1815
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The server exposes a static set of tools, prompts, and resources and never
mutates them at runtime, so it never emits list_changed notifications. When
capabilities are left unset, the go-sdk infers listChanged:true from the
presence of items and advertises tools/prompts/resources list-change support
we don't actually provide - and the 2026-07-28 spec (subscriptions/listen)
tightens expectations around this.
Declare empty tools/prompts/resources capabilities in NewMCPServer so both the
stdio and remote servers advertise honestly. The remote HTTP handler already
set these explicitly; that duplication is now removed in favour of the shared
default, leaving only the remote-specific schema cache.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 95b8432c-f280-472e-a242-d3ca6dc31f19
Keep PEM loading and installation-token provider construction at the CLI leaf, then pass a generic refreshing token provider through the existing HTTP transports. Rebase the feature onto current main and keep the HTTP command unchanged.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 646357dd-c89f-4973-9a5c-e6c5fc18818c
* feat(oauth): add stdio OAuth 2.1 stdio login
Introduce internal/oauth, a self-contained library that performs the
user-facing GitHub OAuth login the stdio server uses to obtain a token
without a pre-provisioned PAT. It is independent of MCP: client concerns
(elicitation) sit behind the Prompter interface so the flows are testable
without a live session.
What it provides:
- Authorization-code + PKCE flow with a local loopback callback server,
state/CSRF validation, and XSS-safe result pages.
- Device-authorization flow as a fallback (headless, containers).
- A Manager that selects the most secure available channel
(browser auto-open -> URL elicitation -> last-resort user action),
runs a single flow at a time, and exposes a refreshing token source.
Both GitHub OAuth Apps and GitHub Apps are supported without special
casing: the token is modeled as an x/oauth2 refreshing TokenSource, so
expiring GitHub App user tokens are renewed transparently (the gap that
made a stored-token approach silently die after ~8h).
When a client lacks secure URL elicitation and the flow falls back to a
tool-response message, the message advises the user that their agent/CLI/
IDE does not appear to support URL elicitation and suggests requesting it
for improved security.
Tests exercise real protocol behavior against an httptest GitHub stand-in:
PKCE challenge/verifier, GitHub App refresh-on-expiry, device polling,
URL elicitation, declined prompts, the last-resort action with advisory,
and single-flight concurrency.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(oauth): reap browser launcher and keep native callback on loopback
Address code review:
- openBrowser: reap the launcher process asynchronously so it does not
linger as a zombie for the lifetime of the server.
- listenCallback: take an explicit bindAll flag and bind to all interfaces
only inside a container (where the published port arrives via eth0).
A native run, even with a fixed callback port, now stays on 127.0.0.1
instead of 0.0.0.0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(oauth): fail fast when a fixed callback port is unavailable
A fixed --oauth-callback-port is registered with the OAuth app and chosen
deliberately, so a bind failure means another process holds the port and
could intercept the authorization redirect. Treat that as fatal instead of
silently downgrading to the device flow, which would mask the conflict.
Also warn, when binding the callback inside a container, that the listener
is on all interfaces and should be published to loopback only so the
authorization code is not exposed on the container network.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(oauth): surface refresh failures, bound refresh, prefer device flow when headless
Addresses pre-merge review of the OAuth stdio core:
- Log a one-time warning when token refresh fails instead of silently
returning an empty access token, so a forced re-login isn't a surprise.
- Bound each background token refresh with a 30s HTTP client timeout so a
stalled GitHub token endpoint can't block tool calls indefinitely.
- On a headless host (no display server) with a random callback port, fall
back to the device-code flow — the only channel reachable from a browser
on another machine — instead of dead-ending on an unreachable localhost
redirect. A generic browser-open failure still offers the manual URL.
- Mark the callback bind failure with a sentinel so the fixed-port-busy
fatal path can't misreport an unrelated error as a port conflict.
- Export NormalizeHost so callers can recognize the default github.com host
(consumed by the build-time baked-in credential guard).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(oauth): wire stdio OAuth 2.1 login into the server (2/4) (#2710)
* feat(oauth): wire stdio OAuth 2.1 login into the server
Connect the internal/oauth core library to the stdio MCP server so users
can authenticate with an OAuth App or GitHub App client ID instead of a
static personal access token.
- BearerAuthTransport gains a TokenProvider that is consulted per request,
letting the lazily-acquired, auto-refreshing OAuth token take effect
without rebuilding the client.
- createGitHubClients uses BearerAuthTransport (and skips go-github's
WithAuthToken, which would pin a static token) when a TokenProvider is set.
- RunStdioServer starts without a token and installs receiving middleware
that runs the authorization flow on the first tool call, surfacing the
auth URL or device code via elicitation (or a tool result as a fallback).
- Tool filtering uses the requested OAuth scopes; the default supported set
hides nothing, while a narrower --oauth-scopes both narrows the grant and
filters tools accordingly.
- A sessionPrompter adapts the MCP server session to oauth.Prompter, keeping
the authorization URL off the model's context.
- New stdio flags: --oauth-client-id/-client-secret/-scopes/-callback-port.
This is stdio-only and deliberately does not touch MCP-HTTP auth.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(oauth): address review — omit empty bearer header, guard token/oauth
- BearerAuthTransport omits the Authorization header entirely when the token
is empty (pre-authorization) rather than sending an empty "Bearer " value.
- RunStdioServer rejects the ambiguous combination of a static Token and an
OAuthManager up front, enforcing the documented mutual exclusivity.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(oauth): clarify SupportedScopes is the stdio default and tool filter
Document that stdio OAuth login requests these scopes by default and then
filters the exposed tools to the scopes actually granted, so a tool whose
required scope is absent from this list is hidden under default OAuth even
though a PAT carrying that scope would expose it. Keep the list in sync with
tool scope requirements when scopes change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Distinguish undeliverable auth prompts from user declines
An elicitation prompt that the client cannot deliver (a transport or
protocol failure) was treated the same as a user actively declining: any
display error cancelled the flow. That conflated a system failure with a
deliberate "no", so a client that advertised URL elicitation but failed
to deliver it would hard-fail the login instead of degrading.
Add an ErrPromptUnavailable sentinel alongside ErrPromptDeclined and have
the MCP adapter return it when Elicit fails at the transport level. The
manager now falls back to the manual user-action channel on an
undeliverable prompt (keeping the background flow alive so the user can
still authorize out of band), while a genuine decline still aborts. A
context-cancelled prompt is checked first so an ending flow is never
misread as a transport failure.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* build(oauth): bake in default OAuth credentials for official releases (3/4) (#2711)
* build(oauth): bake in default OAuth credentials via build-time ldflags
Inject the public OAuth client credentials (stored as the OAUTH_CLIENT_ID
and OAUTH_CLIENT_SECRET repo secrets) at build time via -ldflags so
official binaries and images ship a working default app for zero-config
login. Security relies on PKCE, not on the secret. Local/dev builds leave
the values empty and continue to require an explicit token or
--oauth-client-id.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(oauth): recognize github.com host aliases for the baked-in client
Match the default host via oauth.NormalizeHost instead of only an empty
host string, so an explicit GITHUB_HOST=github.com (or api.github.com)
still counts as the default and keeps zero-config baked-in login working.
GHES and ghe.com users continue to bring their own --oauth-client-id.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(oauth): document stdio OAuth login; make PAT optional in install config (#2717)
Add a dedicated Local Server OAuth Login guide (docs/oauth-login.md) covering
the PKCE/device flows, display channels and the URL-elicitation security
advisory, scope-based tool filtering, the fixed-port Docker recipe and its
loopback/port-safety behavior, bringing your own OAuth or GitHub App, and the
GitHub Enterprise Server / ghe.com requirement to register an app on that host
(custom --gh-host directs login at that instance's authorization server).
Reflect that the local server now logs in with OAuth by default on github.com:
- README: make the stdio Docker install badges OAuth-first (fixed callback port
8085 published to loopback), drop the PAT prompt, and reframe the PAT as an
optional alternative with a pointer to the new guide.
- server.json: make GITHUB_PERSONAL_ACCESS_TOKEN optional and publish the OAuth
callback port so the registry default works without a token.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Annotate read tools with ifc labels
* Dont automatically enable IFCLabels in insiders mode
* ifc: don't label unpublished repo advisories as public
Repository security advisory listings can include draft/triage/closed
advisories (via the state filter), which are not world-readable even on a
public repository. Deriving confidentiality from repo visibility alone
under-classified those results as public.
LabelRepositorySecurityAdvisory now takes an allPublished flag and only
returns a public label when the repo is public AND every returned advisory
is published; otherwise it is private. list_repository_security_advisories
computes allPublished from the response state; the org-wide listing stays
private-untrusted. Adds unit + handler regression tests covering the
draft-advisory-on-public-repo case.
Addresses PR review feedback.
* ifc: fix confidentiality under-classification in releases, collaborators, get_me
Audit for the same bug class as the repo-advisory fix (confidentiality
derived from a coarse signal that misses access-restricted items) found
three more under-classifications:
- Releases (list_releases, get_latest_release, get_release_by_tag): draft
releases are visible only to push-access users and are not world-readable
even on a public repo. New LabelRelease(isPrivate, hasDraft) returns public
only for a non-draft release on a public repo; handlers compute hasDraft
from the response (Draft flag / per-item scan).
- list_repository_collaborators: a collaborator roster requires push access
to list, so it is never world-readable, not even on a public repo. New
LabelCollaboratorRoster() is always PrivateTrusted (mirrors LabelTeam),
replacing the repo-visibility-derived label.
- get_me: the result includes private_gists / total_private_repos /
owned_private_repos, which are not part of the public profile. LabelGetMe
is now PrivateTrusted instead of PublicTrusted.
Verified the remaining public-capable labels are sound: Actions logs are
world-readable on public repos; branches/tags are public metadata; gist,
project, search, and starred-repo labels read per-item visibility and join.
Adds ifc unit tests for the new/changed labels and a get_release_by_tag
handler regression test (draft on public repo -> private); updates the
get_me handler test to assert private.
* ifc: document why list results use one joined label, not per-item
Explain on LabelSearchIssues (and cross-ref from LabelGistList) that a tool
result is delivered as one opaque payload and the IFC engine makes one
allow/deny decision per flow at egress, so the only sound bound for a list is
the meet of every item's label. Per-item labels would only be load-bearing if
the engine could partition a result and route items to different sinks; until
then they would invite unsafe declassification of a public item that arrived
alongside private data. Doc-only change.
Per the MCP Apps 2026-01-26 spec, servers SHOULD check client capabilities
before advertising UI-enabled tools. Extend the inventory strip gate to
remove _meta.ui not only when the feature flag is off, but also when the
request context explicitly reports the client lacks UI support
(HasUISupport returns supported=false, ok=true).
When the capability is unknown (ok=false, e.g. stdio paths), fall through
to the existing feature-flag gate so existing behaviour is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bumps github.com/modelcontextprotocol/go-sdk from v1.6.0 to v1.6.1 and
removes the CrossOriginProtection bypass we previously installed on the
StreamableHTTP handler.
As of go-sdk v1.6.0 the cross-origin check is opt-in: a nil
CrossOriginProtection on StreamableHTTPOptions means no check is run.
v1.6.1 also marks the field itself as deprecated (the SDK recommends
wrapping the handler with middleware instead, and the field will be
removed in v1.8.0).
This server authenticates via bearer tokens, not cookies, so the
Sec-Fetch-Site CSRF check is unnecessary and would block browser-based
MCP clients. Leaving CrossOriginProtection unset preserves that
behavior without depending on a deprecated API.
Supersedes #2541.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ports the gating from PR #2553 onto main (the original merge landed on a
stack base that did not make it to main).
Changes:
- pkg/inventory: FeatureFlagDisable becomes []string (any-listed-on → hide).
FeatureFlagEnable stays as a single string. This avoids the AND-of-enable
semantics from the earlier proposal, which encoded dependencies rather
than rollout knobs and had no real call site. Disable-OR is the case
that does need the slice (LegacyIssueWrite below).
- pkg/github/issues.go: split IssueWrite into IssueWrite (flag-enabled,
exposes issue_fields) and LegacyIssueWrite (flag-disabled, omits it).
Both register as 'issue_write'; mutually exclusive flag annotations
pick exactly one at runtime. Refactored into a shared buildIssueWrite
helper instead of duplicating the ~250-line tool definition.
- pkg/github/issues.go: GetIssue field_values enrichment now requires
the flag at runtime. The verbose REST IssueFieldValues is always
cleared from the response.
- Existing single-flag Disable call sites converted to slices.
- New toolsnap variant issue_write_ff_remote_mcp_issue_fields.snap; the
canonical issue_write.snap is owned by LegacyIssueWrite.
- README + flag docs regenerated.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
X-Forwarded-Host and X-Forwarded-Proto were unconditionally honored when
constructing OAuth resource metadata URLs. In HTTP-mode deployments that
do not set --base-url and are not fronted by a proxy that strips these
headers, this lets an on-path client influence the URL advertised in
WWW-Authenticate and the /.well-known/oauth-protected-resource body.
This is a hardening change rather than a true vulnerability — exploiting
it requires HTTP without --base-url plus an attacker already positioned
to inject the header — but the unsafe default is worth closing.
Default behavior now derives host/scheme from r.Host and the TLS state.
Setups that rely on a trusted internal forwarder (e.g. an in-cluster
gateway that needs to preserve the originating hostname per request) can
opt back in with --trust-proxy-headers / GITHUB_TRUST_PROXY_HEADERS=1.
--base-url continues to take precedence in all cases.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add CSV output for list tools under insiders mode
* fix: resolve rebase feature flag conflicts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Simplify feature-flag handling: collapse CSV dual-variant + skip filtering when no checker (#2516)
* refactor: generic toolset+name sort, clarify feature flag intent
Address review feedback on #2450:
- Collapse the three near-identical sort helpers in pkg/inventory/filters.go
into a generic sortByToolsetThenName so adding new inventory item types
doesn't require copying the comparator.
- Expand the doc comments on the three *WithoutFeatureFiltering helpers to
spell out why they exist: HTTP mode builds a static (process-wide)
inventory as an upper bound, but per-request feature flags from headers
(X-MCP-Features, X-MCP-Insiders) are evaluated later, so feature-flagged
variants must be preserved here.
- Strengthen the doc comment on ResolveFeatureFlags to make the contract
explicit: user-supplied flags are validated against AllowedFeatureFlags,
but insiders expansion deliberately is not — InsidersFeatureFlags may
include server-controlled flags that are not user-toggleable.
CORS comments are intentionally left for the PR author.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(feature-flags): clarify allowed and insiders sets are independent
Also add tests covering:
- a user-toggleable flag (FeatureFlagIssuesGranular) that insiders does
not turn on automatically
- insiders mode not turning on user-only allowed flags
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(inventory): collapse three *WithoutFeatureFiltering helpers into StaticUpperBound
The three parallel methods (AvailableToolsWithoutFeatureFiltering,
AvailableResourceTemplatesWithoutFeatureFiltering,
AvailablePromptsWithoutFeatureFiltering) were always called as a triple
in exactly two places: HTTP buildStaticInventory and its test mirror.
They exist because the dual-variant pattern (sibling tools with mirrored
FeatureFlagEnable / FeatureFlagDisable on the same name, e.g. CSV output)
makes feature filtering at static-build time impossible — both variants
must be kept and resolved per-request.
Replace the three with one method, Inventory.StaticUpperBound(ctx), that
returns (tools, resources, prompts) and carries the rationale in its
doc comment. Reduces API surface, eliminates the triplication, and makes
the single "skip feature filtering" concept obvious to readers.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor: simplify feature-flag handling
Two related simplifications, both about treating insiders as a meta flag
that expands once at startup and then stops mattering:
- Collapse CSV's dual-variant pattern into a single tool whose handler
performs a runtime feature-flag check via deps.IsFeatureEnabled. CSV
is a pure response-format toggle, not a schema change, so it does not
need the dual-name pattern that genuine schema variants (granular
issues/PRs) still use.
- When no feature checker is installed, skip feature-flag filtering and
return the full upper bound. The static HTTP inventory now uses plain
AvailableTools/Resources/Prompts; the per-request inventory always
installs a checker, so MCP registration (which serves a tool name once)
always sees a deduplicated set. The bespoke StaticUpperBound helper and
the isToolEnabledWithFeatureFlags split go away.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* ci(mcp-diff): add insiders + per-feature configs
The mcp-diff matrix now includes:
- --insiders (and --insiders --read-only)
- one config per github.AllowedFeatureFlags entry, generated by
script/print-mcp-diff-configs so new user-controllable flags get
diffed automatically without editing the workflow
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(insiders): explain feature-flag resolution for contributors
Adds a 'How feature flags are resolved' section covering:
- Insiders is a meta flag, like 'all'/'default' for toolsets
- User input -> allowlist filter -> insiders expansion ->
server-side fallback (remote only)
- AllowedFeatureFlags vs InsidersFeatureFlags are independent
- How to add a new feature flag, including the
TestGitHubPackageDoesNotReadInsidersMode guard
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(inventory): make feature-flag gating a regular ToolFilter
Move tool feature-flag evaluation out of isToolEnabled and into a
ToolFilter installed at the head of the pipeline by Build() when
WithFeatureChecker received a non-nil checker. The 'no checker = no
filtering' contract is now expressed structurally (the filter isn't
installed) instead of by a runtime nil check inside the helper.
Resources and prompts have no filter pipeline, so they call the now-pure
featureFlagAllowed helper behind an explicit r.featureChecker != nil
guard at the iteration site.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* perf(inventory): cache extracted toolset IDs in sort comparator
Avoid evaluating the extractor closures up to three times per comparison.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: correct MCP features header in cors
* docs: regenerate README for CSV output toolset
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: remove duplicate MCPFeaturesHeader from CORS headers
* ci(mcp-diff): add streamable-http job with header-based configs
Adds a sibling mcp-diff-http job that exercises the streamable-http
transport against a shared HTTP server, with per-config settings supplied
via X-MCP-* request headers — mirroring how the remote server is invoked
in production (server-side defaults + per-user header overrides).
The config generator gains a -transport flag:
- stdio (default, unchanged behaviour)
- http-headers (emits headers-only configs targeting a shared server)
Two new combined entries layer multiple headers together as a smoke test
for header-merging regressions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: regenerate after merging main
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Sam Morrow <info@sam-morrow.com>
Co-authored-by: sammorrowdrums <sammorrowdrums@github.com>
Dynamic toolset discovery (the meta-tools enable_toolset, list_available_toolsets,
get_toolset_tools and the --dynamic-toolsets / GITHUB_DYNAMIC_TOOLSETS switch)
was a local-only feature never offered by the remote server. Removing it
deletes a meaningful chunk of branching, configuration surface and tests
for a path no longer in active use.
The deprecated closure-based NewServerToolWithDeps generic constructor was
only kept around for the dynamic tool registration path and is removed
together with it. Going forward there are exactly two constructors:
- NewServerTool — raw mcp.ToolHandler, no closure, no unmarshalling
- NewServerToolWithContextHandler[In, Out] — typed handler, deps via context
Inventory methods that only existed for the dynamic path
(ToolsForToolset, IsToolsetEnabled, EnableToolset, EnabledToolsetIDs)
are removed. ResolvedEnabledToolsets loses its dynamic flag.
Also strips dynamic references from the README, server configuration
docs, copilot-instructions, mcp-diff workflow, and conformance-test
script.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(mcp-apps): defer _meta.ui strip to per-request RegisterTools
The MCP Apps `_meta.ui` strip lived in `Builder.Build()`, which calls
`checkFeatureFlag(context.Background())`. The HTTP feature checker
(`createHTTPFeatureChecker`) reads insiders mode from the request
context — a background context never has it set, so the FF reported
MCP Apps off and the strip ran eagerly at server startup. Per-request
inventory factories then served pre-stripped tools regardless of
whether the request actually arrived on the `/insiders` route.
Symptom: `github/github-mcp-server-remote` returns 0 tools with
`_meta.ui` over HTTP `/insiders`, despite the source unconditionally
setting it on `get_me`, `issue_write`, and `create_pull_request`.
VS Code only renders MCP App UIs because of its persistent tool cache
from earlier deploys. Reproducible locally with
`cmd/github-mcp-server http --insiders` plus a vanilla curl tools/list.
Fix: drop the strip from `Build()`. Apply it in `RegisterTools(ctx,…)`
where the per-request context is in scope and the HTTP feature checker
can correctly detect insiders mode (or the remote checker can correctly
read user identity for Statsig flag lookup).
The same root cause affects `github/github-mcp-server-remote` — its
`featureflags.NewComposedFeatureFlagChecker` reads
`requestctx.User(ctx)`, which background context lacks, so the
`remote_mcp_ui_apps` Statsig flag always returned false. The fix here
covers both downstreams since `RegisterTools` is the single entry
point for tool registration.
Stdio mode is unaffected: it uses a closure-captured insiders mode
flag (`createFeatureChecker`) that does not depend on context, and the
per-request strip in `RegisterTools` produces the same outcome.
Verified end-to-end against the deployed remote tool definitions:
HTTP /insiders → 3 tools with _meta.ui (was 0)
HTTP / → 0 tools with _meta.ui (correct)
stdio --insiders → 3 tools with _meta.ui (unchanged)
stdio → 0 tools with _meta.ui (correct)
Adds:
- pkg/http: TestInsidersRoutePreservesUIMeta — pins the regression
- pkg/inventory: updates the existing strip tests to use the new
RegisterTools-as-strip-site contract via a captureRegisteredTools
helper
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* style: gofmt handler_test.go and registry_test.go
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* lint: address revive (context-as-argument) and unused checkFeatureFlag
- Reorder captureRegisteredTools params to put context.Context first
- Remove dead Builder.checkFeatureFlag (was only called by Build's
former MCP Apps strip, now done in RegisterTools via the Inventory
receiver's checkFeatureFlag instead)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GoReleaser Release / release (push) Has been cancelled
MCP Server Diff / mcp-diff (push) Has been cancelled
Publish to MCP Registry / publish (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Build and Test Go Project / build (macos-latest) (push) Has been cancelled
Build and Test Go Project / build (ubuntu-latest) (push) Has been cancelled
Build and Test Go Project / build (windows-latest) (push) Has been cancelled
* Fix set_issue_fields mutation: use correct inline fragments for IssueFieldValue union
The mutation response struct used a single inline fragment
'... on IssueFieldDateValue' with a 'Name' field that doesn't exist
on that type (only IssueFieldSingleSelectValue has 'name'). This
caused GraphQL validation to fail with:
Field 'name' doesn't exist on type 'IssueFieldDateValue'
Since GraphQL validates the entire document (including response
selection sets) before executing any operation, the mutation never
fired at all — no fields were ever set regardless of input.
Fix by adding correct inline fragments for all four union types:
- IssueFieldTextValue (value)
- IssueFieldSingleSelectValue (name)
- IssueFieldDateValue (value)
- IssueFieldNumberValue (value)
* Update test mock to match corrected inline fragments
* Update handler_test.go formatting
Add NormalizeContentType middleware that strips optional parameters
(e.g. charset=utf-8) from application/json Content-Type headers before
the request reaches the Go SDK's StreamableHTTP handler, which performs
strict string matching.
Per RFC 8259, the charset parameter is redundant for JSON but must be
accepted per HTTP semantics.
Fixes#2333
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rebase PR #2282 onto main (post-#2332) and unify feature flag
allowlists into a single source of truth.
- Add MCPAppsFeatureFlag, AllowedFeatureFlags, InsidersFeatureFlags,
and ResolveFeatureFlags in feature_flags.go
- AllowedFeatureFlags includes all user-controllable flags (MCP Apps +
granular), InsidersFeatureFlags only includes MCPAppsFeatureFlag
- HeaderAllowedFeatureFlags() now delegates to AllowedFeatureFlags
- Builder uses feature checker instead of insidersMode bool
- Remove InsidersOnly field from ServerTool and WithInsidersMode from
Builder
- HTTP feature checker uses ResolveFeatureFlags for per-request
resolution with insiders expansion
- Tool handlers check MCPAppsFeatureFlag via IsFeatureEnabled instead
of InsidersMode
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The HTTP server now respects the same static CLI flags as the stdio
server: --toolsets, --tools, --exclude-tools, --read-only,
--dynamic-toolsets, and --insiders.
A static inventory is built once at startup from these flags, producing
a pre-filtered tool/resource/prompt universe. Per-request headers
(X-MCP-Toolsets, X-MCP-Tools, etc.) can only narrow within these
bounds, never expand beyond them. When no static flags are set, the
existing behavior is preserved — headers have full access to all
toolsets.
Fixes#2156
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of new CLI flags (--server-name, --server-title), reuse the
existing string override mechanism that already supports tool title/
description overrides throughout the codebase.
Users can now configure the server name and title via:
- GITHUB_MCP_SERVER_NAME / GITHUB_MCP_SERVER_TITLE env vars
- "SERVER_NAME" / "SERVER_TITLE" keys in github-mcp-server-config.json
This is consistent with how all other user-visible strings are
overridden (e.g. GITHUB_MCP_TOOL_GET_ME_USER_TITLE). No new struct
fields or CLI flags are needed.
Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
Allows users running multiple GitHub MCP Server instances (e.g., for
github.com and GitHub Enterprise Server) to override the server name and
title in the MCP initialization response.
- Add --server-name / GITHUB_SERVER_NAME flag+env to override name
- Add --server-title / GITHUB_SERVER_TITLE flag+env to override title
- Defaults remain "github-mcp-server" and "GitHub MCP Server"
- Applies to both stdio and HTTP server modes
- Add tests for default and custom name/title
Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
CodeQL / Analyze (actions) (push) Has been cancelled
Build and Test Go Project / build (macos-latest) (push) Has been cancelled
Build and Test Go Project / build (ubuntu-latest) (push) Has been cancelled
Build and Test Go Project / build (windows-latest) (push) Has been cancelled
* Move scope storage into its own context key, separately from token info.
This allows us to provide scopes seperately in the remote server, where
we have scopes before we do the auth.
* Skip token extraction if token info already exists in context.
This is to avoid redundant token extraction in remote setup where token info may have already been extracted earlier in the request lifecycle.
* Check for existing scopes in context before fetching from GitHub API in scope challenge middleware
* Return error type for unknown tools in inventory builder and handle it in HTTP handler