@borgresearch/odin-mcp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,18 +1,20 @@
1
1
  # @borgresearch/odin-mcp
2
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.
3
+ [Model Context Protocol](https://modelcontextprotocol.io) server for the [Odin](https://odin.borghq.io) security platform by Borg. It gives coding agents organisation-scoped finding context and a safe GitHub-first remediation loop.
4
+
5
+ The MCP can inspect a finding such as `ODI-12`, show the remediation and suggested branch, link an existing accessible GitHub pull request, and report automatic retest history. It never creates commits, pushes branches, or creates pull requests.
4
6
 
5
7
  ## Prerequisites
6
8
 
7
- - Node.js 22 or newer.
9
+ - Node.js 24 or newer.
8
10
  - 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.
11
+ - An Odin API key. **Read only** is enough for discovery, remediation context, and retest reads. **Read & Write** is required for status changes and linking an existing pull request.
10
12
 
11
13
  ## Quick start
12
14
 
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.
15
+ 1. Sign in to <https://odin.borghq.io>, open **Management > API Keys**, and create a key. The dialog defaults to **Read & Write** for the remediation workflow; choose **Read only** when the agent should only inspect data.
16
+ 2. Copy the full key — it is shown once.
17
+ 3. Add the server to your client:
16
18
 
17
19
  ```sh
18
20
  ODIN_API_KEY=odin_... npx @borgresearch/odin-mcp
@@ -25,75 +27,85 @@ npm install -g @borgresearch/odin-mcp
25
27
  ODIN_API_KEY=odin_... odin-mcp
26
28
  ```
27
29
 
30
+ Restart your MCP client after changing its configuration. Nine tools should be available after startup.
31
+
28
32
  ## Configuration
29
33
 
30
34
  | Variable | Required | Notes |
31
35
  |----------|----------|-------|
32
- | `ODIN_API_KEY` | yes | API key with at least `read` permission. `write` is required for `update_finding_status`. |
36
+ | `ODIN_API_KEY` | yes | An organisation-scoped key with `read`; `write` is required for mutations. |
37
+ | `ODIN_BASE_URL` | no | Odin API base URL. Defaults to `https://odin.borghq.io`. |
33
38
 
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.
39
+ Each call sends `Authorization: Bearer ${ODIN_API_KEY}`. Keys belong to one organisation. If you belong to multiple organisations, create one key per organisation and run a separate MCP server entry for each.
35
40
 
36
41
  ## Tools
37
42
 
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).
43
+ All tools accept one JSON object and return both a Markdown summary and a structured payload.
39
44
 
40
45
  | Name | Permission | Description |
41
46
  |------|------------|-------------|
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. |
47
+ | `list_findings` | Read | List findings with pagination, severity, repository, current status, and title/identifier search. |
48
+ | `get_finding` | Read | Get bounded remediation context, revision content, suggested branch, linked PRs, workflow readiness, and latest retest state. |
49
+ | `update_finding_status` | Read & Write | Move a finding to `MITIGATING`, `OPEN_FOR_RETEST`, or `ACKNOWLEDGED`. Cookie-authenticated callers also need member role. |
50
+ | `link_finding_pull_request` | Read & Write | Link an existing accessible GitHub PR. It does not create, modify, merge, or push a PR. |
51
+ | `get_finding_retests` | Read | Read retest history; it never triggers a retest or spends credits. |
52
+ | `list_pentests` | Read | List pentests for the organisation. |
53
+ | `get_pentest` | Read | Get a pentest's status, schedule, and scope. |
54
+ | `list_assets` | Read | List attack-surface assets with filters and pagination. |
55
+ | `get_asset` | Read | Get an asset and the findings that reference it. |
56
+
57
+ ### Finding references
49
58
 
50
- ### Tool parameters and examples
59
+ Finding references are organisation-local. Prefer a human identifier such as `ODI-12`; an internal `findingId` is retained for chaining tool calls. Odin looks up the numeric suffix only within the organisation belonging to the API key and returns the canonical stored identifier. It never searches across organisations or reveals a cross-organisation match through an error.
51
60
 
52
- #### `list_findings`
61
+ ### `list_findings`
53
62
 
54
- `pentestId?`, `assetId?`, `severity?` (`CRITICAL` \| `HIGH` \| `MEDIUM` \| `LOW` \| `INFORMATIONAL`), `status?`, `search?`, `sortBy?` (`severity` \| `createdAt`), `sortOrder?`, `limit?` (1–200, default 50), `offset?`.
63
+ Optional parameters include `pentestId`, `assetId`, `repository`, `severity` (`CRITICAL` | `HIGH` | `MEDIUM` | `LOW` | `INFORMATIONAL`), `status` (`REPORTED` | `ACKNOWLEDGED` | `MITIGATING` | `OPEN_FOR_RETEST` | `NEEDS_REVISION` | `FIXED_AND_RETESTED`), `search`, `sortBy` (`severity` | `createdAt`), `sortOrder`, `limit` (1–200, default 50), and `offset`.
55
64
 
56
- > _"Show me the open critical and high findings from pentest `p_abc123`."_
57
- > _"Search my findings for anything mentioning `S3`."_
65
+ ### `get_finding`
58
66
 
59
- #### `get_finding`
67
+ Provide `identifier` or `findingId` (at least one is required). The response includes the current status and severity, revision summary/description/impact/details/remediation, affected assets, code reference where available, repository, linked GitHub PR metadata, `suggestedBranch`, retest state, and workflow blockers.
60
68
 
61
- `findingId` (required).
69
+ Example prompt:
62
70
 
63
- > _"What are the remediation steps for finding `f_abc123`?"_
71
+ > Explain `ODI-12`, show me the suggested branch, and tell me what must be ready before I open a PR.
64
72
 
65
- #### `update_finding_status`
73
+ ### GitHub remediation loop
66
74
 
67
- `findingId` (required), `status` (`MITIGATING` \| `OPEN_FOR_RETEST`).
75
+ 1. Ask `get_finding` about the human identifier.
76
+ 2. Create the returned branch, make and test the fix, commit it, push it, and open the GitHub PR yourself in Cursor or your normal development environment.
77
+ 3. Explicitly ask Odin to link that existing PR with `link_finding_pull_request` and its URL. The operation is idempotent and verifies that the organisation's GitHub App installation can access the repository.
78
+ 4. When the linked PR merges, automatic webhook processing may queue a retest. Use `get_finding_retests` to read `QUEUED`, `RUNNING`, `COMPLETED`, `SKIPPED`, or `FAILED` entries and their sanitised verdicts.
68
79
 
69
- > _"I've started working on `f_abc123`, mark it as mitigating."_
70
- > _"I've fixed `f_abc123`, mark it ready for retest."_
80
+ Odin does not infer permission from a matching PR. Ask before linking a PR or changing status. The MCP does not create commits, push branches, create PRs, merge PRs, or manually trigger paid retests.
71
81
 
72
- Other transitions (`RESOLVED`, `ACCEPTED_RISK`, etc.) are reserved for the operations team and are intentionally not writable from the API.
82
+ ### `update_finding_status`
73
83
 
74
- #### `list_pentests` / `get_pentest`
84
+ Provide `identifier` or `findingId` and one of:
75
85
 
76
- `list_pentests` takes no parameters. `get_pentest` requires `pentestId`.
86
+ | Status | Use it when |
87
+ |--------|-------------|
88
+ | `MITIGATING` | You have started work on the fix. |
89
+ | `OPEN_FOR_RETEST` | The fix is ready for verification. |
90
+ | `ACKNOWLEDGED` | You accept the risk for now. |
77
91
 
