@proofofwork-agency/toolpin 0.2.3

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.
Files changed (61) hide show
  1. package/CONTRIBUTING.md +117 -0
  2. package/LICENSE +183 -0
  3. package/README.md +323 -0
  4. package/SECURITY.md +61 -0
  5. package/action.yml +134 -0
  6. package/dist/canonicalJson.js +38 -0
  7. package/dist/capabilities.js +139 -0
  8. package/dist/ci.js +26 -0
  9. package/dist/cli.js +1843 -0
  10. package/dist/clientSupport.js +76 -0
  11. package/dist/codexToml.js +213 -0
  12. package/dist/config.js +337 -0
  13. package/dist/constants.js +3 -0
  14. package/dist/continueYaml.js +76 -0
  15. package/dist/doctor.js +163 -0
  16. package/dist/install.js +191 -0
  17. package/dist/installed.js +405 -0
  18. package/dist/integrity.js +14 -0
  19. package/dist/inventory.js +169 -0
  20. package/dist/packageIntegrity.js +153 -0
  21. package/dist/plan.js +595 -0
  22. package/dist/policy.js +310 -0
  23. package/dist/registry.js +1610 -0
  24. package/dist/runtimeAdvisory.js +80 -0
  25. package/dist/safeFetch.js +157 -0
  26. package/dist/sarif.js +162 -0
  27. package/dist/scan.js +113 -0
  28. package/dist/search.js +44 -0
  29. package/dist/secrets.js +165 -0
  30. package/dist/signing.js +146 -0
  31. package/dist/tester.js +240 -0
  32. package/dist/trust.js +528 -0
  33. package/dist/tui/app.js +1731 -0
  34. package/dist/tui/command.js +50 -0
  35. package/dist/tui/configSnippet.js +11 -0
  36. package/dist/tui/constants.js +37 -0
  37. package/dist/tui/format.js +31 -0
  38. package/dist/tui/installedState.js +23 -0
  39. package/dist/tui/layout.js +65 -0
  40. package/dist/tui/selectors.js +282 -0
  41. package/dist/tui/types.js +1 -0
  42. package/dist/tui/ui/trust.js +77 -0
  43. package/dist/tui/views/installed.js +82 -0
  44. package/dist/tui/views/panels.js +637 -0
  45. package/dist/tui.js +12 -0
  46. package/dist/types.js +1 -0
  47. package/dist/verificationTrust.js +103 -0
  48. package/dist/verify.js +537 -0
  49. package/dist/version.js +1 -0
  50. package/dist/versions.js +127 -0
  51. package/docs/assets/readme/terminal-demo.svg +174 -0
  52. package/docs/assets/readme/tui-browse-overview.jpg +0 -0
  53. package/docs/assets/readme/tui-config-preview.jpg +0 -0
  54. package/docs/assets/readme/tui-help.jpg +0 -0
  55. package/docs/assets/readme/tui-installed-inventory.jpg +0 -0
  56. package/docs/how-to/catch-drift-in-ci.md +189 -0
  57. package/docs/how-to/custom-registries.md +156 -0
  58. package/docs/how-to/toolpin-curated-registry.md +153 -0
  59. package/package.json +76 -0
  60. package/registry/README.md +92 -0
  61. package/registry/v0/servers +115 -0
