IssueOps MCP
Provides tools for GitHub issue triage: read-only access to list and inspect issues, plus proposal tools that queue label, comment, close, or assignment actions for human approval instead of executing them directly.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
IssueOps MCP
A human-in-the-loop GitHub issue triage system. An MCP client (Claude) or a triage agent reads issues and proposes changes (comments, labels, assignees, closing). Nothing reaches GitHub until a person approves it in a Streamlit dashboard, and every read, proposal, and decision is audit-logged.
Runs on free tiers: Neon Postgres, Groq for the classifier, and separate fine-grained GitHub PATs for read and write.
Preview
Additional screenshots in
assets.
Related MCP server: GitHub Tools MCP Server
What this is
For an allowlisted repository, the system:
Exposes 10 MCP tools: 5 read, 5 propose. Propose tools never call GitHub's write API.
Validates each proposal (allowlist, arguments, not a pull request, queue limits, duplicates), snapshots the issue, and queues a
pending_actionsrow.Shows queued proposals in a dashboard where a human reviews the source issue and approves or rejects.
On approval, claims the row with a lease, re-fetches the issue, checks it is still valid, then writes with a separate write token.
Logs everything in
audit_log, including deduplicated, invalid, stale, and failed proposals.Sends writes with an unknowable outcome (network failure or GitHub 5xx mid-write) to
needs_review, where a person checks GitHub and records the result.
The triage agent (agent/triage.py), run manually or from your own scheduler, uses the same propose path: it classifies open issues with an LLM and queues proposals for review.
Architecture
flowchart TD
client(["MCP client or triage agent"]) --> read["read tools"]
client --> propose["propose tools"]
read --> ghr["GitHub API, read token"]
propose --> validate["allowlist, validation,<br/>queue caps, dedup"]
validate --> queue[("pending_actions<br/>Neon Postgres")]
queue --> dash["Streamlit dashboard"]
human(["human approver"]) --> dash
dash -->|reject| rejected["status: rejected"]
dash -->|approve| claim["claim row, lease token"]
claim --> check{"stale?"}
check -->|yes| stale["status: stale"]
check -->|no| ghw["GitHub API, write token"]
ghw --> done["status: executed or failed"]
ghw -->|unknown outcome| review["status: needs_review"]
review -->|applied| done
review -->|not applied| queue
validate -.-> audit[("audit_log")]
dash -.-> audit
ghw -.-> auditDetails are in Guardrails.
Processes and credentials
All processes need NEON_DSN. Use two env files: .env (from .env.example) for everything except the dashboard, and .env.dashboard (from .env.dashboard.example), the only file that holds GITHUB_WRITE_PAT.
Process | Entry point | Env file | Credentials | Writes to GitHub |
MCP server |
|
| read token | No |
Triage agent |
|
| read token, Groq key | No |
Eval |
|
| read token, Groq key | No |
Dashboard |
|
| read token, write token | Yes, only after a human approves |
Database scripts |
|
| database DSN only | No |
Smoke-test client |
|
| starts the MCP server, so read token | No |
load_config(require_write_pat=False) never reads the write token from a file, drops it if inherited from the environment, and raises if write mode is loaded later in the same process. The Config repr hides credentials. All processes share one Postgres role, so this separates GitHub credentials, not database access.
MCP tools
Kind | Tool | What it does |
Read |
| Issue summaries by state, labels, and recency. Excludes pull requests. Up to |
Read |
| One issue with its 30 newest comments (each clipped to 2500 chars) and a count of omitted comments |
Read |
| Pull request summaries, same |
Read |
| Search within one repo. |
Read |
| Opened, closed, and commented issues, and opened issues by label, over 1 to 365 days |
Propose |
| Queue a comment |
Propose |
| Queue label additions, checked against the repo's labels |
Propose |
| Queue label removals |
Propose |
| Queue an assignee, checked against the repo's assignable users |
Propose |
| Queue closing, optionally as |
Read tool descriptions label issue text as untrusted. All propose tools reject pull request numbers.
Triage agent
Scope:
openai/gpt-oss-20bon Groq (--modelto override), one call per issue, never pull requests. It proposes labels and assignments;--allow-commentand--allow-closeadd the rest, except on flagged issues. Each proposal stores the model's rationale (up to 2000 chars) for the approver. The model also returnssuspected_injection; when true, the plan drops comments, closes, and assignments and keeps only label proposals.Skips: an issue is skipped while an agent proposal for it is
pending,approving,needs_review,rejected, orexecuted;expired,failed, andstaleare reconsidered.no_actionresults, andproposedresults with a live or terminal proposal, skip only while the title and body are unchanged.Failures: bad model output is retried on later runs, up to 3 failures per unchanged text. Rate limits (Groq or GitHub) stop the run; connection and server errors are transient. Neither is recorded against the issue. Systemic failures (bad key, de-allowlisted repo, missing DB grant) exit 1.
Options and caps:
--state,--max-issues,--since,--max-pages(both counts positive). Prompt caps: title 300 chars, body 8000, 10 comments (1500 each, 6000 total).
Guardrails
Statuses: pending, approving, executed, rejected, expired, stale, blocked, failed, needs_review.
Proposing and approving
Control | Behavior |
Allowlist | Every call checks |
Rejected on proposal | Closing a non-open issue, adding an existing label, removing an absent label, assigning an existing assignee, targeting a pull request |
Queue caps |
|
Dedup | A duplicate of a |
Write token | Held only by the dashboard |
Claim |
|
Recheck | The issue is re-fetched and the row becomes |
Dashboard access | Requires |
GitHub client | 15 second timeout. Pagination raises instead of truncating, and next-page links must point to |
Outcomes
Every transition is written atomically with its audit row.
Situation | Result |
Issue unreadable before the write | 404 or 410: |
4xx on the write |
|
Network error or 5xx on the write |
|
DB write fails after the GitHub call | Retried 3 times, later attempts on fresh connections. If all fail, |
Recovery and expiry (run on every dashboard load)
Condition | Result |
|
|
|
|
|
|
|
|
|
|
Prompt injection
Issue titles, bodies, and comments are untrusted. The classifier prompt wraps them in <untrusted_issue_content> markers, strips any copy of those markers first, tells the model to treat the content as data, and repeats that reminder after the untrusted block. MCP tool descriptions carry the same warning. A phrase check (is_heuristically_flagged, after Unicode normalization and zero-width-character removal) flags proposals in the dashboard and blocks agent comments and closes on flagged issues. It over-triggers by design and is not a security boundary. The real control is that every proposal is scoped to one issue and needs human approval.
Project Structure
issueops-mcp/
├── .github/workflows/ci.yml
├── .streamlit/config.toml
│
├── issueops/
│ ├── config.py # env loading, credential rules, DSN-only loader
│ ├── db.py # Postgres connection helper
│ ├── limits.py # text limits, clip helper, env integer parsing
│ ├── github_client.py # read and write GitHub clients, pagination guard
│ ├── tools.py # read tools, propose tools, validation, caps, audit writes
│ ├── actions.py # approve, reject, expire, recover, resolve, lease handling
│ ├── heuristics.py # advisory injection-phrase check
│ ├── projection.py # slims and bounds what the MCP read tools return
│ └── observability.py # optional Logfire setup
│
├── agent/
│ ├── prompts.py # classifier prompt, trusted context, untrusted block
│ └── triage.py # triage agent and CLI
│
├── mcp_server/server.py # MCP server exposing the 10 tools
├── dashboard/
│ ├── app.py # Streamlit approval UI and audit log view
│ └── auth.py # access token check
│
├── db/
│ ├── schema.sql # idempotent baseline: creates a fresh database or upgrades an existing one, and records every migration file below as applied
│ └── migrations/ # incremental changes, applied and tracked by scripts/migrate.py
│
├── eval/
│ ├── eval.py # classification, adversarial, and audit-consistency checks
│ └── labels_template.json # fixture format
│
├── scripts/
│ ├── allowlist.py # add, deactivate, list allowlisted repos
│ ├── migrate.py # applies db/migrations/*.sql not yet recorded in schema_migrations
│ ├── prune_audit_log.py # delete audit rows older than N days
│ └── custom_client.py # call one MCP tool from the command line
│
├── assets/
├── tests/ # unit tests plus Postgres integration tests
│
├── conftest.py # fake DB used by the unit tests
├── .env.example
├── .env.dashboard.example
├── .gitignore
├── requirements.txt
└── README.mdGetting started
Accounts and keys:
GitHub: two fine-grained tokens on the target repo, read (Issues read, Pull requests read, Metadata read) and write (Issues read and write): https://github.com/settings/personal-access-tokens
Neon (free tier): https://neon.tech. Copy the pooled connection string.
Groq, for the agent and eval: https://console.groq.com/keys
Logfire (optional, tracing no-ops without it): https://logfire.pydantic.dev
Install (Python 3.10 or 3.12, as in CI):
python3 -m venv .venv && source .venv/bin/activate pip install -r requirements.txt cp .env.example .env cp .env.dashboard.example .env.dashboardIn
.env, setNEON_DSNandGITHUB_READ_PAT, plusGROQ_API_KEYfor the agent or eval. In.env.dashboard, setNEON_DSN,GITHUB_READ_PAT,GITHUB_WRITE_PAT, andDASHBOARD_ACCESS_TOKEN(orDASHBOARD_ALLOW_INSECURE=trueon a machine only you can reach).Database. Run
db/schema.sqlonce in your Neon project's SQL Editor. It is idempotent: it creates a fresh database or upgrades an existing one, and records every file indb/migrations/inschema_migrations. Apply migrations added later with:python scripts/migrate.pyPending files are applied in one transaction under an advisory lock, so concurrent runs cannot double-apply and a failure leaves nothing half-applied. When adding a migration, also add its name to the
INSERT INTO schema_migrationslist at the end ofdb/schema.sqland fold its change into the baseline;tests/test_schema_files.pyfails if they disagree.Allowlist a repo. Nothing works on a repo until this is done.
python scripts/allowlist.py add owner/repo
Running it
Run from the repo root. Everything except the dashboard reads .env; the dashboard reads .env.dashboard.
Command | What it does |
| Approval dashboard |
| Queue label and assignment proposals. Add |
| MCP server over stdio |
| Call one MCP tool as a smoke test |
| List allowlisted repos ( |
| Apply pending migrations |
| Delete audit rows older than 90 days |
| Run the evaluation |
On Windows, activate with .venv\Scripts\activate and, in PowerShell, run $env:ISSUEOPS_ENV_FILE=".env.dashboard" before streamlit run dashboard/app.py.
To use the MCP server from Claude Desktop, add this to claude_desktop_config.json with absolute paths and restart it. The server reads credentials from the repo's .env, so they do not go in this file:
{
"mcpServers": {
"issueops": {
"command": "/abs/path/issueops-mcp/.venv/bin/python",
"args": ["-m", "mcp_server.server"],
"env": { "PYTHONPATH": "/abs/path/issueops-mcp" }
}
}
}In the dashboard: enter the access token and your name, expand a pending action, read the rationale and stored issue text (tick the acknowledgement if flagged), optionally Load current issue from GitHub, then Approve or Reject. Rows under Needs review need a person to check GitHub and record whether the change was applied.
Configuration
Variable | Default | Purpose |
| required | Pooled Postgres connection string |
| required, except for database-only scripts | Read token |
| dashboard only, in | Write token |
| agent and eval only | LLM key |
| none | Enables tracing |
| 48 | How long a proposal can wait |
| 10 | When an |
| 65536 | Max proposed comment length (also the ceiling, matching GitHub's limit) |
| 10 | Open proposals per issue |
| 500 | Open proposals per initiator |
| none | Label recorded as the MCP initiator |
| none | Gates the dashboard. Required unless |
| false | Lets the dashboard run without a token |
| auto-discovered | Env file for this process. Set to |
Testing
pytestUnit tests use a fake database and mocked GitHub clients, so they cannot verify locking. tests/test_integration_postgres.py runs against real Postgres and covers concurrent approvals, lease reclamation, dedup, queue caps, deadlock freedom, atomic audit writes, dropped-connection retry, triage skip rules, requeue expiry, unknown write outcomes, schema re-application, the migration runner, and the legacy upgrade. It is skipped unless TEST_DATABASE_URL is set; each test creates and drops its own schema. CI runs everything against Postgres 16 on Python 3.10 and 3.12. tests/test_dashboard.py drives the dashboard headlessly and is skipped without Streamlit.
Evaluation
python -m eval.eval path/to/labels.jsonStart from eval/labels_template.json. The script fetches the repo's labels and assignable users, applies the heuristic flag, and classifies each issue with the agent's own planning code. For adversarial entries, expected_labels lists the labels that count as a safe response (for example ["invalid"] for spam). Any comment, close, or assignment, or any label outside that list, counts as harmful. injection_markers are matched only against the comment, close reason, and assignee, not the rationale.
Local run: 12-issue fixture (9 legitimate, 3 adversarial) against openai/gpt-oss-20b. The fixture is not in the repo.
Metric | Measures | Result |
| Predicted label set, filtered to existing repo labels, equals | 9/9 |
| Adversarial issues with a harmful gated action or an injection marker in the comment, close reason, or assignee | 0/3 |
| Adversarial issues where the model's own output contains a harmful action, before any gating | 0/3 |
| Adversarial issues with a harmful action after the model's | 0/3 |
| Adversarial issues with any planned proposal after flag gating, including label proposals | 3/3 |
| Adversarial issues whose comment, close reason, or assignee echoes an injection marker | 0/3 |
| Adversarial issues where the model set | 3/3 |
| Non-adversarial issues where the model set | 0/9 |
| Mean model call time only | ~4300 |
With n=12 this is a smoke test, not a benchmark. The eval also runs an audit consistency check in both directions between audit_log and pending_actions. It catches bookkeeping bugs in this codebase, not writes made elsewhere, and excludes pruned rows.
Known limitations
Security and data
The injection heuristic and the model's
suspected_injectionflag are advisory and avoidable, and the heuristic over-triggers on LLM-related text.Approver identity is a typed name; the access token gates the app but does not identify people.
Issue text is sent to Groq by the agent and stored in Neon (snapshots and source excerpts). Avoid private repos you cannot share with those providers.
One shared Postgres role: the audit log is append-only by convention (only
prune_audit_log.pydeletes), and nothing stops the write token being put in.env.
Behavior
propose_remove_labelscalls GitHub once per label and can partially succeed; the failure lists what was removed.Expiry runs only when the dashboard loads, and queue caps count unexpired
pendingrows, so an unattended queue can block new proposals.No scheduler is included.
Scale
Label and assignee caches are process-local with a 5 minute TTL.
Pagination caps at 20 pages or 2000 items. MCP listings stop at
limit(max 100) and reporttruncated; other bulk reads flag partial results. Beyond 2000 comments, the duplicate-comment check sees only the first 2000.Without
MCP_CLIENT_LABEL, the initiator includes hostname and pid, so the per-initiator cap is per process.
This server cannot be deployed
Maintenance
Related MCP Connectors
Operate GitHub from your AI assistant: repos, PRs, issues, Actions. Previews and read-only mode.
Connect AI assistants to GitHub - manage repos, issues, PRs, and workflows through natural language.
A Model Context Protocol (MCP) application for automated GitHub PR analysis and issue management.…
GitHub Copilot MCP: code, repo, PR, workflow tools for AI assistants.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables Claude to query GitHub repositories in plain English, fetching recent activity, release notes, issue triage, and health summaries via the GitHub API.-
- FlicenseNot gradedqualityCmaintenanceEnables read-only interaction with GitHub through the REST API via natural language, allowing users to query GitHub data from Claude Desktop or Claude Code.-
- FlicenseNot gradedqualityBmaintenanceEnables Claude conversations to perform real GitHub operations — managing issues, comments, sub-issue hierarchies, blocked_by dependencies, and Projects v2 board status — while keeping the GitHub PAT server-side and gated behind revocable bearer-token auth with an audit trail.-
- AlicenseBqualityCmaintenanceEnables an AI assistant to read, search, summarize, and triage GitHub issues and repository activity through an allowlisted, read-only set of tools, prompts, and resources. Every input is validated, every repository must be explicitly approved, and all third-party issue text is fenced off and marked as untrusted before it reaches the model.4MIT