Skip to content

Latest commit

 

History

History
498 lines (435 loc) · 26.3 KB

README.md

File metadata and controls

498 lines (435 loc) · 26.3 KB

IssueLens GitHub MCP server

This subproject is the stdio MCP server for the GitHub operations that IssueLens currently needs. The host starts session-owned shared servers and a separate team-memory agent-local server exposing only the write_wiki_pages and patch_wiki_pages tools through the parent-supplied internal --wiki-writer mode. This mode is automatic; users need no environment flag.

Wiki Git network and object operations use the Dulwich Python library (1.2.14), not git.exe, the Git CLI, or a Git subprocess. The Foundry ZIP codeConfiguration uses remote_build with runtime: python_3_13 and installs the root requirements.txt; the standalone MCP github_app_mcp/pyproject.toml declares the same Dulwich dependency. No Git installation, Dockerfile change, or runtime installer is needed in either mode. IssueLens still launches its stdio MCP server as a Python subprocess; the wiki backend never spawns Git, SSH, or credential helpers.

Security model

  • The GitHub App private key is loaded only from an Azure Key Vault secret URI through DefaultAzureCredential. Private-key contents are never accepted as command-line arguments or environment variables.
  • Every tool requires an explicit owner/repository argument.
  • REST reads prefer a repository-scoped GitHub App token and fall back to anonymous access only for public repositories. Private reads and every write require an App installation.
  • Installation tokens are minted for one repository using GitHub's repositories restriction and for only the permission required by the tool. The cache key includes the repository and permission set.
  • Issue write tools are absent from MCP discovery unless the trusted host sets GITHUB_MCP_ENABLE_WRITES=true. The GitHub client enforces the same gate.
  • Only the team-memory agent-local server exposes wiki writes. Shared reader/triage servers do not expose write_wiki_pages or patch_wiki_pages. Tool availability and repository policy do not authorize a maintenance job to write.
  • The server exposes fixed GitHub REST routes. It has no generic HTTP, REST, or GraphQL tool. Wiki operations use the bundled .wiki Git backend for only the validated wiki destination's .wiki.git, with no caller-selected remote.
  • Search qualifiers cannot change repository, organization, or user scope. Repository paths, pagination, file sizes, comments, names, and tool results are bounded.
  • The stdio server writes no application output to stdout outside MCP framing.

Install the App on every target repository and any private related repository IssueLens is authorized to access. Public related repositories can be searched through bounded anonymous reads without an installation.

Tools

Execution contract

Every tool returns the same envelope in both MCP text and structured content:

{
  "success": false,
  "outcome": "not_applied",
  "result": null,
  "error": {
    "type": "conflict",
    "message": "wiki base conflict; re-read the snapshot and reconcile changes before writing",
    "http_status": null
  }
}

On success, success is true, outcome is completed, result contains the documented domain payload, and error is null. Empty lists and no-change results are still successful operations. On failure, MCP isError is true as well; not_applied means no requested effect was applied, while unknown means a write may already have happened. A timeout, missing acknowledgement, invalid write response, or failed publication verification must not be treated as proof that no write occurred. Known HTTP status codes are retained; null does not mean HTTP 0. Unhandled exceptions produce a safe internal_error, not raw exception text. Result byte limits include the envelope.

Tools perform one logical operation. Scoped authentication, input validation, bounded read scans, and publication verification are implementation steps, not additional business operations. Failed operations are not retried or rebased inside tools. Stale installation-cache entries are invalidated without retrying token minting in the same call. Public read-only access selection remains supported, never as a fallback for writes. The owning agent interprets the result and decides whether to correct inputs, reconcile state, retry within its budget, or escalate. This is the same contract used by the host configuration and notification tools.

The shared server registers these REST read tools. The permission shown is used when an App installation is available; public repositories can fall back to anonymous access:

Tool Preferred App permission
get_repository Metadata: read
list_issues Issues: read
get_issue Issues: read
list_issue_comments Issues: read
get_issue_comment Issues: read
list_issue_reactions Issues: read
search_issues Issues: read
list_labels Issues: read
get_file Contents: read
get_pull_request Pull requests: read
list_pull_request_file_manifest Pull requests: read
list_pull_request_files Pull requests: read
list_pull_request_commits Pull requests: read
list_pull_request_reviews Pull requests: read
list_pull_request_review_comments Pull requests: read
get_commit Contents: read
compare_commits Contents: read
list_repository_tree Contents: read
search_repository_content Contents: read
list_merged_pull_requests Pull requests: read

