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.
- 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/repositoryargument. - 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
repositoriesrestriction 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_pagesorpatch_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
.wikiGit 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.
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.
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: omitfilesand aggregatestats.stats(the new default): keep file metadata but omit everypatch.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.
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.
| 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.
Install the subproject into the repository virtual environment:
.\.venv\Scripts\python.exe -m pip install -e .\github_app_mcpRun all isolated tests:
.\.venv\Scripts\python.exe -m unittest discover `
-s .\github_app_mcp\tests -p "test_*.py" -vFor 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.exeTo 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-repositoryThe 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.