opencode-gitlab-plugin 3.0.1 → 3.2.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/CHANGELOG.md +14 -0
- package/README.md +14 -5
- package/dist/index.js +142 -13
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +143 -14
- package/dist/mcp-server.cjs.map +1 -1
- package/package.json +1 -1
package/dist/mcp-server.cjs
CHANGED
|
@@ -29275,9 +29275,22 @@ var MergeRequestsClient = class _MergeRequestsClient extends GitLabApiClient {
|
|
|
29275
29275
|
requestBody
|
|
29276
29276
|
);
|
|
29277
29277
|
}
|
|
29278
|
-
|
|
29278
|
+
/**
|
|
29279
|
+
* Create a note on a merge request.
|
|
29280
|
+
*
|
|
29281
|
+
* When `discussionId` is set the note is a reply on that discussion thread, which the
|
|
29282
|
+
* GitLab Notes API does not allow to be internal. Passing `internal=true` together with a
|
|
29283
|
+
* `discussionId` therefore throws (fail-closed) rather than silently dropping the
|
|
29284
|
+
* confidentiality flag. Post a standalone internal note (omit `discussionId`) instead.
|
|
29285
|
+
*/
|
|
29286
|
+
async createMrNote(projectId, mrIid, body, discussionId, internal) {
|
|
29279
29287
|
const encodedProject = this.encodeProjectId(projectId);
|
|
29280
29288
|
if (discussionId) {
|
|
29289
|
+
if (internal) {
|
|
29290
|
+
throw new Error(
|
|
29291
|
+
"internal=true is not supported when replying to a discussion. The GitLab Notes API does not accept an internal flag on discussion replies, so honouring it silently would drop a confidentiality flag. Post a standalone internal note (omit discussionId) instead."
|
|
29292
|
+
);
|
|
29293
|
+
}
|
|
29281
29294
|
return this.fetch(
|
|
29282
29295
|
"POST",
|
|
29283
29296
|
`/projects/${encodedProject}/merge_requests/${mrIid}/discussions/${discussionId}/notes`,
|
|
@@ -29287,7 +29300,7 @@ var MergeRequestsClient = class _MergeRequestsClient extends GitLabApiClient {
|
|
|
29287
29300
|
return this.fetch(
|
|
29288
29301
|
"POST",
|
|
29289
29302
|
`/projects/${encodedProject}/merge_requests/${mrIid}/notes`,
|
|
29290
|
-
{ body }
|
|
29303
|
+
internal ? { body, internal: true } : { body }
|
|
29291
29304
|
);
|
|
29292
29305
|
}
|
|
29293
29306
|
async updateMrNote(projectId, mrIid, noteId, body) {
|
|
@@ -29753,9 +29766,22 @@ var IssuesClient = class extends GitLabApiClient {
|
|
|
29753
29766
|
`/projects/${encodedProject}/issues/${issueIid}/discussions/${discussionId}`
|
|
29754
29767
|
);
|
|
29755
29768
|
}
|
|
29756
|
-
|
|
29769
|
+
/**
|
|
29770
|
+
* Create a note on an issue.
|
|
29771
|
+
*
|
|
29772
|
+
* When `discussionId` is set the note is a reply on that discussion thread, which the
|
|
29773
|
+
* GitLab Notes API does not allow to be internal. Passing `internal=true` together with a
|
|
29774
|
+
* `discussionId` therefore throws (fail-closed) rather than silently dropping the
|
|
29775
|
+
* confidentiality flag. Post a standalone internal note (omit `discussionId`) instead.
|
|
29776
|
+
*/
|
|
29777
|
+
async createIssueNote(projectId, issueIid, body, discussionId, internal) {
|
|
29757
29778
|
const encodedProject = this.encodeProjectId(projectId);
|
|
29758
29779
|
if (discussionId) {
|
|
29780
|
+
if (internal) {
|
|
29781
|
+
throw new Error(
|
|
29782
|
+
"internal=true is not supported when replying to a discussion. The GitLab Notes API does not accept an internal flag on discussion replies, so honouring it silently would drop a confidentiality flag. Post a standalone internal note (omit discussionId) instead."
|
|
29783
|
+
);
|
|
29784
|
+
}
|
|
29759
29785
|
return this.fetch(
|
|
29760
29786
|
"POST",
|
|
29761
29787
|
`/projects/${encodedProject}/issues/${issueIid}/discussions/${discussionId}/notes`,
|
|
@@ -29765,7 +29791,7 @@ var IssuesClient = class extends GitLabApiClient {
|
|
|
29765
29791
|
return this.fetch(
|
|
29766
29792
|
"POST",
|
|
29767
29793
|
`/projects/${encodedProject}/issues/${issueIid}/notes`,
|
|
29768
|
-
{ body }
|
|
29794
|
+
internal ? { body, internal: true } : { body }
|
|
29769
29795
|
);
|
|
29770
29796
|
}
|
|
29771
29797
|
async resolveIssueDiscussion(projectId, issueIid, discussionId) {
|
|
@@ -31096,9 +31122,22 @@ var EpicsClient = class extends GitLabApiClient {
|
|
|
31096
31122
|
`/groups/${encodedGroup}/epics/${epicIid}/discussions/${discussionId}`
|
|
31097
31123
|
);
|
|
31098
31124
|
}
|
|
31099
|
-
|
|
31125
|
+
/**
|
|
31126
|
+
* Create a note on an epic.
|
|
31127
|
+
*
|
|
31128
|
+
* When `discussionId` is set the note is a reply on that discussion thread, which the
|
|
31129
|
+
* GitLab Notes API does not allow to be internal. Passing `internal=true` together with a
|
|
31130
|
+
* `discussionId` therefore throws (fail-closed) rather than silently dropping the
|
|
31131
|
+
* confidentiality flag. Post a standalone internal note (omit `discussionId`) instead.
|
|
31132
|
+
*/
|
|
31133
|
+
async createEpicNote(groupId, epicIid, body, discussionId, internal) {
|
|
31100
31134
|
const encodedGroup = encodeURIComponent(groupId);
|
|
31101
31135
|
if (discussionId) {
|
|
31136
|
+
if (internal) {
|
|
31137
|
+
throw new Error(
|
|
31138
|
+
"internal=true is not supported when replying to a discussion. The GitLab Notes API does not accept an internal flag on discussion replies, so honouring it silently would drop a confidentiality flag. Post a standalone internal note (omit discussionId) instead."
|
|
31139
|
+
);
|
|
31140
|
+
}
|
|
31102
31141
|
return this.fetch(
|
|
31103
31142
|
"POST",
|
|
31104
31143
|
`/groups/${encodedGroup}/epics/${epicIid}/discussions/${discussionId}/notes`,
|
|
@@ -31108,7 +31147,7 @@ var EpicsClient = class extends GitLabApiClient {
|
|
|
31108
31147
|
return this.fetch(
|
|
31109
31148
|
"POST",
|
|
31110
31149
|
`/groups/${encodedGroup}/epics/${epicIid}/notes`,
|
|
31111
|
-
{ body }
|
|
31150
|
+
internal ? { body, internal: true } : { body }
|
|
31112
31151
|
);
|
|
31113
31152
|
}
|
|
31114
31153
|
async getEpicNote(groupId, epicIid, noteId) {
|
|
@@ -33659,9 +33698,14 @@ Creates a standalone comment (not part of a thread).
|
|
|
33659
33698
|
For replying to existing discussion threads, use gitlab_create_discussion
|
|
33660
33699
|
with discussion_id parameter instead.
|
|
33661
33700
|
|
|
33701
|
+
Set internal=true to post a confidential/internal note visible only to project members
|
|
33702
|
+
(supported for merge requests, issues, and epics; not supported for snippets, where passing
|
|
33703
|
+
internal=true raises an error rather than silently posting a public note).
|
|
33704
|
+
|
|
33662
33705
|
Examples:
|
|
33663
33706
|
- MR comment: resource_type="merge_request", project_id="group/project", iid=123, body="LGTM!"
|
|
33664
33707
|
- Issue comment: resource_type="issue", project_id="group/project", iid=456, body="Working on this"
|
|
33708
|
+
- Internal issue note: resource_type="issue", project_id="group/project", iid=456, body="Internal only", internal=true
|
|
33665
33709
|
- Epic comment: resource_type="epic", group_id="my-group", iid=1, body="Planning complete"
|
|
33666
33710
|
- Snippet comment: resource_type="snippet", project_id="group/project", snippet_id=789, body="Nice code!"`,
|
|
33667
33711
|
args: {
|
|
@@ -33670,27 +33714,53 @@ Examples:
|
|
|
33670
33714
|
project_id: z14.string().optional().describe("Project ID or path. Required for merge_request, issue, snippet"),
|
|
33671
33715
|
group_id: z14.string().optional().describe("Group ID or path. Required for epic"),
|
|
33672
33716
|
iid: z14.number().optional().describe("Internal ID of the resource (for merge_request, issue, epic)"),
|
|
33673
|
-
snippet_id: z14.number().optional().describe("Snippet ID (required for snippet)")
|
|
33717
|
+
snippet_id: z14.number().optional().describe("Snippet ID (required for snippet)"),
|
|
33718
|
+
internal: z14.boolean().optional().describe(
|
|
33719
|
+
"Post as a confidential/internal note visible only to project members (merge requests, issues, and epics; not supported for snippets, where passing true raises an error). Defaults to false (public note)."
|
|
33720
|
+
)
|
|
33674
33721
|
},
|
|
33675
33722
|
execute: async (args, _ctx) => {
|
|
33676
33723
|
validateNoteParams(args.resource_type, args, { allowedTypes: NOTE_RESOURCE_TYPES });
|
|
33724
|
+
if (args.internal && args.resource_type === "snippet") {
|
|
33725
|
+
throw new Error(
|
|
33726
|
+
`internal=true is not supported for snippet notes. Refusing to post a note that was intended to be confidential as a public note.`
|
|
33727
|
+
);
|
|
33728
|
+
}
|
|
33677
33729
|
const client = getGitLabClient();
|
|
33678
33730
|
switch (args.resource_type) {
|
|
33679
33731
|
case "merge_request":
|
|
33680
33732
|
return JSON.stringify(
|
|
33681
|
-
await client.createMrNote(
|
|
33733
|
+
await client.createMrNote(
|
|
33734
|
+
args.project_id,
|
|
33735
|
+
args.iid,
|
|
33736
|
+
args.body,
|
|
33737
|
+
void 0,
|
|
33738
|
+
args.internal
|
|
33739
|
+
),
|
|
33682
33740
|
null,
|
|
33683
33741
|
2
|
|
33684
33742
|
);
|
|
33685
33743
|
case "issue":
|
|
33686
33744
|
return JSON.stringify(
|
|
33687
|
-
await client.createIssueNote(
|
|
33745
|
+
await client.createIssueNote(
|
|
33746
|
+
args.project_id,
|
|
33747
|
+
args.iid,
|
|
33748
|
+
args.body,
|
|
33749
|
+
void 0,
|
|
33750
|
+
args.internal
|
|
33751
|
+
),
|
|
33688
33752
|
null,
|
|
33689
33753
|
2
|
|
33690
33754
|
);
|
|
33691
33755
|
case "epic":
|
|
33692
33756
|
return JSON.stringify(
|
|
33693
|
-
await client.createEpicNote(
|
|
33757
|
+
await client.createEpicNote(
|
|
33758
|
+
args.group_id,
|
|
33759
|
+
args.iid,
|
|
33760
|
+
args.body,
|
|
33761
|
+
void 0,
|
|
33762
|
+
args.internal
|
|
33763
|
+
),
|
|
33694
33764
|
null,
|
|
33695
33765
|
2
|
|
33696
33766
|
);
|
|
@@ -34201,6 +34271,65 @@ Examples:
|
|
|
34201
34271
|
|
|
34202
34272
|
// src/tools/orbit.ts
|
|
34203
34273
|
var z18 = tool.schema;
|
|
34274
|
+
var MAX_REL_TYPES = 10;
|
|
34275
|
+
var SCHEMA_CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
34276
|
+
var schemaCache = /* @__PURE__ */ new Map();
|
|
34277
|
+
var schemaInflight = /* @__PURE__ */ new Map();
|
|
34278
|
+
async function loadOrbitSchema(client) {
|
|
34279
|
+
const key = process.env["GITLAB_INSTANCE_URL"] || "https://gitlab.com";
|
|
34280
|
+
const cached2 = schemaCache.get(key);
|
|
34281
|
+
if (cached2 && cached2.expiresAt > Date.now()) {
|
|
34282
|
+
return cached2.schema;
|
|
34283
|
+
}
|
|
34284
|
+
const inflight = schemaInflight.get(key);
|
|
34285
|
+
if (inflight) {
|
|
34286
|
+
return inflight;
|
|
34287
|
+
}
|
|
34288
|
+
const request = (async () => {
|
|
34289
|
+
try {
|
|
34290
|
+
const schema = await client.fetch("GET", "/orbit/schema");
|
|
34291
|
+
if (!schema || !Array.isArray(schema.edges)) {
|
|
34292
|
+
return void 0;
|
|
34293
|
+
}
|
|
34294
|
+
schemaCache.set(key, { schema, expiresAt: Date.now() + SCHEMA_CACHE_TTL_MS });
|
|
34295
|
+
return schema;
|
|
34296
|
+
} catch {
|
|
34297
|
+
return void 0;
|
|
34298
|
+
}
|
|
34299
|
+
})();
|
|
34300
|
+
schemaInflight.set(key, request);
|
|
34301
|
+
void request.finally(() => {
|
|
34302
|
+
if (schemaInflight.get(key) === request) {
|
|
34303
|
+
schemaInflight.delete(key);
|
|
34304
|
+
}
|
|
34305
|
+
});
|
|
34306
|
+
return request;
|
|
34307
|
+
}
|
|
34308
|
+
function deriveRelTypes(schema, entityType, preferred) {
|
|
34309
|
+
const touching = /* @__PURE__ */ new Set();
|
|
34310
|
+
for (const edge of schema.edges ?? []) {
|
|
34311
|
+
const touchesEntity = (edge.variants ?? []).some(
|
|
34312
|
+
(variant) => variant.source_type === entityType || variant.target_type === entityType
|
|
34313
|
+
);
|
|
34314
|
+
if (touchesEntity) {
|
|
34315
|
+
touching.add(edge.name);
|
|
34316
|
+
}
|
|
34317
|
+
}
|
|
34318
|
+
const rank = (name) => {
|
|
34319
|
+
const index = preferred.indexOf(name);
|
|
34320
|
+
return index === -1 ? preferred.length : index;
|
|
34321
|
+
};
|
|
34322
|
+
return [...touching].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b)).slice(0, MAX_REL_TYPES);
|
|
34323
|
+
}
|
|
34324
|
+
async function defaultRelTypes(client, entityType) {
|
|
34325
|
+
const fallback = orbitRelTypes(entityType);
|
|
34326
|
+
const schema = await loadOrbitSchema(client);
|
|
34327
|
+
if (!schema) {
|
|
34328
|
+
return fallback;
|
|
34329
|
+
}
|
|
34330
|
+
const derived = deriveRelTypes(schema, entityType, fallback);
|
|
34331
|
+
return derived.length > 0 ? derived : fallback;
|
|
34332
|
+
}
|
|
34204
34333
|
function orbitRelTypes(entityType) {
|
|
34205
34334
|
switch (entityType) {
|
|
34206
34335
|
case "MergeRequest":
|
|
@@ -34537,15 +34666,15 @@ then use that ID (e.g., 377844873). For epics, use the work_item_id.`,
|
|
|
34537
34666
|
),
|
|
34538
34667
|
direction: z18.enum(["incoming", "outgoing", "both"]).optional().describe("Direction of relationships to traverse (default: both)"),
|
|
34539
34668
|
rel_types: z18.array(z18.string()).optional().describe(
|
|
34540
|
-
'Relationship types to follow. Required by the API to bound the lookup; if omitted, a
|
|
34669
|
+
'Relationship types to follow. Required by the API to bound the lookup; if omitted, up to 10 edge types that touch the entity type are taken from gitlab_orbit_schema (with a built-in default set as fallback if the schema is unavailable). The API caps this at 10 per selector, so more than 10 entries are truncated to the first 10 and the response includes a "warnings" field. See gitlab_orbit_schema for valid edge types.'
|
|
34541
34670
|
),
|
|
34542
34671
|
limit: z18.number().optional().describe("Maximum number of neighbors to return (default: 30)")
|
|
34543
34672
|
},
|
|
34544
34673
|
execute: async (args, _ctx) => {
|
|
34545
34674
|
const client = getGitLabClient();
|
|
34546
34675
|
const direction = args.direction || "both";
|
|
34547
|
-
const requestedRelTypes = args.rel_types?.length ? args.rel_types :
|
|
34548
|
-
const relTypes = requestedRelTypes.slice(0,
|
|
34676
|
+
const requestedRelTypes = args.rel_types?.length ? args.rel_types : await defaultRelTypes(client, args.entity_type);
|
|
34677
|
+
const relTypes = requestedRelTypes.slice(0, MAX_REL_TYPES);
|
|
34549
34678
|
const warnings = requestedRelTypes.length > relTypes.length ? [`rel_types truncated to 10 (got ${requestedRelTypes.length})`] : [];
|
|
34550
34679
|
const queryObj = {
|
|
34551
34680
|
query_type: "neighbors",
|
|
@@ -34668,7 +34797,7 @@ async function main() {
|
|
|
34668
34797
|
`);
|
|
34669
34798
|
process.exit(1);
|
|
34670
34799
|
}
|
|
34671
|
-
const version2 = true ? "3.0
|
|
34800
|
+
const version2 = true ? "3.2.0" : "0.0.0";
|
|
34672
34801
|
const server = new McpServer({ name: "gitlab", version: version2 });
|
|
34673
34802
|
adaptToolsToMcp(server, mcpTools);
|
|
34674
34803
|
const transport = new StdioServerTransport();
|