- TypeScript 92.5%
- JavaScript 7.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| docs | ||
| nbproject | ||
| scripts | ||
| src | ||
| test | ||
| .gitignore | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.build.json | ||
| tsconfig.json | ||
forgejo-mcp
MCP (Model Context Protocol) server that gives coding agents access to a Forgejo instance — issues (search, read, create, edit, comments, labels, milestones) and wiki pages (read + write).
Built and verified against https://source.16reiche.de (Forgejo 16.0.5).
The API spec used for development is snapshotted in docs/swagger.json.
Setup
npm install
npm run build
Requires Node ≥ 20.
To get a forgejo-mcp command on your PATH (recommended for MCP clients):
npm run install:global # builds, then installs into ~/.local (no sudo needed)
npm run uninstall:global # removes it again
The prefix defaults to ~/.local (override with FORGEJO_MCP_PREFIX or
--prefix <dir>); the script warns if <prefix>/bin is not on your PATH.
It installs from an npm-packed tarball, so the copy is self-contained
(dist/ plus runtime dependencies) and independent of this checkout.
Environment variables
| Variable | Required | Default | Purpose |
|---|---|---|---|
FORGEJO_URL |
yes | — | Instance base URL (/api/v1 is added) |
FORGEJO_TOKEN |
for writes | — | API token (Settings → Applications) |
FORGEJO_TIMEOUT_MS |
no | 30000 |
Per-request timeout; 0 disables it |
The server exits with a clear error if FORGEJO_URL is missing — set it
explicitly in your MCP client configuration (see the OpenCode example below).
Without a token, public read endpoints still work (writes will return 401).
owner and repo are required tool arguments on repository-scoped tools;
issue_search may omit them to search across all repositories the token can
see.
Use with OpenCode
Run npm run install:global once, then register the server in
~/.config/opencode/opencode.json:
{
"mcp": {
"forgejo": {
"type": "local",
"command": ["forgejo-mcp"],
"environment": {
"FORGEJO_URL": "https://source.16reiche.de",
"FORGEJO_TOKEN": "{env:FORGEJO_TOKEN}"
},
"timeout": 30000,
"enabled": true
}
}
}
OpenCode then exposes the tools as forgejo_issue_search,
forgejo_wiki_get_page, … The timeout raises OpenCode's 5 s MCP default so
slower API calls are not cut off.
Any other MCP client works too — the server speaks plain stdio:
forgejo-mcp # after install:global, or from a checkout:
node dist/index.js
Tools (16)
| Tool | Forgejo API | Notes |
|---|---|---|
wiki_list_pages |
GET /repos/{o}/{r}/wiki/pages |
|
wiki_get_page |
GET /repos/{o}/{r}/wiki/page/{name} |
Returns decoded markdown |
wiki_create_page |
POST /repos/{o}/{r}/wiki/new |
Content sent as base64 internally |
wiki_edit_page |
PATCH /repos/{o}/{r}/wiki/page/{name} |
Replaces the whole body; omitting content keeps the current body (fetched and resent) |
wiki_delete_page |
DELETE /repos/{o}/{r}/wiki/page/{name} |
Destructive |
issue_search |
GET /repos/{o}/{r}/issues or GET /repos/issues/search |
repo scope when owner+repo known, else cross-repo; state defaults to all |
issue_get |
GET /repos/{o}/{r}/issues/{index} |
Full body |
issue_create |
POST /repos/{o}/{r}/issues |
Label names resolved to ids (repository labels first, then the organization's), milestones repo-only |
issue_edit |
PATCH /repos/{o}/{r}/issues/{index} + PUT .../labels |
Only provided fields change; labels replaces the label list — names (repository + organization) validated before any mutation, partial failures reported explicitly |
issue_comments_list |
GET /repos/{o}/{r}/issues/{index}/comments |
|
issue_comment_create |
POST /repos/{o}/{r}/issues/{index}/comments |
|
issue_comment_edit |
PATCH /repos/{o}/{r}/issues/{index}/comments/{id} |
|
issue_comment_delete |
DELETE /repos/{o}/{r}/issues/{index}/comments/{id} |
Destructive |
label_list |
GET /repos/{o}/{r}/labels + GET /orgs/{o}/labels |
Repository labels first, then the organization's (if the owner is an org); each label carries scope of "repo" or "org". A missing organization token scope is reported as org_labels_unavailable instead of failing |
milestone_list |
GET /repos/{o}/{r}/milestones |
|
milestone_set |
POST /repos/{o}/{r}/milestones or PATCH .../{id} |
Omit id to create (title required), pass id to update |
Tool outputs are deliberately compact (curated fields, no raw API dumps) to
keep agent context small; issue_get and wiki_get_page return full content.
Scripts
npm run build # compile to dist/
npm run typecheck # tsc over src + tests
npm test # vitest (unit + in-memory MCP integration tests)
npm run install:global # build + install forgejo-mcp into ~/.local/bin
npm run uninstall:global # remove the global install
npm run smoke # read-only live test against the instance
npm run inspect # MCP Inspector for manual exploration
npm run smoke accepts SMOKE_OWNER / SMOKE_REPO to target a specific
repository (defaults to thirdparty/daisy-diff) and needs FORGEJO_URL
(and optionally FORGEJO_TOKEN) in the environment.
npm run inspect starts the MCP Inspector with an ad-hoc server, so the
session is read-only and the server entry cannot be edited in the UI. Pass the environment to the
spawned server explicitly with -e KEY=VALUE:
npm run inspect -- -e FORGEJO_URL=https://codeberg.org -e FORGEJO_TOKEN=<token>
Design notes
- Thin hand-written
fetchclient (~20 endpoints) instead of a generated SDK — the spec snapshot indocs/swagger.jsonis the source of truth. - Response types are hand-derived from the spec's
#/definitions. - Errors are returned as MCP error results (
isError: true) carrying the HTTP status and Forgejo's message — never crashes the server. - Missing
FORGEJO_TOKENlogs a warning and allows anonymous reads (verified against the live instance) instead of failing startup.