Sign In

@pentatrail/mcp-server

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@pentatrail/mcp-server - npm Package Compare versions

Comparing version
0.8.0
to
0.9.0
+9
-6
dist/index.d.ts
/**
* @pentatrail/mcp-server — PentaTrail MCP Server
* @pentatrail/mcp-server — 乗り換えの案内だけを出して終了する版。
*
* Thin HTTP wrapper over the PentaTrail Customer API.
* Connects via stdio transport for Claude Code / Claude Desktop.
* ⚠ このパッケージは道具を持たない。MCP サーバは遠隔(Edge Function
* `supabase/functions/pentatrail-mcp/`)へ移り、道具の定義と HTTP の client も
* そちらへ移送してある。`packages/` は npm 配布専用で内部コードを置かない
* という掟(CLAUDE.md)の帰結で、ここに残す実装は無い。
*
* Environment variables:
* PENTATRAIL_API_KEY — API key (ptk_...)
* PENTATRAIL_API_URL — Base URL of pentatrail-customer-api Edge Function
* ∴ 入口がやることは 1 つだけ: **案内を stderr へ出して 0 以外で終了する。**
* 標準出力は MCP の伝送路なので、案内は必ず stderr へ出す(stdout へ出すと
* 客先の MCP client が JSON-RPC の壊れた行として読む)。終了コードを 0 に
* しないのは、古い設定のまま繋いだ client に「起動した」と誤認させないため。
*/
export {};
+28
-44
"use strict";
/**
* @pentatrail/mcp-server — PentaTrail MCP Server
* @pentatrail/mcp-server — 乗り換えの案内だけを出して終了する版。
*
* Thin HTTP wrapper over the PentaTrail Customer API.
* Connects via stdio transport for Claude Code / Claude Desktop.
* ⚠ このパッケージは道具を持たない。MCP サーバは遠隔(Edge Function
* `supabase/functions/pentatrail-mcp/`)へ移り、道具の定義と HTTP の client も
* そちらへ移送してある。`packages/` は npm 配布専用で内部コードを置かない
* という掟(CLAUDE.md)の帰結で、ここに残す実装は無い。
*
* Environment variables:
* PENTATRAIL_API_KEY — API key (ptk_...)
* PENTATRAIL_API_URL — Base URL of pentatrail-customer-api Edge Function
* ∴ 入口がやることは 1 つだけ: **案内を stderr へ出して 0 以外で終了する。**
* 標準出力は MCP の伝送路なので、案内は必ず stderr へ出す(stdout へ出すと
* 客先の MCP client が JSON-RPC の壊れた行として読む)。終了コードを 0 に
* しないのは、古い設定のまま繋いだ client に「起動した」と誤認させないため。
*/
Object.defineProperty(exports, "__esModule", { value: true });
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const client_js_1 = require("./client.js");
const public_js_1 = require("./tools/public.js");
const version_js_1 = require("./version.js");
const API_KEY = process.env.PENTATRAIL_API_KEY;
const API_URL = process.env.PENTATRAIL_API_URL;
if (!API_KEY || !API_URL) {
console.error('Missing required environment variables: PENTATRAIL_API_KEY, PENTATRAIL_API_URL');
process.exit(1);
}
const client = new client_js_1.PentaTrailClient(API_URL, API_KEY);
const server = new mcp_js_1.McpServer({
name: 'pentatrail',
version: version_js_1.VERSION,
});
// Register the public CTEM surface. It is READ-ONLY: every tool here needs
// only `ctem:read` (#1310). The write tool `ctem_exclude_bulk` used to be
// registered here too; it now lives only in the internal `tools/ctem.ts`,
// because publishing a bulk-exclude without publishing its inverse would put
// a door with no way back on the public surface. The other tool modules under
// src/tools/ are likewise internal and deliberately not registered here.
//
// ⚠ The set of tools this call registers is asserted by
// `src/__tests__/tool-registry.test.ts` and gated in CI by
// `scripts/audit/check-mcp-public-exposure.ts` (every path served here must be
// declared `exposure: "public"` in g2-2-route-spec.json).
(0, public_js_1.registerPublicCtemTools)(server, client);
// Start stdio transport
const transport = new stdio_js_1.StdioServerTransport();
const keepAlive = setInterval(() => { }, 60_000);
async function main() {
await server.connect(transport);
}
main().catch((err) => {
console.error('Failed to start MCP server:', err);
clearInterval(keepAlive);
process.exit(1);
});
// ⚠ 日本語だけにしない。npm から取る相手は日本語話者に限らず、案内が読めなければ
// 「起動しないパッケージ」にしか見えない(乗り換え先へ辿り着けない)。
const NOTICE = [
'PentaTrail MCP は遠隔サーバへ移りました。',
'このパッケージ(stdio + API キー)はもう動きません。',
'',
'新しい繋ぎ先: https://api.pentatrail.co/mcp',
'認可は初回接続時にブラウザで承認します。API キーの設定は不要です。',
'',
'PentaTrail MCP has moved to a remote server.',
'This package (stdio + API key) no longer works.',
'',
'New endpoint: https://api.pentatrail.co/mcp',
'Authorization happens in your browser on first connect. No API key needed.',
].join('\n');
console.error(`@pentatrail/mcp-server ${version_js_1.VERSION}`);
console.error('');
console.error(NOTICE);
process.exit(1);

