git-issuer-mcp
by ESPOIR-DITE
README.md
# git-issuer-mcp
An MCP (Model Context Protocol) server that enables AI agents to create GitHub issues on explicitly allow-listed repositories. It uses GitHub App authentication so the agent never touches tokens directly, and enforces input validation, rate limiting, and repository-level access control.
## How It Works
```
AI Agent → MCP Server (stdio) → GitHub App Auth → GitHub REST API → Repository
```
The server exposes a single `create_issue` tool over MCP's stdio transport. When an agent calls it, the request flows through:
1. **Input validation** — Zod-based schema checks, HTML/script sanitization, base64 payload rejection
2. **Rate limiting** — Sliding-window limiter (default 10 requests/minute)
3. **Repository allowlist** — Only repos listed in `ALLOWED_REPOS` are accepted
4. **GitHub App authentication** — JWT generated from private key, exchanged for a short-lived installation token (cached for 55 minutes)
5. **Issue creation** — Octokit REST client creates the issue and returns the number and URL
All errors are returned as structured JSON with a code and message. Tokens and private keys are never logged or exposed.
## Prerequisites
### 1. Node.js
Node.js **18+** is required (ES2022 target).
```bash
node --version # v18.x or higher
```
### 2. GitHub App
You need a GitHub App installed on your target organization or account. The app grants the server permission to create issues without sharing personal access tokens with the agent.
**Create the app:**
1. Go to **GitHub Settings > Developer settings > GitHub Apps > New GitHub App**
2. Set the following permissions:
- **Repository permissions > Issues**: Read & Write
3. Under "Where can this GitHub App be installed?", choose **Only on this account** (recommended for internal use)
4. Create the app and note the **App ID** from the app settings page
5. Generate a **private key** (.pem file) — download and store it securely
6. **Install** the app on the repositories you want the agent to access
7. After installation, note the **Installation ID** (visible in the URL: `https://github.com/settings/installations/<INSTALLATION_ID>`)
### 3. Environment Variables
| Variable | Required | Description |
|---|---|---|
| `GITHUB_APP_ID` | Yes | App ID from your GitHub App settings page |
| `GITHUB_INSTALLATION_ID` | Yes | Installation ID from the app installation URL |
| `GITHUB_PRIVATE_KEY` | Yes | Path to the `.pem` file **or** the key as a base64-encoded string |
| `ALLOWED_REPOS` | Yes | Comma-separated list of `owner/repo` entries the agent may target |
| `RATE_LIMIT_PER_MINUTE` | No | Max issue creations per minute (default: `10`) |
See [`.env.example`](.env.example) for a documented template.
## Installation
```bash
git clone <repo-url> git-issuer-mcp
cd git-issuer-mcp
npm install
npm run build
```
The compiled output lands in `dist/`.
## Running Tests
Tests use Jest with ts-jest and do **not** require GitHub credentials — all external calls are mocked.
```bash
npm test
```
This runs the full suite covering:
- **`validation.test.ts`** — Repo format, title/body limits, label constraints, HTML sanitization, base64 detection
- **`auth.test.ts`** — Env validation, .pem file loading, base64 key loading, token caching and refresh
- **`issues.test.ts`** — Allowlist enforcement, successful creation, GitHub API error handling
- **`rateLimiter.test.ts`** — Default/custom limits, per-agent tracking, sliding-window expiry
- **`tools.test.ts`** — End-to-end tool handler flow, error code formatting
To type-check without running tests:
```bash
npm run typecheck
```
## Setting Up in Claude Code
Add the server to your Claude Code MCP configuration at `~/.claude.json`:
```json
{
"mcpServers": {
"git-issuer": {
"command": "node",
"args": ["/absolute/path/to/git-issuer-mcp/dist/server.js"],
"env": {
"GITHUB_APP_ID": "123456",
"GITHUB_INSTALLATION_ID": "78901234",
"GITHUB_PRIVATE_KEY": "/absolute/path/to/private-key.pem",
"ALLOWED_REPOS": "your-org/repo-a,your-org/repo-b",
"RATE_LIMIT_PER_MINUTE": "10"
}
}
}
}
```
Claude Code injects the environment variables into the server process at launch. The agent never sees or controls these values.
After saving the config, restart Claude Code. The `create_issue` tool will appear in the agent's available tools.
## Setting Up in Cursor
Add the server to your Cursor MCP configuration. Open **Settings > MCP** (or edit `.cursor/mcp.json` in your project root) and add:
```json
{
"mcpServers": {
"git-issuer": {
"command": "node",
"args": ["/absolute/path/to/git-issuer-mcp/dist/server.js"],
"env": {
"GITHUB_APP_ID": "123456",
"GITHUB_INSTALLATION_ID": "78901234",
"GITHUB_PRIVATE_KEY": "/absolute/path/to/private-key.pem",
"ALLOWED_REPOS": "your-org/repo-a,your-org/repo-b",
"RATE_LIMIT_PER_MINUTE": "10"
}
}
}
}
```
Restart Cursor after saving. The server will start automatically when the agent invokes the `create_issue` tool.
## MCP Tool Reference
### `create_issue`
Create a GitHub issue on an allowed repository.
**Input:**
| Field | Type | Required | Constraints |
|---|---|---|---|
| `repo` | `string` | Yes | `owner/repo` format |
| `title` | `string` | Yes | 1–200 characters |
| `body` | `string` | Yes | Max 10,000 characters |
| `labels` | `string[]` | No | Max 10 labels, each 1–50 characters |
**Success response:**
```json
{
"success": true,
"issue_number": 42,
"issue_url": "https://github.com/your-org/repo/issues/42"
}
```
**Error response:**
```json
{
"success": false,
"error": {
"code": "REPO_NOT_ALLOWED",
"message": "Repository your-org/other-repo is not on the allowlist"
}
}
```
**Error codes:**
| Code | Meaning |
|---|---|
| `VALIDATION_ERROR` | Input failed schema validation or sanitization |
| `REPO_NOT_ALLOWED` | Repository is not in `ALLOWED_REPOS` |
| `RATE_LIMITED` | Too many requests within the rate-limit window |
| `GITHUB_API_ERROR` | GitHub API returned an error |
| `UNAUTHORIZED` | GitHub App authentication failed |
## Security Model
- **Repository allowlist** — The agent can only target repos explicitly listed in `ALLOWED_REPOS`. Everything else is rejected.
- **No token exposure** — Tokens are generated server-side, cached in memory, and never logged or returned to the agent.
- **Input sanitization** — `<script>` blocks, HTML event attributes, and base64 payloads are stripped or rejected before reaching GitHub.
- **Rate limiting** — A configurable sliding-window limiter prevents runaway issue creation.
- **Environment-only config** — All secrets are injected via environment variables by the MCP host. The agent cannot modify them at runtime.
## Project Structure
```
src/
├── server.ts # Entry point — registers tools, starts stdio transport
├── mcp/
│ └── tools.ts # Tool schema and handler (validation → rate limit → create)
├── github/
│ ├── auth.ts # GitHub App JWT + installation token with caching
│ └── issues.ts # Issue creation, allowlist enforcement, structured logging
├── security/
│ ├── validation.ts # Zod schema, HTML sanitization, base64 detection
│ └── rateLimiter.ts # Sliding-window per-agent rate limiter
└── __tests__/
├── auth.test.ts
├── issues.test.ts
├── rateLimiter.test.ts
├── tools.test.ts
└── validation.test.ts
```
## Development
```bash
npm install # Install dependencies
npm run build # Compile TypeScript to dist/
npm run typecheck # Type-check without emitting
npm test # Run test suite
npm start # Start the server (requires env vars)
```
## License
Internal use only.
TDQS
A3.6/5.0
Scored across 1 tool
Disambiguation5/5
Only one tool exists, so there is no possibility of overlap or confusion. The tool's purpose is clearly stated.
Naming Consistency5/5
The single tool name 'create_issue' follows a clear verb_noun convention, so there is no inconsistency.
Tool Count2/5
With only one tool, the server is severely under-scoped for a GitHub issue workflow. A typical issue lifecycle requires at least create, read, and update operations.
Completeness1/5
The tool only supports issue creation and offers no way to list, read, update, or delete issues. This leaves agents unable to track or manage issues after creation, creating a dead end.
Maintenance
ActivityInactive
ResponsivenessNo issues