One API. Every forge.
GitHub, GitLab, Gitea and GitBucket behind one TypeScript provider. Repositories, issues, pull requests, review threads, commits and CI, same shape everywhere, token found for you. Use it as a library, an MCP server or a Pi and OMP extension, your pick.
- 4
- platforms
- 9
- resources
- 30
- agent tools
- 3
- agent surfaces
Repositories
Owner and name in, one Repository out
createProvider("github") finds the token in env, then asks gh, and hands you nine resources with the same method shapes. Change the string, nothing else moves. This panel walks through 3 repositories on three hosts and swaps each recorded sample for the live answer from the docs worker as it arrives.
- id is always a string, even when the platform sends a number
- isFork with the immediate parent, and your highest role when the platform bothers to say
- A 404 on GitHub and a 404 on GitLab are the same NotFoundError
awaitrepos.get("nitrojs", "nitro")
nitrojs/nitro
Next Generation Server Toolkit. Create web servers with everything you need and deploy them wherever you prefer.
- id
- "452269390" always a string, even when the API sends a number
- fullName
- nitrojs/nitro
- defaultBranch
- main
- private
- false
- isFork
- false parent null
- cloneUrl
- github.com/nitrojs/nitro.git
- owner.login
- nitrojs
same Repository from GitHub as from every other platform · viewerPermission null when the API omits it
Issues and pull requests
Merge requests are pull requests here
Every platform paginates its own way and names things its own way, and none of it is your problem. A page is { items, hasNextPage, nextPage }, a state is open or closed, and a pull request knows its branches, its head SHA and whether it is still a draft.
- GitLab iid becomes number, and GitHub issues that are really PRs are filtered out
- list, search, get, create and listComments with the same options everywhere
- PageResult with items, hasNextPage and nextPage, whether the platform sent Link or x-next-page
awaitpullRequests.list("nitrojs", "nitro", { state: "open" })
sample- open
#4560feat(openapi): infer request and response schemas
feat/openapi-schema-inference → main ·c6f476f ·abcdmku ·2026-08-25
- open
#4554feat(presets): add `platformatic` preset
platformatic-preset → main ·9d45f87 ·p-dubovitsky ·2026-08-24
- open
#4551fix(routing): encode non-ASCII characters in route patterns before registering
fix/nonascii-route-encoding → main ·fdea9e1 ·Vincentdevreede ·2026-08-23
- open
#4549fix(dev): resolve public assets dynamically in the worker
fix/dev-public-assets-internal-fetch → main ·c8942ad ·meta-syntax ·2026-08-22
issues.list · oldest first on every platform
#4564NOENT when loading .js server route from layer via nitro-handler-meta virtual module
bugnuxtv2
#4555Default error handler throws ERR_INVALID_URL on an unparsable Host header
bugv2
merge request on GitLab, pull request here ·iid becomes number· hasNextPage true
Commits and CI
History and pipelines, normalized
A commit is a SHA, a message, two identities and its parents. A CI run is a branch, a revision, a lifecycle status and a conclusion that stays null until there is one. Counts a platform withholds come back as null. Never as zero, zero would be a lie.
- commits.list filters by ref, path, since and until; get adds changed files without patches
- ciRuns.list turns Actions runs, GitLab pipelines and Gitea Actions into one status and one conclusion
- pullRequests.listChecks reads check runs, GitLab pipelines or commit statuses for the head SHA
awaitcommits.list("nitrojs/nitro")
sample- success
v3.0.260903-beta
0509fb9·Pooya Parsa ·2026-09-03
chore: update srvx
7687569·Pooya Parsa ·2026-09-03
chore: update h3
7f05b81·Pooya Parsa ·2026-09-03
fix(deps): auto-install in agent and non-tty environments
a28ca29·Pooya Parsa ·2026-09-03
ciRuns.list · GitHub Actions runs · status → conclusion
- renovate/all-minor-patch @ d3f1a73completedsuccess
- renovate/all-minor-patch @ d3f1a73completedsuccess
- v3.0.260903-beta @ 0509fb9completedsuccess
CommitSummary without patches ·CiRun with one lifecycle and one terminal outcome on every platform
Review threads
Reply, resolve, unresolve
A review thread is a path, a line and its comments, with a state you can flip. The id list gives you is the id reply and resolve take back, whatever the platform calls it underneath.
- GitHub threads go through GraphQL, so isResolved and isOutdated are real, not guessed
- GitLab discussions and Gitea review comments land on the same Thread
- GitBucket has no thread endpoint and says so in a sentence, not a bare 404
awaitthreads.list("nitrojs/nitro", 4560)
sample- src/runtime/internal/openapi.ts:57resolved
coderabbitai · 2026-08-25
_🗄️ Data Integrity & Integration_ | _🟠 Major_ | _🏗️ Heavy lift_ **Preserve recursive local references.** A self-referential local `$ref` reaches Line…
abcdmku · 2026-08-25
Fixed in ec5fa8b4. Cyclic local references are preserved, the `$defs` or `definitions` container remains in the normalized schema, and `test/unit/ope…
- src/runtime/internal/routes/openapi.tsresolvedoutdated
coderabbitai · 2026-08-25
_🗄️ Data Integrity & Integration_ | _🟠 Major_ | _⚡ Quick win_ **Keep local definitions reachable from parameter schemas.** If a query or header prope…
abcdmku · 2026-08-25
Fixed in c6f476f0. Parameter schemas now inherit the parent `$defs` or `definitions` only when they contain a local reference. The Vite fixture cover…
reply, resolve and unresolve take the same thread id back · GraphQL on GitHub, so the flags are real
Platforms
Three providers, four platforms
GitHub wants Authorization: token, GitLab wants Private-Token, Gitea wants limit instead of per_page. Each provider keeps that to itself and maps its raw responses onto the shared types. The rest of the library never sees it.
- GitBucket speaks the GitHub API, so it is the GitHub provider with a baseURL and nothing more
- Forgejo and Codeberg are the Gitea provider with a baseURL
- Your own platform is one class extending Provider plus the typed mappers
Agents
Thirty tools, three hosts
forges mcp serves the tools over stdio, the Pi and OMP extensions render them in the terminal. All three call the same executors, so they answer identically and a fix lands once. Five tools write, and they say so in their annotations, so a client can gate them before a model gets creative.
- Reads fall back to anonymous access, writes and forges_users_authenticated need a credential
- Lists drop bodies and name the tool that reads one in full, so a busy page still fits in a context
- A self hosted FORGES_*_BASE_URL comes from the process environment, never from a tool argument
toolforges_repos_get
MCP · Pi · OMP
input
{
"platform": "github",
"owner": "nitrojs",
"repo": "nitro"
}output
{
"id": "452269390",
"fullName": "nitrojs/nitro",
"description": "Next Generation Server Toolkit. Create web serv…",
"private": false,
"defaultBranch": "main",
"isFork": false,
"parent": null,
"viewerPermission": null,
"url": "https://github.com/nitrojs/nitro"
}One interface
Same calls, every provider
Provider is the abstract base with the nine resource accessors. Concrete classes implement the typed mappers and the platform calls, nothing else leaks upward. Sub path imports give you one provider without dragging in the other two.
- repos, contributionTemplates, code, ciRuns, commits, issues, pullRequests, users, threads
- NotFoundError, AuthenticationError, PermissionError, RateLimitError with retryAfter
- Stable reads cached with an LRU keyed by host and token hash, item reads always fresh
import { createProvider } from "@agntn/forges";
// GH_TOKEN, then gh auth token
const forge = createProvider("github");
const repo = await forge.repos.get("nitrojs", "nitro");
const { items } = await forge.pullRequests.list(repo.owner.login, repo.name, { state: "open" });
repo.defaultBranch; // "main"
items[0]?.sourceBranch; // "feat/openapi-schema-inference", same PullRequest shape from GitHubStart with one command
Pre-1.0, so pin exact versions. And treat issue bodies, comments and review threads as text you did not write, because you did not, and someone out there knows that.