trustdex 0.4.4 → 0.5.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
@@ -54,6 +54,7 @@ TrustDex deliberately does not infer "official" status from names, stars, or bra
54
54
  - run as a GitHub Action in CI
55
55
  - fingerprint MCP entries, skills, and plugin manifests without storing secret values
56
56
  - inspect and filter Codex `config.toml` MCP sections
57
+ - inspect and filter Claude Code MCP JSON with explicit transport validation and header execution controls
57
58
  - perform explicit provenance lookups against GitHub, npm, and the official MCP Registry
58
59
  - verify whether the latest GitHub release tag/commit has a GitHub-verified signature
59
60
  - create signed review bundles and automatically require re-review when trust-relevant state drifts
@@ -75,8 +76,8 @@ Requires Node.js 20+.
75
76
  Run directly from npm:
76
77
 
77
78
  ```bash
78
- npx trustdex@0.4.4 --help
79
- npx trustdex@0.4.4 inspect ./mcp.json --pack strict --policy ./trustdex.policy.json
79
+ npx trustdex@0.5.0 --help
80
+ npx trustdex@0.5.0 inspect ./mcp.json --pack strict --policy ./trustdex.policy.json
80
81
  ```
81
82
 
82
83
  Or work from a source checkout:
@@ -166,6 +167,26 @@ The gate removes non-approved MCP sections while preserving unrelated TOML secti
166
167
 
167
168
  See [Using TrustDex with Codex](docs/CODEX.md) for a step-by-step gating workflow and for how this project uses Codex in its own review process.
168
169
 
170
+ ## Claude Code MCP configuration
171
+
172
+ Claude Code uses the `mcpServers` JSON shape. Use the Claude-specific commands so
173
+ transport changes, authentication header metadata, and header helper commands
174
+ are included in inspection and signed review:
175
+
176
+ ```bash
177
+ node ./bin/trustdex.mjs inspect-claude ./.mcp.json --pack strict
178
+ node ./bin/trustdex.mjs gate-claude ./.mcp.json --pack strict --out .trustdex/claude-mcp.json
179
+ claude --strict-mcp-config --mcp-config .trustdex/claude-mcp.json
180
+ ```
181
+
182
+ Check the gate's result before starting Claude. Only `ALLOW` entries are exposed by
183
+ default; `ASK` entries require review. The adapter rejects settings it cannot
184
+ inspect instead of silently trusting them. These commands are available in npm 0.5.0 and later, and from a source checkout.
185
+ Use `npx trustdex@0.5.0` in place of `node ./bin/trustdex.mjs` to run the published CLI.
186
+
187
+ See [Using TrustDex with Claude](docs/CLAUDE.md) for supported settings, PowerShell
188
+ error handling, signed review, and the Claude Desktop workflow.
189
+
169
190
  ## Online provenance observations
170
191
 
171
192
  Normal inspection remains local-only. These commands perform an explicit network lookup only when invoked:
