@borgresearch/odin-mcp 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Borg Research
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # @borgresearch/odin-mcp
2
+
3
+ [Model Context Protocol](https://modelcontextprotocol.io) server for the [Odin](https://odin.borghq.io) security platform by Borg. Lets coding agents query findings, pentests, and attack-surface assets — and transition findings between in-progress states — directly from your editor or chat tool.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js 22 or newer.
8
+ - An Odin account with at least one organisation.
9
+ - An Odin API key (see [Quick start](#quick-start)). `read` permission is enough for every tool except `update_finding_status`, which needs `write` plus the `member` role.
10
+
11
+ ## Quick start
12
+
13
+ 1. **Create an API key.** Sign in to <https://odin.borghq.io>, open **Management > API Keys**, click **Create key**, and pick the permissions you need (`read`, or `read` + `write` if you want the agent to update findings). Copy the full key — it is shown once.
14
+ 2. **Add the server to your client.** Pick your tool below and paste the JSON snippet. Replace `odin_...` with the key you just copied.
15
+ 3. **Verify.** Restart the client, list its connected MCP servers, and confirm `odin` appears with the seven tools listed in [Tools](#tools). A first prompt like _"List my critical findings from the latest pentest"_ should return real data.
16
+
17
+ ```sh
18
+ ODIN_API_KEY=odin_... npx @borgresearch/odin-mcp
19
+ ```
20
+
21
+ You can also install globally:
22
+
23
+ ```sh
24
+ npm install -g @borgresearch/odin-mcp
25
+ ODIN_API_KEY=odin_... odin-mcp
26
+ ```
27
+
28
+ ## Configuration
29
+
30
+ | Variable | Required | Notes |
31
+ |----------|----------|-------|
32
+ | `ODIN_API_KEY` | yes | API key with at least `read` permission. `write` is required for `update_finding_status`. |
33
+
34
+ Each call sends `Authorization: Bearer ${ODIN_API_KEY}`. Keys are organisation-scoped; if you belong to multiple organisations, create one key per organisation and run a separate MCP server entry for each.
35
+
36
+ ## Tools
37
+
38
+ All tools accept a single JSON object and return both a markdown summary (for the agent to read) and a structured payload (for the agent to act on).
39
+
40
+ | Name | Permission | Description |
41
+ |------|------------|-------------|
42
+ | `list_findings` | `read` | List findings, optionally filtered by pentest, asset, severity, status, or search term. Paginated. |
43
+ | `get_finding` | `read` | Fetch a single finding with its latest revision (summary, description, impact, details, remediation, affected assets). |
44
+ | `update_finding_status` | `write`, `member` role | Transition a finding to `MITIGATING` (you are working on the fix) or `OPEN_FOR_RETEST` (the fix is ready). |
45
+ | `list_pentests` | `read` | List all pentests for the organisation, with status and scheduled window. |
46
+ | `get_pentest` | `read` | Fetch a single pentest with status, schedule, and in-scope targets. |
47
+ | `list_assets` | `read` | List attack-surface assets, optionally filtered by type, source, dynamic status, or search term. Paginated. |
48
+ | `get_asset` | `read` | Fetch a single asset with the findings that reference it in their latest revision. |
49
+
50
+ ### Tool parameters and examples
51
+
52
+ #### `list_findings`
53
+
54
+ `pentestId?`, `assetId?`, `severity?` (`CRITICAL` \| `HIGH` \| `MEDIUM` \| `LOW` \| `INFORMATIONAL`), `status?`, `search?`, `sortBy?` (`severity` \| `createdAt`), `sortOrder?`, `limit?` (1–200, default 50), `offset?`.
55
+
56
+ > _"Show me the open critical and high findings from pentest `p_abc123`."_
57
+ > _"Search my findings for anything mentioning `S3`."_
58
+
59
+ #### `get_finding`
60
+
61
+ `findingId` (required).
62
+
63
+ > _"What are the remediation steps for finding `f_abc123`?"_
64
+
65
+ #### `update_finding_status`
66
+
67
+ `findingId` (required), `status` (`MITIGATING` \| `OPEN_FOR_RETEST`).
68
+
69
+ > _"I've started working on `f_abc123`, mark it as mitigating."_
70
+ > _"I've fixed `f_abc123`, mark it ready for retest."_
71
+
72
+ Other transitions (`RESOLVED`, `ACCEPTED_RISK`, etc.) are reserved for the operations team and are intentionally not writable from the API.
73
+
74
+ #### `list_pentests` / `get_pentest`
75
+
76
+ `list_pentests` takes no parameters. `get_pentest` requires `pentestId`.
77
+
78
+ > _"What's in scope for pentest `p_xyz`?"_
79
+ > _"List all my pentests scheduled this quarter."_
80
+
81
+ #### `list_assets`
82
+
83
+ `type?` (`DOMAIN` \| `SUBDOMAIN` \| `IP` \| `URL` \| `WILDCARD`), `source?` (`MANUAL` \| `RECON` \| `IMPORT` \| `AGENT`), `status?`, `search?`, `sortBy?`, `sortOrder?`, `limit?` (1–100, default 50), `offset?`.
84
+
85
+ > _"List subdomains discovered by recon."_
86
+ > _"Which manually added domains are currently down?"_
87
+
88
+ #### `get_asset`
89
+
90
+ `assetId` (required).
91
+
92
+ > _"Summarise findings affecting `api.example.com` and group them by severity."_
93
+
94
+ ## Use with Claude Desktop
95
+
96
+ Add to your `claude_desktop_config.json`:
97
+
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "odin": {
102
+ "command": "npx",
103
+ "args": ["-y", "@borgresearch/odin-mcp"],
104
+ "env": {
105
+ "ODIN_API_KEY": "odin_..."
106
+ }
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ ## Use with Claude Code
113
+
114
+ ```sh
115
+ claude mcp add odin --env ODIN_API_KEY=odin_... -- npx -y @borgresearch/odin-mcp
116
+ ```
117
+
118
+ ## Use with Cursor
119
+
120
+ Open **Settings > MCP** and add a new server, or add the following to your project's `.cursor/mcp.json`:
121
+
122
+ ```json
123
+ {
124
+ "mcpServers": {
125
+ "odin": {
126
+ "command": "npx",
127
+ "args": ["-y", "@borgresearch/odin-mcp"],
128
+ "env": {
129
+ "ODIN_API_KEY": "odin_..."
130
+ }
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ ## Use with Windsurf
137
+
138
+ Add the following to `~/.codeium/windsurf/mcp_config.json`:
139
+
140
+ ```json
141
+ {
142
+ "mcpServers": {
143
+ "odin": {
144
+ "command": "npx",
145
+ "args": ["-y", "@borgresearch/odin-mcp"],
146
+ "env": {
147
+ "ODIN_API_KEY": "odin_..."
148
+ }
149
+ }
150
+ }
151
+ }
152
+ ```
153
+
154
+ ## Use with VS Code (GitHub Copilot)
155
+
156
+ Add the following to your project's `.vscode/mcp.json`:
157
+
158
+ ```json
159
+ {
160
+ "servers": {
161
+ "odin": {
162
+ "command": "npx",
163
+ "args": ["-y", "@borgresearch/odin-mcp"],
164
+ "env": {
165
+ "ODIN_API_KEY": "odin_..."
166
+ }
167
+ }
168
+ }
169
+ }
170
+ ```
171
+
172
+ ## Use with Cline
173
+
174
+ Open **Settings > MCP Servers > Edit MCP Settings** and add:
175
+
176
+ ```json
177
+ {
178
+ "mcpServers": {
179
+ "odin": {
180
+ "command": "npx",
181
+ "args": ["-y", "@borgresearch/odin-mcp"],
182
+ "env": {
183
+ "ODIN_API_KEY": "odin_..."
184
+ }
185
+ }
186
+ }
187
+ }
188
+ ```
189
+
190
+ ## Troubleshooting
191
+
192
+ **`ODIN_API_KEY is not set or invalid`** — the environment variable is missing or the key has been revoked. Confirm the key still exists at **Management > API Keys** and that your client config sets `env.ODIN_API_KEY`. Most clients require a full restart after editing config files.
193
+
194
+ **`API key does not have write permission`** — the call was `update_finding_status` but the key only has `read`. Create a new key with both `read` and `write`, or ask an admin to do it for you.
195
+
196
+ **`Finding not found`** — the finding ID does not belong to the organisation the API key is scoped to. Keys are organisation-scoped; if you have multiple organisations, create one key per organisation and configure them as separate MCP servers (e.g. `odin-acme`, `odin-personal`).
197
+
198
+ **`Rate limit exceeded. Try again in N seconds`** — the API allows 100 requests per minute per key. The error message includes the wait time. If you hit this often, batch your queries (use `list_findings` with filters instead of many `get_finding` calls).
199
+
200
+ **The server starts but no tools appear in the client** — check the client's MCP logs. Most clients require Node.js 22+ on `PATH`; verify with `node --version`. If you use `nvm` or `fnm`, your client may not pick up the active version — point `command` at an absolute path to a Node 22 binary.
201
+
202
+ **Inspecting tool calls locally** — clone the [borg-platform repo](https://github.com/BorgDevelopment/borg-platform), run `pnpm build` then `pnpm inspect` from `frontend/packages/odin-mcp` to launch the MCP Inspector against your local build.
203
+
204
+ ## License
205
+
206
+ UNLICENSED. This package is published for use with the Odin platform; redistribution is not permitted.
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,561 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/client.ts
8
+ var OdinApiError = class extends Error {
9
+ status;
10
+ path;
11
+ method;
12
+ retryAfterSeconds;
13
+ body;
14
+ constructor(params) {
15
+ super(params.message);
16
+ this.name = "OdinApiError";
17
+ this.status = params.status;
18
+ this.path = params.path;
19
+ this.method = params.method;
20
+ this.retryAfterSeconds = params.retryAfterSeconds;
21
+ this.body = params.body;
22
+ }
23
+ };
24
+ var DEFAULT_ODIN_BASE_URL = "https://odin.borghq.io";
25
+ var WRITE_METHODS = /* @__PURE__ */ new Set(["PATCH", "POST", "PUT", "DELETE"]);
26
+ var OdinClient = class {
27
+ baseUrl;
28
+ apiKey;
29
+ fetchImpl;
30
+ constructor(options) {
31
+ if (!options.apiKey) {
32
+ throw new Error("OdinClient requires an apiKey");
33
+ }
34
+ this.apiKey = options.apiKey;
35
+ this.baseUrl = (options.baseUrl ?? DEFAULT_ODIN_BASE_URL).replace(
36
+ /\/+$/,
37
+ ""
38
+ );
39
+ this.fetchImpl = options.fetchImpl ?? fetch;
40
+ }
41
+ async get(path, searchParams) {
42
+ const url = this.buildUrl(path, searchParams);
43
+ return this.request("GET", url, path);
44
+ }
45
+ async patch(path, body) {
46
+ const url = this.buildUrl(path);
47
+ return this.request("PATCH", url, path, body);
48
+ }
49
+ async post(path, body) {
50
+ const url = this.buildUrl(path);
51
+ return this.request("POST", url, path, body);
52
+ }
53
+ buildUrl(path, searchParams) {
54
+ const url = new URL(path.startsWith("/") ? path : `/${path}`, this.baseUrl);
55
+ if (searchParams) {
56
+ for (const [key, value] of Object.entries(searchParams)) {
57
+ if (value === void 0 || value === "") continue;
58
+ url.searchParams.set(key, String(value));
59
+ }
60
+ }
61
+ return url.toString();
62
+ }
63
+ async request(method, url, path, body) {
64
+ const response = await this.fetchImpl(url, {
65
+ method,
66
+ headers: {
67
+ Authorization: `Bearer ${this.apiKey}`,
68
+ "Content-Type": "application/json",
69
+ Accept: "application/json"
70
+ },
71
+ body: body === void 0 ? void 0 : JSON.stringify(body)
72
+ });
73
+ const rawText = await response.text();
74
+ const parsed = rawText ? safeJsonParse(rawText) : null;
75
+ if (!response.ok) {
76
+ const retryAfterSeconds = parseRetryAfter(
77
+ response.headers.get("Retry-After")
78
+ );
79
+ throw new OdinApiError({
80
+ status: response.status,
81
+ path,
82
+ method,
83
+ retryAfterSeconds,
84
+ body: parsed ?? rawText,
85
+ message: formatErrorMessage({
86
+ status: response.status,
87
+ method,
88
+ path,
89
+ retryAfterSeconds,
90
+ parsed,
91
+ rawText
92
+ })
93
+ });
94
+ }
95
+ return parsed;
96
+ }
97
+ };
98
+ function safeJsonParse(text) {
99
+ try {
100
+ return JSON.parse(text);
101
+ } catch {
102
+ return text;
103
+ }
104
+ }
105
+ function parseRetryAfter(header) {
106
+ if (!header) return null;
107
+ const trimmed = header.trim();
108
+ if (/^\d+$/.test(trimmed)) return Number.parseInt(trimmed, 10);
109
+ const dateMs = Date.parse(trimmed);
110
+ if (Number.isNaN(dateMs)) return null;
111
+ const seconds = Math.ceil((dateMs - Date.now()) / 1e3);
112
+ return seconds > 0 ? seconds : null;
113
+ }
114
+ function formatErrorMessage(input) {
115
+ const serverMessage = extractServerMessage(input.parsed);
116
+ const actionable = actionableMessage(input);
117
+ if (actionable) {
118
+ return serverMessage ? `${actionable} (server: ${serverMessage})` : actionable;
119
+ }
120
+ const suffix = serverMessage ? ` \u2014 ${serverMessage}` : input.rawText ? ` \u2014 ${input.rawText.slice(0, 200)}` : "";
121
+ return `Odin API ${input.status} on ${input.path}${suffix}.`;
122
+ }
123
+ function actionableMessage(input) {
124
+ switch (input.status) {
125
+ case 401:
126
+ return "ODIN_API_KEY is not set or invalid. Create one at Management > API Keys in the Odin dashboard.";
127
+ case 403:
128
+ if (WRITE_METHODS.has(input.method)) {
129
+ return "API key does not have write permission. Create a new key with 'Read & Write' permissions.";
130
+ }
131
+ return null;
132
+ case 404:
133
+ if (isFindingDetailPath(input.path)) {
134
+ return "Finding not found. Check the finding ID is correct.";
135
+ }
136
+ return null;
137
+ case 429: {
138
+ const n = input.retryAfterSeconds;
139
+ return n ? `Rate limit exceeded. Try again in ${n} seconds.` : "Rate limit exceeded. Try again shortly.";
140
+ }
141
+ default:
142
+ return null;
143
+ }
144
+ }
145
+ function isFindingDetailPath(path) {
146
+ return /^\/api\/findings\/[^/]+(\/.*)?$/.test(path);
147
+ }
148
+ function extractServerMessage(parsed) {
149
+ if (parsed && typeof parsed === "object" && "error" in parsed) {
150
+ const err = parsed.error;
151
+ if (typeof err === "string") return err;
152
+ }
153
+ return null;
154
+ }
155
+
156
+ // src/tools/get-asset.ts
157
+ import { z } from "zod";
158
+
159
+ // src/formatters/markdown.ts
160
+ function formatFindingsList(data) {
161
+ const { findings, stats, pagination } = data;
162
+ if (findings.length === 0) {
163
+ return "No findings match the supplied filters.";
164
+ }
165
+ const rows = findings.map((f) => {
166
+ const severity = `${f.severity.label} (CVSS ${f.severity.cvss.toFixed(1)})`;
167
+ return `| ${f.findingId} | ${escapePipes(f.title)} | ${severity} | ${f.status} | ${formatDate(f.createdAt)} |`;
168
+ });
169
+ return [
170
+ `Showing ${findings.length} of ${stats.total} finding(s). Offset ${pagination.offset}, limit ${pagination.limit}. hasMore=${pagination.hasMore}.`,
171
+ "",
172
+ "| Finding ID | Title | Severity | Status | Created |",
173
+ "| --- | --- | --- | --- | --- |",
174
+ ...rows
175
+ ].join("\n");
176
+ }
177
+ function formatFindingDetail(data) {
178
+ const { finding, revision } = data;
179
+ const severity = `${finding.severity.label} (CVSS ${finding.severity.cvss.toFixed(1)})`;
180
+ const header = [
181
+ `# ${finding.title}`,
182
+ "",
183
+ `- **ID:** ${finding.findingId}`,
184
+ `- **Severity:** ${severity}`,
185
+ `- **Status:** ${finding.status}`,
186
+ `- **Pentest:** ${finding.context?.pentestId ?? "standalone"}`,
187
+ `- **Created:** ${formatDate(finding.createdAt)}`,
188
+ `- **Updated:** ${formatDate(finding.updatedAt)}`
189
+ ];
190
+ if (!revision) {
191
+ return [...header, "", "_No revision content available._"].join("\n");
192
+ }
193
+ const c = revision.content;
194
+ const sections = [];
195
+ if (c.summaryMd) sections.push("## Summary", c.summaryMd);
196
+ if (c.descriptionMd) sections.push("## Description", c.descriptionMd);
197
+ if (c.businessImpactMd)
198
+ sections.push("## Business Impact", c.businessImpactMd);
199
+ if (c.detailsMd) sections.push("## Details", c.detailsMd);
200
+ if (c.remediationMd) sections.push("## Remediation", c.remediationMd);
201
+ if (c.affectedAssets?.length) {
202
+ sections.push(
203
+ "## Affected Assets",
204
+ c.affectedAssets.map((a) => `- ${a.display} (${a.assetId})`).join("\n")
205
+ );
206
+ }
207
+ return [
208
+ ...header,
209
+ "",
210
+ `_Revision ${revision.revisionNumber}_`,
211
+ "",
212
+ ...sections
213
+ ].join("\n");
214
+ }
215
+ function formatFindingStatusUpdate(finding) {
216
+ return [
217
+ `Updated finding **${finding.findingId}** status to **${finding.status}**.`,
218
+ `Title: ${finding.title}`
219
+ ].join("\n");
220
+ }
221
+ function formatPentestsList(data) {
222
+ if (data.pentests.length === 0) return "No pentests found.";
223
+ const rows = data.pentests.map(formatPentestRow);
224
+ return [
225
+ "| Pentest ID | Name | Status | Start | End | Scope |",
226
+ "| --- | --- | --- | --- | --- | --- |",
227
+ ...rows
228
+ ].join("\n");
229
+ }
230
+ function formatPentestDetail(data) {
231
+ const p = data.pentest;
232
+ const end = p.endAt ? formatDate(p.endAt) : "open";
233
+ const scope = p.scope.length ? p.scope.map((s) => `- ${s}`).join("\n") : "_No scope defined._";
234
+ return [
235
+ `# ${p.name}`,
236
+ "",
237
+ `- **ID:** ${p.pentestId}`,
238
+ `- **Status:** ${p.status}`,
239
+ `- **Start:** ${formatDate(p.startAt)}`,
240
+ `- **End:** ${end}`,
241
+ `- **Created:** ${formatDate(p.createdAt)}`,
242
+ "",
243
+ "## Scope",
244
+ "",
245
+ scope
246
+ ].join("\n");
247
+ }
248
+ function formatPentestRow(p) {
249
+ const end = p.endAt ? formatDate(p.endAt) : "open";
250
+ const scope = p.scope.slice(0, 3).join(", ") + (p.scope.length > 3 ? ", \u2026" : "");
251
+ return `| ${p.pentestId} | ${escapePipes(p.name)} | ${p.status} | ${formatDate(p.startAt)} | ${end} | ${escapePipes(scope)} |`;
252
+ }
253
+ function formatAssetsList(data) {
254
+ const { assets, stats, pagination } = data;
255
+ if (assets.length === 0) return "No assets match the supplied filters.";
256
+ const rows = assets.map(formatAssetRow);
257
+ return [
258
+ `Showing ${assets.length} of ${stats.total} asset(s). Offset ${pagination.offset}, limit ${pagination.limit}. hasMore=${pagination.hasMore}.`,
259
+ "",
260
+ "| Asset ID | Type | Value | Source | Status | Last seen |",
261
+ "| --- | --- | --- | --- | --- | --- |",
262
+ ...rows
263
+ ].join("\n");
264
+ }
265
+ function formatAssetDetail(data) {
266
+ const { asset, findings } = data;
267
+ const header = [
268
+ `# ${asset.value}`,
269
+ "",
270
+ `- **ID:** ${asset.assetId}`,
271
+ `- **Type:** ${asset.type}`,
272
+ `- **Source:** ${asset.source}`,
273
+ `- **Status:** ${asset.status}`,
274
+ `- **Last seen:** ${formatDate(asset.lastSeenAt)}`,
275
+ `- **Created:** ${formatDate(asset.createdAt)}`
276
+ ];
277
+ if (findings.length === 0) {
278
+ return [...header, "", "_No findings reference this asset._"].join("\n");
279
+ }
280
+ const rows = findings.map((f) => {
281
+ const severity = `${f.severity.label} (CVSS ${f.severity.cvss.toFixed(1)})`;
282
+ return `| ${f.findingId} | ${escapePipes(f.title)} | ${severity} | ${f.status} | ${formatDate(f.createdAt)} |`;
283
+ });
284
+ return [
285
+ ...header,
286
+ "",
287
+ `## Findings (${findings.length})`,
288
+ "",
289
+ "| Finding ID | Title | Severity | Status | Created |",
290
+ "| --- | --- | --- | --- | --- |",
291
+ ...rows
292
+ ].join("\n");
293
+ }
294
+ function formatAssetRow(a) {
295
+ return `| ${a.assetId} | ${a.type} | ${escapePipes(a.value)} | ${a.source} | ${a.status} | ${formatDate(a.lastSeenAt)} |`;
296
+ }
297
+ function escapePipes(value) {
298
+ return value.replace(/\|/g, "\\|");
299
+ }
300
+ function formatDate(epoch) {
301
+ if (!epoch) return "\u2014";
302
+ const ms = epoch < 1e12 ? epoch * 1e3 : epoch;
303
+ return new Date(ms).toISOString().slice(0, 10);
304
+ }
305
+
306
+ // src/tools/get-asset.ts
307
+ var getAssetInput = z.object({
308
+ assetId: z.string().min(1)
309
+ });
310
+ var getAssetTool = {
311
+ name: "get_asset",
312
+ title: "Get asset",
313
+ description: "Fetch a single attack-surface asset by ID, including the findings that reference it in their latest revision.",
314
+ inputSchema: getAssetInput,
315
+ handler: (client) => async (params) => {
316
+ const data = await client.get(
317
+ `/api/assets/${encodeURIComponent(params.assetId)}`
318
+ );
319
+ return {
320
+ markdown: formatAssetDetail(data),
321
+ structured: { asset: data.asset, findings: data.findings }
322
+ };
323
+ }
324
+ };
325
+
326
+ // src/tools/get-finding.ts
327
+ import { z as z2 } from "zod";
328
+ var getFindingInput = z2.object({
329
+ findingId: z2.string().min(1)
330
+ });
331
+ var getFindingTool = {
332
+ name: "get_finding",
333
+ title: "Get finding",
334
+ description: "Fetch a single finding by ID, including the latest revision content (description, impact, details, remediation).",
335
+ inputSchema: getFindingInput,
336
+ handler: (client) => async (params) => {
337
+ const data = await client.get(
338
+ `/api/findings/${encodeURIComponent(params.findingId)}`
339
+ );
340
+ return {
341
+ markdown: formatFindingDetail(data),
342
+ structured: {
343
+ finding: data.finding,
344
+ revision: data.revision
345
+ }
346
+ };
347
+ }
348
+ };
349
+
350
+ // src/tools/get-pentest.ts
351
+ import { z as z3 } from "zod";
352
+ var getPentestInput = z3.object({
353
+ pentestId: z3.string().min(1)
354
+ });
355
+ var getPentestTool = {
356
+ name: "get_pentest",
357
+ title: "Get pentest",
358
+ description: "Fetch a single pentest by ID, including status, scheduled window, and in-scope targets.",
359
+ inputSchema: getPentestInput,
360
+ handler: (client) => async (params) => {
361
+ const data = await client.get(
362
+ `/api/pentests/${encodeURIComponent(params.pentestId)}`
363
+ );
364
+ return {
365
+ markdown: formatPentestDetail(data),
366
+ structured: { pentest: data.pentest }
367
+ };
368
+ }
369
+ };
370
+
371
+ // src/tools/list-assets.ts
372
+ import { z as z4 } from "zod";
373
+ var listAssetsInput = z4.object({
374
+ type: z4.enum(["DOMAIN", "SUBDOMAIN", "IP", "URL", "WILDCARD"]).optional(),
375
+ source: z4.enum(["MANUAL", "RECON", "IMPORT", "AGENT"]).optional(),
376
+ status: z4.string().optional().describe(
377
+ "Dynamic asset status set by recon (e.g. ACTIVE, INACTIVE, TLS_ERROR)."
378
+ ),
379
+ search: z4.string().optional(),
380
+ sortBy: z4.enum(["lastSeenAt", "createdAt", "value"]).optional(),
381
+ sortOrder: z4.enum(["asc", "desc"]).optional(),
382
+ limit: z4.number().int().min(1).max(100).optional().default(50),
383
+ offset: z4.number().int().min(0).optional().default(0)
384
+ });
385
+ var listAssetsTool = {
386
+ name: "list_assets",
387
+ title: "List assets",
388
+ description: "List attack-surface assets for the organisation. Supports filtering by type, source, status, and free-text search.",
389
+ inputSchema: listAssetsInput,
390
+ handler: (client) => async (params) => {
391
+ const data = await client.get("/api/assets", params);
392
+ return {
393
+ markdown: formatAssetsList(data),
394
+ structured: {
395
+ assets: data.assets,
396
+ stats: data.stats,
397
+ pagination: data.pagination
398
+ }
399
+ };
400
+ }
401
+ };
402
+
403
+ // src/tools/list-findings.ts
404
+ import { z as z5 } from "zod";
405
+ var listFindingsInput = z5.object({
406
+ pentestId: z5.string().optional().describe("Filter to a single pentest."),
407
+ assetId: z5.string().optional().describe(
408
+ "Filter to findings whose latest revision references this asset."
409
+ ),
410
+ severity: z5.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFORMATIONAL"]).optional(),
411
+ status: z5.enum([
412
+ "DRAFT",
413
+ "IN_REVIEW",
414
+ "REPORTED",
415
+ "MITIGATING",
416
+ "OPEN_FOR_RETEST",
417
+ "RESOLVED",
418
+ "NOT_REPRODUCIBLE",
419
+ "ACCEPTED_RISK"
420
+ ]).optional(),
421
+ search: z5.string().optional().describe("Free-text search on finding titles."),
422
+ sortBy: z5.enum(["severity", "createdAt"]).optional(),
423
+ sortOrder: z5.enum(["asc", "desc"]).optional(),
424
+ limit: z5.number().int().min(1).max(200).optional().default(50),
425
+ offset: z5.number().int().min(0).optional().default(0)
426
+ });
427
+ var listFindingsTool = {
428
+ name: "list_findings",
429
+ title: "List findings",
430
+ description: "List security findings for the organisation. Supports filtering by pentest, severity, status, and free-text search.",
431
+ inputSchema: listFindingsInput,
432
+ handler: (client) => async (params) => {
433
+ const data = await client.get(
434
+ "/api/findings",
435
+ params
436
+ );
437
+ return {
438
+ markdown: formatFindingsList(data),
439
+ structured: {
440
+ findings: data.findings,
441
+ stats: data.stats,
442
+ pagination: data.pagination
443
+ }
444
+ };
445
+ }
446
+ };
447
+
448
+ // src/tools/list-pentests.ts
449
+ import { z as z6 } from "zod";
450
+ var listPentestsInput = z6.object({});
451
+ var listPentestsTool = {
452
+ name: "list_pentests",
453
+ title: "List pentests",
454
+ description: "List all pentests for the organisation, including status, scheduled window, and in-scope targets.",
455
+ inputSchema: listPentestsInput,
456
+ handler: (client) => async () => {
457
+ const data = await client.get("/api/pentests");
458
+ return {
459
+ markdown: formatPentestsList(data),
460
+ structured: { pentests: data.pentests }
461
+ };
462
+ }
463
+ };
464
+
465
+ // src/tools/update-finding-status.ts
466
+ import { z as z7 } from "zod";
467
+ var updateFindingStatusInput = z7.object({
468
+ findingId: z7.string().min(1),
469
+ status: z7.enum(["MITIGATING", "OPEN_FOR_RETEST", "ACKNOWLEDGED"]).describe(
470
+ "Target status. Only MITIGATING, OPEN_FOR_RETEST, and ACKNOWLEDGED are client-writable; transitions are only allowed from REPORTED, MITIGATING, or OPEN_FOR_RETEST. ACKNOWLEDGED marks the finding as accepted-risk and is terminal."
471
+ )
472
+ });
473
+ var updateFindingStatusTool = {
474
+ name: "update_finding_status",
475
+ title: "Update finding status",
476
+ description: "Change a finding's status. Requires an API key with write permission and member role. Only MITIGATING, OPEN_FOR_RETEST, and ACKNOWLEDGED are accepted.",
477
+ inputSchema: updateFindingStatusInput,
478
+ handler: (client) => async (params) => {
479
+ const data = await client.patch(
480
+ `/api/findings/${encodeURIComponent(params.findingId)}/status`,
481
+ { status: params.status }
482
+ );
483
+ return {
484
+ markdown: formatFindingStatusUpdate(data.finding),
485
+ structured: { finding: data.finding }
486
+ };
487
+ }
488
+ };
489
+
490
+ // src/index.ts
491
+ var PACKAGE_VERSION = "0.1.0";
492
+ async function main() {
493
+ const apiKey = process.env.ODIN_API_KEY;
494
+ if (!apiKey) {
495
+ console.error(
496
+ "ODIN_API_KEY is not set or invalid. Create one at Management > API Keys in the Odin dashboard."
497
+ );
498
+ process.exit(1);
499
+ }
500
+ const baseUrl = process.env.ODIN_BASE_URL ?? DEFAULT_ODIN_BASE_URL;
501
+ const client = new OdinClient({ apiKey, baseUrl });
502
+ const server = new McpServer({
503
+ name: "@borgresearch/odin-mcp",
504
+ version: PACKAGE_VERSION
505
+ });
506
+ const tools = [
507
+ listFindingsTool,
508
+ getFindingTool,
509
+ updateFindingStatusTool,
510
+ listPentestsTool,
511
+ getPentestTool,
512
+ listAssetsTool,
513
+ getAssetTool
514
+ ];
515
+ for (const tool of tools) {
516
+ const run = tool.handler(client);
517
+ server.registerTool(
518
+ tool.name,
519
+ {
520
+ title: tool.title,
521
+ description: tool.description,
522
+ inputSchema: tool.inputSchema.shape
523
+ },
524
+ async (params) => {
525
+ try {
526
+ const { markdown, structured } = await run(params);
527
+ return {
528
+ content: [{ type: "text", text: markdown }],
529
+ structuredContent: { markdown, ...toObject(structured) }
530
+ };
531
+ } catch (error) {
532
+ return {
533
+ isError: true,
534
+ content: [{ type: "text", text: toErrorMessage(error) }]
535
+ };
536
+ }
537
+ }
538
+ );
539
+ }
540
+ const transport = new StdioServerTransport();
541
+ await server.connect(transport);
542
+ console.error(
543
+ `odin-mcp ${PACKAGE_VERSION} connected (baseUrl=${baseUrl}, tools=${tools.length}).`
544
+ );
545
+ }
546
+ function toObject(value) {
547
+ if (value && typeof value === "object" && !Array.isArray(value)) {
548
+ return value;
549
+ }
550
+ return { data: value };
551
+ }
552
+ function toErrorMessage(error) {
553
+ if (error instanceof OdinApiError) return error.message;
554
+ if (error instanceof Error) return `Unexpected error: ${error.message}`;
555
+ return `Unexpected error: ${String(error)}`;
556
+ }
557
+ main().catch((error) => {
558
+ console.error("odin-mcp failed to start:", error);
559
+ process.exit(1);
560
+ });
561
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/tools/get-asset.ts","../src/formatters/markdown.ts","../src/tools/get-finding.ts","../src/tools/get-pentest.ts","../src/tools/list-assets.ts","../src/tools/list-findings.ts","../src/tools/list-pentests.ts","../src/tools/update-finding-status.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { DEFAULT_ODIN_BASE_URL, OdinApiError, OdinClient } from \"./client.js\";\nimport { getAssetTool } from \"./tools/get-asset.js\";\nimport { getFindingTool } from \"./tools/get-finding.js\";\nimport { getPentestTool } from \"./tools/get-pentest.js\";\nimport { listAssetsTool } from \"./tools/list-assets.js\";\nimport { listFindingsTool } from \"./tools/list-findings.js\";\nimport { listPentestsTool } from \"./tools/list-pentests.js\";\nimport { updateFindingStatusTool } from \"./tools/update-finding-status.js\";\n\nconst PACKAGE_VERSION = \"0.1.0\";\n\ninterface ToolResult {\n markdown: string;\n structured: unknown;\n}\n\nasync function main() {\n const apiKey = process.env.ODIN_API_KEY;\n if (!apiKey) {\n console.error(\n \"ODIN_API_KEY is not set or invalid. Create one at Management > API Keys in the Odin dashboard.\",\n );\n process.exit(1);\n }\n\n const baseUrl = process.env.ODIN_BASE_URL ?? DEFAULT_ODIN_BASE_URL;\n const client = new OdinClient({ apiKey, baseUrl });\n\n const server = new McpServer({\n name: \"@borgresearch/odin-mcp\",\n version: PACKAGE_VERSION,\n });\n\n const tools = [\n listFindingsTool,\n getFindingTool,\n updateFindingStatusTool,\n listPentestsTool,\n getPentestTool,\n listAssetsTool,\n getAssetTool,\n ];\n\n for (const tool of tools) {\n const run = tool.handler(client) as (\n params: unknown,\n ) => Promise<ToolResult>;\n server.registerTool(\n tool.name,\n {\n title: tool.title,\n description: tool.description,\n inputSchema: tool.inputSchema.shape,\n },\n async (params: unknown) => {\n try {\n const { markdown, structured } = await run(params);\n return {\n content: [{ type: \"text\" as const, text: markdown }],\n structuredContent: { markdown, ...toObject(structured) },\n };\n } catch (error) {\n return {\n isError: true,\n content: [{ type: \"text\" as const, text: toErrorMessage(error) }],\n };\n }\n },\n );\n }\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error(\n `odin-mcp ${PACKAGE_VERSION} connected (baseUrl=${baseUrl}, tools=${tools.length}).`,\n );\n}\n\nfunction toObject(value: unknown): Record<string, unknown> {\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return value as Record<string, unknown>;\n }\n return { data: value };\n}\n\nfunction toErrorMessage(error: unknown): string {\n if (error instanceof OdinApiError) return error.message;\n if (error instanceof Error) return `Unexpected error: ${error.message}`;\n return `Unexpected error: ${String(error)}`;\n}\n\nmain().catch((error) => {\n console.error(\"odin-mcp failed to start:\", error);\n process.exit(1);\n});\n","export class OdinApiError extends Error {\n readonly status: number;\n readonly path: string;\n readonly method: string;\n readonly retryAfterSeconds: number | null;\n readonly body: unknown;\n\n constructor(params: {\n status: number;\n path: string;\n method: string;\n retryAfterSeconds: number | null;\n body: unknown;\n message: string;\n }) {\n super(params.message);\n this.name = \"OdinApiError\";\n this.status = params.status;\n this.path = params.path;\n this.method = params.method;\n this.retryAfterSeconds = params.retryAfterSeconds;\n this.body = params.body;\n }\n}\n\nexport interface OdinClientOptions {\n apiKey: string;\n baseUrl?: string;\n fetchImpl?: typeof fetch;\n}\n\nexport const DEFAULT_ODIN_BASE_URL = \"https://odin.borghq.io\";\n\nconst WRITE_METHODS = new Set([\"PATCH\", \"POST\", \"PUT\", \"DELETE\"]);\n\nexport class OdinClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(options: OdinClientOptions) {\n if (!options.apiKey) {\n throw new Error(\"OdinClient requires an apiKey\");\n }\n this.apiKey = options.apiKey;\n this.baseUrl = (options.baseUrl ?? DEFAULT_ODIN_BASE_URL).replace(\n /\\/+$/,\n \"\",\n );\n this.fetchImpl = options.fetchImpl ?? fetch;\n }\n\n async get<T>(\n path: string,\n searchParams?: Record<string, string | number | undefined>,\n ): Promise<T> {\n const url = this.buildUrl(path, searchParams);\n return this.request<T>(\"GET\", url, path);\n }\n\n async patch<T>(path: string, body: unknown): Promise<T> {\n const url = this.buildUrl(path);\n return this.request<T>(\"PATCH\", url, path, body);\n }\n\n async post<T>(path: string, body: unknown): Promise<T> {\n const url = this.buildUrl(path);\n return this.request<T>(\"POST\", url, path, body);\n }\n\n private buildUrl(\n path: string,\n searchParams?: Record<string, string | number | undefined>,\n ): string {\n const url = new URL(path.startsWith(\"/\") ? path : `/${path}`, this.baseUrl);\n if (searchParams) {\n for (const [key, value] of Object.entries(searchParams)) {\n if (value === undefined || value === \"\") continue;\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n }\n\n private async request<T>(\n method: string,\n url: string,\n path: string,\n body?: unknown,\n ): Promise<T> {\n const response = await this.fetchImpl(url, {\n method,\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n\n const rawText = await response.text();\n const parsed = rawText ? safeJsonParse(rawText) : null;\n\n if (!response.ok) {\n const retryAfterSeconds = parseRetryAfter(\n response.headers.get(\"Retry-After\"),\n );\n throw new OdinApiError({\n status: response.status,\n path,\n method,\n retryAfterSeconds,\n body: parsed ?? rawText,\n message: formatErrorMessage({\n status: response.status,\n method,\n path,\n retryAfterSeconds,\n parsed,\n rawText,\n }),\n });\n }\n\n return parsed as T;\n }\n}\n\nfunction safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction parseRetryAfter(header: string | null): number | null {\n if (!header) return null;\n const trimmed = header.trim();\n if (/^\\d+$/.test(trimmed)) return Number.parseInt(trimmed, 10);\n const dateMs = Date.parse(trimmed);\n if (Number.isNaN(dateMs)) return null;\n const seconds = Math.ceil((dateMs - Date.now()) / 1000);\n return seconds > 0 ? seconds : null;\n}\n\ninterface FormatErrorInput {\n status: number;\n method: string;\n path: string;\n retryAfterSeconds: number | null;\n parsed: unknown;\n rawText: string;\n}\n\nfunction formatErrorMessage(input: FormatErrorInput): string {\n const serverMessage = extractServerMessage(input.parsed);\n const actionable = actionableMessage(input);\n if (actionable) {\n return serverMessage\n ? `${actionable} (server: ${serverMessage})`\n : actionable;\n }\n const suffix = serverMessage\n ? ` — ${serverMessage}`\n : input.rawText\n ? ` — ${input.rawText.slice(0, 200)}`\n : \"\";\n return `Odin API ${input.status} on ${input.path}${suffix}.`;\n}\n\nfunction actionableMessage(input: FormatErrorInput): string | null {\n switch (input.status) {\n case 401:\n return \"ODIN_API_KEY is not set or invalid. Create one at Management > API Keys in the Odin dashboard.\";\n case 403:\n if (WRITE_METHODS.has(input.method)) {\n return \"API key does not have write permission. Create a new key with 'Read & Write' permissions.\";\n }\n return null;\n case 404:\n if (isFindingDetailPath(input.path)) {\n return \"Finding not found. Check the finding ID is correct.\";\n }\n return null;\n case 429: {\n const n = input.retryAfterSeconds;\n return n\n ? `Rate limit exceeded. Try again in ${n} seconds.`\n : \"Rate limit exceeded. Try again shortly.\";\n }\n default:\n return null;\n }\n}\n\nfunction isFindingDetailPath(path: string): boolean {\n // /api/findings/{id} or /api/findings/{id}/status\n return /^\\/api\\/findings\\/[^/]+(\\/.*)?$/.test(path);\n}\n\nfunction extractServerMessage(parsed: unknown): string | null {\n if (parsed && typeof parsed === \"object\" && \"error\" in parsed) {\n const err = (parsed as { error: unknown }).error;\n if (typeof err === \"string\") return err;\n }\n return null;\n}\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatAssetDetail } from \"../formatters/markdown.js\";\nimport type { AssetDetailResponse } from \"../types.js\";\n\nexport const getAssetInput = z.object({\n assetId: z.string().min(1),\n});\n\nexport const getAssetTool = {\n name: \"get_asset\",\n title: \"Get asset\",\n description:\n \"Fetch a single attack-surface asset by ID, including the findings that reference it in their latest revision.\",\n inputSchema: getAssetInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof getAssetInput>) => {\n const data = await client.get<AssetDetailResponse>(\n `/api/assets/${encodeURIComponent(params.assetId)}`,\n );\n return {\n markdown: formatAssetDetail(data),\n structured: { asset: data.asset, findings: data.findings },\n };\n },\n};\n","import type {\n Asset,\n AssetDetailResponse,\n AssetsListResponse,\n FindingDetailResponse,\n FindingSummary,\n FindingsListResponse,\n Pentest,\n PentestDetailResponse,\n PentestListResponse,\n} from \"../types.js\";\n\nexport function formatFindingsList(data: FindingsListResponse): string {\n const { findings, stats, pagination } = data;\n if (findings.length === 0) {\n return \"No findings match the supplied filters.\";\n }\n\n const rows = findings.map((f) => {\n const severity = `${f.severity.label} (CVSS ${f.severity.cvss.toFixed(1)})`;\n return `| ${f.findingId} | ${escapePipes(f.title)} | ${severity} | ${f.status} | ${formatDate(f.createdAt)} |`;\n });\n\n return [\n `Showing ${findings.length} of ${stats.total} finding(s). Offset ${pagination.offset}, limit ${pagination.limit}. hasMore=${pagination.hasMore}.`,\n \"\",\n \"| Finding ID | Title | Severity | Status | Created |\",\n \"| --- | --- | --- | --- | --- |\",\n ...rows,\n ].join(\"\\n\");\n}\n\nexport function formatFindingDetail(data: FindingDetailResponse): string {\n const { finding, revision } = data;\n const severity = `${finding.severity.label} (CVSS ${finding.severity.cvss.toFixed(1)})`;\n const header = [\n `# ${finding.title}`,\n \"\",\n `- **ID:** ${finding.findingId}`,\n `- **Severity:** ${severity}`,\n `- **Status:** ${finding.status}`,\n `- **Pentest:** ${finding.context?.pentestId ?? \"standalone\"}`,\n `- **Created:** ${formatDate(finding.createdAt)}`,\n `- **Updated:** ${formatDate(finding.updatedAt)}`,\n ];\n\n if (!revision) {\n return [...header, \"\", \"_No revision content available._\"].join(\"\\n\");\n }\n\n const c = revision.content;\n const sections: string[] = [];\n if (c.summaryMd) sections.push(\"## Summary\", c.summaryMd);\n if (c.descriptionMd) sections.push(\"## Description\", c.descriptionMd);\n if (c.businessImpactMd)\n sections.push(\"## Business Impact\", c.businessImpactMd);\n if (c.detailsMd) sections.push(\"## Details\", c.detailsMd);\n if (c.remediationMd) sections.push(\"## Remediation\", c.remediationMd);\n if (c.affectedAssets?.length) {\n sections.push(\n \"## Affected Assets\",\n c.affectedAssets.map((a) => `- ${a.display} (${a.assetId})`).join(\"\\n\"),\n );\n }\n\n return [\n ...header,\n \"\",\n `_Revision ${revision.revisionNumber}_`,\n \"\",\n ...sections,\n ].join(\"\\n\");\n}\n\nexport function formatFindingStatusUpdate(finding: FindingSummary): string {\n return [\n `Updated finding **${finding.findingId}** status to **${finding.status}**.`,\n `Title: ${finding.title}`,\n ].join(\"\\n\");\n}\n\nexport function formatPentestsList(data: PentestListResponse): string {\n if (data.pentests.length === 0) return \"No pentests found.\";\n const rows = data.pentests.map(formatPentestRow);\n return [\n \"| Pentest ID | Name | Status | Start | End | Scope |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...rows,\n ].join(\"\\n\");\n}\n\nexport function formatPentestDetail(data: PentestDetailResponse): string {\n const p = data.pentest;\n const end = p.endAt ? formatDate(p.endAt) : \"open\";\n const scope = p.scope.length\n ? p.scope.map((s) => `- ${s}`).join(\"\\n\")\n : \"_No scope defined._\";\n return [\n `# ${p.name}`,\n \"\",\n `- **ID:** ${p.pentestId}`,\n `- **Status:** ${p.status}`,\n `- **Start:** ${formatDate(p.startAt)}`,\n `- **End:** ${end}`,\n `- **Created:** ${formatDate(p.createdAt)}`,\n \"\",\n \"## Scope\",\n \"\",\n scope,\n ].join(\"\\n\");\n}\n\nfunction formatPentestRow(p: Pentest): string {\n const end = p.endAt ? formatDate(p.endAt) : \"open\";\n const scope =\n p.scope.slice(0, 3).join(\", \") + (p.scope.length > 3 ? \", …\" : \"\");\n return `| ${p.pentestId} | ${escapePipes(p.name)} | ${p.status} | ${formatDate(p.startAt)} | ${end} | ${escapePipes(scope)} |`;\n}\n\nexport function formatAssetsList(data: AssetsListResponse): string {\n const { assets, stats, pagination } = data;\n if (assets.length === 0) return \"No assets match the supplied filters.\";\n const rows = assets.map(formatAssetRow);\n return [\n `Showing ${assets.length} of ${stats.total} asset(s). Offset ${pagination.offset}, limit ${pagination.limit}. hasMore=${pagination.hasMore}.`,\n \"\",\n \"| Asset ID | Type | Value | Source | Status | Last seen |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...rows,\n ].join(\"\\n\");\n}\n\nexport function formatAssetDetail(data: AssetDetailResponse): string {\n const { asset, findings } = data;\n const header = [\n `# ${asset.value}`,\n \"\",\n `- **ID:** ${asset.assetId}`,\n `- **Type:** ${asset.type}`,\n `- **Source:** ${asset.source}`,\n `- **Status:** ${asset.status}`,\n `- **Last seen:** ${formatDate(asset.lastSeenAt)}`,\n `- **Created:** ${formatDate(asset.createdAt)}`,\n ];\n\n if (findings.length === 0) {\n return [...header, \"\", \"_No findings reference this asset._\"].join(\"\\n\");\n }\n\n const rows = findings.map((f) => {\n const severity = `${f.severity.label} (CVSS ${f.severity.cvss.toFixed(1)})`;\n return `| ${f.findingId} | ${escapePipes(f.title)} | ${severity} | ${f.status} | ${formatDate(f.createdAt)} |`;\n });\n\n return [\n ...header,\n \"\",\n `## Findings (${findings.length})`,\n \"\",\n \"| Finding ID | Title | Severity | Status | Created |\",\n \"| --- | --- | --- | --- | --- |\",\n ...rows,\n ].join(\"\\n\");\n}\n\nfunction formatAssetRow(a: Asset): string {\n return `| ${a.assetId} | ${a.type} | ${escapePipes(a.value)} | ${a.source} | ${a.status} | ${formatDate(a.lastSeenAt)} |`;\n}\n\nfunction escapePipes(value: string): string {\n return value.replace(/\\|/g, \"\\\\|\");\n}\n\nfunction formatDate(epoch: number): string {\n if (!epoch) return \"—\";\n // Odin stores timestamps as milliseconds for documents and seconds for some fields\n // (pentest startAt/endAt are seconds). Heuristic: values < 10^12 are seconds.\n const ms = epoch < 1e12 ? epoch * 1000 : epoch;\n return new Date(ms).toISOString().slice(0, 10);\n}\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatFindingDetail } from \"../formatters/markdown.js\";\nimport type { FindingDetailResponse } from \"../types.js\";\n\nexport const getFindingInput = z.object({\n findingId: z.string().min(1),\n});\n\nexport const getFindingTool = {\n name: \"get_finding\",\n title: \"Get finding\",\n description:\n \"Fetch a single finding by ID, including the latest revision content (description, impact, details, remediation).\",\n inputSchema: getFindingInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof getFindingInput>) => {\n const data = await client.get<FindingDetailResponse>(\n `/api/findings/${encodeURIComponent(params.findingId)}`,\n );\n return {\n markdown: formatFindingDetail(data),\n structured: {\n finding: data.finding,\n revision: data.revision,\n },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatPentestDetail } from \"../formatters/markdown.js\";\nimport type { PentestDetailResponse } from \"../types.js\";\n\nexport const getPentestInput = z.object({\n pentestId: z.string().min(1),\n});\n\nexport const getPentestTool = {\n name: \"get_pentest\",\n title: \"Get pentest\",\n description:\n \"Fetch a single pentest by ID, including status, scheduled window, and in-scope targets.\",\n inputSchema: getPentestInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof getPentestInput>) => {\n const data = await client.get<PentestDetailResponse>(\n `/api/pentests/${encodeURIComponent(params.pentestId)}`,\n );\n return {\n markdown: formatPentestDetail(data),\n structured: { pentest: data.pentest },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatAssetsList } from \"../formatters/markdown.js\";\nimport type { AssetsListResponse } from \"../types.js\";\n\nexport const listAssetsInput = z.object({\n type: z.enum([\"DOMAIN\", \"SUBDOMAIN\", \"IP\", \"URL\", \"WILDCARD\"]).optional(),\n source: z.enum([\"MANUAL\", \"RECON\", \"IMPORT\", \"AGENT\"]).optional(),\n status: z\n .string()\n .optional()\n .describe(\n \"Dynamic asset status set by recon (e.g. ACTIVE, INACTIVE, TLS_ERROR).\",\n ),\n search: z.string().optional(),\n sortBy: z.enum([\"lastSeenAt\", \"createdAt\", \"value\"]).optional(),\n sortOrder: z.enum([\"asc\", \"desc\"]).optional(),\n limit: z.number().int().min(1).max(100).optional().default(50),\n offset: z.number().int().min(0).optional().default(0),\n});\n\nexport const listAssetsTool = {\n name: \"list_assets\",\n title: \"List assets\",\n description:\n \"List attack-surface assets for the organisation. Supports filtering by type, source, status, and free-text search.\",\n inputSchema: listAssetsInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof listAssetsInput>) => {\n const data = await client.get<AssetsListResponse>(\"/api/assets\", params);\n return {\n markdown: formatAssetsList(data),\n structured: {\n assets: data.assets,\n stats: data.stats,\n pagination: data.pagination,\n },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatFindingsList } from \"../formatters/markdown.js\";\nimport type { FindingsListResponse } from \"../types.js\";\n\nexport const listFindingsInput = z.object({\n pentestId: z.string().optional().describe(\"Filter to a single pentest.\"),\n assetId: z\n .string()\n .optional()\n .describe(\n \"Filter to findings whose latest revision references this asset.\",\n ),\n severity: z\n .enum([\"CRITICAL\", \"HIGH\", \"MEDIUM\", \"LOW\", \"INFORMATIONAL\"])\n .optional(),\n status: z\n .enum([\n \"DRAFT\",\n \"IN_REVIEW\",\n \"REPORTED\",\n \"MITIGATING\",\n \"OPEN_FOR_RETEST\",\n \"RESOLVED\",\n \"NOT_REPRODUCIBLE\",\n \"ACCEPTED_RISK\",\n ])\n .optional(),\n search: z.string().optional().describe(\"Free-text search on finding titles.\"),\n sortBy: z.enum([\"severity\", \"createdAt\"]).optional(),\n sortOrder: z.enum([\"asc\", \"desc\"]).optional(),\n limit: z.number().int().min(1).max(200).optional().default(50),\n offset: z.number().int().min(0).optional().default(0),\n});\n\nexport const listFindingsTool = {\n name: \"list_findings\",\n title: \"List findings\",\n description:\n \"List security findings for the organisation. Supports filtering by pentest, severity, status, and free-text search.\",\n inputSchema: listFindingsInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof listFindingsInput>) => {\n const data = await client.get<FindingsListResponse>(\n \"/api/findings\",\n params,\n );\n return {\n markdown: formatFindingsList(data),\n structured: {\n findings: data.findings,\n stats: data.stats,\n pagination: data.pagination,\n },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatPentestsList } from \"../formatters/markdown.js\";\nimport type { PentestListResponse } from \"../types.js\";\n\nexport const listPentestsInput = z.object({});\n\nexport const listPentestsTool = {\n name: \"list_pentests\",\n title: \"List pentests\",\n description:\n \"List all pentests for the organisation, including status, scheduled window, and in-scope targets.\",\n inputSchema: listPentestsInput,\n handler: (client: OdinClient) => async () => {\n const data = await client.get<PentestListResponse>(\"/api/pentests\");\n return {\n markdown: formatPentestsList(data),\n structured: { pentests: data.pentests },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatFindingStatusUpdate } from \"../formatters/markdown.js\";\nimport type { FindingStatusUpdateResponse } from \"../types.js\";\n\nexport const updateFindingStatusInput = z.object({\n findingId: z.string().min(1),\n status: z\n .enum([\"MITIGATING\", \"OPEN_FOR_RETEST\", \"ACKNOWLEDGED\"])\n .describe(\n \"Target status. Only MITIGATING, OPEN_FOR_RETEST, and ACKNOWLEDGED are client-writable; transitions are only allowed from REPORTED, MITIGATING, or OPEN_FOR_RETEST. ACKNOWLEDGED marks the finding as accepted-risk and is terminal.\",\n ),\n});\n\nexport const updateFindingStatusTool = {\n name: \"update_finding_status\",\n title: \"Update finding status\",\n description:\n \"Change a finding's status. Requires an API key with write permission and member role. Only MITIGATING, OPEN_FOR_RETEST, and ACKNOWLEDGED are accepted.\",\n inputSchema: updateFindingStatusInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof updateFindingStatusInput>) => {\n const data = await client.patch<FindingStatusUpdateResponse>(\n `/api/findings/${encodeURIComponent(params.findingId)}/status`,\n { status: params.status },\n );\n return {\n markdown: formatFindingStatusUpdate(data.finding),\n structured: { finding: data.finding },\n };\n },\n};\n"],"mappings":"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","names":["z","z","z","z","z","z","z","z","z","z","z","z"]}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@borgresearch/odin-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for the Odin security API",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "odin",
9
+ "security"
10
+ ],
11
+ "homepage": "https://github.com/BorgDevelopment/borg-platform/tree/develop/frontend/packages/odin-mcp",
12
+ "bugs": {
13
+ "url": "https://github.com/BorgDevelopment/borg-platform/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/BorgDevelopment/borg-platform.git",
18
+ "directory": "frontend/packages/odin-mcp"
19
+ },
20
+ "license": "MIT",
21
+ "author": "Borg Research",
22
+ "type": "module",
23
+ "bin": {
24
+ "odin-mcp": "./dist/index.js"
25
+ },
26
+ "main": "./dist/index.js",
27
+ "module": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.js"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public",
42
+ "registry": "https://registry.npmjs.org/"
43
+ },
44
+ "engines": {
45
+ "node": ">=22.0.0"
46
+ },
47
+ "scripts": {
48
+ "build": "tsup",
49
+ "dev": "tsup --watch",
50
+ "typecheck": "tsc --noEmit",
51
+ "test": "vitest run",
52
+ "test:run": "vitest run",
53
+ "lint": "biome check .",
54
+ "format": "biome format --write .",
55
+ "inspect": "mcp-inspector node dist/index.js",
56
+ "inspect:dev": "pnpm build && pnpm inspect"
57
+ },
58
+ "dependencies": {
59
+ "@modelcontextprotocol/sdk": "^1.29.0",
60
+ "zod": "^4.3.6"
61
+ },
62
+ "devDependencies": {
63
+ "@biomejs/biome": "2.3.14",
64
+ "@modelcontextprotocol/inspector": "^0.21.2",
65
+ "@types/node": "^22.19.8",
66
+ "tsup": "^8.3.5",
67
+ "typescript": "^5.9.3",
68
+ "vitest": "^4.1.0"
69
+ }
70
+ }