78
- > _"What's in scope for pentest `p_xyz`?"_
79
- > _"List all my pentests scheduled this quarter."_
92
+ A Read-only key receives an actionable 403. Create a new Read & Write key for mutations; existing key permissions are not silently upgraded. `NEEDS_REVISION` and `FIXED_AND_RETESTED` are controlled by Borg's retest process.
80
93
 
81
- #### `list_assets`
94
+ ### `link_finding_pull_request`
82
95
 
83
- `type?` (`DOMAIN` \| `SUBDOMAIN` \| `IP` \| `URL` \| `WILDCARD`), `source?` (`MANUAL` \| `RECON` \| `IMPORT` \| `AGENT`), `status?`, `search?`, `sortBy?`, `sortOrder?`, `limit?` (1–100, default 50), `offset?`.
96
+ Provide `identifier` or `findingId` and an existing GitHub pull request `url`. The GitHub integration must be active and able to access the repository. The tool returns the canonical finding reference, verified PR metadata, resulting status, and workflow readiness. It does not modify GitHub.
84
97
 
85
- > _"List subdomains discovered by recon."_
86
- > _"Which manually added domains are currently down?"_
98
+ ### `get_finding_retests`
87
99
 
88
- #### `get_asset`
100
+ Provide `identifier` or `findingId`. The tool is read-only and distinguishes no history from a history containing queued/running, completed, skipped, or failed runs. It does not call the manual retest endpoint and never spends credits.
89
101
 
90
- `assetId` (required).
102
+ ### Other tools
91
103
 
92
- > _"Summarise findings affecting `api.example.com` and group them by severity."_
104
+ `list_pentests` takes no parameters and `get_pentest` requires `pentestId`. `list_assets` and `get_asset` retain their existing type, source, status, search, sorting, and pagination parameters.
93
105
 
94
106
  ## Use with Claude Desktop
95
107
 
96
- Add to your `claude_desktop_config.json`:
108
+ Add to `claude_desktop_config.json`:
97
109
 
98
110
  ```json
99
111
  {
@@ -112,12 +124,23 @@ Add to your `claude_desktop_config.json`:
112
124
  ## Use with Claude Code
113
125
 
114
126
  ```sh
115
- claude mcp add odin --env ODIN_API_KEY=odin_... -- npx -y @borgresearch/odin-mcp
127
+ claude mcp add --transport stdio --env ODIN_API_KEY=odin_... odin -- npx -y @borgresearch/odin-mcp
128
+ ```
129
+
130
+ ## Use with Codex
131
+
132
+ Add to `~/.codex/config.toml`, then restart Codex:
133
+
134
+ ```toml
135
+ [mcp_servers.odin]
136
+ command = "npx"
137
+ args = ["-y", "@borgresearch/odin-mcp"]
138
+ env = { ODIN_API_KEY = "odin_..." }
116
139
  ```
117
140
 
118
141
  ## Use with Cursor
119
142
 
120
- Open **Settings > MCP** and add a new server, or add the following to your project's `.cursor/mcp.json`:
143
+ Open **Settings > MCP**, or add this to `.cursor/mcp.json`:
121
144
 
122
145
  ```json
123
146
  {
@@ -135,7 +158,7 @@ Open **Settings > MCP** and add a new server, or add the following to your proje
135
158
 
136
159
  ## Use with Windsurf
137
160
 
138
- Add the following to `~/.codeium/windsurf/mcp_config.json`:
161
+ Add to `~/.codeium/windsurf/mcp_config.json`:
139
162
 
140
163
  ```json
141
164
  {
@@ -153,7 +176,7 @@ Add the following to `~/.codeium/windsurf/mcp_config.json`:
153
176
 
154
177
  ## Use with VS Code (GitHub Copilot)
155
178
 
156
- Add the following to your project's `.vscode/mcp.json`:
179
+ Add to `.vscode/mcp.json`:
157
180
 
158
181
  ```json
159
182
  {
@@ -171,35 +194,28 @@ Add the following to your project's `.vscode/mcp.json`:
171
194
 
172
195
  ## Use with Cline
173
196
 
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
- ```
197
+ Open **Settings > MCP Servers > Edit MCP Settings** and add the same `mcpServers.odin` JSON shape shown for Cursor. Pass `ODIN_API_KEY` in the server environment and restart the client after editing its MCP configuration.
189
198
 
190
199
  ## Troubleshooting
191
200
 
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.
201
+ **`ODIN_API_KEY is not set or invalid`** — check the key under **Management > API Keys**, confirm the client config sets `env.ODIN_API_KEY`, and restart the client.
193
202
 
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.
203
+ **`API key does not have write permission`** — the agent attempted a status change or PR link with a Read-only key. Create a new key with **Read & Write** permissions.
195
204
 
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`).
205
+ **`Finding not found`** — check the `ODI-12` reference and the organisation associated with the key. Keys and human identifiers are organisation-scoped by design.
197
206
 
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).
207
+ **Retest history is unavailable** — the organisation's retest automation capability is gated or not configured. This does not enable manual retests; check the GitHub and automatic-retest settings.
199
208
 
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.
209
+ **The server starts but no tools appear** — most clients require a full restart after changing configuration. Confirm Node.js 24 or newer is on the client's `PATH` and inspect its MCP logs.
201
210
 
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.
211
+ ## Development
212
+
213
+ ```sh
214
+ pnpm test:run
215
+ pnpm typecheck
216
+ pnpm lint
217
+ pnpm build
218
+ ```
203
219
 
204
220
  ## License
