@jswk1/gitlab-mcp 1.0.0 → 1.1.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
@@ -1,97 +1,74 @@
1
1
  # GitLab read-only MCP server
2
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.
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
4
 
5
- ## Setup
5
+ ## Setup
6
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/).
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.
8
10
 
9
- ```sh
10
- npm ci
11
- npm run build
12
- ```
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.
13
12
 
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
-
16
- ### Use the published npm package
17
-
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):
19
-
20
- ```json
21
- {
22
- "mcpServers": {
23
- "gitlab": {
24
- "command": "npx",
25
- "args": ["--yes", "--package=@jswk1/gitlab-mcp@1.0.0", "gitlab-mcp"],
26
- "env": {
27
- "GITLAB_URL": "https://gitlab.example.internal",
28
- "GITLAB_TOKEN": "YOUR_PERSONAL_READ_API_TOKEN",
29
- "GITLAB_USER_ID": "123"
30
- }
31
- }
32
- }
33
- }
34
- ```
35
-
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.
37
-
38
- ### Publish to npm
39
-
40
- Review the files npm will publish and release:
41
-
42
- ```sh
43
- npm login
44
- npm pack --dry-run
45
- npm publish
46
- ```
47
-
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):
13
+ ### Option 1: npm package
14
+
15
+ Configure your MCP client to run the npm package:
51
16
 
52
17
  ```json
53
18
  {
54
19
  "mcpServers": {
55
20
  "gitlab": {
56
- "command": "node",
57
- "args": ["/Users/developer/Code/gitlab-mcp/dist/index.js"],
21
+ "command": "npx",
22
+ "args": ["--yes", "--package=@jswk1/gitlab-mcp@1.1.0", "gitlab-mcp"],
58
23
  "env": {
59
24
  "GITLAB_URL": "https://gitlab.example.internal",
60
- "GITLAB_TOKEN": "YOUR_PERSONAL_READ_API_TOKEN",
61
- "GITLAB_USER_ID": "123"
25
+ "GITLAB_TOKEN": "YOUR_PERSONAL_READ_API_TOKEN"
62
26
  }
63
27
  }
64
28
  }
65
29
  }
66
30
  ```
67
31
 
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.
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
69
35
 
70
- For a terminal session, copy `.env.example` to `.env`, fill it locally, and run:
36
+ From the repository root:
71
37
 
72
38
  ```sh
73
- node --env-file=.env dist/index.js
39
+ npm ci
40
+ npm run build
74
41
  ```
75
42
 
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.
43
+ Point your MCP client at the compiled entry point using an absolute path:
77
44
 
78
- ## Docker
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "gitlab": {
49
+ "command": "node",
50
+ "args": ["/absolute/path/to/gitlab-mcp/dist/index.js"],
51
+ "env": {
52
+ "GITLAB_URL": "https://gitlab.example.internal",
53
+ "GITLAB_TOKEN": "YOUR_PERSONAL_READ_API_TOKEN"
54
+ }
55
+ }
56
+ }
57
+ }
58
+ ```
79
59
 
80
- Build the image from the repository root:
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.
81
61
 
82
- ```sh
83
- npm run docker:build
84
- ```
62
+ ### Option 3: Docker
85
63
 
86
- Copy `.env.example` to `.env` and fill in your instance URL and personal token, then run:
64
+ From the repository root:
87
65
 
88
66
  ```sh
89
- docker run --rm -i --env-file .env gitlab-readonly-mcp
67
+ npm run docker:build
68
+ cp .env.example .env
90
69
  ```
91
70
 
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:
71
+ Fill in `GITLAB_URL` and `GITLAB_TOKEN` in `.env`, then configure your MCP client:
95
72
 
96
73
  ```json
97
74
  {
@@ -100,131 +77,137 @@ For an MCP host, use the following configuration, replacing the URL, token and u
100
77
  "command": "docker",
101
78
  "args": [
102
79
  "run", "--rm", "-i",
103
- "-e", "GITLAB_URL",
104
- "-e", "GITLAB_TOKEN",
105
- "-e", "GITLAB_USER_ID",
80
+ "--env-file", "/absolute/path/to/gitlab-mcp/.env",
106
81
  "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
- }
82
+ ]
113
83
  }
114
84
  }
115
85
  }
116
86
  ```
117
87
 
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.
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.
119
89
 
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.
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.
121
91
 
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
- ```
92
+ After source updates, rebuild the image and restart your client's MCP connection to create a new container.
130
93
 
131
- If `.env` also contains `NODE_EXTRA_CA_CERTS`, this explicit `-e` value overrides its host path with the container path.
94
+ ## Workflows
132
95
 
133
- ## Tools
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.
134
97
 
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 |
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 |
156
104
 
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.
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.
158
106
 
159
- ## User profiles
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.
160
108
 
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.
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.
162
110
 
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 |
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.
168
112
 
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.
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.
170
114
 
171
- Example requests to your AI assistant:
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.
172
116
 
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.”
117
+ Example requests:
177
118
 
178
- Clients that support MCP prompts can also use:
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.”
179
127
 
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 |
128
+ Clients supporting MCP prompts can use these shortcuts:
188
129
 
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.
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` |
194
139
 
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.
140
+ ## Available tools
196
141
 
197
- ## Typical workflows
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 |
198
164
 
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.
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.
200
166
 
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.
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.
202
168
 
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/).
169
+ ## Attachment cache
204
170
 
205
- ## Configuration and boundaries
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.
206
172
 
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 |
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. 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.
215
174
 
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.
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.
217
179
 
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.
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.
219
181
 
220
- ## Development
182
+ For Docker, persist the cache by adding these arguments before the image name in the existing configuration:
221
183
 
222
- ```sh
223
- npm run check
224
- npm test
225
- npm run build
184
+ ```json
185
+ "--mount", "type=volume,src=gitlab-mcp-cache,dst=/home/node/.cache/gitlab-mcp"
226
186
  ```
227
187
 
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.
188
+ Without a volume, Docker's `--rm` removes cached content when the container exits.
189
+
190
+ ## Configuration
229
191
 
230
- Implementation uses the [official MCP TypeScript SDK](https://ts.sdk.modelcontextprotocol.io/server) and [GitLab REST API](https://docs.gitlab.com/api/).
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/GIF/WebP return native images; 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. 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"}