npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rubicontv/forgejo-mcp

v0.17.0

Published

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

Readme

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.