@simplysf/simply-gitlab-mcp 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -100,6 +100,13 @@ they reach you, but the text itself is data, never instructions.
100
100
 
101
101
  **Your token never appears in output.** It is redacted from every error message and response body.
102
102
 
103
+ ## Guides
104
+
105
+ Setup, client configuration, and the tool catalogue:
106
+ **https://simplysf.github.io/simply-cli/gitlab/guides/mcp-server/**
107
+
108
+ That guide also ships inside this package, at [`docs/guides/mcp-server.md`](docs/guides/mcp-server.md).
109
+
103
110
  ## Documentation
104
111
 
105
112
  - [Migration and architecture](../../docs/design/0002-migration-from-gitlab-mcp.md)
@@ -0,0 +1,425 @@
1
+ ---
2
+ title: MCP server
3
+ description: Give an AI agent in Claude Desktop, Claude Code, Cursor, Gemini CLI, or VS Code the same GitLab capabilities as the CLI, as Model Context Protocol tools, read-only by default.
4
+ ---
5
+
6
+ `@simplysf/simply-gitlab-mcp` is a [Model Context Protocol](https://modelcontextprotocol.io/)
7
+ server with one tool per CLI command. It calls the same library the CLI is built on, in-process, so
8
+ the agent gets the same credential handling, `dryRun` previews, read-only guard, and error messages
9
+ described elsewhere in these guides, and the same raw JSON the CLI prints with `--json`.
10
+
11
+ Using it takes four steps: install the server, tell your MCP client how to launch it, check the
12
+ connection, then ask the agent for what you want in plain language. This page walks through each,
13
+ then covers letting the agent write, the full tool list, results and errors, and what to check when
14
+ something does not work.
15
+
16
+ ## Install
17
+
18
+ The server needs Node.js 22 or later.
19
+
20
+ ```sh
21
+ npm install -g @simplysf/simply-gitlab-mcp
22
+ simply-gitlab-mcp --help
23
+ ```
24
+
25
+ `--help` prints the options and every tool the server can register, so it doubles as a check that
26
+ the install worked. It prints to stderr, because stdout is reserved for the protocol stream.
27
+
28
+ You can skip the global install and have the client fetch the package on demand with
29
+ `npx -y @simplysf/simply-gitlab-mcp` as the command; the client configurations below show both
30
+ forms.
31
+
32
+ ## Connection settings
33
+
34
+ The server reads the same variables as the CLI, described in [Credentials](/guides/credentials/):
35
+
36
+ ```
37
+ GITLAB_TOKEN=glpat-...
38
+ GITLAB_URL=https://gitlab.example.com # omit entirely for gitlab.com
39
+ ```
40
+
41
+ There are only two, and `GITLAB_URL` defaults to `https://gitlab.com` — a gitlab.com user needs the
42
+ token and nothing else.
43
+
44
+ Put them in a `.env` file and pass it with `--env-file`, or set them in the `env` block of the
45
+ client's server entry. A variable already in the environment wins over the file, as it does for the
46
+ CLI. Settings are resolved when a tool is called rather than at startup, so a missing token is
47
+ reported by the tool that needed it instead of killing a server the client has already launched.
48
+
49
+ Give the token `read_api` scope unless you intend to enable writes. That is the only layer that
50
+ actually binds — see [Write safety](/guides/write-safety/).
51
+
52
+ Use an absolute path for the env file. MCP clients launch the server from a working directory of
53
+ their own, so `~` and relative paths may not resolve. On Windows, write the path with forward
54
+ slashes or doubled backslashes inside JSON: `"C:/Users/me/gitlab.env"`.
55
+
56
+ ## Configure a client
57
+
58
+ ### Claude Desktop
59
+
60
+ Edit `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/`; Windows:
61
+ `%APPDATA%\Claude\`) and restart Claude Desktop:
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "simply-gitlab": {
67
+ "command": "simply",
68
+ "args": ["gitlab", "mcp", "--env-file", "/home/me/gitlab.env"]
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ The same entry with the settings inline and no global install:
75
+
76
+ ```json
77
+ {
78
+ "mcpServers": {
79
+ "simply-gitlab": {
80
+ "command": "npx",
81
+ "args": ["gitlab", "mcp"],
82
+ "env": {
83
+ "GITLAB_URL": "https://gitlab.example.com",
84
+ "GITLAB_TOKEN": "glpat-..."
85
+ }
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ ### Claude Code
92
+
93
+ ```sh
94
+ claude mcp add simply-gitlab -- simply-gitlab-mcp --env-file ~/gitlab.env
95
+ ```
96
+
97
+ Add `--scope project` to write the entry to a `.mcp.json` at the repository root instead, which can
98
+ be committed so a team shares the server definition. Keep tokens out of a committed file by
99
+ pointing at an env file each person holds locally. Inside a session, `/mcp` lists the configured
100
+ servers and whether each one connected.
101
+
102
+ ### Cursor
103
+
104
+ Cursor reads `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` for every project, with the
105
+ same `mcpServers` shape as Claude Desktop.
106
+
107
+ ### Gemini CLI
108
+
109
+ Gemini CLI reads `~/.gemini/settings.json` for every project, or `.gemini/settings.json` inside a
110
+ project, with the same `mcpServers` shape as Claude Desktop:
111
+
112
+ ```json
113
+ {
114
+ "mcpServers": {
115
+ "simply-gitlab": {
116
+ "command": "simply",
117
+ "args": ["gitlab", "mcp", "--env-file", "/home/me/gitlab.env"]
118
+ }
119
+ }
120
+ }
121
+ ```
122
+
123
+ `gemini mcp add` writes the same entry. `--scope user` puts it in the global file; the default
124
+ `project` scope writes `.gemini/settings.json`, and `-e` sets one variable on the entry:
125
+
126
+ ```sh
127
+ gemini mcp add --scope user \
128
+ -e GITLAB_URL=https://gitlab.example.com \
129
+ -e GITLAB_TOKEN=glpat-... \
130
+ simply-gitlab simply-gitlab-mcp
131
+ ```
132
+
133
+ Flags meant for the server itself, `--env-file` and `--allow-writes`, are easier to add by editing
134
+ `settings.json`, because `gemini mcp add` reads a leading-dash argument as one of its own options.
135
+
136
+ Values in `env` expand `$NAME` from the environment Gemini started in, which keeps the token out of
137
+ the settings file without an env file:
138
+
139
+ ```json
140
+ {
141
+ "mcpServers": {
142
+ "simply-gitlab": {
143
+ "command": "npx",
144
+ "args": ["gitlab", "mcp"],
145
+ "env": {
146
+ "GITLAB_URL": "https://gitlab.example.com",
147
+ "GITLAB_TOKEN": "$GITLAB_TOKEN"
148
+ }
149
+ }
150
+ }
151
+ }
152
+ ```
153
+
154
+ Two other fields on an entry matter here. `trust: true` bypasses the per-call confirmation for every
155
+ tool on that server, so leave it off on any entry started with `--allow-writes` and let a person
156
+ approve each write. `includeTools` and `excludeTools` narrow what the model is shown, which is a
157
+ second way to express the split in [Let the agent write](#let-the-agent-write): a write entry can
158
+ register the merge-request tools while holding back the ones that push commits.
159
+
160
+ ```json
161
+ {
162
+ "mcpServers": {
163
+ "simply-gitlab-write": {
164
+ "command": "simply",
165
+ "args": ["gitlab", "mcp", "--allow-writes", "--env-file", "/home/me/gitlab-write.env"],
166
+ "excludeTools": ["gitlab_commit_create", "gitlab_file_create", "gitlab_file_update"],
167
+ "trust": false
168
+ }
169
+ }
170
+ }
171
+ ```
172
+
173
+ Inside a session, `/mcp` lists the configured servers, whether each connected, and the tools it
174
+ registered.
175
+
176
+ ### Gemini Code Assist
177
+
178
+ Agent mode in the VS Code extension reads that same `~/.gemini/settings.json`, so an entry added for
179
+ Gemini CLI is already in place. In IntelliJ the file is `mcp.json` in the IDE's configuration
180
+ directory, holding the same `mcpServers` object.
181
+
182
+ ### VS Code
183
+
184
+ VS Code reads `.vscode/mcp.json`, with a `servers` key and an explicit transport type:
185
+
186
+ ```json
187
+ {
188
+ "servers": {
189
+ "simply-gitlab": {
190
+ "type": "stdio",
191
+ "command": "simply",
192
+ "args": ["gitlab", "mcp", "--env-file", "/home/me/gitlab.env"]
193
+ }
194
+ }
195
+ }
196
+ ```
197
+
198
+ ### Any other client
199
+
200
+ The server speaks MCP over stdio, so any client that can launch a local command works: give it the
201
+ command, its arguments, and optionally an environment, as above. It announces itself as
202
+ `simply-gitlab` with the package version during the handshake and supplies instructions that
203
+ summarise the tool conventions for the host's model.
204
+
205
+ ## Check the connection
206
+
207
+ Ask the agent to list the projects you are a member of. It should call `gitlab_project_list` with
208
+ `membership: true` and come back with paths and ids. If it does, the server is running and the
209
+ token works; the [troubleshooting](#troubleshooting) table covers the cases where it does not.
210
+
211
+ There is no `whoami` tool, because GitLab has no cheap identity endpoint worth a tool of its own —
212
+ a project listing answers the same question and gives the agent the paths it needs next.
213
+
214
+ ## Ask for what you want
215
+
216
+ You do not call tools yourself. Describe the outcome, and the agent chooses the tools and fills in
217
+ their inputs from the descriptions the server publishes. Some examples of what a request turns
218
+ into:
219
+
220
+ | You ask | The agent calls |
221
+ | ----------------------------------------------- | ------------------------------------------------------------------------------------ |
222
+ | "Why did CI fail on group/project?" | `gitlab_ci_job_list` with `scope: ["failed"]`, then `gitlab_ci_job_log` on the first |
223
+ | "What merge requests am I reviewing?" | `gitlab_mr_list` with `reviewerUsername` |
224
+ | "Summarise MR 42 and what changed in it." | `gitlab_mr_view`, then `gitlab_commit_list` on the source branch |
225
+ | "Where is `UMCN_Account` defined?" | `gitlab_search` with `scope: "blobs"` and a `project` |
226
+ | "When did `src/index.ts` last change, and who?" | `gitlab_commit_list` with `path: "src/index.ts"` |
227
+ | "Show me the README on the `release` branch." | `gitlab_file_view` with `ref: "release"` |
228
+ | "What's deployed to production?" | `gitlab_ci_environment_list` |
229
+ | "Which CI variables are set for production?" | `gitlab_ci_variable_list` — keys and scopes only, never values |
230
+
231
+ Each tool's inputs are the matching command's flags in camel case, with arrays where the CLI takes
232
+ a repeatable or comma-separated flag; see the [Command Reference](/reference/) for what each does.
233
+ The second request above, for instance, becomes a call like:
234
+
235
+ ```json
236
+ { "project": "group/project", "state": "opened", "reviewerUsername": "me", "limit": 20 }
237
+ ```
238
+
239
+ and returns the same envelope the CLI prints under `--json`:
240
+
241
+ ```json
242
+ { "items": [...], "total": 3, "pages": 1, "complete": true }
243
+ ```
244
+
245
+ A few things make the agent's job easier:
246
+
247
+ - **Name the project by path.** `group/subgroup/project` works everywhere a numeric id does, and
248
+ the agent does not have to look the id up first. It is not URL-encoded by the caller.
249
+ - **Merge requests go by `iid`.** That is the number in the web URL, and it is what the tools take
250
+ — not the `id` that also appears in the payload. "MR 42" is unambiguous; the tools validate the
251
+ shape and refuse anything that is not a positive whole number.
252
+ - **`complete: false` means there is more.** Every list returns `{ items, total?, pages, complete }`,
253
+ and a `false` there means the limit cut the results short. Ask for a higher `limit` rather than
254
+ assuming the list is the whole answer.
255
+ - **Job logs come back tailed.** `gitlab_ci_job_log` returns the last 200 lines by default, because
256
+ a trace is regularly tens of megabytes. `tail: 0` returns all of it, which is rarely what you
257
+ want in a context window.
258
+ - **Blob search needs Advanced Search.** The `blobs`, `commits`, `wiki_blobs`, `notes`, and
259
+ `snippet_titles` scopes need Elasticsearch enabled on the instance. Where it is not, GitLab
260
+ answers with an empty list and a `200` rather than an error — so an empty blob search may mean
261
+ "not enabled" rather than "no matches". The tool description says so, so the agent can tell you
262
+ that instead of concluding the code does not exist.
263
+
264
+ ## Let the agent write
265
+
266
+ Started without options, the server registers only the 16 read tools. Start it with
267
+ `--allow-writes` to also register the six that create a branch, write a file, push a commit, or
268
+ open and update a merge request. Even then, `GITLAB_READ_ONLY` in the environment refuses every
269
+ write, exactly as it does for the CLI.
270
+
271
+ That mirrors the two-credential-file arrangement in [Write safety](/guides/write-safety/): give the
272
+ agent's everyday server a `read_api` token, and configure a second server entry with
273
+ `--allow-writes` and an `api` token that is enabled only when a person means to let the agent
274
+ write. In an `mcpServers` configuration the pair looks like this:
275
+
276
+ ```json
277
+ {
278
+ "mcpServers": {
279
+ "simply-gitlab": {
280
+ "command": "simply",
281
+ "args": ["gitlab", "mcp", "--env-file", "/home/me/gitlab.env"]
282
+ },
283
+ "simply-gitlab-write": {
284
+ "command": "simply",
285
+ "args": ["gitlab", "mcp", "--allow-writes", "--env-file", "/home/me/gitlab-write.env"]
286
+ }
287
+ }
288
+ }
289
+ ```
290
+
291
+ A `read_api` token is still the only layer that binds; the server-side default just keeps write
292
+ tools out of the agent's normal loop entirely. Most clients also ask you to approve each tool call,
293
+ and the tools carry MCP annotations (`readOnlyHint`, `destructiveHint`) so a host can tell a read
294
+ from a write when it asks.
295
+
296
+ With writes allowed:
297
+
298
+ - **Ask for a preview first.** All six write tools accept `dryRun: true`, which returns the request
299
+ that would be sent without sending it. "Show me what you'd send to open a merge request from
300
+ `feature/thing` into `main`, then do it" produces a call like the one below, which you can check
301
+ before the agent repeats it without `dryRun`.
302
+
303
+ ```json
304
+ {
305
+ "project": "group/project",
306
+ "sourceBranch": "feature/thing",
307
+ "targetBranch": "main",
308
+ "title": "feat: the thing",
309
+ "dryRun": true
310
+ }
311
+ ```
312
+
313
+ - **Nothing here deletes.** There is no delete tool in the catalogue, and so no `confirm` gate: every
314
+ write either creates something or edits a merge request, and both are recoverable. That is why
315
+ `dryRun` carries the weight it does.
316
+ - **A file update replaces the whole file.** There is no partial edit, so an agent must read the
317
+ file before writing it. Passing `lastCommitId` from `gitlab_file_view` makes GitLab reject the
318
+ write if anything changed meanwhile, instead of silently overwriting someone.
319
+ - **A batch commit is atomic.** `gitlab_commit_create` takes an array of actions — create, update,
320
+ delete, move, chmod — and writes them as one commit. Each action is validated before anything is
321
+ sent, so a malformed entry comes back naming its own index rather than as an opaque `400`.
322
+
323
+ ## Tools
324
+
325
+ | Read tools (always) | Write tools (`--allow-writes`) |
326
+ | ---------------------------- | ------------------------------ |
327
+ | `gitlab_project_list` | `gitlab_file_create` |
328
+ | `gitlab_project_view` | `gitlab_file_update` |
329
+ | `gitlab_file_view` | `gitlab_branch_create` |
330
+ | `gitlab_branch_list` | `gitlab_commit_create` |
331
+ | `gitlab_commit_list` | `gitlab_mr_create` |
332
+ | `gitlab_commit_view` | `gitlab_mr_update` |
333
+ | `gitlab_commit_diff` | |
334
+ | `gitlab_mr_list` | |
335
+ | `gitlab_mr_view` | |
336
+ | `gitlab_tag_list` | |
337
+ | `gitlab_release_list` | |
338
+ | `gitlab_ci_job_list` | |
339
+ | `gitlab_ci_job_log` | |
340
+ | `gitlab_ci_variable_list` | |
341
+ | `gitlab_ci_environment_list` | |
342
+ | `gitlab_search` | |
343
+
344
+ ### CI variable values are never returned
345
+
346
+ `gitlab_ci_variable_list` reports each variable's key, environment scope, and whether it is
347
+ protected or masked. Every **value** comes back as `<hidden>`, and there is no input to reveal them.
348
+
349
+ GitLab's own API does not work this way: `GET /projects/:id/variables` returns every value in clear
350
+ text, including the ones marked "masked", because masking only hides a value in job logs. Those
351
+ values are deploy keys and production credentials, and a value revealed to an agent lands in a
352
+ context window, a transcript, and whatever that agent writes next.
353
+
354
+ This is the one place the MCP server deliberately offers less than the CLI. A person who needs a
355
+ value runs `simply gitlab ci variable list --reveal`.
356
+
357
+ ## Results and errors
358
+
359
+ A successful call returns what the CLI prints with `--json`, verbatim. A failure returns an error
360
+ result whose text is a JSON object with a stable `code`:
361
+
362
+ | `code` | Meaning |
363
+ | -------- | ------------------------------------------------------------------------------------ |
364
+ | `config` | Missing or contradictory settings, a refused write, bad input. |
365
+ | `auth` | The instance rejected the credentials; carries the HTTP `status`. |
366
+ | `error` | Any other failure, including an API error with its HTTP `status` and sanitised body. |
367
+
368
+ These map one-to-one onto the CLI's exit codes 2, 3, and 1, described in
369
+ [Scripts and agents](/guides/scripting/), and carry the same `name` and `message`, scrubbed of
370
+ credentials and control characters the same way. The agent sees these errors and can act on them,
371
+ so a missing token or a wrong project path usually ends in the agent telling you what went wrong
372
+ rather than in silence.
373
+
374
+ Text the instance produced — merge request descriptions, commit messages, file contents, job traces
375
+ — is written by anyone who can push or open a merge request. Control characters are stripped before
376
+ it reaches the agent, and the tool descriptions say plainly that the content is data rather than
377
+ instructions.
378
+
379
+ ## Troubleshooting
380
+
381
+ Run `simply-gitlab-mcp --help` at a terminal first. If it prints the tool list, the package is
382
+ installed and working, and the problem is in how the client launches it or what it passes.
383
+
384
+ | Symptom | Cause and fix |
385
+ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
386
+ | The client reports the server failed to start | Desktop apps do not load your shell profile, so the command may not be on their `PATH`. Use the full path to the executable (it lives in the directory `npm prefix -g` reports, under `bin/` on macOS and Linux), or the `npx` form above. |
387
+ | The client's log says `Env file ... not found.` | The only startup failure. The path is wrong or relative; make it absolute. The server exits with code 2. |
388
+ | Every tool returns `code: "config"` naming `GITLAB_TOKEN` | The settings are not reaching the server: the env file is not being read, or the variable was set in a shell the client did not inherit. Pass `--env-file` or an `env` block explicitly. |
389
+ | `code: "auth"` with status `401` | The token is wrong, expired, or revoked. Create a new one under **Preferences › Access tokens**. |
390
+ | `code: "auth"` with status `403` on a write | The token is `read_api`-scoped, or your role on the project does not permit the change. The first is the intended outcome for the everyday server; use the write-capable entry for this change. |
391
+ | `code: "error"` with status `404` on a project you can see | The project reference is wrong. Use the numeric id, or the full path including every parent group — and do not URL-encode it yourself. |
392
+ | A merge request tool acts on the wrong merge request | An `id` was passed where an `iid` belongs. The `iid` is the number in the web URL; both are integers, so nothing rejects the wrong one. |
393
+ | The agent says it has no tool to create or update anything | The server was started without `--allow-writes`. That is the default; see [Let the agent write](#let-the-agent-write). |
394
+ | A write returns `code: "config"` mentioning `GITLAB_READ_ONLY` | The variable is set in the environment or the env file. Unset it, or point the write server at a credential file meant for writing. |
395
+ | A blob search returns nothing for code you know exists | Advanced Search is probably not enabled on the instance; GitLab answers those scopes with an empty list and a `200`. Try `scope: "merge_requests"` or the project's file tree instead. |
396
+
397
+ ## Embed in your own process
398
+
399
+ The stdio binary is the usual way to run the server, but the package also exports what it is built
400
+ from, so a host process can serve the same tools over a transport of its choosing:
401
+
402
+ ```js
403
+ import { createServer, startServer } from '@simplysf/simply-gitlab-mcp';
404
+
405
+ // Serve over stdio, exactly as the binary does.
406
+ await startServer({ allowWrites: false, envFile: '/home/me/gitlab.env' });
407
+
408
+ // Or build the server and attach a transport yourself.
409
+ const server = createServer({ allowWrites: true, env: process.env });
410
+ await server.connect(transport);
411
+ ```
412
+
413
+ `createServer` returns the MCP SDK's `McpServer` with the tools registered and nothing connected.
414
+ `TOOLS` is the full catalogue, `selectTools(allowWrites)` is the subset a given configuration
415
+ registers, and `invokeTool` and `mapError` run one tool and map a failure onto the error object
416
+ above, for tests and hosts that need to drive the server without a transport.
417
+
418
+ ## Options
419
+
420
+ ```
421
+ simply-gitlab-mcp [--allow-writes] [--env-file <path>]
422
+ ```
423
+
424
+ `--help` prints the options and the full tool list. Because stdout is the protocol stream, it prints
425
+ to stderr.
@@ -61,5 +61,5 @@
61
61
  ]
62
62
  }
63
63
  },
64
- "version": "0.4.0"
64
+ "version": "0.5.0"
65
65
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@simplysf/simply-gitlab-mcp",
3
3
  "description": "Model Context Protocol server exposing GitLab to AI agents, built on @simplysf/simply-gitlab-core",
4
- "version": "0.4.0",
4
+ "version": "0.5.0",
5
5
  "author": "@ClayChipps",
6
6
  "homepage": "https://github.com/SimplySF/simply-gitlab",
7
7
  "bugs": "https://github.com/SimplySF/simply-gitlab/issues",
@@ -14,6 +14,7 @@
14
14
  "node": ">=22.0.0"
15
15
  },
16
16
  "files": [
17
+ "/docs",
17
18
  "/lib",
18
19
  "/oclif.manifest.json"
19
20
  ],
@@ -35,7 +36,7 @@
35
36
  "dependencies": {
36
37
  "@modelcontextprotocol/sdk": "^1.30.0",
37
38
  "@oclif/core": "^4.11.2",
38
- "@simplysf/simply-gitlab-core": "^0.3.0",
39
+ "@simplysf/simply-gitlab-core": "^0.4.0",
39
40
  "zod": "^4.5.4"
40
41
  },
41
42
  "devDependencies": {
@@ -183,5 +184,5 @@
183
184
  }
184
185
  }
185
186
  },
186
- "gitHead": "e6370885cb5c0abab7407699132425fd0ea33202"
187
+ "gitHead": "1695d8c8c195df25f1bda573e1f3367ad1d31ded"
187
188
  }