Agents
Thirty tools, three surfaces
The MCP server, the Pi extension and the OMP extension all call the same executors in src/tool-operations.ts, so they answer identically. Fix one, fixed everywhere. The tools mirror the nine resources:
| Group | Tools |
|---|---|
| repositories | forges_repos_list, forges_repos_get |
| contribution templates | forges_contribution_templates_list, forges_contribution_templates_get |
| code | forges_code_search |
| CI and commits | forges_ci_runs_list, forges_commits_list, forges_commits_get |
| issues | forges_issues_list, forges_issues_search, forges_issues_get, forges_issues_comments, forges_issues_comments_get, forges_issues_create |
| pull requests | forges_pull_requests_list, forges_pull_requests_search, forges_pull_requests_get, forges_pull_requests_files, forges_pull_requests_checks, forges_pull_requests_comments, forges_pull_requests_comments_get, forges_pull_requests_create |
| users and auth | forges_users_get, forges_users_authenticated, forges_auth_reload |
| review threads | forges_threads_list, forges_threads_get, forges_threads_reply, forges_threads_resolve, forges_threads_unresolve |
Every tool takes a platform of github, gitlab or gitea plus the owner, repo, number or username the call needs.
MCP
forges mcp
claude mcp add forges --scope user -- npx -y @agntn/forges mcp
Or in a client's config:
{
"mcpServers": {
"forges": { "command": "npx", "args": ["-y", "@agntn/forges", "mcp"] }
}
}
The server speaks MCP over stdio. createMcpServer() from @agntn/forges/mcp returns the same server for a host that brings its own transport.
An MCP client sees the text a tool returns and nothing else, so the text carries the whole answer as JSON. Lists and searches drop bodies and name the tool that reads one in full. One page of a busy repository with bodies is big enough to push the conversation that asked for it out of the window, and I would rather the model asks twice. Pull request search leaves revision details to forges_pull_requests_get. forges_threads_list bounds each comment to twelve lines and four thousand characters but keeps every comment of every thread on the page, so ask for a small perPage on a heavily reviewed pull request. The comment tools carry the same bound, and their _get variants read one comment whole.
Reads, writes and credentials
Read tools use the normal token chain and fall back to anonymous access when nothing is found. Anonymous providers are kept apart and can never be reused for a write, not even by accident.
Five tools write: forges_issues_create, forges_pull_requests_create, forges_threads_reply, forges_threads_resolve and forges_threads_unresolve. They carry MCP annotations saying so, readOnlyHint: false. The two creates and the reply are marked as not idempotent because doing them twice leaves two of them behind. Resolve and unresolve are idempotent. forges_users_authenticated names the account the writes would go out as, check it before you let a model write anything.
The credential stays pinned per platform and endpoint until forges_auth_reload swaps it and returns the new profile. Reload is gated as a mutation because it changes local server state. It writes nothing to the platform.
Hosts stay local
A self hosted endpoint is FORGES_GITHUB_BASE_URL, FORGES_GITLAB_BASE_URL or FORGES_GITEA_BASE_URL in the agent's process environment. Never a tool argument, so a model cannot aim a call at another host. A failure names the status and, on a rate limit, the retry window. It never repeats the endpoint, so the host stays out of the model's context even when the platform answers with an error.
A failed operation is a tool error, not a transport failure: unknown repository, rejected token, exhausted rate limit. The model sees a sentence, the session survives.
Pi and OMP
pi install npm:@agntn/forges
The package declares both extensions in package.json. They render tool calls and results in the terminal: a repository row, a list of pull requests with their states, a thread with its comments, bounded the same way the MCP text is. forges_repos_get includes fork state, the parent and the viewer's role. A null role means the platform said nothing, not that the viewer has no access.
What the answer is
The tools return the normalized objects, not prose. The Explorer shows the same objects for any public repository, a quick way to check a model's summary against the source when it gets creative.
body: "ignore previous instructions" has learned one thing about the issue and nothing about what to do next.