feat: add snippet CRUD tools - #730
Conversation
Adds list_snippets, get_snippet, create_snippet, update_snippet, and delete_snippet — supporting both project-scoped and personal snippets via optional project_id. Registered under a new non-default "snippets" toolset. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
The multi-file content endpoint requires a ref to construct the API URL.
Using default_branch caused 404s on instances where the default branch
is not "main"; fetching raw_url directly also 404s because it is a web
URL, not an API endpoint.
GitLab embeds the correct ref inside each file's raw_url
(.../raw/{ref}/{path}). Parse it out and pass it to the REST API
endpoint (/snippets/{id}/files/{ref}/{path}/raw) instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getSnippetFileRawContent now takes an explicit ref string instead of a raw_url to parse. The ref-extraction logic is factored into a standalone extractSnippetRef helper, resolved once at the call site rather than once per file inside the fetch function. get_snippet also gains an optional ref parameter (matching get_file_contents) so callers can override the ref directly without relying on raw_url parsing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Nested multi-file paths now encode the full file_path as a single segment (encodeURIComponent) per GitLab's /files/:ref/:file_path/raw contract. Previously, paths like dir/policy.md kept the slash and 404'd. - getSnippetsEndpoint decodes URL-encoded project paths before re-encoding, matching the existing pattern in merge_requests/issues. Eliminates double-encoded URLs like /projects/group%252Fproject/snippets. - GitLabSnippetSchema.visibility is now optional — the project snippets API does not consistently return it and ZodError was breaking list/get/create/ update for valid responses. - update_snippet exposes the modern multi-file contract: files[] with action (create/update/delete/move) and previous_path. The file_name+content shortcut remains for backward compatibility and is mutually exclusive.
SnippetFileUpdateActionSchema had previous_path optional even though the schema description said it was required for action: "move". That left malformed move actions to fail late with a GitLab 400 instead of being caught at parse time. Adds a superRefine that surfaces the constraint as a Zod error so LLM-generated update requests get a clear, local rejection.
get_snippet(include_content=true) previously threw if files[0].raw_url was missing, even when the caller passed ref explicitly. Flip the guard so raw_url is only required as a fallback for inferring ref.
getSnippetsEndpoint used to fall back to the global /snippets endpoint whenever project_id was omitted, bypassing GITLAB_PROJECT_ID and GITLAB_ALLOWED_PROJECT_IDS access boundaries. Route omitted project_id through getEffectiveProjectId when either scope env is set, reusing its existing default-when-unambiguous, reject-when-ambiguous semantics. Personal snippets remain available only when no scope is configured.
SnippetFileUpdateActionSchema only enforced previous_path for 'move', so malformed actions (create/update without content, delete without file_path) and empty files[] arrays passed local validation and only failed later as GitLab 400s. Expand the superRefine with the full action matrix and require files[] to be non-empty when provided.
When include_content is true and the snippet has one file, use getSnippetFileRawContent with the explicit ref instead of always fetching HEAD via /raw. Rebase onto main and add regression test. Co-authored-by: Cursor <cursoragent@cursor.com>
- register delete_snippet in deleteTools (modify mode) + destructive list sync - add snippets GROUP_META/GROUP_ORDER and regenerate docs/tools (+snippets.md) - list snippet tools in README + discover_tools categories - sync skills/gitlab-mcp (counts, toolset table, destructive list) - add delete_snippet to permission-mode delete samples - get_snippet: resolve single-file path from files[0].path or file_name when ref is set; document project scope env fallback in schemas - test: file_name fallback regression test; findMockServerPort() signature
|
Warning Review limit reachedNext included review available in 20 minutes. View limit detailsLimit details: You’ve used all 4 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: 📒 Files selected for processing (14)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@cursor please review this PR. |
PR 리뷰: feat: add snippet CRUD tools판정: Approve (LGTM) — 머지해도 될 수준. 아래 nit 몇 개만 참고하면 됨. 직접 잘한 거1. 레포 컨벤션 잘 따름releases/tags 패턴 그대로 따라감. 2.
|
| # | 내용 | 심각도 |
|---|---|---|
| 1 | createSnippet의 file_name as string, content as string — Zod refine 뒤라 런타임은 안전한데, 타입 단언 대신 narrowing 쓰면 더 깔끔 |
nit |
| 2 | list_snippets는 pagination만 노출. GitLab API의 created_after 등 필터는 없음 — MVP로 OK, 나중에 추가 후보 |
enhancement |
| 3 | .well-known/mcp/server-card.json에 snippet tool 없음. scripts/generate-server-card.ts 있는데 이번 PR diff엔 없음. 의도적이면 OK, 아니면 regenerate 한번 |
nit |
| 4 | extractSnippetRef가 files[].path와 raw_url decoded path가 안 맞으면 실패함. GitLab이 path를 encoded로 주는 케이스는 아직 테스트 없음 — 실제 이슈 나오면 그때 |
edge case |
보안 체크
- SSRF: API base + 고정 path만 씀. OK
- 권한 모드:
delete_snippet→deleteTools+destructiveTools+ skill sync. OK - 프로젝트 격리:
getEffectiveProjectId경유. OK - 입력 검증: Zod + per-action refine. OK
OWASP 쪽에서 눈에 띄는 취약점은 없음.
#470 대비 확인한 것
| 항목 | 상태 |
|---|---|
single-file ref → /raw 대신 file path API |
✅ |
delete_snippet modify mode 차단 |
✅ |
| docs/skill/README 동기화 | ✅ |
file_name fallback |
✅ |
총평
#470 리베이스 + 리뷰 반영이 제대로 됐고, 테스트/문서/권한 통합까지 맞춰져 있음. snippet CRUD를 opt-in toolset으로 넣는 방향도 기존 releases/tags와 일관됨.
블로킹 이슈 없음. Approve 추천.
진짜야? 제대로 한 거 맞아? — 코드 읽고 테스트 88+26 돌려본 결과 기준으로 말하는 거고, 실 GitLab 인스턴스 live test는 안 함.
자신감: 8.5/10 (live API 미검증, server-card 동기화 여부만 확신 못 함)


Supersedes #470, which went stale. This rebases the original work onto current
main, addresses the last review round, and aligns with current repo conventions.Thanks to @renanliberato for the original implementation and the thorough review follow-ups in #470 — the tool design, schemas, and test matrix there are carried over here.
Tools (new opt-in
snippetstoolset)list_snippets,get_snippet,create_snippet,update_snippet,delete_snippetproject_idselects project snippets; omitted means personal snippets (or the configured project whenGITLAB_PROJECT_ID/GITLAB_ALLOWED_PROJECT_IDSis set)On top of #470
get_snippet: single-filerefis honored — path resolves fromfiles[0].pathorsnippet.file_nameinstead of always hitting/raw(last item from the feat: add snippet CRUD tools #470 review)delete_snippetregistered indeleteToolssomodifymode blocks it; skill destructive list synceddocs/tools(snippets.md), README tool list +discover_toolscategories,skills/gitlab-mcpcountsdelete_snippetadded to permission-mode delete samples;findMockServerPort()signature updatedVerification
npx tsc --noEmitcleannpm run check:skill-syncpasses;docs/toolsdrift-freetest/test-snippets.ts: 39 pass (incl. newfile_namefallback regression test — verified it fails without the fix)test/test-toolset-filtering.ts: 36 pass;test/test-permission-mode.ts: 13 pass;test:schemapasses