@zereight/mcp-gitlab 2.1.66 → 2.1.68

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/build/index.js CHANGED
@@ -114,7 +114,7 @@ GitLabDiscussionNoteSchema, // Added
114
114
  GitLabDiscussionSchema,
115
115
  // Draft Notes Schemas
116
116
  GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabProtectedBranchSchema, GitLabGroupSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestPipelineSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabGroupMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineScheduleSchema, GitLabPipelineScheduleVariableSchema, GitLabPipelineSchedulePlayResultSchema, ListPipelineSchedulesSchema, GetPipelineScheduleSchema, ListPipelineSchedulePipelinesSchema, CreatePipelineScheduleSchema, UpdatePipelineScheduleSchema, DeletePipelineScheduleSchema, PlayPipelineScheduleSchema, TakeOwnershipPipelineScheduleSchema, GetPipelineScheduleVariableSchema, CreatePipelineScheduleVariableSchema, UpdatePipelineScheduleVariableSchema, DeletePipelineScheduleVariableSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabTodoSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitStatusesSchema, ListBranchesSchema, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, GitLabCiVariableSchema, ListProjectVariablesSchema, GetProjectVariableSchema, CreateProjectVariableSchema, UpdateProjectVariableSchema, DeleteProjectVariableSchema, ListGroupVariablesSchema, GetGroupVariableSchema, CreateGroupVariableSchema, UpdateGroupVariableSchema, DeleteGroupVariableSchema, GitLabDependencyProxySchema, GitLabDependencyProxyBlobSchema, GetDependencyProxySettingsSchema, UpdateDependencyProxySettingsSchema, ListDependencyProxyBlobsSchema, PurgeDependencyProxyCacheSchema, ListProjectVulnerabilitiesSchema, GetVulnerabilitySchema, DismissVulnerabilitySchema, ConfirmVulnerabilitySchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
117
- GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListGroupMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListProjectMembersSchema, ListGroupMembersSchema, ListProjectMilestonesSchema, ListGroupMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, OrbitQuerySchema, OrbitSchemaSchema, OrbitStatusSchema, OrbitToolsSchema, MarkAllTodosDoneSchema, MarkTodoDoneSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateProjectSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, GetMergeRequestDiscussionSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, CreateWebhookSchema, UpdateWebhookSchema, DeleteWebhookSchema, ListWebhookEventsSchema, GetWebhookEventSchema, HealthCheckSchema, } from "./schemas.js";
117
+ GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListGroupMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListProjectMembersSchema, ListGroupMembersSchema, ListProjectMilestonesSchema, ListGroupMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, OrbitQuerySchema, OrbitSchemaSchema, OrbitStatusSchema, OrbitToolsSchema, MarkAllTodosDoneSchema, MarkTodoDoneSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateProjectSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, ListSnippetsSchema, GetSnippetSchema, CreateSnippetSchema, UpdateSnippetSchema, DeleteSnippetSchema, GitLabSnippetSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, GetMergeRequestDiscussionSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, CreateWebhookSchema, UpdateWebhookSchema, DeleteWebhookSchema, ListWebhookEventsSchema, GetWebhookEventSchema, HealthCheckSchema, } from "./schemas.js";
118
118
  import { randomUUID, timingSafeEqual, } from "node:crypto";
119
119
  import { createLogger } from "./utils/logger.js";
120
120
  const logger = createLogger();
@@ -7605,6 +7605,163 @@ async function executeGitLabGraphQL(query, variables = {}) {
7605
7605
  clearTimeout(timeout);
7606
7606
  }
7607
7607
  }
