@ttpears/gitlab-mcp-server 1.14.1 → 1.15.1
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 +67 -10
- package/dist/analytics.d.ts +13 -1
- package/dist/analytics.d.ts.map +1 -1
- package/dist/analytics.js +38 -0
- package/dist/analytics.js.map +1 -1
- package/dist/gitlab-client.d.ts +21 -0
- package/dist/gitlab-client.d.ts.map +1 -1
- package/dist/gitlab-client.js +123 -0
- package/dist/gitlab-client.js.map +1 -1
- package/dist/index.d.ts +1 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -28
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +261 -40
- package/dist/tools.js.map +1 -1
- package/package.json +1 -2
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { validateUserConfig } from './config.js';
|
|
3
|
-
import { resolveWindow, fetchUserEventsInWindow, bucketBy, countByAction, buildEnvelope, } from './analytics.js';
|
|
3
|
+
import { resolveWindow, fetchUserEventsInWindow, fetchGroupEventsInWindow, bucketBy, countByAction, buildEnvelope, } from './analytics.js';
|
|
4
4
|
// Schema for user credentials (empty object coerces to undefined for header-based auth)
|
|
5
5
|
const UserCredentialsSchema = z.object({
|
|
6
6
|
gitlabUrl: z.string().url().optional(),
|
|
@@ -173,9 +173,6 @@ const createIssueTool = {
|
|
|
173
173
|
})),
|
|
174
174
|
handler: async (input, client, userConfig) => {
|
|
175
175
|
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
176
|
-
if (!credentials) {
|
|
177
|
-
throw new Error('User authentication is required for creating issues. Please provide your GitLab credentials.');
|
|
178
|
-
}
|
|
179
176
|
const result = await client.createIssue(input.projectPath, input.title, input.description, credentials);
|
|
180
177
|
const payload = result.createIssue;
|
|
181
178
|
if (payload.errors && payload.errors.length > 0) {
|
|
@@ -204,9 +201,6 @@ const createMergeRequestTool = {
|
|
|
204
201
|
})),
|
|
205
202
|
handler: async (input, client, userConfig) => {
|
|
206
203
|
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
207
|
-
if (!credentials) {
|
|
208
|
-
throw new Error('User authentication is required for creating merge requests. Please provide your GitLab credentials.');
|
|
209
|
-
}
|
|
210
204
|
const result = await client.createMergeRequest(input.projectPath, input.title, input.sourceBranch, input.targetBranch, input.description, credentials);
|
|
211
205
|
const payload = result.createMergeRequest;
|
|
212
206
|
if (payload.errors && payload.errors.length > 0) {
|
|
@@ -234,9 +228,6 @@ const executeCustomQueryTool = {
|
|
|
234
228
|
})),
|
|
235
229
|
handler: async (input, client, userConfig) => {
|
|
236
230
|
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
237
|
-
if (input.requiresWrite && !credentials) {
|
|
238
|
-
throw new Error('User authentication is required for write operations. Please provide your GitLab credentials.');
|
|
239
|
-
}
|
|
240
231
|
return await client.query(input.query, input.variables, credentials, input.requiresWrite);
|
|
241
232
|
},
|
|
242
233
|
};
|
|
@@ -283,9 +274,6 @@ const updateIssueTool = {
|
|
|
283
274
|
})),
|
|
284
275
|
handler: async (input, client, userConfig) => {
|
|
285
276
|
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
286
|
-
if (!credentials) {
|
|
287
|
-
throw new Error('User authentication is required to update issues.');
|
|
288
|
-
}
|
|
289
277
|
const result = await client.updateIssueComposite(input.projectPath, input.iid, {
|
|
290
278
|
title: input.title,
|
|
291
279
|
description: input.description,
|
|
@@ -309,9 +297,6 @@ const deleteIssueTool = {
|
|
|
309
297
|
})),
|
|
310
298
|
handler: async (input, client, userConfig) => {
|
|
311
299
|
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
312
|
-
if (!credentials) {
|
|
313
|
-
throw new Error('User authentication is required for deleting issues.');
|
|
314
|
-
}
|
|
315
300
|
await client.destroyIssue(input.projectPath.trim(), input.iid.trim(), credentials);
|
|
316
301
|
return { projectPath: input.projectPath, iid: input.iid, deleted: true };
|
|
317
302
|
},
|
|
@@ -338,9 +323,6 @@ const updateMergeRequestTool = {
|
|
|
338
323
|
})),
|
|
339
324
|
handler: async (input, client, userConfig) => {
|
|
340
325
|
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
341
|
-
if (!credentials) {
|
|
342
|
-
throw new Error('User authentication is required to update merge requests.');
|
|
343
|
-
}
|
|
344
326
|
const result = await client.updateMergeRequestComposite(input.projectPath, input.iid, {
|
|
345
327
|
title: input.title,
|
|
346
328
|
description: input.description,
|
|
@@ -803,9 +785,6 @@ const managePipelineTool = {
|
|
|
803
785
|
})),
|
|
804
786
|
handler: async (input, client, userConfig) => {
|
|
805
787
|
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
806
|
-
if (!credentials) {
|
|
807
|
-
throw new Error('User authentication is required for pipeline management. Please provide your GitLab credentials.');
|
|
808
|
-
}
|
|
809
788
|
return await client.managePipeline(input.projectPath, input.pipelineIid, input.action, credentials);
|
|
810
789
|
},
|
|
811
790
|
};
|
|
@@ -902,6 +881,168 @@ const getNotesTool = {
|
|
|
902
881
|
return noteable.notes;
|
|
903
882
|
},
|
|
904
883
|
};
|
|
884
|
+
const getIssueContextTool = {
|
|
885
|
+
name: 'get_issue_context',
|
|
886
|
+
title: 'Get Issue Context',
|
|
887
|
+
description: 'Bundle issue body, all notes (paginated up to maxNotes), related merge requests (mentioning), closing merge requests, linked issues (relates_to/blocks/is_blocked_by) into a single call. Use this instead of fanning out across get_issues + get_notes + search_merge_requests when investigating an issue.',
|
|
888
|
+
requiresAuth: false,
|
|
889
|
+
requiresWrite: false,
|
|
890
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
891
|
+
inputSchema: withUserAuth(z.object({
|
|
892
|
+
projectPath: z.string().min(1).describe('Full project path (e.g. "my-group/my-project")'),
|
|
893
|
+
iid: z.string().min(1).describe('Issue IID (the number shown in the GitLab UI)'),
|
|
894
|
+
maxNotes: z.number().int().min(1).max(500).default(100).describe('Cap on notes fetched. Default 100.'),
|
|
895
|
+
includeSystemNotes: z.boolean().default(false).describe('Include system-generated notes (label changes, assignment events). Default false.'),
|
|
896
|
+
})),
|
|
897
|
+
handler: async (input, client, userConfig) => {
|
|
898
|
+
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
899
|
+
const projectPath = input.projectPath.trim();
|
|
900
|
+
const iid = input.iid.trim();
|
|
901
|
+
const [detail, notesResult, related, closedBy, links] = await Promise.all([
|
|
902
|
+
client.getIssueDetail(projectPath, iid, credentials),
|
|
903
|
+
client.getNotes(projectPath, 'issue', iid, input.maxNotes, undefined, true, credentials),
|
|
904
|
+
client.getIssueRelatedMergeRequests(projectPath, iid, credentials).catch(() => []),
|
|
905
|
+
client.getIssueClosedBy(projectPath, iid, credentials).catch(() => []),
|
|
906
|
+
client.getIssueLinks(projectPath, iid, credentials).catch(() => []),
|
|
907
|
+
]);
|
|
908
|
+
const issue = detail?.project?.issue;
|
|
909
|
+
if (!issue) {
|
|
910
|
+
throw new Error(`Issue ${projectPath}#${iid} not found.`);
|
|
911
|
+
}
|
|
912
|
+
const allNotes = Array.isArray(notesResult?.nodes) ? notesResult.nodes : [];
|
|
913
|
+
const notes = input.includeSystemNotes ? allNotes : allNotes.filter((n) => !n.system);
|
|
914
|
+
const summarizeMR = (mr) => ({
|
|
915
|
+
iid: mr.iid,
|
|
916
|
+
title: mr.title,
|
|
917
|
+
state: mr.state,
|
|
918
|
+
webUrl: mr.web_url ?? mr.webUrl,
|
|
919
|
+
author: mr.author?.username ?? null,
|
|
920
|
+
sourceBranch: mr.source_branch ?? mr.sourceBranch,
|
|
921
|
+
targetBranch: mr.target_branch ?? mr.targetBranch,
|
|
922
|
+
});
|
|
923
|
+
const summarizeLink = (l) => ({
|
|
924
|
+
iid: String(l.iid),
|
|
925
|
+
title: l.title,
|
|
926
|
+
state: l.state,
|
|
927
|
+
linkType: l.link_type ?? null,
|
|
928
|
+
webUrl: l.web_url ?? null,
|
|
929
|
+
projectId: l.project_id ?? null,
|
|
930
|
+
});
|
|
931
|
+
return {
|
|
932
|
+
project: { fullPath: projectPath },
|
|
933
|
+
issue,
|
|
934
|
+
notes: {
|
|
935
|
+
count: notes.length,
|
|
936
|
+
truncated: !!notesResult?.hasMore,
|
|
937
|
+
nodes: notes,
|
|
938
|
+
},
|
|
939
|
+
relatedMergeRequests: related.map(summarizeMR),
|
|
940
|
+
closingMergeRequests: closedBy.map(summarizeMR),
|
|
941
|
+
linkedIssues: links.map(summarizeLink),
|
|
942
|
+
};
|
|
943
|
+
},
|
|
944
|
+
};
|
|
945
|
+
const getMergeRequestContextTool = {
|
|
946
|
+
name: 'get_merge_request_context',
|
|
947
|
+
title: 'Get Merge Request Context',
|
|
948
|
+
description: 'Bundle MR body, all notes (paginated up to maxNotes, filtered to non-system by default), commits, pipeline summary, reviewers with approval state, and issues this MR will close into a single call. Use this instead of fanning out across get_merge_requests + get_notes + get_merge_request_commits + get_merge_request_pipelines when investigating an MR.',
|
|
949
|
+
requiresAuth: false,
|
|
950
|
+
requiresWrite: false,
|
|
951
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
952
|
+
inputSchema: withUserAuth(z.object({
|
|
953
|
+
projectPath: z.string().min(1).describe('Full project path (e.g. "my-group/my-project")'),
|
|
954
|
+
iid: z.string().min(1).describe('Merge request IID'),
|
|
955
|
+
maxNotes: z.number().int().min(1).max(500).default(100).describe('Cap on notes fetched. Default 100.'),
|
|
956
|
+
maxCommits: z.number().int().min(1).max(500).default(50).describe('Cap on commits fetched. Default 50.'),
|
|
957
|
+
includeSystemNotes: z.boolean().default(false).describe('Include system-generated notes. Default false.'),
|
|
958
|
+
})),
|
|
959
|
+
handler: async (input, client, userConfig) => {
|
|
960
|
+
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
961
|
+
const projectPath = input.projectPath.trim();
|
|
962
|
+
const iid = input.iid.trim();
|
|
963
|
+
const [detail, notesResult, commitsResult, pipelinesResult, reviewers, closesIssues] = await Promise.all([
|
|
964
|
+
client.getMergeRequestDetail(projectPath, iid, credentials),
|
|
965
|
+
client.getNotes(projectPath, 'merge_request', iid, input.maxNotes, undefined, true, credentials),
|
|
966
|
+
client.getMergeRequestCommits(projectPath, iid, input.maxCommits, undefined, true, credentials),
|
|
967
|
+
client.getMergeRequestPipelines(projectPath, iid, 5, undefined, false, credentials).catch(() => null),
|
|
968
|
+
client.getMergeRequestReviewers(projectPath, iid, credentials).catch(() => null),
|
|
969
|
+
client.getMergeRequestClosesIssues(projectPath, iid, credentials).catch(() => []),
|
|
970
|
+
]);
|
|
971
|
+
const mr = detail?.project?.mergeRequest;
|
|
972
|
+
if (!mr) {
|
|
973
|
+
throw new Error(`Merge request ${projectPath}!${iid} not found.`);
|
|
974
|
+
}
|
|
975
|
+
const allNotes = Array.isArray(notesResult?.nodes) ? notesResult.nodes : [];
|
|
976
|
+
const notes = input.includeSystemNotes ? allNotes : allNotes.filter((n) => !n.system);
|
|
977
|
+
return {
|
|
978
|
+
project: { fullPath: projectPath },
|
|
979
|
+
mergeRequest: mr,
|
|
980
|
+
notes: {
|
|
981
|
+
count: notes.length,
|
|
982
|
+
truncated: !!notesResult?.hasMore,
|
|
983
|
+
nodes: notes,
|
|
984
|
+
},
|
|
985
|
+
commits: {
|
|
986
|
+
count: Array.isArray(commitsResult?.nodes) ? commitsResult.nodes.length : 0,
|
|
987
|
+
truncated: !!commitsResult?.hasMore,
|
|
988
|
+
nodes: commitsResult?.nodes ?? [],
|
|
989
|
+
},
|
|
990
|
+
pipelines: pipelinesResult ?? null,
|
|
991
|
+
reviewers: reviewers ?? null,
|
|
992
|
+
closesIssues: closesIssues.map((i) => ({
|
|
993
|
+
iid: String(i.iid),
|
|
994
|
+
title: i.title,
|
|
995
|
+
state: i.state,
|
|
996
|
+
webUrl: i.web_url ?? null,
|
|
997
|
+
projectId: i.project_id ?? null,
|
|
998
|
+
})),
|
|
999
|
+
};
|
|
1000
|
+
},
|
|
1001
|
+
};
|
|
1002
|
+
const searchNotesTool = {
|
|
1003
|
+
name: 'search_notes',
|
|
1004
|
+
title: 'Search Notes (Comments)',
|
|
1005
|
+
description: 'Full-text search across issue and merge request comments. Scope can be global, a project, or a group. NOTE: on self-hosted GitLab, the "notes" search scope requires Advanced Search (Elasticsearch) to be enabled — without it, this endpoint returns an error. search_gitlab does NOT search note bodies; this tool does.',
|
|
1006
|
+
requiresAuth: false,
|
|
1007
|
+
requiresWrite: false,
|
|
1008
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
1009
|
+
inputSchema: withUserAuth(z.object({
|
|
1010
|
+
search: z.string().min(1).describe('Search term. Trimmed; empty rejected.'),
|
|
1011
|
+
scope: z.enum(['global', 'project', 'group']).default('global').describe('Search scope. Default global.'),
|
|
1012
|
+
projectPath: z.string().optional().describe('Required when scope=project. Full project path.'),
|
|
1013
|
+
groupPath: z.string().optional().describe('Required when scope=group. Full group path.'),
|
|
1014
|
+
perPage: z.number().int().min(1).max(100).default(20).describe('Results per page. Default 20.'),
|
|
1015
|
+
page: z.number().int().min(1).default(1).describe('Page number. Default 1.'),
|
|
1016
|
+
})),
|
|
1017
|
+
handler: async (input, client, userConfig) => {
|
|
1018
|
+
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
1019
|
+
const search = input.search.trim();
|
|
1020
|
+
if (!search)
|
|
1021
|
+
throw new Error('search_notes requires a non-empty search term.');
|
|
1022
|
+
if (input.scope === 'project' && !input.projectPath) {
|
|
1023
|
+
throw new Error('search_notes scope=project requires projectPath.');
|
|
1024
|
+
}
|
|
1025
|
+
if (input.scope === 'group' && !input.groupPath) {
|
|
1026
|
+
throw new Error('search_notes scope=group requires groupPath.');
|
|
1027
|
+
}
|
|
1028
|
+
const results = await client.searchNotes({
|
|
1029
|
+
search,
|
|
1030
|
+
scope: input.scope,
|
|
1031
|
+
projectPath: input.projectPath?.trim(),
|
|
1032
|
+
groupPath: input.groupPath?.trim(),
|
|
1033
|
+
perPage: input.perPage,
|
|
1034
|
+
page: input.page,
|
|
1035
|
+
}, credentials);
|
|
1036
|
+
return {
|
|
1037
|
+
scope: input.scope,
|
|
1038
|
+
search,
|
|
1039
|
+
page: input.page,
|
|
1040
|
+
perPage: input.perPage,
|
|
1041
|
+
count: Array.isArray(results) ? results.length : 0,
|
|
1042
|
+
nodes: results,
|
|
1043
|
+
};
|
|
1044
|
+
},
|
|
1045
|
+
};
|
|
905
1046
|
const createNoteTool = {
|
|
906
1047
|
name: 'create_note',
|
|
907
1048
|
title: 'Create Note',
|
|
@@ -922,9 +1063,6 @@ const createNoteTool = {
|
|
|
922
1063
|
})),
|
|
923
1064
|
handler: async (input, client, userConfig) => {
|
|
924
1065
|
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
925
|
-
if (!credentials) {
|
|
926
|
-
throw new Error('User authentication is required for creating notes. Please provide your GitLab credentials.');
|
|
927
|
-
}
|
|
928
1066
|
const result = await client.createNote(input.projectPath, input.noteableType, input.iid, input.body, input.internal, credentials);
|
|
929
1067
|
if (result.errors && result.errors.length > 0) {
|
|
930
1068
|
throw new Error(`Failed to create note: ${result.errors.join(', ')}`);
|
|
@@ -944,9 +1082,6 @@ const deleteNoteTool = {
|
|
|
944
1082
|
})),
|
|
945
1083
|
handler: async (input, client, userConfig) => {
|
|
946
1084
|
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
947
|
-
if (!credentials) {
|
|
948
|
-
throw new Error('User authentication is required for deleting notes.');
|
|
949
|
-
}
|
|
950
1085
|
await client.destroyNote(input.noteId.trim(), credentials);
|
|
951
1086
|
return { noteId: input.noteId, deleted: true };
|
|
952
1087
|
},
|
|
@@ -964,9 +1099,6 @@ const updateNoteTool = {
|
|
|
964
1099
|
})),
|
|
965
1100
|
handler: async (input, client, userConfig) => {
|
|
966
1101
|
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
967
|
-
if (!credentials) {
|
|
968
|
-
throw new Error('User authentication is required for updating notes.');
|
|
969
|
-
}
|
|
970
1102
|
const result = await client.updateNote(input.noteId.trim(), input.body, credentials);
|
|
971
1103
|
return result.note;
|
|
972
1104
|
},
|
|
@@ -1363,9 +1495,6 @@ const createBroadcastMessageTool = {
|
|
|
1363
1495
|
inputSchema: withUserAuth(z.object(BroadcastMessageFields)),
|
|
1364
1496
|
handler: async (input, client, userConfig) => {
|
|
1365
1497
|
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
1366
|
-
if (!credentials) {
|
|
1367
|
-
throw new Error('User authentication is required for creating broadcast messages.');
|
|
1368
|
-
}
|
|
1369
1498
|
const { userCredentials, ...body } = input;
|
|
1370
1499
|
return client.createBroadcastMessage(body, credentials);
|
|
1371
1500
|
},
|
|
@@ -1392,9 +1521,6 @@ const updateBroadcastMessageTool = {
|
|
|
1392
1521
|
})),
|
|
1393
1522
|
handler: async (input, client, userConfig) => {
|
|
1394
1523
|
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
1395
|
-
if (!credentials) {
|
|
1396
|
-
throw new Error('User authentication is required for updating broadcast messages.');
|
|
1397
|
-
}
|
|
1398
1524
|
const { id, userCredentials, ...body } = input;
|
|
1399
1525
|
return client.updateBroadcastMessage(id, body, credentials);
|
|
1400
1526
|
},
|
|
@@ -1411,9 +1537,6 @@ const deleteBroadcastMessageTool = {
|
|
|
1411
1537
|
})),
|
|
1412
1538
|
handler: async (input, client, userConfig) => {
|
|
1413
1539
|
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
1414
|
-
if (!credentials) {
|
|
1415
|
-
throw new Error('User authentication is required for deleting broadcast messages.');
|
|
1416
|
-
}
|
|
1417
1540
|
await client.deleteBroadcastMessage(input.id, credentials);
|
|
1418
1541
|
return { id: input.id, deleted: true };
|
|
1419
1542
|
},
|
|
@@ -1612,6 +1735,100 @@ const analyticsUserSummaryTool = {
|
|
|
1612
1735
|
return buildEnvelope({ type: 'user', identifier: user }, window, events, { byProject, byDay }, truncated);
|
|
1613
1736
|
},
|
|
1614
1737
|
};
|
|
1738
|
+
const analyticsGroupSummaryTool = {
|
|
1739
|
+
name: 'analytics_group_summary',
|
|
1740
|
+
title: 'Group Activity Summary',
|
|
1741
|
+
description: 'Aggregated activity summary for an entire group (optionally including subgroups) over a time window — totals by action type (pushes, MRs opened/merged, comments, approvals), with breakdowns by project, by contributor, and by day. Use this to answer "what did this team do" without fanning out across list_project_events yourself.',
|
|
1742
|
+
requiresAuth: false,
|
|
1743
|
+
requiresWrite: false,
|
|
1744
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
1745
|
+
inputSchema: withUserAuth(z.object({
|
|
1746
|
+
group: z
|
|
1747
|
+
.string()
|
|
1748
|
+
.min(1)
|
|
1749
|
+
.describe('Group full path (e.g. "my-group" or "my-group/sub-group")'),
|
|
1750
|
+
since: z
|
|
1751
|
+
.string()
|
|
1752
|
+
.describe('ISO date/datetime, inclusive lower bound (e.g. "2026-04-01" or "2026-04-01T00:00:00Z")'),
|
|
1753
|
+
until: z
|
|
1754
|
+
.string()
|
|
1755
|
+
.optional()
|
|
1756
|
+
.describe('ISO date/datetime, inclusive upper bound. Defaults to now.'),
|
|
1757
|
+
includeSubgroups: z
|
|
1758
|
+
.boolean()
|
|
1759
|
+
.default(true)
|
|
1760
|
+
.describe('Recurse into subgroup projects. Defaults to true.'),
|
|
1761
|
+
topContributors: z
|
|
1762
|
+
.number()
|
|
1763
|
+
.int()
|
|
1764
|
+
.min(1)
|
|
1765
|
+
.max(100)
|
|
1766
|
+
.default(10)
|
|
1767
|
+
.describe('Cap on the byUser[] list (sorted by event count desc). Default 10.'),
|
|
1768
|
+
maxEvents: z
|
|
1769
|
+
.number()
|
|
1770
|
+
.int()
|
|
1771
|
+
.min(1)
|
|
1772
|
+
.max(10000)
|
|
1773
|
+
.optional()
|
|
1774
|
+
.describe('Global cap on events fetched across all projects. Defaults to 2000; envelope flags truncated:true if reached.'),
|
|
1775
|
+
maxProjects: z
|
|
1776
|
+
.number()
|
|
1777
|
+
.int()
|
|
1778
|
+
.min(1)
|
|
1779
|
+
.max(2000)
|
|
1780
|
+
.optional()
|
|
1781
|
+
.describe('Cap on projects scanned. Defaults to 500.'),
|
|
1782
|
+
})),
|
|
1783
|
+
handler: async (input, client, userConfig) => {
|
|
1784
|
+
const credentials = input.userCredentials ? validateUserConfig(input.userCredentials) : userConfig;
|
|
1785
|
+
const group = input.group.trim();
|
|
1786
|
+
const window = resolveWindow(input.since, input.until);
|
|
1787
|
+
const { events, truncated, projects, projectsTruncated } = await fetchGroupEventsInWindow(client, group, {
|
|
1788
|
+
window,
|
|
1789
|
+
maxEvents: input.maxEvents,
|
|
1790
|
+
includeSubgroups: input.includeSubgroups,
|
|
1791
|
+
maxProjects: input.maxProjects,
|
|
1792
|
+
}, credentials);
|
|
1793
|
+
const projectPathById = new Map();
|
|
1794
|
+
for (const p of projects)
|
|
1795
|
+
projectPathById.set(p.id, p.fullPath);
|
|
1796
|
+
const byProject = [];
|
|
1797
|
+
for (const [projectId, bucket] of bucketBy(events, (e) => e.projectId)) {
|
|
1798
|
+
byProject.push({
|
|
1799
|
+
projectId,
|
|
1800
|
+
projectPath: projectPathById.get(projectId) ?? null,
|
|
1801
|
+
totals: countByAction(bucket),
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
byProject.sort((a, b) => b.totals.events - a.totals.events);
|
|
1805
|
+
const byUserAll = [];
|
|
1806
|
+
for (const [username, bucket] of bucketBy(events, (e) => e.authorUsername)) {
|
|
1807
|
+
byUserAll.push({ username, totals: countByAction(bucket) });
|
|
1808
|
+
}
|
|
1809
|
+
byUserAll.sort((a, b) => b.totals.events - a.totals.events);
|
|
1810
|
+
const byUser = byUserAll.slice(0, input.topContributors);
|
|
1811
|
+
const byDay = [];
|
|
1812
|
+
for (const [date, bucket] of bucketBy(events, (e) => e.createdAt.toISOString().slice(0, 10))) {
|
|
1813
|
+
byDay.push({ date, totals: countByAction(bucket) });
|
|
1814
|
+
}
|
|
1815
|
+
byDay.sort((a, b) => a.date.localeCompare(b.date));
|
|
1816
|
+
const envelope = buildEnvelope({ type: 'group', identifier: group }, window, events, {
|
|
1817
|
+
byProject,
|
|
1818
|
+
byUser,
|
|
1819
|
+
byDay,
|
|
1820
|
+
projectsScanned: projects.length,
|
|
1821
|
+
contributorsTotal: byUserAll.length,
|
|
1822
|
+
}, truncated);
|
|
1823
|
+
if (projectsTruncated) {
|
|
1824
|
+
envelope.warnings = [
|
|
1825
|
+
...envelope.warnings,
|
|
1826
|
+
`Group has more projects than maxProjects cap (${input.maxProjects ?? 500}); some projects were not scanned.`,
|
|
1827
|
+
];
|
|
1828
|
+
}
|
|
1829
|
+
return envelope;
|
|
1830
|
+
},
|
|
1831
|
+
};
|
|
1615
1832
|
const REVIEW_BUCKETS = ['<1d', '1-3d', '3-7d', '7-14d', '>14d'];
|
|
1616
1833
|
function emptyReviewBuckets() {
|
|
1617
1834
|
return { '<1d': 0, '1-3d': 0, '3-7d': 0, '7-14d': 0, '>14d': 0 };
|
|
@@ -1745,6 +1962,8 @@ export const readOnlyTools = [
|
|
|
1745
1962
|
getMergeRequestDiffsTool,
|
|
1746
1963
|
getMergeRequestCommitsTool,
|
|
1747
1964
|
getNotesTool,
|
|
1965
|
+
getIssueContextTool,
|
|
1966
|
+
getMergeRequestContextTool,
|
|
1748
1967
|
listMilestonesTool,
|
|
1749
1968
|
listIterationsTool,
|
|
1750
1969
|
getTimeTrackingTool,
|
|
@@ -1758,6 +1977,7 @@ export const readOnlyTools = [
|
|
|
1758
1977
|
listProjectEventsTool,
|
|
1759
1978
|
listMyEventsTool,
|
|
1760
1979
|
analyticsUserSummaryTool,
|
|
1980
|
+
analyticsGroupSummaryTool,
|
|
1761
1981
|
analyticsReviewBottlenecksTool,
|
|
1762
1982
|
];
|
|
1763
1983
|
export const userAuthTools = [
|
|
@@ -1785,6 +2005,7 @@ export const searchTools = [
|
|
|
1785
2005
|
searchUsersTool,
|
|
1786
2006
|
searchGroupsTool,
|
|
1787
2007
|
searchLabelsTool,
|
|
2008
|
+
searchNotesTool,
|
|
1788
2009
|
browseRepositoryTool,
|
|
1789
2010
|
getFileContentTool,
|
|
1790
2011
|
listGroupMembersTool,
|