@jswk1/gitlab-mcp 1.0.0 → 1.1.1

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
@@ -1,230 +1,213 @@
1
- # GitLab read-only MCP server
2
-
3
- A TypeScript/Node.js MCP server for self-managed GitLab. The AI is already working in a local source repository: this server supplies GitLab-specific context such as complete issues, screenshots and attachments, MR changes and discussion threads. The AI host reads and edits local code and Markdown documentation using its own repository tools. GitLab access remains read-only. The server does not call an LLM, mount the working repository, or edit local files itself.
4
-
1
+ # GitLab read-only MCP server
2
+
3
+ Give your AI assistant GitLab issues, merge requests, discussions and attachments from a self-managed instance. The server reads GitLab; your assistant uses its own tools to inspect and edit your local repository.
4
+
5
5
  ## Setup
6
-
7
- Requires Node.js 20.20+ and GitLab 17.4+ for upload downloads by secret/filename. Other read endpoints may work on older versions but are not tested there. API compatibility reference: [GitLab Markdown uploads API](https://docs.gitlab.com/api/project_markdown_uploads/).
8
-
9
- ```sh
10
- npm ci
11
- npm run build
12
- ```
13
-
14
- Create a personal access token for each developer with **read_api** scope. GitLab enforces the token owner's project and issue permissions, including confidential issue visibility. Do not use an administrator, impersonation, or shared service token for developer sessions. See [GitLab REST authentication](https://docs.gitlab.com/api/rest/authentication/).
15
6
 
16
- ### Use the published npm package
7
+ - **GitLab 17.4+** for attachment downloads. Older versions are untested.
8
+ - **A personal access token with `read_api` scope**, belonging to the developer using the assistant. The token owner's GitLab permissions determine access, including confidential issues.
9
+ - **Node.js 20.20+** for npm/local setup, or **Docker** for container setup.
10
+
11
+ Choose one option below and add its configuration to your MCP client's settings. All options use **stdio**: the client starts the server and communicates over stdin/stdout. Run one server process per developer/client; there is no HTTP endpoint.
17
12
 
18
- After the package is published, configure your MCP host to run it with `npx` (pin a version instead of `latest` if you want reproducible installs):
13
+ ### Option 1: npm package
14
+
15
+ Configure your MCP client to run the npm package:
19
16
 
20
17
  ```json
21
18
  {
22
19
  "mcpServers": {
23
20
  "gitlab": {
24
21
  "command": "npx",
25
- "args": ["--yes", "--package=@jswk1/gitlab-mcp@1.0.0", "gitlab-mcp"],
22
+ "args": ["--yes", "--package=@jswk1/gitlab-mcp@1.1.0", "gitlab-mcp"],
23
+ "env": {
24
+ "GITLAB_URL": "https://gitlab.example.internal",
25
+ "GITLAB_TOKEN": "YOUR_PERSONAL_READ_API_TOKEN"
26
+ }
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ The first run downloads the package. Keep a pinned version for reproducible installs. If the package is unavailable, build locally or use Docker.
33
+
34
+ ### Option 2: local build over stdio
35
+
36
+ From the repository root:
37
+
38
+ ```sh
39
+ npm ci
40
+ npm run build
41
+ ```
42
+
43
+ Point your MCP client at the compiled entry point using an absolute path:
44
+
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "gitlab": {
49
+ "command": "node",
50
+ "args": ["/absolute/path/to/gitlab-mcp/dist/index.js"],
26
51
  "env": {
27
52
  "GITLAB_URL": "https://gitlab.example.internal",
28
- "GITLAB_TOKEN": "YOUR_PERSONAL_READ_API_TOKEN",
29
- "GITLAB_USER_ID": "123"
53
+ "GITLAB_TOKEN": "YOUR_PERSONAL_READ_API_TOKEN"
30
54
  }
31
55
  }
32
56
  }
33
57
  }
34
58
  ```
35
59
 
36
- The first run downloads the package; Node.js 20.20+ must be installed on the host. Keep the token in the host's private configuration, not in a committed file.
60
+ To launch manually, copy `.env.example` to `.env`, fill in your credentials, then run `node --env-file=.env dist/index.js`. The process waits for MCP messages; it is not an interactive terminal application.
37
61
 
38
- ### Publish to npm
62
+ ### Option 3: Docker
39
63
 
40
- Review the files npm will publish and release:
64
+ From the repository root:
41
65
 
42
66
  ```sh
43
- npm login
44
- npm pack --dry-run
45
- npm publish
67
+ npm run docker:build
68
+ cp .env.example .env
69
+ ```
70
+
71
+ Fill in `GITLAB_URL` and `GITLAB_TOKEN` in `.env`, then configure your MCP client:
72
+
73
+ ```json
74
+ {
75
+ "mcpServers": {
76
+ "gitlab": {
77
+ "command": "docker",
78
+ "args": [
79
+ "run", "--rm", "-i",
80
+ "--env-file", "/absolute/path/to/gitlab-mcp/.env",
81
+ "gitlab-readonly-mcp"
82
+ ]
83
+ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ Keep `-i`; do not add `-t` or `-d`. No ports or repository mounts are needed. The image runs as a non-root user and receives credentials at runtime.
89
+
90
+ GitLab must be reachable from inside the container. On Docker Desktop, use `host.docker.internal` to reach GitLab on your host. On Linux Docker Engine, also add `--add-host=host.docker.internal:host-gateway` to the Docker arguments when using that hostname.
91
+
92
+ After source updates, rebuild the image and restart your client's MCP connection to create a new container.
93
+
94
+ ## Workflows
95
+
96
+ Ask your assistant to work on an issue or MR in your local repository. It can start with `gitlab_get_workflow`, passing `project`, `iid`, `target` (`issue` or `merge_request`), optional `profile`, optional `goal` to specify scope, and optional `mode` for developer/documentalist issue resolution.
97
+
98
+ | Profile | Issue | Merge request |
99
+ | --- | --- | --- |
100
+ | `developer` (default) | Investigate and guide the user through a fix/feature, or implement automatically when selected | Review changed code for bugs, regressions and improvements |
101
+ | `tester` | Inspect the latest related implementation and prepare prioritized manual test cases | Prepare a manual test plan for changed behavior |
102
+ | `documentalist` | Guide documentation implementation, or edit automatically when selected, using merged/current implementation | Update local Markdown docs to match the implementation |
103
+ | `business_analyst` (business analyst/designer) | Report on client requirements and issue status; reconcile external sheets | Summarize business/design impact for client reporting |
104
+
105
+ Profiles apply per task and share the same tools and permissions. The workflow tool returns guidance; the assistant gathers evidence and performs local work. Tester workflows produce plans, not test execution. Implementation and documentation edits require a client with local repository access and editing tools.
106
+
107
+ Developer and documentalist issue resolution offers **guided mode (recommended)** and **fully automated mode**. If the user asks to resolve, fix, implement or update an issue without choosing a mode, the assistant asks which they prefer and waits before making changes. A plain implementation request does not select automation. Explicit requests such as “automatically implement this issue” skip the question, as do explicit guided requests or an existing mode choice for the same task. Analysis-only, plan-only and draft-only requests keep their scope without a mode question.
108
+
109
+ In guided mode, the user edits the code, tests or documentation. The assistant investigates, outlines the approach, then gives one manageable step at a time with relevant files, what to change, why, pitfalls and checks. It waits for the user's changes/results, reviews them and adapts the next step. In automated mode, the assistant performs the implementation and checks. Neither mode authorizes commits, pushes or GitLab writes.
110
+
111
+ Clients can pass `mode: "guided"` or `mode: "automated"` to `gitlab_get_workflow`, `implement_issue`, `investigate_issue` or `update_documentation`. Omitting it (or passing `"ask"`) delegates the mode question and interpretation of explicit user intent to the host assistant. Workflow calls do not persist choices; the host carries the user's selection forward for the same task. Mode selection applies to developer/documentalist issue implementation; MR reviews, tester and reporting workflows retain their existing behavior.
112
+
113
+ For aggregate reporting, select `profile: "business_analyst"`, `target: "report"`, `project` and a `goal`; omit `iid`. This mode does not require a source checkout. The assistant uses host file tools to generate actual CSV files with the requested columns (or returns CSV text if file tools are unavailable). External sheets require a user-provided file or an authorized host connector; this server does not connect to or update sheets itself.
114
+
115
+ Client attribution must use an explicit label or supplied mapping. Reports follow all issue pages and filter `closed_at` locally with inclusive start and exclusive end dates in the agreed timezone. Current closed issues do not describe historical closure events for reopened issues. Milestone membership and closure dates are separate filters. Reports include source links, exact filters, retrieval time, totals and coverage limits. Client-facing exports omit confidential/internal content unless authorized, and CSV text cells are protected against spreadsheet formulas. Reconciliation preserves stable issue and external row identifiers and reports duplicates, missing rows and conflicts.
116
+
117
+ Example requests:
118
+
119
+ - “Guide me through fixing issue #123. I want to make the changes myself.”
120
+ - “Automatically implement issue #123, including the relevant tests.”
121
+ - “Read issue #123 in `group/project`, including comments and attachments, and fix the validation bug in this repository.”
122
+ - “Review MR !456 in `group/project`. Return findings limited to changed code, ordered by severity.”
123
+ - “Use the tester profile for issue #123 in `group/project` and prepare the critical manual test cases for the latest related branch.”
124
+ - “Use the documentalist profile for MR !456 in `group/project` and update the affected Markdown docs.”
125
+ - “Use the business analyst/designer profile: export issues closed for client X in Q3 2026 from `group/project`. Client X uses label `client::X`; use Europe/Warsaw time and include IID, title, milestone, closed date and URL.”
126
+ - “Generate a CSV of closed client X issues in milestone `Release 2.0`, then reconcile it with this client sheet and report missing or conflicting rows.”
127
+
128
+ Clients supporting MCP prompts can use these shortcuts:
129
+
130
+ | Prompt | Purpose | Arguments |
131
+ | --- | --- | --- |
132
+ | `report_issues` | Generate reports/CSVs or reconcile sheets as business analyst/designer | `project`, `goal` |
133
+ | `implement_issue` | Resolve an issue as developer in guided or automated mode | `project`, `iid`, optional `goal`, `mode` |
134
+ | `investigate_issue` | Investigate or act on an issue | `project`, `iid`, optional `profile`, `goal`, `mode` |
135
+ | `review_merge_request` | Review an MR using the selected profile | `project`, `iid`, optional `profile`, `goal` |
136
+ | `prepare_test_plan` | Plan manual tests as tester | `project`, `iid`, `target`, optional `goal` |
137
+ | `update_documentation` | Resolve documentation as documentalist; issues offer guided/automated modes | `project`, `iid`, `target`, optional `goal`, `mode` |
138
+ | `draft_documentation` | Return documentation drafts without editing files | `project`, `iid`, `target` |
139
+
140
+ ## Available tools
141
+
142
+ | Tool | Purpose |
143
+ | --- | --- |
144
+ | `gitlab_get_workflow` | Get task guidance for a profile and issue/MR or aggregate report |
145
+ | `gitlab_current_user` | Identify the token owner |
146
+ | `gitlab_list_projects` | Find accessible projects |
147
+ | `gitlab_get_project` | Read project metadata and default branch |
148
+ | `gitlab_list_branches` | Find branches and head commit metadata |
149
+ | `gitlab_get_branch` | Resolve a branch's current head SHA and commit date |
150
+ | `gitlab_compare_refs` | Compare branches or commits, including changes without an MR |
151
+ | `gitlab_list_issues` | Search issues by text, labels, state, milestone title and update lower bound |
152
+ | `gitlab_get_issue` | Read issue metadata, description and discovered uploads |
153
+ | `gitlab_issue_related_merge_requests` | Find MRs associated with an issue |
154
+ | `gitlab_get_discussions` | Read issue/MR threads, replies, review positions and uploads |
155
+ | `gitlab_read_attachment` | Reuse cached analysis or read an upload by project, secret and filename |
156
+ | `gitlab_save_attachment_summary` | Save completed host analysis in the local cache; never writes to GitLab |
157
+ | `gitlab_list_merge_requests` | Search merge requests |
158
+ | `gitlab_get_merge_request` | Read MR metadata, description, diff SHAs and pipeline summary |
159
+ | `gitlab_get_merge_request_diffs` | Read changed files and unified diffs |
160
+ | `gitlab_list_merge_request_commits` | Read MR commits |
161
+ | `gitlab_repository_tree` | Browse repository paths when unavailable locally |
162
+ | `gitlab_read_file` | Read repository text when unavailable locally |
163
+ | `gitlab_prepare_review_comment` | Prepare a general or validated inline draft without posting |
164
+
165
+ `project` accepts a numeric ID or `group/subgroup/repository` path. Issue/MR `iid` is the project-local number in its web URL. Paginated tools return `items` and `nextPage`; follow pages until `nextPage` is null for complete results. Default page size is 20, maximum 100. Ref comparisons do not paginate diffs; GitLab can omit or truncate diffs, so check `compare_timeout`, `collapsed` and `too_large` where returned.
166
+
167
+ Issue reporting filters use the [GitLab Issues API](https://docs.gitlab.com/api/issues/). `milestone` is a title; `updated_after` is an update-time lower bound, not a closure-time filter.
168
+
169
+ ## Attachment cache
170
+
171
+ Attachment downloads and analysis summaries persist locally across server restarts. The assistant first calls `gitlab_read_attachment`. If a summary exists for the requested `analysis_focus` (default `general`), the server returns that summary instead of sending the image/video back for interpretation. Otherwise it reuses cached bytes or downloads the upload once. Concurrent reads of the same upload share one download.
172
+
173
+ After actually inspecting the attachment, the assistant calls `gitlab_save_attachment_summary` with the returned `sha256`, matching `analysis_focus`, `inspected: true`, factual `summary`, inspection `method`, `coverage` and `limitations`. The server stores the host's analysis; it does not run a vision/video model itself. Summaries are bound to the downloaded content hash. GIF summaries use `method: "animation_frames"`; older image summaries are bypassed to avoid reusing first-frame-only analysis. Claim complete GIF coverage only after inspecting every frame. Video frames or transcripts must be marked partial with explicit limitations; an unsupported download alone does not count as inspection. The workflows tell the assistant to save and reuse these summaries during subsequent resolution steps.
174
+
175
+ - `reuse_summary: false` reopens the cached content for more detailed inspection without downloading again.
176
+ - A different `analysis_focus` keeps a separate summary for a different question (up to 10 scopes per upload).
177
+ - `refresh: true` downloads a fresh copy and invalidates previous summaries, even if the bytes are unchanged.
178
+ - `output: "resource"` returns cached raw bytes for a capable host/parser instead of a summary.
179
+
180
+ The default directory is `~/.cache/gitlab-mcp/attachments`. Entries expire for reuse after seven days and the cache retains at most 256 uploads across server processes, evicting older files when new entries are saved. Each upload remains subject to `GITLAB_MAX_BYTES`. Cache files contain private attachment content and summaries, use owner-only file permissions, and never store the token. Entries are isolated by GitLab instance, token, project and upload identity; token rotation starts a separate cache. Cached reads require a live authenticated project-access check, so they cannot be used offline. This check validates project access, not whether an individual upload has since been deleted; use refresh when fresh attachment availability matters. Remove the cache directory with the server stopped to clear all stored content. If disk caching is unavailable, downloads still use a bounded in-memory cache; a failed summary persistence request returns an error.
181
+
182
+ For Docker, persist the cache by adding these arguments before the image name in the existing configuration:
183
+
184
+ ```json
185
+ "--mount", "type=volume,src=gitlab-mcp-cache,dst=/home/node/.cache/gitlab-mcp"
46
186
  ```
47
187
 
48
- The scoped name avoids collisions with unscoped packages, but `@jswk1` must be an npm scope you control. For the first publish of a scoped package, use `npm publish --access public`; npm requires an authenticated account with publish access to that scope. For later releases, bump the version first, for example with `npm version patch`, then publish again. Add public repository/issue URLs to `package.json` only if those URLs are intended to be public.
49
-
50
- Configure your MCP host to start this command (replace the path, URL and token locally):
51
-
52
- ```json
53
- {
54
- "mcpServers": {
55
- "gitlab": {
56
- "command": "node",
57
- "args": ["/Users/developer/Code/gitlab-mcp/dist/index.js"],
58
- "env": {
59
- "GITLAB_URL": "https://gitlab.example.internal",
60
- "GITLAB_TOKEN": "YOUR_PERSONAL_READ_API_TOKEN",
61
- "GITLAB_USER_ID": "123"
62
- }
63
- }
64
- }
65
- }
66
- ```
67
-
68
- `GITLAB_USER_ID` is optional but recommended: startup calls `/user` and refuses to expose tools if the token belongs to someone else. Use the numeric user ID. Tokens are supplied through the environment, never through tool arguments or URLs. Keep host configurations containing tokens private. Restart the process after rotating a token.
69
-
70
- For a terminal session, copy `.env.example` to `.env`, fill it locally, and run:
71
-
72
- ```sh
73
- node --env-file=.env dist/index.js
74
- ```
75
-
76
- The server uses stdio (stdout is reserved for MCP messages). Run **one process per developer/MCP host**. No shared HTTP listener or multi-user token store is provided; do not share a process across users. A central HTTP deployment would require separate MCP authentication and user-scoped token resolution.
77
-
78
- ## Docker
79
-
80
- Build the image from the repository root:
81
-
82
- ```sh
83
- npm run docker:build
84
- ```
85
-
86
- Copy `.env.example` to `.env` and fill in your instance URL and personal token, then run:
87
-
88
- ```sh
89
- docker run --rm -i --env-file .env gitlab-readonly-mcp
90
- ```
91
-
92
- The container runs as a non-root user and contains the compiled server and production dependencies. Credentials are supplied at runtime; `.env` files are excluded from the build context. The image uses a [multi-stage build](https://docs.docker.com/build/building/multi-stage/).
93
-
94
- For an MCP host, use the following configuration, replacing the URL, token and user ID locally:
95
-
96
- ```json
97
- {
98
- "mcpServers": {
99
- "gitlab": {
100
- "command": "docker",
101
- "args": [
102
- "run", "--rm", "-i",
103
- "-e", "GITLAB_URL",
104
- "-e", "GITLAB_TOKEN",
105
- "-e", "GITLAB_USER_ID",
106
- "gitlab-readonly-mcp"
107
- ],
108
- "env": {
109
- "GITLAB_URL": "https://gitlab.example.internal",
110
- "GITLAB_TOKEN": "YOUR_PERSONAL_READ_API_TOKEN",
111
- "GITLAB_USER_ID": "123"
112
- }
113
- }
114
- }
115
- }
116
- ```
117
-
118
- Keep `-i` so the MCP host can send messages through stdin. Do not add `-t` or `-d`: MCP requires an attached, non-TTY stdio connection. No published ports are needed. See the [Docker run reference](https://docs.docker.com/reference/cli/docker/container/run/) for environment and stdin options.
119
-
120
- The GitLab URL must be reachable from the container. For GitLab running on your Docker Desktop host, use `host.docker.internal` instead of `localhost`. On Linux Docker Engine, add `--add-host=host.docker.internal:host-gateway` when using that hostname. For GitLab in another container, attach both containers to the same Docker network and use its service name.
121
-
122
- For an internal CA, mount a readable PEM file and point Node at the path inside the container:
123
-
124
- ```sh
125
- docker run --rm -i --env-file .env \
126
- --mount type=bind,source=/home/developer/certs/company-ca.pem,target=/certs/company-ca.pem,readonly \
127
- -e NODE_EXTRA_CA_CERTS=/certs/company-ca.pem \
128
- gitlab-readonly-mcp
129
- ```
130
-
131
- If `.env` also contains `NODE_EXTRA_CA_CERTS`, this explicit `-e` value overrides its host path with the container path.
132
-
133
- ## Tools
134
-
135
- | Tool | Purpose |
136
- | --- | --- |
137
- | `gitlab_get_workflow` | Select a developer, tester or documentalist workflow for the current issue/MR task |
138
- | `gitlab_current_user` | Verify the acting user |
139
- | `gitlab_list_projects` | Search accessible projects |
140
- | `gitlab_get_project` | Read project identity and default branch |
141
- | `gitlab_list_branches` | Discover issue branch candidates and head commit metadata |
142
- | `gitlab_get_branch` | Resolve a branch's current head SHA and commit date, including fork branches |
143
- | `gitlab_compare_refs` | Read branch/commit changes, including branches without an MR |
144
- | `gitlab_list_issues` | Search issues by text, labels and state |
145
- | `gitlab_get_issue` | Full issue metadata, description and upload discovery |
146
- | `gitlab_issue_related_merge_requests` | Merge requests associated with an issue |
147
- | `gitlab_get_discussions` | Issue/MR discussion threads, replies, review positions and uploads |
148
- | `gitlab_read_attachment` | Download an upload by project, secret and filename |
149
- | `gitlab_list_merge_requests` | Search merge requests |
150
- | `gitlab_get_merge_request` | MR metadata, description, diff SHAs, pipeline summary and uploads |
151
- | `gitlab_get_merge_request_diffs` | Paginated changed files and unified diffs |
152
- | `gitlab_list_merge_request_commits` | Commits in a merge request |
153
- | `gitlab_repository_tree` | Optional API fallback for repository paths unavailable locally |
154
- | `gitlab_read_file` | Optional API fallback for repository text unavailable locally |
155
- | `gitlab_prepare_review_comment` | General or validated inline comment draft, without posting |
156
-
157
- Projects accept a numeric ID or `group/subgroup/repository` path. Issue and MR identifiers are project-local **IIDs**, the numbers in their web URLs. Paginated tools return `items` and `nextPage`; continue until `nextPage` is null when full context is needed. Default page size is 20, maximum 100.
158
-
159
- ## User profiles
160
-
161
- Choose a profile **per task**. The default is `developer`; a profile selection is never stored or carried into later calls. All profiles retain the same read-only tools and the same token-based GitLab permissions. Profiles guide what the assistant investigates and how it presents its findings; they are not access-control roles.
162
-
163
- | Profile | Issue workflow | Merge request workflow |
164
- | --- | --- | --- |
165
- | `developer` | Read the complete issue and implement the requested fix/feature directly in local code | Return a Markdown review of bugs, regressions, code smells and improvements limited to changed code |
166
- | `tester` | Read the issue and latest related branch changes; return risk-prioritized manual test cases in Markdown | Inspect current changes and requirements; return a prioritized Markdown manual test plan |
167
- | `documentalist` | Read the issue, comments and merged/current MR changes; edit affected local Markdown documentation | Compare the implementation with current local docs and edit the relevant Markdown sections |
168
-
169
- Start with `gitlab_get_workflow`, passing `profile`, `target` (`issue` or `merge_request`), `project`, `iid`, and optionally `goal` containing the user's request and scope. This returns the role-specific workflow. The assistant gathers GitLab evidence through MCP and uses host tools for local repository work. The workflow call does not fetch data, analyze code or apply edits by itself. Developer and documentalist workflows honor partial scope, analysis-only and draft-only requests; otherwise implementation/documentation requests must result in actual local file edits, not only a plan.
170
-
171
- Example requests to your AI assistant:
172
-
173
- - **Developer, implementation:** “Read issue #123 in `group/project`, including all comments, images and attachments, and fix the bug in this repository. Implement only the validation changes.”
174
- - **Developer, review:** “Review MR !456 in `group/project`. Read all comments and compare original versus changed source. Return Markdown findings for bugs, regressions, code smells and easy improvements, limited to changed code.”
175
- - **Tester:** “Use the tester profile for issue #123 in `group/project`. Read the complete issue and the latest related branch changes. Return the critical manual test flows as Markdown, sorted by priority/risk.”
176
- - **Documentalist:** “Read issue #123 in `group/project` and prepare the documentation changes. Read the merged implementation, or the MR changes if not merged, then update the affected Markdown files in this repository.”
177
-
178
- Clients that support MCP prompts can also use:
179
-
180
- | Prompt | Arguments |
181
- | --- | --- |
182
- | `implement_issue` | `project`, `iid`, optional `goal`; implements locally using developer |
183
- | `investigate_issue` | `project`, `iid`, optional `profile` and `goal` |
184
- | `review_merge_request` | `project`, `iid`, optional `profile` and `goal` |
185
- | `prepare_test_plan` | `project`, `iid`, `target`, optional `goal`; always uses tester |
186
- | `update_documentation` | `project`, `iid`, `target`, optional `goal`; edits local Markdown using documentalist |
187
- | `draft_documentation` | `project`, `iid`, `target`; always uses documentalist |
188
-
189
- `draft_documentation` explicitly requests drafts without local edits; use `update_documentation` for direct Markdown changes. The AI host needs access to the local repository and appropriate editing tools for implementation/documentation tasks; no repository volume mount into the MCP container is needed.
190
-
191
- For testers, the explicitly requested branch/MR takes precedence. Otherwise inspect all related MR source branches and resolve their current head SHAs and commit dates. Do not use MR `updated_at` as the latest-code signal: comments can update it. If no MR exists, branch links and the conventional `<issue-IID>-` prefix can help discover candidates, but the prefix alone does not prove association. Verify the relationship, record the selection rationale and ask for clarification when the latest implementation is ambiguous. Read changes at the selected head SHA and recheck it before finalizing. `gitlab_compare_refs` supports branch changes without an MR; it returns GitLab's `compare_timeout`, `collapsed` and `too_large` indicators unchanged. Its diffs are not paginated. See the [GitLab Branches API](https://docs.gitlab.com/api/branches/) and [comparison API](https://docs.gitlab.com/api/repositories/#compare-branches-tags-or-commits).
192
-
193
- For documentalists, inspect all relevant merged changes, including merge/squash commit SHAs, and current MR changes when unmerged. A merged implementation can be read from Git history even after its source branch is deleted. Update the current working repository's documentation; mark unmerged behavior accurately. A merge does not by itself establish deployment or release status. The tester workflow returns a manual test plan; it does not execute tests or confirm the fix works. All roles cite evidence and report gaps. Documentation outside the repository (wiki pages or external services) requires separate access.
194
-
195
- After updating an existing Docker installation, run `npm run docker:build`, recreate the container using the new image and restart the AI client's MCP connection. Rebuilding an image alone does not update an already running container.
196
-
197
- ## Typical workflows
198
-
199
- **Complete issue context:** read the full issue, every discussion page and all replies; download every discovered upload from the description and comments using its `secret` and decoded `filename`. Keep an asset coverage checklist and explicitly report anything unread. Descriptions retain labels, assignees, milestone, author and other API metadata. PNG/JPEG/GIF/WebP uploads return native MCP image content for a vision-capable assistant. Text/log/JSON attachments return text. Other files (PDF, Office, archives, video, etc.) return an embedded binary MCP resource; interpretation requires a host/parser that supports that format. No OCR or document parser is built in. External URLs, wiki uploads and other non-project-upload links are not downloaded by this server. These limits must be surfaced rather than claiming everything was inspected.
200
-
201
- **Review an MR:** read metadata, all diff pages and all existing discussions, including resolved threads. Use local Git to compare original source at `diff_refs.base_sha` with changed source at `diff_refs.head_sha`; `start_sha` identifies the target snapshot. Fetch missing refs using host tools when allowed, preserving the working tree. For forks, account for `source_project_id`. Read surrounding code/callers for context, but report only changed-code findings or regressions introduced by the change. Return Markdown findings sorted by severity, with optional code smells, improvements and small refactors separated from defects. Avoid duplicates from existing comments. No local edits or GitLab posting are needed. `gitlab_prepare_review_comment` remains available if the user explicitly requests comment drafts; general drafts omit path/line.
202
-
203
- Inline drafts validate the line against the current unified diff, include base/start/head SHAs, and recheck MR refs before returning. Renamed files retain both paths. Validation scans up to 20 pages of 100 files; a line outside available diff context is rejected. GitLab can limit diffs independently of pagination; inspect `collapsed`/`too_large` flags and report incomplete context. Draft SHAs can become stale after generation; any separate publishing workflow must revalidate them. Inline comments on unchanged context include both old and new line numbers, following the [GitLab Discussions API](https://docs.gitlab.com/api/discussions/).
204
-
205
- ## Configuration and boundaries
206
-
207
- | Variable | Default | Purpose |
208
- | --- | --- | --- |
209
- | `GITLAB_URL` | Required | Instance root, optionally including a subpath; no `/api/v4` suffix |
210
- | `GITLAB_TOKEN` | Required | Current developer's personal access token |
211
- | `GITLAB_USER_ID` | Optional | Expected numeric user ID |
212
- | `GITLAB_TIMEOUT_MS` | `30000` | Timeout for each REST request and response body |
213
- | `GITLAB_MAX_BYTES` | `5242880` | Maximum response bytes per request, enforced while streaming |
214
- | `NODE_EXTRA_CA_CERTS` | Optional | PEM file with your organization's trusted CA |
215
-
216
- Use HTTPS for token confidentiality. HTTP is supported for isolated local development. TLS verification remains enabled; configure your internal CA rather than disabling verification. All GitLab requests use GET; there are no write tools, generic URL fetch tools, or disk attachment writes. Redirects are rejected so credentials cannot be forwarded to storage or external hosts. Configure the canonical GitLab URL; uploads that redirect to object storage are consequently unsupported. Oversized files/pages fail explicitly; reduce page size or request a smaller repository file.
217
-
218
- GitLab text, code and attachments are untrusted input. Server instructions direct assistants to treat them as data and ignore embedded commands. The host and model remain responsible for following this boundary. API errors omit response bodies and tokens. Private source and attachments enter the connected assistant's context; select an AI host appropriate for your organization's data policy.
219
-
220
- ## Development
221
-
222
- ```sh
223
- npm run check
224
- npm test
225
- npm run build
226
- ```
227
-
228
- Tests use mocked REST responses and an in-memory MCP client/server round trip; no real tokens or GitLab instance are needed. Live compatibility and actual user permissions should also be checked against your instance before rollout.
229
-
230
- Implementation uses the [official MCP TypeScript SDK](https://ts.sdk.modelcontextprotocol.io/server) and [GitLab REST API](https://docs.gitlab.com/api/).
188
+ Without a volume, Docker's `--rm` removes cached content when the container exits.
189
+
190
+ ## Configuration
191
+
192
+ | Environment variable | Default | Purpose |
193
+ | --- | --- | --- |
194
+ | `GITLAB_URL` | Required | GitLab instance root, optionally with a subpath; omit `/api/v4` |
195
+ | `GITLAB_TOKEN` | Required | Developer's personal access token with `read_api` |
196
+ | `GITLAB_USER_ID` | Optional | Expected numeric user ID; startup fails if the token belongs to another user |
197
+ | `GITLAB_TIMEOUT_MS` | `30000` | Timeout per API request and response body |
198
+ | `GITLAB_CACHE_DIR` | `~/.cache/gitlab-mcp/attachments` | Local attachment and summary cache directory |
199
+ | `GITLAB_CACHE_TTL_MS` | `604800000` (7 days) | Maximum age for reuse since download |
200
+ | `GITLAB_CACHE_MAX_ENTRIES` | `256` | Maximum retained uploads |
201
+ | `GITLAB_MAX_BYTES` | `5242880` (5 MiB) | Maximum response size per request |
202
+ | `NODE_EXTRA_CA_CERTS` | Optional | Path to a trusted internal CA certificate in PEM format |
203
+
204
+ Keep tokens in private client settings or an uncommitted `.env` file. Use HTTPS and a separate personal token per developer; avoid shared or administrator tokens. Restart the server after changing credentials.
205
+
206
+ For an internal CA, set `NODE_EXTRA_CA_CERTS` instead of disabling TLS verification. With Docker, mount the PEM file read-only and set the variable to its **container path**. Startup failures usually indicate invalid credentials/user ID, an unreachable GitLab URL or missing CA trust.
207
+
208
+ ## Limits and data handling
209
+
210
+ - **Read-only GitLab access:** all API requests use GET. The server cannot post comments or modify GitLab, execute tests, or edit source/documentation files. It writes only its local attachment cache. Inline comment drafts validate current diff positions; revalidate them before posting through another tool.
211
+ - **Attachments depend on client support:** PNG/JPEG/WebP return native images; GIFs return composited PNG frames with timestamps and delays, ten at a time (and up to 8 MB of PNG data per response). Follow `nextFrame` using `frame_start` until it is null to inspect the whole animation. GIF decoding is bounded to 40 million decoded pixels and 1000 frames; failures are reported as unread; text returns inline content; PDFs return binary resources requiring a capable client. Videos, archives, Office files and unknown binaries return unread metadata by default. Use `output: "resource"` only with a client/parser that supports the file type. GIF decoding uses sharp/libvips. No OCR, video conversion or document parser is built in.
212
+ - **Download limits:** only GitLab project uploads are supported. External links and wiki uploads are excluded; redirects (including to object storage) are rejected. Oversized responses fail explicitly. Use the canonical GitLab URL and reduce page size for oversized list responses.
213
+ - **Private data enters your assistant's context:** choose a client/model suitable for your organization's data policy. Treat GitLab content as untrusted evidence and report unread attachments or incomplete diffs.
@@ -0,0 +1,27 @@
1
+ export declare const contentHash: (bytes: Buffer | string) => string;
2
+ export interface AttachmentSummary {
3
+ focus: string;
4
+ summary: string;
5
+ coverage: 'complete' | 'partial';
6
+ method: string;
7
+ limitations: string[];
8
+ savedAt: string;
9
+ }
10
+ export interface CachedAttachment {
11
+ version: 1;
12
+ sha256: string;
13
+ mimeType: string;
14
+ data: string;
15
+ downloadedAt: string;
16
+ summaries: AttachmentSummary[];
17
+ }
18
+ export declare class AttachmentCache {
19
+ private readonly directory;
20
+ private readonly maxBytes;
21
+ private readonly ttlMs;
22
+ private readonly maxEntries;
23
+ private readonly memory;
24
+ constructor(directory: string | undefined, maxBytes: number, ttlMs?: number, maxEntries?: number);
25
+ get(key: string): Promise<CachedAttachment | undefined>;
26
+ put(key: string, entry: CachedAttachment): Promise<void>;
27
+ }
@@ -0,0 +1,72 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ export const contentHash = (bytes) => createHash('sha256').update(bytes).digest('hex');
5
+ // Cache keys include instance, credentials and upload identity; never persist a token.
6
+ export class AttachmentCache {
7
+ directory;
8
+ maxBytes;
9
+ ttlMs;
10
+ maxEntries;
11
+ memory = new Map();
12
+ constructor(directory, maxBytes, ttlMs = 7 * 24 * 60 * 60 * 1000, maxEntries = 256) {
13
+ this.directory = directory;
14
+ this.maxBytes = maxBytes;
15
+ this.ttlMs = ttlMs;
16
+ this.maxEntries = maxEntries;
17
+ }
18
+ async get(key) {
19
+ let entry = this.memory.get(key);
20
+ if (!entry && this.directory) {
21
+ try {
22
+ const path = join(this.directory, `${key}.json`);
23
+ if ((await stat(path)).size > Math.ceil(this.maxBytes * 4 / 3) + 1024 * 1024)
24
+ return undefined;
25
+ entry = JSON.parse(await readFile(path, 'utf8'));
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
31
+ if (!entry)
32
+ return undefined;
33
+ try {
34
+ const bytes = Buffer.from(entry.data, 'base64');
35
+ const age = Date.now() - Date.parse(entry.downloadedAt);
36
+ if (!Array.isArray(entry.summaries))
37
+ return undefined;
38
+ if (!entry.summaries.every(summary => summary && typeof summary.focus === 'string' && typeof summary.summary === 'string' && ['complete', 'partial'].includes(summary.coverage) && typeof summary.method === 'string' && typeof summary.savedAt === 'string' && Array.isArray(summary.limitations) && summary.limitations.every(value => typeof value === 'string')))
39
+ return undefined;
40
+ if (entry.version !== 1 || !Number.isFinite(age) || age < 0 || age >= this.ttlMs || bytes.length > this.maxBytes || contentHash(bytes) !== entry.sha256 || typeof entry.mimeType !== 'string' || !Array.isArray(entry.summaries))
41
+ return undefined;
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ return entry;
47
+ }
48
+ async put(key, entry) {
49
+ this.memory.delete(key);
50
+ this.memory.set(key, entry);
51
+ while (this.memory.size > this.maxEntries)
52
+ this.memory.delete(this.memory.keys().next().value);
53
+ if (this.directory) {
54
+ await mkdir(this.directory, { recursive: true, mode: 0o700 });
55
+ const path = join(this.directory, `${key}.json`);
56
+ const temporary = `${path}.${randomUUID()}.tmp`;
57
+ try {
58
+ await writeFile(temporary, JSON.stringify(entry), { mode: 0o600, flag: 'wx' });
59
+ await rename(temporary, path);
60
+ }
61
+ finally {
62
+ await unlink(temporary).catch(() => { });
63
+ }
64
+ // Bound on-disk retention, including entries from earlier server processes.
65
+ const files = (await readdir(this.directory)).filter(name => /^[a-f\d]{64}\.json$/.test(name));
66
+ const dated = await Promise.all(files.map(async (name) => ({ name, time: (await stat(join(this.directory, name)).catch(() => ({ mtimeMs: 0 }))).mtimeMs })));
67
+ dated.sort((a, b) => b.time - a.time);
68
+ await Promise.all(dated.slice(this.maxEntries).map(file => unlink(join(this.directory, file.name)).catch(() => { })));
69
+ }
70
+ }
71
+ }
72
+ //# sourceMappingURL=attachment-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-cache.js","sourceRoot":"","sources":["../src/attachment-cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare function uploadMimeType(bytes: Buffer, filename: string, contentType: string | null): string;
@@ -0,0 +1,28 @@
1
+ import { extname } from 'node:path';
2
+ // GitLab/proxies may serve uploads with a generic download Content-Type.
3
+ // Prefer recognizable bytes, then the filename, only for generic headers.
4
+ export function uploadMimeType(bytes, filename, contentType) {
5
+ const declared = contentType?.split(';')[0]?.trim().toLowerCase();
6
+ if (declared && !['application/octet-stream', 'binary/octet-stream', 'application/binary'].includes(declared))
7
+ return declared;
8
+ if (bytes.subarray(0, 8).equals(Buffer.from('89504e470d0a1a0a', 'hex')))
9
+ return 'image/png';
10
+ if (bytes.length >= 3 && bytes.subarray(0, 3).equals(Buffer.from('ffd8ff', 'hex')))
11
+ return 'image/jpeg';
12
+ if (['GIF87a', 'GIF89a'].includes(bytes.toString('ascii', 0, 6)))
13
+ return 'image/gif';
14
+ if (bytes.toString('ascii', 0, 4) === 'RIFF' && bytes.toString('ascii', 8, 12) === 'WEBP')
15
+ return 'image/webp';
16
+ if (bytes.toString('ascii', 0, 5) === '%PDF-')
17
+ return 'application/pdf';
18
+ const types = {
19
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
20
+ '.gif': 'image/gif', '.webp': 'image/webp', '.pdf': 'application/pdf',
21
+ '.mp4': 'video/mp4', '.m4v': 'video/mp4', '.mov': 'video/quicktime',
22
+ '.webm': 'video/webm', '.avi': 'video/x-msvideo', '.mkv': 'video/x-matroska',
23
+ '.txt': 'text/plain', '.log': 'text/plain', '.md': 'text/markdown',
24
+ '.csv': 'text/csv', '.json': 'application/json', '.xml': 'application/xml',
25
+ };
26
+ return types[extname(filename).toLowerCase()] ?? 'application/octet-stream';
27
+ }
28
+ //# sourceMappingURL=attachments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachments.js","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,yEAAyE;AACzE,0EAA0E;AAC1E,MAAM,UAAU,cAAc,CAAC,KAAa,EAAE,QAAgB,EAAE,WAA0B;IACxF,MAAM,QAAQ,GAAG,WAAW,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAClE,IAAI,QAAQ,IAAI,CAAC,CAAC,0BAA0B,EAAE,qBAAqB,EAAE,oBAAoB,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC/H,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;QAAE,OAAO,WAAW,CAAC;IAC5F,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAAE,OAAO,YAAY,CAAC;IACxG,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAAE,OAAO,WAAW,CAAC;IACrF,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,MAAM;QAAE,OAAO,YAAY,CAAC;IAC/G,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,OAAO;QAAE,OAAO,iBAAiB,CAAC;IACxE,MAAM,KAAK,GAA2B;QACpC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY;QAChE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,iBAAiB;QACrE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB;QACnE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,kBAAkB;QAC5E,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,eAAe;QAClE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,EAAE,iBAAiB;KAC3E,CAAC;IACF,OAAO,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,0BAA0B,CAAC;AAC9E,CAAC"}
package/dist/config.d.ts CHANGED
@@ -4,5 +4,8 @@ export interface Config {
4
4
  expectedUserId?: number;
5
5
  timeoutMs: number;
6
6
  maxBytes: number;
7
+ cacheDir?: string;
8
+ cacheTtlMs?: number;
9
+ cacheMaxEntries?: number;
7
10
  }
8
11
  export declare function readConfig(env?: NodeJS.ProcessEnv): Config;
package/dist/config.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
1
3
  function integer(value, fallback, name) {
2
4
  const parsed = value === undefined ? fallback : Number(value);
3
5
  if (!Number.isSafeInteger(parsed) || parsed <= 0)
@@ -17,6 +19,9 @@ export function readConfig(env = process.env) {
17
19
  baseUrl: url.href.replace(/\/$/, ''), token: env.GITLAB_TOKEN.trim(),
18
20
  expectedUserId: env.GITLAB_USER_ID === undefined ? undefined : integer(env.GITLAB_USER_ID, 1, 'GITLAB_USER_ID'),
19
21
  timeoutMs: integer(env.GITLAB_TIMEOUT_MS, 30000, 'GITLAB_TIMEOUT_MS'),
22
+ cacheDir: env.GITLAB_CACHE_DIR || join(homedir(), '.cache', 'gitlab-mcp', 'attachments'),
23
+ cacheTtlMs: integer(env.GITLAB_CACHE_TTL_MS, 7 * 24 * 60 * 60 * 1000, 'GITLAB_CACHE_TTL_MS'),
24
+ cacheMaxEntries: integer(env.GITLAB_CACHE_MAX_ENTRIES, 256, 'GITLAB_CACHE_MAX_ENTRIES'),
20
25
  maxBytes: integer(env.GITLAB_MAX_BYTES, 5 * 1024 * 1024, 'GITLAB_MAX_BYTES'),
21
26
  };
22
27
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAQA,SAAS,OAAO,CAAC,KAAyB,EAAE,QAAgB,EAAE,IAAY;IACxE,MAAM,MAAM,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9D,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,6BAA6B,CAAC,CAAC;IACxG,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,IAAI,CAAC,GAAG,CAAC,UAAU,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC9G,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACpC,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;QAC1G,MAAM,IAAI,KAAK,CAAC,oFAAoF,CAAC,CAAC;IACxG,CAAC;IACD,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC7E,OAAO;QACL,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE;QACpE,cAAc,EAAE,GAAG,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,EAAE,gBAAgB,CAAC;QAC/G,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,KAAK,EAAE,mBAAmB,CAAC;QACrE,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,kBAAkB,CAAC;KAC7E,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAajC,SAAS,OAAO,CAAC,KAAyB,EAAE,QAAgB,EAAE,IAAY;IACxE,MAAM,MAAM,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9D,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,6BAA6B,CAAC,CAAC;IACxG,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,IAAI,CAAC,GAAG,CAAC,UAAU,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC9G,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACpC,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;QAC1G,MAAM,IAAI,KAAK,CAAC,oFAAoF,CAAC,CAAC;IACxG,CAAC;IACD,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC7E,OAAO;QACL,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE;QACpE,cAAc,EAAE,GAAG,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,EAAE,gBAAgB,CAAC;QAC/G,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,KAAK,EAAE,mBAAmB,CAAC;QACrE,QAAQ,EAAE,GAAG,CAAC,gBAAgB,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,CAAC;QACxF,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,qBAAqB,CAAC;QAC5F,eAAe,EAAE,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,GAAG,EAAE,0BAA0B,CAAC;QACvF,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,kBAAkB,CAAC;KAC7E,CAAC;AACJ,CAAC"}