AI assistants can write code, explain a repository and prepare changes.
But without access to your GitLab instance, they only know what you paste into the conversation.
This quickly creates some problems:
- repository context becomes outdated;
- copying files by hand is slow;
- write operations need strict permissions;
- sharing a full GitLab token with each client is not a good idea.
So I built GitLab MCP Server.
It is a stateless Model Context Protocol server written with Symfony. It lets ChatGPT and other MCP clients work with GitLab.com or a self-hosted GitLab instance.
What can it do?
The server exposes GitLab operations as MCP tools.
Read tools can:
- list and inspect projects;
- browse a repository tree;
- read files;
- list branches.
Write tools can:
- create or update a project;
- archive or unarchive a project;
- create a branch;
- create or update files;
- apply up to 100 file changes in one atomic commit.
Destructive tools are isolated from normal write tools. Deleting a file or a project requires an additional permission and must be explicitly enabled.
Security first
Giving an AI client access to source code requires more than putting a GitLab token in an environment variable.
The server separates permissions into three scopes:
gitlab.read
gitlab.write
gitlab.destructive
A write operation requires both gitlab.read and gitlab.write. A destructive operation also requires gitlab.destructive.
The production authentication mode uses OAuth 2.1. The identity provider handles Authorization Code with PKCE, while the MCP server validates access tokens locally with the provider JWKS.
It checks:
- the JWT signature and algorithm;
- issuer and audience;
- expiration and activation dates;
- authenticated subject;
- required scopes.
The server also provides:
- project and namespace allowlists;
- Origin validation;
- rate limiting by authenticated subject;
- response sanitization;
- separate switches for read, write and destructive tools.
Static bearer authentication is still available for local development and migration, but OAuth is the recommended mode for a published application.
Stateless MCP transport
GitLab MCP Server implements Streamable HTTP:
POST /mcp
JSON-RPC messages use POST /mcp.
The server also accepts GET /mcp as a lightweight Server-Sent Events stream for MCP negotiation and keep-alive. No application session is stored on the server.
Protocol discovery remains public, so a client can call:
-
initialize; -
ping; -
tools/list.
Every tools/call request is authenticated before GitLab is contacted.
OAuth protected-resource metadata is published at:
/.well-known/oauth-protected-resource
/.well-known/oauth-protected-resource/mcp
This lets compatible clients discover the authorization server and the required scopes.
Installation
The project requires PHP 8.2 or newer and Symfony 7.4 LTS.
Clone the repository:
git clone https://git.small-project.dev/pub/apps/gitlab-mcp-symfony.git
cd gitlab-mcp-symfony
cp .env .env.local
For a local test, configure a static MCP token and your GitLab access token:
MCP_PUBLIC_URL=http://localhost:8080/mcp
MCP_AUTH_MODE=static
MCP_AUTH_TOKEN=replace-with-a-long-random-secret
GITLAB_URL=https://gitlab.example.com
GITLAB_TOKEN=glpat-xxxxxxxx
GITLAB_READ_ENABLED=true
GITLAB_WRITE_ENABLED=false
GITLAB_DESTRUCTIVE_ENABLED=false
Then start it with Docker:
docker compose up -d --build
The MCP endpoint is now available at:
http://127.0.0.1:8080/mcp
Keep write and destructive operations disabled until the read-only connection is working.
OAuth configuration
In production, configure the server as an OAuth resource server:
MCP_AUTH_MODE=oauth
MCP_OAUTH_RESOURCE=https://mcp.example.com/mcp
MCP_OAUTH_ISSUER=https://auth.example.com/realms/example
MCP_OAUTH_JWKS_URI=https://auth.example.com/realms/example/protocol/openid-connect/certs
MCP_OAUTH_READ_SCOPE=gitlab.read
MCP_OAUTH_WRITE_SCOPE=gitlab.write
MCP_OAUTH_DESTRUCTIVE_SCOPE=gitlab.destructive
Keycloak, Auth0, Okta, Cognito or another compatible OAuth/OIDC provider can issue the access token.
The OIDC openid scope is automatically advertised with the GitLab permissions. The resource value must exactly match the audience, or resource claim, included in the access token.
Deployment
The repository contains:
- a FrankenPHP production image;
- a Docker Compose configuration;
- unit and functional tests;
- PHPStan checks;
- a GitLab CI pipeline;
- Kubernetes deployment scripts.
Health endpoints are also available:
GET /healthz
GET /readyz
/healthz checks the application process. /readyz verifies the GitLab connection and, in OAuth mode, access to the JWKS endpoint.
Why Symfony?
Symfony already provides solid HTTP handling, dependency injection, configuration, logging and testing tools.
For an MCP server exposed on the Internet, these foundations are more useful than building a new framework around the protocol.
The application stays small, but the security and deployment parts remain explicit and testable.
Open source
GitLab MCP Server is released under the GNU GPL v3 license.
Source code and documentation:
https://git.small-project.dev/pub/apps/gitlab-mcp-symfony
The project is still evolving. Feedback, bug reports and contributions are welcome.
Which GitLab operation would you like to expose next?
Top comments (1)
The scope split and project/namespace allowlists are a strong baseline. For write tools, I would add one more boundary: bind every mutation to the repository state that was reviewed.
For example, create/update-files could require the expected project ID, target branch, base commit SHA, and an idempotency key. The server can produce a preflight plan (paths, operation types, total bytes, base SHA, policy result), hash it, and require the execution call to reference that digest. Immediately before committing, re-resolve the project and branch, re-check the allowlist and scopes, and fail on base-SHA drift.
That matters especially for the “up to 100 files atomically” tool: atomicity prevents a partial commit, but it does not limit blast radius or stop a stale agent plan from overwriting newer work. Protected-branch rules and merge-request approvals should remain authoritative rather than being recreated inside MCP.
A useful audit receipt would connect subject, token issuer/audience, tool/schema version, project ID, branch, base/new commit SHAs, changed paths, policy decision, and logical operation ID. Then retry/timeout tests can prove that one intended change creates at most one commit.