@zereight/mcp-gitlab 2.1.58 → 2.1.60

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/schemas.js CHANGED
@@ -119,6 +119,8 @@ export const GitLabPipelineJobSchema = z.object({
119
119
  web_url: z.string().optional(),
120
120
  allow_failure: z.coerce.boolean().optional(),
121
121
  retried: z.coerce.boolean().optional(),
122
+ when: z.string().optional(),
123
+ needs: z.array(z.unknown()).optional(),
122
124
  tag_list: z.array(z.string()).optional(),
123
125
  runner: z
124
126
  .object({
@@ -257,6 +259,38 @@ export const GetPipelineSchema = z.object({
257
259
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
258
260
  pipeline_id: z.coerce.string().describe("The ID of the pipeline"),
259
261
  });
262
+ export const GetPipelineVariablesSchema = GetPipelineSchema.merge(PaginationOptionsSchema);
263
+ export const UpdatePipelineMetadataSchema = GetPipelineSchema.extend({
264
+ name: z.string().min(1).describe("New pipeline name"),
265
+ });
266
+ export const DeletePipelineSchema = GetPipelineSchema;
267
+ export const PipelineReportSchema = GetPipelineSchema.merge(PaginationOptionsSchema);
268
+ export const PlayPipelineJobsSchema = z.object({
269
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
270
+ job_ids: z
271
+ .array(z.coerce.string())
272
+ .min(1)
273
+ .refine((jobIds) => new Set(jobIds).size === jobIds.length, "job_ids must not contain duplicates")
274
+ .describe("Job IDs to play, in dependency order"),
275
+ job_variables_attributes: z
276
+ .array(z.object({ key: z.string(), value: z.string() }))
277
+ .optional()
278
+ .describe("Custom job variables to use when running each job"),
279
+ timeout_seconds: z.coerce
280
+ .number()
281
+ .int()
282
+ .min(1)
283
+ .max(600)
284
+ .optional()
285
+ .describe("Maximum seconds to wait for each job to reach a terminal status (applied per job; total duration scales with the batch size)"),
286
+ poll_interval_seconds: z.coerce
287
+ .number()
288
+ .int()
289
+ .min(1)
290
+ .max(30)
291
+ .optional()
292
+ .describe("Seconds between status polls while waiting for each job"),
293
+ });
260
294
  export const GitLabMergeRequestPipelineSchema = z.object({
261
295
  id: z.coerce.string(),
262
296
  sha: z.string(),
@@ -380,6 +414,28 @@ export const GetCiCatalogResourceSchema = z.union([
380
414
  message: "Provide exactly one of id or full_path",
381
415
  });
382
416
  // Deployment related schemas
417
+ const GitLabDeploymentApprovalUserSchema = z.object({
418
+ id: z.coerce.string().optional(),
419
+ username: z.string().optional(),
420
+ name: z.string().optional(),
421
+ state: z.string().optional(),
422
+ avatar_url: z.string().nullable().optional(),
423
+ web_url: z.string().optional(),
424
+ });
425
+ const GitLabDeploymentApprovalSchema = z.object({
426
+ user: GitLabDeploymentApprovalUserSchema.optional(),
427
+ status: z.string().optional(),
428
+ created_at: z.string().optional(),
429
+ comment: z.string().nullable().optional(),
430
+ });
431
+ const GitLabDeploymentApprovalRuleSchema = z.object({
432
+ user_id: z.coerce.string().nullable().optional(),
433
+ group_id: z.coerce.string().nullable().optional(),
434
+ access_level: z.coerce.number().nullable().optional(),
435
+ access_level_description: z.string().nullable().optional(),
436
+ required_approvals: z.coerce.number().optional(),
437
+ deployment_approvals: z.array(GitLabDeploymentApprovalSchema).optional(),
438
+ });
383
439
  export const GitLabDeploymentSchema = z.object({
384
440
  id: z.coerce.string(),
385
441
  iid: z.coerce.string().optional(),
@@ -427,6 +483,11 @@ export const GitLabDeploymentSchema = z.object({
427
483
  })
428
484
  .optional(),
429
485
  web_url: z.string().optional(),
486
+ pending_approval_count: z.coerce.number().optional(),
487
+ approvals: z.array(GitLabDeploymentApprovalSchema).optional(),
488
+ approval_summary: z
489
+ .object({ rules: z.array(GitLabDeploymentApprovalRuleSchema).optional() })
490
+ .optional(),
430
491
  });
431
492
  export const ListDeploymentsSchema = z
432
493
  .object({
@@ -457,6 +518,19 @@ export const GetDeploymentSchema = z.object({
457
518
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
458
519
  deployment_id: z.coerce.string().describe("The ID of the deployment"),
459
520
  });
521
+ export const CreateDeploymentSchema = z.object({
522
+ project_id: z.coerce.string(), environment: z.string(), sha: z.string(), ref: z.string(),
523
+ tag: z.boolean(), status: z.enum(["running", "success", "failed", "canceled"]),
524
+ });
525
+ export const UpdateDeploymentSchema = GetDeploymentSchema.extend({
526
+ status: z.enum(["running", "success", "failed", "canceled"]),
527
+ });
528
+ export const DeploymentApprovalSchema = GetDeploymentSchema.extend({
529
+ status: z.enum(["approved", "rejected"]),
530
+ comment: z.string().optional(),
531
+ represented_as: z.string().optional(),
532
+ });
533
+ export const ListDeploymentMergeRequestsSchema = GetDeploymentSchema.merge(PaginationOptionsSchema);
460
534
  // Environment related schemas
461
535
  const GitLabEnvironmentLastDeploymentSchema = z.object({
462
536
  id: z.coerce.string().optional(),
@@ -496,6 +570,31 @@ export const GetEnvironmentSchema = z.object({
496
570
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
497
571
  environment_id: z.coerce.string().describe("The ID of the environment"),
498
572
  });
573
+ export const UpdateEnvironmentSchema = GetEnvironmentSchema.extend({
574
+ external_url: z.string().url().nullable().optional(),
575
+ tier: z.enum(["production", "staging", "testing", "development", "other"]).optional(),
576
+ auto_stop_setting: z.enum(["always", "with_action"]).optional(),
577
+ });
578
+ export const StopEnvironmentSchema = GetEnvironmentSchema.extend({ force: coerceBooleanString.optional() });
579
+ export const StopStaleEnvironmentsSchema = z.object({ project_id: z.coerce.string(), before: z.string() });
580
+ export const DeleteReviewAppEnvironmentsSchema = z.object({
581
+ project_id: z.coerce.string(), before: z.string().optional(), limit: z.coerce.number().optional(),
582
+ dry_run: coerceBooleanString
583
+ .optional()
584
+ .describe("Preview the cleanup when true (default); set to false to schedule deletion one week later"),
585
+ });
586
+ export const PipelineTriggerIdSchema = z.object({ project_id: z.coerce.string(), trigger_id: z.coerce.string() });
587
+ export const ListPipelineTriggersSchema = z.object({ project_id: z.coerce.string() });
588
+ export const CreatePipelineTriggerSchema = z.object({ project_id: z.coerce.string(), description: z.string() });
589
+ export const UpdatePipelineTriggerSchema = PipelineTriggerIdSchema.extend({ description: z.string() });
590
+ export const TriggerPipelineSchema = z.object({
591
+ project_id: z.coerce.string(), token: z.string(), ref: z.string(),
592
+ variables: z.record(z.string()).optional(),
593
+ inputs: z
594
+ .record(z.unknown())
595
+ .optional()
596
+ .describe("Structured pipeline inputs; supported from GitLab 17.10 behind a feature flag and generally available from GitLab 18.1. Omit on older GitLab versions."),
597
+ });
499
598
  // Schema for creating a new pipeline
500
599
  export const CreatePipelineSchema = z.object({
501
600
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
@@ -771,15 +870,51 @@ export const PlayPipelineJobSchema = z.object({
771
870
  }))
772
871
  .optional()
773
872
  .describe("Custom job variables to use when running the job"),
873
+ job_inputs: z.record(z.unknown()).optional().describe("Typed job input values"),
774
874
  });
775
875
  // Schema for retrying a job
776
- export const RetryPipelineJobSchema = PipelineJobControlSchema;
876
+ export const RetryPipelineJobSchema = PipelineJobControlSchema.extend({
877
+ job_inputs: z.record(z.unknown()).optional().describe("Typed job input values"),
878
+ });
777
879
  // Schema for canceling a job
778
880
  export const CancelPipelineJobSchema = z.object({
779
881
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
780
882
  job_id: z.coerce.string().describe("The ID of the job"),
781
883
  force: z.coerce.boolean().optional().describe("Force cancellation of the job"),
782
884
  });
885
+ export const ErasePipelineJobSchema = PipelineJobControlSchema;
886
+ export const WaitForPipelineSchema = GetPipelineSchema.extend({
887
+ timeout_seconds: z.coerce
888
+ .number()
889
+ .int()
890
+ .min(1)
891
+ .max(600)
892
+ .optional()
893
+ .describe("Maximum seconds to wait for this pipeline to reach a terminal status"),
894
+ poll_interval_seconds: z.coerce
895
+ .number()
896
+ .int()
897
+ .min(1)
898
+ .max(30)
899
+ .optional()
900
+ .describe("Seconds between status polls while waiting for this pipeline"),
901
+ });
902
+ export const WaitForPipelineJobSchema = PipelineJobControlSchema.extend({
903
+ timeout_seconds: z.coerce
904
+ .number()
905
+ .int()
906
+ .min(1)
907
+ .max(600)
908
+ .optional()
909
+ .describe("Maximum seconds to wait for this job to reach a terminal status"),
910
+ poll_interval_seconds: z.coerce
911
+ .number()
912
+ .int()
913
+ .min(1)
914
+ .max(30)
915
+ .optional()
916
+ .describe("Seconds between status polls while waiting for this job"),
917
+ });
783
918
  // User schemas
784
919
  export const GitLabUserSchema = z.object({
785
920
  username: z.string().optional(), // Changed from login to match GitLab API
@@ -2360,8 +2495,7 @@ export const ListGroupMergeRequestsSchema = ListMergeRequestsSchema.omit({
2360
2495
  project_id: true,
2361
2496
  }).extend({
2362
2497
  group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
2363
- non_archived: z.coerce
2364
- .boolean()
2498
+ non_archived: coerceBooleanString
2365
2499
  .optional()
2366
2500
  .describe("Return merge requests from non-archived projects only. Defaults to true."),
2367
2501
  source_project_id: z.coerce
@@ -4500,3 +4634,23 @@ export const ConfirmVulnerabilitySchema = z.object({
4500
4634
  .describe("The ID of the vulnerability to confirm (numeric or GraphQL global ID)"),
4501
4635
  comment: z.string().optional().describe("Optional comment explaining the confirmation"),
4502
4636
  });
4637
+ // --- GitLab Orbit (knowledge graph) ---
4638
+ // Query the Orbit property graph: groups, projects, MRs, pipelines, issues,
4639
+ // vulnerabilities, and source code indexed as one queryable graph.
4640
+ // Requires Premium/Ultimate; Beta; query calls consume GitLab credits.
4641
+ export const OrbitQuerySchema = z.object({
4642
+ query: z
4643
+ .record(z.string(), z.unknown())
4644
+ .describe("GitLab Orbit query DSL object (see the Orbit query-language reference for node/edge types and operators)"),
4645
+ format: z
4646
+ .enum(["raw", "llm"])
4647
+ .optional()
4648
+ .default("llm")
4649
+ .describe("Response format: 'raw' for structured JSON, 'llm' for compact agent-optimized text (default)"),
4650
+ });
4651
+ // Schema for fetching the current Orbit graph schema (node/edge types)
4652
+ export const OrbitSchemaSchema = z.object({});
4653
+ // Schema for checking Orbit indexing status
4654
+ export const OrbitStatusSchema = z.object({});
4655
+ // Schema for listing the MCP tool definitions Orbit exposes
4656
+ export const OrbitToolsSchema = z.object({});
@@ -22,6 +22,11 @@ const REFERENCE_FIELD_ALLOWLIST = new Set([
22
22
  "new_file",
23
23
  "new_path",
24
24
  "old_path",
25
+ "name",
26
+ "page",
27
+ "per_page",
28
+ "pipeline_id",
29
+ "job_inputs",
25
30
  "project_id",
26
31
  "renamed_file",
27
32
  ]);
@@ -119,6 +119,10 @@ const GROUP_META = {
119
119
  title: "Vulnerabilities",
120
120
  blurb: "AI-assisted vulnerability triage — list findings, inspect details, dismiss with reason, or confirm for remediation. Backed by the GitLab GraphQL API; requires GitLab Ultimate.",
121
121
  },
122
+ orbit: {
123
+ title: "GitLab Orbit",
124
+ blurb: "Query the Orbit SDLC knowledge graph (Beta; Premium/Ultimate). Graph queries consume GitLab credits; schema, status, and tool listing are free.",
125
+ },
122
126
  };
123
127
  const GROUP_ORDER = [
124
128
  "projects",
@@ -141,6 +145,7 @@ const GROUP_ORDER = [
141
145
  "search",
142
146
  "dependency_proxy",
143
147
  "vulnerabilities",
148
+ "orbit",
144
149
  ];
145
150
  // Authoritative classification — uses the `readOnlyTools` set from
146
151
  // tools/registry.ts. That set is what the server itself consults to decide
@@ -62,7 +62,7 @@ async function callTool(toolName, args, env) {
62
62
  env: {
63
63
  ...process.env,
64
64
  ...env,
65
- GITLAB_READ_ONLY_MODE: "true",
65
+ GITLAB_READ_ONLY_MODE: env.GITLAB_READ_ONLY_MODE ?? "true",
66
66
  USE_PIPELINE: "true",
67
67
  },
68
68
  });
@@ -238,8 +238,62 @@ describe("deployment and environment tools", () => {
238
238
  stage: "deploy",
239
239
  pipeline: { id: "190349", status: "success", ref: "master", sha: "abc123" },
240
240
  },
241
+ pending_approval_count: 1,
242
+ approvals: [
243
+ {
244
+ user: {
245
+ id: "100",
246
+ username: "security-user-1",
247
+ name: "Security User",
248
+ state: "active",
249
+ web_url: "https://gitlab.example.com/security-user-1",
250
+ },
251
+ status: "approved",
252
+ created_at: "2026-02-20T16:33:00.000Z",
253
+ comment: "Looks good to me",
254
+ },
255
+ ],
256
+ approval_summary: {
257
+ rules: [
258
+ {
259
+ group_id: "134",
260
+ access_level: null,
261
+ access_level_description: "security-group",
262
+ required_approvals: 2,
263
+ deployment_approvals: [
264
+ {
265
+ user: {
266
+ id: "100",
267
+ username: "security-user-1",
268
+ name: "Security User",
269
+ state: "active",
270
+ web_url: "https://gitlab.example.com/security-user-1",
271
+ },
272
+ status: "approved",
273
+ created_at: "2026-02-20T16:33:00.000Z",
274
+ comment: "Looks good to me",
275
+ },
276
+ ],
277
+ },
278
+ ],
279
+ },
241
280
  });
242
281
  });
282
+ mockGitLab.addMockHandler("post", `/projects/${TEST_PROJECT_ID}/deployments`, (_req, res) => {
283
+ res.status(201).json({ id: "created-deployment", status: "created" });
284
+ });
285
+ mockGitLab.addMockHandler("put", `/projects/${TEST_PROJECT_ID}/deployments/${TEST_DEPLOYMENT_ID}`, (_req, res) => {
286
+ res.json({ id: TEST_DEPLOYMENT_ID, status: "success" });
287
+ });
288
+ mockGitLab.addMockHandler("delete", `/projects/${TEST_PROJECT_ID}/deployments/${TEST_DEPLOYMENT_ID}`, (_req, res) => {
289
+ res.status(204).send();
290
+ });
291
+ mockGitLab.addMockHandler("get", `/projects/${TEST_PROJECT_ID}/deployments/${TEST_DEPLOYMENT_ID}/merge_requests`, (_req, res) => {
292
+ res.json([{ iid: TEST_MERGE_REQUEST_IID, title: "Shipped change" }]);
293
+ });
294
+ mockGitLab.addMockHandler("post", `/projects/${TEST_PROJECT_ID}/deployments/${TEST_DEPLOYMENT_ID}/approval`, (_req, res) => {
295
+ res.json({ id: TEST_DEPLOYMENT_ID, status: "approved" });
296
+ });
243
297
  mockGitLab.addMockHandler("get", `/projects/${TEST_PROJECT_ID}/environments`, (req, res) => {
244
298
  res.json([
245
299
  {
@@ -298,6 +352,35 @@ describe("deployment and environment tools", () => {
298
352
  assert.strictEqual(result.id, TEST_DEPLOYMENT_ID);
299
353
  assert.strictEqual(result.ref, "master");
300
354
  assert.strictEqual(result.status, "success");
355
+ assert.strictEqual(result.pending_approval_count, 1);
356
+ assert.strictEqual(result.approvals[0].status, "approved");
357
+ assert.strictEqual(result.approvals[0].user.username, "security-user-1");
358
+ assert.strictEqual(result.approval_summary.rules[0].group_id, "134");
359
+ assert.strictEqual(result.approval_summary.rules[0].deployment_approvals[0].comment, "Looks good to me");
360
+ });
361
+ test("supports deployment lifecycle and approval operations", async () => {
362
+ const env = {
363
+ GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
364
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
365
+ GITLAB_READ_ONLY_MODE: "false",
366
+ };
367
+ const created = await callTool("create_deployment", {
368
+ project_id: TEST_PROJECT_ID,
369
+ environment: "stage",
370
+ sha: "abc123",
371
+ ref: "master",
372
+ tag: false,
373
+ status: "running",
374
+ }, env);
375
+ assert.strictEqual(created.id, "created-deployment");
376
+ const updated = await callTool("update_deployment", { project_id: TEST_PROJECT_ID, deployment_id: TEST_DEPLOYMENT_ID, status: "success" }, env);
377
+ assert.strictEqual(updated.status, "success");
378
+ const mergeRequests = await callTool("list_deployment_merge_requests", { project_id: TEST_PROJECT_ID, deployment_id: TEST_DEPLOYMENT_ID }, env);
379
+ assert.strictEqual(mergeRequests[0].iid, TEST_MERGE_REQUEST_IID);
380
+ const approval = await callTool("approve_deployment", { project_id: TEST_PROJECT_ID, deployment_id: TEST_DEPLOYMENT_ID, status: "approved" }, env);
381
+ assert.strictEqual(approval.status, "approved");
382
+ const deleted = await callTool("delete_deployment", { project_id: TEST_PROJECT_ID, deployment_id: TEST_DEPLOYMENT_ID }, env);
383
+ assert.deepStrictEqual(deleted, { success: true });
301
384
  });
302
385
  test("list_environments returns environment list", async () => {
303
386
  const result = await callTool("list_environments", { project_id: TEST_PROJECT_ID }, {
@@ -0,0 +1,155 @@
1
+ import { describe, test, before, after } from "node:test";
2
+ import assert from "node:assert";
3
+ import { spawn } from "child_process";
4
+ import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";
5
+ const MOCK_TOKEN = "glpat-mock-token-12345";
6
+ const mockSchema = {
7
+ nodes: [{ name: "Project", properties: ["id", "name"] }],
8
+ edges: [{ name: "HAS_MERGE_REQUEST", from: "Project", to: "MergeRequest" }],
9
+ };
10
+ const mockStatus = {
11
+ status: "ready",
12
+ last_indexed_at: "2026-09-05T10:00:00Z",
13
+ indexed_groups: 3,
14
+ };
15
+ const mockTools = {
16
+ tools: [{ name: "orbit_query", description: "Execute a graph query" }],
17
+ };
18
+ const mockQueryRaw = {
19
+ results: [{ project: "group/a", failures: 7 }],
20
+ };
21
+ const mockQueryLlm = "Top failing project: group/a (7 failures).";
22
+ function baseEnv(mockGitLabUrl) {
23
+ return {
24
+ GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
25
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
26
+ GITLAB_TOOLSETS: "orbit",
27
+ };
28
+ }
29
+ async function callToolAsync(env, name, args) {
30
+ return new Promise((resolve, reject) => {
31
+ const proc = spawn("node", ["build/index.js"], {
32
+ stdio: ["pipe", "pipe", "pipe"],
33
+ env: { ...process.env, ...env },
34
+ });
35
+ let output = "";
36
+ let errorOutput = "";
37
+ proc.stdout?.on("data", (chunk) => (output += chunk));
38
+ proc.stderr?.on("data", (chunk) => (errorOutput += chunk));
39
+ proc.on("close", code => {
40
+ if (code !== 0) {
41
+ return reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
42
+ }
43
+ const line = output.split("\n").find(entry => entry.startsWith("{"));
44
+ if (!line) {
45
+ return reject(new Error("No JSON output found"));
46
+ }
47
+ try {
48
+ const response = JSON.parse(line);
49
+ if (response.error) {
50
+ reject(new Error(JSON.stringify(response.error)));
51
+ return;
52
+ }
53
+ const content = response.result?.content?.[0]?.text;
54
+ if (content === undefined) {
55
+ reject(new Error("No tool result content"));
56
+ return;
57
+ }
58
+ resolve(content);
59
+ }
60
+ catch (error) {
61
+ reject(error);
62
+ }
63
+ });
64
+ proc.stdin?.end(JSON.stringify({
65
+ jsonrpc: "2.0",
66
+ id: 1,
67
+ method: "tools/call",
68
+ params: { name, arguments: args },
69
+ }) + "\n");
70
+ });
71
+ }
72
+ describe("orbit tools", () => {
73
+ let mockGitLab;
74
+ let mockGitLabUrl;
75
+ let lastQueryBody = null;
76
+ before(async () => {
77
+ const mockPort = await findMockServerPort();
78
+ mockGitLab = new MockGitLabServer({
79
+ port: mockPort,
80
+ validTokens: [MOCK_TOKEN],
81
+ });
82
+ mockGitLab.addMockHandler("get", "/orbit/schema", (_req, res) => {
83
+ res.json(mockSchema);
84
+ });
85
+ mockGitLab.addMockHandler("get", "/orbit/status", (_req, res) => {
86
+ res.json(mockStatus);
87
+ });
88
+ mockGitLab.addMockHandler("get", "/orbit/tools", (_req, res) => {
89
+ res.json(mockTools);
90
+ });
91
+ mockGitLab.addMockHandler("post", "/orbit/query", (req, res) => {
92
+ lastQueryBody = req.body;
93
+ if (req.body?.format === "raw") {
94
+ res.json(mockQueryRaw);
95
+ return;
96
+ }
97
+ res.type("text/plain").send(mockQueryLlm);
98
+ });
99
+ await mockGitLab.start();
100
+ mockGitLabUrl = mockGitLab.getUrl();
101
+ });
102
+ after(async () => {
103
+ await mockGitLab.stop();
104
+ });
105
+ test("orbit_get_schema returns the graph schema", async () => {
106
+ const text = await callToolAsync(baseEnv(mockGitLabUrl), "orbit_get_schema", {});
107
+ assert.deepEqual(JSON.parse(text), mockSchema);
108
+ });
109
+ test("orbit_get_status returns indexing status", async () => {
110
+ const text = await callToolAsync(baseEnv(mockGitLabUrl), "orbit_get_status", {});
111
+ assert.deepEqual(JSON.parse(text), mockStatus);
112
+ });
113
+ test("orbit_list_tools returns tool definitions", async () => {
114
+ const text = await callToolAsync(baseEnv(mockGitLabUrl), "orbit_list_tools", {});
115
+ assert.deepEqual(JSON.parse(text), mockTools);
116
+ });
117
+ test("orbit_query with format=raw returns structured JSON", async () => {
118
+ const query = { node: "Project", limit: 5 };
119
+ const text = await callToolAsync(baseEnv(mockGitLabUrl), "orbit_query", {
120
+ query,
121
+ format: "raw",
122
+ });
123
+ assert.deepEqual(JSON.parse(text), mockQueryRaw);
124
+ assert.deepEqual(lastQueryBody, { query, format: "raw" });
125
+ });
126
+ test("orbit_query defaults to llm format and passes text through", async () => {
127
+ const query = { node: "Project", limit: 5 };
128
+ const text = await callToolAsync(baseEnv(mockGitLabUrl), "orbit_query", { query });
129
+ assert.equal(text, mockQueryLlm);
130
+ assert.deepEqual(lastQueryBody, { query, format: "llm" });
131
+ });
132
+ for (const toolName of [
133
+ "orbit_query",
134
+ "orbit_get_schema",
135
+ "orbit_get_status",
136
+ "orbit_list_tools",
137
+ ]) {
138
+ test(`${toolName} is refused under strict project scope`, async () => {
139
+ const env = {
140
+ ...baseEnv(mockGitLabUrl),
141
+ ENABLE_STRICT_PROJECT_SCOPE: "true",
142
+ GITLAB_ALLOWED_PROJECT_IDS: "1,2",
143
+ };
144
+ const args = toolName === "orbit_query" ? { query: {} } : {};
145
+ let message = "";
146
+ try {
147
+ message = await callToolAsync(env, toolName, args);
148
+ }
149
+ catch (error) {
150
+ message = error.message;
151
+ }
152
+ assert.match(message, /not allowed while a project allowlist is in effect/);
153
+ });
154
+ }
155
+ });
@@ -67,6 +67,7 @@ const MODIFY_SAMPLE_TOOLS = [
67
67
  "create_branch",
68
68
  "push_files",
69
69
  "merge_merge_request",
70
+ "stop_stale_environments",
70
71
  ];
71
72
  const READ_SAMPLE_TOOLS = ["list_issues", "get_project", "list_merge_requests"];
72
73
  describe("Permission Mode", { concurrency: 1 }, () => {
@@ -23,7 +23,7 @@ const TOOLSET_TOOL_COUNTS = {
23
23
  projects: 11,
24
24
  labels: 5,
25
25
  ci: 4,
26
- pipelines: 31,
26
+ pipelines: 56,
27
27
  milestones: 17,
28
28
  wiki: 10,
29
29
  releases: 7,
@@ -36,6 +36,7 @@ const TOOLSET_TOOL_COUNTS = {
36
36
  variables: 10,
37
37
  dependency_proxy: 4,
38
38
  vulnerabilities: 4,
39
+ orbit: 4,
39
40
  };
40
41
  const LEGACY_PIPELINE_CI_TOOL_COUNT = 2;
41
42
  const LEGACY_PIPELINE_TOOL_COUNT = TOOLSET_TOOL_COUNTS.pipelines + LEGACY_PIPELINE_CI_TOOL_COUNT;
@@ -62,6 +63,7 @@ const NON_DEFAULT_TOOLSETS = [
62
63
  "variables",
63
64
  "dependency_proxy",
64
65
  "vulnerabilities",
66
+ "orbit",
65
67
  ];
66
68
  // discover_tools meta-tool is always force-injected (Step 5.5)
67
69
  const DISCOVER_TOOLS_COUNT = 1;