@borgresearch/odin-mcp 0.2.0 → 0.3.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 +91 -16
- package/dist/chunk-4LCKW236.js +205 -0
- package/dist/chunk-4LCKW236.js.map +1 -0
- package/dist/chunk-D5FYYTYV.js +1211 -0
- package/dist/chunk-D5FYYTYV.js.map +1 -0
- package/dist/chunk-MHMO7K4D.js +120 -0
- package/dist/chunk-MHMO7K4D.js.map +1 -0
- package/dist/chunk-XHJYVUHG.js +195 -0
- package/dist/chunk-XHJYVUHG.js.map +1 -0
- package/dist/client.d.ts +39 -0
- package/dist/client.js +12 -0
- package/dist/client.js.map +1 -0
- package/dist/guidance.d.ts +53 -0
- package/dist/guidance.js +18 -0
- package/dist/guidance.js.map +1 -0
- package/dist/http.d.ts +8 -0
- package/dist/http.js +34 -0
- package/dist/http.js.map +1 -0
- package/dist/index.js +15 -782
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +11 -0
- package/dist/server.js +13 -0
- package/dist/server.js.map +1 -0
- package/dist/tools.d.ts +29 -0
- package/dist/tools.js +9 -0
- package/dist/tools.js.map +1 -0
- package/package.json +28 -6
|
@@ -0,0 +1,1211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
OdinApiError
|
|
4
|
+
} from "./chunk-XHJYVUHG.js";
|
|
5
|
+
|
|
6
|
+
// src/tools/finding-actions.ts
|
|
7
|
+
import { z as z3 } from "zod";
|
|
8
|
+
|
|
9
|
+
// src/tools/finding-reference.ts
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
var findingReferenceFields = {
|
|
12
|
+
identifier: z.string().trim().min(1).optional().describe(
|
|
13
|
+
'Human-readable finding identifier such as "ODI-12". Preferred input.'
|
|
14
|
+
),
|
|
15
|
+
findingId: z.string().trim().min(1).optional().describe(
|
|
16
|
+
"Raw internal finding ID, useful when chaining from another tool."
|
|
17
|
+
)
|
|
18
|
+
};
|
|
19
|
+
var findingReferenceInput = z.object(findingReferenceFields).refine((value) => Boolean(value.identifier || value.findingId), {
|
|
20
|
+
message: "Provide either identifier or findingId."
|
|
21
|
+
});
|
|
22
|
+
function findingReferenceValue(reference) {
|
|
23
|
+
const value = reference.identifier ?? reference.findingId;
|
|
24
|
+
if (!value) {
|
|
25
|
+
throw new Error("A finding identifier or findingId is required.");
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function findingReferencePath(reference, suffix = "") {
|
|
30
|
+
return `/api/findings/${encodeURIComponent(findingReferenceValue(reference))}${suffix}`;
|
|
31
|
+
}
|
|
32
|
+
async function resolveFindingId(client, reference) {
|
|
33
|
+
if (reference.findingId) return reference.findingId;
|
|
34
|
+
const data = await client.get(
|
|
35
|
+
findingReferencePath(reference)
|
|
36
|
+
);
|
|
37
|
+
return data.remediationContext?.findingId ?? data.finding.findingId;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/tools/finding-tool.ts
|
|
41
|
+
import { z as z2 } from "zod";
|
|
42
|
+
function findingToolInput(fields) {
|
|
43
|
+
return z2.object({ ...findingReferenceFields, ...fields }).refine(
|
|
44
|
+
(value) => {
|
|
45
|
+
const reference = value;
|
|
46
|
+
return Boolean(reference.identifier || reference.findingId);
|
|
47
|
+
},
|
|
48
|
+
{ message: "Provide either identifier or findingId." }
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
var readAnnotations = {
|
|
52
|
+
readOnlyHint: true,
|
|
53
|
+
destructiveHint: false,
|
|
54
|
+
idempotentHint: true,
|
|
55
|
+
openWorldHint: false
|
|
56
|
+
};
|
|
57
|
+
var writeAnnotations = {
|
|
58
|
+
readOnlyHint: false,
|
|
59
|
+
destructiveHint: true,
|
|
60
|
+
idempotentHint: false,
|
|
61
|
+
openWorldHint: true
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// src/tools/finding-actions.ts
|
|
65
|
+
var followFindingInput = findingToolInput({
|
|
66
|
+
follow: z3.boolean().describe("True to follow the finding, false to stop following it.")
|
|
67
|
+
});
|
|
68
|
+
var followFindingTool = {
|
|
69
|
+
name: "follow_finding",
|
|
70
|
+
title: "Follow or unfollow a finding",
|
|
71
|
+
description: "Follow a finding to get its notifications, or stop following it. Requires write permission and acts as the API key creator. The assignee always follows their finding and can't unfollow it.",
|
|
72
|
+
annotations: { ...writeAnnotations, idempotentHint: true },
|
|
73
|
+
inputSchema: followFindingInput,
|
|
74
|
+
handler: (client) => async (params) => {
|
|
75
|
+
const data = await client.put(
|
|
76
|
+
findingReferencePath(params, "/subscription"),
|
|
77
|
+
{ subscribed: params.follow }
|
|
78
|
+
);
|
|
79
|
+
const label = findingReferenceValue(params);
|
|
80
|
+
return {
|
|
81
|
+
markdown: data.subscribed ? `Following ${label}.` : `Stopped following ${label}.`,
|
|
82
|
+
structured: data
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var requestFindingRetestInput = findingToolInput({});
|
|
87
|
+
var requestFindingRetestTool = {
|
|
88
|
+
name: "request_finding_retest",
|
|
89
|
+
title: "Request a finding retest",
|
|
90
|
+
description: "Queue a manual retest that checks whether a finding is still exploitable. Spends credits, so confirm with the user first. Works for findings in REPORTED, ACKNOWLEDGED, MITIGATING or OPEN_FOR_RETEST status with no retest already queued or running. Requires write permission and acts as the API key creator. Read the result later with get_finding_retests.",
|
|
91
|
+
annotations: writeAnnotations,
|
|
92
|
+
inputSchema: requestFindingRetestInput,
|
|
93
|
+
handler: (client) => async (params) => {
|
|
94
|
+
try {
|
|
95
|
+
const data = await client.post(
|
|
96
|
+
findingReferencePath(params, "/retest"),
|
|
97
|
+
{}
|
|
98
|
+
);
|
|
99
|
+
return {
|
|
100
|
+
markdown: `Queued retest ${data.retestId} for ${findingReferenceValue(params)}.`,
|
|
101
|
+
structured: data
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error instanceof OdinApiError && isRetestUnavailable(error.body)) {
|
|
105
|
+
const reason = "Retests aren't available for this organisation.";
|
|
106
|
+
return {
|
|
107
|
+
markdown: reason,
|
|
108
|
+
structured: { state: "unavailable", reason }
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
function isRetestUnavailable(body) {
|
|
116
|
+
return typeof body === "object" && body !== null && body.code === "RETEST_UNAVAILABLE";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/tools/finding-attachments.ts
|
|
120
|
+
import { z as z4 } from "zod";
|
|
121
|
+
var prepareFindingAttachmentUploadInput = findingToolInput({
|
|
122
|
+
filename: z4.string().min(1).max(500),
|
|
123
|
+
contentType: z4.enum([
|
|
124
|
+
"image/png",
|
|
125
|
+
"image/jpeg",
|
|
126
|
+
"image/gif",
|
|
127
|
+
"image/webp",
|
|
128
|
+
"application/pdf",
|
|
129
|
+
"application/zip",
|
|
130
|
+
"application/json",
|
|
131
|
+
"application/octet-stream",
|
|
132
|
+
"text/plain",
|
|
133
|
+
"text/markdown",
|
|
134
|
+
"text/csv",
|
|
135
|
+
"text/log"
|
|
136
|
+
]),
|
|
137
|
+
sizeBytes: z4.number().int().min(1).max(25 * 1024 * 1024)
|
|
138
|
+
});
|
|
139
|
+
var prepareFindingAttachmentUploadTool = {
|
|
140
|
+
name: "prepare_finding_attachment_upload",
|
|
141
|
+
title: "Prepare a private discussion attachment upload",
|
|
142
|
+
description: "Create a five-minute signed upload URL for a file up to 25 MiB. Requires write permission. Does not upload bytes or post a message. The caller must PUT the exact file to uploadUrl using requiredHeaders, without the Odin API key, then pass publicUrl as the attachment URL to create_finding_message. publicUrl is a private gs:// object reference. Treat signed URLs as temporary secrets.",
|
|
143
|
+
annotations: writeAnnotations,
|
|
144
|
+
inputSchema: prepareFindingAttachmentUploadInput,
|
|
145
|
+
handler: (client) => async (params) => {
|
|
146
|
+
const data = await client.post(
|
|
147
|
+
findingReferencePath(params, "/thread/upload-url"),
|
|
148
|
+
{
|
|
149
|
+
filename: params.filename,
|
|
150
|
+
contentType: params.contentType,
|
|
151
|
+
sizeBytes: params.sizeBytes
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
return {
|
|
155
|
+
markdown: "Upload URL prepared; no bytes have been uploaded. PUT the file to uploadUrl with requiredHeaders, then attach publicUrl in a discussion message. Do not send the Odin API key to the upload URL.",
|
|
156
|
+
structured: data
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
var getFindingAttachmentUrlInput = findingToolInput({
|
|
161
|
+
messageId: z4.string().min(1),
|
|
162
|
+
url: z4.string().url().describe(
|
|
163
|
+
"Exact private attachment URL from this message's attachments array."
|
|
164
|
+
)
|
|
165
|
+
});
|
|
166
|
+
var getFindingAttachmentUrlTool = {
|
|
167
|
+
name: "get_finding_attachment_url",
|
|
168
|
+
title: "Get a temporary attachment download URL",
|
|
169
|
+
description: "Resolve a stored discussion attachment to a ten-minute signed download URL. Requires read permission and the attachment must belong to the selected finding and message. Does not download bytes or forward the Odin key to storage. Treat the returned signed URL as a temporary secret.",
|
|
170
|
+
annotations: { ...readAnnotations, openWorldHint: true },
|
|
171
|
+
inputSchema: getFindingAttachmentUrlInput,
|
|
172
|
+
handler: (client) => async (params) => {
|
|
173
|
+
const url = await client.getRedirectUrl(
|
|
174
|
+
findingReferencePath(
|
|
175
|
+
params,
|
|
176
|
+
`/thread/attachments/${encodeURIComponent(params.messageId)}`
|
|
177
|
+
),
|
|
178
|
+
{ url: params.url }
|
|
179
|
+
);
|
|
180
|
+
return {
|
|
181
|
+
markdown: "Temporary attachment download URL resolved. No bytes were downloaded; the URL is in the structured result.",
|
|
182
|
+
structured: { url }
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
var getFindingSlackUrlInput = findingToolInput({
|
|
187
|
+
messageId: z4.string().min(1).optional(),
|
|
188
|
+
threadId: z4.string().min(1).optional()
|
|
189
|
+
}).refine((value) => Boolean(value.messageId || value.threadId), {
|
|
190
|
+
message: "Provide a messageId or threadId."
|
|
191
|
+
});
|
|
192
|
+
var getFindingSlackUrlTool = {
|
|
193
|
+
name: "get_finding_slack_url",
|
|
194
|
+
title: "Get a synced discussion's Slack permalink",
|
|
195
|
+
description: "Resolve a synced message or thread to its Slack permalink. Requires read permission. Does not navigate to Slack or forward the Odin API key.",
|
|
196
|
+
annotations: { ...readAnnotations, openWorldHint: true },
|
|
197
|
+
inputSchema: getFindingSlackUrlInput,
|
|
198
|
+
handler: (client) => async (params) => {
|
|
199
|
+
const url = await client.getRedirectUrl(
|
|
200
|
+
findingReferencePath(params, "/thread/slack-link"),
|
|
201
|
+
{ messageId: params.messageId, threadId: params.threadId }
|
|
202
|
+
);
|
|
203
|
+
return { markdown: `Slack permalink: ${url}`, structured: { url } };
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// src/tools/finding-collaboration.ts
|
|
208
|
+
import { z as z5 } from "zod";
|
|
209
|
+
var attachmentInput = z5.object({
|
|
210
|
+
type: z5.enum(["IMAGE", "FILE"]),
|
|
211
|
+
url: z5.string().url().describe(
|
|
212
|
+
"Private gs:// URL returned by prepare_finding_attachment_upload after uploading the file."
|
|
213
|
+
),
|
|
214
|
+
name: z5.string().min(1).max(500),
|
|
215
|
+
sizeBytes: z5.number().int().nonnegative().optional(),
|
|
216
|
+
mimeType: z5.string().min(1).max(255).optional()
|
|
217
|
+
});
|
|
218
|
+
var assignFindingInput = findingToolInput({
|
|
219
|
+
assigneeUserId: z5.string().min(1).nullable().describe("Organisation member user ID, or null to clear assignment.")
|
|
220
|
+
});
|
|
221
|
+
var assignFindingTool = {
|
|
222
|
+
name: "assign_finding",
|
|
223
|
+
title: "Assign a finding",
|
|
224
|
+
description: "Assign a finding to an organisation member, or clear its assignment. Requires write permission. Assignment can notify the assignee by email and Slack and synchronise connected issue trackers.",
|
|
225
|
+
annotations: { ...writeAnnotations, idempotentHint: true },
|
|
226
|
+
inputSchema: assignFindingInput,
|
|
227
|
+
handler: (client) => async (params) => {
|
|
228
|
+
const data = await client.patch(
|
|
229
|
+
findingReferencePath(params, "/assignee"),
|
|
230
|
+
{ assigneeUserId: params.assigneeUserId }
|
|
231
|
+
);
|
|
232
|
+
return {
|
|
233
|
+
markdown: `Updated assignment for ${data.finding.identifier ?? data.finding.findingId}: ${params.assigneeUserId ?? "unassigned"}.`,
|
|
234
|
+
structured: { finding: data.finding }
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
var getFindingDiscussionInput = findingToolInput({
|
|
239
|
+
limit: z5.number().int().min(1).max(100).optional(),
|
|
240
|
+
offset: z5.number().int().nonnegative().optional(),
|
|
241
|
+
before: z5.number().int().nonnegative().optional().describe(
|
|
242
|
+
"Only messages before this timestamp, in the API's stored timestamp units."
|
|
243
|
+
)
|
|
244
|
+
});
|
|
245
|
+
var getFindingDiscussionTool = {
|
|
246
|
+
name: "get_finding_discussion",
|
|
247
|
+
title: "Read finding discussions",
|
|
248
|
+
description: "Read native and Slack-synchronised conversation blocks for a finding. Each block paginates independently. API keys have no personal read or unread state.",
|
|
249
|
+
annotations: readAnnotations,
|
|
250
|
+
inputSchema: getFindingDiscussionInput,
|
|
251
|
+
handler: (client) => async (params) => {
|
|
252
|
+
const data = await client.get(
|
|
253
|
+
findingReferencePath(params, "/thread"),
|
|
254
|
+
{ limit: params.limit, offset: params.offset, before: params.before }
|
|
255
|
+
);
|
|
256
|
+
const markdown = data.blocks.length ? data.blocks.map(
|
|
257
|
+
(block) => [
|
|
258
|
+
`## ${block.thread.type} discussion ${block.thread.threadId} (${block.thread.status})`,
|
|
259
|
+
`More messages: ${block.hasMore}.`,
|
|
260
|
+
...block.messages.map(
|
|
261
|
+
(message) => `### ${message.author.displayName} (${message.messageId})
|
|
262
|
+
${message.bodyMd}`
|
|
263
|
+
)
|
|
264
|
+
].join("\n\n")
|
|
265
|
+
).join("\n\n") : "No discussions exist for this finding.";
|
|
266
|
+
return { markdown, structured: data };
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var createFindingMessageInput = findingToolInput({
|
|
270
|
+
bodyMd: z5.string().min(1).max(5e4).describe(
|
|
271
|
+
"Markdown body. Mention syntax: @[Label](USER:id), @[Label](OPERATOR:id), @[ODI-12](FINDING:id), or @[Pentest](PENTEST:id); use list_finding_mentionables to discover IDs."
|
|
272
|
+
),
|
|
273
|
+
threadId: z5.string().min(1).optional().describe("Existing block to reply to; omit for the native discussion."),
|
|
274
|
+
attachments: z5.array(attachmentInput).max(20).optional()
|
|
275
|
+
});
|
|
276
|
+
var createFindingMessageTool = {
|
|
277
|
+
name: "create_finding_message",
|
|
278
|
+
title: "Post a finding discussion message",
|
|
279
|
+
description: "Post a Markdown discussion message. Requires write permission and acts as the API key creator. May send email, mention notifications and Slack messages. Repeating a call creates another message.",
|
|
280
|
+
annotations: writeAnnotations,
|
|
281
|
+
inputSchema: createFindingMessageInput,
|
|
282
|
+
handler: (client) => async (params) => {
|
|
283
|
+
const data = await client.post(
|
|
284
|
+
findingReferencePath(params, "/thread"),
|
|
285
|
+
{
|
|
286
|
+
bodyMd: params.bodyMd,
|
|
287
|
+
threadId: params.threadId,
|
|
288
|
+
attachments: params.attachments
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
return {
|
|
292
|
+
markdown: `Posted message ${data.message.messageId}.`,
|
|
293
|
+
structured: data
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var editFindingMessageInput = findingToolInput({
|
|
298
|
+
messageId: z5.string().min(1),
|
|
299
|
+
bodyMd: z5.string().min(1).max(5e4)
|
|
300
|
+
});
|
|
301
|
+
var editFindingMessageTool = {
|
|
302
|
+
name: "edit_finding_message",
|
|
303
|
+
title: "Edit a finding discussion message",
|
|
304
|
+
description: "Edit a message authored by the API key creator. Requires write permission. New mentions can send notifications and the edit can synchronise to Slack.",
|
|
305
|
+
annotations: writeAnnotations,
|
|
306
|
+
inputSchema: editFindingMessageInput,
|
|
307
|
+
handler: (client) => async (params) => {
|
|
308
|
+
const data = await client.patch(
|
|
309
|
+
findingReferencePath(
|
|
310
|
+
params,
|
|
311
|
+
`/thread/${encodeURIComponent(params.messageId)}`
|
|
312
|
+
),
|
|
313
|
+
{ bodyMd: params.bodyMd }
|
|
314
|
+
);
|
|
315
|
+
return {
|
|
316
|
+
markdown: `Edited message ${data.message.messageId}.`,
|
|
317
|
+
structured: data
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
var deleteFindingMessageInput = findingToolInput({
|
|
322
|
+
messageId: z5.string().min(1)
|
|
323
|
+
});
|
|
324
|
+
var deleteFindingMessageTool = {
|
|
325
|
+
name: "delete_finding_message",
|
|
326
|
+
title: "Delete a finding discussion message",
|
|
327
|
+
description: "Delete a message authored by the API key creator. Requires write permission and may delete its Slack mirror. Confirm the user's intent before deleting.",
|
|
328
|
+
annotations: writeAnnotations,
|
|
329
|
+
inputSchema: deleteFindingMessageInput,
|
|
330
|
+
handler: (client) => async (params) => {
|
|
331
|
+
const data = await client.delete(
|
|
332
|
+
findingReferencePath(
|
|
333
|
+
params,
|
|
334
|
+
`/thread/${encodeURIComponent(params.messageId)}`
|
|
335
|
+
)
|
|
336
|
+
);
|
|
337
|
+
return {
|
|
338
|
+
markdown: `Deleted message ${params.messageId}.`,
|
|
339
|
+
structured: data
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
var updateFindingDiscussionInput = findingToolInput({
|
|
344
|
+
action: z5.enum(["resolve", "reopen"])
|
|
345
|
+
});
|
|
346
|
+
var updateFindingDiscussionTool = {
|
|
347
|
+
name: "update_finding_discussion",
|
|
348
|
+
title: "Resolve or reopen the native discussion",
|
|
349
|
+
description: "Resolve or reopen a finding's native discussion. Requires write permission and may synchronise the status to Slack. This does not change the finding's security status.",
|
|
350
|
+
annotations: writeAnnotations,
|
|
351
|
+
inputSchema: updateFindingDiscussionInput,
|
|
352
|
+
handler: (client) => async (params) => {
|
|
353
|
+
const data = await client.post(
|
|
354
|
+
findingReferencePath(params, "/thread/resolve"),
|
|
355
|
+
{ action: params.action }
|
|
356
|
+
);
|
|
357
|
+
return {
|
|
358
|
+
markdown: `Discussion status: ${data.thread?.status ?? params.action}.`,
|
|
359
|
+
structured: data
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
var listFindingMentionablesInput = findingToolInput({
|
|
364
|
+
q: z5.string().trim().max(100).optional()
|
|
365
|
+
});
|
|
366
|
+
var listFindingMentionablesTool = {
|
|
367
|
+
name: "list_finding_mentionables",
|
|
368
|
+
title: "Find people and entities to mention",
|
|
369
|
+
description: "Find authorised people, findings and pentests that can be mentioned in a finding discussion. Returns up to eight people, eight findings and eight pentests matching q.",
|
|
370
|
+
annotations: readAnnotations,
|
|
371
|
+
inputSchema: listFindingMentionablesInput,
|
|
372
|
+
handler: (client) => async (params) => {
|
|
373
|
+
const data = await client.get(
|
|
374
|
+
findingReferencePath(params, "/thread/mentionables"),
|
|
375
|
+
{ q: params.q }
|
|
376
|
+
);
|
|
377
|
+
return {
|
|
378
|
+
markdown: data.mentionables.map((item) => `- ${item.label}: ${item.id}`).join("\n") || "No matching mentionables.",
|
|
379
|
+
structured: data
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
var disconnectFindingDiscussionInput = findingToolInput({
|
|
384
|
+
threadId: z5.string().min(1)
|
|
385
|
+
});
|
|
386
|
+
var disconnectFindingDiscussionTool = {
|
|
387
|
+
name: "disconnect_finding_slack_thread",
|
|
388
|
+
title: "Disconnect a synced Slack thread",
|
|
389
|
+
description: "Disconnect a finding's SLACK_SYNC discussion block while preserving imported messages. Stops future synchronisation and may post a confirmation to Slack. Requires write permission. Confirm the user's intent before disconnecting.",
|
|
390
|
+
annotations: writeAnnotations,
|
|
391
|
+
inputSchema: disconnectFindingDiscussionInput,
|
|
392
|
+
handler: (client) => async (params) => {
|
|
393
|
+
const data = await client.post(
|
|
394
|
+
findingReferencePath(params, "/thread/disconnect"),
|
|
395
|
+
{ threadId: params.threadId }
|
|
396
|
+
);
|
|
397
|
+
return {
|
|
398
|
+
markdown: `Disconnected Slack discussion ${params.threadId}. Existing messages are preserved.`,
|
|
399
|
+
structured: data
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// src/tools/finding-pull-requests.ts
|
|
405
|
+
import { z as z6 } from "zod";
|
|
406
|
+
var listFindingPullRequestsInput = findingToolInput({
|
|
407
|
+
query: z6.string().optional().describe("PR number, title search, owner/repo#123, or GitHub PR URL."),
|
|
408
|
+
page: z6.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional()
|
|
409
|
+
});
|
|
410
|
+
var listFindingPullRequestsTool = {
|
|
411
|
+
name: "list_finding_pull_requests",
|
|
412
|
+
title: "Find accessible pull requests to link",
|
|
413
|
+
description: "Search accessible GitHub pull requests for this finding. Defaults to the finding's repository, otherwise its connected GitHub organisation. Results are advisory; link_finding_pull_request verifies the chosen PR. Requires read permission and a configured GitHub integration.",
|
|
414
|
+
annotations: { ...readAnnotations, openWorldHint: true },
|
|
415
|
+
inputSchema: listFindingPullRequestsInput,
|
|
416
|
+
handler: (client) => async (params) => {
|
|
417
|
+
const data = await client.get(
|
|
418
|
+
findingReferencePath(params, "/link-pr"),
|
|
419
|
+
{ query: params.query, page: params.page }
|
|
420
|
+
);
|
|
421
|
+
return {
|
|
422
|
+
markdown: [
|
|
423
|
+
`Accessible pull requests (${data.repository ?? "connected organisation"}); more pages: ${data.pagination.hasMore}.`,
|
|
424
|
+
...data.pullRequests.map(
|
|
425
|
+
(pr) => `- ${pr.repo}#${pr.number}: ${pr.title} (${pr.state}) ${pr.url}`
|
|
426
|
+
)
|
|
427
|
+
].join("\n"),
|
|
428
|
+
structured: data
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
var unlinkFindingPullRequestInput = findingToolInput({
|
|
433
|
+
prNodeId: z6.string().min(1).describe(
|
|
434
|
+
"Stored prNodeId of a manually linked pull request from get_finding."
|
|
435
|
+
)
|
|
436
|
+
});
|
|
437
|
+
var unlinkFindingPullRequestTool = {
|
|
438
|
+
name: "unlink_finding_pull_request",
|
|
439
|
+
title: "Unlink a manually linked pull request",
|
|
440
|
+
description: "Remove a manual finding-to-PR link. Requires write permission. Automatic links cannot be removed with this operation. Does not modify, close, merge or delete the GitHub PR. Confirm the user's intent before removing the link.",
|
|
441
|
+
annotations: { ...writeAnnotations, openWorldHint: false },
|
|
442
|
+
inputSchema: unlinkFindingPullRequestInput,
|
|
443
|
+
handler: (client) => async (params) => {
|
|
444
|
+
const data = await client.delete(
|
|
445
|
+
findingReferencePath(params, "/link-pr"),
|
|
446
|
+
{ prNodeId: params.prNodeId }
|
|
447
|
+
);
|
|
448
|
+
return {
|
|
449
|
+
markdown: `Removed manual pull request link ${params.prNodeId} from ${data.finding.identifier ?? data.finding.findingId}.`,
|
|
450
|
+
structured: data
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// src/tools/findings-overview.ts
|
|
456
|
+
import { z as z8 } from "zod";
|
|
457
|
+
|
|
458
|
+
// src/tools/list-findings.ts
|
|
459
|
+
import { z as z7 } from "zod";
|
|
460
|
+
|
|
461
|
+
// src/formatters/markdown.ts
|
|
462
|
+
var STATUS_LABELS = {
|
|
463
|
+
REPORTED: "Reported",
|
|
464
|
+
ACKNOWLEDGED: "Risk accepted",
|
|
465
|
+
MITIGATING: "Fixing",
|
|
466
|
+
OPEN_FOR_RETEST: "Open for Retest",
|
|
467
|
+
NEEDS_REVISION: "Needs Revision",
|
|
468
|
+
FIXED_AND_RETESTED: "Fixed & Retested"
|
|
469
|
+
};
|
|
470
|
+
var RETEST_STATUS_LABELS = {
|
|
471
|
+
QUEUED: "Queued",
|
|
472
|
+
RUNNING: "Running",
|
|
473
|
+
COMPLETED: "Completed",
|
|
474
|
+
FAILED: "Failed",
|
|
475
|
+
SKIPPED: "Skipped"
|
|
476
|
+
};
|
|
477
|
+
function formatFindingsList(data) {
|
|
478
|
+
const { findings, pagination } = data;
|
|
479
|
+
if (findings.length === 0) {
|
|
480
|
+
return "No findings match the supplied filters.";
|
|
481
|
+
}
|
|
482
|
+
const rows = findings.map((f) => {
|
|
483
|
+
const severity = `${f.severity.label} (CVSS ${f.severity.cvss.toFixed(1)})`;
|
|
484
|
+
return `| ${f.identifier ?? "\u2014"} | ${f.findingId} | ${escapePipes(f.title)} | ${severity} | ${formatStatus(f.status)} | ${formatDate(f.createdAt)} |`;
|
|
485
|
+
});
|
|
486
|
+
return [
|
|
487
|
+
`Showing ${findings.length} of ${pagination.total} finding(s). Offset ${pagination.offset}, limit ${pagination.limit}. hasMore=${pagination.hasMore}.`,
|
|
488
|
+
"",
|
|
489
|
+
"| Identifier | Internal ID | Title | Severity | Status | Created |",
|
|
490
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
491
|
+
...rows
|
|
492
|
+
].join("\n");
|
|
493
|
+
}
|
|
494
|
+
function formatFindingDetail(data) {
|
|
495
|
+
const { finding, revision } = data;
|
|
496
|
+
const context = data.remediationContext;
|
|
497
|
+
const identifier = context?.identifier ?? finding.identifier ?? null;
|
|
498
|
+
const displayIdentifier = identifier ?? finding.findingId;
|
|
499
|
+
const severity = `${finding.severity.label} (CVSS ${finding.severity.cvss.toFixed(1)})`;
|
|
500
|
+
const header = [
|
|
501
|
+
`# ${escapePipes(displayIdentifier)} \u2014 ${escapePipes(finding.title)}`,
|
|
502
|
+
"",
|
|
503
|
+
`- **Finding:** ${displayIdentifier}`,
|
|
504
|
+
`- **Internal ID:** ${finding.findingId}`,
|
|
505
|
+
`- **Severity:** ${severity}`,
|
|
506
|
+
`- **Status:** ${formatStatus(finding.status)}`,
|
|
507
|
+
`- **Repository:** ${context?.repository ?? finding.repository ?? "Not recorded"}`,
|
|
508
|
+
`- **Pentest:** ${finding.context?.pentestId ?? "standalone"}`,
|
|
509
|
+
`- **Created:** ${formatDate(finding.createdAt)}`,
|
|
510
|
+
`- **Updated:** ${formatDate(finding.updatedAt)}`
|
|
511
|
+
];
|
|
512
|
+
const content = context ? {
|
|
513
|
+
revisionNumber: context.revision.revisionNumber,
|
|
514
|
+
summary: context.revision.summary,
|
|
515
|
+
description: context.revision.description,
|
|
516
|
+
impact: context.revision.impact,
|
|
517
|
+
details: context.revision.details,
|
|
518
|
+
remediation: context.revision.remediation,
|
|
519
|
+
affectedAssets: context.revision.affectedAssets,
|
|
520
|
+
codeReference: context.revision.codeReference
|
|
521
|
+
} : revision ? {
|
|
522
|
+
revisionNumber: revision.revisionNumber,
|
|
523
|
+
summary: revision.content.summaryMd,
|
|
524
|
+
description: revision.content.descriptionMd,
|
|
525
|
+
impact: revision.content.businessImpactMd,
|
|
526
|
+
details: revision.content.detailsMd,
|
|
527
|
+
remediation: revision.content.remediationMd,
|
|
528
|
+
affectedAssets: revision.content.affectedAssets ?? [],
|
|
529
|
+
codeReference: null
|
|
530
|
+
} : null;
|
|
531
|
+
const sections = [];
|
|
532
|
+
if (!content) {
|
|
533
|
+
sections.push("_No revision content available._");
|
|
534
|
+
} else {
|
|
535
|
+
sections.push(`_Revision ${content.revisionNumber ?? "unknown"}_`);
|
|
536
|
+
if (content.summary) sections.push("## Summary", content.summary);
|
|
537
|
+
if (content.description)
|
|
538
|
+
sections.push("## Description", content.description);
|
|
539
|
+
if (content.impact) sections.push("## Business Impact", content.impact);
|
|
540
|
+
if (content.details) sections.push("## Details", content.details);
|
|
541
|
+
if (content.remediation)
|
|
542
|
+
sections.push("## Remediation", content.remediation);
|
|
543
|
+
if (content.codeReference) {
|
|
544
|
+
sections.push(
|
|
545
|
+
"## Code reference",
|
|
546
|
+
`- ${content.codeReference.file}:${content.codeReference.line}`
|
|
547
|
+
);
|
|
548
|
+
if (content.codeReference.snippet) {
|
|
549
|
+
sections.push("```", content.codeReference.snippet, "```");
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (content.affectedAssets?.length) {
|
|
553
|
+
sections.push(
|
|
554
|
+
"## Affected Assets",
|
|
555
|
+
content.affectedAssets.map((asset) => `- ${asset.display} (${asset.assetId})`).join("\n")
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (context) {
|
|
560
|
+
sections.push(
|
|
561
|
+
"## Remediation workflow",
|
|
562
|
+
`- **Suggested branch:** ${context.suggestedBranch ?? "Not available"}`,
|
|
563
|
+
`- **Ready for a user-created PR:** ${yesNo(context.workflow.readyForUserCreatedPullRequest)}`,
|
|
564
|
+
`- **GitHub connected:** ${yesNo(context.workflow.githubConnected)}`,
|
|
565
|
+
`- **Automatic retests:** ${context.workflow.automaticRetestsReady ? "Ready after merge" : "Not ready"}`
|
|
566
|
+
);
|
|
567
|
+
if (context.workflow.blockers.length) {
|
|
568
|
+
sections.push(
|
|
569
|
+
"### Blockers",
|
|
570
|
+
context.workflow.blockers.map((blocker) => `- ${blocker}`).join("\n")
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
if (context.workflow.nextSteps.length) {
|
|
574
|
+
sections.push(
|
|
575
|
+
"### Next steps",
|
|
576
|
+
context.workflow.nextSteps.map((step) => `- ${step}`).join("\n")
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
if (context.linkedPullRequests.length) {
|
|
580
|
+
sections.push(
|
|
581
|
+
"## Linked GitHub pull requests",
|
|
582
|
+
context.linkedPullRequests.map(
|
|
583
|
+
(pullRequest) => `- [#${pullRequest.number}](${pullRequest.url}) ${escapePipes(pullRequest.title)} \u2014 ${pullRequest.state}, linked via ${pullRequest.linkedVia}`
|
|
584
|
+
).join("\n")
|
|
585
|
+
);
|
|
586
|
+
} else {
|
|
587
|
+
sections.push("## Linked GitHub pull requests", "_None linked._");
|
|
588
|
+
}
|
|
589
|
+
sections.push("## Latest retest");
|
|
590
|
+
if (context.latestRetest.state === "unavailable") {
|
|
591
|
+
sections.push(`_${context.latestRetest.reason}_`);
|
|
592
|
+
} else if (context.latestRetest.state === "none") {
|
|
593
|
+
sections.push("_No retest history is available yet._");
|
|
594
|
+
} else {
|
|
595
|
+
sections.push(formatRetestSummary(context.latestRetest.retest));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return [...header, "", ...sections].join("\n");
|
|
599
|
+
}
|
|
600
|
+
function formatFindingStatusUpdate(finding) {
|
|
601
|
+
const identifier = finding.identifier ?? finding.findingId;
|
|
602
|
+
return [
|
|
603
|
+
`Updated finding **${identifier}** (internal ID ${finding.findingId}) status to **${formatStatus(finding.status)}**.`,
|
|
604
|
+
`Title: ${finding.title}`
|
|
605
|
+
].join("\n");
|
|
606
|
+
}
|
|
607
|
+
function formatFindingPullRequestLink(data) {
|
|
608
|
+
const identifier = data.finding.identifier ?? data.finding.findingId;
|
|
609
|
+
const pullRequest = data.pullRequest;
|
|
610
|
+
const readiness = data.workflow.automaticRetestsReady ? "Automatic retesting is ready after this PR merges." : "Automatic retesting is not currently ready; review the blockers below.";
|
|
611
|
+
const blockers = data.workflow.blockers.length ? `
|
|
612
|
+
${data.workflow.blockers.map((blocker) => `- ${blocker}`).join("\n")}` : "";
|
|
613
|
+
return [
|
|
614
|
+
`Linked existing GitHub pull request [#${pullRequest.number}](${pullRequest.url}) to **${identifier}**.`,
|
|
615
|
+
`Repository: ${pullRequest.repo}; state: ${pullRequest.state}; finding status: ${formatStatus(data.finding.status)}.`,
|
|
616
|
+
"Odin did not create, modify, merge, or push this pull request.",
|
|
617
|
+
readiness,
|
|
618
|
+
blockers ? `Workflow blockers:${blockers}` : ""
|
|
619
|
+
].filter(Boolean).join("\n");
|
|
620
|
+
}
|
|
621
|
+
function formatFindingRetests(data) {
|
|
622
|
+
if (data.state === "none" || data.retests.length === 0) {
|
|
623
|
+
return "No retest history is available for this finding.";
|
|
624
|
+
}
|
|
625
|
+
return [
|
|
626
|
+
`Retest history (${data.retests.length}):`,
|
|
627
|
+
"",
|
|
628
|
+
"| Retest | Status | Trigger | Verdict | Created | Completed |",
|
|
629
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
630
|
+
...data.retests.map(
|
|
631
|
+
(retest) => `| ${retest.retestId} | ${RETEST_STATUS_LABELS[retest.status]} | ${retest.trigger} | ${retest.combinedVerdict ?? "\u2014"} | ${formatDate(retest.createdAt)} | ${formatDate(retest.completedAt ?? 0)} |`
|
|
632
|
+
),
|
|
633
|
+
"",
|
|
634
|
+
"Queued and running entries are still in progress; completed, skipped, and failed entries are final for that run."
|
|
635
|
+
].join("\n");
|
|
636
|
+
}
|
|
637
|
+
function formatPentestsList(data) {
|
|
638
|
+
if (data.pentests.length === 0) return "No pentests found.";
|
|
639
|
+
const rows = data.pentests.map(formatPentestRow);
|
|
640
|
+
return [
|
|
641
|
+
"| Pentest ID | Name | Status | Start | End | Scope |",
|
|
642
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
643
|
+
...rows
|
|
644
|
+
].join("\n");
|
|
645
|
+
}
|
|
646
|
+
function formatPentestDetail(data) {
|
|
647
|
+
const p = data.pentest;
|
|
648
|
+
const end = p.endAt ? formatDate(p.endAt) : "open";
|
|
649
|
+
const scope = p.scope.length ? p.scope.map((s) => `- ${s}`).join("\n") : "_No scope defined._";
|
|
650
|
+
return [
|
|
651
|
+
`# ${p.name}`,
|
|
652
|
+
"",
|
|
653
|
+
`- **ID:** ${p.pentestId}`,
|
|
654
|
+
`- **Status:** ${p.status}`,
|
|
655
|
+
`- **Start:** ${formatDate(p.startAt)}`,
|
|
656
|
+
`- **End:** ${end}`,
|
|
657
|
+
`- **Created:** ${formatDate(p.createdAt)}`,
|
|
658
|
+
"",
|
|
659
|
+
"## Scope",
|
|
660
|
+
"",
|
|
661
|
+
scope
|
|
662
|
+
].join("\n");
|
|
663
|
+
}
|
|
664
|
+
function formatFindingsForAsset(findings) {
|
|
665
|
+
if (findings.length === 0) return "_No findings reference this asset._";
|
|
666
|
+
return [
|
|
667
|
+
`## Findings (${findings.length})`,
|
|
668
|
+
"",
|
|
669
|
+
"| Identifier | Internal ID | Title | Severity | Status | Created |",
|
|
670
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
671
|
+
...findings.map(formatFindingRow)
|
|
672
|
+
].join("\n");
|
|
673
|
+
}
|
|
674
|
+
function formatAssetsList(data) {
|
|
675
|
+
const { assets, pagination } = data;
|
|
676
|
+
if (assets.length === 0) return "No assets match the supplied filters.";
|
|
677
|
+
const rows = assets.map(formatAssetRow);
|
|
678
|
+
return [
|
|
679
|
+
`Showing ${assets.length} of ${pagination.total} asset(s). Offset ${pagination.offset}, limit ${pagination.limit}. hasMore=${pagination.hasMore}.`,
|
|
680
|
+
"",
|
|
681
|
+
"| Asset ID | Type | Value | Source | Status | Last seen |",
|
|
682
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
683
|
+
...rows
|
|
684
|
+
].join("\n");
|
|
685
|
+
}
|
|
686
|
+
function formatAssetDetail(data) {
|
|
687
|
+
const { asset, findings } = data;
|
|
688
|
+
const header = [
|
|
689
|
+
`# ${asset.value}`,
|
|
690
|
+
"",
|
|
691
|
+
`- **ID:** ${asset.assetId}`,
|
|
692
|
+
`- **Type:** ${asset.type}`,
|
|
693
|
+
`- **Source:** ${asset.source}`,
|
|
694
|
+
`- **Status:** ${asset.status}`,
|
|
695
|
+
`- **Last seen:** ${formatDate(asset.lastSeenAt)}`,
|
|
696
|
+
`- **Created:** ${formatDate(asset.createdAt)}`
|
|
697
|
+
];
|
|
698
|
+
return [...header, "", formatFindingsForAsset(findings)].join("\n");
|
|
699
|
+
}
|
|
700
|
+
function formatFindingRow(finding) {
|
|
701
|
+
const severity = `${finding.severity.label} (CVSS ${finding.severity.cvss.toFixed(1)})`;
|
|
702
|
+
return `| ${finding.identifier ?? "\u2014"} | ${finding.findingId} | ${escapePipes(finding.title)} | ${severity} | ${formatStatus(finding.status)} | ${formatDate(finding.createdAt)} |`;
|
|
703
|
+
}
|
|
704
|
+
function formatPentestRow(p) {
|
|
705
|
+
const end = p.endAt ? formatDate(p.endAt) : "open";
|
|
706
|
+
const scope = p.scope.slice(0, 3).join(", ") + (p.scope.length > 3 ? ", \u2026" : "");
|
|
707
|
+
return `| ${p.pentestId} | ${escapePipes(p.name)} | ${p.status} | ${formatDate(p.startAt)} | ${end} | ${escapePipes(scope)} |`;
|
|
708
|
+
}
|
|
709
|
+
function formatAssetRow(a) {
|
|
710
|
+
return `| ${a.assetId} | ${a.type} | ${escapePipes(a.value)} | ${a.source} | ${a.status} | ${formatDate(a.lastSeenAt)} |`;
|
|
711
|
+
}
|
|
712
|
+
function formatRetestSummary(retest) {
|
|
713
|
+
return [
|
|
714
|
+
`- **Status:** ${RETEST_STATUS_LABELS[retest.status]}`,
|
|
715
|
+
`- **Trigger:** ${retest.trigger}`,
|
|
716
|
+
`- **Verdict:** ${retest.combinedVerdict ?? "Not available yet"}`,
|
|
717
|
+
`- **Finding action:** ${retest.statusAction ?? "None"}`,
|
|
718
|
+
`- **Created:** ${formatDate(retest.createdAt)}`,
|
|
719
|
+
`- **Completed:** ${formatDate(retest.completedAt ?? 0)}`
|
|
720
|
+
].join("\n");
|
|
721
|
+
}
|
|
722
|
+
function formatStatus(status) {
|
|
723
|
+
return STATUS_LABELS[status] ?? status;
|
|
724
|
+
}
|
|
725
|
+
function yesNo(value) {
|
|
726
|
+
return value ? "Yes" : "No";
|
|
727
|
+
}
|
|
728
|
+
function escapePipes(value) {
|
|
729
|
+
return value.replace(/\|/g, "\\|");
|
|
730
|
+
}
|
|
731
|
+
function formatDate(epoch) {
|
|
732
|
+
if (!epoch) return "\u2014";
|
|
733
|
+
const ms = epoch < 1e12 ? epoch * 1e3 : epoch;
|
|
734
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/tools/list-findings.ts
|
|
738
|
+
var severityInput = z7.enum([
|
|
739
|
+
"CRITICAL",
|
|
740
|
+
"HIGH",
|
|
741
|
+
"MEDIUM",
|
|
742
|
+
"LOW",
|
|
743
|
+
"INFORMATIONAL"
|
|
744
|
+
]);
|
|
745
|
+
var statusInput = z7.enum([
|
|
746
|
+
"REPORTED",
|
|
747
|
+
"ACKNOWLEDGED",
|
|
748
|
+
"MITIGATING",
|
|
749
|
+
"OPEN_FOR_RETEST",
|
|
750
|
+
"NEEDS_REVISION",
|
|
751
|
+
"FIXED_AND_RETESTED"
|
|
752
|
+
]);
|
|
753
|
+
var sourceInput = z7.enum([
|
|
754
|
+
"MANUAL",
|
|
755
|
+
"WATCHDOG",
|
|
756
|
+
"IMPORT",
|
|
757
|
+
"AGENT",
|
|
758
|
+
"THREAT_MODEL"
|
|
759
|
+
]);
|
|
760
|
+
var confirmationTypeInput = z7.enum(["static", "partial", "exploited"]);
|
|
761
|
+
var assigneeInput = z7.string().trim().min(1).max(500);
|
|
762
|
+
var findingFilterFields = {
|
|
763
|
+
pentestId: z7.string().optional().describe("Filter to a single pentest."),
|
|
764
|
+
assetId: z7.string().optional().describe(
|
|
765
|
+
"Filter to findings whose latest revision references this asset."
|
|
766
|
+
),
|
|
767
|
+
repository: z7.string().optional().describe("Filter to a source repository such as owner/name."),
|
|
768
|
+
severity: z7.union([severityInput, z7.array(severityInput).min(1)]).optional(),
|
|
769
|
+
status: z7.union([statusInput, z7.array(statusInput).min(1)]).optional(),
|
|
770
|
+
excludeStatuses: z7.union([statusInput, z7.array(statusInput).min(1)]).optional().describe("Exclude these client-visible statuses."),
|
|
771
|
+
source: z7.union([sourceInput, z7.array(sourceInput).min(1)]).optional(),
|
|
772
|
+
confirmationType: z7.union([confirmationTypeInput, z7.array(confirmationTypeInput).min(1)]).optional(),
|
|
773
|
+
assignee: z7.union([assigneeInput, z7.array(assigneeInput).min(1)]).refine((value) => String(value).length <= 500, {
|
|
774
|
+
message: "Combined assignee filter must be at most 500 characters."
|
|
775
|
+
}).optional().describe(
|
|
776
|
+
"Client user ID(s), or __unassigned__ to include findings without an assignee."
|
|
777
|
+
),
|
|
778
|
+
cvssMin: z7.number().min(0).max(10).optional(),
|
|
779
|
+
cvssMax: z7.number().min(0).max(10).optional(),
|
|
780
|
+
hideDormant: z7.boolean().optional().describe("Hide findings on dormant assets."),
|
|
781
|
+
search: z7.string().optional().describe("Free-text search on finding titles and human identifiers.")
|
|
782
|
+
};
|
|
783
|
+
var listFindingsInput = z7.object({
|
|
784
|
+
...findingFilterFields,
|
|
785
|
+
lean: z7.boolean().optional().describe(
|
|
786
|
+
"Exclude heavy evidence from list results; details stay available."
|
|
787
|
+
),
|
|
788
|
+
stats: z7.boolean().optional().describe("Include aggregate statistics (defaults to true)."),
|
|
789
|
+
sortBy: z7.enum(["severity", "createdAt"]).optional(),
|
|
790
|
+
sortOrder: z7.enum(["asc", "desc"]).optional(),
|
|
791
|
+
limit: z7.number().int().min(1).max(200).optional().default(50),
|
|
792
|
+
offset: z7.number().int().min(0).optional().default(0)
|
|
793
|
+
});
|
|
794
|
+
function serializeFindingQuery(params) {
|
|
795
|
+
return Object.fromEntries(
|
|
796
|
+
Object.entries(params).map(([key, value]) => [
|
|
797
|
+
key,
|
|
798
|
+
Array.isArray(value) ? value.join(",") : typeof value === "boolean" ? String(value) : value
|
|
799
|
+
])
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
var listFindingsTool = {
|
|
803
|
+
name: "list_findings",
|
|
804
|
+
title: "List findings",
|
|
805
|
+
description: "List security findings for the organisation. Filter by pentest, asset, repository, severity, client-visible status, source, confirmation type, assignee, CVSS range, or dormant assets. Search titles or human identifiers such as ODI-12.",
|
|
806
|
+
annotations: readAnnotations,
|
|
807
|
+
inputSchema: listFindingsInput,
|
|
808
|
+
handler: (client) => async (params) => {
|
|
809
|
+
const data = await client.get(
|
|
810
|
+
"/api/findings",
|
|
811
|
+
serializeFindingQuery(params)
|
|
812
|
+
);
|
|
813
|
+
return {
|
|
814
|
+
markdown: formatFindingsList(data),
|
|
815
|
+
structured: {
|
|
816
|
+
findings: data.findings,
|
|
817
|
+
stats: data.stats,
|
|
818
|
+
pagination: data.pagination
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
// src/tools/findings-overview.ts
|
|
825
|
+
var getFindingStatsInput = z8.object({
|
|
826
|
+
pentestId: z8.string().optional()
|
|
827
|
+
});
|
|
828
|
+
var getFindingStatsTool = {
|
|
829
|
+
name: "get_finding_stats",
|
|
830
|
+
title: "Get finding statistics",
|
|
831
|
+
description: "Read aggregate finding totals by severity and status, optionally scoped to a pentest. These totals do not apply other finding list filters.",
|
|
832
|
+
annotations: readAnnotations,
|
|
833
|
+
inputSchema: getFindingStatsInput,
|
|
834
|
+
handler: (client) => async (params) => {
|
|
835
|
+
const data = await client.get(
|
|
836
|
+
"/api/findings/stats",
|
|
837
|
+
params
|
|
838
|
+
);
|
|
839
|
+
return {
|
|
840
|
+
markdown: `Total findings: ${data.stats.total}.
|
|
841
|
+
${JSON.stringify(data.stats, null, 2)}`,
|
|
842
|
+
structured: data
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
var listFindingRepositoriesTool = {
|
|
847
|
+
name: "list_finding_repositories",
|
|
848
|
+
title: "List repositories with findings",
|
|
849
|
+
description: "List organisation repositories available for the repository filter in list_findings.",
|
|
850
|
+
annotations: readAnnotations,
|
|
851
|
+
inputSchema: z8.object({}),
|
|
852
|
+
handler: (client) => async () => {
|
|
853
|
+
const data = await client.get(
|
|
854
|
+
"/api/findings/repositories"
|
|
855
|
+
);
|
|
856
|
+
return {
|
|
857
|
+
markdown: data.repositories.map((repository) => `- ${repository}`).join("\n") || "No repositories with findings.",
|
|
858
|
+
structured: data
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
var listFindingAssetsTool = {
|
|
863
|
+
name: "list_finding_assets",
|
|
864
|
+
title: "List assets referenced by findings",
|
|
865
|
+
description: "List organisation assets referenced by findings for use with the assetId filter. This differs from listing all attack-surface assets.",
|
|
866
|
+
annotations: readAnnotations,
|
|
867
|
+
inputSchema: z8.object({}),
|
|
868
|
+
handler: (client) => async () => {
|
|
869
|
+
const data = await client.get(
|
|
870
|
+
"/api/findings/assets"
|
|
871
|
+
);
|
|
872
|
+
return {
|
|
873
|
+
markdown: data.assets.map((asset) => `- ${asset.value} (${asset.type}, ${asset.assetId})`).join("\n") || "No assets referenced by findings.",
|
|
874
|
+
structured: data
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
var exportFindingsInput = z8.object(findingFilterFields);
|
|
879
|
+
var exportFindingsTool = {
|
|
880
|
+
name: "export_findings",
|
|
881
|
+
title: "Export findings and revision content",
|
|
882
|
+
description: "Export findings matching the supplied filters, including revision Markdown and affected assets. The API returns all matching rows without pagination; narrow the filters for large organisations. This reads data and creates no file.",
|
|
883
|
+
annotations: readAnnotations,
|
|
884
|
+
inputSchema: exportFindingsInput,
|
|
885
|
+
handler: (client) => async (params) => {
|
|
886
|
+
const data = await client.get(
|
|
887
|
+
"/api/findings/export",
|
|
888
|
+
serializeFindingQuery(params)
|
|
889
|
+
);
|
|
890
|
+
return {
|
|
891
|
+
markdown: `Exported ${data.findings.length} matching finding(s). Revision content is in the structured findings result.`,
|
|
892
|
+
structured: data
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
var findingSummariesInput = z8.object({
|
|
897
|
+
findingIds: z8.array(z8.string().trim().min(1)).max(200).describe(
|
|
898
|
+
"Up to 200 internal finding IDs from list_findings. Human identifiers are not accepted by these batch endpoints."
|
|
899
|
+
)
|
|
900
|
+
});
|
|
901
|
+
var getFindingRetestSummariesTool = {
|
|
902
|
+
name: "get_finding_retest_summaries",
|
|
903
|
+
title: "Get latest retests for several findings",
|
|
904
|
+
description: "Read the latest retest summary for up to 200 finding IDs. Requires read permission and the organisation's retest capability; never starts a retest or spends credits. Missing entries have no latest retest.",
|
|
905
|
+
annotations: readAnnotations,
|
|
906
|
+
inputSchema: findingSummariesInput,
|
|
907
|
+
handler: (client) => async (params) => {
|
|
908
|
+
const data = await client.post(
|
|
909
|
+
"/api/findings/retest-summaries",
|
|
910
|
+
params
|
|
911
|
+
);
|
|
912
|
+
return {
|
|
913
|
+
markdown: `Latest retests available for ${Object.keys(data.summaries).length} finding(s).
|
|
914
|
+
${JSON.stringify(data.summaries, null, 2)}`,
|
|
915
|
+
structured: data
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
var getFindingDiscussionSummariesTool = {
|
|
920
|
+
name: "get_finding_discussion_summaries",
|
|
921
|
+
title: "Get discussion summaries for several findings",
|
|
922
|
+
description: "Read message counts and unresolved discussion state for up to 200 finding IDs. API keys have no personal unread state; unreadCount is zero for key-authenticated calls.",
|
|
923
|
+
annotations: readAnnotations,
|
|
924
|
+
inputSchema: findingSummariesInput,
|
|
925
|
+
handler: (client) => async (params) => {
|
|
926
|
+
const data = await client.post(
|
|
927
|
+
"/api/findings/thread-summaries",
|
|
928
|
+
params
|
|
929
|
+
);
|
|
930
|
+
return {
|
|
931
|
+
markdown: `Discussion summaries available for ${Object.keys(data.summaries).length} finding(s).
|
|
932
|
+
${JSON.stringify(data.summaries, null, 2)}`,
|
|
933
|
+
structured: data
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
// src/tools/additional-tools.ts
|
|
939
|
+
var additionalFindingTools = [
|
|
940
|
+
getFindingStatsTool,
|
|
941
|
+
listFindingRepositoriesTool,
|
|
942
|
+
listFindingAssetsTool,
|
|
943
|
+
exportFindingsTool,
|
|
944
|
+
getFindingRetestSummariesTool,
|
|
945
|
+
getFindingDiscussionSummariesTool,
|
|
946
|
+
assignFindingTool,
|
|
947
|
+
getFindingDiscussionTool,
|
|
948
|
+
createFindingMessageTool,
|
|
949
|
+
editFindingMessageTool,
|
|
950
|
+
deleteFindingMessageTool,
|
|
951
|
+
updateFindingDiscussionTool,
|
|
952
|
+
listFindingMentionablesTool,
|
|
953
|
+
disconnectFindingDiscussionTool,
|
|
954
|
+
prepareFindingAttachmentUploadTool,
|
|
955
|
+
getFindingAttachmentUrlTool,
|
|
956
|
+
getFindingSlackUrlTool,
|
|
957
|
+
listFindingPullRequestsTool,
|
|
958
|
+
unlinkFindingPullRequestTool,
|
|
959
|
+
followFindingTool,
|
|
960
|
+
requestFindingRetestTool
|
|
961
|
+
];
|
|
962
|
+
|
|
963
|
+
// src/tools/get-asset.ts
|
|
964
|
+
import { z as z9 } from "zod";
|
|
965
|
+
var getAssetInput = z9.object({
|
|
966
|
+
assetId: z9.string().min(1)
|
|
967
|
+
});
|
|
968
|
+
var getAssetTool = {
|
|
969
|
+
name: "get_asset",
|
|
970
|
+
title: "Get asset",
|
|
971
|
+
description: "Fetch a single attack-surface asset by ID, including the findings that reference it in their latest revision.",
|
|
972
|
+
annotations: readAnnotations,
|
|
973
|
+
inputSchema: getAssetInput,
|
|
974
|
+
handler: (client) => async (params) => {
|
|
975
|
+
const data = await client.get(
|
|
976
|
+
`/api/assets/${encodeURIComponent(params.assetId)}`
|
|
977
|
+
);
|
|
978
|
+
return {
|
|
979
|
+
markdown: formatAssetDetail(data),
|
|
980
|
+
structured: { asset: data.asset, findings: data.findings }
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
// src/tools/get-finding.ts
|
|
986
|
+
var getFindingInput = findingReferenceInput;
|
|
987
|
+
var getFindingTool = {
|
|
988
|
+
name: "get_finding",
|
|
989
|
+
title: "Get finding remediation context",
|
|
990
|
+
description: "Fetch a finding by human identifier such as ODI-12 (preferred) or by internal findingId. Returns bounded remediation context, the suggested branch name, linked GitHub pull requests, workflow readiness, and retest state. Odin only provides context and can link an existing PR; it never creates commits, pushes branches, or creates pull requests.",
|
|
991
|
+
annotations: readAnnotations,
|
|
992
|
+
inputSchema: getFindingInput,
|
|
993
|
+
handler: (client) => async (params) => {
|
|
994
|
+
const data = await client.get(
|
|
995
|
+
findingReferencePath(params)
|
|
996
|
+
);
|
|
997
|
+
return {
|
|
998
|
+
markdown: formatFindingDetail(data),
|
|
999
|
+
structured: {
|
|
1000
|
+
finding: data.remediationContext ?? data.finding,
|
|
1001
|
+
revision: data.remediationContext?.revision ?? data.revision,
|
|
1002
|
+
remediationContext: data.remediationContext ?? null
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
// src/tools/get-finding-retests.ts
|
|
1009
|
+
import { z as z10 } from "zod";
|
|
1010
|
+
var getFindingRetestsInput = z10.object(findingReferenceFields).refine((value) => Boolean(value.identifier || value.findingId), {
|
|
1011
|
+
message: "Provide either identifier or findingId."
|
|
1012
|
+
});
|
|
1013
|
+
var getFindingRetestsTool = {
|
|
1014
|
+
name: "get_finding_retests",
|
|
1015
|
+
title: "Get finding retest history",
|
|
1016
|
+
description: "Read the retest history for a finding by human identifier such as ODI-12 or internal findingId. Distinguishes no history, queued or running, completed, skipped, failed, and gated/unavailable results. This tool never triggers a retest or spends credits.",
|
|
1017
|
+
annotations: readAnnotations,
|
|
1018
|
+
inputSchema: getFindingRetestsInput,
|
|
1019
|
+
handler: (client) => async (params) => {
|
|
1020
|
+
try {
|
|
1021
|
+
const data = await client.get(
|
|
1022
|
+
findingReferencePath(params, "/retests")
|
|
1023
|
+
);
|
|
1024
|
+
return {
|
|
1025
|
+
markdown: formatFindingRetests(data),
|
|
1026
|
+
structured: {
|
|
1027
|
+
state: data.state,
|
|
1028
|
+
retests: data.retests
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
if (error instanceof OdinApiError && error.status === 404 && isRetestHistoryUnavailable(error.body)) {
|
|
1033
|
+
const reason = "Retest history is gated or unavailable for this organisation.";
|
|
1034
|
+
return {
|
|
1035
|
+
markdown: reason,
|
|
1036
|
+
structured: { state: "unavailable", reason, retests: [] }
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
throw error;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
function isRetestHistoryUnavailable(body) {
|
|
1044
|
+
return typeof body === "object" && body !== null && "code" in body && body.code === "RETEST_HISTORY_UNAVAILABLE";
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/tools/get-pentest.ts
|
|
1048
|
+
import { z as z11 } from "zod";
|
|
1049
|
+
var getPentestInput = z11.object({
|
|
1050
|
+
pentestId: z11.string().min(1)
|
|
1051
|
+
});
|
|
1052
|
+
var getPentestTool = {
|
|
1053
|
+
name: "get_pentest",
|
|
1054
|
+
title: "Get pentest",
|
|
1055
|
+
description: "Fetch a single pentest by ID, including status, scheduled window, and in-scope targets.",
|
|
1056
|
+
annotations: readAnnotations,
|
|
1057
|
+
inputSchema: getPentestInput,
|
|
1058
|
+
handler: (client) => async (params) => {
|
|
1059
|
+
const data = await client.get(
|
|
1060
|
+
`/api/pentests/${encodeURIComponent(params.pentestId)}`
|
|
1061
|
+
);
|
|
1062
|
+
return {
|
|
1063
|
+
markdown: formatPentestDetail(data),
|
|
1064
|
+
structured: { pentest: data.pentest }
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
// src/tools/link-finding-pull-request.ts
|
|
1070
|
+
import { z as z12 } from "zod";
|
|
1071
|
+
var linkFindingPullRequestInput = z12.object({
|
|
1072
|
+
...findingReferenceFields,
|
|
1073
|
+
url: z12.string().url().describe("Existing GitHub pull request URL to link to the finding.")
|
|
1074
|
+
}).refine((value) => Boolean(value.identifier || value.findingId), {
|
|
1075
|
+
message: "Provide either identifier or findingId."
|
|
1076
|
+
});
|
|
1077
|
+
var linkFindingPullRequestTool = {
|
|
1078
|
+
name: "link_finding_pull_request",
|
|
1079
|
+
title: "Link an existing GitHub pull request",
|
|
1080
|
+
description: "Link an existing accessible GitHub pull request to a finding identified by ODI-12 or findingId. This operation is idempotent. It does not create, modify, merge, or push a pull request, commit, or branch, and it requires an API key with Read & Write permission; cookie-authenticated callers also need member role. Ask the user before linking.",
|
|
1081
|
+
annotations: { ...writeAnnotations, idempotentHint: true },
|
|
1082
|
+
inputSchema: linkFindingPullRequestInput,
|
|
1083
|
+
handler: (client) => async (params) => {
|
|
1084
|
+
const data = await client.post(
|
|
1085
|
+
findingReferencePath(params, "/link-pr"),
|
|
1086
|
+
{ url: params.url }
|
|
1087
|
+
);
|
|
1088
|
+
return {
|
|
1089
|
+
markdown: formatFindingPullRequestLink(data),
|
|
1090
|
+
structured: {
|
|
1091
|
+
finding: data.finding,
|
|
1092
|
+
pullRequest: data.pullRequest,
|
|
1093
|
+
linkSource: data.linkSource,
|
|
1094
|
+
workflow: data.workflow
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
// src/tools/list-assets.ts
|
|
1101
|
+
import { z as z13 } from "zod";
|
|
1102
|
+
var listAssetsInput = z13.object({
|
|
1103
|
+
type: z13.enum(["DOMAIN", "SUBDOMAIN", "IP"]).optional(),
|
|
1104
|
+
source: z13.enum(["MANUAL", "RECON", "IMPORT", "AGENT"]).optional(),
|
|
1105
|
+
status: z13.string().optional().describe(
|
|
1106
|
+
"Dynamic asset status set by recon (e.g. ACTIVE, INACTIVE, TLS_ERROR)."
|
|
1107
|
+
),
|
|
1108
|
+
search: z13.string().optional(),
|
|
1109
|
+
stats: z13.boolean().optional().describe("Include aggregate statistics (defaults to true)."),
|
|
1110
|
+
sortBy: z13.enum(["lastSeenAt", "createdAt", "value"]).optional(),
|
|
1111
|
+
sortOrder: z13.enum(["asc", "desc"]).optional(),
|
|
1112
|
+
limit: z13.number().int().min(1).max(100).optional().default(50),
|
|
1113
|
+
offset: z13.number().int().min(0).optional().default(0)
|
|
1114
|
+
});
|
|
1115
|
+
var listAssetsTool = {
|
|
1116
|
+
name: "list_assets",
|
|
1117
|
+
title: "List assets",
|
|
1118
|
+
description: "List attack-surface assets for the organisation. Supports filtering by type, source, status, and free-text search.",
|
|
1119
|
+
annotations: readAnnotations,
|
|
1120
|
+
inputSchema: listAssetsInput,
|
|
1121
|
+
handler: (client) => async (params) => {
|
|
1122
|
+
const data = await client.get("/api/assets", {
|
|
1123
|
+
...params,
|
|
1124
|
+
stats: params.stats === void 0 ? void 0 : String(params.stats)
|
|
1125
|
+
});
|
|
1126
|
+
return {
|
|
1127
|
+
markdown: formatAssetsList(data),
|
|
1128
|
+
structured: {
|
|
1129
|
+
assets: data.assets,
|
|
1130
|
+
stats: data.stats,
|
|
1131
|
+
pagination: data.pagination
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
// src/tools/list-pentests.ts
|
|
1138
|
+
import { z as z14 } from "zod";
|
|
1139
|
+
var listPentestsInput = z14.object({});
|
|
1140
|
+
var listPentestsTool = {
|
|
1141
|
+
name: "list_pentests",
|
|
1142
|
+
title: "List pentests",
|
|
1143
|
+
description: "List all pentests for the organisation, including status, scheduled window, and in-scope targets.",
|
|
1144
|
+
annotations: readAnnotations,
|
|
1145
|
+
inputSchema: listPentestsInput,
|
|
1146
|
+
handler: (client) => async () => {
|
|
1147
|
+
const data = await client.get("/api/pentests");
|
|
1148
|
+
return {
|
|
1149
|
+
markdown: formatPentestsList(data),
|
|
1150
|
+
structured: { pentests: data.pentests }
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
// src/tools/update-finding-status.ts
|
|
1156
|
+
import { z as z15 } from "zod";
|
|
1157
|
+
var updateFindingStatusInput = z15.object({
|
|
1158
|
+
...findingReferenceFields,
|
|
1159
|
+
status: z15.enum(["MITIGATING", "OPEN_FOR_RETEST", "ACKNOWLEDGED"]).describe(
|
|
1160
|
+
"Target status. MITIGATING (shown in Odin as Fixing) means work has started, OPEN_FOR_RETEST means the fix is ready for verification, and ACKNOWLEDGED (shown as Risk accepted) means the organisation accepts the risk for now."
|
|
1161
|
+
)
|
|
1162
|
+
}).refine((value) => Boolean(value.identifier || value.findingId), {
|
|
1163
|
+
message: "Provide either identifier or findingId."
|
|
1164
|
+
});
|
|
1165
|
+
var updateFindingStatusTool = {
|
|
1166
|
+
name: "update_finding_status",
|
|
1167
|
+
title: "Update finding status",
|
|
1168
|
+
description: "Change a finding's status by human identifier such as ODI-12 or internal findingId. Requires an API key with Read & Write permission; cookie-authenticated callers also need member role. Only use this after the user explicitly asks for the status change; a matching pull request is not permission to mutate a finding.",
|
|
1169
|
+
annotations: { ...writeAnnotations, idempotentHint: true },
|
|
1170
|
+
inputSchema: updateFindingStatusInput,
|
|
1171
|
+
handler: (client) => async (params) => {
|
|
1172
|
+
const findingId = await resolveFindingId(client, params);
|
|
1173
|
+
const data = await client.patch(
|
|
1174
|
+
`/api/findings/${encodeURIComponent(findingId)}/status`,
|
|
1175
|
+
{ status: params.status }
|
|
1176
|
+
);
|
|
1177
|
+
return {
|
|
1178
|
+
markdown: formatFindingStatusUpdate(data.finding),
|
|
1179
|
+
structured: { finding: data.finding }
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
// src/tools.ts
|
|
1185
|
+
var definitions = [
|
|
1186
|
+
listFindingsTool,
|
|
1187
|
+
getFindingTool,
|
|
1188
|
+
updateFindingStatusTool,
|
|
1189
|
+
linkFindingPullRequestTool,
|
|
1190
|
+
getFindingRetestsTool,
|
|
1191
|
+
listPentestsTool,
|
|
1192
|
+
getPentestTool,
|
|
1193
|
+
listAssetsTool,
|
|
1194
|
+
getAssetTool,
|
|
1195
|
+
...additionalFindingTools
|
|
1196
|
+
];
|
|
1197
|
+
var ODIN_TOOLS = definitions.map((tool) => ({
|
|
1198
|
+
name: tool.name,
|
|
1199
|
+
title: tool.title,
|
|
1200
|
+
description: tool.description,
|
|
1201
|
+
inputSchema: tool.inputSchema,
|
|
1202
|
+
annotations: tool.annotations,
|
|
1203
|
+
run: (client, params) => tool.handler(client)(
|
|
1204
|
+
params
|
|
1205
|
+
)
|
|
1206
|
+
}));
|
|
1207
|
+
|
|
1208
|
+
export {
|
|
1209
|
+
ODIN_TOOLS
|
|
1210
|
+
};
|
|
1211
|
+
//# sourceMappingURL=chunk-D5FYYTYV.js.map
|