@@ -311,7 +332,7 @@ The private key should never be committed.
311
332
  ```yaml
312
333
  - uses: actions/checkout@v4
313
334
 
314
- - uses: grigent/trustdex@v0.4.4
335
+ - uses: grigent/trustdex@v0.5.0
315
336
  with:
316
337
  config: ./mcp.json
317
338
  pack: official-first
package/bin/trustdex.mjs CHANGED
@@ -6,6 +6,7 @@ import process from 'node:process';
6
6
 
7
7
  import { inspectMcpConfig } from '../src/inspect.mjs';
8
8
  import { parseCodexMcpToml, gateCodexToml } from '../src/codex-config.mjs';
9
+ import { parseClaudeMcpConfig } from '../src/claude-config.mjs';
9
10
  import { inspectSkillText, inspectPluginManifest } from '../src/extensions.mjs';
10
11
  import { evaluateAll } from '../src/policy.mjs';
11
12
  import { getPolicyPack, mergePolicy } from '../src/policy-packs.mjs';
@@ -48,6 +49,13 @@ Usage:
48
49
  trustdex gate-codex <config.toml> --out gated.toml [--pack ...] [--policy policy.json]
49
50
  [--trust-store trust-store.json] [--include-ask]
50
51
 
52
+ trustdex inspect-claude <.mcp.json> [--pack ...] [--policy policy.json] [--json]
53
+ trustdex gate-claude <.mcp.json> --out gated.json [--pack ...] [--policy policy.json]
54
+ trustdex snapshot-claude <.mcp.json> --out snapshot.json [--pack ...]
55
+ trustdex approve-claude <.mcp.json> --private key.pem --dir review [--pack ...]
56
+ trustdex recheck-claude <.mcp.json> --review-dir review --public key.pem [--json]
57
+ Claude commands accept the same policy/trust-store flags as their generic counterparts.
58
+
51
59
  trustdex provenance github <owner/repo> [--json]
52
60
  trustdex provenance github-release <owner/repo> [--json]
53
61
  trustdex provenance npm <package> [--json]
@@ -146,8 +154,14 @@ function setInspectExitCode(results) {
146
154
  }
147
155
 
148
156
  async function evaluateMcp(configPath, args) {
149
- const config = await readJson(configPath);
150
- const inspected = inspectMcpConfig(config);
157
+ const isClaude = args[0].endsWith('-claude');
158
+ let config;
159
+ try { config = await readJson(configPath); } catch (error) {
160
+ if (isClaude && error instanceof SyntaxError) throw new Error('Invalid Claude MCP JSON.');
161
+ throw error;
162
+ }
163
+ const inspectionConfig = isClaude ? parseClaudeMcpConfig(config) : config;
164
+ const inspected = inspectMcpConfig(inspectionConfig);
151
165
  const store = await loadTrustStore(args);
152
166
  const withProvenance = store ? attachProvenanceAll(inspected, store) : inspected;
153
167
  const policy = await loadPolicy(args);
@@ -225,7 +239,7 @@ async function main() {
225
239
  return;
226
240
  }
227
241
 
228
- if (command === 'inspect') {
242
+ if (command === 'inspect' || command === 'inspect-claude') {
229
243
  const configPath = args[1];
230
244
  if (!configPath) throw new Error('inspect requires a path to an MCP JSON file.');
231
245
 
@@ -311,7 +325,7 @@ async function main() {
311
325
  return;
312
326
  }
313
327
 
314
- if (command === 'gate') {
328
+ if (command === 'gate' || command === 'gate-claude') {
315
329
  const configPath = args[1];
316
330
  const out = getFlag(args, '--out');
317
331
  if (!configPath || !out) throw new Error('gate requires <mcp.json> and --out <gated.json>.');
@@ -332,7 +346,7 @@ async function main() {
332
346
  return;
333
347
  }
334
348
 
335
- if (command === 'approve') {
349
+ if (command === 'approve' || command === 'approve-claude') {
336
350
  const configPath = args[1];
337
351
  const privatePath = getFlag(args, '--private');
338
352
  const reviewDir = getFlag(args, '--dir');
@@ -364,7 +378,7 @@ async function main() {
364
378
  return;
365
379
  }
366
380
 
367
- if (command === 'recheck') {
381
+ if (command === 'recheck' || command === 'recheck-claude') {
368
382
  const configPath = args[1];
369
383
  const reviewDir = getFlag(args, '--review-dir');
370
384
  const publicPath = getFlag(args, '--public');
@@ -409,7 +423,7 @@ async function main() {
409
423
  return;
410
424
  }
411
425
 
412
- if (command === 'snapshot') {
426
+ if (command === 'snapshot' || command === 'snapshot-claude') {
413
427
  const configPath = args[1];
414
428
  const out = getFlag(args, '--out');
415
429
  if (!configPath || !out) throw new Error('snapshot requires <mcp.json> and --out <snapshot.json>.');
package/docs/CLAUDE.md ADDED
@@ -0,0 +1,133 @@
1
+ # Using TrustDex with Claude
2
+
3
+ TrustDex can inspect Claude MCP JSON before Claude Code or Claude Desktop receives it.
4
+ It evaluates configuration evidence locally, does not start MCP servers, and does not
5
+ upload your configuration. An ALLOW decision means the evidence matched your policy;
6
+ it does not prove that server code is safe.
7
+
8
+ The Claude-specific commands are available in TrustDex 0.5.0 and later.
9
+ Use `npx trustdex@0.5.0` in place of `node ./bin/trustdex.mjs` in the examples
10
+ below to run the published CLI, or use the commands directly from a source checkout.
11
+
12
+ ## Supported configuration
13
+
14
+ Use a dedicated JSON file with a top-level `mcpServers` object:
15
+
16
+ ```json
17
+ {
18
+ "mcpServers": {
19
+ "reviewed-tools": {
20
+ "type": "http",
21
+ "url": "https://your-reviewed-server.example/mcp"
22
+ }
23
+ }
24
+ }
25
+ ```
26
+
27
+ The URL above is illustrative; configure your own reviewed server and trust policy.
28
+
29
+ Supported server fields:
30
+
31
+ | Transport | Fields |
32
+ | --- | --- |
33
+ | `stdio` (also the default when `type` is omitted) | `type`, `command`, `args`, `env` |
34
+ | `http` or `sse` | `type`, `url`, `headers`, `headersHelper` |
35
+
36
+ Remote entries require an explicit `type`. Command arguments and environment/header
37
+ values must be strings. Unknown root/server fields, other transports, malformed
38
+ entries, unresolved command/argument/endpoint variables, and URLs containing
39
+ credentials, query parameters, or fragments fail closed. This intentionally excludes
40
+ custom `oauth`, `alwaysLoad`, WebSocket settings, and future controls until they have
41
+ an explicit inspection implementation. Ordinary server-managed OAuth needs no
42
+ `oauth` field and remains Claude's responsibility.
43
+
44
+ Authentication headers add `static-http-headers` and/or `env-http-headers` signals.
45
+ Built-in packs require review for them. `headersHelper` adds
46
+ `header-helper-command`, which built-in packs block because Claude executes it in a
47
+ shell. TrustDex never runs the helper.
48
+
49
+ Fingerprints include the transport, header names, referenced environment-variable
50
+ names, and helper presence. Header/variable values, defaults, and helper command text
51
+ are omitted. Rotating a credential value alone does not invalidate a review; changing
52
+ its header name or environment reference does. Avoid putting credentials in command
53
+ arguments or URL paths. Gated files preserve original allowed entries and can contain
54
+ credentials supplied in the input: keep them local and out of version control.
55
+
56
+ ## Claude Code
57
+
58
+ Claude Code reads project MCP configuration from `.mcp.json`. Inspect it using the
59
+ Claude adapter, then supply only the generated configuration for that session.
60
+
61
+ From a source checkout in PowerShell:
62
+
63
+ ```powershell
64
+ node ./bin/trustdex.mjs inspect-claude ./.mcp.json --pack strict --policy ./trustdex.policy.json
65
+
66
+ node ./bin/trustdex.mjs gate-claude ./.mcp.json --pack strict --policy ./trustdex.policy.json --out ./.trustdex/claude-mcp.json
67
+ if ($LASTEXITCODE -ne 0) { throw "Resolve the gate error or pending reviews before starting Claude." }
68
+
69
+ claude --strict-mcp-config --mcp-config ./.trustdex/claude-mcp.json
70
+ ```
71
+
72
+ The policy file is your explicit trust policy, not a list inferred from vendor names.
73
+ Omit `--policy` to use the strict defaults, which block unknown sources and ask about
74
+ local programs. By default, the gate includes only ALLOW entries. ASK and BLOCK entries
75
+ stay out. A gate exit code of `1` indicates pending ASK decisions; `2` indicates failure.
76
+ Do not launch using an old output file after a failed gate.
77
+
78
+ The `--strict-mcp-config` flag matters: `--mcp-config` alone adds a configuration
79
+ alongside other MCP sources. Organizations using managed MCP configuration should
80
+ also verify the managed policy's effective servers. This workflow does not gate
81
+ Claude's built-in tools or inspect every skill/plugin automatically.
82
+
83
+ Inside the resulting Claude Code session, use `/mcp` to verify the actual loaded
84
+ servers. Configuration/unit tests do not replace that runtime check.
85
+
86
+ Official references:
87
+ [Claude Code MCP](https://code.claude.com/docs/en/mcp) and
88
+ [CLI flags](https://code.claude.com/docs/en/cli-reference).
89
+
90
+ ## Signed review and drift
91
+
92
+ Keep the Claude adapter throughout a review workflow; generic MCP commands do not
93
+ inspect Claude-specific controls.
94
+
95
+ ```bash
96
+ node ./bin/trustdex.mjs keygen --private .trustdex/private.pem --public .trustdex/public.pem
97
+ node ./bin/trustdex.mjs approve-claude ./.mcp.json --private .trustdex/private.pem --dir .trustdex/claude-review --pack strict --policy ./trustdex.policy.json
98
+ node ./bin/trustdex.mjs recheck-claude ./.mcp.json --review-dir .trustdex/claude-review --public .trustdex/public.pem
99
+ node ./bin/trustdex.mjs snapshot-claude ./.mcp.json --out .trustdex/claude-snapshot.json --pack strict --policy ./trustdex.policy.json
100
+ ```
101
+
102
+ Approval refuses BLOCK decisions. ASK decisions require an explicit human review;
103
+ running `approve-claude` records that review but does not automatically change the
104
+ gate's policy. Rechecks return APPROVED, NEEDS_REVIEW, or INVALID. A pre-existing
105
+ generic review has a different fingerprint and must be reviewed with the Claude adapter.
106
+ Private keys must never be committed.
107
+
108
+ ## Claude Desktop
109
+
110
+ For a local stdio MCP configuration, export just the `mcpServers` object from
111
+ `claude_desktop_config.json` into a dedicated input file. Do not feed unrelated
112
+ Desktop preferences into the MCP adapter. Use `inspect-claude` and `gate-claude`
113
+ on that file, review the result, then replace only the Desktop config's `mcpServers`
114
+ section with the gated section and restart Claude Desktop. Keep a backup of the
115
+ original configuration.
116
+
117
+ TrustDex does not modify your active Desktop settings or install a connector.
118
+ Desktop extensions and account-level remote connectors require separate review;
119
+ this JSON gate does not automatically govern them.
120
+
121
+ ## Inert example
122
+
123
+ `examples/claude.mcp.json` uses reserved `.invalid` domains, a placeholder
124
+ environment-variable reference, and an inert helper string. Do not launch it in
125
+ Claude. It is for static inspection only:
126
+
127
+ ```bash
128
+ node ./bin/trustdex.mjs inspect-claude ./examples/claude.mcp.json --pack strict --policy ./examples/claude.policy.json
129
+ node ./bin/trustdex.mjs gate-claude ./examples/claude.mcp.json --pack strict --policy ./examples/claude.policy.json --out .trustdex/example-claude.json
130
+ ```
131
+
132
+ Inspection intentionally exits `2`. The expected decisions are one ALLOW, one ASK,
133
+ and two BLOCK; the gate writes only `reviewed` and exits `1` because a review remains.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trustdex",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "A local-first trust, provenance, and review gate for AI agent tools, MCP servers, skills, and plugins.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "test": "node --test",
11
11
  "test:research": "node --test test/research-fixtures.test.mjs",
12
- "check": "node --check bin/trustdex.mjs && node --check src/canonical.mjs && node --check src/codex-config.mjs && node --check src/extensions.mjs && node --check src/gate.mjs && node --check src/inspect.mjs && node --check src/policy.mjs && node --check src/policy-packs.mjs && node --check src/provenance.mjs && node --check src/provenance-online.mjs && node --check src/review.mjs && node --check src/snapshot.mjs && node --check src/trust-record.mjs",
12
+ "check": "node --check bin/trustdex.mjs && node --check src/canonical.mjs && node --check src/claude-config.mjs && node --check src/codex-config.mjs && node --check src/extensions.mjs && node --check src/gate.mjs && node --check src/inspect.mjs && node --check src/policy.mjs && node --check src/policy-packs.mjs && node --check src/provenance.mjs && node --check src/provenance-online.mjs && node --check src/review.mjs && node --check src/snapshot.mjs && node --check src/trust-record.mjs",
13
13
  "prepublishOnly": "npm test && npm run check"
14
14
  },
15
15
  "engines": {
@@ -0,0 +1,106 @@
1
+ const SERVER_KEYS = {
2
+ stdio: new Set(['type', 'command', 'args', 'env']),
3
+ http: new Set(['type', 'url', 'headers', 'headersHelper']),
4
+ sse: new Set(['type', 'url', 'headers', 'headersHelper'])
5
+ };
6
+ const ENV_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-[^}]*)?\}/g;
7
+
8
+ function isRecord(value) {
9
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
10
+ }
11
+
12
+ function requireKeys(value, allowed, context) {
13
+ if (Object.keys(value).some((key) => !allowed.has(key))) {
14
+ throw new Error(`Unsupported ${context} field. Claude MCP inspection fails closed on unknown settings.`);
15
+ }
16
+ }
17
+
18
+ function environmentReferences(value) {
19
+ return [...new Set([...value.matchAll(ENV_REFERENCE)].map((match) => match[1]))].sort();
20
+ }
21
+
22
+ // Return inspection input only. Gating must preserve entries from the original
23
+ // configuration, never these internal adapter annotations.
24
+ export function parseClaudeMcpConfig(config) {
25
+ if (!isRecord(config) || !isRecord(config.mcpServers)) {
26
+ throw new Error('Expected a Claude MCP JSON object containing an mcpServers object.');
27
+ }
28
+ requireKeys(config, new Set(['mcpServers']), 'Claude MCP root');
29
+
30
+ const entries = Object.entries(config.mcpServers).map(([name, server]) => {
31
+ if (!/^[A-Za-z0-9_-]+$/.test(name) || ['__proto__', 'constructor', 'prototype'].includes(name)) {
32
+ throw new Error('Unsupported Claude MCP server name.');
33
+ }
34
+ if (!isRecord(server)) throw new Error('Each Claude MCP server must be an object.');
35
+ const type = server.type === undefined ? 'stdio' : server.type;
36
+ if (typeof type !== 'string' || !Object.hasOwn(SERVER_KEYS, type)) {
37
+ throw new Error('Unsupported Claude MCP transport. Supported: stdio, http, sse.');
38
+ }
39
+ requireKeys(server, SERVER_KEYS[type], 'Claude MCP server');
40
+ const signals = [];
41
+ const adapter = { runtime: 'claude-mcp-v1', transport: type };
42
+
43
+ if (type === 'stdio') {
44
+ if (typeof server.command !== 'string' || !server.command.trim()) {
45
+ throw new Error('Claude stdio MCP servers require a non-empty command.');
46
+ }
47
+ if (server.command.includes('${')) {
48
+ throw new Error('Claude command variable expansion is unsupported; use an explicit executable.');
49
+ }
50
+ if (server.args !== undefined && (!Array.isArray(server.args) || server.args.some((arg) => typeof arg !== 'string' || arg.includes('${')))) {
51
+ throw new Error('Claude MCP args must be strings without unresolved variable expansion.');
52
+ }
53
+ if (server.env !== undefined && (!isRecord(server.env) || Object.values(server.env).some((value) => typeof value !== 'string'))) {
54
+ throw new Error('Claude MCP env must contain string values.');
55
+ }
56
+ // Reference names are safe metadata; do not fingerprint env values or defaults.
57
+ adapter.envReferences = Object.fromEntries(Object.entries(server.env || {})
58
+ .sort(([a], [b]) => a.localeCompare(b))
59
+ .map(([key, value]) => [key, environmentReferences(value)]));
60
+ } else {
61
+ if (typeof server.url !== 'string' || !server.url.trim() || server.url.includes('${')) {
62
+ throw new Error('Claude remote MCP servers require a literal HTTP(S) URL.');
63
+ }
64
+ let endpoint;
65
+ try { endpoint = new URL(server.url); } catch {
66
+ throw new Error('Invalid Claude MCP URL.');
67
+ }
68
+ if (!['https:', 'http:'].includes(endpoint.protocol)) {
69
+ throw new Error('Unsupported Claude MCP URL scheme.');
70
+ }
71
+ // The generic inspector fingerprints its input URL. Reject URL credentials
72
+ // and query/fragment values before they can enter that fingerprint.
73
+ if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
74
+ throw new Error('Claude MCP URL credentials, query parameters, and fragments are unsupported; use headers.');
75
+ }
76
+ if (server.headers !== undefined) {
77
+ if (!isRecord(server.headers) || Object.values(server.headers).some((value) => typeof value !== 'string')) {
78
+ throw new Error('Claude MCP headers must contain string values.');
79
+ }
80
+ adapter.headerNames = Object.keys(server.headers).sort();
81
+ adapter.headerReferences = Object.fromEntries(Object.entries(server.headers)
82
+ .sort(([a], [b]) => a.localeCompare(b))
83
+ .map(([key, value]) => [key, environmentReferences(value)]));
84
+ if (adapter.headerNames.length) {
85
+ const references = Object.values(adapter.headerReferences);
86
+ if (references.some((refs) => refs.length)) signals.push('env-http-headers');
87
+ if (references.some((refs) => !refs.length)) signals.push('static-http-headers');
88
+ }
89
+ }
90
+ if (server.headersHelper !== undefined) {
91
+ if (typeof server.headersHelper !== 'string' || !server.headersHelper.trim()) {
92
+ throw new Error('Claude MCP headersHelper must be a non-empty string.');
93
+ }
94
+ signals.push('header-helper-command');
95
+ adapter.hasHeadersHelper = true;
96
+ }
97
+ }
98
+
99
+ return [name, {
100
+ ...server,
101
+ _trustdexSignals: signals,
102
+ _trustdexFingerprintData: adapter
103
+ }];
104
+ });
105
+ return { mcpServers: Object.fromEntries(entries) };
106
+ }