If you want your AI coding assistant to read GitLab issues, review merge requests, or check pipeline status, it needs API access.
The zereight/gitlab-mcp server provides exactly that over MCP.
The problem is that most setup examples put your personal access token directly into a JSON config file.
I wanted to avoid that.
Instead, this setup keeps the token in your shell environment and lets the MCP configuration reference it.
Same goal, but the secret does not have to be manually pasted into the configuration. It also makes rotating the token much easier.
This guide walks through the entire setup, including two problems that commonly cause this configuration to fail.
Prerequisites
You will need:
- Node.js 18.17 or newer
- A GitLab account on gitlab.com or a self-hosted GitLab instance
- An MCP client. I am using Claude Code here, but the general configuration approach is portable.
Step 1: Create a Personal Access Token
In GitLab, go to:
User Settings > Access Tokens
Create a new personal access token.
Choose the scope based on what you want the assistant to be able to do:
| Scope | Gets you |
|---|---|
read_api |
Read-only access. Enough to read issues, merge requests, and pipelines |
api |
Full read and write access. Needed to post comments or update issues |
Set an expiry date.
Make sure you copy the token when GitLab shows it. You will not be able to view the token again later.
Step 2: Work Out Your API URL
The MCP server needs the GitLab API endpoint, not your normal GitLab web URL.
The API URL ends with /api/v4:
https://gitlab.com/api/v4
https://gitlab.example.com/api/v4
For a self-hosted instance, if your GitLab URL is:
https://gitlab.example.com
then your API URL is:
https://gitlab.example.com/api/v4
This is an easy place to make a mistake.
Do not use the bare hostname and do not use the project URL. The server needs the API endpoint.
Step 3: Export the Variables
Add both variables to your shell profile.
For Bash:
~/.bashrc
For Zsh:
~/.zshrc
For example:
export GITLAB_PERSONAL_ACCESS_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
export GITLAB_API_URL=https://gitlab.example.com/api/v4
The export Is Important
This is probably the single most common way this setup fails.
Without export, you create a shell variable that exists only inside the shell where you defined it.
With export, you create an environment variable that is inherited by processes launched from that shell.
That distinction matters because MCP servers run as child processes.
So this:
FOO=bar
echo $FOO
works:
bar
But a child process will not see FOO unless it was exported:
FOO=bar
bash -c 'echo $FOO'
The result is empty.
You can verify the environment from a child process without printing the actual token:
bash -lic 'echo "token set: ${GITLAB_PERSONAL_ACCESS_TOKEN:+yes}"; echo "url: $GITLAB_API_URL"'
You want something like:
token set: yes
url: https://gitlab.example.com/api/v4
If the token line is blank, either:
- You forgot
export - You edited a profile file your shell does not actually read
- You have not started a new shell after changing the profile
Step 4: Confirm the Token Works Before Involving MCP
Before troubleshooting MCP, make sure the GitLab credentials themselves work.
This takes about ten seconds and can save a lot of confusion.
If this request fails, the problem is your token or API URL. No amount of MCP configuration will fix it.
Run:
curl -s -H "PRIVATE-TOKEN: $GITLAB_PERSONAL_ACCESS_TOKEN" \
"$GITLAB_API_URL/user"
You should get your GitLab user object back.
Then check that you can see the projects you expect:
curl -s -H "PRIVATE-TOKEN: $GITLAB_PERSONAL_ACCESS_TOKEN" \
"$GITLAB_API_URL/projects?membership=true&per_page=20"
Take note of the numeric id of a project you care about.
It turns out to be more reliable than using the project path in some situations, which we will cover in the troubleshooting section.
Step 5: Install the Server
There are several ways to install the server.
Using npm:
npm install -g @zereight/mcp-gitlab
Or using Homebrew:
brew tap zereight/gitlab-mcp https://github.com/zereight/gitlab-mcp
brew install zereight/gitlab-mcp/zereight-mcp-gitlab
Both approaches give you the zereight-mcp-gitlab binary.
You can also skip installing it globally and let npx fetch it when it launches. That trades a little startup time for always getting the current package.
Check what you have installed:
command -v zereight-mcp-gitlab
npm ls -g --depth=0 | grep gitlab
Step 6: Register the MCP Server
Now for the interesting part.
The goal is to keep the token out of the MCP configuration itself.
With Claude Code:
claude mcp add -s user gitlab \
-e 'GITLAB_PERSONAL_ACCESS_TOKEN=${GITLAB_PERSONAL_ACCESS_TOKEN}' \
-e 'GITLAB_API_URL=${GITLAB_API_URL}' \
-e 'GITLAB_PERMISSION_MODE=modify' \
-- zereight-mcp-gitlab
There are three things worth paying attention to here.
1. The Single Quotes
Notice the single quotes around the ${...} values:
-e 'GITLAB_PERSONAL_ACCESS_TOKEN=${GITLAB_PERSONAL_ACCESS_TOKEN}'
The quotes prevent your shell from expanding the variable when you run the command.
The idea is for the literal ${GITLAB_PERSONAL_ACCESS_TOKEN} reference to be stored in the configuration, with the client resolving it when the server launches.
If you use double quotes instead:
-e "GITLAB_PERSONAL_ACCESS_TOKEN=$GITLAB_PERSONAL_ACCESS_TOKEN"
your shell expands the variable immediately.
That means today's token gets written into the configuration, which defeats the purpose of keeping the secret out of the config file.
There is an important caveat here, which we will revisit in Troubleshooting: variable expansion behavior can depend on how the MCP server is configured and launched.
2. Use User Scope
I am explicitly using:
-s user
rather than the default local scope.
A local-scope server is associated with the directory where you registered it.
That can become annoying when you open another repository and discover that the server is no longer available.
You may then register it again, potentially ending up with multiple copies of the same server configuration.
User scope registers the server once so it is available across your directories.
3. Set the Permission Mode
The command also includes:
GITLAB_PERMISSION_MODE=modify
We will look at the available modes in the next section.
Equivalent JSON Configuration
If you prefer editing the MCP configuration directly, the equivalent entry looks like this:
{
"mcpServers": {
"gitlab": {
"type": "stdio",
"command": "zereight-mcp-gitlab",
"args": [],
"env": {
"GITLAB_PERSONAL_ACCESS_TOKEN": "${GITLAB_PERSONAL_ACCESS_TOKEN}",
"GITLAB_API_URL": "${GITLAB_API_URL}",
"GITLAB_PERMISSION_MODE": "modify"
}
}
}
}
The important part is that the token value is a variable reference rather than the actual token.
Step 7: Choose a Permission Mode
The server defaults to full, which exposes every destructive tool it has.
I prefer setting the permission mode explicitly.
| Mode | Read | Create and update | Delete |
|---|---|---|---|
readonly |
Yes | No | No |
modify |
Yes | Yes | Blocked |
full |
Yes | Yes | Yes |
For most workflows, modify is a good middle ground.
It lets the assistant read issues, post review comments, and update descriptions while preventing delete operations.
If the assistant only needs to look things up, use:
readonly
I would avoid full unless you genuinely need the destructive operations.
Step 8: Restart and Verify
MCP servers are read by the client at startup, and a running client keeps the environment it was launched with.
This matters if you edited your shell profile in Step 3.
Your current Claude Code session may still have the old environment.
Start a fresh terminal and launch Claude Code again.
Then run:
claude mcp list
claude mcp get gitlab
You want to see the GitLab server connected without any missing-variable warning.
Finally, exercise the server.
Ask your assistant to fetch a real GitLab issue and verify that it can access the repository.
If you are using modify mode, also confirm that the delete tools are not available.
Trimming the Toolset
The server exposes 261+ tools.
That is a lot of surface area.
Depending on your context budget and workflow, exposing hundreds of tools can consume context that could otherwise be used for the actual task.
You can narrow the toolset if needed.
For example, only enable specific groups:
GITLAB_TOOLSETS=issues,merge_requests,pipelines
Or explicitly allow individual tools:
GITLAB_TOOLS=get_issue,list_issues,create_note
You can also use a deny-list:
GITLAB_DENIED_TOOLS_REGEX='^delete_'
My recommendation is to start broad and narrow the toolset only if it becomes a problem.
There is no need to optimize this upfront.
Troubleshooting
Server connects, but every call returns 401
Your token is probably not reaching the MCP process.
The most common cause is the missing export from Step 3.
Check it from a child process:
bash -lic 'echo ${GITLAB_PERSONAL_ACCESS_TOKEN:+set}'
You should get:
set
If you do not, check your shell profile and make sure the variable is exported.
claude mcp list warns about a missing variable
This means the ${VAR} reference did not resolve as expected.
Variable expansion is documented for .mcp.json and project-scoped servers, but whether the same behavior applies to user-scoped entries is not stated outright.
So this is something worth verifying rather than assuming.
If expansion fails, the configuration can still load while the literal string:
${GITLAB_PERSONAL_ACCESS_TOKEN}
gets passed to the server as the token.
The server can therefore appear healthy while every authenticated request fails with a 401.
If you run into this, you can interpolate the environment variable when adding the server instead:
claude mcp remove -s user gitlab
claude mcp add -s user gitlab \
-e "GITLAB_PERSONAL_ACCESS_TOKEN=$GITLAB_PERSONAL_ACCESS_TOKEN" \
-e "GITLAB_API_URL=$GITLAB_API_URL" \
-e 'GITLAB_PERMISSION_MODE=modify' \
-- zereight-mcp-gitlab
The tradeoff is that the actual secret is now written into the configuration.
You still keep a single user-scoped server entry, but you lose the benefit of keeping the secret out of the file.
404 Project Not Found for a project you can definitely see
GitLab's API accepts a URL-encoded project path such as:
group%2Fproject
However, some HTTP clients normalize %2F back into / before sending the request.
That can turn:
/projects/group%2Fproject/...
into:
/projects/group/project/...
which GitLab does not recognize as the intended project path.
curl --path-as-is can force the encoded form through, but the more durable solution is to use the numeric project ID from Step 4.
For example:
123456
Using the numeric ID avoids the path-encoding problem entirely.
The server is missing in one repo but present in another
You probably registered it with local scope.
Re-register it at user scope:
claude mcp add -s user gitlab ...
Then clean up the old local entries from the directories where they were created:
(cd ~/code/some-project && claude mcp remove -s local gitlab)
Nothing changed after editing my shell profile
Start a new terminal and restart Claude Code.
The running client will not magically inherit environment changes made after it started.
Wrapping Up
The whole setup boils down to four things:
- A GitLab personal access token with the right scope
- An API URL ending in
/api/v4 - Both values exported into your environment
- A single user-scoped MCP server entry
The most important detail is the environment variable.
If it is not exported, the MCP process cannot see it.
If the variable reference is not expanded by the client, the server may appear connected while authentication fails.
That is why I recommend verifying the environment from a child process and testing the GitLab API directly before involving MCP.
Once those pieces are correct, the rest tends to fall into place.
Top comments (0)