alasin-mcp
Provides branch-and-commit access to a Forgejo instance: list repositories, branches and folders; read files and diffs; create work branches; and commit multiple files in a single commit, while avoiding merges, deletions, settings changes and secrets.
Provides branch-and-commit access to a Gitea instance: list repositories, branches and folders; read files and diffs; create work branches; and commit multiple files in a single commit, while avoiding merges, deletions, settings changes and secrets.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@alasin-mcpcreate a branch fix/typo on acme/api and commit the corrected README"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
alasin-mcp
A remote, multi-tenant MCP server that gives AI agents branch-and-commit access to a Forgejo or Gitea instance.
An agent can read repositories, create a work branch and commit several files to it in one commit. It can't merge, delete, change settings or touch secrets: a person reviews and merges the branch. Alasin is Finnish for "anvil".
Transport: Streamable HTTP (stateless) at
/mcp. Designed to sit behind an OAuth gateway that forwards a bearer token, or to verify JWTs from your identity provider.Multi-tenant: one deployment serves many tenants, each with its own Forgejo. The tenant comes only from the verified token.
Audited and traced: every tool call is an OpenTelemetry span and an append-only audit event; commits carry a
Trace-Idtrailer.
Licence: Apache-2.0.
Tools
Tool | Kind | What it does |
| start | Opens a work session. Returns |
| read | Repositories the tenant may reach (the tenant's allowlist ∩ what the Forgejo account can see). |
| read | Branches, with whether each is protected. |
| read | A folder's entries at a ref (default branch by default). |
| read | A file at a ref; text as UTF-8, anything else as base64; size-capped. |
| read | A branch's commits and a unified diff per file against the default branch (or |
| write | A new branch from the default branch (or |
| write | One commit with several files ( |
Not provided, by design: merging, deleting branches, files or repositories, repository
settings, administration, webhooks and Actions secrets. File deletion and renames are
not exposed either (the contents API's delete operation and from_path are never sent).
Commit messages get trailers added by the server:
Update the headline
Co-Authored-By: Claude <noreply@anthropic.com>
Trace-Id: 0af7651916cd43dd8448eb211c80319c
Alasin-Session: ws_7ZBUuDKkAf-wwjrEiPQ15Uz5The model can't set Trace-Id or Alasin-Session itself; a message containing them is
refused.
Related MCP server: forgejo-mcp
Security model
Identity. Every request to /mcp needs Authorization: Bearer <token>. A
TokenVerifier turns the token into a Principal(tenant, subject, name, email). The
principal is attached to that HTTP request only; tools read it from the request that
carried their call, so concurrent requests can't see each other's identity. No tool has
a tenant, host or URL argument, and headers can't change the principal.
Tenancy. A TenantResolver maps the principal to the tenant's settings: Forgejo
base URL, auth mode, credentials and policy. The Forgejo URL comes only from there. A
repository argument is validated as owner/name (no URLs, no ..) and checked against
the tenant's allowlist, so naming another tenant's repository or host reaches nothing
outside the caller's own Forgejo.
Isolation.
Work sessions, stored tokens and cache entries are keyed by
(tenant, subject). A session ID presented by any other principal is "unknown".Stored tokens (user-delegated mode) are encrypted with AES-256-GCM under a per-tenant data key. Each data key is wrapped by a master key (envelope encryption). The associated data binds every ciphertext to its tenant and subject, so a row copied to another tenant or subject fails to decrypt. Swap
LocalKeyWrapperfor a KMS client by implementingKeyWrapper.Rate limits are per tenant. Audit files are written per tenant.
tests/test_cross_tenant.pyfails if a valid token for tenant A can reach tenant B's Forgejo, cache, session or stored token, including through B's repository or host named in an argument, and under concurrent interleaved calls.
Writes. The server refuses writes to the default branch, to branches matching
protected_branches, to branches outside branch_prefix (when set), and to existing
branches the account can't push to. This is defence in depth: Forgejo's branch
protection and protected file patterns are the real gate, and they apply to the
contents API too (a commit touching a protected path is rejected with 403).
CI workflow folders. Files under .forgejo/, .gitea/ and .github/ (any case)
are refused by default, because a forge with Actions enabled runs them, with the
repository's secrets. A tenant can opt in with allow_ci_paths = true, or change the
list with ci_paths. Protect these paths in Forgejo as well.
Request size. Bodies over server.max_request_bytes (default 8 MiB) are refused with
413 before authentication. Only HTTP is served; WebSocket connections are closed.
Outbound requests. Every value placed in a Forgejo URL is validated and percent-encoded per segment. The HTTP client never follows redirects, so a token can't be carried to another host.
Audit. One outcome event per tool call, success, failure or cancellation: time,
tenant, subject, the caller's name and email when the token carries them, client
address, tool, session ID, trace and span IDs, arguments, outcome, result summary,
repository, branch, commit, author, committer and auth mode. File contents are recorded
as size and SHA-256, not copied. Authorization links and OAuth state never reach audit
events or span status.
Writes fail closed. Before any mutating Forgejo call (creating a branch, committing) an attempt event is written; if it can't be recorded, the write isn't made. The outcome event is written afterwards whatever happens. If only the outcome can't be recorded, the call still returns its result, because the write has landed and an error would invite a retry that duplicates it; the attempt event is the record. With several sinks, every sink is tried, and a failure in any of them counts as a failure.
Auth modes
Chosen per tenant with auth_mode.
Mode | Forgejo credential | Commit author | Committer | Status |
| one token per tenant | the service account, or a trusted override (below) | the service account | implemented |
| Forgejo OAuth2 per person (authorization code + PKCE), stored encrypted, refreshed | the person | the person | implemented, unit-tested; not yet run against a live Forgejo OAuth app |
| short-lived per-repository token from a | the end user from the verified token | the platform bot | interface only; no minter ships |
Service-account author override. By default the service account authors commits.
author_source lets the deployer choose a trusted source for the author; the committer
stays the service account and the audit records both:
token_claims:nameandemailfrom the verified inbound token (JWT verifier). The token must carryemail_verified: true.trusted_header: aName <email>header named byauthor_header. It is honoured only from callers that authenticated with an[[auth.static]]credential markedgateway = true; any other caller sending it is refused, and the config won't load without such a credential. Your gateway must set this header itself and strip any copy sent by clients.tool_argument:author_nameandauthor_emailtool arguments. The model controls these; use only when that is acceptable. Without this opt-in the arguments are refused.
User-delegated mode. When a person has no token yet, tools return a Forgejo
authorization link. Forgejo redirects to /oauth/forgejo/callback; the server exchanges
the code, then checks that the Forgejo account's match_field (default email) equals
the caller's match_claim. When matching on email, the caller's token must carry
email_verified: true and the address must be a verified address of the Forgejo account
(GET /user/emails). That check stops a link opened by someone else from binding their
Forgejo account to your subject. Refreshes are serialised per person; a refresh that
Forgejo rejects with invalid_grant drops the stored token, while any other failure
(an outage, for example) keeps it. Register an OAuth2 application in Forgejo with
the callback URL as its redirect URI. Requires server.master_key_*.
Platform-scoped mode. Implement alasin_mcp.credentials.TokenMinter:
class TokenMinter(Protocol):
async def mint(
self, tenant: TenantConfig, principal: Principal, repo: RepoRef
) -> MintedToken: ...and name a factory package.module:factory (called with the TenantConfig) in the
tenant's platform.minter. Without one, calls fail with a clear "no token minter is
configured" error. Tokens are per repository, so list_repos isn't available in this
mode. The author's email must be verified (email_verified: true).
Branch creation
Chosen per tenant with branch_creation:
api(default): the server's Forgejo account creates the branch.webhook: for setups where another service must create branches. The server sendsPOST <branch_webhook.url>with the JSON body{"tenant", "repo", "branch", "from_ref", "session_id", "subject"}and these headers:X-Alasin-Timestamp: Unix seconds.X-Alasin-Signature:sha256=followed by the hex HMAC-SHA256 of"<timestamp>." + body, keyed with the tenant's webhook secret.
The secret is re-read on every call. After a 2xx reply the server polls Forgejo until the branch exists, up to
timeout_seconds. The receiver should verify the signature and reject stale timestamps.
To add another strategy, implement alasin_mcp.branching.BranchCreator and register it
in Services.branch_creators under a name tenants can select.
Tenant resolver
class TenantResolver(Protocol):
def resolve(self, principal: Principal) -> TenantConfig:
"""Return the tenant's settings, or raise UnknownTenant."""The default, StaticTenantResolver, serves the [[tenants]] from the config file. To
look tenants up elsewhere (a database, a control plane), implement the protocol and
pass it to alasin_mcp.app.from_config(cfg, resolver=...). The principal's tenant comes
from the verifier; the resolver decides what that tenant may reach.
Inbound verifiers implement:
class TokenVerifier(Protocol):
async def verify(self, token: str) -> Principal | None: ...Two ship: StaticTokenVerifier (fixed credentials, each bound to a tenant and subject,
compared by SHA-256 in constant time) and JwtVerifier (JWKS, issuer, audience, expiry;
the tenant from a configured claim).
Sessions and tracing
begin_change mints a work-session ID; every other tool requires it. Sessions expire
after server.session_ttl_seconds (default 12 h).
begin_change always starts a new trace, which becomes the work session's trace. If the
client sent _meta.traceparent (MCP SEP-414), its trace is recorded as a span link, not
adopted, so a client can't choose the Trace-Id stamped into commits. Later tool spans
are parented into the session's trace, so one work session is one trace. Incoming traces
on later calls are linked too. The session's trace ID goes into the Trace-Id trailer and
every audit event.
Spans are exported over OTLP/HTTP when telemetry.otlp_endpoint is set; export is off
by default. Trace IDs are generated either way.
Configuration
ALASIN_CONFIG names a TOML file (default /etc/alasin/config.toml). Secrets are never
written in it: every secret is referenced as <name>_file (a path) or <name>_env (an
environment variable name). See examples/config.toml.
Key | Default | Meaning |
|
| Bind address. |
|
|
|
|
| Proxies whose |
|
| SQLite: sessions, encrypted tokens, OAuth state. |
|
|
|
| none | Base64 of 32 random bytes ( |
|
| Work-session lifetime. |
|
| OTLP/HTTP traces endpoint. |
| 8 MiB | Largest request body, checked before authentication. |
|
| |
|
| |
| The OAuth front door; see OAuth sign-in. | |
|
| |
| Lowercase tenant ID. | |
| Base URL of the tenant's Forgejo. | |
|
|
|
| The service account's Forgejo token (scopes | |
|
|
|
|
| Never written by the server. |
|
| If set, work branches must start with it. |
|
| See Auth modes. |
|
| The |
|
| Per tenant. |
| 1 MB, 50, 5 MB | Read and commit caps. |
|
| |
|
| |
|
| CI workflow folders; see Security model. |
|
|
|
|
|
OAuth sign-in
With [auth.oauth], MCP clients that speak OAuth, such as claude.ai connectors, sign in
to this server directly; no gateway is needed. provider = "origo" runs
origo, an OAuth 2.1 + PKCE authorization server, in
this process. Install it with the extra: pip install 'alasin-mcp[origo]' (the image
includes it).
A request to
/mcpwithout a valid token gets a 401 whoseWWW-Authenticatenames the protected-resource metadata, so the client can discover where to sign in.The discovery and sign-in routes (
/.well-known/...,/authorize,/token,/register,/userinfo) are public; each checks its own credentials.Every token from the front door belongs to the one
tenantconfigured. Its subject isoauth:<client id>: the OAuth client, not a person. origo has no user accounts, so everyone who signs in through one client shares its work sessions. For that reason the tenant must useauth_mode = "service_account"; auser_delegatedorplatform_scopedtenant is refused, since it would share per-person Forgejo tokens.The
oauth:subject prefix is reserved: an[[auth.static]]subject using it is refused, and a JWT whose subject starts with it is rejected.The
base_urlhost must be listed inserver.allowed_hosts, so the DNS-rebinding check covers the host clients sign in at.[[auth.static]]and[auth.jwt]keep working beside it on the same/mcp, so a machine caller (a tool registry listing tools, say) can keep a static bearer.
Warning:
public_registrationlets anyone on the internet connect. With it on, any client that can reach this server registers itself at/registerand gets a token for the tenant: origo checks no user. It is refused unless the config also setspublic_registration_i_understand_anyone_can_connect = true. Leave it off and list each client under[[auth.oauth.clients]], unless something in front of this server (a network policy, an access proxy) already limits who can reach it.
Another OAuth or OIDC issuer can be added as a further front door (oauth.py): its
verifier is a JwtVerifier against the issuer's JWKS.
Running
docker build -t alasin-mcp .
docker run -p 8000:8000 \
-v ./config.toml:/etc/alasin/config.toml:ro \
-v ./secrets:/run/secrets:ro \
-v alasin-state:/var/lib/alasin \
alasin-mcpBehind an OAuth gateway: the gateway authenticates the user, then forwards /mcp to this
server with Authorization: Bearer <the static credential>. /healthz is public, as is
/oauth/forgejo/callback (used only by user-delegated tenants). Point allowed_hosts at
the host name the gateway uses for this upstream.
The Forgejo service account needs write access to the lane repositories and nothing more: not an admin, not an organisation owner. Put branch protection on the default branch, and protected file patterns on work branches for anything the agent must not change.
Development
uv sync
uv run ruff check . && uv run ruff format --check . && uv run pyright
uv run pytest tests/test_units.py tests/test_cross_tenant.py # no Docker needed
uv run pytest tests/test_integration.py # starts Forgejo in DockerThe integration tests start a pinned Forgejo image (tests/forgejo.py) with a 512 MB
memory cap and tear it down afterwards. Every test has a timeout. CI sets
ALASIN_REQUIRE_FORGEJO=1, which turns a missing Docker into a failure instead of a skip.
Source files carry an SPDX-License-Identifier: Apache-2.0 header
(scripts/check_spdx.py checks it).
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
AI-native git hosting — repos, PRs, issues, CI gates, and AI code review over MCP (60 tools).
Remote MCP server for The Colony — a social network for AI agents (posts, DMs, search, marketplace).
MCP server for AI agents to plan, verify, and deploy Cloudflare-native apps.
Related MCP Servers
- AlicenseAqualityCmaintenanceAn MCP server that enables AI assistants to read, create, and manage issues, repositories, and releases on Gitea, Forgejo, or Codeberg instances via personal access tokens.21119 PyPI2MIT
- AlicenseBqualityDmaintenanceA Model Context Protocol (MCP) server for Forgejo and Gitea instances, enabling AI assistants to interact with repositories, issues, pull requests, and more.100522 npm13MIT
- AlicenseNot gradedqualityDmaintenanceA secure and scalable Git MCP server giving AI agents powerful version control for local and (soon) serverless environments.202 npmApache 2.0
- AlicenseCqualityDmaintenanceA policy-aware MCP server for GitHub and GitHub Actions that enables safe AI-assisted infrastructure workflows—inspecting repositories, preparing branches and pull requests, and constrained remote mutations behind explicit preview-bound approval tokens.18MIT