Skip to content

spike: context-scoped MCP server modes (repo / pull-request / project)#2693

Draft
SamMorrowDrums wants to merge 2 commits into
mainfrom
sammorrowdrums/scoped-server-modes-spike
Draft

spike: context-scoped MCP server modes (repo / pull-request / project)#2693
SamMorrowDrums wants to merge 2 commits into
mainfrom
sammorrowdrums/scoped-server-modes-spike

Conversation

@SamMorrowDrums

@SamMorrowDrums SamMorrowDrums commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

What this is

A spike / prototype (draft, not for merge) exploring context-scoped MCP server modes. The operator binds the server once to a single GitHub context — a repository, a pull request, or a ProjectsV2 project — and the server then presents a bespoke, purpose-built tool surface rather than a reduced copy of the full GitHub server.

The guiding principle is interface-first: a scoped server should look like it was designed for that one context, not like the general server with parameters stripped out.

⚠️ Expected CI: MCP Server Diff (stdio) is red on this PR

This is expected for the introducing PR and not a head-side bug. The diff action runs each config against both the PR tree and a baseline checkout of main. The four scoped configs (scope-repository, scope-repository+read-only, scope-pull-request, scope-project) pass the new --repository / --pull-request / --project flags, which don't exist on main yet, so the baseline server exits immediately → MCP error -32000: Connection closed.

The head server starts cleanly on all four (verified locally: 16 / 10 / 9 / 3 tools with bespoke titles), and the surfaces are locked by per-surface toolsnaps. The diff check resolves automatically once the flags are on main (i.e. there's a baseline to diff against). The configs are intentionally left wired into the matrix so the surfaces are tracked from then on.

How it works

New pkg/binding package transforms the tool universe for the selected scope. For each admitted tool it:

  • Removes the context-identifying params (owner, repo, pullNumber, owner_type, project_number) from the advertised input schema and injects the fixed values server-side at call time.
  • Narrows the method enum to the operations the scope supports, pruning disallowed values from the advertised schema (not merely rejecting them at runtime) so the schema is an honest description of the surface.
  • Rewrites the tool description so the surface reads as bespoke.
  • Enforces the boundary in the handler: caller-supplied fixed/rejected params are refused, denied methods are blocked, and scoped search queries that could escape the bound context (cross-context qualifiers or boolean grouping) are rejected.

Membership is an explicit per-mode manifest (fail-closed): a newly added server tool is invisible to a scoped surface until it is deliberately admitted.

Surfaces (default flags)

Mode Bound context Tools
--repository owner/repo one repo 16 (10 read-only)
--pull-request owner/repo#N one PR (+ repo reads for context) 9
--project owner_type/owner/N one ProjectsV2 project 3

Wiring

  • NewScopedInventory pre-transforms the universe before the existing inventory filter pipeline — read-only, feature flags, and PAT-scope filters still apply on top.
  • Mutually-exclusive stdio flags --repository / --pull-request / --project select the scope.
  • The scoped server advertises a bespoke title and instructions stating the context is fixed.

Validation

  • Adversarial + singleton-safety unit tests (supplying a fixed param, a repo: query qualifier, or a denied method are all rejected; the package-level tool singletons are never mutated).
  • Fail-closed manifest coverage (every admitted tool exists and has a description).
  • Per-surface toolsnaps under pkg/binding/__toolsnaps__/{repo,pull_request,project}/ lock the advertised API per surface, so any tool change must be re-wired into every surface to keep snapshots green.
  • The mcp-diff config generator gains scoped stdio entries so the diff workflow tracks these surfaces too (see the CI note above).

script/lint clean, full script/test green.

Design notes / decisions

  • Confinement boundary, not just convenience: a scoped server structurally cannot reach another context through an admitted tool.
  • Project mode is intentionally cross-repo: add_project_item accepts any item_owner/item_repo because ProjectsV2 projects legitimately aggregate issues/PRs from across GitHub. The bound context is the project; cross-repo item references are in-scope by design.
  • PR mode includes a couple of {owner,repo}-bound repository reads (get_file_contents, get_commit) for reviewer context — still single-repo confined.

Deferred (out of scope for this spike)

  • HTTP scoped roots/middleware (/repos/{owner}/{repo}, /repos/{owner}/{repo}/pulls/{n}, /projects/{owner_type}/{owner}/{n}) + header-based scope config (which would also give the scoped configs an http-headers diff path).
  • Scoped resource templates + prompts (currently dropped in scoped modes).
  • A combined multi-project / project+repo mode.

🤖 Spike generated with assistance from Copilot. Not intended for merge as-is — opening to review the approach.

SamMorrowDrums and others added 2 commits June 15, 2026 13:46
Prototype a binding layer that pins the MCP server to a single GitHub
context so it presents a bespoke, purpose-built tool surface rather than
a reduced copy of the full server.

A new `pkg/binding` package transforms the tool universe for one of three
scopes — a repository, a pull request, or a ProjectsV2 project. For each
admitted tool it:

- removes the context-identifying params (owner, repo, pullNumber, ...)
  from the advertised input schema and injects the fixed values at call
  time;
- narrows the `method` enum to the operations the scope supports, pruning
  disallowed values from the schema (not just rejecting them at runtime);
- rewrites the tool description so the surface reads as bespoke;
- enforces the boundary in the handler: caller-supplied fixed/rejected
  params are refused, denied methods are blocked, and scoped search
  queries that could escape the bound context (cross-context qualifiers
  or boolean grouping) are rejected.

Membership is an explicit per-mode manifest (fail-closed): a new server
tool is invisible to a scoped surface until it is deliberately admitted.

Wiring: `NewScopedInventory` pre-transforms the universe before the
existing inventory filter pipeline (read-only, feature flags, PAT scopes
still apply); `--repository` / `--pull-request` / `--project` stdio flags
select the scope; the scoped server advertises a bespoke title and
instructions.

Validation: adversarial + singleton-safety unit tests, fail-closed
manifest coverage, and per-surface toolsnaps under
`pkg/binding/__toolsnaps__/{repo,pull_request,project}/` so tool changes
must be re-wired into every surface. The mcp-diff config generator gains
scoped stdio entries so the diff workflow tracks these surfaces too.

Deferred: HTTP scoped roots/middleware, scoped resources + prompts, and a
combined multi-project mode.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Scoped tool descriptions previously used generic relative phrasing
("this repository" / "this pull request" / "this project") that never
named the bound resource. Render each manifest description as a
text/template against the bound Context so the advertised surface names
the concrete resource it operates on (e.g. octocat/Hello-World#42),
reinforcing the bespoke, purpose-built feel.

- Add RepoRef/PullRef/ProjectRef helpers to Context.
- Render Description templates in bindTool (missingkey=error, fail loud
  on parse/exec error; non-template descriptions pass through unchanged).
- Rewrite repo, pull_request, and project manifest descriptions to name
  the resource via RepoRef/PullRef/ProjectRef.
- Regenerate per-surface toolsnaps; add tests locking rendered output
  and malformed-template failure.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

1 participant