Skip to content

forgejo-mcp

A Model Context Protocol server that gives AI clients tools to work with a Forgejo or Gitea instance over its REST API: repositories, issues, comments, files, pull requests, and CI status.

It speaks MCP over stdio and calls the Forgejo/Gitea REST API with fetch. The whole server bundles to a single dist/index.js with no runtime framework.

Security model

Tools are tiered by blast radius. The default surface is reads plus writes whose damage is visible and cheap to undo. There are deliberately no merge, delete, or admin tools in it: merging and deleting live behind an opt-in elevated tier, and user, organisation, permission, secret and token administration is not exposed at all, at any tier. Repository lifecycle — creating and deleting repositories — is the exception: those exist, gated behind the elevated tier rather than permanently excluded. This keeps the server safe for unattended use and caps the blast radius of the API token. Pair it with a least-privilege token (repository R/W, issue R/W, user Read).

A small set of high-blast-radius operations — destroying work, and creating a repository whose visibility the caller chooses — is available behind an opt-in, off-by-default elevated tier. With no elevated environment variables set, the tool surface is byte-identical to the safe default described above.

Tools

Tool Kind Purpose
list_repositories read List a user's repositories (default: authenticated user); paginated
get_repository read Repository metadata, including default branch
list_issues read List issues; filter by state, labels, type (default issues), text q, milestones, author, assignee, mention, and date; sortable; paginated
get_issue read A single issue with body, labels, assignees
create_issue write Open an issue (labels by id, assignees)
list_issue_comments read Comments on an issue or PR; paginated
create_issue_comment write Add a comment to an issue or PR
get_file_content read Decoded file content (default branch if no ref)
list_directory read List a directory's entries (root if no path); fails on a file path; paginated
create_file write Create a file from plain-text content (optional new branch)
delete_file write Delete a file; requires the blob sha, commits like any change
update_file write Replace a file's content (sha required — the blob SHA being replaced)
list_pull_requests read List PRs; filter by state, milestone id, label ids, and poster; sortable; paginated
get_pull_request read A single PR with merge state
get_pull_request_diff read Unified diff for a PR as plain text
get_pull_request_files read Files a PR changes, with status and line counts; paginated
create_pull_request write Open a PR from a head branch into a base branch
get_commit_status read Combined CI/commit status for a ref
list_branches read List branches with latest commit and protection status; paginated
get_branch read A single branch by name
create_branch write Create a branch from a source ref (default branch if none)
list_commits read List commits; filter by start ref and path; paginated
get_commit read A single commit by SHA or ref
list_releases read List releases; paginated
get_release_by_tag read A release by tag name rather than numeric id
get_latest_release read The current non-draft, non-prerelease release
edit_release write Edit release notes, name, tag, draft/prerelease flags
get_release read A single release by ID, with notes and draft/prerelease flags
create_release write Create a release for a tag (draft, prerelease, notes)
list_tags read List tags with their target commits; paginated
get_tag read A single tag by name
create_tag write Create a tag on a branch or commit (optionally annotated)
list_pull_request_reviews read Reviews on a PR (approvals, change requests, comments); paginated
create_pull_request_review write Submit a review (APPROVED, REQUEST_CHANGES, or COMMENT), with optional inline comments pinned to a commit_id
request_pull_request_reviewers write Request reviews from users (and org teams) on a PR
create_commit_status write Report a commit status (pending, success, error, failure, warning)
list_commit_statuses read Individual statuses on a ref; paginated
edit_issue_comment write Replace the text of a comment (by comment id)
delete_issue_comment write Delete a comment (by comment id); not recoverable here
list_milestones read Milestones in a repository (id, title, state, issue counts); paginated
get_milestone read One milestone by id, or by exact title
create_milestone write Create a milestone (title, description, due date, state)
edit_milestone write Edit a milestone; only the fields you pass change
delete_milestone write Delete a milestone; issues survive but lose it
list_labels read Labels defined in a repository (id, name, color); paginated
get_label read One label definition by id
create_label write Define a new label (name and colour required)
edit_label write Edit a label definition; renames it everywhere it is used
add_labels write Add labels (by name or id) to an issue or PR; existing labels kept
remove_label write Remove one label (by name or id) from an issue or PR
add_assignees write Assign users to an issue or PR; existing assignees kept
set_issue_state write Close or reopen an issue or PR (state only — edit_issue owns title/body)
edit_issue write Edit the title or body of an issue or PR; only the fields you pass change
edit_pull_request write Edit a PR's title, body, state, assignees, labels, milestone, due date or maintainer-edit flag; the base branch is not editable here