With a supplied ref, search_repository_content scans an immutable snapshot: at most 64 regular files, 256 KiB of eligible content (64 KiB per file), and 66 content API requests for a full SHA (one lightweight /git/commits object, one tree, and up to 64 blobs). Branch/tag inputs resolve through fixed /git/ref/heads, /git/ref/tags, and at most eight /git/tags reads before the immutable commit-object read: at most ten additional requests, still inside the same scan deadline. HEAD first reads the repository's default branch. Search never downloads the patch-bearing /commits/{ref} response. Abbreviated SHA inputs remain supported using a bounded, metadata-only one-item commit list after branch/tag lookup misses, then the full Git commit object; this is not the patch-bearing commit-detail endpoint. App authentication lookup/minting is performed once per scan; initial authentication overhead is additional and is not counted in those 66 content requests. One HTTP client is reused for all content requests in a scan, never shared across scans, and closed on success, error, cancellation, or deadline expiry. A fixed 60-second overall scan time budget includes authentication, commit/tree/blob response-body reads, and local result construction; it never resets per request. Exceeding it fails explicitly without returning partial results or falling back to indexed search. Host cancellation propagates after cleanup. A missing-installation result is retained only for that scan; no negative authentication cache survives the scan. Anonymous fallback remains read-only and never authorizes writes. Without ref, search uses GitHub's indexed default-branch code search with its existing single-request 30-second HTTP timeout, not the scan budget.

Paged PR and commit reads

Large changes use existing tools and ordinary Copilot tool/model turns, not custom diff readers or an analysis runtime.

get_pull_request pins its metadata request to GitHub REST API 2022-11-28 to retain the authoritative merge_commit_sha needed for merged-source verification. GitHub removed that field in API version 2026-03-10. The pin applies to both App-authenticated and anonymous public PR reads; other REST operations and App token minting retain 2026-03-10. No additional permissions, fallback lookup, or inferred merge SHA is introduced. An open PR's null or temporary merge SHA is preserved as returned by GitHub and is not evidence of a completed merge. Removing this compatibility pin requires an authoritative replacement for merged-commit verification.

get_commit(repository, sha, detail="stats", per_page=30, page=1) adopts the official GitHub MCP detail semantics:

  • none: omit files and aggregate stats.
  • stats (the new default): keep file metadata but omit every patch.
  • full_patch: include available patches; request small pages for large changes.

IssueLens retains its repository and snake-case pagination parameters, SHA validation, and REST commit metadata including parents and tree identity. This is supported control alignment, not drop-in compatibility with every upstream tool or response projection. Existing callers needing patches must now explicitly request full_patch. Aggregate commit stats describe the commit, not an individual page; do not add them again for every page.

Commit-detail and PR-manifest downloads have a 1 MiB streamed HTTP ceiling so metadata projection can process responses above the ordinary 128 KiB HTTP limit. Projection precedes the unchanged 100,000-byte ASCII JSON result ceiling. Other REST tools retain their existing limits. Oversized input or output fails explicitly; no file or patch is silently truncated to fit.

list_pull_request_file_manifest projects GitHub's REST changed-file response to metadata before applying the model-result limit, removing patches and resource URLs. Start with up to 100 files and follow next_page; compare the unique paths observed with get_pull_request.changed_files. This compact inventory supports an explicit evidence plan before raw change content enters the model context. Its download uses the same bounded 1 MiB allowance as commit-detail projection, and public repositories retain anonymous-read fallback when no App installation is available.

list_pull_request_files keeps its REST list response and includes available patches for evidence groups that need the exact diff, rename metadata, or deletion context. Start with a bounded page such as 5 rather than one file per model turn. Reduce per_page after a size error, down to 1. When changing page size, restart at page 1 and deduplicate after confirming source identity; otherwise the changed offsets can omit files. REST file pagers accept per_page=1..100 and page=1..3000, allowing small pages beyond the ordinary 100-page limit. Empty or short pages end enumeration. Both PR file APIs remain subject to GitHub's 3,000-file ceiling. PR commit lists have a separate 250-commit cap; compare_commits exposes at most 300 files and has no pagination here.

