@gremlin/mcp-server 2.2.1 → 2.3.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/Readme.md +21 -0
- package/build/client/gremlin.d.ts +27 -0
- package/build/client/interface.d.ts +22 -0
- package/build/elicitation.d.ts +27 -0
- package/build/index.d.ts +11 -0
- package/build/index.mjs +821 -0
- package/build/main.d.ts +1 -0
- package/build/main.mjs +15545 -8258
- package/build/openapi/spec-loader.d.ts +41 -0
- package/build/resources/index.d.ts +9 -0
- package/build/tools/company.d.ts +49 -0
- package/build/tools/index.d.ts +3 -0
- package/build/tools/openapi.d.ts +49 -0
- package/build/tools/reliability-management.d.ts +98 -0
- package/build/tools/services.d.ts +55 -0
- package/build/tools/teams.d.ts +7 -0
- package/build/types.d.ts +155 -0
- package/package.json +9 -3
package/build/index.mjs
ADDED
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
// src/elicitation.ts
|
|
2
|
+
var McpElicitationClient = class {
|
|
3
|
+
constructor(server) {
|
|
4
|
+
this.server = server;
|
|
5
|
+
}
|
|
6
|
+
async confirm(prompt, options) {
|
|
7
|
+
const [proceed, ...rest] = options;
|
|
8
|
+
const cancel = rest[rest.length - 1] ?? proceed;
|
|
9
|
+
const result = await this.server.server.elicitInput({
|
|
10
|
+
message: prompt,
|
|
11
|
+
requestedSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
confirmed: {
|
|
15
|
+
type: "boolean",
|
|
16
|
+
title: proceed.label,
|
|
17
|
+
description: proceed.description,
|
|
18
|
+
default: false
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
required: ["confirmed"]
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
if (result.action === "accept" && result.content?.["confirmed"] === true) {
|
|
25
|
+
return proceed.value;
|
|
26
|
+
}
|
|
27
|
+
return cancel.value;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/tools/reliability-management.ts
|
|
32
|
+
import z from "zod";
|
|
33
|
+
function summarizeScenarioRun(testRun) {
|
|
34
|
+
const { graph: _graph, ...runSummary } = testRun.run;
|
|
35
|
+
return {
|
|
36
|
+
...testRun,
|
|
37
|
+
run: runSummary
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function createGetReliabilityExperimentTool(api) {
|
|
41
|
+
return {
|
|
42
|
+
name: "get_reliability_experiments",
|
|
43
|
+
description: "Retrieves recent reliability experiment for a specific service.",
|
|
44
|
+
schema: {
|
|
45
|
+
teamId: z.string().describe("The ID of the team that owns the service."),
|
|
46
|
+
serviceId: z.string().describe("The ID of the service to retrieve the reliability experiment."),
|
|
47
|
+
dependencyId: z.string().optional().describe("The ID of the dependency to retrieve the reliability experiment for, if applicable."),
|
|
48
|
+
testId: z.string().optional().describe("The ID of the reliability test to retrieve the experiment for, if applicable."),
|
|
49
|
+
limit: z.number().optional().describe("The maximum number of results to return. Defaults to 100."),
|
|
50
|
+
includeScenarioRun: z.boolean().optional().describe("Include the full scenario run graph data. Defaults to false. Only set to true when you need detailed step-by-step execution data.")
|
|
51
|
+
},
|
|
52
|
+
handler: async (args) => {
|
|
53
|
+
const { serviceId, teamId, dependencyId, testId, limit, includeScenarioRun } = args;
|
|
54
|
+
if (!serviceId || !teamId) {
|
|
55
|
+
throw new Error(`got ${JSON.stringify(args)} but expected { serviceId: string, teamId: string }`);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const results = await api.getReliabilityExperiment(serviceId, teamId, dependencyId, testId, limit);
|
|
59
|
+
if (includeScenarioRun) {
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
if ("items" in results) {
|
|
63
|
+
results.items = results.items.map(summarizeScenarioRun);
|
|
64
|
+
} else {
|
|
65
|
+
return summarizeScenarioRun(results);
|
|
66
|
+
}
|
|
67
|
+
return results;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(`Error fetching reliability report`, error);
|
|
70
|
+
throw new Error(`Failed to fetch reliability report: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function createGetReliabilityReportTool(api) {
|
|
76
|
+
return {
|
|
77
|
+
name: "get_reliability_report",
|
|
78
|
+
description: "Retrieves the reliability report for a specific service.",
|
|
79
|
+
schema: {
|
|
80
|
+
teamId: z.string().describe("The ID of the team that owns the service."),
|
|
81
|
+
serviceId: z.string().describe("The ID of the service to retrieve the reliability report."),
|
|
82
|
+
date: z.string().optional().describe("The date for which to retrieve the reliability report, in YYYY-MM-DD format. Defaults to today.")
|
|
83
|
+
},
|
|
84
|
+
handler: async (args) => {
|
|
85
|
+
const { serviceId, teamId, date } = args;
|
|
86
|
+
if (!serviceId || !teamId) {
|
|
87
|
+
throw new Error(`got ${JSON.stringify(args)} but expected { serviceId: string, teamId: string }`);
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
return await api.getReliabilityReport(serviceId, teamId, date);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(`Error fetching reliability report`, error);
|
|
93
|
+
throw new Error(`Failed to fetch reliability report: ${error instanceof Error ? error.message : String(error)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function createGetCurrentTestSuiteTool(api) {
|
|
99
|
+
return {
|
|
100
|
+
name: "get_current_test_suite",
|
|
101
|
+
description: "Retrieves the current test suite for a specific team. Or all if no team is specified.",
|
|
102
|
+
schema: {
|
|
103
|
+
teamId: z.string().optional().describe("The ID of the team you're examining the current test suite for.")
|
|
104
|
+
},
|
|
105
|
+
handler: async (args) => {
|
|
106
|
+
const { teamId } = args;
|
|
107
|
+
try {
|
|
108
|
+
const testSuites = await api.getAllTestSuite();
|
|
109
|
+
if (!teamId) {
|
|
110
|
+
return testSuites;
|
|
111
|
+
}
|
|
112
|
+
if (!testSuites || !Array.isArray(testSuites)) {
|
|
113
|
+
throw new Error("No test suites found or invalid format.");
|
|
114
|
+
}
|
|
115
|
+
if (testSuites.length === 0) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
if (!testSuites.some((suite) => suite.targetTeamIds.includes(teamId))) {
|
|
119
|
+
throw new Error(`No test suites found for team ID: ${teamId}`);
|
|
120
|
+
}
|
|
121
|
+
return testSuites.filter((suite) => suite.targetTeamIds.includes(teamId));
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(`Error fetching current test suite`, error);
|
|
124
|
+
throw new Error(`Failed to fetch current test suite: ${error instanceof Error ? error.message : String(error)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function createRunReliabilityTestTool(api) {
|
|
130
|
+
return {
|
|
131
|
+
name: "run_reliability_test",
|
|
132
|
+
description: [
|
|
133
|
+
"Run a reliability test for a service.",
|
|
134
|
+
"Use get_reliability_report to discover valid reliabilityTestId, dependencyId, and failureFlagName values for a service.",
|
|
135
|
+
"You can also extract these parameters from a previous reliability test run (via get_reliability_experiments) to rerun a test.",
|
|
136
|
+
"Requires the SERVICES_RUN privilege.",
|
|
137
|
+
"This call may fail with HTTP 400 if a test is already running or scheduled for the service.",
|
|
138
|
+
"When this happens, use get_pending_test_runs to see what is queued and wait for it to finish before retrying."
|
|
139
|
+
].join(" "),
|
|
140
|
+
schema: {
|
|
141
|
+
teamId: z.string().describe("The ID of the team that owns the service."),
|
|
142
|
+
serviceId: z.string().describe("The ID of the service to run the reliability test against."),
|
|
143
|
+
reliabilityTestId: z.string().describe("The ID of the reliability test to run. Found in the reliability report's policyStates as 'reliabilityTestId'."),
|
|
144
|
+
dependencyId: z.string().optional().describe("The ID of the dependency to target, if the test is a dependency test. Found in the reliability report's policyStates."),
|
|
145
|
+
failureFlagName: z.string().optional().describe("The name of the failure flag to target, if the test uses failure flags. Found in the reliability report's policyStates."),
|
|
146
|
+
includeScenarioRun: z.boolean().optional().describe("Include the full scenario run graph data. Defaults to false. Only set to true when you need detailed step-by-step execution data.")
|
|
147
|
+
},
|
|
148
|
+
annotations: {
|
|
149
|
+
destructiveHint: true,
|
|
150
|
+
idempotentHint: false,
|
|
151
|
+
openWorldHint: true
|
|
152
|
+
},
|
|
153
|
+
handler: async (args) => {
|
|
154
|
+
const { teamId, serviceId, reliabilityTestId, dependencyId, failureFlagName, includeScenarioRun } = args;
|
|
155
|
+
if (!teamId || !serviceId || !reliabilityTestId) {
|
|
156
|
+
throw new Error(`got ${JSON.stringify(args)} but expected { teamId: string, serviceId: string, reliabilityTestId: string }`);
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const result = await api.runReliabilityTest(reliabilityTestId, teamId, {
|
|
160
|
+
serviceId,
|
|
161
|
+
dependencyId,
|
|
162
|
+
failureFlagName
|
|
163
|
+
});
|
|
164
|
+
if (includeScenarioRun) {
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
return summarizeScenarioRun(result);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(`Error running reliability test`, error);
|
|
170
|
+
throw new Error(`Failed to run reliability test: ${error instanceof Error ? error.message : String(error)}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function createGetPendingTestRunsTool(api) {
|
|
176
|
+
return {
|
|
177
|
+
name: "get_pending_test_runs",
|
|
178
|
+
description: [
|
|
179
|
+
"Get pending reliability test runs for a service, ordered by expected trigger time.",
|
|
180
|
+
"Shows tests queued via schedule, Run All, or manual trigger that have not yet started.",
|
|
181
|
+
"Use this to check whether a test is already queued before calling run_reliability_test,",
|
|
182
|
+
"or to understand why run_reliability_test returned a 400 error."
|
|
183
|
+
].join(" "),
|
|
184
|
+
schema: {
|
|
185
|
+
teamId: z.string().describe("The ID of the team that owns the service."),
|
|
186
|
+
serviceId: z.string().describe("The ID of the service to check for pending test runs.")
|
|
187
|
+
},
|
|
188
|
+
handler: async (args) => {
|
|
189
|
+
const { teamId, serviceId } = args;
|
|
190
|
+
if (!teamId || !serviceId) {
|
|
191
|
+
throw new Error(`got ${JSON.stringify(args)} but expected { teamId: string, serviceId: string }`);
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
return await api.getPendingTestRuns(serviceId, teamId);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error(`Error fetching pending test runs`, error);
|
|
197
|
+
throw new Error(`Failed to fetch pending test runs: ${error instanceof Error ? error.message : String(error)}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function createGetRecentReliabilityTestsTool(api) {
|
|
203
|
+
return {
|
|
204
|
+
name: "get_recent_reliability_tests",
|
|
205
|
+
description: "Retrieves recent reliability tests for a given team.",
|
|
206
|
+
schema: {
|
|
207
|
+
teamId: z.string().describe("The ID of the team that owns the service."),
|
|
208
|
+
pageSize: z.number().optional().describe("The maximum number of results to return. Defaults to 5."),
|
|
209
|
+
pageToken: z.string().optional().describe("The token for pagination, if applicable.")
|
|
210
|
+
},
|
|
211
|
+
handler: async (args) => {
|
|
212
|
+
const { teamId, pageSize, pageToken } = args;
|
|
213
|
+
let limit = pageSize;
|
|
214
|
+
if (!teamId) {
|
|
215
|
+
throw new Error(`got ${JSON.stringify(args)} but expected { teamId: string }`);
|
|
216
|
+
}
|
|
217
|
+
if (!limit) {
|
|
218
|
+
limit = 5;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
return await api.getRecentReliabilityTests(teamId, limit, pageToken);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error(`Error fetching recent reliability tests`, error);
|
|
224
|
+
throw new Error(`Failed to fetch recent reliability tests: ${error instanceof Error ? error.message : String(error)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/tools/services.ts
|
|
231
|
+
import z2 from "zod";
|
|
232
|
+
function createGetServiceDependenciesTool(api) {
|
|
233
|
+
return {
|
|
234
|
+
name: "get_service_dependencies",
|
|
235
|
+
description: "Retrieves the service dependencies for a specific service.",
|
|
236
|
+
schema: {
|
|
237
|
+
teamId: z2.string().describe("The ID of the team that owns the service."),
|
|
238
|
+
serviceId: z2.string().describe("The ID of the service to retrieve the dependencies for")
|
|
239
|
+
},
|
|
240
|
+
handler: async (args) => {
|
|
241
|
+
const { serviceId, teamId } = args;
|
|
242
|
+
if (!serviceId || !teamId) {
|
|
243
|
+
throw new Error(`got ${JSON.stringify(args)} but expected { serviceId: string, teamId: string }`);
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
return await api.getServiceDependencies(serviceId, teamId);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error(`Error fetching service dependencies`, error);
|
|
249
|
+
throw new Error(`Failed to fetch service dependencies: ${error instanceof Error ? error.message : String(error)}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function createListServiceRisksTool(api) {
|
|
255
|
+
return {
|
|
256
|
+
name: "list_service_risks",
|
|
257
|
+
description: "Lists the risks associated with a specific service.",
|
|
258
|
+
schema: {
|
|
259
|
+
teamId: z2.string().describe("The ID of the team that owns the service."),
|
|
260
|
+
serviceId: z2.string().describe("The ID of the service to retrieve risks for.")
|
|
261
|
+
},
|
|
262
|
+
handler: async (args) => {
|
|
263
|
+
const { serviceId, teamId } = args;
|
|
264
|
+
if (!serviceId || !teamId) {
|
|
265
|
+
throw new Error(`got ${JSON.stringify(args)} but expected { serviceId: string, teamId: string }`);
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
return await api.getServiceRisks(serviceId, teamId);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error(`Error fetching service risks`, error);
|
|
271
|
+
throw new Error(`Failed to fetch service risks: ${error instanceof Error ? error.message : String(error)}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function createGetServiceStatusChecksTool(api) {
|
|
277
|
+
return {
|
|
278
|
+
name: "get_service_status_checks",
|
|
279
|
+
description: "Retrieves the status checks for a specific service.",
|
|
280
|
+
schema: {
|
|
281
|
+
teamId: z2.string().describe("The ID of the team that owns the service."),
|
|
282
|
+
serviceId: z2.string().describe("The ID of the service to retrieve status checks for.")
|
|
283
|
+
},
|
|
284
|
+
handler: async (args) => {
|
|
285
|
+
const { serviceId, teamId } = args;
|
|
286
|
+
if (!serviceId || !teamId) {
|
|
287
|
+
throw new Error(`got ${JSON.stringify(args)} but expected { serviceId: string, teamId: string }`);
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
return await api.getServiceStatusChecks(serviceId, teamId);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error(`Error fetching service status checks`, error);
|
|
293
|
+
throw new Error(`Failed to fetch service status checks: ${error instanceof Error ? error.message : String(error)}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function createListServicesTool(api) {
|
|
299
|
+
return {
|
|
300
|
+
name: "list_services",
|
|
301
|
+
description: "Lists available reliability management services (RM Services for short). Returns service names, descriptions, score, and targeting information.",
|
|
302
|
+
schema: {
|
|
303
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
304
|
+
"type": "object",
|
|
305
|
+
"properties": {}
|
|
306
|
+
},
|
|
307
|
+
/**
|
|
308
|
+
* Handles the list_services tool request with pagination and search
|
|
309
|
+
*
|
|
310
|
+
* @param params - none currently, but will be extended for pagination
|
|
311
|
+
* @returns list of services with their details
|
|
312
|
+
*/
|
|
313
|
+
handler: async (params) => {
|
|
314
|
+
try {
|
|
315
|
+
const self = await api.getSelf();
|
|
316
|
+
const services = [];
|
|
317
|
+
for (const team of self.team_memberships) {
|
|
318
|
+
const teamServices = (await api.listServicesForTeam(team)).items.map((s) => {
|
|
319
|
+
s.schedulableTests = [];
|
|
320
|
+
return s;
|
|
321
|
+
});
|
|
322
|
+
services.push(...teamServices);
|
|
323
|
+
}
|
|
324
|
+
return services;
|
|
325
|
+
} catch (error) {
|
|
326
|
+
console.error(`Error fetching services`, error);
|
|
327
|
+
throw new Error(`Failed to fetch services: ${error instanceof Error ? error.message : String(error)}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/tools/teams.ts
|
|
334
|
+
function createListTeamsTool(api) {
|
|
335
|
+
return {
|
|
336
|
+
name: "list_teams",
|
|
337
|
+
description: "Lists all teams you have access to",
|
|
338
|
+
schema: {},
|
|
339
|
+
handler: async () => {
|
|
340
|
+
try {
|
|
341
|
+
return await api.listTeams();
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.error(`Error fetching teams`, error);
|
|
344
|
+
throw new Error(`Failed to fetch teams: ${error instanceof Error ? error.message : String(error)}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/tools/company.ts
|
|
351
|
+
import z3 from "zod";
|
|
352
|
+
var reportPeriodSchema = z3.enum(["MONTHS", "WEEKS", "DAYS"]);
|
|
353
|
+
var teamIdSchema = z3.string().describe(
|
|
354
|
+
"The team identifier. Use the list_teams tool to find available teams and match by name or ID."
|
|
355
|
+
);
|
|
356
|
+
function createGetPricingReportTool(api) {
|
|
357
|
+
return {
|
|
358
|
+
name: "get_pricing_report",
|
|
359
|
+
description: "Fetches the pricing usage report for the company over a specified date range. Returns usage broken down by tracking period including active agents, targetable applications, and unique targets by type (host, container, application).",
|
|
360
|
+
schema: {
|
|
361
|
+
startDate: z3.string().describe("Start date (yyyy-mm-dd) for the pricing usage. Should be within the current contract duration."),
|
|
362
|
+
endDate: z3.string().describe("End date (yyyy-mm-dd) for the pricing usage. Should be within the current contract duration."),
|
|
363
|
+
trackingPeriod: z3.enum(["Daily", "Weekly", "Monthly"]).optional().describe("Tracking period for the pricing usage. Defaults to the currently configured period for the company's plan.")
|
|
364
|
+
},
|
|
365
|
+
handler: async (args) => {
|
|
366
|
+
const { startDate, endDate, trackingPeriod } = args;
|
|
367
|
+
if (!startDate || !endDate) {
|
|
368
|
+
throw new Error(`got ${JSON.stringify(args)} but expected { startDate: string, endDate: string, trackingPeriod?: "Daily" | "Weekly" | "Monthly" }`);
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
return await api.getPricingReport(startDate, endDate, trackingPeriod);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
throw new Error(`Failed to fetch pricing report: ${error instanceof Error ? error.message : String(error)}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function createGetClientSummaryTool(api) {
|
|
379
|
+
return {
|
|
380
|
+
name: "get_client_summary",
|
|
381
|
+
description: "Loads the client (agent) summary for a team over a specified time period. Shows agent activity and status. Requires a teamId, use the list_teams tool first to find available teams.",
|
|
382
|
+
schema: {
|
|
383
|
+
teamId: teamIdSchema,
|
|
384
|
+
start: z3.string().describe("Start date (yyyy-mm-dd) for the report."),
|
|
385
|
+
end: z3.string().describe("End date (yyyy-mm-dd) for the report."),
|
|
386
|
+
period: reportPeriodSchema.describe("Aggregation period for the report: MONTHS, WEEKS, or DAYS.")
|
|
387
|
+
},
|
|
388
|
+
handler: async (args) => {
|
|
389
|
+
const { teamId, start, end, period } = args;
|
|
390
|
+
if (!teamId || !start || !end || !period) {
|
|
391
|
+
throw new Error(`got ${JSON.stringify(args)} but expected { teamId: string, start: string, end: string, period: "MONTHS" | "WEEKS" | "DAYS" }`);
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
return await api.getClientSummary(teamId, start, end, period);
|
|
395
|
+
} catch (error) {
|
|
396
|
+
throw new Error(`Failed to fetch client summary: ${error instanceof Error ? error.message : String(error)}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function createGetAttackSummaryTool(api) {
|
|
402
|
+
return {
|
|
403
|
+
name: "get_attack_summary",
|
|
404
|
+
description: "Loads the attack summary for a team over a specified time period. Shows attack activity and results. Requires a teamId, use the list_teams tool first to find available teams.",
|
|
405
|
+
schema: {
|
|
406
|
+
teamId: teamIdSchema,
|
|
407
|
+
start: z3.string().describe("Start date (yyyy-mm-dd) for the report."),
|
|
408
|
+
end: z3.string().describe("End date (yyyy-mm-dd) for the report."),
|
|
409
|
+
period: reportPeriodSchema.describe("Aggregation period for the report: MONTHS, WEEKS, or DAYS.")
|
|
410
|
+
},
|
|
411
|
+
handler: async (args) => {
|
|
412
|
+
const { teamId, start, end, period } = args;
|
|
413
|
+
if (!teamId || !start || !end || !period) {
|
|
414
|
+
throw new Error(`got ${JSON.stringify(args)} but expected { teamId: string, start: string, end: string, period: "MONTHS" | "WEEKS" | "DAYS" }`);
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
return await api.getAttackSummary(teamId, start, end, period);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
throw new Error(`Failed to fetch attack summary: ${error instanceof Error ? error.message : String(error)}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/tools/openapi.ts
|
|
426
|
+
import z4 from "zod";
|
|
427
|
+
|
|
428
|
+
// src/openapi/spec-loader.ts
|
|
429
|
+
var SPEC_URL = "https://api.gremlin.com/v1/openapi.json";
|
|
430
|
+
var HTTP_METHODS = ["get", "post", "put", "delete", "patch"];
|
|
431
|
+
var SPEC_TTL_MS = 60 * 60 * 1e3;
|
|
432
|
+
var cachedSpec = null;
|
|
433
|
+
var cachedAt = 0;
|
|
434
|
+
var specFetchPromise = null;
|
|
435
|
+
async function getSpec() {
|
|
436
|
+
if (cachedSpec && Date.now() - cachedAt < SPEC_TTL_MS) return cachedSpec;
|
|
437
|
+
if (!specFetchPromise) {
|
|
438
|
+
specFetchPromise = fetchSpec().then((spec) => {
|
|
439
|
+
cachedSpec = spec;
|
|
440
|
+
cachedAt = Date.now();
|
|
441
|
+
specFetchPromise = null;
|
|
442
|
+
return spec;
|
|
443
|
+
}).catch((err) => {
|
|
444
|
+
specFetchPromise = null;
|
|
445
|
+
throw err;
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return specFetchPromise;
|
|
449
|
+
}
|
|
450
|
+
async function fetchSpec() {
|
|
451
|
+
const res = await fetch(SPEC_URL);
|
|
452
|
+
if (!res.ok) {
|
|
453
|
+
throw new Error(`Failed to fetch Gremlin OpenAPI spec: HTTP ${res.status}`);
|
|
454
|
+
}
|
|
455
|
+
return res.json();
|
|
456
|
+
}
|
|
457
|
+
function scoreToken(token, pathLower, op) {
|
|
458
|
+
let score = 0;
|
|
459
|
+
if (pathLower.includes(token)) score += 3;
|
|
460
|
+
if (op.operationId?.toLowerCase().includes(token)) score += 3;
|
|
461
|
+
if (op.summary?.toLowerCase().includes(token)) score += 2;
|
|
462
|
+
if (op.tags?.some((t) => t.toLowerCase().includes(token))) score += 2;
|
|
463
|
+
if (op.description?.toLowerCase().includes(token)) score += 1;
|
|
464
|
+
return score;
|
|
465
|
+
}
|
|
466
|
+
function tokenize(query) {
|
|
467
|
+
return query.toLowerCase().split(/[\s/\-_{}]+/).filter((t) => t.length > 0);
|
|
468
|
+
}
|
|
469
|
+
function searchSpec(spec, query, methodFilter, tagFilter, topN = 10) {
|
|
470
|
+
const queryLower = query.toLowerCase();
|
|
471
|
+
const tokens = tokenize(query);
|
|
472
|
+
const methodLower = methodFilter?.toLowerCase();
|
|
473
|
+
const tagLower = tagFilter?.toLowerCase();
|
|
474
|
+
const scored = [];
|
|
475
|
+
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
|
476
|
+
for (const method of HTTP_METHODS) {
|
|
477
|
+
const op = pathItem[method];
|
|
478
|
+
if (!op) continue;
|
|
479
|
+
if (methodLower && method !== methodLower) continue;
|
|
480
|
+
if (tagLower && !op.tags?.some((t) => t.toLowerCase().includes(tagLower))) continue;
|
|
481
|
+
const pathLower = path.toLowerCase();
|
|
482
|
+
const phraseScore = scoreToken(queryLower, pathLower, op);
|
|
483
|
+
let tokenScore = 0;
|
|
484
|
+
let allTokensMatched = true;
|
|
485
|
+
for (const token of tokens) {
|
|
486
|
+
const ts = scoreToken(token, pathLower, op);
|
|
487
|
+
if (ts === 0) {
|
|
488
|
+
allTokensMatched = false;
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
tokenScore += ts;
|
|
492
|
+
}
|
|
493
|
+
if (!allTokensMatched) tokenScore = 0;
|
|
494
|
+
const score = Math.max(phraseScore, tokenScore);
|
|
495
|
+
if (score === 0) continue;
|
|
496
|
+
scored.push({
|
|
497
|
+
score,
|
|
498
|
+
match: {
|
|
499
|
+
method: method.toUpperCase(),
|
|
500
|
+
path,
|
|
501
|
+
summary: op.summary,
|
|
502
|
+
description: op.description,
|
|
503
|
+
operationId: op.operationId,
|
|
504
|
+
tags: op.tags,
|
|
505
|
+
parameters: op.parameters,
|
|
506
|
+
requestBody: op.requestBody
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, topN).map((s) => s.match);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/tools/openapi.ts
|
|
515
|
+
function createSearchGremlinApiTool(_api) {
|
|
516
|
+
return {
|
|
517
|
+
name: "search_gremlin_api",
|
|
518
|
+
description: [
|
|
519
|
+
"Search the Gremlin OpenAPI spec to discover available API endpoints.",
|
|
520
|
+
"Returns matching endpoints with their method, path, parameters, and request body schema.",
|
|
521
|
+
"Use this before execute_gremlin_api to find the correct path and parameter names.",
|
|
522
|
+
"Paths use OpenAPI template syntax (e.g. /reliability-tests/{reliabilityTestId}/runs) \u2014",
|
|
523
|
+
"pass them directly to execute_gremlin_api."
|
|
524
|
+
].join(" "),
|
|
525
|
+
schema: {
|
|
526
|
+
query: z4.string().describe(
|
|
527
|
+
"Text to search for in endpoint paths, summaries, operationIds, tags, and descriptions."
|
|
528
|
+
),
|
|
529
|
+
method: z4.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional().describe("Filter results to a specific HTTP method."),
|
|
530
|
+
tag: z4.string().optional().describe("Filter results to endpoints with this tag (partial match, case-insensitive)."),
|
|
531
|
+
limit: z4.number().int().min(1).max(50).optional().describe("Maximum number of results to return. Defaults to 10.")
|
|
532
|
+
},
|
|
533
|
+
handler: async (args) => {
|
|
534
|
+
const { query, method, tag, limit = 10 } = args;
|
|
535
|
+
if (!query?.trim()) {
|
|
536
|
+
throw new Error("query must be a non-empty string");
|
|
537
|
+
}
|
|
538
|
+
let spec;
|
|
539
|
+
try {
|
|
540
|
+
spec = await getSpec();
|
|
541
|
+
} catch (err) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`Failed to load Gremlin OpenAPI spec: ${err instanceof Error ? err.message : String(err)}`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
const results = searchSpec(spec, query, method, tag, limit);
|
|
547
|
+
if (results.length === 0) {
|
|
548
|
+
return {
|
|
549
|
+
message: "No matching endpoints found. Try a broader query or different filters.",
|
|
550
|
+
results: []
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
message: `Found ${results.length} matching endpoint(s).`,
|
|
555
|
+
results
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function getRunPrivileges(spec, specPath, method) {
|
|
561
|
+
const op = spec.paths[specPath]?.[method.toLowerCase()];
|
|
562
|
+
if (!op?.security) return [];
|
|
563
|
+
const privileges = [];
|
|
564
|
+
for (const secReq of op.security) {
|
|
565
|
+
for (const perms of Object.values(secReq)) {
|
|
566
|
+
for (const perm of perms) {
|
|
567
|
+
if (perm.endsWith("_RUN")) privileges.push(perm);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return privileges;
|
|
572
|
+
}
|
|
573
|
+
function createExecuteGremlinApiTool(api, elicitation) {
|
|
574
|
+
return {
|
|
575
|
+
name: "execute_gremlin_api",
|
|
576
|
+
description: [
|
|
577
|
+
"Execute any Gremlin API endpoint directly.",
|
|
578
|
+
"Use search_gremlin_api first to discover the correct path, method, and parameter names.",
|
|
579
|
+
"The path should use OpenAPI template syntax for path parameters",
|
|
580
|
+
"(e.g. /reliability-tests/{reliabilityTestId}/runs) \u2014 they are substituted automatically.",
|
|
581
|
+
"WARNING: This tool can trigger real chaos experiments. Verify the endpoint and parameters carefully."
|
|
582
|
+
].join(" "),
|
|
583
|
+
schema: {
|
|
584
|
+
method: z4.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).describe("HTTP method for the request."),
|
|
585
|
+
path: z4.string().describe(
|
|
586
|
+
"API path as shown in the OpenAPI spec, e.g. /reliability-tests/{reliabilityTestId}/runs. Leading slash is optional."
|
|
587
|
+
),
|
|
588
|
+
pathParams: z4.record(z4.string()).optional().describe(
|
|
589
|
+
"Values to substitute into path template variables, e.g. { reliabilityTestId: 'abc123' }."
|
|
590
|
+
),
|
|
591
|
+
queryParams: z4.record(z4.string()).optional().describe("Query string parameters, e.g. { teamId: 'my-team' }."),
|
|
592
|
+
body: z4.record(z4.unknown()).optional().describe("Request body for POST/PUT/PATCH requests."),
|
|
593
|
+
confirmExecution: z4.boolean().optional().describe(
|
|
594
|
+
"Set to true to explicitly confirm execution of endpoints that require a *_RUN privilege, bypassing the interactive elicitation prompt. Use this when your MCP client does not support elicitation. Only set this after verifying the endpoint and parameters."
|
|
595
|
+
)
|
|
596
|
+
},
|
|
597
|
+
annotations: {
|
|
598
|
+
destructiveHint: true,
|
|
599
|
+
idempotentHint: false,
|
|
600
|
+
openWorldHint: true
|
|
601
|
+
},
|
|
602
|
+
handler: async (args) => {
|
|
603
|
+
const { method, path: rawPath, pathParams, queryParams, body, confirmExecution } = args;
|
|
604
|
+
const specPath = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
|
|
605
|
+
let runPrivileges = [];
|
|
606
|
+
try {
|
|
607
|
+
const spec = await getSpec();
|
|
608
|
+
runPrivileges = getRunPrivileges(spec, specPath, method);
|
|
609
|
+
} catch (err) {
|
|
610
|
+
console.error(
|
|
611
|
+
`Warning: could not load spec to check permissions for ${method} ${specPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
if (runPrivileges.length > 0 && !confirmExecution) {
|
|
615
|
+
let answer;
|
|
616
|
+
try {
|
|
617
|
+
answer = await elicitation.confirm(
|
|
618
|
+
`This endpoint requires the **${runPrivileges.join(", ")}** privilege(s), which can trigger live chaos experiments. Do you want to proceed?
|
|
619
|
+
|
|
620
|
+
**Endpoint:** \`${method} ${specPath}\``,
|
|
621
|
+
[
|
|
622
|
+
{ label: "Execute", value: "confirmed", description: "Proceed with the API call." },
|
|
623
|
+
{ label: "Cancel", value: "cancelled", description: "Abort without making the request." }
|
|
624
|
+
]
|
|
625
|
+
);
|
|
626
|
+
} catch (err) {
|
|
627
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
628
|
+
throw new Error(
|
|
629
|
+
`Cannot confirm execution of ${method} ${specPath}: the runtime does not support interactive prompts. This endpoint requires the ${runPrivileges.join(", ")} privilege(s). Pass confirmExecution: true to bypass the prompt and proceed directly. (${msg})`
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
if (answer !== "confirmed") {
|
|
633
|
+
throw new Error(`Execution cancelled. The request was not sent to Gremlin.`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
let resolvedPath = specPath.slice(1);
|
|
637
|
+
if (pathParams) {
|
|
638
|
+
for (const [key, value] of Object.entries(pathParams)) {
|
|
639
|
+
resolvedPath = resolvedPath.replace(
|
|
640
|
+
new RegExp(`\\{${key}\\}`, "g"),
|
|
641
|
+
encodeURIComponent(value)
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const unresolved = resolvedPath.match(/\{[^}]+\}/g);
|
|
646
|
+
if (unresolved) {
|
|
647
|
+
throw new Error(
|
|
648
|
+
`Path still contains unresolved template variables: ${unresolved.join(", ")}. Provide values for these in pathParams.`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
return await api.execute(method, resolvedPath, queryParams, body);
|
|
653
|
+
} catch (err) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
`Gremlin API call failed: ${err instanceof Error ? err.message : String(err)}`
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/tools/index.ts
|
|
663
|
+
function registerTools(server, api) {
|
|
664
|
+
const tools = [
|
|
665
|
+
createListServicesTool(api),
|
|
666
|
+
createGetServiceDependenciesTool(api),
|
|
667
|
+
createGetServiceStatusChecksTool(api),
|
|
668
|
+
createListServiceRisksTool(api),
|
|
669
|
+
createGetReliabilityReportTool(api),
|
|
670
|
+
createGetReliabilityExperimentTool(api),
|
|
671
|
+
createGetRecentReliabilityTestsTool(api),
|
|
672
|
+
createGetCurrentTestSuiteTool(api),
|
|
673
|
+
createRunReliabilityTestTool(api),
|
|
674
|
+
createGetPendingTestRunsTool(api),
|
|
675
|
+
createListTeamsTool(api),
|
|
676
|
+
createGetPricingReportTool(api),
|
|
677
|
+
createGetClientSummaryTool(api),
|
|
678
|
+
createGetAttackSummaryTool(api),
|
|
679
|
+
createSearchGremlinApiTool(api),
|
|
680
|
+
createExecuteGremlinApiTool(api, new McpElicitationClient(server))
|
|
681
|
+
];
|
|
682
|
+
for (const tool of tools) {
|
|
683
|
+
server.tool(
|
|
684
|
+
tool.name,
|
|
685
|
+
tool.description,
|
|
686
|
+
tool.schema,
|
|
687
|
+
//tool.annotations || { example: "annotation"},
|
|
688
|
+
async (args, extra) => {
|
|
689
|
+
try {
|
|
690
|
+
const result = await tool.handler(args, extra);
|
|
691
|
+
if (result && typeof result === "object" && "content" in result) {
|
|
692
|
+
return result;
|
|
693
|
+
}
|
|
694
|
+
return {
|
|
695
|
+
content: [
|
|
696
|
+
{
|
|
697
|
+
type: "text",
|
|
698
|
+
text: typeof result === "string" ? result : JSON.stringify(result, null, 2)
|
|
699
|
+
}
|
|
700
|
+
]
|
|
701
|
+
};
|
|
702
|
+
} catch (error) {
|
|
703
|
+
return {
|
|
704
|
+
content: [
|
|
705
|
+
{
|
|
706
|
+
type: "text",
|
|
707
|
+
text: error instanceof Error ? error.message : String(error)
|
|
708
|
+
}
|
|
709
|
+
],
|
|
710
|
+
isError: true
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/resources/index.ts
|
|
719
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
720
|
+
function registerResources(server, api) {
|
|
721
|
+
server.resource(
|
|
722
|
+
"teams",
|
|
723
|
+
new ResourceTemplate("gremlin://team/{teamId}", {
|
|
724
|
+
list: async () => {
|
|
725
|
+
const resources = [];
|
|
726
|
+
try {
|
|
727
|
+
const teams = await api.listTeams();
|
|
728
|
+
teams.forEach((team) => {
|
|
729
|
+
resources.push({
|
|
730
|
+
uri: `gremlin://team/${team.identifier}`,
|
|
731
|
+
name: team.name,
|
|
732
|
+
companyId: team.companyId,
|
|
733
|
+
production: team.production
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
} catch (error) {
|
|
737
|
+
console.error(`Error fetching teams`, error);
|
|
738
|
+
}
|
|
739
|
+
return { resources };
|
|
740
|
+
}
|
|
741
|
+
}),
|
|
742
|
+
async (_uri, variables) => {
|
|
743
|
+
const teamId = Array.isArray(variables.teamId) ? variables.teamId[0] : variables.teamId;
|
|
744
|
+
try {
|
|
745
|
+
const team = await api.getTeam(teamId);
|
|
746
|
+
return {
|
|
747
|
+
contents: [{
|
|
748
|
+
uri: `gremlin://team/${teamId}`,
|
|
749
|
+
text: JSON.stringify(team, null, 2),
|
|
750
|
+
mimeType: "application/json"
|
|
751
|
+
}]
|
|
752
|
+
};
|
|
753
|
+
} catch (error) {
|
|
754
|
+
throw new Error(`Failed to get team: ${error instanceof Error ? error.message : String(error)}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
);
|
|
758
|
+
server.resource(
|
|
759
|
+
"services",
|
|
760
|
+
new ResourceTemplate("gremlin://service/{teamId}/{serviceId}", {
|
|
761
|
+
list: async () => {
|
|
762
|
+
const resources = [];
|
|
763
|
+
try {
|
|
764
|
+
const self = await api.getSelf();
|
|
765
|
+
const services = [];
|
|
766
|
+
for (const team of self.team_memberships) {
|
|
767
|
+
const teamServices = (await api.listServicesForTeam(team)).items;
|
|
768
|
+
services.push(...teamServices);
|
|
769
|
+
}
|
|
770
|
+
services.forEach((service) => {
|
|
771
|
+
resources.push({
|
|
772
|
+
uri: `gremlin://service/${service.teamId}/${service.serviceId}`,
|
|
773
|
+
name: service.name,
|
|
774
|
+
targets: service.targetingStrategy || service.applicationSelector || ""
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
} catch (error) {
|
|
778
|
+
console.error(`Error fetching services`, error);
|
|
779
|
+
}
|
|
780
|
+
return { resources };
|
|
781
|
+
}
|
|
782
|
+
}),
|
|
783
|
+
async (_uri, variables) => {
|
|
784
|
+
const serviceId = Array.isArray(variables.serviceId) ? variables.serviceId[0] : variables.serviceId;
|
|
785
|
+
const teamId = Array.isArray(variables.teamId) ? variables.teamId[0] : variables.teamId;
|
|
786
|
+
try {
|
|
787
|
+
const service = await api.getService(serviceId, teamId);
|
|
788
|
+
return {
|
|
789
|
+
contents: [{
|
|
790
|
+
uri: `gremlin://service/${teamId}/${serviceId}`,
|
|
791
|
+
text: JSON.stringify(service, null, 2),
|
|
792
|
+
mimeType: "application/json"
|
|
793
|
+
}]
|
|
794
|
+
};
|
|
795
|
+
} catch (error) {
|
|
796
|
+
throw new Error(`Failed to get service: ${error instanceof Error ? error.message : String(error)}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
export {
|
|
802
|
+
McpElicitationClient,
|
|
803
|
+
createExecuteGremlinApiTool,
|
|
804
|
+
createGetAttackSummaryTool,
|
|
805
|
+
createGetClientSummaryTool,
|
|
806
|
+
createGetCurrentTestSuiteTool,
|
|
807
|
+
createGetPendingTestRunsTool,
|
|
808
|
+
createGetPricingReportTool,
|
|
809
|
+
createGetRecentReliabilityTestsTool,
|
|
810
|
+
createGetReliabilityExperimentTool,
|
|
811
|
+
createGetReliabilityReportTool,
|
|
812
|
+
createGetServiceDependenciesTool,
|
|
813
|
+
createGetServiceStatusChecksTool,
|
|
814
|
+
createListServiceRisksTool,
|
|
815
|
+
createListServicesTool,
|
|
816
|
+
createListTeamsTool,
|
|
817
|
+
createRunReliabilityTestTool,
|
|
818
|
+
createSearchGremlinApiTool,
|
|
819
|
+
registerResources,
|
|
820
|
+
registerTools
|
|
821
|
+
};
|