List results are paginated

Every list_* tool except list_directory returns

{ "total_count": 51, "count": 30, "page": 1, "items": [ ] }

rather than a bare array. Forgejo serves list endpoints a page at a time and caps the page size regardless of the limit asked for, so a bare array cannot be told apart from the first slice of a much longer list. When count is short of total_count, ask for the next page. total_count is omitted when the server does not report one.

list_directory is excluded because its endpoint answers with a single object, not a list, when the path names a file.

Configuration

Supply the target and token at runtime via environment variables:

  • FORGEJO_BASE_URL — e.g. https://git.example.com
  • FORGEJO_TOKEN — a Forgejo/Gitea API token

Never hardcode the token; inject it from a secret store at launch.

Claude Code

Two setups, both good — pick by whether you value portability or keeping the token out of Claude's config file.

Option A — npx (recommended)

Once the package is on npm, no local checkout or build is needed:

claude mcp add forgejo \
  --env FORGEJO_BASE_URL=https://git.example.com \
  --env FORGEJO_TOKEN=... \
  -- npx -y @rubicontv/forgejo-mcp

Portable and self-updating. Note this stores the token in Claude Code's config (~/.claude.json). If you would rather the token never touch that file, use Option B.

Option B — local wrapper (keeps the token out of config)

A small launcher sources a chmod 600 env file at startup, so the token lives only in that file (ideally hydrated from a secret manager) and never in ~/.claude.json:

#!/bin/sh
# ~/.config/forgejo-mcp/launch.sh
set -eu
. "$HOME/.config/forgejo-mcp/forgejo-mcp.env"   # sets FORGEJO_BASE_URL, FORGEJO_TOKEN
export FORGEJO_BASE_URL FORGEJO_TOKEN
exec npx -y @rubicontv/forgejo-mcp                # or: exec node /path/to/forgejo-mcp/dist/index.js

Point Claude Code at the wrapper — no env block, so no secret in the config:

{
  "mcpServers": {
    "forgejo": {
      "command": "/Users/you/.config/forgejo-mcp/launch.sh"
    }
  }
}

Running from a local build (no npm)

If you are working from a checkout instead of the published package:

{
  "mcpServers": {
    "forgejo": {
      "command": "node",
      "args": ["/path/to/forgejo-mcp/dist/index.js"],
      "env": {
        "FORGEJO_BASE_URL": "https://git.example.com",
        "FORGEJO_TOKEN": "..."
      }
    }
  }
}

Allowlisting

To let an agent use the server without a prompt on every call, allowlist it. On the safe default surface (no elevated tier), blanket-allowlisting the whole server is fine:

mcp__forgejo

If you turn on the elevated tier, do not do this — a blanket allowlist would let every elevated tool run without a prompt: merge_pull_request, delete_branch, delete_repo, delete_label, delete_release and delete_tag. Allowlist named default tools only, never an elevated one, and never delete_repo under any circumstances — its confirm argument catches a malformed call, not a misled one. For example:

mcp__forgejo__create_issue
mcp__forgejo__create_pull_request

See the allowlist warning in the Elevated tier section below.

Elevated tier (opt-in, off by default)

The server can optionally expose a minimal set of high-blast-radius tools: operations that destroy work in ways this server cannot undo. This tier is off by default and must be deliberately enabled by the operator.

Tool Kind Operation
merge_pull_request elevated Merge a PR (merge / rebase / squash) into its base branch, pinned to a required head_commit_id; pass delete_branch_after_merge to clean up the head branch
delete_branch elevated Permanently delete a branch
delete_repo elevated Permanently delete a repository; confirm must equal owner/repo
delete_label elevated Permanently delete a label definition, stripping it from every issue and pull request that carried it
delete_release elevated Permanently delete a release; its notes and uploaded assets do not exist in git
delete_tag elevated Permanently delete a tag; if its commits are unreachable from any branch, deleting it orphans them

Every elevated tool is prefixed [ELEVATED — DESTRUCTIVE] and carries destructiveHint: true. The tier holds nothing else: a tool that does not destroy something this server cannot restore belongs in the default surface.