@@ -0,0 +1,127 @@
1
+ export function knownVersions(servers, name) {
2
+ const byVersion = new Map();
3
+ for (const server of servers) {
4
+ if (server.name !== name)
5
+ continue;
6
+ const existing = byVersion.get(server.version);
7
+ if (!existing || server.isLatest) {
8
+ byVersion.set(server.version, {
9
+ version: server.version,
10
+ source: server.registrySource,
11
+ isLatest: server.isLatest,
12
+ });
13
+ }
14
+ }
15
+ const sorted = [...byVersion.values()].sort(compareKnownVersions);
16
+ return sorted.map((entry, index) => ({ ...entry, isLatest: entry.isLatest || index === 0 }));
17
+ }
18
+ export function latestKnownVersion(servers, name) {
19
+ return knownVersions(servers, name)[0];
20
+ }
21
+ export function compareLockedToLatest(name, lockedVersion, servers) {
22
+ const versions = knownVersions(servers, name);
23
+ const latest = versions[0];
24
+ if (!lockedVersion || !latest) {
25
+ return {
26
+ name,
27
+ lockedVersion,
28
+ latestVersion: latest?.version,
29
+ status: "unknown",
30
+ previousVersions: versions.slice(1),
31
+ };
32
+ }
33
+ const delta = compareSemver(latest.version, lockedVersion);
34
+ return {
35
+ name,
36
+ lockedVersion,
37
+ latestVersion: latest.version,
38
+ status: delta === undefined
39
+ ? "unknown"
40
+ : delta > 0
41
+ ? "update-available"
42
+ : delta < 0
43
+ ? "ahead-of-registry"
44
+ : "current",
45
+ previousVersions: versions.filter((entry) => entry.version !== latest.version),
46
+ };
47
+ }
48
+ export function compareVersionish(a, b) {
49
+ return compareSemver(a, b) ?? 0;
50
+ }
51
+ export function compareVersionStatus(a, b) {
52
+ return compareSemver(a, b);
53
+ }
54
+ function compareKnownVersions(left, right) {
55
+ const semverDelta = compareSemver(right.version, left.version);
56
+ if (semverDelta !== undefined && semverDelta !== 0)
57
+ return semverDelta;
58
+ if (left.isLatest !== right.isLatest)
59
+ return left.isLatest ? -1 : 1;
60
+ return 0;
61
+ }
62
+ const SEMVER_PATTERN = new RegExp("^v?"
63
+ + "(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)"
64
+ + "(?:-((?:0|[1-9]\\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*))?"
65
+ + "(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?"
66
+ + "$");
67
+ function compareSemver(a, b) {
68
+ const left = parseSemver(a);
69
+ const right = parseSemver(b);
70
+ if (!left || !right)
71
+ return undefined;
72
+ const versionDelta = compareNumbers(left.major, right.major)
73
+ || compareNumbers(left.minor, right.minor)
74
+ || compareNumbers(left.patch, right.patch);
75
+ if (versionDelta !== 0)
76
+ return versionDelta;
77
+ return comparePrerelease(left.prerelease, right.prerelease);
78
+ }
79
+ function parseSemver(version) {
80
+ const match = SEMVER_PATTERN.exec(version);
81
+ if (!match)
82
+ return undefined;
83
+ return {
84
+ major: Number.parseInt(match[1], 10),
85
+ minor: Number.parseInt(match[2], 10),
86
+ patch: Number.parseInt(match[3], 10),
87
+ prerelease: match[4]?.split(".") ?? [],
88
+ };
89
+ }
90
+ function comparePrerelease(left, right) {
91
+ if (left.length === 0 && right.length === 0)
92
+ return 0;
93
+ if (left.length === 0)
94
+ return 1;
95
+ if (right.length === 0)
96
+ return -1;
97
+ const length = Math.max(left.length, right.length);
98
+ for (let i = 0; i < length; i += 1) {
99
+ const leftIdentifier = left[i];
100
+ const rightIdentifier = right[i];
101
+ if (leftIdentifier === undefined)
102
+ return -1;
103
+ if (rightIdentifier === undefined)
104
+ return 1;
105
+ const identifierDelta = comparePrereleaseIdentifier(leftIdentifier, rightIdentifier);
106
+ if (identifierDelta !== 0)
107
+ return identifierDelta;
108
+ }
109
+ return 0;
110
+ }
111
+ function comparePrereleaseIdentifier(left, right) {
112
+ const leftNumeric = isNumericIdentifier(left);
113
+ const rightNumeric = isNumericIdentifier(right);
114
+ if (leftNumeric && rightNumeric)
115
+ return compareNumbers(Number.parseInt(left, 10), Number.parseInt(right, 10));
116
+ if (leftNumeric)
117
+ return -1;
118
+ if (rightNumeric)
119
+ return 1;
120
+ return left === right ? 0 : left > right ? 1 : -1;
121
+ }
122
+ function compareNumbers(left, right) {
123
+ return left === right ? 0 : left > right ? 1 : -1;
124
+ }
125
+ function isNumericIdentifier(value) {
126
+ return /^(0|[1-9]\d*)$/.test(value);
127
+ }
@@ -0,0 +1,174 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="560" viewBox="0 0 1200 560" role="img" aria-labelledby="title desc">
2
+ <title id="title">Animated ToolPin terminal workflow</title>
3
+ <desc id="desc">A looping terminal animation showing ToolPin search, plan, install, lock, and CI drift-check commands.</desc>
4
+ <style>
5
+ :root {
6
+ color-scheme: dark;
7
+ }
8
+ svg {
9
+ background: #111;
10
+ }
11
+ .window {
12
+ fill: #181818;
13
+ stroke: #303030;
14
+ stroke-width: 1;
15
+ }
16
+ .topbar {
17
+ fill: #222;
18
+ }
19
+ .dot-red {
20
+ fill: #ff5f57;
21
+ }
22
+ .dot-yellow {
23
+ fill: #ffbd2e;
24
+ }
25
+ .dot-green {
26
+ fill: #28c840;
27
+ }
28
+ text {
29
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
30
+ font-size: 24px;
31
+ fill: #d8d8d8;
32
+ }
33
+ .small {
34
+ font-size: 19px;
35
+ fill: #999;
36
+ }
37
+ .prompt {
38
+ fill: #70e1bd;
39
+ font-weight: 700;
40
+ }
41
+ .cmd {
42
+ fill: #f5f5f5;
43
+ }
44
+ .muted {
45
+ fill: #9a9a9a;
46
+ }
47
+ .ok {
48
+ fill: #70e1bd;
49
+ font-weight: 700;
50
+ }
51
+ .warn {
52
+ fill: #ffd75f;
53
+ font-weight: 700;
54
+ }
55
+ .blue {
56
+ fill: #7cc7ff;
57
+ font-weight: 700;
58
+ }
59
+ .bar-ok {
60
+ fill: #70e1bd;
61
+ }
62
+ .bar-warn {
63
+ fill: #ffd75f;
64
+ }
65
+ .bar-bg {
66
+ fill: #3a3a3a;
67
+ }
68
+ .cursor {
69
+ fill: #70e1bd;
70
+ animation: blink 1s steps(2, start) infinite;
71
+ }
72
+ .frame {
73
+ opacity: 0;
74
+ animation: frames 12s steps(1, end) infinite;
75
+ }
76
+ .f1 {
77
+ animation-delay: 0s;
78
+ }
79
+ .f2 {
80
+ animation-delay: -9.6s;
81
+ }
82
+ .f3 {
83
+ animation-delay: -7.2s;
84
+ }
85
+ .f4 {
86
+ animation-delay: -4.8s;
87
+ }
88
+ .f5 {
89
+ animation-delay: -2.4s;
90
+ }
91
+ @keyframes frames {
92
+ 0%, 19.99% {
93
+ opacity: 1;
94
+ }
95
+ 20%, 100% {
96
+ opacity: 0;
97
+ }
98
+ }
99
+ @keyframes blink {
100
+ 0%, 45% {
101
+ opacity: 1;
102
+ }
103
+ 46%, 100% {
104
+ opacity: 0;
105
+ }
106
+ }
107
+ </style>
108
+
109
+ <rect class="window" x="24" y="24" width="1152" height="512" rx="14"/>
110
+ <rect class="topbar" x="24" y="24" width="1152" height="58" rx="14"/>
111
+ <rect class="topbar" x="24" y="62" width="1152" height="20"/>
112
+ <circle class="dot-red" cx="60" cy="53" r="9"/>
113
+ <circle class="dot-yellow" cx="90" cy="53" r="9"/>
114
+ <circle class="dot-green" cx="120" cy="53" r="9"/>
115
+ <text x="160" y="61" class="small">ToolPin v0.2.2 - MCP install review gate - toolpin or tpn</text>
116
+
117
+ <g class="frame f1">
118
+ <text x="62" y="126"><tspan class="prompt">$</tspan><tspan class="cmd"> toolpin search contextrelay --source toolpin</tspan></text>
119
+ <text x="62" y="174" class="muted">loaded 1 ToolPin-curated server</text>
120
+ <text x="62" y="222"><tspan class="ok">toolpin</tspan> ContextRelay <tspan class="ok">EVIDENCE OK</tspan></text>
121
+ <text x="62" y="270" class="muted">@proofofwork-agency/contextrelay@3.9.2</text>
122
+ <text x="62" y="318" class="muted">Local coordination bridge for Codex and Claude Code agents.</text>
123
+ <rect class="bar-ok" x="62" y="370" width="390" height="22" rx="4"/>
124
+ <rect class="bar-bg" x="452" y="370" width="84" height="22" rx="4"/>
125
+ <text x="62" y="440"><tspan class="prompt">&gt;</tspan> select, review, then install only after the plan looks right</text>
126
+ <rect class="cursor" x="1070" y="418" width="14" height="28"/>
127
+ </g>
128
+
129
+ <g class="frame f2">
130
+ <text x="62" y="126"><tspan class="prompt">$</tspan><tspan class="cmd"> toolpin plan @proofofwork-agency/contextrelay --client codex --live --verify</tspan></text>
131
+ <text x="62" y="178"><tspan class="blue">overview</tspan> @proofofwork-agency/contextrelay@3.9.2</text>
132
+ <text x="62" y="226">runtime npm transport stdio secrets <tspan class="ok">none declared</tspan></text>
133
+ <text x="62" y="274">evidence <tspan class="ok">ToolPin-verified npm_integrity_verified</tspan></text>
134
+ <text x="62" y="322">badges source repo, namespaced, npm, pinned version</text>
135
+ <text x="62" y="382">profile</text>
136
+ <rect class="bar-ok" x="190" y="360" width="380" height="22" rx="4"/>
137
+ <text x="596" y="382"><tspan class="ok">100%</tspan> metadata profile</text>
138
+ <text x="62" y="436">integrity</text>
139
+ <rect class="bar-ok" x="190" y="414" width="380" height="22" rx="4"/>
140
+ <text x="596" y="436"><tspan class="ok">100%</tspan> artifact evidence</text>
141
+ </g>
142
+
143
+ <g class="frame f3">
144
+ <text x="62" y="126"><tspan class="prompt">$</tspan><tspan class="cmd"> toolpin install @proofofwork-agency/contextrelay --client codex --scope global --update-lock</tspan></text>
145
+ <text x="62" y="184"><tspan class="blue">Install</tspan></text>
146
+ <text x="62" y="232">server @proofofwork-agency/contextrelay@3.9.2</text>
147
+ <text x="62" y="280">client codex scope global</text>
148
+ <text x="62" y="328">config <tspan class="ok">updated</tspan>: ~/.codex/config.toml</text>
149
+ <text x="62" y="376">lock <tspan class="ok">mcp-lock.json updated</tspan></text>
150
+ <text x="62" y="424" class="muted">- Requires Node.js and npm/npx on PATH.</text>
151
+ <text x="62" y="472"><tspan class="ok">done</tspan> installed for codex</text>
152
+ </g>
153
+
154
+ <g class="frame f4">
155
+ <text x="62" y="126"><tspan class="prompt">$</tspan><tspan class="cmd"> toolpin audit --verify</tspan></text>
156
+ <text x="62" y="184"><tspan class="blue">Audit findings</tspan></text>
157
+ <text x="62" y="232">lockfile mcp-lock.json</text>
158
+ <text x="62" y="280">checked 1 locked, 15 config file(s)</text>
159
+ <text x="62" y="328">doctor <tspan class="ok">ok</tspan> installed config matches lockfile</text>
160
+ <text x="62" y="376">secrets <tspan class="ok">ok</tspan> no plaintext secret findings</text>
161
+ <text x="62" y="424">evidence <tspan class="ok">ok</tspan> trusted artifact proof is fresh</text>
162
+ <text x="62" y="472"><tspan class="ok">no findings</tspan></text>
163
+ </g>
164
+
165
+ <g class="frame f5">
166
+ <text x="62" y="126"><tspan class="prompt">$</tspan><tspan class="cmd"> toolpin ci --file mcp-lock.json --live --verify</tspan></text>
167
+ <text x="62" y="184">resolving locked server plans...</text>
168
+ <text x="62" y="232">checking policy, generated config, manifest, evidence...</text>
169
+ <text x="62" y="280">comparing reviewed lock entries...</text>
170
+ <text x="62" y="348"><tspan class="ok">PASS</tspan> mcp-lock.json matches reviewed install state</text>
171
+ <text x="62" y="408"><tspan class="warn">commit</tspan> mcp-lock.json and run this in pull requests</text>
172
+ <text x="62" y="468"><tspan class="prompt">$</tspan><tspan class="cmd"> git push </tspan><tspan class="muted"># ToolPin protects the PR</tspan></text>
173
+ </g>
174
+ </svg>
Binary file
@@ -0,0 +1,189 @@
1
+ # Catch Drift in CI
2
+
3
+ Use `toolpin ci` when `mcp-lock.json` is committed to a repository and pull
4
+ requests should fail if registry metadata, generated client config, policy, or
5
+ optional signatures no longer match the reviewed lockfile.
6
+
7
+ `toolpin ci` is read-only. It re-resolves locked entries, rebuilds install
8
+ plans, checks lock integrity, enforces policy unless bypassed, and exits
9
+ non-zero on drift. It does not update `mcp-lock.json`.
10
+
11
+ `toolpin ci --sarif` emits SARIF 2.1.0 JSON to stdout and still exits non-zero
12
+ on drift:
13
+
14
+ ```bash
15
+ toolpin ci --file mcp-lock.json --live --sarif > toolpin.sarif
16
+ ```
17
+
18
+ Automatic GitHub code-scanning upload is not wired into the composite action in
19
+ this pass; add an explicit upload step after reviewing the desired repository
20
+ permissions.
21
+
22
+ ## Basic GitHub Action
23
+
24
+ After the repository is public and tagged, call the composite action from your
25
+ workflow. The action installs ToolPin from the action source by default, so it
26
+ does not require npm publish:
27
+
28
+ ```yaml
29
+ name: ToolPin
30
+
31
+ on:
32
+ pull_request:
33
+ push:
34
+ branches: [main]
35
+
36
+ permissions:
37
+ contents: read
38
+
39
+ jobs:
40
+ mcp-lock:
41
+ runs-on: ubuntu-latest
42
+ steps:
43
+ - uses: actions/checkout@v4
44
+ - uses: proofofwork-agency/toolpin@v0.2.3
45
+ with:
46
+ file: mcp-lock.json
47
+ live: "true"
48
+ ```
49
+
50
+ The action builds ToolPin from `$GITHUB_ACTION_PATH` and runs:
51
+
52
+ ```bash
53
+ toolpin ci --file mcp-lock.json --source all --live --policy .toolpin/policy.json
54
+ ```
55
+
56
+ When `.toolpin/policy.json` is absent, the current CLI treats policy enforcement
57
+ as a no-op.
58
+
59
+ ## Direct CLI Workflow
60
+
61
+ Use this form when you want the workflow to install the npm package directly
62
+ instead of using the composite Action:
63
+
64
+ ```yaml
65
+ name: ToolPin
66
+
67
+ on:
68
+ pull_request:
69
+
70
+ permissions:
71
+ contents: read
72
+
73
+ jobs:
74
+ mcp-lock:
75
+ runs-on: ubuntu-latest
76
+ steps:
77
+ - uses: actions/checkout@v4
78
+ - uses: actions/setup-node@v4
79
+ with:
80
+ node-version: 22
81
+ - run: npm install -g @proofofwork-agency/toolpin
82
+ - run: toolpin ci --file mcp-lock.json --live
83
+ ```
84
+
85
+ For unreleased source-checkout development, run `npm ci`, `npm test`, and
86
+ `npm run dev -- ci --file mcp-lock.json --live`.
87
+
88
+ ## Digest Pin
89
+
90
+ `--expect-digest` compares the whole-lock digest against a value provided by
91
+ CI. Store the expected digest outside the pull request being checked, for
92
+ example as a GitHub Actions variable or secret.
93
+
94
+ Generate the digest after reviewing a lockfile change:
95
+
96
+ ```bash
97
+ toolpin lock digest --file mcp-lock.json
98
+ ```
99
+
100
+ Use it in CI:
101
+
102
+ ```yaml
103
+ - uses: proofofwork-agency/toolpin@v0.2.3
104
+ with:
105
+ file: mcp-lock.json
106
+ live: "true"
107
+ expect-digest: ${{ vars.TOOLPIN_LOCK_DIGEST }}
108
+ ```
109
+
110
+ Do not commit the expected digest next to `mcp-lock.json`; a PR that changes
111
+ both files would defeat the check.
112
+
113
+ ## Detached Signature
114
+
115
+ ToolPin can verify a detached Ed25519 signature before registry resolution:
116
+
117
+ ```bash
118
+ toolpin lock sign --policy .toolpin/policy.json --key private.pem --file mcp-lock.json --signature mcp-lock.sig
119
+ toolpin lock verify-signature --policy .toolpin/policy.json --key public.pem --file mcp-lock.json --signature mcp-lock.sig
120
+ ```
121
+
122
+ Then in CI:
123
+
124
+ ```yaml
125
+ - uses: proofofwork-agency/toolpin@v0.2.3
126
+ with:
127
+ file: mcp-lock.json
128
+ live: "true"
129
+ signature: mcp-lock.sig
130
+ public-key: public.pem
131
+ ```
132
+
133
+ Commit `mcp-lock.sig` and the public key only after review. Never commit the
134
+ private key. A signature is meaningful only when the private key and public
135
+ trust root are controlled outside the PR path.
136
+
137
+ ## Policy and Live Verification
138
+
139
+ To enforce a non-default policy path:
140
+
141
+ ```yaml
142
+ - uses: proofofwork-agency/toolpin@v0.2.3
143
+ with:
144
+ policy: security/toolpin-policy.json
145
+ ```
146
+
147
+ To make CI skip policy enforcement explicitly:
148
+
149
+ ```yaml
150
+ - uses: proofofwork-agency/toolpin@v0.2.3
151
+ with:
152
+ no-policy: "true"
153
+ ```
154
+
155
+ To re-run verification before comparing locked plans, use the stricter CI
156
+ posture:
157
+
158
+ ```yaml
159
+ - uses: proofofwork-agency/toolpin@v0.2.3
160
+ with:
161
+ live: "true"
162
+ verify: "true"
163
+ timeout: "15000"
164
+ ```
165
+
166
+ `verify: "true"` can require network access, local runtimes, and server
167
+ credentials for live MCP probes. Use `skip-live-verification: "true"` when you
168
+ want artifact/metadata verification without live `tools/list` probing. Treat
169
+ that as a conscious downgrade: it skips capability hashing and CI rejects it for
170
+ entries that already have live capability pins.
171
+
172
+ ## What Fails the Build
173
+
174
+ CI exits non-zero when:
175
+
176
+ - `mcp-lock.json` is missing, empty, malformed, or has an unsupported version.
177
+ - Per-entry lock integrity is missing or invalid.
178
+ - A locked server/client no longer resolves to the reviewed install plan
179
+ (version, target, trust, generated config, or capability manifest drifted).
180
+ - The whole-lock digest (`--expect-digest`) does not match.
181
+ - Detached signature verification fails, or `signature` and `public-key` are not
182
+ supplied as a pair.
183
+ - The selected policy rejects a locked entry.
184
+ - `--verify` finds critical verification findings.
185
+ - `--sarif` changes the output format only; it does not make failing checks
186
+ advisory.
187
+
188
+ Use `toolpin install --update-lock` or `toolpin lock <server> --client <client>`
189
+ only after reviewing the drift locally. CI should not update the lockfile.
@@ -0,0 +1,156 @@
1
+ # Custom Registries
2
+
3
+ ToolPin can read built-in registries and repo-owned registry definitions from `.toolpin/registries.json`.
4
+
5
+ Built-in sources:
6
+
7
+ - `toolpin`: ToolPin hosted curated registry with bundled fallback, installable,
8
+ `curated` trust, pinned, and always enabled.
9
+ - `official`: Official MCP Registry, installable, `canonical` trust.
10
+ - `docker`: Docker MCP Catalog, installable, `curated` trust.
11
+ - `pulsemcp`, `smithery`, `glama`: known directory sources, disabled by default. Enable one explicitly before it appears in `--source all`, browse, or search. `pulsemcp` is auth-gated, Smithery hosted targets require `--allow-hosted-directory-targets`, and Glama entries become installable only when ToolPin can match their repository to an Official MCP Registry entry with lockable targets.
12
+
13
+ Use source preferences to turn optional built-ins on or off. The pinned
14
+ `toolpin` source is always enabled:
15
+
16
+ ```sh
17
+ toolpin registry list
18
+ toolpin registry enable glama
19
+ toolpin registry disable glama
20
+ ```
21
+
22
+ Those commands write a top-level `sources` preference block:
23
+
24
+ ```json
25
+ {
26
+ "sources": {
27
+ "glama": {
28
+ "enabled": true
29
+ },
30
+ "smithery": {
31
+ "enabled": false
32
+ }
33
+ },
34
+ "registries": []
35
+ }
36
+ ```
37
+
38
+ ToolPin also maintains a GitHub-backed curated registry. Current ToolPin
39
+ versions expose it as the built-in `toolpin` source and fetch
40
+ `https://raw.githubusercontent.com/proofofwork-agency/toolpin/main/registry/v0/servers`
41
+ first, falling back to the bundled snapshot if the hosted file is unavailable
42
+ or invalid.
43
+
44
+ See [ToolPin Curated Registry](./toolpin-curated-registry.md) for the PR-based
45
+ review workflow.
46
+
47
+ ## Official-Compatible Registry
48
+
49
+ Use this for company or private registries that expose the MCP Registry `/v0/servers` response shape.
50
+
51
+ ```json
52
+ {
53
+ "registries": [
54
+ {
55
+ "id": "company",
56
+ "type": "official-compatible",
57
+ "url": "https://registry.company.com/v0",
58
+ "mode": "installable",
59
+ "trust": "private"
60
+ }
61
+ ]
62
+ }
63
+ ```
64
+
65
+ Then run:
66
+
67
+ ```sh
68
+ toolpin registry list
69
+ toolpin ingest --source company
70
+ toolpin search postgres --source company
71
+ toolpin install company/postgres --client claude --update-lock
72
+ ```
73
+
74
+ Installable entries still need enough machine-readable metadata for ToolPin to build a lockable install plan: a package or remote target, version, transport, source metadata, and any declared secrets.
75
+
76
+ Verified metadata comes from an installable source that exposes reviewable MCP
77
+ Registry-style server records, such as the Official MCP Registry, Docker MCP
78
+ Catalog, or an `official-compatible` custom/curated registry. For a directory
79
+ result to become installable, curate it into metadata with package/remotes,
80
+ exact versions, repository URLs, and artifact pins where available, such as OCI
81
+ `@sha256:` digests or MCPB `fileSha256` values.
82
+
83
+ ## Discovery Registries
84
+
85
+ Broad directories and scraped indexes should start as discovery sources:
86
+
87
+ ```json
88
+ {
89
+ "registries": [
90
+ {
91
+ "id": "glama-public",
92
+ "type": "http-json",
93
+ "url": "https://example.com/mcp-servers.json",
94
+ "mode": "discovery"
95
+ }
96
+ ]
97
+ }
98
+ ```
99
+
100
+ Discovery entries can appear in search and info views when their source is enabled. ToolPin refuses to install them until they normalize into a source that is explicitly marked `installable`, or an adapter can safely re-resolve them to an installable registry entry.
101
+
102
+ This is why a source such as Glama can be useful for discovery while still not
103
+ providing installable metadata. It may describe many servers, gateways, or
104
+ directory matches, but ToolPin still needs a matching official entry with a
105
+ lockable package or remote target before it can review, install, and enforce the
106
+ entry.
107
+
108
+ This keeps the product claim precise: ToolPin can search broad directories, but only installs servers it can normalize, review, lock, and enforce.
109
+
110
+ ## Config Reference
111
+
112
+ Each entry in `.toolpin/registries.json` supports:
113
+
114
+ | Field | Required | Default | Notes |
115
+ |---|---|---|---|
116
+ | `id` | yes | — | Stable source identifier used by `--source`. |
117
+ | `url` | yes | — | Registry endpoint. For `official-compatible`, ToolPin appends `/servers`. |
118
+ | `type` | no | `official-compatible` | `official-compatible` (MCP Registry `/v0/servers` shape) or `http-json` (response with a `servers` or `entries` array). |
119
+ | `mode` | no | `installable` for `official-compatible`, `discovery` for `http-json` | Whether entries from this source can be installed. |
120
+ | `label` | no | the `id` | Display label in `toolpin registry list` and the TUI. |
121
+ | `trust` | no | `private` | One of `canonical`, `curated`, `directory`, `private`. |
122
+ | `enabled` | no | `true` | For custom registry entries, set to `false` to hide the source from `--source all`; optional built-ins are toggled through top-level `sources` preferences or `toolpin registry enable/disable`. Pinned sources such as `toolpin` cannot be disabled. |
123
+ | `authEnv` | no | — | Environment variable name that holds an auth token; marks the source `authRequired`. Advisory only — ToolPin does not yet send this token in registry requests. |
124
+ | `description` | no | generated | Human-readable source description. |
125
+
126
+ Invalid `type` or `mode` values are rejected. If a custom entry reuses a built-in `id`, the built-in wins and the custom entry is ignored.
127
+
128
+ ## Source Resolution and Caching
129
+
130
+ `--source all` fetches every enabled source in parallel and deduplicates entries by repository URL, server name, and version. On collisions, `toolpin` beats `official`, which beats `docker`, which beats any custom source. Disabled sources can remain in the cache, but browse/search filter them out until they are enabled again.
131
+
132
+ The TUI Browse list uses the same source priority by default: `toolpin`,
133
+ `official`, `docker`, then other enabled sources. Press `a` to cycle sort modes
134
+ or `g` to cycle the exact source filter.
135
+
136
+ `toolpin ingest` always fetches live and writes the combined result to `.toolpin/registry-cache.json` (a `{ generatedAt, entries }` document). Other commands read that cache when `--live` is omitted; if the cache is missing or does not contain the requested source, they transparently fall back to a live fetch. Pass `--live` to bypass the cache entirely. A cache file that exists but is not valid registry-cache JSON raises a `CacheSchemaError` instead of falling back.
137
+
138
+ ## TUI Installed View
139
+
140
+ Run:
141
+
142
+ ```sh
143
+ toolpin tui
144
+ ```
145
+
146
+ Open the `Installed` tab to inspect servers already written to supported config files across folder/project and global/user scopes. The view shows:
147
+
148
+ - config file, client, and scope
149
+ - locked, unlocked, or drift state
150
+ - registry match status: `registry:exact`, `registry:alias`, or `registry:none`
151
+ - locked and latest known versions
152
+ - lifecycle action: `action:update`, `action:adopt`, or `action:none`
153
+ - test source: `test:config` when ToolPin can test the installed client config
154
+ - delete, update/adopt, explicit version relock, doctor, and `test-installed` actions
155
+
156
+ Runtime status is advisory. ToolPin can mark a server `reachable` after `t` succeeds and `stale` when lock/config or version drift is detected. It does not claim process monitoring unless ToolPin owns the runtime or can query a client/gateway API.