7608
+ /**
7609
+ * Build the snippets endpoint URL.
7610
+ *
7611
+ * When projectId is provided, or when a project allowlist is in effect (env
7612
+ * GITLAB_PROJECT_ID / GITLAB_ALLOWED_PROJECT_IDS or a session scope narrowed via
7613
+ * X-GitLab-Allowed-Project-Ids), returns the project snippets endpoint — routing
7614
+ * through getEffectiveProjectId so the effective scope is enforced. Only falls
7615
+ * back to the personal /snippets endpoint when no scope is active at all.
7616
+ */
7617
+ function getSnippetsEndpoint(projectId) {
7618
+ const scopeActive = getEffectiveAllowedProjectIds().length > 0 || !!GITLAB_PROJECT_ID;
7619
+ if (projectId || scopeActive) {
7620
+ const decoded = projectId ? decodeURIComponent(projectId) : "";
7621
+ const effectiveProjectId = getEffectiveProjectId(decoded);
7622
+ return `${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/snippets`;
7623
+ }
7624
+ return `${getEffectiveApiUrl()}/snippets`;
7625
+ }
7626
+ /**
7627
+ * List snippets — project snippets if projectId is given, otherwise personal snippets.
7628
+ */
7629
+ async function listSnippets(projectId, options = {}) {
7630
+ const url = new URL(getSnippetsEndpoint(projectId));
7631
+ Object.entries(options).forEach(([key, value]) => {
7632
+ if (value !== undefined) {
7633
+ url.searchParams.append(key, String(value));
7634
+ }
7635
+ });
7636
+ const response = await fetch(url.toString(), {
7637
+ ...getFetchConfig(),
7638
+ });
7639
+ await handleGitLabError(response);
7640
+ const data = await response.json();
7641
+ return GitLabSnippetSchema.array().parse(data);
7642
+ }
7643
+ /**
7644
+ * Get a snippet's metadata. Use getSnippetRawContent to fetch the raw file content.
7645
+ */
7646
+ async function getSnippet(projectId, snippetId) {
7647
+ const response = await fetch(`${getSnippetsEndpoint(projectId)}/${snippetId}`, {
7648
+ ...getFetchConfig(),
7649
+ });
7650
+ await handleGitLabError(response);
7651
+ const data = await response.json();
7652
+ return GitLabSnippetSchema.parse(data);
7653
+ }
7654
+ /**
7655
+ * Get the raw content of a single-file snippet via the `/raw` endpoint.
7656
+ * For multi-file snippets, use getSnippetFileRawContent instead.
7657
+ */
7658
+ async function getSnippetRawContent(projectId, snippetId) {
7659
+ const response = await fetch(`${getSnippetsEndpoint(projectId)}/${snippetId}/raw`, {
7660
+ ...getFetchConfig(),
7661
+ });
7662
+ await handleGitLabError(response);
7663
+ return await response.text();
7664
+ }
7665
+ /**
7666
+ * Extract the ref (branch/tag/commit) from a snippet file's raw_url.
7667
+ * Anchors on /snippets/{id}/raw/ so branch names or file paths containing
7668
+ * the word "raw" don't produce a false match.
7669
+ */
7670
+ function extractSnippetRef(rawUrl, snippetId, filePath) {
7671
+ const rawMarker = `/snippets/${snippetId}/raw/`;
7672
+ const decoded = decodeURIComponent(new URL(rawUrl).pathname);
7673
+ const markerIdx = decoded.indexOf(rawMarker);
7674
+ if (markerIdx === -1) {
7675
+ throw new Error(`Cannot extract ref from snippet file raw_url: ${rawUrl}`);
7676
+ }
7677
+ const afterRaw = decoded.slice(markerIdx + rawMarker.length); // "{ref}/{filePath}"
7678
+ const fileStart = afterRaw.lastIndexOf("/" + filePath);
7679
+ if (fileStart === -1) {
7680
+ throw new Error(`Cannot locate file path "${filePath}" in snippet file raw_url: ${rawUrl}`);
7681
+ }
7682
+ return afterRaw.slice(0, fileStart);
7683
+ }
7684
+ /**
7685
+ * Fetch the raw content of one file inside a multi-file snippet.
7686
+ * Accepts an explicit ref (branch/tag/commit) — callers resolve it via
7687
+ * extractSnippetRef or a user-supplied parameter before calling this.
7688
+ */
7689
+ async function getSnippetFileRawContent(projectId, snippetId, ref, filePath) {
7690
+ const encodedRef = encodeGitLabPathSegment(ref);
7691
+ const encodedPath = encodeGitLabPathSegment(filePath);
7692
+ const url = `${getSnippetsEndpoint(projectId)}/${snippetId}/files/${encodedRef}/${encodedPath}/raw`;
7693
+ const response = await fetch(url, { ...getFetchConfig() });
7694
+ await handleGitLabError(response);
7695
+ return await response.text();
7696
+ }
7697
+ function snippetCreateFilesPayload(options) {
7698
+ if (options.files !== undefined && options.files.length > 0) {
7699
+ return options.files;
7700
+ }
7701
+ if (options.file_name !== undefined && options.content !== undefined) {
7702
+ return [{ file_path: options.file_name, content: options.content }];
7703
+ }
7704
+ throw new Error("Provide either files[] (multi-file) or both file_name and content (single-file)");
7705
+ }
7706
+ /**
7707
+ * Create a snippet — project-scoped if projectId is given, otherwise personal.
7708
+ */
7709
+ async function createSnippet(projectId, options) {
7710
+ const { title, description, visibility } = options;
7711
+ const body = {
7712
+ title,
7713
+ visibility: visibility ?? "private",
7714
+ files: snippetCreateFilesPayload(options),
7715
+ };
7716
+ if (description !== undefined) {
7717
+ body.description = description;
7718
+ }
7719
+ const response = await fetch(getSnippetsEndpoint(projectId), {
7720
+ ...getFetchConfig(),
7721
+ method: "POST",
7722
+ body: JSON.stringify(body),
7723
+ });
7724
+ await handleGitLabError(response);
7725
+ const data = await response.json();
7726
+ return GitLabSnippetSchema.parse(data);
7727
+ }
7728
+ /**
7729
+ * Update an existing snippet — project-scoped if projectId is given, otherwise personal.
7730
+ */
7731
+ async function updateSnippet(projectId, options) {
7732
+ const { snippet_id, title, file_name, content, files, description, visibility } = options;
7733
+ const body = {};
7734
+ if (title !== undefined)
7735
+ body.title = title;
7736
+ if (description !== undefined)
7737
+ body.description = description;
7738
+ if (visibility !== undefined)
7739
+ body.visibility = visibility;
7740
+ if (files !== undefined) {
7741
+ body.files = files;
7742
+ }
7743
+ else if (file_name !== undefined && content !== undefined) {
7744
+ body.files = [{ action: "update", file_path: file_name, content }];
7745
+ }
7746
+ const response = await fetch(`${getSnippetsEndpoint(projectId)}/${snippet_id}`, {
7747
+ ...getFetchConfig(),
7748
+ method: "PUT",
7749
+ body: JSON.stringify(body),
7750
+ });
7751
+ await handleGitLabError(response);
7752
+ const data = await response.json();
7753
+ return GitLabSnippetSchema.parse(data);
7754
+ }
7755
+ /**
7756
+ * Delete a snippet — project-scoped if projectId is given, otherwise personal.
7757
+ */
7758
+ async function deleteSnippet(projectId, snippetId) {
7759
+ const response = await fetch(`${getSnippetsEndpoint(projectId)}/${snippetId}`, {
7760
+ ...getFetchConfig(),
7761
+ method: "DELETE",
7762
+ });
7763
+ await handleGitLabError(response);
7764
+ }
7608
7765
  // Request handlers are now registered inside createServer() factory function