PR file manifests and patch pages cannot be pinned to a SHA. The capability skill checks PR base/head SHAs and changed-file counts before and after paging, and requires re-establishing evidence if they changed. Commit pages and get_file context reads should use full verified SHAs. Missing patches, binary content, oversized individual files, and API caps remain explicit limitations. get_file still supports only bounded UTF-8 files up to 64 KiB; no automatic resource-link download or arbitrary range-read support is implied.

Wiki tools always take repository as the source project, even when its memory is stored in another repository's wiki. The shared package policy parser validates the source's structured instructions.team_memory with a required policy path and optional wiki_repository. The latter is a GitHub parent repository identifier, not a wiki UI name or Git URL. The config tool returns resolved wiki_repository alongside policy content; Markdown guides only organization and topics, never target, arbitrary Git URL, token, or shell settings.

Every wiki read/write tool independently re-reads and validates that same mapping and resolves credentials and transport to the destination. An omitted field, config, or domain defaults to the source project's own wiki. A misconfigured or inaccessible target fails without silent fallback. The App must be installed at the destination with the required read/write permission, not just at the source; tokens are scoped to that actual destination and operation. Wiki reads require that App access rather than the anonymous fallback used by public REST reads. Installation access is not source-user authorization.

Validated team_memory.wiki_repository selects only the wiki capability's destination; it does not broaden source reads, other writes, or notifications. Do not publish private/internal-source knowledge to a public wiki or read a private/internal wiki for public-source context. Cross-repository mappings between private/internal repositories are rejected for both reads and writes because their audience relationship cannot be verified; use the source project's own wiki. No audience or ACL matching mechanism is implemented. Same-repository access skips the cross-repository audience comparison and remains supported, as do public-to-public mappings. A private/internal source may read a public wiki, and a public source may write public information to a private/internal wiki, subject to job authorization and destination App access.

The shared read-only server also registers these wiki read tools, which use destination-scoped App access:

Tool Purpose Required App permission
get_wiki_snapshot Resolve the initialized wiki's default branch and full SHA Contents: read
list_wiki_pages List bounded Markdown pages at a snapshot Contents: read
get_wiki_page Read a bounded page at a snapshot Contents: read
search_wiki Search pages at a snapshot Contents: read
list_wiki_history Read bounded history at a snapshot Contents: read
get_wiki_diff Compare explicit wiki revisions Contents: read

Every successful wiki response includes source_repository and the actual resolved wiki_repository. list_wiki_pages, search_wiki, and list_wiki_history return their original lists under result; get_wiki_diff returns its original diff string under result. Empty lists and empty diffs use the same envelope. Callers must read this field instead of treating these responses as bare lists or text. Snapshot, page, and write responses retain their existing top-level payload fields alongside the identity metadata. The complete response, including the envelope, remains subject to result-size and credential-exposure checks. Continue passing the source project to tools, not the destination returned in the metadata.

Pin page, search, and history reads to the same full SHA and compare explicit SHAs for diffs. Only HEAD or full SHAs are accepted as wiki refs. Keep private knowledge separate from public wikis and results; anonymous public reads never authorize writes. Merged PR metadata, files, commits, and source reads provide evidence, not authority to update the wiki.

Issue write tools are registered only when GITHUB_MCP_ENABLE_WRITES is enabled:

Tool Minimum token permission
add_labels Issues: write
set_assignees Issues: write
add_issue_comment Issues: write
add_eyes_reaction Issues: write for issue, pull-request body, and issue-comment targets; Pull requests: write for pull-request review comments

add_eyes_reaction accepts only issue, pull_request, issue_comment, and pull_request_review_comment targets and always posts {"content":"eyes"} to the corresponding fixed GitHub reaction route. GitHub returns 201 for a new reaction and 200 for the existing reaction when the same App repeats the request, so no reaction pre-read or separate retry tracker is needed. The acknowledgement remains after processing finishes.

Direct wiki maintenance

write_wiki_pages and patch_wiki_pages are available only to the team-memory agent and require an App token scoped to the mapped destination with Contents: write. They write only an existing initialized wiki. For source microsoft/project configured with wiki_repository: microsoft/team-knowledge, the tool still accepts the source project, not the destination:

snapshot_response = get_wiki_snapshot(repository="microsoft/project")
if not snapshot_response["success"]:
  raise RuntimeError(snapshot_response["error"]["message"])