@@ -1,1 +0,1 @@

export declare const VERSION = "0.8.0";
export declare const VERSION = "0.9.0";
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VERSION = void 0;
// The single place the server version is written. `index.ts` reports this to clients
// at startup, and `__tests__/version-consistency.test.ts` asserts it matches package.json,
// package-lock.json and server.json. Keep all of them in step when releasing —
// The single place the server version is written. `index.ts` reports this on the
// hand-off notice, and `__tests__/version-consistency.test.ts` asserts it matches
// package.json, package-lock.json and server.json. Keep all of them in step —
// see RELEASING.md.
exports.VERSION = '0.8.0';
exports.VERSION = '0.9.0';
{
"name": "@pentatrail/mcp-server",
"version": "0.8.0",
"version": "0.9.0",
"mcpName": "co.pentatrail/ctem",
"description": "PentaTrail MCP Server — access your CTEM / ASM (attack surface management) data from an AI agent via the Model Context Protocol",
"description": "Retired — PentaTrail MCP moved to a remote server at https://api.pentatrail.co/mcp. This package only prints the hand-off notice.",
"keywords": [

@@ -35,6 +35,2 @@ "mcp",

},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0",
"zod": "^3.24.0"
},
"devDependencies": {

@@ -55,9 +51,3 @@ "@types/node": "^22.0.0",

"dist/version.js",
"dist/version.d.ts",
"dist/client.js",
"dist/client.d.ts",
"dist/tools/public.js",
"dist/tools/public.d.ts",
"dist/tools/finding-count-copy.js",
"dist/tools/finding-count-copy.d.ts"
"dist/version.d.ts"
],

@@ -64,0 +54,0 @@ "license": "UNLICENSED",

+104
-17
# @pentatrail/mcp-server
PentaTrail MCP Server for accessing CTEM (Continuous Threat Exposure Management) data via [Model Context Protocol](https://modelcontextprotocol.io/).
> ## ⚠ Retired — the server moved to a remote endpoint
>
> **PentaTrail MCP は遠隔サーバへ移りました。このパッケージ(stdio + API キー)はもう動きません。**
>
> **新しい繋ぎ先: `https://api.pentatrail.co/mcp`**
> **認可は初回接続時にブラウザで承認します。API キーの設定は不要です。**
>
> The PentaTrail MCP server is now hosted by us and reached over Streamable HTTP at the
> URL above. This npm package no longer contains a server: installing it and running it
> prints this notice and exits. Point your MCP client at the remote URL instead — it
> opens a browser on first connect so you can approve access, and there is no API key to
> create, paste, or store anywhere.
>
> API keys (`ptk_...`) are no longer accepted, so a client still configured the old way
> fails to authenticate rather than falling back to anything. Nothing you configure by
> hand carries over; set the server up again from the remote URL.
>
> The endpoint above is the same for every MCP client — point your client at it and approve
> the request in the browser when prompted.
>
> ⚠ Per-client setup instructions are not published yet, and the admin console does not link
> to them. Both land with the customer-facing documentation rewrite that ships in the same
> release as this hand-off.
This package is a thin stdio wrapper over the [PentaTrail Customer API](https://pentatrail.co). It works with Claude Code, Claude Desktop, and any MCP-compatible client.
PentaTrail MCP gives an AI agent access to your CTEM (Continuous Threat Exposure
Management) data via [Model Context Protocol](https://modelcontextprotocol.io/). The tool
surface it serves is **read-only**. The 16 read tools carried over unchanged; the remote
adds one more — `ctem_list_my_contracts`, for picking which contract a call reads when you
belong to several. What else changed is how you connect.
The published server exposes the public CTEM tools registered by
`registerPublicCtemTools`, and that surface is **read-only**. Internal
MCP-only tools are not registered.
## The retired configuration
## Setup
⚠ **Everything below this line describes the setup that no longer works.** It is kept so
that anyone who finds an old configuration on their machine can recognise it and knows
what to replace. Do not follow it as instructions.
### Run
The simplest way to use the server is with `npx`:
```bash

@@ -21,6 +45,59 @@ npx @pentatrail/mcp-server

This is the command your MCP client runs for you. Running it by hand exits immediately,
because the environment variables below are not set. Configure your client instead —
the command belongs in the client's config, not in your terminal.
⚠ On this version it exits immediately no matter what you set: it prints the hand-off
notice above and returns a non-zero status, so an MCP client configured this way reports
a failed server rather than a working one.
### Configure
Add to your MCP client config (e.g. `~/.claude/settings.json` or `claude_desktop_config.json`):
The two clients read their MCP configuration from different places. Neither of the
configurations below works any more; find yours and replace it with the remote URL.
#### Claude Code
```bash
claude mcp add pentatrail -s user -e PENTATRAIL_API_KEY='${PENTATRAIL_API_KEY}' -e PENTATRAIL_API_URL=https://api.pentatrail.co -- npx @pentatrail/mcp-server
```
⚠ Do not put the API key itself in this command. `${PENTATRAIL_API_KEY}` is a reference,
not a value: no API key is written to the config file (`~/.claude.json`), and Claude Code
reads it from the environment when it starts the server. Keep the quotes — without them
your shell expands the reference first and the value is written into the file. This
command uses bash / zsh syntax. If it does not work on Windows, run
`claude mcp get pentatrail` and check whether the environment line remains
PENTATRAIL_API_KEY=${PENTATRAIL_API_KEY}. Set `PENTATRAIL_API_KEY` in the environment
that starts your MCP client, and supply the value to that environment from an OS
credential store (Keychain on macOS, Credential Manager on Windows, or a secrets
manager on Linux / WSL). Shell profile settings may not apply to a client launched from
the desktop.
`-s user` registers the server for your user account, so it is available in every
project. Without it the server is registered only for the directory you ran the command
in, so it will not appear when you start Claude Code somewhere else.
Confirm the server registered:
```bash
claude mcp get pentatrail
```
The reference is registered for your user account when the output says "User config" and
the environment line remains PENTATRAIL_API_KEY=${PENTATRAIL_API_KEY}. If it shows a
value shaped like ptk_..., the API key is stored in plain text in the configuration
file. Revoke that API key in the admin console and register it again with the quotes.
claude mcp get displays saved values on screen, so do not run it while sharing or
recording your screen. "✔ Connected" does not prove that the API key is valid.
#### Claude Desktop
Add this block to `claude_desktop_config.json`:
⚠ The Claude Desktop configuration file keeps the value you write, so treat the API key
as stored there in plain text. Avoid it on shared machines, configure API IP Allowlist
under User Management, and revoke the API key from the admin console when you no longer
need it.
```json

@@ -43,9 +120,17 @@ {

| Variable | Required | Description |
|----------|----------|-------------|
| `PENTATRAIL_API_KEY` | Yes | Your API key (`ptk_...`). Generate from PentaTrail Dashboard > Settings. |
| `PENTATRAIL_API_URL` | Yes | `https://api.pentatrail.co` (production) |
⚠ Neither variable is read any more, and the API keys they carried are no longer accepted.
Remove them from your client configuration and from wherever you stored the key.
## Available Tools
| Variable | Status | Description |
|----------|--------|-------------|
| `PENTATRAIL_API_KEY` | No longer used | Was your API key (`ptk_...`). Authorization now happens in the browser on first connect. |
| `PENTATRAIL_API_URL` | No longer used | Was `https://api.pentatrail.co`. The MCP endpoint is `https://api.pentatrail.co/mcp`. |
## Tools
These are served by the remote server at `https://api.pentatrail.co/mcp`. The surface is
read-only. The 16 tools below carried over unchanged. ⚠ The remote adds one that is not
listed below — `ctem_list_my_contracts` — which returns the contracts you belong to; other
tools name it when a call is ambiguous because you belong to more than one.
### CTEM Read

@@ -84,8 +169,10 @@

- Node.js >= 20
- PentaTrail account with an active contract
- A PentaTrail API key
- An MCP client that can connect to a remote server over Streamable HTTP
- A browser, to approve access on first connect
- A PentaTrail account with an active contract
⚠ No Node.js runtime and no API key are needed any more — nothing runs on your machine.
## License
UNLICENSED - Proprietary software. All rights reserved.
/**
* PentaTrail REST API HTTP Client
*/
export interface ApiResponse<T = unknown> {
success: boolean;
data?: T;
pagination?: {
page: number;
limit: number;
total_count: number;
has_next: boolean;
};
error?: {
code: string;
message: string;
retriable: boolean;
};
timestamp: string;
correlation_id: string;
}
export declare class PentaTrailClient {
private apiUrl;
private apiKey;
constructor(apiUrl: string, apiKey: string);
get<T = unknown>(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<ApiResponse<T>>;
post<T = unknown>(path: string, data: Record<string, unknown>): Promise<ApiResponse<T>>;
put<T = unknown>(path: string, data: Record<string, unknown>): Promise<ApiResponse<T>>;
delete<T = unknown>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>>;
}
"use strict";
/**
* PentaTrail REST API HTTP Client
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PentaTrailClient = void 0;
class PentaTrailClient {
apiUrl;
apiKey;
constructor(apiUrl, apiKey) {
this.apiUrl = apiUrl.replace(/\/$/, '');
this.apiKey = apiKey;
}
async get(path, params) {
const url = new URL(`${this.apiUrl}${path}`);
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
url.searchParams.set(key, String(value));
}
}
}
const res = await fetch(url.toString(), {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
});
const body = await res.json();
return body;
}
async post(path, data) {
const res = await fetch(`${this.apiUrl}${path}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
return await res.json();
}
async put(path, data) {
const res = await fetch(`${this.apiUrl}${path}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
return await res.json();
}
async delete(path, params) {
const url = new URL(`${this.apiUrl}${path}`);
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value !== undefined)
url.searchParams.set(key, value);
}
}
const res = await fetch(url.toString(), {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
},
});
return await res.json();
}
}
exports.PentaTrailClient = PentaTrailClient;
/** 一覧の道具の説明のうち、件数の定義を述べる部分。 */
export declare const FINDINGS_DEFAULT_COPY: string;
/** 資産件数の道具の説明のうち、3 項目の関係を述べる部分。 */
export declare const ASSET_COUNTS_FINDING_COPY: string;
/** TDL 内訳の道具の説明のうち、情報レベルと除外ホストを述べる部分。 */
export declare const TDL_COUNTS_FINDING_COPY: string;
/**
* `ctem_summarize_findings` の `finding_count` は**別物**である、という注意。
*
* ⚠ 同じ出荷面に `finding_count` という名の欄が 2 つある。`ctem_get_asset_counts` の方は
* 統一定義(未対処 ∧ 情報レベル以外)だが、`/v1/ctem/findings/summary` の方は
* vuln_id ごとの素の `count(*)` で、既定では状態も情報レベルも絞らない。
* 「唯一の定義」と言い切る以上、こちらに区別を書かないと読み手が取り違える。
*/
export declare const SUMMARIZE_FINDING_COUNT_CAVEAT: string;
/**
* `severity` パラメータの説明(生スキャナの値であり include_info と独立)。
*
* ⚠ **`include_info` を持つ道具にだけ使う。** 件数の母集団から情報レベルを外す、という
* 後半の一節は「情報レベルを外す面」でしか成り立たない。切り替えを持たない
* `ctem_summarize_findings` には `SUMMARIZE_SEVERITY_PARAM_COPY` を使う。
*/
export declare const SEVERITY_PARAM_COPY: string;
/**
* `ctem_summarize_findings` の `severity` パラメータの説明。
*
* ⚠ こちらの道具は情報レベルを**除外しない**(`SUMMARIZE_FINDING_COUNT_CAVEAT` の通り)。
* `SEVERITY_PARAM_COPY` を流用すると、同じ説明文の中で「除外しない」と
* 「TDL が info なら除外される」が並び、しかも**この道具に無い `include_info`**を
* 顧客に案内することになる。∴ 生の値で絞るだけ、と短く言う。
*/
export declare const SUMMARIZE_SEVERITY_PARAM_COPY: string;
/** `include_info` パラメータの説明。 */
export declare const INCLUDE_INFO_PARAM_COPY: string;
/** `status` パラメータの説明(土台の選択であって絞り込みではない)。 */
export declare const STATUS_PARAM_COPY = "Which status set to look at (a base selection, not a filter): open, closed, accepted, all. Defaults to open";
"use strict";
// 「顧客に見える脆弱性の件数」の定義を説明する文を **1 か所**に置く (#1386)。
//
// ⚠⚠ なぜ共有するか: この定義は出荷面 (`tools/public.ts`) と内部面 (`tools/ctem.ts`) の
// **両方**に同名の道具として存在する。#1386 の修正を最初に内部面だけへ入れてしまい、
// 顧客の道具は旧定義のまま出荷される状態になった(`src/index.ts` が登録するのは
// 出荷面だけ、`package.json` の `files` も `dist/tools/public.js` だけ)。
// 文字列を各ファイルに書くと、片方だけ直る形が**また**起きる。∴ ここから参照する。
//
// ⚠ 文言を変えるときは `src/__tests__/public-finding-count-definition.test.ts` が
// 否定の構造まで見ているので、意味を反転させると赤くなる。
//
// spec: docs/superpowers/specs/2026-08-10-finding-count-single-definition-design.md §3-⑥
Object.defineProperty(exports, "__esModule", { value: true });
exports.STATUS_PARAM_COPY = exports.INCLUDE_INFO_PARAM_COPY = exports.SUMMARIZE_SEVERITY_PARAM_COPY = exports.SEVERITY_PARAM_COPY = exports.SUMMARIZE_FINDING_COUNT_CAVEAT = exports.TDL_COUNTS_FINDING_COPY = exports.ASSET_COUNTS_FINDING_COPY = exports.FINDINGS_DEFAULT_COPY = void 0;
/** 一覧の道具の説明のうち、件数の定義を述べる部分。 */
exports.FINDINGS_DEFAULT_COPY = 'IMPORTANT: by default only OPEN findings are returned, and informational (info) findings are excluded. ' +
'Pass include_info=true to add informational findings, or status=all to see every status; ' +
'note that priority_key=info returns nothing unless include_info=true, because a filter never adds rows to the set. ' +
'Whether a finding counts as informational is decided by the product TDL (priority_key) alone — ' +
'the raw scanner severity field is independent, so severity=info still returns findings whose TDL is not info.';
/** 資産件数の道具の説明のうち、3 項目の関係を述べる部分。 */
exports.ASSET_COUNTS_FINDING_COPY = 'Findings come back as three separate numbers: ' +
'finding_count = open and NOT informational (this is the single definition of "vulnerability count" ' +
'and matches what the dashboard shows), finding_info_count = open and informational, ' +
'finding_accepted_count = accepted and not informational. ' +
'The three are NOT a partition of the open total but a breakdown of why rows were left out of it, ' +
'so do not add them together: a finding that is both accepted and informational appears in none of them. ' +
'Count endpoints have no include_info-style switch.';
/** TDL 内訳の道具の説明のうち、情報レベルと除外ホストを述べる部分。 */
exports.TDL_COUNTS_FINDING_COPY = 'Only findings on active hosts, and hosts the customer excluded are not counted. ' +
'Informational (info) findings are NOT part of these counts — the buckets sum to the same total that ' +
'ctem_get_asset_counts reports as finding_count; use finding_info_count there when you need the informational number.';
/**
* `ctem_summarize_findings` の `finding_count` は**別物**である、という注意。
*
* ⚠ 同じ出荷面に `finding_count` という名の欄が 2 つある。`ctem_get_asset_counts` の方は
* 統一定義(未対処 ∧ 情報レベル以外)だが、`/v1/ctem/findings/summary` の方は
* vuln_id ごとの素の `count(*)` で、既定では状態も情報レベルも絞らない。
* 「唯一の定義」と言い切る以上、こちらに区別を書かないと読み手が取り違える。
*/
exports.SUMMARIZE_FINDING_COUNT_CAVEAT = 'NOTE: the finding_count field here is a per-vuln_id row count and is NOT the unified vulnerability ' +
'count — unlike ctem_get_asset_counts.finding_count it does not exclude informational findings and, ' +
'unless you pass status, it does not restrict to open findings. Use ctem_get_asset_counts for the ' +
'single definition.';
/**
* `severity` パラメータの説明(生スキャナの値であり include_info と独立)。
*
* ⚠ **`include_info` を持つ道具にだけ使う。** 件数の母集団から情報レベルを外す、という
* 後半の一節は「情報レベルを外す面」でしか成り立たない。切り替えを持たない
* `ctem_summarize_findings` には `SUMMARIZE_SEVERITY_PARAM_COPY` を使う。
*/
exports.SEVERITY_PARAM_COPY = 'Filter by raw scanner severity: critical, high, medium, low, info. This field is INDEPENDENT of ' +
'include_info: whether a finding counts as informational is decided by the product TDL (priority_key) ' +
'alone, so severity=info still returns findings whose TDL is not info, and a finding whose TDL is info ' +
'is excluded whatever its severity.';
/**
* `ctem_summarize_findings` の `severity` パラメータの説明。
*
* ⚠ こちらの道具は情報レベルを**除外しない**(`SUMMARIZE_FINDING_COUNT_CAVEAT` の通り)。
* `SEVERITY_PARAM_COPY` を流用すると、同じ説明文の中で「除外しない」と
* 「TDL が info なら除外される」が並び、しかも**この道具に無い `include_info`**を
* 顧客に案内することになる。∴ 生の値で絞るだけ、と短く言う。
*/
exports.SUMMARIZE_SEVERITY_PARAM_COPY = 'Filter by raw scanner severity: critical, high, medium, low, info. This is the raw scanner value, ' +
'not the product TDL (priority_key), and this endpoint does not exclude informational findings.';
/** `include_info` パラメータの説明。 */
exports.INCLUDE_INFO_PARAM_COPY = 'Add informational (non-TDL) findings to the set (default: false). This is an include operation, not a ' +
'filter: informational findings are never part of a vulnerability count unless you ask for them. ' +
'Note that priority_key=info returns rows only together with include_info=true.';
/** `status` パラメータの説明(土台の選択であって絞り込みではない)。 */
exports.STATUS_PARAM_COPY = 'Which status set to look at (a base selection, not a filter): open, closed, accepted, all. Defaults to open';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { PentaTrailClient } from '../client.js';
export declare function registerPublicCtemTools(server: McpServer, client: PentaTrailClient): void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerPublicCtemTools = registerPublicCtemTools;
const zod_1 = require("zod");
const finding_count_copy_js_1 = require("./finding-count-copy.js");
function registerPublicCtemTools(server, client) {
server.tool('ctem_list_domains', '[READ] List the monitored domains for the authenticated contract. Start here: by default it returns active domains only, and every domain in that default result can be passed straight to the other tools as domain_id. Setting include_archived drops the status filter entirely, so the result then also contains non-active domains (archived, and legacy paused) whose ids the other tools reject with PT_API_NOT_FOUND.', {
include_archived: zod_1.z.boolean().optional().describe('Also return non-active domains — archived and legacy paused (default: false). Only active ids are accepted as domain_id elsewhere.'),
}, async (params) => {
const res = await client.get('/v1/ctem/domains', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
server.tool('ctem_list_hosts', '[READ] List hosts for a domain with enrichment (port/tech/finding counts). Excluded hosts are filtered out by default.', {
domain_id: zod_1.z.string().uuid().describe('Origin domain ID (required)'),
page: zod_1.z.number().int().min(1).optional().describe('Page number (default: 1)'),
limit: zod_1.z.number().int().min(1).max(100).optional().describe('Items per page (default: 50, max: 100)'),
host_status: zod_1.z.string().optional().describe('Filter by host status: discovered, active, dns_only, inactive, third_party'),
search: zod_1.z.string().optional().describe('Search by FQDN'),
source_type: zod_1.z.string().optional().describe('Filter by scan source type'),
show_excluded: zod_1.z.boolean().optional().describe('Include excluded hosts in result (default: false)'),
order_by: zod_1.z.enum(['fqdn', 'first_seen_at', 'last_seen_at', 'host_status']).optional().describe('Sort field (default: fqdn)'),
ascending: zod_1.z.boolean().optional().describe('Sort ascending (default: true)'),
}, async (params) => {
const res = await client.get('/v1/ctem/hosts', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
server.tool('ctem_list_findings', `[READ] List vulnerabilities/findings for a domain. ${finding_count_copy_js_1.FINDINGS_DEFAULT_COPY} The same definition — open and not informational — is what ctem_get_asset_counts reports as finding_count and what ctem_get_tdl_counts buckets. Default sort: Threat Discovery Level (TDL5=most critical). Only findings on active hosts are returned, and hosts the customer excluded are never counted. Response includes deep_scan_status, deep_scan_summary_ja/en (Haiku-generated scan result summaries), and ineligible_reason (CWE-based reason when deep scan is not applicable, null otherwise).`, {
domain_id: zod_1.z.string().uuid().describe('Origin domain ID (required)'),
page: zod_1.z.number().int().min(1).optional().describe('Page number (default: 1)'),
limit: zod_1.z.number().int().min(1).max(100).optional().describe('Items per page (default: 50, max: 100)'),
status: zod_1.z.enum(['open', 'closed', 'accepted', 'all']).optional()
.describe(finding_count_copy_js_1.STATUS_PARAM_COPY),
include_info: zod_1.z.boolean().optional()
.describe(finding_count_copy_js_1.INCLUDE_INFO_PARAM_COPY),
severity: zod_1.z.enum(['critical', 'high', 'medium', 'low', 'info']).optional()
.describe(finding_count_copy_js_1.SEVERITY_PARAM_COPY),
search: zod_1.z.string().optional().describe('Search by title, vuln_id, or target'),
target_fqdn: zod_1.z.string().optional().describe('Filter by affected FQDN'),
source_filter: zod_1.z.enum(['all', 'passive_only', 'deep_only']).optional().describe('Filter by scan source type'),
vuln_id: zod_1.z.string().optional().describe('Filter by exact vuln_id'),
priority_key: zod_1.z.enum(['tdl5', 'tdl4', 'tdl3', 'tdl2', 'tdl1', 'info']).optional()
.describe('Filter by Threat Discovery Level: tdl5=critical+exploitable, tdl1=lowest'),
order_by: zod_1.z.enum(['effective_priority_rank', 'cvss', 'severity_order', 'first_seen_at', 'last_seen_at']).optional()
.describe('Sort field (default: effective_priority_rank = TDL order)'),
ascending: zod_1.z.boolean().optional().describe('Sort ascending (default: true)'),
}, async (params) => {
const res = await client.get('/v1/ctem/findings', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
server.tool('ctem_get_asset_counts', `[READ] Get counts of each asset type (hosts, IPs, ports, tech, etc.) for a domain. ${finding_count_copy_js_1.ASSET_COUNTS_FINDING_COPY}`, {
domain_id: zod_1.z.string().uuid().describe('Origin domain ID (required)'),
}, async (params) => {
const res = await client.get('/v1/ctem/assets/counts', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
server.tool('ctem_get_tdl_counts', `[READ] Get open finding counts grouped by Threat Discovery Level (TDL). TDL5=most critical (CVSS>=9 + EPSS>=0.1), TDL1=lowest. ${finding_count_copy_js_1.TDL_COUNTS_FINDING_COPY}`, {
domain_id: zod_1.z.string().uuid().describe('Origin domain ID (required)'),
}, async (params) => {
const res = await client.get('/v1/ctem/findings/severity', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
server.tool('ctem_get_scores', '[READ] Get live domain scores: asset counts (hosts, IPs, ports, tech, buckets, URLs) and findings breakdown by severity and status', {
domain_id: zod_1.z.string().uuid().describe('Origin domain ID (required)'),
}, async (params) => {
const res = await client.get('/v1/ctem/scores', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
server.tool('ctem_get_score_trend', '[READ] Get security score trend over time for a domain (snapshot-based)', {
domain_id: zod_1.z.string().uuid().describe('Origin domain ID (required)'),
days: zod_1.z.number().int().min(7).max(365).optional().describe('Number of days to look back (default: 56)'),
}, async (params) => {
const res = await client.get('/v1/ctem/scores/trend', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
server.tool('ctem_list_ports', '[READ] List open ports grouped by port/protocol for a domain. Supports filtering by source type (passive/deep scan), port number, and protocol.', {
domain_id: zod_1.z.string().uuid().describe('Origin domain ID (required)'),
page: zod_1.z.number().int().min(1).optional().describe('Page number (default: 1)'),
limit: zod_1.z.number().int().min(1).max(100).optional().describe('Items per page (default: 50, max: 100)'),
search: zod_1.z.string().optional().describe('Search by port number or service name'),
source_type: zod_1.z.string().optional().describe('Filter by scan source type'),
port: zod_1.z.number().int().optional().describe('Filter by exact port number'),
protocol: zod_1.z.string().optional().describe('Filter by protocol (e.g. tcp, udp)'),
order_by: zod_1.z.enum(['target', 'port', 'protocol', 'service', 'first_seen_at', 'last_seen_at']).optional()
.describe('Sort field (default: target)'),
ascending: zod_1.z.boolean().optional().describe('Sort ascending (default: true)'),
}, async (params) => {
const res = await client.get('/v1/ctem/ports', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
server.tool('ctem_list_tech', '[READ] List detected technologies grouped by category/name for a domain. Supports filtering by source type, tech category, and tech name.', {
domain_id: zod_1.z.string().uuid().describe('Origin domain ID (required)'),
page: zod_1.z.number().int().min(1).optional().describe('Page number (default: 1)'),
limit: zod_1.z.number().int().min(1).max(100).optional().describe('Items per page (default: 50, max: 100)'),
search: zod_1.z.string().optional().describe('Search by technology name'),
source_type: zod_1.z.string().optional().describe('Filter by scan source type'),
tech_category: zod_1.z.string().optional().describe('Filter by technology category (e.g. CMS, Web Framework)'),
tech_name: zod_1.z.string().optional().describe('Filter by exact technology name'),
order_by: zod_1.z.enum(['tech_name', 'tech_category', 'target', 'first_seen_at', 'last_seen_at']).optional()
.describe('Sort field (default: tech_name)'),
ascending: zod_1.z.boolean().optional().describe('Sort ascending (default: true)'),
}, async (params) => {
const res = await client.get('/v1/ctem/tech', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
// ── Phase B2: asm-api wrapper plug ──────────────────────
// ── ctem_list_port_groups ──
server.tool('ctem_list_port_groups', '[READ] Aggregate count of hosts per (port, protocol, service) for a domain. Used by Discovery → Ports tab.', {
domain_id: zod_1.z.string().uuid().describe('Origin domain ID (required)'),
}, async (params) => {
const res = await client.get('/v1/ctem/ports/groups', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
// ── ctem_list_tech_groups ──
server.tool('ctem_list_tech_groups', '[READ] Aggregate hierarchy of tech_category → tech_name → host_count for a domain. Used by Discovery → Tech tab.', {
domain_id: zod_1.z.string().uuid().describe('Origin domain ID (required)'),
}, async (params) => {
const res = await client.get('/v1/ctem/tech/groups', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
// ── ctem_list_buckets_merged ──
server.tool('ctem_list_buckets_merged', '[READ] Cloud buckets de-duplicated by (cloud_type, bucket_name). Used by Discovery → Bucket tab merged view.', {
domain_id: zod_1.z.string().uuid().describe('Origin domain ID (required)'),
page: zod_1.z.number().int().min(1).optional(),
limit: zod_1.z.number().int().min(1).max(100).optional(),
cloud_type: zod_1.z.string().optional().describe('Filter by cloud provider: aws_s3, gcp_gcs, azure_blob, etc.'),
search: zod_1.z.string().optional(),
}, async (params) => {
const res = await client.get('/v1/ctem/buckets/merged', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
// ── ctem_summarize_findings ──
server.tool('ctem_summarize_findings', `[READ] Vuln-aggregated finding rollup for a domain. Returns one row per vuln_id with severity, severity_order, finding_count, max_cvss, max_epss, kev_count, affected_hosts. Filterable by status/severity. Used by Discovery → Vulnerability tab summary. ${finding_count_copy_js_1.SUMMARIZE_FINDING_COUNT_CAVEAT}`, {
domain_id: zod_1.z.string().uuid().describe('Origin domain ID (required)'),
status: zod_1.z.string().optional().describe('Filter by status: open, closed, accepted'),
severity: zod_1.z.enum(['critical', 'high', 'medium', 'low', 'info']).optional()
.describe(finding_count_copy_js_1.SUMMARIZE_SEVERITY_PARAM_COPY),
}, async (params) => {
const res = await client.get('/v1/ctem/findings/summary', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
// ── ctem_list_ips ──
server.tool('ctem_list_ips', '[READ] Paginated list of IP addresses discovered for a domain.', {
domain_id: zod_1.z.string().uuid().describe('Origin domain ID (required)'),
page: zod_1.z.number().int().min(1).optional(),
limit: zod_1.z.number().int().min(1).max(100).optional(),
search: zod_1.z.string().optional(),
source_type: zod_1.z.string().optional().describe('Filter by source: dns, probe, ct, etc.'),
order_by: zod_1.z.enum(['ip', 'first_seen_at']).optional(),
ascending: zod_1.z.boolean().optional(),
}, async (params) => {
const res = await client.get('/v1/ctem/ips', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
// ── ctem_list_urls ──
server.tool('ctem_list_urls', '[READ] Paginated list of URLs discovered for a domain.', {
domain_id: zod_1.z.string().uuid().describe('Origin domain ID (required)'),
page: zod_1.z.number().int().min(1).optional(),
limit: zod_1.z.number().int().min(1).max(100).optional(),
search: zod_1.z.string().optional(),
source_type: zod_1.z.string().optional(),
status_code_range: zod_1.z.enum(['2xx3xx', '2xx', '3xx', '4xx', '5xx']).optional()
.describe('HTTP status filter. Only these five tokens are matched server-side; a range like "200-299" silently returns nothing.'),
order_by: zod_1.z.enum(['url', 'status_code', 'first_seen_at']).optional(),
ascending: zod_1.z.boolean().optional(),
}, async (params) => {
const res = await client.get('/v1/ctem/urls', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
// ── ctem_list_buckets_paginated ──
// NOTE: existing tool `ctem_list_buckets` returns the grouped view
// (customer_api_ctem_bucket_groups, HTTP /v1/ctem/buckets). This new tool returns
// the plain paginated list (customer_api_ctem_buckets, HTTP /v1/ctem/buckets/list).
// Asymmetric vs ports/tech where the simple name is paginated and `/groups` is grouped.
server.tool('ctem_list_buckets_paginated', '[READ] Paginated list of cloud storage buckets discovered for a domain (one row per bucket, no aggregation). Use ctem_list_buckets_merged for the de-duplicated view.', {
domain_id: zod_1.z.string().uuid().describe('Origin domain ID (required)'),
page: zod_1.z.number().int().min(1).optional(),
limit: zod_1.z.number().int().min(1).max(100).optional(),
search: zod_1.z.string().optional(),
source_type: zod_1.z.string().optional(),
order_by: zod_1.z.enum(['bucket_name', 'first_seen_at']).optional(),
ascending: zod_1.z.boolean().optional(),
}, async (params) => {
const res = await client.get('/v1/ctem/buckets/list', params);
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }], isError: !res.success };
});
}