205
221
 
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ var OdinClient = class {
27
27
  baseUrl;
28
28
  apiKey;
29
29
  fetchImpl;
30
+ vercelProtectionBypassSecret;
30
31
  constructor(options) {
31
32
  if (!options.apiKey) {
32
33
  throw new Error("OdinClient requires an apiKey");
@@ -37,6 +38,7 @@ var OdinClient = class {
37
38
  ""
38
39
  );
39
40
  this.fetchImpl = options.fetchImpl ?? fetch;
41
+ this.vercelProtectionBypassSecret = options.vercelProtectionBypassSecret?.trim() || void 0;
40
42
  }
41
43
  async get(path, searchParams) {
42
44
  const url = this.buildUrl(path, searchParams);
@@ -46,6 +48,8 @@ var OdinClient = class {
46
48
  const url = this.buildUrl(path);
47
49
  return this.request("PATCH", url, path, body);
48
50
  }
51
+ // Public HTTP method API — kept alongside get/patch even when unused internally
52
+ // fallow-ignore-next-line unused-class-member
49
53
  async post(path, body) {
50
54
  const url = this.buildUrl(path);
51
55
  return this.request("POST", url, path, body);
@@ -61,13 +65,17 @@ var OdinClient = class {
61
65
  return url.toString();
62
66
  }
63
67
  async request(method, url, path, body) {
68
+ const headers = {
69
+ Authorization: `Bearer ${this.apiKey}`,
70
+ "Content-Type": "application/json",
71
+ Accept: "application/json"
72
+ };
73
+ if (this.vercelProtectionBypassSecret) {
74
+ headers["x-vercel-protection-bypass"] = this.vercelProtectionBypassSecret;
75
+ }
64
76
  const response = await this.fetchImpl(url, {
65
77
  method,
66
- headers: {
67
- Authorization: `Bearer ${this.apiKey}`,
68
- "Content-Type": "application/json",
69
- Accept: "application/json"
70
- },
78
+ headers,
71
79
  body: body === void 0 ? void 0 : JSON.stringify(body)
72
80
  });
73
81
  const rawText = await response.text();
@@ -157,6 +165,21 @@ function extractServerMessage(parsed) {
157
165
  import { z } from "zod";
158
166
 
159
167
  // src/formatters/markdown.ts
168
+ var STATUS_LABELS = {
169
+ REPORTED: "Reported",
170
+ ACKNOWLEDGED: "Acknowledged",
171
+ MITIGATING: "Mitigating",
172
+ OPEN_FOR_RETEST: "Open for Retest",
173
+ NEEDS_REVISION: "Needs Revision",
174
+ FIXED_AND_RETESTED: "Fixed & Retested"
175
+ };
176
+ var RETEST_STATUS_LABELS = {
177
+ QUEUED: "Queued",
178
+ RUNNING: "Running",
179
+ COMPLETED: "Completed",
180
+ FAILED: "Failed",
181
+ SKIPPED: "Skipped"
182
+ };
160
183
  function formatFindingsList(data) {
161
184
  const { findings, stats, pagination } = data;
162
185
  if (findings.length === 0) {
@@ -164,60 +187,159 @@ function formatFindingsList(data) {
164
187
  }
165
188
  const rows = findings.map((f) => {
166
189
  const severity = `${f.severity.label} (CVSS ${f.severity.cvss.toFixed(1)})`;
167
- return `| ${f.findingId} | ${escapePipes(f.title)} | ${severity} | ${f.status} | ${formatDate(f.createdAt)} |`;
190
+ return `| ${f.identifier ?? "\u2014"} | ${f.findingId} | ${escapePipes(f.title)} | ${severity} | ${formatStatus(f.status)} | ${formatDate(f.createdAt)} |`;
168
191
  });
169
192
  return [
170
193
  `Showing ${findings.length} of ${stats.total} finding(s). Offset ${pagination.offset}, limit ${pagination.limit}. hasMore=${pagination.hasMore}.`,
171
194
  "",
172
- "| Finding ID | Title | Severity | Status | Created |",
173
- "| --- | --- | --- | --- | --- |",
195
+ "| Identifier | Internal ID | Title | Severity | Status | Created |",
196
+ "| --- | --- | --- | --- | --- | --- |",
174
197
  ...rows
175
198
  ].join("\n");
176
199
  }
177
200
  function formatFindingDetail(data) {
178
201
  const { finding, revision } = data;
202
+ const context = data.remediationContext;
203
+ const identifier = context?.identifier ?? finding.identifier ?? null;
204
+ const displayIdentifier = identifier ?? finding.findingId;
179
205
  const severity = `${finding.severity.label} (CVSS ${finding.severity.cvss.toFixed(1)})`;
180
206
  const header = [
181
- `# ${finding.title}`,
207
+ `# ${escapePipes(displayIdentifier)} \u2014 ${escapePipes(finding.title)}`,
182
208
  "",
183
- `- **ID:** ${finding.findingId}`,
209
+ `- **Finding:** ${displayIdentifier}`,
210
+ `- **Internal ID:** ${finding.findingId}`,
184
211
  `- **Severity:** ${severity}`,
185
- `- **Status:** ${finding.status}`,
212
+ `- **Status:** ${formatStatus(finding.status)}`,
213
+ `- **Repository:** ${context?.repository ?? finding.repository ?? "Not recorded"}`,
186
214
  `- **Pentest:** ${finding.context?.pentestId ?? "standalone"}`,
187
215
  `- **Created:** ${formatDate(finding.createdAt)}`,
188
216
  `- **Updated:** ${formatDate(finding.updatedAt)}`
189
217
  ];
190
- if (!revision) {
191
- return [...header, "", "_No revision content available._"].join("\n");
192
- }
193
- const c = revision.content;
218
+ const content = context ? {
219
+ revisionNumber: context.revision.revisionNumber,
220
+ summary: context.revision.summary,
221
+ description: context.revision.description,
222
+ impact: context.revision.impact,
223
+ details: context.revision.details,
224
+ remediation: context.revision.remediation,
225
+ affectedAssets: context.revision.affectedAssets,
226
+ codeReference: context.revision.codeReference
227
+ } : revision ? {
228
+ revisionNumber: revision.revisionNumber,
229
+ summary: revision.content.summaryMd,
230
+ description: revision.content.descriptionMd,
231
+ impact: revision.content.businessImpactMd,
232
+ details: revision.content.detailsMd,
233
+ remediation: revision.content.remediationMd,
234
+ affectedAssets: revision.content.affectedAssets ?? [],
235
+ codeReference: null
236
+ } : null;
194
237
  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) {
238
+ if (!content) {
239
+ sections.push("_No revision content available._");
240
+ } else {
241
+ sections.push(`_Revision ${content.revisionNumber ?? "unknown"}_`);
242
+ if (content.summary) sections.push("## Summary", content.summary);
243
+ if (content.description)
244
+ sections.push("## Description", content.description);
245
+ if (content.impact) sections.push("## Business Impact", content.impact);
246
+ if (content.details) sections.push("## Details", content.details);
247
+ if (content.remediation)
248
+ sections.push("## Remediation", content.remediation);
249
+ if (content.codeReference) {
250
+ sections.push(
251
+ "## Code reference",
252
+ `- ${content.codeReference.file}:${content.codeReference.line}`
253
+ );
254
+ if (content.codeReference.snippet) {
255
+ sections.push("```", content.codeReference.snippet, "```");
256
+ }
257
+ }
258
+ if (content.affectedAssets?.length) {
259
+ sections.push(
260
+ "## Affected Assets",
261
+ content.affectedAssets.map((asset) => `- ${asset.display} (${asset.assetId})`).join("\n")
262
+ );
263
+ }
264
+ }
265
+ if (context) {
202
266
  sections.push(
203
- "## Affected Assets",
204
- c.affectedAssets.map((a) => `- ${a.display} (${a.assetId})`).join("\n")
267
+ "## Remediation workflow",
268
+ `- **Suggested branch:** ${context.suggestedBranch ?? "Not available"}`,
269
+ `- **Ready for a user-created PR:** ${yesNo(context.workflow.readyForUserCreatedPullRequest)}`,
270
+ `- **GitHub connected:** ${yesNo(context.workflow.githubConnected)}`,
271
+ `- **Automatic retests:** ${context.workflow.automaticRetestsReady ? "Ready after merge" : "Not ready"}`
205
272
  );
273
+ if (context.workflow.blockers.length) {
274
+ sections.push(
275
+ "### Blockers",
276
+ context.workflow.blockers.map((blocker) => `- ${blocker}`).join("\n")
277
+ );
278
+ }
279
+ if (context.workflow.nextSteps.length) {
280
+ sections.push(
281
+ "### Next steps",
282
+ context.workflow.nextSteps.map((step) => `- ${step}`).join("\n")
283
+ );
284
+ }
285
+ if (context.linkedPullRequests.length) {
286
+ sections.push(
287
+ "## Linked GitHub pull requests",
288
+ context.linkedPullRequests.map(
289
+ (pullRequest) => `- [#${pullRequest.number}](${pullRequest.url}) ${escapePipes(pullRequest.title)} \u2014 ${pullRequest.state}, linked via ${pullRequest.linkedVia}`
290
+ ).join("\n")
291
+ );
292
+ } else {
293
+ sections.push("## Linked GitHub pull requests", "_None linked._");
294
+ }
295
+ sections.push("## Latest retest");
296
+ if (context.latestRetest.state === "unavailable") {
297
+ sections.push(`_${context.latestRetest.reason}_`);
298
+ } else if (context.latestRetest.state === "none") {
299
+ sections.push("_No retest history is available yet._");
300
+ } else {
301
+ sections.push(formatRetestSummary(context.latestRetest.retest));
302
+ }
206
303
  }
207
- return [
208
- ...header,
209
- "",
210
- `_Revision ${revision.revisionNumber}_`,
211
- "",
212
- ...sections
213
- ].join("\n");
304
+ return [...header, "", ...sections].join("\n");
214
305
  }
215
306
  function formatFindingStatusUpdate(finding) {
307
+ const identifier = finding.identifier ?? finding.findingId;
216
308
  return [
217
- `Updated finding **${finding.findingId}** status to **${finding.status}**.`,
309
+ `Updated finding **${identifier}** (internal ID ${finding.findingId}) status to **${formatStatus(finding.status)}**.`,
218
310
  `Title: ${finding.title}`
219
311
  ].join("\n");
220
312
  }
313
+ function formatFindingPullRequestLink(data) {
314
+ const identifier = data.finding.identifier ?? data.finding.findingId;
315
+ const pullRequest = data.pullRequest;
316
+ const readiness = data.workflow.automaticRetestsReady ? "Automatic retesting is ready after this PR merges." : "Automatic retesting is not currently ready; review the blockers below.";
317
+ const blockers = data.workflow.blockers.length ? `
318
+ ${data.workflow.blockers.map((blocker) => `- ${blocker}`).join("\n")}` : "";
319
+ return [
320
+ `Linked existing GitHub pull request [#${pullRequest.number}](${pullRequest.url}) to **${identifier}**.`,
321
+ `Repository: ${pullRequest.repo}; state: ${pullRequest.state}; finding status: ${formatStatus(data.finding.status)}.`,
322
+ "Odin did not create, modify, merge, or push this pull request.",
323
+ readiness,
324
+ blockers ? `Workflow blockers:${blockers}` : ""
325
+ ].filter(Boolean).join("\n");
326
+ }
327
+ function formatFindingRetests(data) {
328
+ if (data.state === "none" || data.retests.length === 0) {
329
+ return "No retest history is available for this finding.";
330
+ }
331
+ return [
332
+ `Retest history (${data.retests.length}):`,
333
+ "",
334
+ "| Retest | Status | Trigger | Verdict | Created | Completed |",
335
+ "| --- | --- | --- | --- | --- | --- |",
336
+ ...data.retests.map(
337
+ (retest) => `| ${retest.retestId} | ${RETEST_STATUS_LABELS[retest.status]} | ${retest.trigger} | ${retest.combinedVerdict ?? "\u2014"} | ${formatDate(retest.createdAt)} | ${formatDate(retest.completedAt ?? 0)} |`
338
+ ),
339
+ "",
340
+ "Queued and running entries are still in progress; completed, skipped, and failed entries are final for that run."
341
+ ].join("\n");
342
+ }
221
343
  function formatPentestsList(data) {
222
344
  if (data.pentests.length === 0) return "No pentests found.";
223
345
  const rows = data.pentests.map(formatPentestRow);
@@ -245,10 +367,15 @@ function formatPentestDetail(data) {
245
367
  scope
246
368
  ].join("\n");
247
369
  }
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)} |`;
370
+ function formatFindingsForAsset(findings) {
371
+ if (findings.length === 0) return "_No findings reference this asset._";
372
+ return [
373
+ `## Findings (${findings.length})`,
374
+ "",
375
+ "| Identifier | Internal ID | Title | Severity | Status | Created |",
376
+ "| --- | --- | --- | --- | --- | --- |",
377
+ ...findings.map(formatFindingRow)
378
+ ].join("\n");
252
379
  }
253
380
  function formatAssetsList(data) {
254
381
  const { assets, stats, pagination } = data;
@@ -274,26 +401,36 @@ function formatAssetDetail(data) {
274
401
  `- **Last seen:** ${formatDate(asset.lastSeenAt)}`,
275
402
  `- **Created:** ${formatDate(asset.createdAt)}`
276
403
  ];
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");
404
+ return [...header, "", formatFindingsForAsset(findings)].join("\n");
405
+ }
406
+ function formatFindingRow(finding) {
407
+ const severity = `${finding.severity.label} (CVSS ${finding.severity.cvss.toFixed(1)})`;
408
+ return `| ${finding.identifier ?? "\u2014"} | ${finding.findingId} | ${escapePipes(finding.title)} | ${severity} | ${formatStatus(finding.status)} | ${formatDate(finding.createdAt)} |`;
409
+ }
410
+ function formatPentestRow(p) {
411
+ const end = p.endAt ? formatDate(p.endAt) : "open";
412
+ const scope = p.scope.slice(0, 3).join(", ") + (p.scope.length > 3 ? ", \u2026" : "");
413
+ return `| ${p.pentestId} | ${escapePipes(p.name)} | ${p.status} | ${formatDate(p.startAt)} | ${end} | ${escapePipes(scope)} |`;
293
414
  }
294
415
  function formatAssetRow(a) {
295
416
  return `| ${a.assetId} | ${a.type} | ${escapePipes(a.value)} | ${a.source} | ${a.status} | ${formatDate(a.lastSeenAt)} |`;
296
417
  }
418
+ function formatRetestSummary(retest) {
419
+ return [
420
+ `- **Status:** ${RETEST_STATUS_LABELS[retest.status]}`,
421
+ `- **Trigger:** ${retest.trigger}`,
422
+ `- **Verdict:** ${retest.combinedVerdict ?? "Not available yet"}`,
423
+ `- **Finding action:** ${retest.statusAction ?? "None"}`,
424
+ `- **Created:** ${formatDate(retest.createdAt)}`,
425
+ `- **Completed:** ${formatDate(retest.completedAt ?? 0)}`
426
+ ].join("\n");
427
+ }
428
+ function formatStatus(status) {
429
+ return STATUS_LABELS[status] ?? status;
430
+ }
431
+ function yesNo(value) {
432
+ return value ? "Yes" : "No";
433
+ }
297
434
  function escapePipes(value) {
298
435
  return value.replace(/\|/g, "\\|");
299
436
  }
@@ -323,34 +460,101 @@ var getAssetTool = {
323
460
  }
324
461
  };
325
462
 
326
- // src/tools/get-finding.ts
463
+ // src/tools/finding-reference.ts
327
464
  import { z as z2 } from "zod";
328
- var getFindingInput = z2.object({
329
- findingId: z2.string().min(1)
465
+ var findingReferenceFields = {
466
+ identifier: z2.string().trim().min(1).optional().describe(
467
+ 'Human-readable finding identifier such as "ODI-12". Preferred input.'
468
+ ),
469
+ findingId: z2.string().trim().min(1).optional().describe(
470
+ "Raw internal finding ID, useful when chaining from another tool."
471
+ )
472
+ };
473
+ var findingReferenceInput = z2.object(findingReferenceFields).refine((value) => Boolean(value.identifier || value.findingId), {
474
+ message: "Provide either identifier or findingId."
330
475
  });
476
+ function findingReferenceValue(reference) {
477
+ const value = reference.identifier ?? reference.findingId;
478
+ if (!value) {
479
+ throw new Error("A finding identifier or findingId is required.");
480
+ }
481
+ return value;
482
+ }
483
+ function findingReferencePath(reference, suffix = "") {
484
+ return `/api/findings/${encodeURIComponent(findingReferenceValue(reference))}${suffix}`;
485
+ }
486
+ async function resolveFindingId(client, reference) {
487
+ if (reference.findingId) return reference.findingId;
488
+ const data = await client.get(
489
+ findingReferencePath(reference)
490
+ );
491
+ return data.remediationContext?.findingId ?? data.finding.findingId;
492
+ }
493
+
494
+ // src/tools/get-finding.ts
495
+ var getFindingInput = findingReferenceInput;
331
496
  var getFindingTool = {
332
497
  name: "get_finding",
333
- title: "Get finding",
334
- description: "Fetch a single finding by ID, including the latest revision content (description, impact, details, remediation).",
498
+ title: "Get finding remediation context",
499
+ 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.",
335
500
  inputSchema: getFindingInput,
336
501
  handler: (client) => async (params) => {
337
502
  const data = await client.get(
338
- `/api/findings/${encodeURIComponent(params.findingId)}`
503
+ findingReferencePath(params)
339
504
  );
340
505
  return {
341
506
  markdown: formatFindingDetail(data),
342
507
  structured: {
343
- finding: data.finding,
344
- revision: data.revision
508
+ finding: data.remediationContext ?? data.finding,
509
+ revision: data.remediationContext?.revision ?? data.revision,
510
+ remediationContext: data.remediationContext ?? null
345
511
  }
346
512
  };
347
513
  }
348
514
  };
349
515
 
350
- // src/tools/get-pentest.ts
516
+ // src/tools/get-finding-retests.ts
351
517
  import { z as z3 } from "zod";
352
- var getPentestInput = z3.object({
353
- pentestId: z3.string().min(1)
518
+ var getFindingRetestsInput = z3.object(findingReferenceFields).refine((value) => Boolean(value.identifier || value.findingId), {
519
+ message: "Provide either identifier or findingId."
520
+ });
521
+ var getFindingRetestsTool = {
522
+ name: "get_finding_retests",
523
+ title: "Get finding retest history",
524
+ 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.",
525
+ inputSchema: getFindingRetestsInput,
526
+ handler: (client) => async (params) => {
527
+ try {
528
+ const data = await client.get(
529
+ findingReferencePath(params, "/retests")
530
+ );
531
+ return {
532
+ markdown: formatFindingRetests(data),
533
+ structured: {
534
+ state: data.state,
535
+ retests: data.retests
536
+ }
537
+ };
538
+ } catch (error) {
539
+ if (error instanceof OdinApiError && error.status === 404 && isRetestHistoryUnavailable(error.body)) {
540
+ const reason = "Retest history is gated or unavailable for this organisation.";
541
+ return {
542
+ markdown: reason,
543
+ structured: { state: "unavailable", reason, retests: [] }
544
+ };
545
+ }
546
+ throw error;
547
+ }
548
+ }
549
+ };
550
+ function isRetestHistoryUnavailable(body) {
551
+ return typeof body === "object" && body !== null && "code" in body && body.code === "RETEST_HISTORY_UNAVAILABLE";
552
+ }
553
+
554
+ // src/tools/get-pentest.ts
555
+ import { z as z4 } from "zod";
556
+ var getPentestInput = z4.object({
557
+ pentestId: z4.string().min(1)
354
558
  });
355
559
  var getPentestTool = {
356
560
  name: "get_pentest",
@@ -368,19 +572,49 @@ var getPentestTool = {
368
572
  }
369
573
  };
370
574
 
575
+ // src/tools/link-finding-pull-request.ts
576
+ import { z as z5 } from "zod";
577
+ var linkFindingPullRequestInput = z5.object({
578
+ ...findingReferenceFields,
579
+ url: z5.string().url().describe("Existing GitHub pull request URL to link to the finding.")
580
+ }).refine((value) => Boolean(value.identifier || value.findingId), {
581
+ message: "Provide either identifier or findingId."
582
+ });
583
+ var linkFindingPullRequestTool = {
584
+ name: "link_finding_pull_request",
585
+ title: "Link an existing GitHub pull request",
586
+ 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.",
587
+ inputSchema: linkFindingPullRequestInput,
588
+ handler: (client) => async (params) => {
589
+ const data = await client.post(
590
+ findingReferencePath(params, "/link-pr"),
591
+ { url: params.url }
592
+ );
593
+ return {
594
+ markdown: formatFindingPullRequestLink(data),
595
+ structured: {
596
+ finding: data.finding,
597
+ pullRequest: data.pullRequest,
598
+ linkSource: data.linkSource,
599
+ workflow: data.workflow
600
+ }
601
+ };
602
+ }
603
+ };
604
+
371
605
  // 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(
606
+ import { z as z6 } from "zod";
607
+ var listAssetsInput = z6.object({
608
+ type: z6.enum(["DOMAIN", "SUBDOMAIN", "IP"]).optional(),
609
+ source: z6.enum(["MANUAL", "RECON", "IMPORT", "AGENT"]).optional(),
610
+ status: z6.string().optional().describe(
377
611
  "Dynamic asset status set by recon (e.g. ACTIVE, INACTIVE, TLS_ERROR)."
378
612
  ),
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)
613
+ search: z6.string().optional(),
614
+ sortBy: z6.enum(["lastSeenAt", "createdAt", "value"]).optional(),
615
+ sortOrder: z6.enum(["asc", "desc"]).optional(),
616
+ limit: z6.number().int().min(1).max(100).optional().default(50),
617
+ offset: z6.number().int().min(0).optional().default(0)
384
618
  });
385
619
  var listAssetsTool = {
386
620
  name: "list_assets",
@@ -401,33 +635,32 @@ var listAssetsTool = {
401
635
  };
402
636
 
403
637
  // 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(
638
+ import { z as z7 } from "zod";
639
+ var listFindingsInput = z7.object({
640
+ pentestId: z7.string().optional().describe("Filter to a single pentest."),
641
+ assetId: z7.string().optional().describe(
408
642
  "Filter to findings whose latest revision references this asset."
409
643
  ),
410
- severity: z5.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFORMATIONAL"]).optional(),
411
- status: z5.enum([
412
- "DRAFT",
413
- "IN_REVIEW",
644
+ repository: z7.string().optional().describe("Filter to a source repository such as owner/name."),
645
+ severity: z7.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFORMATIONAL"]).optional(),
646
+ status: z7.enum([
414
647
  "REPORTED",
648
+ "ACKNOWLEDGED",
415
649
  "MITIGATING",
416
650
  "OPEN_FOR_RETEST",
417
- "RESOLVED",
418
- "NOT_REPRODUCIBLE",
419
- "ACCEPTED_RISK"
651
+ "NEEDS_REVISION",
652
+ "FIXED_AND_RETESTED"
420
653
  ]).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)
654
+ search: z7.string().optional().describe("Free-text search on finding titles and human identifiers."),
655
+ sortBy: z7.enum(["severity", "createdAt"]).optional(),
656
+ sortOrder: z7.enum(["asc", "desc"]).optional(),
657
+ limit: z7.number().int().min(1).max(200).optional().default(50),
658
+ offset: z7.number().int().min(0).optional().default(0)
426
659
  });
427
660
  var listFindingsTool = {
428
661
  name: "list_findings",
429
662
  title: "List findings",
430
- description: "List security findings for the organisation. Supports filtering by pentest, severity, status, and free-text search.",
663
+ description: "List security findings for the organisation. Supports filtering by pentest, asset, repository, severity, current client-visible status, and searching finding titles or human identifiers such as ODI-12.",
431
664
  inputSchema: listFindingsInput,
432
665
  handler: (client) => async (params) => {
433
666
  const data = await client.get(
@@ -446,8 +679,8 @@ var listFindingsTool = {
446
679
  };
447
680
 
448
681
  // src/tools/list-pentests.ts
449
- import { z as z6 } from "zod";
450
- var listPentestsInput = z6.object({});
682
+ import { z as z8 } from "zod";
683
+ var listPentestsInput = z8.object({});
451
684
  var listPentestsTool = {
452
685
  name: "list_pentests",
453
686
  title: "List pentests",
@@ -463,21 +696,24 @@ var listPentestsTool = {
463
696
  };
464
697
 
465
698
  // 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."
699
+ import { z as z9 } from "zod";
700
+ var updateFindingStatusInput = z9.object({
701
+ ...findingReferenceFields,
702
+ status: z9.enum(["MITIGATING", "OPEN_FOR_RETEST", "ACKNOWLEDGED"]).describe(
703
+ "Target status. MITIGATING means work has started, OPEN_FOR_RETEST means the fix is ready for verification, and ACKNOWLEDGED means the organisation accepts the risk for now."
471
704
  )
705
+ }).refine((value) => Boolean(value.identifier || value.findingId), {
706
+ message: "Provide either identifier or findingId."
472
707
  });
473
708
  var updateFindingStatusTool = {
474
709
  name: "update_finding_status",
475
710
  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.",
711
+ 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.",
477
712
  inputSchema: updateFindingStatusInput,
478
713
  handler: (client) => async (params) => {
714
+ const findingId = await resolveFindingId(client, params);
479
715
  const data = await client.patch(
480
- `/api/findings/${encodeURIComponent(params.findingId)}/status`,
716
+ `/api/findings/${encodeURIComponent(findingId)}/status`,
481
717
  { status: params.status }
482
718
  );
483
719
  return {
@@ -488,7 +724,7 @@ var updateFindingStatusTool = {
488
724
  };
489
725
 
490
726
  // src/index.ts
491
- var PACKAGE_VERSION = "0.1.0";
727
+ var PACKAGE_VERSION = "0.2.0";
492
728
  async function main() {
493
729
  const apiKey = process.env.ODIN_API_KEY;
494
730
  if (!apiKey) {
@@ -498,7 +734,11 @@ async function main() {
498
734
  process.exit(1);
499
735
  }
500
736
  const baseUrl = process.env.ODIN_BASE_URL ?? DEFAULT_ODIN_BASE_URL;
501
- const client = new OdinClient({ apiKey, baseUrl });
737
+ const client = new OdinClient({
738
+ apiKey,
739
+ baseUrl,
740
+ vercelProtectionBypassSecret: process.env.ODIN_VERCEL_PROTECTION_BYPASS_SECRET
741
+ });
502
742
  const server = new McpServer({
503
743
  name: "@borgresearch/odin-mcp",
504
744
  version: PACKAGE_VERSION
@@ -507,6 +747,8 @@ async function main() {
507
747
  listFindingsTool,
508
748
  getFindingTool,
509
749
  updateFindingStatusTool,
750
+ linkFindingPullRequestTool,
751
+ getFindingRetestsTool,
510
752
  listPentestsTool,
511
753
  getPentestTool,
512
754
  listAssetsTool,
package/dist/index.js.map CHANGED
@@ -1 +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"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/tools/get-asset.ts","../src/formatters/markdown.ts","../src/tools/finding-reference.ts","../src/tools/get-finding.ts","../src/tools/get-finding-retests.ts","../src/tools/get-pentest.ts","../src/tools/link-finding-pull-request.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 { getFindingRetestsTool } from \"./tools/get-finding-retests.js\";\nimport { getPentestTool } from \"./tools/get-pentest.js\";\nimport { linkFindingPullRequestTool } from \"./tools/link-finding-pull-request.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.2.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({\n apiKey,\n baseUrl,\n vercelProtectionBypassSecret:\n process.env.ODIN_VERCEL_PROTECTION_BYPASS_SECRET,\n });\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 linkFindingPullRequestTool,\n getFindingRetestsTool,\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 vercelProtectionBypassSecret?: string;\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 private readonly vercelProtectionBypassSecret: string | undefined;\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 this.vercelProtectionBypassSecret =\n options.vercelProtectionBypassSecret?.trim() || undefined;\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 // Public HTTP method API — kept alongside get/patch even when unused internally\n // fallow-ignore-next-line unused-class-member\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 headers: Record<string, string> = {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n };\n if (this.vercelProtectionBypassSecret) {\n headers[\"x-vercel-protection-bypass\"] = this.vercelProtectionBypassSecret;\n }\n\n const response = await this.fetchImpl(url, {\n method,\n headers,\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 FindingStatus,\n FindingSummary,\n FindingsListResponse,\n LinkFindingPullRequestResponse,\n Pentest,\n PentestDetailResponse,\n PentestListResponse,\n Retest,\n RetestHistoryResponse,\n} from \"../types.js\";\n\nconst STATUS_LABELS: Record<FindingStatus, string> = {\n REPORTED: \"Reported\",\n ACKNOWLEDGED: \"Acknowledged\",\n MITIGATING: \"Mitigating\",\n OPEN_FOR_RETEST: \"Open for Retest\",\n NEEDS_REVISION: \"Needs Revision\",\n FIXED_AND_RETESTED: \"Fixed & Retested\",\n};\n\nconst RETEST_STATUS_LABELS: Record<Retest[\"status\"], string> = {\n QUEUED: \"Queued\",\n RUNNING: \"Running\",\n COMPLETED: \"Completed\",\n FAILED: \"Failed\",\n SKIPPED: \"Skipped\",\n};\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.identifier ?? \"—\"} | ${f.findingId} | ${escapePipes(f.title)} | ${severity} | ${formatStatus(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 \"| Identifier | Internal 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 context = data.remediationContext;\n const identifier = context?.identifier ?? finding.identifier ?? null;\n const displayIdentifier = identifier ?? finding.findingId;\n const severity = `${finding.severity.label} (CVSS ${finding.severity.cvss.toFixed(1)})`;\n const header = [\n `# ${escapePipes(displayIdentifier)} — ${escapePipes(finding.title)}`,\n \"\",\n `- **Finding:** ${displayIdentifier}`,\n `- **Internal ID:** ${finding.findingId}`,\n `- **Severity:** ${severity}`,\n `- **Status:** ${formatStatus(finding.status)}`,\n `- **Repository:** ${context?.repository ?? finding.repository ?? \"Not recorded\"}`,\n `- **Pentest:** ${finding.context?.pentestId ?? \"standalone\"}`,\n `- **Created:** ${formatDate(finding.createdAt)}`,\n `- **Updated:** ${formatDate(finding.updatedAt)}`,\n ];\n\n const content = context\n ? {\n revisionNumber: context.revision.revisionNumber,\n summary: context.revision.summary,\n description: context.revision.description,\n impact: context.revision.impact,\n details: context.revision.details,\n remediation: context.revision.remediation,\n affectedAssets: context.revision.affectedAssets,\n codeReference: context.revision.codeReference,\n }\n : revision\n ? {\n revisionNumber: revision.revisionNumber,\n summary: revision.content.summaryMd,\n description: revision.content.descriptionMd,\n impact: revision.content.businessImpactMd,\n details: revision.content.detailsMd,\n remediation: revision.content.remediationMd,\n affectedAssets: revision.content.affectedAssets ?? [],\n codeReference: null,\n }\n : null;\n\n const sections: string[] = [];\n if (!content) {\n sections.push(\"_No revision content available._\");\n } else {\n sections.push(`_Revision ${content.revisionNumber ?? \"unknown\"}_`);\n if (content.summary) sections.push(\"## Summary\", content.summary);\n if (content.description)\n sections.push(\"## Description\", content.description);\n if (content.impact) sections.push(\"## Business Impact\", content.impact);\n if (content.details) sections.push(\"## Details\", content.details);\n if (content.remediation)\n sections.push(\"## Remediation\", content.remediation);\n if (content.codeReference) {\n sections.push(\n \"## Code reference\",\n `- ${content.codeReference.file}:${content.codeReference.line}`,\n );\n if (content.codeReference.snippet) {\n sections.push(\"```\", content.codeReference.snippet, \"```\");\n }\n }\n if (content.affectedAssets?.length) {\n sections.push(\n \"## Affected Assets\",\n content.affectedAssets\n .map((asset) => `- ${asset.display} (${asset.assetId})`)\n .join(\"\\n\"),\n );\n }\n }\n\n if (context) {\n sections.push(\n \"## Remediation workflow\",\n `- **Suggested branch:** ${context.suggestedBranch ?? \"Not available\"}`,\n `- **Ready for a user-created PR:** ${yesNo(context.workflow.readyForUserCreatedPullRequest)}`,\n `- **GitHub connected:** ${yesNo(context.workflow.githubConnected)}`,\n `- **Automatic retests:** ${context.workflow.automaticRetestsReady ? \"Ready after merge\" : \"Not ready\"}`,\n );\n if (context.workflow.blockers.length) {\n sections.push(\n \"### Blockers\",\n context.workflow.blockers.map((blocker) => `- ${blocker}`).join(\"\\n\"),\n );\n }\n if (context.workflow.nextSteps.length) {\n sections.push(\n \"### Next steps\",\n context.workflow.nextSteps.map((step) => `- ${step}`).join(\"\\n\"),\n );\n }\n\n if (context.linkedPullRequests.length) {\n sections.push(\n \"## Linked GitHub pull requests\",\n context.linkedPullRequests\n .map(\n (pullRequest) =>\n `- [#${pullRequest.number}](${pullRequest.url}) ${escapePipes(pullRequest.title)} — ${pullRequest.state}, linked via ${pullRequest.linkedVia}`,\n )\n .join(\"\\n\"),\n );\n } else {\n sections.push(\"## Linked GitHub pull requests\", \"_None linked._\");\n }\n\n sections.push(\"## Latest retest\");\n if (context.latestRetest.state === \"unavailable\") {\n sections.push(`_${context.latestRetest.reason}_`);\n } else if (context.latestRetest.state === \"none\") {\n sections.push(\"_No retest history is available yet._\");\n } else {\n sections.push(formatRetestSummary(context.latestRetest.retest));\n }\n }\n\n return [...header, \"\", ...sections].join(\"\\n\");\n}\n\nexport function formatFindingStatusUpdate(finding: FindingSummary): string {\n const identifier = finding.identifier ?? finding.findingId;\n return [\n `Updated finding **${identifier}** (internal ID ${finding.findingId}) status to **${formatStatus(finding.status)}**.`,\n `Title: ${finding.title}`,\n ].join(\"\\n\");\n}\n\nexport function formatFindingPullRequestLink(\n data: LinkFindingPullRequestResponse,\n): string {\n const identifier = data.finding.identifier ?? data.finding.findingId;\n const pullRequest = data.pullRequest;\n const readiness = data.workflow.automaticRetestsReady\n ? \"Automatic retesting is ready after this PR merges.\"\n : \"Automatic retesting is not currently ready; review the blockers below.\";\n const blockers = data.workflow.blockers.length\n ? `\\n${data.workflow.blockers.map((blocker) => `- ${blocker}`).join(\"\\n\")}`\n : \"\";\n\n return [\n `Linked existing GitHub pull request [#${pullRequest.number}](${pullRequest.url}) to **${identifier}**.`,\n `Repository: ${pullRequest.repo}; state: ${pullRequest.state}; finding status: ${formatStatus(data.finding.status)}.`,\n \"Odin did not create, modify, merge, or push this pull request.\",\n readiness,\n blockers ? `Workflow blockers:${blockers}` : \"\",\n ]\n .filter(Boolean)\n .join(\"\\n\");\n}\n\nexport function formatFindingRetests(data: RetestHistoryResponse): string {\n if (data.state === \"none\" || data.retests.length === 0) {\n return \"No retest history is available for this finding.\";\n }\n\n return [\n `Retest history (${data.retests.length}):`,\n \"\",\n \"| Retest | Status | Trigger | Verdict | Created | Completed |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...data.retests.map(\n (retest) =>\n `| ${retest.retestId} | ${RETEST_STATUS_LABELS[retest.status]} | ${retest.trigger} | ${retest.combinedVerdict ?? \"—\"} | ${formatDate(retest.createdAt)} | ${formatDate(retest.completedAt ?? 0)} |`,\n ),\n \"\",\n \"Queued and running entries are still in progress; completed, skipped, and failed entries are final for that run.\",\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\nexport function formatFindingsForAsset(findings: FindingSummary[]): string {\n if (findings.length === 0) return \"_No findings reference this asset._\";\n return [\n `## Findings (${findings.length})`,\n \"\",\n \"| Identifier | Internal ID | Title | Severity | Status | Created |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...findings.map(formatFindingRow),\n ].join(\"\\n\");\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 return [...header, \"\", formatFindingsForAsset(findings)].join(\"\\n\");\n}\n\nfunction formatFindingRow(finding: FindingSummary): string {\n const severity = `${finding.severity.label} (CVSS ${finding.severity.cvss.toFixed(1)})`;\n return `| ${finding.identifier ?? \"—\"} | ${finding.findingId} | ${escapePipes(finding.title)} | ${severity} | ${formatStatus(finding.status)} | ${formatDate(finding.createdAt)} |`;\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\nfunction formatAssetRow(a: Asset): string {\n return `| ${a.assetId} | ${a.type} | ${escapePipes(a.value)} | ${a.source} | ${a.status} | ${formatDate(a.lastSeenAt)} |`;\n}\n\nfunction formatRetestSummary(retest: {\n status: Retest[\"status\"];\n combinedVerdict: Retest[\"combinedVerdict\"];\n statusAction: Retest[\"statusAction\"];\n trigger: Retest[\"trigger\"];\n createdAt: number;\n completedAt: number | null;\n}): string {\n return [\n `- **Status:** ${RETEST_STATUS_LABELS[retest.status]}`,\n `- **Trigger:** ${retest.trigger}`,\n `- **Verdict:** ${retest.combinedVerdict ?? \"Not available yet\"}`,\n `- **Finding action:** ${retest.statusAction ?? \"None\"}`,\n `- **Created:** ${formatDate(retest.createdAt)}`,\n `- **Completed:** ${formatDate(retest.completedAt ?? 0)}`,\n ].join(\"\\n\");\n}\n\nfunction formatStatus(status: FindingStatus): string {\n return STATUS_LABELS[status] ?? status;\n}\n\nfunction yesNo(value: boolean): string {\n return value ? \"Yes\" : \"No\";\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 type { FindingDetailResponse } from \"../types.js\";\n\nexport const findingReferenceFields = {\n identifier: z\n .string()\n .trim()\n .min(1)\n .optional()\n .describe(\n 'Human-readable finding identifier such as \"ODI-12\". Preferred input.',\n ),\n findingId: z\n .string()\n .trim()\n .min(1)\n .optional()\n .describe(\n \"Raw internal finding ID, useful when chaining from another tool.\",\n ),\n};\n\nexport const findingReferenceInput = z\n .object(findingReferenceFields)\n .refine((value) => Boolean(value.identifier || value.findingId), {\n message: \"Provide either identifier or findingId.\",\n });\n\nexport type FindingReferenceInput = z.infer<typeof findingReferenceInput>;\n\n/** Prefer the human reference when both values are supplied. */\nexport function findingReferenceValue(\n reference: FindingReferenceInput,\n): string {\n const value = reference.identifier ?? reference.findingId;\n if (!value) {\n throw new Error(\"A finding identifier or findingId is required.\");\n }\n return value;\n}\n\nexport function findingReferencePath(\n reference: FindingReferenceInput,\n suffix = \"\",\n): string {\n return `/api/findings/${encodeURIComponent(findingReferenceValue(reference))}${suffix}`;\n}\n\n/**\n * Resolves only when a caller supplied an identifier. A supplied internal ID\n * is already suitable for mutation chaining and avoids an unnecessary read.\n */\nexport async function resolveFindingId(\n client: OdinClient,\n reference: FindingReferenceInput,\n): Promise<string> {\n if (reference.findingId) return reference.findingId;\n\n const data = await client.get<FindingDetailResponse>(\n findingReferencePath(reference),\n );\n return data.remediationContext?.findingId ?? data.finding.findingId;\n}\n","import type { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatFindingDetail } from \"../formatters/markdown.js\";\nimport type { FindingDetailResponse } from \"../types.js\";\nimport {\n findingReferenceInput,\n findingReferencePath,\n} from \"./finding-reference.js\";\n\nexport const getFindingInput = findingReferenceInput;\n\nexport const getFindingTool = {\n name: \"get_finding\",\n title: \"Get finding remediation context\",\n description:\n \"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.\",\n inputSchema: getFindingInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof getFindingInput>) => {\n const data = await client.get<FindingDetailResponse>(\n findingReferencePath(params),\n );\n return {\n markdown: formatFindingDetail(data),\n structured: {\n finding: data.remediationContext ?? data.finding,\n revision: data.remediationContext?.revision ?? data.revision,\n remediationContext: data.remediationContext ?? null,\n },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { OdinApiError } from \"../client.js\";\nimport { formatFindingRetests } from \"../formatters/markdown.js\";\nimport type { RetestHistoryResponse } from \"../types.js\";\nimport {\n findingReferenceFields,\n findingReferencePath,\n} from \"./finding-reference.js\";\n\nexport const getFindingRetestsInput = z\n .object(findingReferenceFields)\n .refine((value) => Boolean(value.identifier || value.findingId), {\n message: \"Provide either identifier or findingId.\",\n });\n\nexport const getFindingRetestsTool = {\n name: \"get_finding_retests\",\n title: \"Get finding retest history\",\n description:\n \"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.\",\n inputSchema: getFindingRetestsInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof getFindingRetestsInput>) => {\n try {\n const data = await client.get<RetestHistoryResponse>(\n findingReferencePath(params, \"/retests\"),\n );\n return {\n markdown: formatFindingRetests(data),\n structured: {\n state: data.state,\n retests: data.retests,\n },\n };\n } catch (error) {\n if (\n error instanceof OdinApiError &&\n error.status === 404 &&\n isRetestHistoryUnavailable(error.body)\n ) {\n const reason =\n \"Retest history is gated or unavailable for this organisation.\";\n return {\n markdown: reason,\n structured: { state: \"unavailable\", reason, retests: [] },\n };\n }\n throw error;\n }\n },\n};\n\nfunction isRetestHistoryUnavailable(body: unknown): boolean {\n return (\n typeof body === \"object\" &&\n body !== null &&\n \"code\" in body &&\n (body as { code?: unknown }).code === \"RETEST_HISTORY_UNAVAILABLE\"\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 { formatFindingPullRequestLink } from \"../formatters/markdown.js\";\nimport type { LinkFindingPullRequestResponse } from \"../types.js\";\nimport {\n findingReferenceFields,\n findingReferencePath,\n} from \"./finding-reference.js\";\n\nexport const linkFindingPullRequestInput = z\n .object({\n ...findingReferenceFields,\n url: z\n .string()\n .url()\n .describe(\"Existing GitHub pull request URL to link to the finding.\"),\n })\n .refine((value) => Boolean(value.identifier || value.findingId), {\n message: \"Provide either identifier or findingId.\",\n });\n\nexport const linkFindingPullRequestTool = {\n name: \"link_finding_pull_request\",\n title: \"Link an existing GitHub pull request\",\n description:\n \"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.\",\n inputSchema: linkFindingPullRequestInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof linkFindingPullRequestInput>) => {\n const data = await client.post<LinkFindingPullRequestResponse>(\n findingReferencePath(params, \"/link-pr\"),\n { url: params.url },\n );\n return {\n markdown: formatFindingPullRequestLink(data),\n structured: {\n finding: data.finding,\n pullRequest: data.pullRequest,\n linkSource: data.linkSource,\n workflow: data.workflow,\n },\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\"]).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 repository: z\n .string()\n .optional()\n .describe(\"Filter to a source repository such as owner/name.\"),\n severity: z\n .enum([\"CRITICAL\", \"HIGH\", \"MEDIUM\", \"LOW\", \"INFORMATIONAL\"])\n .optional(),\n status: z\n .enum([\n \"REPORTED\",\n \"ACKNOWLEDGED\",\n \"MITIGATING\",\n \"OPEN_FOR_RETEST\",\n \"NEEDS_REVISION\",\n \"FIXED_AND_RETESTED\",\n ])\n .optional(),\n search: z\n .string()\n .optional()\n .describe(\"Free-text search on finding titles and human identifiers.\"),\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, asset, repository, severity, current client-visible status, and searching finding titles or human identifiers such as ODI-12.\",\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\";\nimport {\n findingReferenceFields,\n resolveFindingId,\n} from \"./finding-reference.js\";\n\nexport const updateFindingStatusInput = z\n .object({\n ...findingReferenceFields,\n status: z\n .enum([\"MITIGATING\", \"OPEN_FOR_RETEST\", \"ACKNOWLEDGED\"])\n .describe(\n \"Target status. MITIGATING means work has started, OPEN_FOR_RETEST means the fix is ready for verification, and ACKNOWLEDGED means the organisation accepts the risk for now.\",\n ),\n })\n .refine((value) => Boolean(value.identifier || value.findingId), {\n message: \"Provide either identifier or findingId.\",\n });\n\nexport const updateFindingStatusTool = {\n name: \"update_finding_status\",\n title: \"Update finding status\",\n description:\n \"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.\",\n inputSchema: updateFindingStatusInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof updateFindingStatusInput>) => {\n const findingId = await resolveFindingId(client, params);\n const data = await client.patch<FindingStatusUpdateResponse>(\n `/api/findings/${encodeURIComponent(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","z","z","z"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@borgresearch/odin-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for the Odin security API",
5
5
  "keywords": [
6
6
  "mcp",
@@ -42,7 +42,7 @@
42
42
  "registry": "https://registry.npmjs.org/"
43
43
  },
44
44
  "engines": {
45
- "node": ">=22.0.0"
45
+ "node": ">=24.0.0"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsup",
@@ -62,7 +62,7 @@
62
62
  "devDependencies": {
63
63
  "@biomejs/biome": "2.3.14",
64
64
  "@modelcontextprotocol/inspector": "^0.21.2",
65
- "@types/node": "^22.19.8",
65
+ "@types/node": "^24.10.1",
66
66
  "tsup": "^8.3.5",
67
67
  "typescript": "^5.9.3",
68
68
  "vitest": "^4.1.0"