Forgejo MCP
This repository has been archived on 2026-07-03. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
  • Rust 92.7%
  • Just 4.3%
  • Dockerfile 2%
  • Nushell 1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
David 4541e8cd83
All checks were successful
Check / fmt + clippy + build + tests (push) Successful in 38s
Build binary (Linux) / Build and publish forgejo-mcp binary (Linux x86_64) (push) Successful in 1m20s
Build binary (Windows) / Build and publish forgejo-mcp binary (Windows x86_64) (push) Successful in 1m24s
Build binary (Linux static musl) / Build and publish forgejo-mcp binary (Linux x86_64 static musl) (push) Successful in 2m11s
Merge pull request 'feat(auth): authenticate with FORGEJO_TOKEN env var, fall back to keys.json (FJMCP-21)' (#31) from feat/FJMCP-21-forgejo-token-env into main
Reviewed-on: #31
2026-06-25 11:19:31 +02:00
.forgejo/workflows Merge branch 'main' into fix/GOV-14-trigger-derived-publish-channels 2026-06-22 19:06:49 +02:00
oci-build fix(build): install OpenSSL build toolchain in Windows builder 2026-06-08 01:29:20 +02:00
src feat(auth): authenticate with FORGEJO_TOKEN env var, fall back to keys.json 2026-06-24 06:09:31 -04:00
.dockerignore chore: bring repo to full governance compliance (FJMCP-1) 2026-05-26 21:37:15 -04:00
.gitignore chore: Sync .gitignore 2026-06-06 13:21:35 -04:00
build.rs feat: add clap CLI with serve and version self-updater (FJMCP-3) 2026-05-28 04:46:17 -04:00
Cargo.lock Release v0.2.3 2026-06-22 22:57:17 -04:00
Cargo.toml Release v0.2.3 2026-06-22 22:57:17 -04:00
CLAUDE.md feat(server): add release, tag, and package tools 2026-06-14 15:02:21 -04:00
justfile fix(release): correct lock-sync comments and gate stale lockfile 2026-06-22 20:14:20 -04:00
LICENSE-APACHE chore: bring repo to full governance compliance (FJMCP-1) 2026-05-26 21:37:15 -04:00
LICENSE-MIT chore: bring repo to full governance compliance (FJMCP-1) 2026-05-26 21:37:15 -04:00
README.md feat(auth): authenticate with FORGEJO_TOKEN env var, fall back to keys.json 2026-06-24 06:09:31 -04:00

forgejo-mcp

A Model Context Protocol server that exposes Forgejo operations to MCP clients. It is built on the forgejo-cli crates (fj-client, fj-core), so authentication and API calls reuse the same code path as the fj CLI rather than reimplementing them.

What it does

Speaks MCP over stdio (default) or Streamable HTTP (MCP_TRANSPORT=http, see Transports) and exposes a small set of tools:

Read-only tools (always available):

Tool Arguments Returns
whoami none The authenticated user for the configured host.
get_repo owner, repo Repository metadata.
list_issues owner, repo, state? (open/closed/all, default open), labels?, milestones?, assignee? One page of issues.
list_all_issues owner, repo, state?, labels?, milestones?, assignee? Every issue, following pagination.
get_issue owner, repo, index A single issue by number.
list_issue_comments owner, repo, index, since?, before? (RFC 3339) An issue's comments.
get_issue_templates owner, repo The repo's issue templates.
get_issue_config owner, repo Blank-issue policy and contact links.
list_issue_attachments owner, repo, index An issue's attachments (each with id, name, size, browser_download_url).
download_issue_attachment owner, repo, index, asset_id, dest_path? Writes the attachment to a file; returns { path, bytes, filename } (not the bytes).
get_pull_request owner, repo, index A pull request by number.
get_pull_request_by_base_head owner, repo, base, head A pull request by base/head labels.
find_pull_request_by_branch owner, repo, head The open PR from a branch, or null.
list_pull_request_files owner, repo, index A PR's changed files (paginated).
list_pull_request_commits owner, repo, index A PR's commits (paginated).
compare_refs owner, repo, basehead (base...head) A diff comparison of two refs.
get_pull_request_diff owner, repo, index, diff_type? (diff/patch, default diff) Raw diff/patch text (not JSON).
list_pull_request_reviews owner, repo, index A PR's reviews (paginated).
get_pull_request_review_comments owner, repo, index, review_id A review's inline comments.
get_pull_request_commit_statuses owner, repo, sha Combined commit statuses for a SHA.
get_pull_request_template owner, repo Decoded PR template text plus is_yaml, or null.
list_repo_contents owner, repo, ref? The repo's top-level contents.
get_raw_file owner, repo, filepath, ref? A file's raw UTF-8 text (a note for binary files).
list_repo_labels owner, repo The repo's issue labels.
list_deploy_keys owner, repo The repo's deploy keys.
get_deploy_key owner, repo, id A single deploy key.
list_branch_protections owner, repo The repo's branch protection rules.
get_branch_protection owner, repo, name A single branch protection rule.
list_collaborators owner, repo The repo's collaborators.
get_collaborator_permission owner, repo, collaborator A collaborator's effective permission.
list_hooks owner, repo The repo's webhooks.
get_hook owner, repo, id A single webhook.
list_releases owner, repo, draft?, pre_release?, q? The repo's releases.
get_release_by_tag owner, repo, tag A release by tag name.
find_release owner, repo, name A release by display name.
get_release_archive owner, repo, archive (e.g. v1.0.0.zip), dest_path? Writes the archive to a file; returns { path, bytes, filename } (not the bytes).
download_release_attachment owner, repo, release_id, asset_id, dest_path? Writes the attachment to a file; returns { path, bytes, filename } (not the bytes).
list_tags owner, repo, page? (1-based, 20/page) One page of tags.
get_tag owner, repo, name A single tag.
list_packages owner, type? (cargo/generic/container/npm/...), q? The owner's packages (one entry per version).
get_package owner, type, name, version A package version's metadata.
list_package_files owner, type, name, version A package version's files.
list_orgs page? (1-based) One page of all organizations.
list_my_orgs none Organizations the authenticated user belongs to.
get_org org An organization by name.
list_org_activity org, date? (YYYY-MM-DD) An organization's activity feed.
get_org_member_count org { org, member_count }.
get_org_team_count org { org, team_count }.
list_org_members org, page? (1-based) One page of an org's members.
list_org_public_members org, page? (1-based) One page of an org's public members.
is_org_member org, username { org, username, is_member }.
is_org_public_member org, username { org, username, is_public_member }.
list_org_repos org, page? (1-based) One page of an org's repositories.
list_org_labels org An organization's issue labels.
list_teams org All teams in an organization.
list_team_repos id, page? (1-based, 20/page) One page of the repos a team can access.
list_team_members id, page? (1-based, 20/page) One page of a team's members.
search_users q?, uid?, sort? Matching users.
get_user username A user by username.
list_user_following username Everyone a user follows.
list_my_following none Everyone the authenticated user follows.
list_user_followers username A user's followers.
list_my_followers none The authenticated user's followers.
list_user_starred username, page? (1-based, 50/page) One page of a user's starred repos.
list_my_starred page? (1-based, 50/page) One page of the authenticated user's starred repos.
list_user_repos username, page? (1-based, 50/page) One page of a user's repositories.
list_my_repos page? (1-based, 50/page), order_by? One page of the authenticated user's repositories.
list_user_orgs username The organizations a user belongs to.
list_my_orgs_for_user none The authenticated user's organizations (same result as list_my_orgs).
list_user_activity username, date? (YYYY-MM-DD), only_performed_by? A user's activity feed.
list_my_ssh_keys none The authenticated user's SSH keys.
get_ssh_key id One of the authenticated user's SSH keys.
list_my_gpg_keys none The authenticated user's GPG keys.
get_gpg_key id One of the authenticated user's GPG keys.
get_gpg_verification_token none { token } for proving GPG key ownership.
list_action_tasks owner, repo, page? (1-based, 20/page) One page of action tasks (recent workflow runs).
list_repo_variables owner, repo The repo's action variables.
get_repo_variable owner, repo, name A repo action variable, including its value.
list_org_variables org An organization's action variables.
get_org_variable org, name An org action variable, including its value.
list_repo_secrets owner, repo The repo's action secrets (names only; values are never exposed).
list_wiki_pages owner, repo The repo's wiki page metadata.
get_wiki_page owner, repo, page A single wiki page by name.
get_wiki_repo owner, repo Repository metadata (used to derive the wiki clone URL).

EDIT-tier tools (require --allow-edit; see Write permissions):

Tool Arguments Returns
create_issue owner, repo, title, body?, assignees?, labels?, milestone? The created issue.
edit_issue owner, repo, index, title?, body?, state?, assignees?, milestone? The updated issue.
create_issue_comment owner, repo, index, body The created comment.
edit_issue_comment owner, repo, id, body The updated comment.
set_issue_labels owner, repo, index, add?, rm? (label names) unknown_labels: the add names that don't exist.
add_issue_attachment owner, repo, index, file_path? or content? (base64) + filename?, name? The created attachment (uploads a local file or base64 bytes).
create_pull_request owner, repo, title, head, base, body?, assignees?, labels?, milestone? The created pull request.
merge_pull_request owner, repo, index, style? (merge/rebase/rebase-merge/squash/fast-forward-only/manually-merged, default merge), message? { merged: true, index }.
edit_repo owner, repo, name?, description?, website?, private?, archived?, default_branch?, has_issues?, has_wiki?, has_pull_requests? The updated repository.
create_org_repo org, name, description?, private?, auto_init?, default_branch?, readme?, license?, gitignores? The created repository.
create_user_repo name, description?, private?, auto_init?, default_branch?, readme?, license?, gitignores? The created repository.
fork_repo owner, repo, name?, organization? The created fork.
migrate_repo clone_addr, repo_name, repo_owner?, description?, private?, mirror?, auth_username?, auth_password?, auth_token? The migrated repository.
star_repo owner, repo { starred: true, owner, repo }.
unstar_repo owner, repo { starred: false, owner, repo }.
create_repo_label owner, repo, name, color, description?, exclusive? The created label.
edit_repo_label owner, repo, id, name?, color?, description?, exclusive? The updated label.
create_deploy_key owner, repo, title, key, read_only? The created deploy key.
create_branch_protection owner, repo, rule_name, enable_push?, enable_status_check?, status_check_contexts?, required_approvals?, block_on_outdated_branch?, require_signed_commits? The created rule.
edit_branch_protection owner, repo, name, enable_push?, enable_status_check?, status_check_contexts?, required_approvals?, block_on_outdated_branch?, require_signed_commits? The updated rule.
add_collaborator owner, repo, collaborator, permission? (read/write/admin, default write) { added: true, collaborator }.
create_hook owner, repo, type, url, content_type? (json/form, default json), events?, active?, branch_filter? The created webhook.
edit_hook owner, repo, id, url?, content_type?, events?, active?, branch_filter? The updated webhook.
test_hook owner, repo, id, ref? { tested: true, id }.
create_release owner, repo, tag_name, target_commitish?, name?, body?, draft?, prerelease?, hide_archive_links? The created release.
edit_release owner, repo, id, tag_name?, target_commitish?, name?, body?, draft?, prerelease?, hide_archive_links? The updated release.
create_release_attachment owner, repo, id, file_path, name? The created attachment (uploads a local file).
create_tag owner, repo, tag_name, target?, message? The created tag.
create_org username, full_name?, description?, email?, location?, website?, visibility? (public/limited/private), repo_admin_change_team_access? The created organization.
edit_org org, full_name?, description?, email?, location?, website?, visibility?, repo_admin_change_team_access? The updated organization.
conceal_org_member org, username { concealed: true, org, username }.
create_org_label org, name, color, description?, exclusive? The created label.
edit_org_label org, id, name?, color?, description?, exclusive? The updated label.
create_team org, name, description?, permission? (read/write/admin), can_create_org_repo?, includes_all_repositories? The created team.
edit_team id, name, description?, permission?, can_create_org_repo?, includes_all_repositories? The updated team.
add_team_repo id, org, repo { added: true, id, org, repo }.
add_team_member id, username { added: true, id, username }.
follow_user username { followed: true, username }.
unfollow_user username { followed: false, username }.
block_user username { blocked: true, username }.
unblock_user username { blocked: false, username }.
update_user_settings full_name?, description?, website?, location?, language?, theme?, diff_view_style?, hide_email?, hide_activity?, hide_pronouns?, pronouns?, enable_repo_unit_hints? The updated settings.
add_email emails The account's emails after adding.
upload_ssh_key title, key, read_only? The created SSH key.
upload_gpg_key armored_public_key, armored_signature? The created GPG key.
verify_gpg_key key_id, armored_signature? The verified GPG key.
create_repo_variable owner, repo, name, value { created: true, name }.
update_repo_variable owner, repo, name, value { updated: true, name }.
create_org_variable org, name, value { created: true, name }.
update_org_variable org, name, value { updated: true, name }.
create_repo_secret owner, repo, name, data (sensitive credential; stored write-only) { created: true, name }.
dispatch_workflow owner, repo, workflow, ref, inputs?, return_run_info? The dispatched run info, or null.

DELETE-tier tools (require --allow-delete; see Write permissions):

Tool Arguments Returns
delete_repo owner, repo { deleted: true, owner, repo }.
delete_repo_label owner, repo, id { deleted: true, id }.
delete_issue_attachment owner, repo, index, asset_id { deleted: true, index, asset_id }.
delete_deploy_key owner, repo, id { deleted: true, id }.
delete_branch_protection owner, repo, name { deleted: true, name }.
remove_collaborator owner, repo, collaborator { removed: true, collaborator }.
delete_hook owner, repo, id { deleted: true, id }.
delete_release owner, repo, id { deleted: true, id }.
delete_release_by_tag owner, repo, tag { deleted: true, tag }.
delete_release_attachment owner, repo, release_id, asset_id { deleted: true, release_id, asset_id }.
delete_tag owner, repo, name { deleted: true, name }.
delete_package owner, type, name, version { deleted: true, type, name, version }.
delete_org_label org, id { deleted: true, org, id }.
delete_team id { deleted: true, id }.
remove_team_repo id, org, repo { removed: true, id, org, repo }.
remove_team_member id, username { removed: true, id, username }.
delete_email emails { deleted: true, emails }.
delete_ssh_key id { deleted: true, id }.
delete_gpg_key id { deleted: true, id }.
delete_repo_variable owner, repo, name { deleted: true, name }.
delete_org_variable org, name { deleted: true, name }.
delete_repo_secret owner, repo, name { deleted: true, name }.

Each tool returns the raw forgejo-api struct as pretty JSON text, except get_pull_request_diff, which returns the raw diff/patch string, get_raw_file, which returns the file's raw text, and get_release_archive / download_release_attachment / download_issue_attachment, which write bytes to a local file and return { path, bytes, filename }. Label arguments take label names; unknown names are dropped (or surfaced, for set_issue_labels). list_issues returns a single page; list_all_issues follows pagination.

Authentication

The server authenticates with one of two sources, in precedence order:

  1. FORGEJO_TOKEN (preferred for deployments). When set and non-empty, the server uses it as the bearer token for FORGEJO_HOST directly, with no fj CLI install or keys.json required. This is the supported path for the HTTP service mode. Generate a token in the Forgejo UI under Settings -> Applications -> Generate New Token.

    $env.FORGEJO_TOKEN = "<your-forgejo-access-token>"
    
  2. fj CLI keys.json fallback (convenient for local development). When FORGEJO_TOKEN is unset, credentials come from the fj CLI's keys.json (managed by directories under the Cyborus/forgejo-cli data dir). Authenticate once with the CLI:

    fj auth add-key --host dev.a8n.run
    

    The server loads keys.json on each call, so token refreshes performed by the CLI are picked up automatically. Hosts with no stored login fall back to an unauthenticated client.

Configuration

Variable Default Purpose
FORGEJO_HOST https://dev.a8n.run Base URL of the target Forgejo instance.
FORGEJO_TOKEN (none) Forgejo access token. When set and non-empty, authenticates directly and takes precedence over the fj CLI's keys.json; when unset, the server falls back to keys.json.
RUST_LOG info Log filter (logs go to stderr; over stdio, stdout carries the JSON-RPC stream).
MCP_TRANSPORT stdio Transport to start: stdio or http. Unset or unrecognized values use stdio.
MCP_HTTP_ADDR 127.0.0.1:8080 HTTP-mode bind address. Override to 0.0.0.0:<port> for a container or daemon.
MCP_HTTP_TOKEN (none) HTTP-mode bearer token. Required in http mode: the server refuses to start without it.
MCP_ALLOW_EDIT (unset) Truthy (1/true) grants the EDIT write tier; env equivalent of --allow-edit.
MCP_ALLOW_DELETE (unset) Truthy (1/true) grants the DELETE write tier; env equivalent of --allow-delete.
MCP_ALLOW_GLOBAL_WRITE (unset) Truthy (1/true) grants both tiers; env equivalent of --allow-global-write.

Write permissions

Write access is default-deny and split into three tiers, modelled on Deno's --allow-* switches. With no flag set the server runs the read-only tools and refuses the EDIT-tier ones (create_*, edit_*, set_issue_labels, merge_pull_request) with an error naming the flag to set.

Tier Always on? Enabled by Covers
READ yes (no flag) get_*, list_*, search, view, contents/raw reads, ...
EDIT no --allow-edit / MCP_ALLOW_EDIT=1 create_*, edit_*, update_*, merge, fork, reversible self-toggles, access grants, ...
DELETE no --allow-delete / MCP_ALLOW_DELETE=1 delete_*, access/security revocations, destructive removals, ...

--allow-global-write (env MCP_ALLOW_GLOBAL_WRITE=1) is the master switch and implies both EDIT and DELETE.

A CLI flag, when present, grants its tier; when absent, the matching env var's truthiness (1 or true, case-insensitive) decides. A flag can only grant a tier, never revoke it (default-deny). The effective policy is logged at info on startup in both transports.

Gated tools stay visible in tools/list regardless of policy. Calling one whose tier is disabled returns an error naming the exact flag and env var to set, rather than silently hiding the tool.

On the HTTP transport the bearer token gates all access (see Streamable HTTP), so granting write tiers on an HTTP deployment widens the blast radius of a leaked token.

# stdio, allow creates/edits but not deletes:
./target/release/forgejo-mcp serve --allow-edit

# HTTP, grant everything via env:
$env.MCP_ALLOW_GLOBAL_WRITE = "1"
./target/release/forgejo-mcp serve

Transports

The serve subcommand selects its transport from MCP_TRANSPORT at startup.

stdio (default)

The server reads JSON-RPC from stdin and writes it to stdout; an MCP client launches one process per session and it exits when the client disconnects. This is the mode the client registration example below uses.

Streamable HTTP

MCP_TRANSPORT=http runs a long-lived, network-reachable process that serves the Streamable HTTP MCP endpoint at POST/GET/DELETE {MCP_HTTP_ADDR}/mcp. It binds MCP_HTTP_ADDR and shuts down gracefully on SIGINT/SIGTERM.

Because the server holds Forgejo credentials (FORGEJO_TOKEN, or the fj CLI's keys.json), any reachable endpoint grants authenticated Forgejo access. The HTTP listener is therefore authenticated by default and fails closed: every request must carry Authorization: Bearer <MCP_HTTP_TOKEN> or it gets 401 Unauthorized, and the server refuses to start when MCP_HTTP_TOKEN is unset or empty. TLS is not terminated in-process; put a reverse proxy in front for a public deployment. Supply the upstream credential with FORGEJO_TOKEN so the deployment needs no fj CLI install.

$env.FORGEJO_HOST = "https://dev.a8n.run"
$env.FORGEJO_TOKEN = "<your-forgejo-access-token>"
$env.MCP_TRANSPORT = "http"
$env.MCP_HTTP_ADDR = "127.0.0.1:8080"
$env.MCP_HTTP_TOKEN = "a-long-random-secret"
./target/release/forgejo-mcp serve

Build and run

cargo build --release
$env.FORGEJO_HOST = "https://dev.a8n.run"
./target/release/forgejo-mcp serve

The server reads JSON-RPC from stdin and writes to stdout, so run it directly only for a smoke test. Normally an MCP client launches it. A bare forgejo-mcp with no subcommand prints usage and exits; the stdio server lives behind serve.

CLI

forgejo-mcp --help            # usage for all subcommands
forgejo-mcp --version         # forgejo-mcp <semver> (<git-hash>, built <build-date>)
forgejo-mcp serve             # start the MCP stdio server (read-only by default)
forgejo-mcp serve --allow-edit            # also allow create/edit/merge (EDIT tier)
forgejo-mcp serve --allow-global-write    # allow all writes incl. deletes
forgejo-mcp version           # same banner as --version (alias: version show)
forgejo-mcp version check     # ask the registry whether a newer build is published
forgejo-mcp version update    # download + verify + self-replace the running binary

version update accepts --dry-run (probe + report, no download), --force (reinstall even when not newer), and --url <base> (override the baked registry URL). It verifies a published .sha256 sidecar when present (hard-fail on mismatch, skip when absent) and smoke-tests the swapped binary with --version before reporting success.

Register with an MCP client

Example claude registration (stdio transport):

claude mcp add forgejo --env FORGEJO_HOST=https://dev.a8n.run -- /absolute/path/to/forgejo-mcp/target/release/forgejo-mcp serve

For a running Streamable HTTP server, register the URL transport and pass the bearer token as a header:

claude mcp add --transport http forgejo http://127.0.0.1:8080/mcp --header "Authorization: Bearer a-long-random-secret"

Adding a tool

  1. Add a #[derive(Deserialize, JsonSchema)] params struct in src/server.rs.
  2. Add a #[tool] method on the #[tool_router] impl that builds the client with self.api(), calls the matching fj_core::* wrapper, and returns json(&result).
  3. If the operation does not yet exist in fj-core, add it there first rather than calling forgejo-api directly, so the CLI and the MCP server stay in sync.

Development

This repo uses a justfile for the standard dev workflow. Run just (or just --list) to see every recipe.

just install-hooks   # one-time per clone: install the pre-commit hook
just check           # fmt + clippy + build + builder-stage docker compile
just test            # cargo test
just build           # release binary
just build-docker-export   # extract the dynamic glibc Linux binary via the OCI build
just build-static          # build the fully static musl Linux binary via Docker
just build-windows         # cross-compile the Windows .exe
just create-release minor  # bump version, push release branch, open the PR via fj

just pre-commit runs the same fmt + clippy + build + test steps as .forgejo/workflows/check.yml, inside the rust-builder-glibc image so the toolchain matches CI. Conventions follow the a8n-run/governance repo; see CLAUDE.md for forgejo-mcp-specific notes.

Releases

CI publishes binaries to the Forgejo Generic Packages registry on every push to main and on v* tags:

Artifact Target Linking
forgejo-mcp-linux-x86_64 x86_64-unknown-linux-gnu Dynamic glibc (needs libssl3 at runtime).
forgejo-mcp-linux-x86_64-static x86_64-unknown-linux-musl Fully static (vendored OpenSSL/libgit2/libssh2; runs on any Linux).
forgejo-mcp-windows-x86_64.exe x86_64-pc-windows-gnu mingw cross-compile.

just create-release <major|minor|hotfix> opens the release PR; once merged, .forgejo/workflows/create-release.yml tags and publishes automatically.

Other implementations

Unrelated projects that also implement an MCP server for Forgejo under the same name:

License

Dual-licensed under Apache-2.0 OR MIT (LICENSE-APACHE, LICENSE-MIT), matching the forgejo-cli crates this depends on.