7609
7766
  // to ensure each transport connection gets its own Server instance (GHSA-345p-7cg4-v4c7).
7610
7767
  async function handleToolCall(params) {
@@ -10129,6 +10286,82 @@ async function handleToolCall(params) {
10129
10286
  content: [{ type: "text", text: JSON.stringify(signature) }],
10130
10287
  };
10131
10288
  }
10289
+ case "list_snippets": {
10290
+ const args = ListSnippetsSchema.parse(params.arguments);
10291
+ const { project_id, ...options } = args;
10292
+ const snippets = await listSnippets(project_id, options);
10293
+ return {
10294
+ content: [{ type: "text", text: JSON.stringify(snippets, null, 2) }],
10295
+ };
10296
+ }
10297
+ case "get_snippet": {
10298
+ const args = GetSnippetSchema.parse(params.arguments);
10299
+ const snippet = await getSnippet(args.project_id, args.snippet_id);
10300
+ const result = { ...snippet };
10301
+ if (args.include_content) {
10302
+ const files = snippet.files ?? [];
10303
+ if (files.length > 1) {
10304
+ const firstFile = files[0];
10305
+ let ref;
10306
+ if (args.ref !== undefined) {
10307
+ ref = args.ref;
10308
+ }
10309
+ else {
10310
+ if (!firstFile.raw_url)
10311
+ throw new Error(`Snippet file "${firstFile.path}" has no raw_url`);
10312
+ ref = extractSnippetRef(firstFile.raw_url, args.snippet_id, firstFile.path);
10313
+ }
10314
+ result.files = await Promise.all(files.map(async (f) => ({
10315
+ ...f,
10316
+ content: await getSnippetFileRawContent(args.project_id, args.snippet_id, ref, f.path),
10317
+ })));
10318
+ }
10319
+ else {
10320
+ const file = files[0];
10321
+ const filePath = file?.path ?? snippet.file_name ?? undefined;
10322
+ if (args.ref !== undefined && filePath) {
10323
+ result.content = await getSnippetFileRawContent(args.project_id, args.snippet_id, args.ref, filePath);
10324
+ }
10325
+ else {
10326
+ result.content = await getSnippetRawContent(args.project_id, args.snippet_id);
10327
+ }
10328
+ }
10329
+ }
10330
+ return {
10331
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
10332
+ };
10333
+ }
10334
+ case "create_snippet": {
10335
+ const args = CreateSnippetSchema.parse(params.arguments);
10336
+ const { project_id, ...options } = args;
10337
+ const snippet = await createSnippet(project_id, options);
10338
+ return {
10339
+ content: [{ type: "text", text: JSON.stringify(snippet, null, 2) }],
10340
+ };
10341
+ }
10342
+ case "update_snippet": {
10343
+ const args = UpdateSnippetSchema.parse(params.arguments);
10344
+ const { project_id, ...options } = args;
10345
+ const snippet = await updateSnippet(project_id, options);
10346
+ return {
10347
+ content: [{ type: "text", text: JSON.stringify(snippet, null, 2) }],
10348
+ };
10349
+ }
10350
+ case "delete_snippet": {
10351
+ const args = DeleteSnippetSchema.parse(params.arguments);
10352
+ await deleteSnippet(args.project_id, args.snippet_id);
10353
+ return {
10354
+ content: [
10355
+ {
10356
+ type: "text",
10357
+ text: JSON.stringify({
10358
+ status: "success",
10359
+ message: `Snippet ${args.snippet_id} deleted successfully`,
10360
+ }, null, 2),
10361
+ },
10362
+ ],
10363
+ };
10364
+ }
10132
10365
  case "list_webhooks": {
10133
10366
  const args = ListWebhooksSchema.parse(params.arguments);
10134
10367
  const webhooks = await listWebhooks(args);
package/build/schemas.js CHANGED
@@ -1355,6 +1355,9 @@ export const GitLabDiffSchema = z.object({
1355
1355
  new_file: z.coerce.boolean(),
1356
1356
  renamed_file: z.coerce.boolean(),
1357
1357
  deleted_file: z.coerce.boolean(),
1358
+ generated_file: z.coerce.boolean().optional(),
1359
+ too_large: z.coerce.boolean().optional(),
1360
+ collapsed: z.coerce.boolean().optional(),
1358
1361
  });
1359
1362
  // Response schemas for operations
1360
1363
  export const GitLabCreateUpdateFileResponseSchema = z.object({
@@ -3844,6 +3847,193 @@ export const GitLabTagSchema = z.object({
3844
3847
  protected: z.boolean(),
3845
3848
  created_at: z.string().nullable(),
3846
3849
  });
3850
+ // --- Snippet schemas ---
3851
+ export const ListSnippetsSchema = z
3852
+ .object({
3853
+ project_id: z
3854
+ .coerce.string()
3855
+ .optional()
3856
+ .describe("Project ID or URL-encoded path. Omit for personal snippets, or the configured project when GITLAB_PROJECT_ID / GITLAB_ALLOWED_PROJECT_IDS is set."),
3857
+ })
3858
+ .merge(PaginationOptionsSchema);
3859
+ export const GetSnippetSchema = z.object({
3860
+ project_id: z
3861
+ .coerce.string()
3862
+ .optional()
3863
+ .describe("Project ID or URL-encoded path. Omit for personal snippets, or the configured project when GITLAB_PROJECT_ID / GITLAB_ALLOWED_PROJECT_IDS is set."),
3864
+ snippet_id: z.coerce.number().describe("The snippet ID"),
3865
+ include_content: coerceBooleanString
3866
+ .optional()
3867
+ .default(false)
3868
+ .describe("Whether to fetch the raw file content (default: false)"),
3869
+ ref: z
3870
+ .string()
3871
+ .optional()
3872
+ .describe("Branch, tag, or commit to fetch content from. Inferred from the snippet's raw_url when omitted."),
3873
+ });
3874
+ export const SnippetFileSchema = z.object({
3875
+ file_path: z.string().describe("File path within the snippet (e.g., 'src/script.py')"),
3876
+ content: z.string().describe("File content"),
3877
+ });
3878
+ export const SnippetFileUpdateActionSchema = z
3879
+ .object({
3880
+ action: z
3881
+ .enum(["create", "update", "delete", "move"])
3882
+ .describe("Type of action to perform on the file"),
3883
+ file_path: z
3884
+ .string()
3885
+ .optional()
3886
+ .describe("File path of the snippet file (required for all actions). For 'move', this is the new path; for 'create'/'update'/'delete', the target path."),
3887
+ previous_path: z
3888
+ .string()
3889
+ .optional()
3890
+ .describe("Previous path of the snippet file (required for 'move')."),
3891
+ content: z
3892
+ .string()
3893
+ .optional()
3894
+ .describe("Content of the snippet file (required for 'create' and 'update')."),
3895
+ })
3896
+ .superRefine((data, ctx) => {
3897
+ if (data.file_path === undefined) {
3898
+ ctx.addIssue({
3899
+ code: z.ZodIssueCode.custom,
3900
+ message: "file_path is required for all snippet file actions",
3901
+ path: ["file_path"],
3902
+ });
3903
+ }
3904
+ if ((data.action === "create" || data.action === "update") && data.content === undefined) {
3905
+ ctx.addIssue({
3906
+ code: z.ZodIssueCode.custom,
3907
+ message: `content is required for action: '${data.action}'`,
3908
+ path: ["content"],
3909
+ });
3910
+ }
3911
+ if (data.action === "move" && data.previous_path === undefined) {
3912
+ ctx.addIssue({
3913
+ code: z.ZodIssueCode.custom,
3914
+ message: "previous_path is required for action: 'move'",
3915
+ path: ["previous_path"],
3916
+ });
3917
+ }
3918
+ })
3919
+ .describe("Multi-file snippet update action. Mirrors GitLab's PUT /snippets/:id files[] payload.");
3920
+ export const CreateSnippetSchema = z
3921
+ .object({
3922
+ project_id: z
3923
+ .coerce.string()
3924
+ .optional()
3925
+ .describe("Project ID or URL-encoded path. Omit for a personal snippet, or the configured project when GITLAB_PROJECT_ID / GITLAB_ALLOWED_PROJECT_IDS is set."),
3926
+ title: z.string().describe("Snippet title"),
3927
+ file_name: z
3928
+ .string()
3929
+ .optional()
3930
+ .describe("File name for a single-file snippet (e.g., 'script.py'). Use together with content. Mutually exclusive with files[]."),
3931
+ content: z
3932
+ .string()
3933
+ .optional()
3934
+ .describe("File content for a single-file snippet. Use together with file_name."),
3935
+ files: z
3936
+ .array(SnippetFileSchema)
3937
+ .optional()
3938
+ .describe("Array of files for a multi-file snippet. Each item needs file_path and content. Mutually exclusive with file_name/content."),
3939
+ description: z.string().optional().describe("Optional snippet description"),
3940
+ visibility: z
3941
+ .enum(["private", "internal", "public"])
3942
+ .optional()
3943
+ .describe("Snippet visibility (default: private)"),
3944
+ })
3945
+ .refine(data => (data.files !== undefined && data.files.length > 0) ||
3946
+ (data.file_name !== undefined && data.content !== undefined), {
3947
+ message: "Provide either files[] (multi-file) or both file_name and content (single-file)",
3948
+ path: ["files"],
3949
+ })
3950
+ .refine(data => !(data.files !== undefined &&
3951
+ (data.file_name !== undefined || data.content !== undefined)), {
3952
+ message: "Cannot mix files[] with file_name/content — pick one shape",
3953
+ path: ["files"],
3954
+ });
3955
+ export const UpdateSnippetSchema = z
3956
+ .object({
3957
+ project_id: z
3958
+ .coerce.string()
3959
+ .optional()
3960
+ .describe("Project ID or URL-encoded path. Omit for personal snippets, or the configured project when GITLAB_PROJECT_ID / GITLAB_ALLOWED_PROJECT_IDS is set."),
3961
+ snippet_id: z.coerce.number().describe("The snippet ID to update"),
3962
+ title: z.string().optional().describe("New title"),
3963
+ file_name: z
3964
+ .string()
3965
+ .optional()
3966
+ .describe("File name to update. Provide together with content to replace a single-file snippet's contents."),
3967
+ content: z.string().optional().describe("New file content (requires file_name)"),
3968
+ files: z
3969
+ .array(SnippetFileUpdateActionSchema)
3970
+ .min(1, "files[] must be non-empty when provided")
3971
+ .optional()
3972
+ .describe("Multi-file update actions. Each item has 'action' (create/update/delete/move) plus the relevant path/content fields. Use this for renames (action: 'move' with previous_path), deletions, and additions. Mutually exclusive with file_name/content."),
3973
+ description: z.string().optional().describe("New description"),
3974
+ visibility: z
3975
+ .enum(["private", "internal", "public"])
3976
+ .optional()
3977
+ .describe("New visibility"),
3978
+ })
3979
+ .refine(data => data.title !== undefined ||
3980
+ data.content !== undefined ||
3981
+ data.file_name !== undefined ||
3982
+ data.files !== undefined ||
3983
+ data.description !== undefined ||
3984
+ data.visibility !== undefined, { message: "At least one field must be provided to update" })
3985
+ .refine(data => !(data.files !== undefined &&
3986
+ (data.file_name !== undefined || data.content !== undefined)), {
3987
+ message: "Cannot mix files[] with file_name/content — pick one shape",
3988
+ path: ["files"],
3989
+ })
3990
+ .refine(data => !(data.content !== undefined && data.file_name === undefined), {
3991
+ message: "content requires file_name",
3992
+ path: ["content"],
3993
+ })
3994
+ .refine(data => !(data.file_name !== undefined && data.content === undefined), {
3995
+ message: "file_name requires content — renaming a file alone is not supported (the modern GitLab snippet API requires a files[] action with previous_path)",
3996
+ path: ["file_name"],
3997
+ });
3998
+ export const DeleteSnippetSchema = z.object({
3999
+ project_id: z
4000
+ .coerce.string()
4001
+ .optional()
4002
+ .describe("Project ID or URL-encoded path. Omit for personal snippets, or the configured project when GITLAB_PROJECT_ID / GITLAB_ALLOWED_PROJECT_IDS is set."),
4003
+ snippet_id: z.coerce.number().describe("The snippet ID to delete"),
4004
+ });
4005
+ export const GitLabSnippetAuthorSchema = z
4006
+ .object({
4007
+ id: z.number().optional(),
4008
+ username: z.string().optional(),
4009
+ name: z.string().optional(),
4010
+ state: z.string().optional(),
4011
+ avatar_url: z.string().nullable().optional(),
4012
+ web_url: z.string().optional(),
4013
+ })
4014
+ .passthrough();
4015
+ export const GitLabSnippetFileSchema = z
4016
+ .object({
4017
+ path: z.string(),
4018
+ raw_url: z.string().optional(),
4019
+ })
4020
+ .passthrough();
4021
+ export const GitLabSnippetSchema = z
4022
+ .object({
4023
+ id: z.number(),
4024
+ title: z.string(),
4025
+ description: z.string().nullable().optional(),
4026
+ visibility: z.string().optional(),
4027
+ author: GitLabSnippetAuthorSchema.optional(),
4028
+ updated_at: z.string().optional(),
4029
+ created_at: z.string().optional(),
4030
+ web_url: z.string().optional(),
4031
+ raw_url: z.string().optional(),
4032
+ file_name: z.string().nullable().optional(),
4033
+ files: z.array(GitLabSnippetFileSchema).optional(),
4034
+ project_id: z.number().nullable().optional(),
4035
+ })
4036
+ .passthrough();
3847
4037
  export const GitLabTagSignatureSchema = z.object({
3848
4038
  signature_type: z.literal("X509"),
3849
4039
  verification_status: z.string(),
@@ -123,6 +123,10 @@ const GROUP_META = {
123
123
  title: "GitLab Orbit",
124
124
  blurb: "Query the Orbit SDLC knowledge graph (Beta; Premium/Ultimate). Graph queries consume GitLab credits; schema, status, and tool listing are free.",
125
125
  },
126
+ snippets: {
127
+ title: "Snippets",
128
+ blurb: "Snippet CRUD — list, get (with optional file content), create, update, and delete personal or project snippets.",
129
+ },
126
130
  };
127
131
  const GROUP_ORDER = [
128
132
  "projects",
@@ -146,6 +150,7 @@ const GROUP_ORDER = [
146
150
  "dependency_proxy",
147
151
  "vulnerabilities",
148
152
  "orbit",
153
+ "snippets",
149
154
  ];
150
155
  // Authoritative classification — uses the `readOnlyTools` set from
151
156
  // tools/registry.ts. That set is what the server itself consults to decide
@@ -50,6 +50,7 @@ export const GROUP_ORDER = [
50
50
  "dependency_proxy",
51
51
  "vulnerabilities",
52
52
  "orbit",
53
+ "snippets",
53
54
  ];
54
55
  export const GROUP_TITLE = {
55
56
  merge_requests: "Merge Requests",
@@ -73,6 +74,7 @@ export const GROUP_TITLE = {
73
74
  dependency_proxy: "Dependency Proxy",
74
75
  vulnerabilities: "Vulnerabilities",
75
76
  orbit: "GitLab Orbit",
77
+ snippets: "Snippets",
76
78
  meta: "Meta & GraphQL",
77
79
  };
78
80
  export const CANARY_INVOKED = [
@@ -0,0 +1,36 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { GitLabDiffSchema } from "../schemas.js";
4
+ test("diff schema keeps GitLab's generated_file, too_large and collapsed flags", () => {
5
+ const parsed = GitLabDiffSchema.parse({
6
+ diff: "@@ -1 +1 @@",
7
+ new_path: "src/Foo.php",
8
+ old_path: "src/Foo.php",
9
+ a_mode: "100644",
10
+ b_mode: "100644",
11
+ new_file: false,
12
+ renamed_file: false,
13
+ deleted_file: false,
14
+ generated_file: true,
15
+ too_large: true,
16
+ collapsed: false,
17
+ });
18
+ assert.equal(parsed.generated_file, true);
19
+ assert.equal(parsed.too_large, true);
20
+ assert.equal(parsed.collapsed, false);
21
+ });
22
+ test("diff schema still parses entries without the optional flags", () => {
23
+ const parsed = GitLabDiffSchema.parse({
24
+ diff: "@@ -1 +1 @@",
25
+ new_path: "src/Foo.php",
26
+ old_path: "src/Foo.php",
27
+ a_mode: "100644",
28
+ b_mode: "100644",
29
+ new_file: false,
30
+ renamed_file: false,
31
+ deleted_file: false,
32
+ });
33
+ assert.equal("generated_file" in parsed, false);
34
+ assert.equal("too_large" in parsed, false);
35
+ assert.equal("collapsed" in parsed, false);
36
+ });