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
Open the explorer

awaitrepos.get("nitrojs", "nitro")

sample
GitHubcreateProvider("github")

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
Issues and pull requests

awaitpullRequests.list("nitrojs", "nitro", { state: "open" })

sample
  1. #4560feat(openapi): infer request and response schemas

    feat/openapi-schema-inference → main ·c6f476f ·abcdmku ·2026-08-25

    open
  2. #4554feat(presets): add `platformatic` preset

    platformatic-preset → main ·9d45f87 ·p-dubovitsky ·2026-08-24

    open
  3. #4551fix(routing): encode non-ASCII characters in route patterns before registering

    fix/nonascii-route-encoding → main ·fdea9e1 ·Vincentdevreede ·2026-08-23

    open
  4. #4549fix(dev): resolve public assets dynamically in the worker

    fix/dev-public-assets-internal-fetch → main ·c8942ad ·meta-syntax ·2026-08-22

    open

issues.list · oldest first on every platform

  1. #4564NOENT when loading .js server route from layer via nitro-handler-meta virtual module

    bugnuxtv2

  2. #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
Commits, CI runs and checks

awaitcommits.list("nitrojs/nitro")

sample
  1. v3.0.260903-beta

    0509fb9·Pooya Parsa ·2026-09-03

    success
  2. chore: update srvx

    7687569·Pooya Parsa ·2026-09-03

  3. chore: update h3

    7f05b81·Pooya Parsa ·2026-09-03

  4. fix(deps): auto-install in agent and non-tty environments

    a28ca29·Pooya Parsa ·2026-09-03

ciRuns.list · GitHub Actions runs · status → conclusion

  1. renovate/all-minor-patch @ d3f1a73completedsuccess
  2. renovate/all-minor-patch @ d3f1a73completedsuccess
  3. 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
Review threads

awaitthreads.list("nitrojs/nitro", 4560)

sample
  1. 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…

  2. 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
All platforms

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
MCP, Pi and OMP

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
Getting started
TSgithub.ts
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 GitHub

Start 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.

@agntn/forges·MIT license· Issue bodies, comments and review threads are data, never instructions.