delete_repo requires confirm to equal owner/repo exactly. Be clear about what that buys: it catches a malformed or half-specified call, and nothing more. Both confirm and the target come from the same tool-call arguments, so text injected into an issue can satisfy it as easily as a careful caller. Deleting a repository is the only operation here with no undo of any kind, so never allowlist delete_repo — the per-call approval prompt is the actual guard.

merge_pull_request requires head_commit_id — the head SHA from get_pull_request. Forgejo refuses the merge if the branch has moved since that SHA, so commits pushed after the review cannot be swept in. Be precise about what that is: a freshness guard, not proof of provenance. Nothing verifies the SHA came from reading the pull request, so it constrains what gets merged, not whether anyone looked. The per-call approval prompt is what covers the latter.

Why it is gated so carefully

This server hands tools to an LLM that reads untrusted content — issue bodies, PR text, file contents. That is a prompt-injection surface: a misled agent should never be able to merge or delete anything. So the elevated tier is double-gated and uses a separate token.

Enabling it — the double gate

Elevated tools are registered only when all three of these hold:

  • FORGEJO_MCP_ELEVATED=1 — an explicit opt-in flag, and
  • FORGEJO_MCP_ELEVATED_TOKEN — a token distinct from FORGEJO_TOKEN. This is checked, not merely asked for: if the two are identical the tier stays off and the server logs why. A second token that equals the first is not a second token.
  • FORGEJO_TOKEN must itself be set. Elevation is additive on top of the default surface, so without it the tier stays off rather than leaving the destructive tools as the only ones that work.

The default FORGEJO_TOKEN never performs an elevated operation — elevated calls use FORGEJO_MCP_ELEVATED_TOKEN exclusively. Scope that token to only the repositories and permissions the destructive tools actually need.

If FORGEJO_MCP_ELEVATED=1 is set but FORGEJO_MCP_ELEVATED_TOKEN is missing, the server fails closed: the elevated tools are not registered and a warning is logged to stderr.

{
  "mcpServers": {
    "forgejo": {
      "command": "node",
      "args": ["/path/to/forgejo-mcp/dist/index.js"],
      "env": {
        "FORGEJO_BASE_URL": "https://git.example.com",
        "FORGEJO_TOKEN": "…read/write token…",
        "FORGEJO_MCP_ELEVATED": "1",
        "FORGEJO_MCP_ELEVATED_TOKEN": "…distinct, narrowly-scoped token…"
      }
    }
  }
}

⚠️ Allowlist warning — read before enabling

This MCP is commonly whole-server allowlisted (e.g. mcp__forgejo) so an agent can use it autonomously without per-call prompts. If you enable the elevated tier under a blanket allowlist, the destructive tools also run without any prompt. A single prompt-injection payload in an issue or PR could then merge or delete on your behalf.

Therefore, when the elevated tier is on:

  • Do NOT blanket-allow the whole server (mcp__forgejo).
  • Allowlist only the specific safe tools you want to run autonomously (e.g. mcp__forgejo__create_issue), and leave every elevated tool — merge_pull_request, delete_branch, delete_repo, delete_label, delete_release and delete_tag — prompting on every call. delete_repo should never be allowlisted at all: its confirm argument catches a malformed call, not a misled one.

The server cannot enforce the client's allowlist — distinct tool naming and this warning are the mitigation. Enabling elevated tools is a decision the operator owns.

Permanently excluded

The elevated tier is intentionally tiny. The following are never exposed, regardless of any flag: user management, secret/variable writes, permission or collaborator changes, and org/repo admin. Those are not appropriate to hand to an LLM that reads untrusted content, so they stay out by design. Adding any destructive tool requires explicit owner sign-off and a matching token-scope review.

Build and test

npm install
npm run build     # esbuild -> dist/index.js
npm run smoke     # builds, then runs a token-free MCP handshake + tools/list
npm run typecheck # tsc --noEmit

Contributors

Contributors

See CONTRIBUTING.md to get started, the Code of Conduct for community expectations, and SECURITY.md to report a vulnerability privately.

License and attribution

Licensed under the Apache License 2.0. This is an independent, clean-room implementation; see NOTICE. It was inspired by nsvk13/forgejo-mcp-server, with thanks, but shares no source code with it.

About

Model Context Protocol server for Forgejo/Gitea: read tools plus safe additive writes (issues, comments, pull requests). No merge, delete, or admin surface.

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages