@chainpatrol/mcp 1.10.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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # @chainpatrol/mcp
2
+
3
+ ## 1.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8d8861e: Add `@chainpatrol/mcp`, an MCP server exposing every public API operation as a
8
+ tool, and a `chainpatrol mcp` command that serves it over stdio.
9
+
10
+ The tool surface is generated from the external API router rather than written
11
+ by hand, so agents reach the same capabilities the API exposes. Large value
12
+ lists and long explanatory prose are served as MCP resources instead of being
13
+ inlined in every schema, and workflow guides (organization healthcheck, trend
14
+ search, weekly customer-success sweep) ship as MCP prompts.
15
+
16
+ Point an MCP client at `chainpatrol mcp` after `chainpatrol login`, or set
17
+ `CHAINPATROL_API_KEY` for a service account.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @chainpatrol/mcp
2
+
3
+ The official ChainPatrol [MCP](https://modelcontextprotocol.io) server. Every
4
+ public API operation is available as a tool, so an agent never has to drop down
5
+ to raw HTTP.
6
+
7
+ ## Use it
8
+
9
+ The server ships with the CLI, which already handles login:
10
+
11
+ ```bash
12
+ npm install -g @chainpatrol/cli
13
+ chainpatrol login
14
+ chainpatrol mcp # speaks MCP on stdio
15
+ ```
16
+
17
+ Point an MCP client at that command. For Claude Code:
18
+
19
+ ```jsonc
20
+ {
21
+ "mcpServers": {
22
+ "chainpatrol": {
23
+ "command": "chainpatrol",
24
+ "args": ["mcp"],
25
+ },
26
+ },
27
+ }
28
+ ```
29
+
30
+ For a service account, set `CHAINPATROL_API_KEY` instead of logging in.
31
+
32
+ ## What it exposes
33
+
34
+ - **Tools** — one per public API operation: asset checks and search, reports,
35
+ proposal review, detections and detection configs, takedowns, threats,
36
+ metrics, healthchecks, and organization management.
37
+ - **Resources** — the large value lists (`chainpatrol://enums/asset_type`) and
38
+ the guides explaining what a proposal label or reject reason means.
39
+ - **Prompts** — the organization healthcheck, trend search, and weekly
40
+ customer-success sweep.
41
+
42
+ Most tools are scoped to one organization and there is no implicit default, so
43
+ call `user_orgs` first and pass the slug explicitly.
44
+
45
+ ## Configuration
46
+
47
+ | Variable | Purpose |
48
+ | ------------------------ | ------------------------------------------------------------- |
49
+ | `CHAINPATROL_API_KEY` | API key; takes precedence over a stored login. |
50
+ | `CHAINPATROL_API_URL` | API base URL. Defaults to `https://app.chainpatrol.io`. |
51
+ | `CHAINPATROL_CONFIG_DIR` | Where credentials are read from. |
52
+ | `CHAINPATROL_MCP_TOOLS` | Comma-separated tool names to expose. Everything, by default. |
53
+
54
+ `CHAINPATROL_MCP_TOOLS` exists only to trade breadth for context budget — it is
55
+ not an access control. Authorization is enforced by the API against the
56
+ credential in use. Both entry points read it: `chainpatrol-mcp`, and
57
+ `chainpatrol mcp`, where an explicit `--tools` takes precedence.
58
+
59
+ ## The tool surface is generated
60
+
61
+ `src/generated/tools.json` is produced from the external tRPC router by
62
+ `yarn tools:generate`, so a tool exists for every public operation by
63
+ construction rather than by hand. `yarn tools:check` fails when the committed
64
+ manifest no longer matches the router, and runs in CI — a new API endpoint
65
+ cannot ship without the MCP surface catching up.
66
+
67
+ Two deliberate gaps, both listed with reasons in the manifest's `excluded`
68
+ array: `/internal/*` paths, and the three inbound webhook receivers
69
+ (Cloudflare, GitHub, Notion) that third-party services call and users do not.
@@ -0,0 +1,29 @@
1
+ declare const DEFAULT_API_URL = "https://app.chainpatrol.io";
2
+ interface StoredConfig {
3
+ apiUrl: string;
4
+ defaultOrg?: string;
5
+ }
6
+ declare function configDir(): string;
7
+ declare function readJsonFile<T>(path: string): T | null;
8
+ /**
9
+ * The configuration as written on disk, with no environment applied.
10
+ *
11
+ * Use this for read-modify-write: saving the *effective* config would persist a
12
+ * one-off `CHAINPATROL_API_URL` as the stored default, so a later command run
13
+ * without the variable would keep talking to the wrong API.
14
+ */
15
+ declare function readStoredConfig(): StoredConfig;
16
+ /**
17
+ * The effective API URL.
18
+ *
19
+ * `CHAINPATROL_API_URL` takes precedence over a stored `apiUrl` — MCP clients
20
+ * launch a server with an environment rather than a config file — and is read
21
+ * on each call rather than once at module load, so a value set after import
22
+ * still applies. A blank value counts as unset.
23
+ *
24
+ * Takes an already-read `stored` config when the caller has one, so resolving
25
+ * the whole configuration does not read the same file twice.
26
+ */
27
+ declare function resolveApiUrl(stored?: StoredConfig): string;
28
+
29
+ export { DEFAULT_API_URL, type StoredConfig, configDir, readJsonFile, readStoredConfig, resolveApiUrl };
@@ -0,0 +1,15 @@
1
+ import {
2
+ DEFAULT_API_URL,
3
+ configDir,
4
+ readJsonFile,
5
+ readStoredConfig,
6
+ resolveApiUrl
7
+ } from "./chunk-4VHYP4ZH.js";
8
+ export {
9
+ DEFAULT_API_URL,
10
+ configDir,
11
+ readJsonFile,
12
+ readStoredConfig,
13
+ resolveApiUrl
14
+ };
15
+ //# sourceMappingURL=chainpatrol-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,39 @@
1
+ // src/chainpatrol-config.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ var DEFAULT_API_URL = "https://app.chainpatrol.io";
6
+ function configDir() {
7
+ if (process.env.CHAINPATROL_CONFIG_DIR) return process.env.CHAINPATROL_CONFIG_DIR;
8
+ const legacy = join(homedir(), ".chainpatrol");
9
+ const xdg = join(
10
+ process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"),
11
+ "chainpatrol"
12
+ );
13
+ return existsSync(legacy) && !existsSync(xdg) ? legacy : xdg;
14
+ }
15
+ function readJsonFile(path) {
16
+ if (!existsSync(path)) return null;
17
+ try {
18
+ return JSON.parse(readFileSync(path, "utf-8"));
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ function readStoredConfig() {
24
+ const stored = readJsonFile(join(configDir(), "config.json"));
25
+ return { apiUrl: DEFAULT_API_URL, ...stored };
26
+ }
27
+ function resolveApiUrl(stored = readStoredConfig()) {
28
+ const fromEnv = process.env.CHAINPATROL_API_URL?.trim();
29
+ return fromEnv || stored.apiUrl;
30
+ }
31
+
32
+ export {
33
+ DEFAULT_API_URL,
34
+ configDir,
35
+ readJsonFile,
36
+ readStoredConfig,
37
+ resolveApiUrl
38
+ };
39
+ //# sourceMappingURL=chunk-4VHYP4ZH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/chainpatrol-config.ts"],"sourcesContent":["/**\n * The on-disk configuration shared by every ChainPatrol entry point.\n *\n * `chainpatrol`, `chainpatrol mcp` and the standalone `chainpatrol-mcp` binary\n * all read the same `config.json`, so the rules for finding it and for letting\n * `CHAINPATROL_API_URL` override it live here rather than in each of them. They\n * were duplicated once and drifted on exactly that precedence, which is the\n * reason this module exists.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport const DEFAULT_API_URL = \"https://app.chainpatrol.io\";\n\nexport interface StoredConfig {\n apiUrl: string;\n defaultOrg?: string;\n}\n\nexport function configDir(): string {\n if (process.env.CHAINPATROL_CONFIG_DIR) return process.env.CHAINPATROL_CONFIG_DIR;\n\n // The legacy path wins only when it is the one that exists.\n const legacy = join(homedir(), \".chainpatrol\");\n const xdg = join(\n process.env.XDG_CONFIG_HOME ?? join(homedir(), \".config\"),\n \"chainpatrol\",\n );\n return existsSync(legacy) && !existsSync(xdg) ? legacy : xdg;\n}\n\nexport function readJsonFile<T>(path: string): T | null {\n if (!existsSync(path)) return null;\n try {\n return JSON.parse(readFileSync(path, \"utf-8\")) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * The configuration as written on disk, with no environment applied.\n *\n * Use this for read-modify-write: saving the *effective* config would persist a\n * one-off `CHAINPATROL_API_URL` as the stored default, so a later command run\n * without the variable would keep talking to the wrong API.\n */\nexport function readStoredConfig(): StoredConfig {\n const stored = readJsonFile<Partial<StoredConfig>>(join(configDir(), \"config.json\"));\n return { apiUrl: DEFAULT_API_URL, ...stored };\n}\n\n/**\n * The effective API URL.\n *\n * `CHAINPATROL_API_URL` takes precedence over a stored `apiUrl` — MCP clients\n * launch a server with an environment rather than a config file — and is read\n * on each call rather than once at module load, so a value set after import\n * still applies. A blank value counts as unset.\n *\n * Takes an already-read `stored` config when the caller has one, so resolving\n * the whole configuration does not read the same file twice.\n */\nexport function resolveApiUrl(stored: StoredConfig = readStoredConfig()): string {\n const fromEnv = process.env.CHAINPATROL_API_URL?.trim();\n return fromEnv || stored.apiUrl;\n}\n"],"mappings":";AASA,SAAS,YAAY,oBAAoB;AACzC,SAAS,eAAe;AACxB,SAAS,YAAY;AAEd,IAAM,kBAAkB;AAOxB,SAAS,YAAoB;AAClC,MAAI,QAAQ,IAAI,uBAAwB,QAAO,QAAQ,IAAI;AAG3D,QAAM,SAAS,KAAK,QAAQ,GAAG,cAAc;AAC7C,QAAM,MAAM;AAAA,IACV,QAAQ,IAAI,mBAAmB,KAAK,QAAQ,GAAG,SAAS;AAAA,IACxD;AAAA,EACF;AACA,SAAO,WAAW,MAAM,KAAK,CAAC,WAAW,GAAG,IAAI,SAAS;AAC3D;AAEO,SAAS,aAAgB,MAAwB;AACtD,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO;AAC9B,MAAI;AACF,WAAO,KAAK,MAAM,aAAa,MAAM,OAAO,CAAC;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASO,SAAS,mBAAiC;AAC/C,QAAM,SAAS,aAAoC,KAAK,UAAU,GAAG,aAAa,CAAC;AACnF,SAAO,EAAE,QAAQ,iBAAiB,GAAG,OAAO;AAC9C;AAaO,SAAS,cAAc,SAAuB,iBAAiB,GAAW;AAC/E,QAAM,UAAU,QAAQ,IAAI,qBAAqB,KAAK;AACtD,SAAO,WAAW,OAAO;AAC3B;","names":[]}