read_snapshot = snapshot_response["result"]
write_response = write_wiki_pages(
  repository="microsoft/project",
  pages={"Architecture.md": full_utf8_content},
  expected_wiki_repository=read_snapshot["wiki_repository"],
  expected_base=read_snapshot["sha"],
  message=short_summary,
)

For an existing page, prefer an exact patch so the model does not reproduce unrelated content:

page_response = get_wiki_page(
  repository="microsoft/project",
  path="Architecture.md",
  ref=read_snapshot["sha"],
)
page = page_response["result"]
patch_response = patch_wiki_pages(
  repository="microsoft/project",
  patches={
    "Architecture.md": {
      "expected_sha": page["sha"],
      "replacements": [{"old": exact_anchor, "new": replacement_with_anchor}],
    },
  },
  expected_wiki_repository=read_snapshot["wiki_repository"],
  expected_base=read_snapshot["sha"],
  message=short_summary,
)

Every non-empty old anchor must match exactly once in the original pinned page. Replacements are evaluated against that original content and cannot overlap. The backend rejects changed page SHAs, missing or repeated anchors, overlaps, and resulting limit violations before publishing. All untouched content is preserved byte-for-byte.

Both expected values are required and come from the same snapshot read. expected_wiki_repository is a precondition, never a destination override. It must be a valid GitHub owner/repository identifier and match the freshly resolved policy destination case-insensitively before any destination metadata, token lookup, or wiki access. Policy still selects the actual destination and scoped App credentials. A mismatch is rejected even if the SHA is unchanged.

read_snapshot["sha"] is the full SHA returned by the snapshot read. write_wiki_pages accepts complete UTF-8 contents for changed .md pages. patch_wiki_pages accepts 1-20 existing pages, at most 40 exact replacements, and 256 KiB of replacement input. Resulting pages share the normal limits of 64 KiB per page and 256 KiB per batch. Create/update only; deletion and rename are unsupported. Pages cite evidence and include the full source commit SHA, not an abbreviation, where relevant; the short commit message also includes that full SHA. No generic URL, force, token, credential, or approval arguments are accepted.

