- Rust 86.5%
- Fluent 11.6%
- Dockerfile 0.6%
- HTML 0.4%
- Just 0.4%
- Other 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Check / fmt + clippy + build + tests (push) Successful in 6m1s
Reviewed-on: #92 |
||
| .cargo | ||
| .forgejo/workflows | ||
| .idea | ||
| common@4de5dfa607 | ||
| crates | ||
| docs | ||
| oci-build | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .gitmodules | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| compose.mcp.yml | ||
| flake.lock | ||
| flake.nix | ||
| justfile | ||
| LICENSE-APACHE | ||
| LICENSE-MIT | ||
| README.md | ||
fj, a CLI client for Forgejo
Note
This is a hard fork of forgejo-cli, created and maintained by the Forgejo CLI contributors. All credit for the original tool belongs to them, and this fork inherits their dual Apache-2.0 / MIT licensing (see Licensing below).
The fork exists for one reason: this copy is developed using AI-assisted code, which the Forgejo governance does not accept (per the Forgejo AI Agreement). To respect that policy, AI-assisted work happens here rather than being submitted upstream. The fork is operated in the spirit of the Forgejo Code of Conduct, and changes are not contributed back unless they can be reauthored to comply with the upstream AI Agreement.
If you want the canonical, AI-free tool, use the upstream project at https://codeberg.org/forgejo-contrib/forgejo-cli.
fj is a CLI client for Forgejo, akin to gh, glab, or tea!
You can...
- Open, edit, comment on, close issues
- Create and merge pull requests
- Easily create AGit pull requests, no need to fork!
- Create, star, watch, and edit repositories
- Manage organizations and teams
- Publish new releases
- Run a Model Context Protocol (MCP) server so AI agents can drive Forgejo (
fj mcp serve)
all from the command line! Check out the wiki for more details and explanations.
fj doesn't try to replace your usage of git, it's meant to work alongside it.
It handles all the Forgejo-specific things that git doesn't.
Installation
Pre-built binaries are available for x86_64 Windows and x86_64 Linux (GNU) on the
releases tab.
See the (forgejo-cli) wiki page on installation for more options.
Building on macOS
There is no pre-built macOS binary, but cargo build --release works on a stock Mac (Intel or Apple silicon) with only the Xcode command line tools (xcode-select --install) installed. No Homebrew, pkg-config, or system OpenSSL is required: because macOS ships neither OpenSSL headers nor pkg-config, and git2's https + ssh features pull in openssl-sys (via libgit2-sys and libssh2-sys), the fj crate declares a vendored openssl for cfg(target_os = "macos") so OpenSSL is compiled from source as part of the build. That is declared twice, once under [target.'cfg(target_os = "macos")'.dependencies] and once under the matching build-dependencies: ssh2-config has an unconditional git2 build-dependency, and Cargo's v2 resolver resolves build-dependency features separately, so the build-script half of the graph needs its own vendored entry.
The vendored build shells out to perl and cc, both of which come with macOS and the command line tools. Expect the first build to take a few minutes longer while OpenSSL compiles; it is cached afterwards.
Note that just check and just pre-commit run the checks inside dev_image via docker run, so they additionally need Docker. Plain cargo build, cargo test, cargo fmt, and cargo clippy run natively without it.
Architecture
fj is a Cargo workspace (Cargo.toml) with four members. The split exists so the API and domain logic stay reusable
by the Forgejo MCP server without dragging in the terminal CLI.
crates/fj-client(library): the connection and credential layer.keys.jsonstorage (KeyInfo/LoginInfo),forgejo_api::Forgejoclient construction, OAuth client-id lookup, and host / SSH / URL resolution.crates/fj-core(library): CLI-agnostic domain logic. Per-command operation modules that wrapforgejo-apicalls and return its structs (issues,prs,repo,org,release,user,actions,tag,wiki), the issue-template parser, and shared types such asRepoName.crates/fj(binary): everything CLI-specific. Theclapcommand tree,crosstermterminal handling,fluentlocalization, interactive prompts, and all table / markdown output rendering.crates/fj-mcp(library): the Model Context Protocol server surface. Wraps thefj-coreoperations as MCP tools over stdio and Streamable HTTP; thefjbinary drives it viafj mcp serve.
Reuse contract
fj-client and fj-core are CLI-free and intended for reuse: the Forgejo MCP server (crates/fj-mcp) depends on them. To keep
them reusable, neither library may depend on clap, crossterm, or any fluent-* crate - the command tree,
terminal handling, localization, and all rendering stay in the fj binary. Verify with:
cargo tree -p fj-client -p fj-core -e normal | grep -E 'clap|crossterm|fluent'
which must return nothing.
comrak (CommonMark) is the one intentional exception, and it is deliberately not part of that grep. It is allowed
in fj-core because it powers the issue-template parser (crates/fj-core/src/template/yaml.rs), which reads and
rewrites a markdown AST via comrak::parse_document and comrak::format_commonmark. That is parsing, which an MCP
consumer needs, not rendering. Terminal / ANSI markdown rendering (the markdown() printer in crates/fj/src/main.rs)
lives only in the fj binary.
Using the crates from another project
Pull the libraries in as a git dependency, pinned to a release tag:
[dependencies]
fj-core = { git = "https://dev.a8n.run/pandoras-box/forgejo-cli.git", tag = "v0.6.0" }
fj-client = { git = "https://dev.a8n.run/pandoras-box/forgejo-cli.git", tag = "v0.6.0" }
fj-core depends on fj-client, so depending on fj-core alone pulls the client layer in transitively. If the crates
are published to the Forgejo Cargo package registry on
dev.a8n.run, a downstream project can configure that as a sparse
registry and depend on fj-core / fj-client by version
instead of by git tag.
MCP server
fj embeds a Model Context Protocol server that exposes Forgejo operations as MCP
tools, so an MCP client (Claude, an IDE, an agent) can drive Forgejo through the same fj-core operations the CLI uses.
Start it with:
fj mcp serve
By default it speaks MCP over stdio (JSON-RPC on stdout, logs on stderr), which is how an MCP client launches it per
session. For an MCP-client config, prefer the explicit fj mcp stdio form, which always runs the stdio transport
regardless of FORGEJO_MCP_TRANSPORT and takes the same write-policy flags as serve:
fj mcp stdio
Register it with Claude:
claude mcp add forgejo -- fj mcp stdio
fj mcp serve without FORGEJO_MCP_TRANSPORT remains equivalent to fj mcp stdio for backward compatibility.
Set FORGEJO_MCP_TRANSPORT=http to run serve instead as a long-lived, multi-user Streamable HTTP web service
bound to FORGEJO_MCP_HTTP_ADDR (default 127.0.0.1:8080). The web layer comes from the shared
mcp-web crate: local accounts (email + password + TOTP), browser
sessions, an OAuth 2.1 Authorization Server (dynamic client registration, PKCE, both .well-known discovery
documents), encryption at rest, and a SQLite state store with an expiry sweep. /mcp is gated by the crate's
Resource-Server bearer middleware, so a caller must present an app-issued OAuth access token bound to a local account;
an unauthenticated request gets a 401 carrying a WWW-Authenticate challenge that points at the protected-resource
metadata, which is how Claude discovers the authorization server and completes the OAuth flow.
The service fails closed at startup if FORGEJO_MCP_MASTER_KEY (AEAD encryption at rest) or FORGEJO_MCP_SESSION_KEY
(session-cookie signing) is missing or malformed; each is a base64 encoding of 32 bytes, minted with
random binary 32 | encode base64. Neither is ever generated: a fresh key on restart would invalidate every session and orphan
every encrypted credential, so a missing key is a misconfiguration, not something the service papers over.
FORGEJO_MCP_ADMIN_EMAIL bootstraps the first admin account (the signup matching it is created as an admin).
Host and credentials. Each account acts through its own Forgejo instance: a host plus a personal access token
(minted in Forgejo under Settings -> Applications -> Generate Token), stored encrypted at rest and set on the account's
/instance page. Every tool call resolves the calling account's host and token from that row, so two accounts reach two
different Forgejo servers and no request executes as a host-level identity or unauthenticated. An account with no
instance yet gets an error naming where to add one, not a silent fallback. FORGEJO_HOST, FORGEJO_TOKEN, and the
fj CLI's keys.json are not consulted in HTTP mode; they remain the stdio transport's single-identity
configuration. Saving or testing an instance probes the Forgejo user endpoint and reports the resolved login, so a bad
token, an unreachable host, or a non-Forgejo URL surfaces there rather than at tool-call time.
Write policy (default-deny). With no opt-in the server exposes only read-only tools. The three tiers are
edit (create / edit / update / merge / fork), delete (destructive / access-revoking), and global write (both).
Over stdio (fj mcp stdio, or serve without FORGEJO_MCP_TRANSPORT=http) the server has one identity, so the
--allow-edit / --allow-delete / --allow-global-write flags (and their FORGEJO_MCP_ALLOW_* env equivalents,
truthy on 1 / true) are the effective policy, exactly as before.
Over HTTP the server is multi-user, so each account chooses its own policy on its /instance page (three toggles,
default-deny), and the FORGEJO_MCP_ALLOW_* env vars change meaning: they are now the server-level ceiling, not the
effective policy. The effective policy for a call is the intersection of the ceiling and the account's own choice, so:
- Setting
FORGEJO_MCP_ALLOW_EDIT=1(etc.) grants a maximum an account may opt into, not a permission every account now has. An upgraded deployment does not silently start mutating: accounts still default to deny and must opt in. - A tier the ceiling does not permit is shown disabled on the account page and can never be enabled, so a read-only
deployment (no
FORGEJO_MCP_ALLOW_*set) stays read-only whatever any account ticks. - A denied tool call tells the account whether it has not enabled the tier (tick the box on
/instance) or the deployment forbids it (ask the operator), because those need different actions.
| Env var | Purpose |
|---|---|
FORGEJO_HOST |
stdio only: target Forgejo instance (overridden by --host). Ignored in HTTP mode (per-account host). |
FORGEJO_TOKEN |
stdio only: auth token, falling back to keys.json when unset. Ignored in HTTP mode (per-account token). |
FORGEJO_MCP_TRANSPORT |
http selects the Streamable HTTP web service; anything else is stdio. |
FORGEJO_MCP_HTTP_ADDR |
HTTP bind address (default 127.0.0.1:8080). |
FORGEJO_MCP_MASTER_KEY |
Required in HTTP mode: base64 32-byte AEAD key for encryption at rest (fails closed; never generated). |
FORGEJO_MCP_SESSION_KEY |
Required in HTTP mode: base64 32-byte key signing the session cookie (fails closed; never generated). |
FORGEJO_MCP_ADMIN_EMAIL |
Optional: the signup matching this email is bootstrapped as the first admin. |
FORGEJO_MCP_DATABASE_URL |
Optional: sqlx SQLite URL for the state store (default sqlite://forgejo-mcp.db). |
FORGEJO_MCP_PUBLIC_URL |
Optional: public base URL every issued OAuth URL anchors on (default http://<bind addr>). |
FORGEJO_MCP_ALLOW_EDIT / FORGEJO_MCP_ALLOW_DELETE / FORGEJO_MCP_ALLOW_GLOBAL_WRITE |
stdio: the effective write policy. HTTP: the server-level ceiling (max any account may enable; per-account choice is set on /instance, default-deny). |
Run it as an HTTP web service (Docker)
The HTTP transport ships as a container configured entirely by environment variables. compose.mcp.yml is the
single supported deployment: it builds the runtime image from source, runs fj mcp serve with
FORGEJO_MCP_TRANSPORT=http, and persists the SQLite state store on a named volume.
cp .env.example .env
# Fill in the required values, minting each key with `random binary 32 | encode base64`:
# FORGEJO_MCP_PUBLIC_URL, FORGEJO_MCP_MASTER_KEY, FORGEJO_MCP_SESSION_KEY
docker compose --file compose.mcp.yml up --build --detach
CI also publishes that same runtime image, so a host that should not carry a Rust toolchain or a source checkout can
pull it instead of building: dev.a8n.run/pandoras-box/forgejo-cli:latest tracks main and :vX.Y.Z is the immutable
release tag (linux/amd64). See docs/deployment.md.
See docs/deployment.md for reverse-proxy TLS termination, the public base URL contract, volume
persistence, key generation and custody, first-admin bootstrap, and upgrades. .env.example lists every variable the
service reads.
Important
Removed: the earlier
fj mcp service install/uninstallsystemd path is gone. If you ran it, stop and disable the unit yourself before upgrading (systemctl --user disable --now forgejo-mcp.service, remove the unit file and~/.config/forgejo-cli/mcp.env); the migration steps are indocs/deployment.md.
Licensing
This project is licensed under either Apache License Version 2.0 or MIT License at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.