A 403 carrying X-RateLimit-Remaining: 0, or a secondary rate limit
documentation URL, reached copilotReviewErrMsg as a rate limit error. It
was explained as a missing repository or missing write access, and the
repository read it triggered was refused for the same reason, so the
caller paid an extra call to be told the wrong thing.
Return the base message for both rate limit error types so the rate
limit text stands on its own, and trim the helper and its tests to the
comments the code cannot state.
Co-authored-by: Dylan Pulver <dylanpulver@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The review request endpoint requires write access to the repository, and
GitHub refuses a caller without it with 404 Not Found rather than a
permission error. Authoring the pull request does not grant that access,
so a fork contributor can be offered a Copilot review by the web UI and
still be refused by request_copilot_review, with nothing in the tool
result to say why.
On 403 or 404 the tool now reads the repository once so it can name the
cause. A caller without write access is told so directly and pointed at
the web UI. When the repository cannot be read at all, or when write
access is present, the message says so and points at the likelier
cause.
The benchmarks and the reference-implementation equivalence harness
existed to justify the sanitizer rewrite. They have served that purpose,
so remove them along with the verbatim copy of the old pipeline they
carried.
Five checks move into sanitize_test.go rather than going away, because
none of them reference the old implementation and all of them guard
behaviour the rewrite introduced:
- isHTMLInert must be a fixed point of the live bluemonday policy,
checked byte by byte and as whole strings, with the accepted byte set
pinned explicitly. Nothing else fails if that set is widened, and
widening it changes sanitizer output.
- Both filters are fixed points on their own output, which is what
licenses Sanitize to skip its second pass.
- Clean ASCII sanitizes with zero allocations.
- Invalid UTF-8 is re-encoded to U+FFFD.
- Known payloads still lose content.
Net -560 lines.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sanitizing user-authored response fields ran multiple allocating passes
over every string regardless of content: FilterInvisibleCharacters
converted the whole input to []rune and back, FilterCodeFenceMetadata
split and rejoined every line, and bluemonday ran unconditionally. On
comment- and issue-heavy responses this dominated conversion CPU and
allocation.
Three changes, none of which alter output or widen what the policy
allows:
- FilterInvisibleCharacters scans first and copies only from the first
filtered rune, skipping ASCII runs without decoding them. Invalid
UTF-8 is still re-encoded to U+FFFD, matching the []rune round trip it
replaces.
- FilterCodeFenceMetadata walks lines in place and returns the input
when no line changes.
- FilterHTMLTags skips bluemonday for input that is provably a fixed
point of the policy: printable ASCII, TAB and LF, with none of the
five characters html.EscapeString rewrites. Sanitize also skips the
second invisible/code-fence pass when HTML normalization returned its
input unchanged, since both filters are fixed points there.
Equivalence is pinned by a verbatim copy of the previous pipeline: the
new code is diffed against it over a corpus of ~22k deterministic cases
plus two fuzz targets, and the fast path is checked byte by byte against
the live bluemonday policy.
Benchmarks (Intel Ultra 9 185H, n=6):
Sanitize/TitleASCII 5.35µs -> 114ns 1 -100% allocs
Sanitize/Comment1KiB 45.3µs -> 1.27µs 1 -100% allocs
Sanitize/Body64KiB 2.47ms -> 85.9µs 1 -100% allocs
30 issues x 2KiB body 3.00ms -> 88.6µs 1.55MiB -> 1.9KiB
100 comments x 1KiB 5.04ms -> 169µs 2.19MiB -> 6.3KiB
Content that genuinely needs rewriting still pays for it, and non-ASCII
text still goes through bluemonday by design.
Fixes#3117
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The cache changes carried explanatory comments that restated the code or
narrated what each step did. Drop them and keep only what the code cannot
express: that cache2go never reclaims a named table, that its own expiry
slides on every read, that createdAt survives entry updates, and that
RepoAccessOpts is shared across requests. Exported options keep a short
doc comment.
Comment-only; no behavior change.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Isolating identities by deriving a cache2go table name per token grew a
process-wide registry that is never reclaimed: cache2go creates each named
table on first use and never evicts it, so every distinct bearer token —
including invalid ones, since the table was built before GitHub validated
the token — permanently added a table.
Keep a single cache table and scope entries instead. WithIdentity stores a
SHA-256 digest of the identity and prefixes each entry key with it, so
different identities still cannot observe each other's trust decisions,
while per-identity state is reclaimed by the table's ordinary TTL cleanup.
WithCacheName stays for tenant/test isolation, with docs warning against
deriving names from request data.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The repo-access cache used by lockdown mode relied on cache2go's sliding
expiry: every read extends an entry's life, so a frequently-accessed
entry could keep a stale trust decision (e.g. revoked push access)
alive indefinitely instead of refreshing after its TTL.
Separately, cache2go.Cache(name) returns a process-wide singleton table
keyed by name. In HTTP mode, RequestDeps.GetRepoAccessCache built a new
RepoAccessCache per request but always reused the same default-named
table, so trust decisions computed under one caller's credentials could
be served to a different caller for the same owner/repo, without ever
validating the second caller's own access.
Fixes:
- Track each cache entry's original creation time and bound its maximum
age from that fixed point, not from last access, so entries are
refreshed after a fixed TTL regardless of read frequency.
- Add lockdown.CacheNameForIdentity, which derives a stable, hashed
cache-table name from a request identity (e.g. auth token). Two calls
for the same identity return the same name (reusing a warm cache
across a session's repeated requests); different identities always
get different names (no shared cache state).
- RequestDeps.GetRepoAccessCache now scopes each request's cache to the
requesting token's identity via CacheNameForIdentity, closing the
cross-identity leak in HTTP/multi-tenant deployments. Stdio mode is
unaffected: it constructs a single RepoAccessCache for the whole
process lifetime, as before.
Tests added:
- TestRepoAccessCacheBoundedExpiryIgnoresRepeatedAccess and
TestRepoAccessCacheNewUserDoesNotResetEntryAge exercise bounded expiry
deterministically via an injectable clock (no sleeps).
- TestCacheNameForIdentity and
TestRepoAccessCacheIdentityScopedNamesPreventCrossIdentityLeakage
cover the naming helper and cross-identity isolation at the lockdown
package level.
- TestGetRepoAccessCacheIsolatesTrustDecisionsPerIdentity in
pkg/github mirrors the HTTP server's exact construction pattern
end-to-end and fails without the dependencies.go fix.
Fixes#3107
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Trim the GetPullRequestCommits doc comment to a single terse sentence
and remove a test comment that only restated the test name.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Route every MinimalIssueRef/MinimalPullRequestRef construction through shared
constructors that sanitize the user-authored title, so issue_dependency_read,
issue_dependency_write and find_duplicate no longer forward raw issue titles.
Also sanitize the get_file_blame commit message headline, after truncation so
the headline is still cut at the author's real first line break.
Extends the sanitization regression suite with the project status update body,
both ref constructors and the dependency ref, and adds tool-level regression
tests for find_duplicate and get_file_blame.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sanitization was previously applied ad hoc at a handful of tool call
sites (GetIssue, GetPullRequest, ListPullRequests) rather than in the
shared convertToMinimal* converters, so equivalent user-authored text
returned by other tools (issue comments, PR reviews, review comments,
releases, commit messages, discussions, project item titles) was
returned unsanitized.
- Apply sanitize.Sanitize inside the convertToMinimal* helpers in
minimal_types.go for issue/PR titles and bodies, issue comments, PR
reviews, review comments, releases, commit messages, and project
item content titles. This is the single, shared conversion point
used by nearly every read tool, so fixing it there covers get/list
issues, pull requests, comments, reviews, review comments, releases,
commits, and project items consistently.
- Add a sanitizeIssueTitleAndBody helper and use it for the two
response paths that marshal a raw *github.Issue directly instead of
a Minimal* type: search_issues (SearchIssueResult.MarshalJSON) and
search_pull_requests (searchHandler).
- Sanitize discussion titles/bodies/comments (list_discussions,
get_discussion, get_discussion_comments), which previously had no
sanitization at all, via a new newMinimalDiscussionComment
constructor and inline fixes.
- Sanitize project status update bodies.
- Remove the now-redundant scattered sanitize calls in GetIssue,
GetPullRequest, and ListPullRequests now that the shared converters
sanitize on their own.
Patches, diffs, and raw file contents are intentionally left
untouched to preserve fidelity.
Adds table-driven regression tests covering every touched converter,
the search_issues/search_pull_requests raw-passthrough paths, and a
fidelity check that patches/diffs are not altered.
Fixes#3106
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
pull_request_read's get_commits method previously returned commit
messages without any lockdown check, unlike get_diff and get_files
which restrict the whole result when the PR author lacks push access.
Commit content is part of the same untrusted head branch as the diff
and file list, so GetPullRequestCommits now reuses
enforcePullRequestLockdown for consistent, fail-closed behavior
without adding a per-commit permission lookup.
Also updates the lockdown documentation in README.md and
docs/server-configuration.md to:
- list pull_request_read:get_diff, get_files, and get_commits among
the tools that error when the PR author lacks push access (get_diff
and get_files were already implemented this way but undocumented)
- clarify that lockdown mode is a best-effort content filter to
reduce prompt-injection risk, not an authorization boundary
- document the existing intentional trusted-bot exception
(github-actions[bot], copilot) accurately
Fixes#3105
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Only tolerate GraphQL schema validation failures that show the optional Issue.issueFieldValues selection or its known fragments are unsupported. Surface client, auth, rate-limit, network, resolver, malformed response, and unrelated GraphQL failures.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Clarify that delete:false is ignored, retain mutual exclusion for delete:true, and reject invalid delete types. Add focused schema and handler regressions.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The `delete` property of issue_write's issue_fields items was declared with
Enum: []any{true}, making true its only legal value. The property is optional,
but a client that fills every property of a schema -- common, since
OpenAI-style strict function calling requires every property to appear in
`required` -- had no way to express "not deleting this field": there is no
false in the enum and no null in the type. `value` offers no alternative
either, being typed ["string","number","boolean"] with no null.
The MCP Go SDK validates arguments against the resolved input schema before
the handler runs, so delete: false was rejected at schema validation and never
reached optionalIssueWriteFields. Such clients sent delete: true alongside a
value instead and hit the handler's mutual-exclusion check, so issue_write
could never set an issue field for them.
Remove the enum so false is a legal no-op, and document that omitting the
property or setting it to false leaves the field unchanged. No handler change
is needed: the code already branches on `if deleteField`, so false falls
through to the normal value path, and the mutual-exclusion check for
delete: true still applies. Add tests for optionalIssueWriteFields, which had
none.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Report contentless large symlink targets as not returned while preserving their ResourceLink and requested-path identity.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 21420b11-5dae-49b6-ac77-965faec7f88b
Keep the lazy blob-identity check and bounded tree fallback while consolidating metadata handling and request-count tests.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 21420b11-5dae-49b6-ac77-965faec7f88b
Detect internal symlink dereferences from Git blob identity mismatches, disclose explicit links and submodules, and preserve requested-path resource output. Use bounded exact-path tree inspection only when inline content is unavailable.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Remove persistent repository and push-files guidance in favor of concise runtime recovery content. Resolve refs directly through Git Trees, skip inspection for explicit symlinks and opt-ins, and lock request counts in tests.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Detect existing symlinks through the Git tree and require an explicit opt-in before changing their targets. Return the resolved repository target so callers can safely update the linked file instead.
Refs #2997
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Normalize missing or zero-length tool arguments to an empty object while preserving invalid JSON and required-parameter validation.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Handle alternate issue-field validation messages, preserve primary and retry errors, and avoid runtime result type switches.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Retry list_issues without custom issue field dependencies only when the
host schema lacks them. Preserve explicit field filters and propagate
unrelated GraphQL errors.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Verify issue_read returns assigned logins and a definitive empty array for unassigned issues. Exercise the same empty-array contract through list_issues field filtering.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The list_issues GraphQL fragment never selected assignees, so the tool
could not report who an issue was assigned to. Its nearest field, user,
is the issue author, which callers conflate with the assignee. Answering
"is anything unassigned?" therefore cost one list_issues call plus one
issue_read per candidate, and a truncated sweep invites a fabricated
answer drawn from the author instead.
Add an assignees selection to IssueFragment, flatten it to logins in
fragmentToMinimalIssue, and add "assignees" to listIssuesItemFieldEnum so
it is selectable through fields. GitHub caps issue assignees at 10, so
first: 100 cannot truncate; it also matches the page size already used
for assignees in copilot.go.
Drop omitempty from MinimalIssue.Assignees and initialize the slice in
both converters so an unassigned issue serializes as [] rather than an
absent key, which is what lets a caller identify unassigned issues from
a single response. This also affects issue_read, the other MinimalIssue
consumer, which now reports "assignees": [] instead of omitting the key.
Both tools expose a delete action, so the tool-level annotation must remain
conservative even though their other actions only update preferences.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: be9c15fe-a113-4ebd-b9ab-09a520d522b6
create_branch currently forwards the compact 422 dump, which hides
ruleset details the GitHub UI already shows. Unwrap ErrorResponse so agents
can see each validation message and recover.
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
* Add visible fields to project views
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1421a5d5-fdce-4c0e-9528-56d555ec30d4
* Fail fast and surface orphaned views on project view writes
Reject roadmap layouts before enumerating project fields in both the
create and update paths, and verify view ownership before resolving
visible fields on update, so rejected requests no longer pay for a
paginated field listing.
Skip the follow-up filter mutation when the filter is explicitly null,
since a new view has no filter to clear, and include the created view ID
when cleanup after a failed filter mutation also fails so the caller can
recover the orphaned view.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1421a5d5-fdce-4c0e-9528-56d555ec30d4
* Add basic project view management
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c6f8ede6-efee-4191-900d-59a1bb0af000
* Harden project view mutations
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c6f8ede6-efee-4191-900d-59a1bb0af000
* Resolve project view fields by name
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c6f8ede6-efee-4191-900d-59a1bb0af000
* Clear project view filters with explicit null
Align the filter parameter with the nullable-parameter convention: omit
to preserve, pass null to clear. Empty strings are now rejected rather
than treated as a clear sentinel. The GraphQL and REST wire format is
unchanged, since the API still clears a filter with an empty string.
Also replace the "<nil>" string comparison in deleteProjectView with a
direct nil check on the returned ID.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Use caller-specific project field hints
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c6f8ede6-efee-4191-900d-59a1bb0af000
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c6f8ede6-efee-4191-900d-59a1bb0af000
Return compact response types for workflow run and workflow job lists while retaining diagnostic, step, and runner metadata.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0eecbca7-7271-4a04-8d28-d952c27ed9c1
* Order list_label results by issue count (descending)
Sends orderBy: {field: ISSUE_COUNT, direction: DESC} on the GraphQL
labels query so the most-used labels (by issue count) are returned
first. ISSUE_COUNT is accepted by the GitHub GraphQL API but is not
part of the public schema docs or the githubv4 client library's
LabelOrderField constants, so it is defined locally.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* regen docs
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Add non-default find_duplicate tool gated by duplicate_detection flag
* Trim find_duplicate output to spec fields and relax confidence_threshold bounds
* Attach repo-visibility IFC label to find_duplicate results
* Return closing pull requests from issue_read
Answering "is there a PR that closes this issue?" previously required
listing pull requests and grepping their bodies for closing keywords,
which is expensive and unreliable. GraphQL already exposes
Issue.closedByPullRequestsReferences.
Add it to the existing issue_read `get` enrichment query so the answer
comes back in the same round-trip as the hierarchy signals, as a compact
`closed_by_pull_requests` list. An enriched issue with no closing pull
requests serializes an explicit empty list so an agent can stop looking.
Lockdown mode filters references whose author cannot be verified as safe
content, mirroring the existing parent reference handling.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a0b58914-0d94-47a9-8229-c0ef7e32e69f
* Cap embedded closing pull requests and report the total
This enrichment runs on every issue_read get, so embedding up to 25
references costs more than the common case is worth. Embed at most 5,
keeping orderByState so open pull requests are the ones that survive.
Select totalCount alongside the nodes and return the summary as an
object of total_count plus references, so the rare issue with more than
five linked pull requests cannot be read as a complete list. The common
zero-to-two case stays compact and an empty result stays definitive.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a0b58914-0d94-47a9-8229-c0ef7e32e69f
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a0b58914-0d94-47a9-8229-c0ef7e32e69f
The content parameter is passed to the API as plain text and the server
base64-encodes it, but the description said only "Content of the file".
The REST endpoint this wraps documents its own content field as base64,
so a model reading the tool description has a strong reason to encode the
content itself. When it does, the server encodes again and the file is
committed containing base64 text. Every layer reports success.
Describe the value by how it should end up on disk rather than by what
not to do, so a file whose contents are legitimately base64 is still
unambiguous, and name the encoding step so the conflict with the REST
API docs is resolved rather than merely overridden.
Documentation Check / docs-check (push) Has been cancelled
golangci-lint / lint (push) Has been cancelled
GoReleaser Release / release (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
MCP Server Diff / mcp-diff (push) Has been cancelled
MCP Server Diff / mcp-diff-http (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
Docker / build (push) Has been cancelled
Add a regression test locking in the capability contract set by NewMCPServer:
tools, prompts, and resources are advertised without list-changed
notifications, the deprecated logging capability is not advertised, and the
inferred completions capability is preserved. Covers both the stdio path (full
inventory, items present) and the HTTP path (inventory emptied for the
discovery request), which share the same NewMCPServer entry point.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 95b8432c-f280-472e-a242-d3ca6dc31f19
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