Typed validation, byte and cooperative time budgets, and redirect denial bound the internal HTTPS x-access-token transport. Only SHA-1 Git repositories (GitHub's current format) are supported; SHA-256 repositories are rejected. Operations use bounded temporary PACK storage and in-memory Git objects, without a full worktree checkout, hooks, filters, or Git config discovery. Timeouts are cooperative across socket, library, and DNS operations, not a hard CPU deadline. Diffs report binary-change notices, not binary patches; unchanged assets are preserved byte-for-byte and page deletion is unsupported.

The backend in src/issuelens_github_mcp/wiki.py owns snapshots and an atomic Git commit, persisting knowledge and history with a non-force, exact-old-ref update. The new commit's only parent and ref lease are the supplied expected_base. A call attempts at most one publication, without an internal rebase or retry. Stale bases and ref races return conflict; explicit receive-pack rejections return conflict when recognized or publish_rejected otherwise, with outcome: not_applied. Missing acknowledgements, lost responses, and failed post-push verification return outcome: unknown. Messages are safe diagnostics, never raw remote stderr. An entirely matching batch may return no-change on a verified descendant; partially matching batches, mode changes, and rewritten history remain conflicts.

No separate host publisher, database, or proposal/approval persistence is involved. If no knowledge changes, do not write. On a stale-base conflict, the agent may re-read and regenerate against a fresh snapshot in separate calls. It must preserve concurrent edits, re-establish authorization if the destination changed, and stop on ambiguous human intent or repeated failure; never blindly retry. If a response was lost, compare desired contents with current pages first. On a destination mismatch, read a fresh snapshot and re-establish destination, authorization, and evidence; never automatically overwrite or reuse edits against another wiki, or merely replace the expected repository to retry. Report the new SHA and status only when confirmed by the tool result, not merely because a write was attempted.

Only an explicit current request or parent handoff authorizing a wiki update for that target authorizes maintenance, regardless of its origin. Sensitive, conflicting, destructive, or unsupported requests need ordinary human interaction. The read-only skill never writes or delegates ordinary reads to the writer. Git provides knowledge, history, and conflict detection, not a durable job queue, reconciliation service, external scheduler, or guaranteed exactly-once delivery. The optional post-merge dispatcher uses only the pinned standalone dispatch action to validate the target and dispatch source repository, run/attempt, and requested before/after SHAs to the queued coordinator. The dispatcher does not authenticate to Azure or invoke the agent. Only the coordinator performs Azure OIDC login and agent submission through the shared IssueLens action with request-type: team-memory. The central workflow owns the queue and only calls the action (plus trusted local checkout); the invocation action internally authenticates the allowed source, verifies its run/head and current default-branch ancestry, and retrieves the complete bounded commit range through paginated GitHub reads before Azure login. There is no source script, checkout, upload, or download. A trusted coordinator source-name/ID map permits public cross-repository sources with independent source-read credentials; the IssueLens pilot leaves it empty, accepting only its own source. The generic dispatcher is self-contained and never imports invocation scripts, prepares job evidence, or grants job authorization. This is authorized range reconciliation, not attestation of an original push boundary: the run API cannot recover original before or push flags. The request preserves this limitation and keeps source and coordinator revisions distinct. Automatic push requests and manual PR requests share the coordinator's queue. This pilot accepts only microsoft/IssueLens sources and its configured wiki; external direct-action consumers remain supported but are not queued by it. The shared IssueLens action also supports issue-loop and direct tasks without imposing the wiki result schema on their answers. Neither action accesses the wiki directly or holds App credentials. Task constraints and result formats are supplied in the request, not assumed by the agent or the tools. The agent revalidates source evidence and uses these same tools, privacy guards, and paired write preconditions. See the setup and retry guidance.

Configuration

Environment variable Required Description
GITHUB_APP_ID Yes Numeric GitHub App ID
GITHUB_APP_PRIVATE_KEY_SECRET_URI Yes Azure Key Vault secret URI containing the App PEM
GITHUB_MCP_ENABLE_WRITES No false by default; enables issue write tools only for an authorized session

Wiki destination configuration belongs only in the source project's structured instructions.team_memory.wiki_repository; there is no per-repository App environment configuration. The parent supplies --wiki-writer automatically only for the team-memory agent-local MCP server with tools: ["write_wiki_pages", "patch_wiki_pages"]. Users need no environment flag. The existing GITHUB_MCP_ENABLE_WRITES gate remains for triage issue writes and does not enable wiki writes or require a new user setting. Explicit job authorization and destination App installation access are both still required.

The process identity needs Azure Key Vault secret get permission. The GitHub App needs Metadata read, Contents read, Issues read/write, and Pull requests read/write for the issue toolset; wiki maintenance additionally needs Contents write at the destination. GitHub narrows each minted token below the App's maximum permissions. All private-key, installation, and token caches live only in the stdio process and are discarded when that session-owned process exits.

Local verification

Install the subproject into the repository virtual environment:

.\.venv\Scripts\python.exe -m pip install -e .\github_app_mcp

Run all isolated tests:

.\.venv\Scripts\python.exe -m unittest discover `
  -s .\github_app_mcp\tests -p "test_*.py" -v

For a real local server, authenticate to Azure using a credential supported by DefaultAzureCredential, set the required variables above, and run:

.\.venv\Scripts\issuelens-github-mcp.exe

To check whether the configured App is installed for one or more repositories, run the diagnostic script from the repository root:

.\.venv\Scripts\python.exe .\github_app_mcp\scripts\check_installations.py `
  microsoft/IssueLens microsoft/another-repository

The script loads the root .env file when python-dotenv is installed, reads the App private key from Key Vault using DefaultAzureCredential, and prints only the installation status and installation ID. Exit code 0 means every target is installed, 1 means at least one target is not installed, and 2 means configuration, authentication, or GitHub API validation failed.

Equivalent Copilot SDK stdio configuration shape:

{
    "type": "stdio",
    "command": r"C:\path\to\.venv\Scripts\issuelens-github-mcp.exe",
    "env": {
        "GITHUB_APP_ID": "123456",
        "GITHUB_APP_PRIVATE_KEY_SECRET_URI": (
            "https://vault-name.vault.azure.net/secrets/github-app-key"
        ),
        "GITHUB_MCP_ENABLE_WRITES": "false",
    },
    "tools": ["*"],
}

Do not add private-key contents to this configuration.