@wickedevolutions/abilities-mcp 1.5.1 → 1.5.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.
- package/CHANGELOG.md +50 -0
- package/abilities-mcp.js +8 -0
- package/lib/auth/keychain-secret-store.js +198 -31
- package/lib/cli/config-store.js +167 -0
- package/lib/config-source-line.js +85 -0
- package/lib/config.js +36 -5
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Abilities MCP are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.5.3] - 2026-05-04
|
|
6
|
+
|
|
7
|
+
**macOS hotfix: OAuth in Claude Desktop's `.mcpb` runtime now works.** Hotfix to v1.5.2's `.mcpb` operator UX on macOS. v1.5.2 shipped with keytar prebuilds bundled, but Claude Desktop's hardened-runtime host process on macOS rejects native modules with mismatched code-signing Team IDs — a system-level macOS protection that applies to every hardened app, not a Claude Desktop quirk. This blocked OAuth inside Claude Desktop's `.mcpb` runtime even though the bundle itself is structurally correct (loads cleanly via system Node from the extracted `.mcpb`). The hotfix adds a darwin-only shell-out to the macOS `security` CLI when keytar fails to load. Validated end-to-end on darwin-arm64 by an operator running the documented progression (install `.mcpb` → `upgrade-auth` → `add-site` → multi-site OAuth in the same Claude Desktop entry, read and write confirmed live on two production WordPress sites via OAuth bearer) before release.
|
|
8
|
+
|
|
9
|
+
Bridge-only release — no companion adapter or ai release this hotfix.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`KeychainSecretStore` falls back to the macOS `security` CLI when keytar fails to load on darwin** (PR [#40](https://github.com/Wicked-Evolutions/abilities-mcp/pull/40), closes [#39](https://github.com/Wicked-Evolutions/abilities-mcp/issues/39)). When `require('keytar')` throws on darwin (the hardened-runtime Team ID mismatch), `_load()` sets `_fallbackMode = 'security-cli'` instead of throwing. `get` / `set` / `delete` then dispatch to `security find-generic-password -w` / `add-generic-password -U` / `delete-generic-password` via `child_process.execFile` (no shell — argv passes verbatim, no shell-injection surface). `findAll` returns `[]` in fallback mode (security CLI has no clean enumerate-by-service; the bridge runtime path doesn't depend on it — only the CLI subcommand `list-sites` does, which runs in system Node where keytar loads normally). Stderr matching `/could not be found/i` maps to keytar's null (get) / false (delete) return semantics; other stderr propagates as `SecretStoreError` code `security_cli_failed`. `isAvailable()` returns true in both keytar and fallback modes. **Linux/Windows behavior unchanged** — keytar load failures on those platforms still throw `keytar_unavailable` (no fallback engaged; the security CLI is darwin-only). Test seams (`requireKeytar`, `platform`, `exec` injection on the constructor) added for fallback-path unit testing without breaking the test runtime's real `require('keytar')`. New `test/auth/keychain-secret-store-darwin-fallback.test.js` covers keytar-success preservation, fallback engagement on darwin, "could not be found" mapping, error propagation, `isAvailable` in both modes, linux/win32 throw-not-fallback, and `findAll` in fallback mode. The first time the `.mcpb`-installed bridge accesses a keychain entry on darwin, macOS will prompt for keychain access — operator clicks "Always Allow" once per entry and the prompt persists thereafter. This is a macOS keychain ACL property; the same prompt would have appeared with keytar-native if it had loaded.
|
|
14
|
+
|
|
15
|
+
### Internal
|
|
16
|
+
|
|
17
|
+
- **Test count:** `265 → 275` (+10 in `keychain-secret-store-darwin-fallback.test.js`).
|
|
18
|
+
- **Bundle size unchanged** (~420 kB packed, ~1.3 MB unpacked — the four platform-specific keytar binaries dominate; the secret-store code change is small relative to that).
|
|
19
|
+
|
|
20
|
+
### Known unverified — research outstanding for a follow-up release
|
|
21
|
+
|
|
22
|
+
- **Linux:** whether keytar's libsecret native binding loads cleanly inside Linux Claude Desktop's `.mcpb` runtime is unverified. Likely works (Linux's runtime model differs from macOS — no Team ID matching), but no Linux Claude Desktop access during this hotfix to test empirically. Operators on Linux Claude Desktop should test and report.
|
|
23
|
+
- **Windows:** whether keytar's win32 native binding loads cleanly inside Windows Claude Desktop's hardened process is unverified. If it doesn't, a separate Windows-specific fix shape is needed (PowerShell credential cmdlets, or formally limiting Windows operators to the CLI install path). Tracked for a follow-up release.
|
|
24
|
+
|
|
25
|
+
## [1.5.2] - 2026-05-03
|
|
26
|
+
|
|
27
|
+
**OAuth flow now works inside `.mcpb`.** This release makes the documented `.mcpb` operator UX work end-to-end: install the extension from Claude Desktop with an Application Password, then run `abilities-mcp upgrade-auth <site>` from a terminal to migrate that single connection to OAuth in place, then `abilities-mcp add-site https://other.com` to add more sites — all surfacing through the same Claude Desktop "Abilities MCP" entry. Before this release, the keytar binary wasn't bundled with the `.mcpb` and the `.mcpb` install never persisted to `~/.abilities-mcp/wp-sites.json`, so the documented progression failed at the moment OAuth touched the keychain.
|
|
28
|
+
|
|
29
|
+
Bridge-only release — no companion adapter or ai release this sprint.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`.mcpb` bundle now ships keytar prebuilds for darwin x64, darwin arm64, win32 x64, and linux x64** (PR [#35](https://github.com/Wicked-Evolutions/abilities-mcp/pull/35), closes [#33](https://github.com/Wicked-Evolutions/abilities-mcp/issues/33)). Without these, `KeychainSecretStore` failed at first request with `Cannot find module 'keytar'` even on the host platform — verified empirically against the v1.5.1 bundle. The pack pipeline moves from a single-line `mcpb pack` invocation to a staging-directory build (`scripts/pack-mcpb.js`) that fetches each platform's prebuild via `prebuild-install` and patches keytar's hardcoded single-slot loader (`var keytar = require('../build/Release/keytar.node')` in keytar 7.9.0) with a multi-platform-aware loader keyed on `process.platform`-`process.arch`. The patch is staging-only — `node_modules/keytar/` in the project tree is byte-identical pre-pack and post-pack, pinned by the new `scripts/verify-pack-isolation.js` (run via `npm run verify:pack-isolation`). Pre-patch substring assertion on the upstream loader fails loud if a future keytar bump changes the loader shape.
|
|
34
|
+
|
|
35
|
+
- **Bridge emits one operator-visible `Config source:` line on startup to stderr** (PR [#36](https://github.com/Wicked-Evolutions/abilities-mcp/pull/36), closes [#32](https://github.com/Wicked-Evolutions/abilities-mcp/issues/32)). Captured in Claude Desktop's per-server MCP log so the operator can tell at a glance which `loadConfig` source won. Names the source (`env-var` / `[explicit-config]` / `[script-adjacent]` / `[home-dir]` / `legacy-cli`), the file path (tildified) or hostname, the site count, and the per-site auth method. Sample output:
|
|
36
|
+
```
|
|
37
|
+
Config source: ABILITIES_MCP_URL env var (single-site basic auth: example.com as wp_user)
|
|
38
|
+
Config source: [home-dir] ~/.abilities-mcp/wp-sites.json (3 sites: helena oauth, wicked oauth, tnn apppassword)
|
|
39
|
+
```
|
|
40
|
+
No secrets — only IDs, methods, hostnames, paths, counts. Always-on, not gated by `--debug`.
|
|
41
|
+
|
|
42
|
+
- **`.mcpb` install seeds `~/.abilities-mcp/wp-sites.json` on first launch** (PR [#37](https://github.com/Wicked-Evolutions/abilities-mcp/pull/37), closes [#34](https://github.com/Wicked-Evolutions/abilities-mcp/issues/34)). When the env-var-mode bridge boots and the home-dir config doesn't exist, `seedFromEnvIfMissing` writes a v2 apppassword entry derived from the `ABILITIES_MCP_*` env vars before serving the first MCP request. `list-sites`, `upgrade-auth`, and `add-site` now operate on a single source of truth that already includes the `.mcpb`-installed site. The site-id is derived from the URL hostname, matching `add-site`'s `deriveSiteId`. Guards: pre-existing `wp-sites.json` is **never overwritten**; missing env vars / malformed URL / keytar unavailable → graceful no-op (bridge falls back to env-var-only mode); file-write failure → keychain entry rolled back so operators don't accumulate orphan secrets.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **keytar pinned `^7.9.0` → `~7.9.0`** (patch versions only) so the staging script's pre-patch loader-shape substring assertion has a stable target. CLI install behavior is unchanged — keytar stays in `optionalDependencies` (skips gracefully on platforms without prebuilds).
|
|
47
|
+
- **`_configSource` discriminant renamed `'env'` → `'env-var'`** to align with the documented set (`explicit-config`, `script-adjacent`, `home-dir`, `env-var`, `legacy-cli`). Internal field, prefixed with underscore; only one test was reading the prior value (updated).
|
|
48
|
+
|
|
49
|
+
### Internal
|
|
50
|
+
|
|
51
|
+
- **Bundle size:** `~115 kB → ~413 kB` packed / `~1.2 MB` unpacked. The four platform-specific keytar prebuilds (darwin-x64 ~83 kB, darwin-arm64 ~99 kB, win32-x64 ~707 kB, linux-x64 ~76 kB) are embedded for the `.mcpb` install path. CLI install paths are unaffected — keytar stays in `optionalDependencies` and is host-only via the operator's `npm install`.
|
|
52
|
+
- **Test count:** `237 → 265` (+28 across the sprint: 0 new in PR #35, +15 in PR #36, +13 in PR #37). Node CI matrix unchanged: 18, 20, 22.
|
|
53
|
+
- New maintenance scripts: `npm run pack:mcpb` (staging build + multi-platform prebuild fetch), `npm run verify:pack-isolation` (asserts `node_modules/keytar/` byte-identity across pack runs).
|
|
54
|
+
|
|
5
55
|
## [1.5.1] - 2026-05-02
|
|
6
56
|
|
|
7
57
|
Stretch-to-stable release. Closes the OAuth 2.1 alpha audit pass and the two integration-seam regressions surfaced during Helena's Phase B operator verification, plus the async-config tech-debt sweep that shares the bridge's startup path. No new features, no surface changes — the v1.5.x line is now stable for broader operator adoption.
|
package/abilities-mcp.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
const { createLogger } = require('./lib/logger');
|
|
26
26
|
const { loadConfig, buildSiteKeyEnum, resolveConfigFilePath } = require('./lib/config');
|
|
27
|
+
const { formatConfigSourceLine } = require('./lib/config-source-line');
|
|
27
28
|
const { ConnectionPool } = require('./lib/connection-pool');
|
|
28
29
|
const { ToolCatalog } = require('./lib/tool-catalog');
|
|
29
30
|
const { McpRouter } = require('./lib/router');
|
|
@@ -141,6 +142,13 @@ if (!isSubcommandInvocation) {
|
|
|
141
142
|
process.exit(1);
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
// Emit a single config-source line to stderr so operators can diagnose
|
|
146
|
+
// which mode the bridge is in at a glance (Claude Desktop's MCP log
|
|
147
|
+
// captures the server's stderr stream). Always-on, not gated by --debug:
|
|
148
|
+
// operator-visibility is the entire point of #32 and createLogger is a
|
|
149
|
+
// debug-only file logger that wouldn't reach Claude Desktop's log.
|
|
150
|
+
process.stderr.write(formatConfigSourceLine(config) + '\n');
|
|
151
|
+
|
|
144
152
|
const isMultiSite = config._isMultiSite;
|
|
145
153
|
const siteKeys = buildSiteKeyEnum(config);
|
|
146
154
|
log(`Config loaded: ${siteKeys.length} site(s): ${siteKeys.join(', ')} (default: ${config.defaultSite})`);
|
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { execFile } = require('node:child_process');
|
|
4
|
+
|
|
3
5
|
const { SecretStoreError } = require('./errors');
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
|
-
* KeychainSecretStore — keytar-backed SecretStore
|
|
8
|
+
* KeychainSecretStore — keytar-backed SecretStore with a darwin-only
|
|
9
|
+
* `security` CLI fallback for Claude Desktop's hardened-runtime barrier.
|
|
10
|
+
*
|
|
11
|
+
* keytar wraps macOS Keychain, Windows Credential Manager, and Linux libsecret
|
|
12
|
+
* via a native binding (`build/Release/keytar.node`). It is declared as an
|
|
13
|
+
* `optionalDependency` so a failed native build does not break `npm install`
|
|
14
|
+
* for env-var-only operators.
|
|
15
|
+
*
|
|
16
|
+
* **The darwin fallback (issue #39).** When the bundled keytar binary fails
|
|
17
|
+
* to dlopen inside Claude Desktop's hardened-runtime process — the macOS
|
|
18
|
+
* code-signing rejects native binaries with mismatched Team IDs, Anthropic-
|
|
19
|
+
* signed Claude Desktop refuses to load npm-distribution-signed keytar.node —
|
|
20
|
+
* we fall back to shelling out to the macOS `security` CLI via
|
|
21
|
+
* `child_process.execFile`. `security` is always installed on macOS, doesn't
|
|
22
|
+
* require dynamic native loading, operates against the same macOS Keychain,
|
|
23
|
+
* and runs as a child process out from under Claude Desktop's hardened-
|
|
24
|
+
* runtime restrictions. The fallback fires only on darwin; on linux/win32 a
|
|
25
|
+
* keytar load failure still throws `keytar_unavailable` (current behavior).
|
|
26
|
+
*
|
|
27
|
+
* Outside the .mcpb path — system Node, CLI install, npx, source clone —
|
|
28
|
+
* keytar loads normally and the fallback never engages.
|
|
7
29
|
*
|
|
8
|
-
* keytar
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `keytar_unavailable` — callers can detect that and fall back to a different
|
|
13
|
-
* store (e.g. MemorySecretStore for tests, or surface to the user).
|
|
30
|
+
* If keytar is unavailable at runtime AND the darwin fallback also can't run,
|
|
31
|
+
* every method throws `SecretStoreError` with code `keytar_unavailable` —
|
|
32
|
+
* callers can detect that and fall back to a different store (e.g.
|
|
33
|
+
* MemorySecretStore for tests, or surface to the user).
|
|
14
34
|
*
|
|
15
35
|
* Implements the SecretStore interface defined in `secret-store.js`.
|
|
16
36
|
*
|
|
@@ -21,38 +41,67 @@ const { SecretStoreError } = require('./errors');
|
|
|
21
41
|
class KeychainSecretStore {
|
|
22
42
|
/**
|
|
23
43
|
* @param {object} [opts]
|
|
24
|
-
* @param {object} [opts.keytar]
|
|
25
|
-
*
|
|
26
|
-
*
|
|
44
|
+
* @param {object} [opts.keytar] Inject a keytar module — primarily for tests.
|
|
45
|
+
* When omitted, keytar is required lazily on
|
|
46
|
+
* first use.
|
|
47
|
+
* @param {Function} [opts.requireKeytar] Override the require call used to load
|
|
48
|
+
* keytar. Test seam: pass a function that
|
|
49
|
+
* throws to simulate the .mcpb-path dlopen
|
|
50
|
+
* rejection without breaking the real
|
|
51
|
+
* require('keytar') in the test runtime.
|
|
52
|
+
* @param {string} [opts.platform] Override `process.platform` for the
|
|
53
|
+
* fallback-eligibility decision. Test seam.
|
|
54
|
+
* @param {Function} [opts.exec] Override `child_process.execFile`. Test
|
|
55
|
+
* seam for the security-CLI fallback path.
|
|
27
56
|
*/
|
|
28
57
|
constructor(opts = {}) {
|
|
29
58
|
this._injected = opts.keytar || null;
|
|
30
59
|
this._keytar = null;
|
|
31
60
|
this._loadAttempted = false;
|
|
32
61
|
this._loadError = null;
|
|
62
|
+
this._fallbackMode = null; // null | 'security-cli'
|
|
63
|
+
|
|
64
|
+
this._requireKeytar = opts.requireKeytar || ((id) => require(id));
|
|
65
|
+
this._platform = opts.platform || process.platform;
|
|
66
|
+
this._exec = opts.exec || execFile;
|
|
33
67
|
}
|
|
34
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Lazy load. Sets one of three terminal states:
|
|
71
|
+
* - `this._keytar` populated (keytar loaded normally; primary path)
|
|
72
|
+
* - `this._fallbackMode === 'security-cli'` (darwin fallback engaged)
|
|
73
|
+
* - `this._loadError` set + throws (non-darwin keytar failure)
|
|
74
|
+
*/
|
|
35
75
|
_load() {
|
|
36
|
-
if (this._keytar
|
|
76
|
+
if (this._keytar || this._fallbackMode) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
37
79
|
if (this._loadAttempted) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
return this._keytar;
|
|
80
|
+
// Previously failed and we cached the error.
|
|
81
|
+
throw new SecretStoreError(
|
|
82
|
+
`OS keychain unavailable: ${this._loadError.message}`,
|
|
83
|
+
{ code: 'keytar_unavailable', cause: this._loadError }
|
|
84
|
+
);
|
|
45
85
|
}
|
|
46
86
|
this._loadAttempted = true;
|
|
87
|
+
|
|
47
88
|
if (this._injected) {
|
|
48
89
|
this._keytar = this._injected;
|
|
49
|
-
return
|
|
90
|
+
return;
|
|
50
91
|
}
|
|
92
|
+
|
|
51
93
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return this._keytar;
|
|
94
|
+
this._keytar = this._requireKeytar('keytar');
|
|
95
|
+
return;
|
|
55
96
|
} catch (err) {
|
|
97
|
+
// darwin: Claude Desktop's hardened-runtime rejects bundled keytar.node
|
|
98
|
+
// with a Team ID mismatch (issue #39). Fall back to the `security` CLI
|
|
99
|
+
// rather than throwing — the bridge keeps working against the same
|
|
100
|
+
// macOS Keychain, just via shell-out.
|
|
101
|
+
if (this._platform === 'darwin') {
|
|
102
|
+
this._fallbackMode = 'security-cli';
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
56
105
|
this._loadError = err;
|
|
57
106
|
throw new SecretStoreError(
|
|
58
107
|
`OS keychain unavailable: ${err.message}`,
|
|
@@ -61,7 +110,10 @@ class KeychainSecretStore {
|
|
|
61
110
|
}
|
|
62
111
|
}
|
|
63
112
|
|
|
64
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* @returns {Promise<boolean>} true if keytar can be loaded on this host OR
|
|
115
|
+
* the darwin security-CLI fallback is engaged.
|
|
116
|
+
*/
|
|
65
117
|
async isAvailable() {
|
|
66
118
|
try {
|
|
67
119
|
this._load();
|
|
@@ -72,27 +124,142 @@ class KeychainSecretStore {
|
|
|
72
124
|
}
|
|
73
125
|
|
|
74
126
|
async get(service, account) {
|
|
75
|
-
|
|
76
|
-
|
|
127
|
+
this._load();
|
|
128
|
+
if (this._fallbackMode === 'security-cli') {
|
|
129
|
+
return this._securityGet(service, account);
|
|
130
|
+
}
|
|
131
|
+
return this._keytar.getPassword(service, account);
|
|
77
132
|
}
|
|
78
133
|
|
|
79
134
|
async set(service, account, secret) {
|
|
80
135
|
if (typeof secret !== 'string') {
|
|
81
136
|
throw new TypeError('SecretStore.set: secret must be a string');
|
|
82
137
|
}
|
|
83
|
-
|
|
84
|
-
|
|
138
|
+
this._load();
|
|
139
|
+
if (this._fallbackMode === 'security-cli') {
|
|
140
|
+
return this._securitySet(service, account, secret);
|
|
141
|
+
}
|
|
142
|
+
await this._keytar.setPassword(service, account, secret);
|
|
85
143
|
}
|
|
86
144
|
|
|
87
145
|
async delete(service, account) {
|
|
88
|
-
|
|
89
|
-
|
|
146
|
+
this._load();
|
|
147
|
+
if (this._fallbackMode === 'security-cli') {
|
|
148
|
+
return this._securityDelete(service, account);
|
|
149
|
+
}
|
|
150
|
+
return this._keytar.deletePassword(service, account);
|
|
90
151
|
}
|
|
91
152
|
|
|
92
153
|
async findAll(service) {
|
|
93
|
-
|
|
94
|
-
|
|
154
|
+
this._load();
|
|
155
|
+
if (this._fallbackMode === 'security-cli') {
|
|
156
|
+
// The macOS `security` CLI has no clean enumerate-by-service mode.
|
|
157
|
+
// Returning [] here is safe because the bridge runtime path never
|
|
158
|
+
// calls findAll — only the CLI subcommand `list-sites` does, and that
|
|
159
|
+
// runs in system Node where keytar loads normally and this branch
|
|
160
|
+
// is never taken. Documented in the issue body's "findAll" note.
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
return this._keytar.findCredentials(service);
|
|
95
164
|
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------
|
|
167
|
+
// darwin `security` CLI fallback — internal helpers.
|
|
168
|
+
// ---------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Run the `security` CLI with the given args. Returns { stdout, stderr }
|
|
172
|
+
* on success, rejects with an error carrying `.stderr` / `.stdout` /
|
|
173
|
+
* `.code` (exit code) on failure. Uses `execFile` (not `exec`) so args
|
|
174
|
+
* are not shell-interpreted — the password / account / service strings
|
|
175
|
+
* pass through verbatim, no shell injection surface.
|
|
176
|
+
*/
|
|
177
|
+
_execSecurity(args) {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
this._exec('security', args, {}, (err, stdout, stderr) => {
|
|
180
|
+
const stdoutStr = typeof stdout === 'string'
|
|
181
|
+
? stdout
|
|
182
|
+
: (stdout ? stdout.toString() : '');
|
|
183
|
+
const stderrStr = typeof stderr === 'string'
|
|
184
|
+
? stderr
|
|
185
|
+
: (stderr ? stderr.toString() : '');
|
|
186
|
+
if (err) {
|
|
187
|
+
// Real child_process.execFile populates err.stderr / err.stdout
|
|
188
|
+
// and ALSO passes them as cb args. Preserve whichever the caller
|
|
189
|
+
// already attached (some test doubles attach to the err object
|
|
190
|
+
// and don't pass via cb args); fall back to the cb args otherwise.
|
|
191
|
+
if (typeof err.stderr !== 'string') err.stderr = stderrStr;
|
|
192
|
+
if (typeof err.stdout !== 'string') err.stdout = stdoutStr;
|
|
193
|
+
return reject(err);
|
|
194
|
+
}
|
|
195
|
+
resolve({ stdout: stdoutStr, stderr: stderrStr });
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async _securityGet(service, account) {
|
|
201
|
+
try {
|
|
202
|
+
const { stdout } = await this._execSecurity([
|
|
203
|
+
'find-generic-password', '-s', service, '-a', account, '-w',
|
|
204
|
+
]);
|
|
205
|
+
// -w prints just the password to stdout, terminated by a newline.
|
|
206
|
+
return stdout.replace(/\n$/, '');
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (_isNotFound(err)) return null;
|
|
209
|
+
throw new SecretStoreError(
|
|
210
|
+
`security find-generic-password failed: ${(err.stderr || err.message || '').trim()}`,
|
|
211
|
+
{ code: 'security_cli_failed', cause: err }
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async _securitySet(service, account, secret) {
|
|
217
|
+
// -U updates the existing entry if present, adds it otherwise.
|
|
218
|
+
// Note: passing the password as the last argv element is the standard
|
|
219
|
+
// pattern for non-interactive `security` use; the macOS `security` CLI
|
|
220
|
+
// exposes no stdin-only password input mode for non-interactive callers.
|
|
221
|
+
// This is the same trade-off keytar's own native binding makes — the
|
|
222
|
+
// password lives in process memory until the syscall completes.
|
|
223
|
+
try {
|
|
224
|
+
await this._execSecurity([
|
|
225
|
+
'add-generic-password', '-U', '-s', service, '-a', account, '-w', secret,
|
|
226
|
+
]);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
throw new SecretStoreError(
|
|
229
|
+
`security add-generic-password failed: ${(err.stderr || err.message || '').trim()}`,
|
|
230
|
+
{ code: 'security_cli_failed', cause: err }
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async _securityDelete(service, account) {
|
|
236
|
+
try {
|
|
237
|
+
await this._execSecurity([
|
|
238
|
+
'delete-generic-password', '-s', service, '-a', account,
|
|
239
|
+
]);
|
|
240
|
+
return true;
|
|
241
|
+
} catch (err) {
|
|
242
|
+
if (_isNotFound(err)) return false;
|
|
243
|
+
throw new SecretStoreError(
|
|
244
|
+
`security delete-generic-password failed: ${(err.stderr || err.message || '').trim()}`,
|
|
245
|
+
{ code: 'security_cli_failed', cause: err }
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Detect the macOS `security` CLI's "entry not found" condition. Stderr from
|
|
253
|
+
* `find-generic-password` / `delete-generic-password` against a missing entry
|
|
254
|
+
* looks like:
|
|
255
|
+
* security: SecKeychainSearchCopyNext: The specified item could not be
|
|
256
|
+
* found in the keychain.
|
|
257
|
+
* Match on the substring "could not be found" (case-insensitive) to map it
|
|
258
|
+
* to keytar's null/false return semantics.
|
|
259
|
+
*/
|
|
260
|
+
function _isNotFound(err) {
|
|
261
|
+
const stderr = (err && err.stderr) || '';
|
|
262
|
+
return /could not be found/i.test(stderr);
|
|
96
263
|
}
|
|
97
264
|
|
|
98
265
|
module.exports = { KeychainSecretStore };
|
package/lib/cli/config-store.js
CHANGED
|
@@ -6,8 +6,12 @@ const os = require('node:os');
|
|
|
6
6
|
|
|
7
7
|
const { SCHEMA_VERSION, validate, emptyConfig } = require('../auth/schema-v2');
|
|
8
8
|
const { _atomicWrite } = require('../auth/config-migration');
|
|
9
|
+
const { AUTH_STATUS } = require('../auth/events');
|
|
10
|
+
const { makeRef } = require('../auth/secret-store');
|
|
9
11
|
const { CliError, EXIT_CONFIG } = require('./errors');
|
|
10
12
|
|
|
13
|
+
const SECRET_SERVICE = 'abilities-mcp';
|
|
14
|
+
|
|
11
15
|
/**
|
|
12
16
|
* Read / write the v2 wp-sites.json file from a CLI command.
|
|
13
17
|
*
|
|
@@ -152,10 +156,173 @@ function freshConfig() {
|
|
|
152
156
|
return emptyConfig();
|
|
153
157
|
}
|
|
154
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Derive a site-id from a URL hostname. Mirrors `add-site`'s deriveSiteId so a
|
|
161
|
+
* `.mcpb`-seeded site collides with a CLI-added entry for the same host (the
|
|
162
|
+
* file-absence guard in `seedFromEnvIfMissing` prevents the collision in
|
|
163
|
+
* practice; the parity matters for `upgrade-auth <site-id>` to be intuitive).
|
|
164
|
+
*
|
|
165
|
+
* @param {string} siteUrl
|
|
166
|
+
* @returns {string|null} Site-id or null if URL is unparseable.
|
|
167
|
+
*/
|
|
168
|
+
function deriveSiteId(siteUrl) {
|
|
169
|
+
let host;
|
|
170
|
+
try { host = new URL(siteUrl).hostname; }
|
|
171
|
+
catch { return null; }
|
|
172
|
+
const trimmed = host.replace(/^www\./, '');
|
|
173
|
+
const dot = trimmed.indexOf('.');
|
|
174
|
+
return dot > 0 ? trimmed.slice(0, dot) : trimmed;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Seed wp-sites.json from env vars (`ABILITIES_MCP_URL/USERNAME/PASSWORD`)
|
|
179
|
+
* when the file doesn't yet exist. Used on first launch of the `.mcpb`
|
|
180
|
+
* extension so subsequent CLI commands (`list-sites`, `upgrade-auth`,
|
|
181
|
+
* `add-site`) operate on a single source of truth that already includes
|
|
182
|
+
* the site Claude Desktop is connected to.
|
|
183
|
+
*
|
|
184
|
+
* Behavior:
|
|
185
|
+
* - If `configPath` already exists → no-op. Operators who manage their own
|
|
186
|
+
* `wp-sites.json` are never overwritten.
|
|
187
|
+
* - If keytar isn't loadable on this host (e.g. the .mcpb is somehow
|
|
188
|
+
* running without the bundled keytar prebuild) → no-op. The bridge
|
|
189
|
+
* falls back to env-var-only mode. Graceful degradation.
|
|
190
|
+
* - If any of the three env vars is missing → no-op. Should not happen
|
|
191
|
+
* in the .mcpb path (manifest user_config marks all three required) but
|
|
192
|
+
* guards against partial env in other invocations.
|
|
193
|
+
* - Otherwise: writes the App Password to keychain via the shared
|
|
194
|
+
* SecretStore, builds a v2 apppassword entry shaped to pass both the
|
|
195
|
+
* schema-v2 validator and the bridge's runtime validateSiteConfig
|
|
196
|
+
* (matching the migration `_convertSite` pattern — preserves
|
|
197
|
+
* `transport: 'http'` and the legacy http block alongside the v2 auth
|
|
198
|
+
* block, with `password_ref` in both).
|
|
199
|
+
*
|
|
200
|
+
* If the keychain write succeeds but the file write fails the keychain
|
|
201
|
+
* entry is rolled back so the operator's keychain doesn't accumulate
|
|
202
|
+
* orphans on repeated failures.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} configPath Absolute path of the wp-sites.json to seed.
|
|
205
|
+
* @param {object} env Environment shape — expects ABILITIES_MCP_URL,
|
|
206
|
+
* ABILITIES_MCP_USERNAME, ABILITIES_MCP_PASSWORD.
|
|
207
|
+
* Defaults to process.env.
|
|
208
|
+
* @param {object} [deps]
|
|
209
|
+
* @param {object} [deps.secretStore] Inject for tests (a MemorySecretStore).
|
|
210
|
+
* Defaults to a fresh KeychainSecretStore.
|
|
211
|
+
* @returns {Promise<{
|
|
212
|
+
* seeded: boolean,
|
|
213
|
+
* reason?: 'exists'|'missing-env-vars'|'keytar-unavailable'|'invalid-url'|'error',
|
|
214
|
+
* siteId?: string,
|
|
215
|
+
* configPath?: string,
|
|
216
|
+
* error?: Error,
|
|
217
|
+
* }>}
|
|
218
|
+
*/
|
|
219
|
+
async function seedFromEnvIfMissing(configPath, env, deps = {}) {
|
|
220
|
+
if (!configPath) {
|
|
221
|
+
return { seeded: false, reason: 'missing-env-vars' };
|
|
222
|
+
}
|
|
223
|
+
if (fs.existsSync(configPath)) {
|
|
224
|
+
return { seeded: false, reason: 'exists' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const url = env && env.ABILITIES_MCP_URL;
|
|
228
|
+
const username = env && env.ABILITIES_MCP_USERNAME;
|
|
229
|
+
const password = env && env.ABILITIES_MCP_PASSWORD;
|
|
230
|
+
if (!url || !username || !password) {
|
|
231
|
+
return { seeded: false, reason: 'missing-env-vars' };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let parsedUrl;
|
|
235
|
+
try { parsedUrl = new URL(url); }
|
|
236
|
+
catch { return { seeded: false, reason: 'invalid-url' }; }
|
|
237
|
+
|
|
238
|
+
const siteId = deriveSiteId(url);
|
|
239
|
+
if (!siteId) {
|
|
240
|
+
return { seeded: false, reason: 'invalid-url' };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Lazily build a SecretStore so SSH-only / env-var-only setups never load
|
|
244
|
+
// keytar on the seed path — the no-op "missing env vars" exit above keeps
|
|
245
|
+
// them out, but keep the require deferred for symmetry with the runtime.
|
|
246
|
+
let secretStore = deps.secretStore;
|
|
247
|
+
if (!secretStore) {
|
|
248
|
+
const { KeychainSecretStore } = require('../auth/keychain-secret-store');
|
|
249
|
+
secretStore = new KeychainSecretStore();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Probe keytar before writing. If it isn't loadable (e.g. the .mcpb
|
|
253
|
+
// somehow shipped without the bundled binary) we skip seeding — the
|
|
254
|
+
// bridge keeps working in env-var-only mode and the operator can run
|
|
255
|
+
// `abilities-mcp add-site` from a CLI install instead.
|
|
256
|
+
if (typeof secretStore.isAvailable === 'function') {
|
|
257
|
+
const available = await secretStore.isAvailable();
|
|
258
|
+
if (!available) {
|
|
259
|
+
return { seeded: false, reason: 'keytar-unavailable' };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Build the endpoint the same way buildEnvConfig does — strip trailing
|
|
264
|
+
// slash, append the adapter route. This is the URL the runtime will hit
|
|
265
|
+
// for App-Password requests.
|
|
266
|
+
const base = (parsedUrl.origin + parsedUrl.pathname).replace(/\/+$/, '');
|
|
267
|
+
const endpoint = `${base}/wp-json/mcp/mcp-adapter-default-server`;
|
|
268
|
+
const account = `${siteId}/apppassword`;
|
|
269
|
+
const passwordRef = makeRef(SECRET_SERVICE, account);
|
|
270
|
+
|
|
271
|
+
// Write the secret to keychain first. If the file write below fails we
|
|
272
|
+
// roll this back so the keychain doesn't accumulate orphan entries on
|
|
273
|
+
// repeated seed attempts.
|
|
274
|
+
try {
|
|
275
|
+
await secretStore.set(SECRET_SERVICE, account, password);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
return { seeded: false, reason: 'error', error: err };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const allowInsecure = parsedUrl.protocol === 'http:';
|
|
281
|
+
const site = {
|
|
282
|
+
label: parsedUrl.hostname,
|
|
283
|
+
url: parsedUrl.origin,
|
|
284
|
+
transport: 'http',
|
|
285
|
+
http: {
|
|
286
|
+
endpoint,
|
|
287
|
+
username,
|
|
288
|
+
password_ref: passwordRef,
|
|
289
|
+
},
|
|
290
|
+
auth: {
|
|
291
|
+
method: 'apppassword',
|
|
292
|
+
username,
|
|
293
|
+
password_ref: passwordRef,
|
|
294
|
+
},
|
|
295
|
+
auth_status: AUTH_STATUS.ACTIVE,
|
|
296
|
+
};
|
|
297
|
+
if (allowInsecure) site.allowInsecure = true;
|
|
298
|
+
|
|
299
|
+
const v2Config = {
|
|
300
|
+
$schema: 'https://wickedevolutions.com/schemas/abilities-mcp/wp-sites/v2.json',
|
|
301
|
+
schema_version: SCHEMA_VERSION,
|
|
302
|
+
defaultSite: siteId,
|
|
303
|
+
sites: { [siteId]: site },
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const dir = path.dirname(configPath);
|
|
308
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
309
|
+
await _atomicWrite(configPath, v2Config);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
// Roll back the keychain write so we don't leave an orphan secret.
|
|
312
|
+
try { await secretStore.delete(SECRET_SERVICE, account); }
|
|
313
|
+
catch { /* best-effort rollback */ }
|
|
314
|
+
return { seeded: false, reason: 'error', error: err };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { seeded: true, siteId, configPath };
|
|
318
|
+
}
|
|
319
|
+
|
|
155
320
|
module.exports = {
|
|
156
321
|
resolveConfigPath,
|
|
157
322
|
readConfig,
|
|
158
323
|
writeConfig,
|
|
159
324
|
freshConfig,
|
|
325
|
+
seedFromEnvIfMissing,
|
|
326
|
+
deriveSiteId,
|
|
160
327
|
HOME_DIR_REL,
|
|
161
328
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format the operator-visible startup diagnostic line that names which config
|
|
7
|
+
* source `loadConfig` resolved to and what's in it.
|
|
8
|
+
*
|
|
9
|
+
* Output goes to stderr where Claude Desktop's MCP log captures it
|
|
10
|
+
* (visible in `mcp-server-WordPress (Abilities MCP).log` on macOS), so the
|
|
11
|
+
* operator can tell at a glance:
|
|
12
|
+
* - Whether the .mcpb extension is in env-var single-site mode or has
|
|
13
|
+
* handed off to a home-dir wp-sites.json.
|
|
14
|
+
* - How many sites are configured and what auth method each uses.
|
|
15
|
+
* - Which file path or env var is the source of truth right now.
|
|
16
|
+
*
|
|
17
|
+
* Discriminants set in `lib/config.js`:
|
|
18
|
+
* - 'explicit-config' — args.config / --config=<path>
|
|
19
|
+
* - 'script-adjacent' — wp-sites.json next to abilities-mcp.js
|
|
20
|
+
* - 'home-dir' — ~/.abilities-mcp/wp-sites.json
|
|
21
|
+
* - 'env-var' — ABILITIES_MCP_URL injected by Claude Desktop user_config
|
|
22
|
+
* - 'legacy-cli' — --host / --path (mcp-ssh-bridge backward compat)
|
|
23
|
+
*
|
|
24
|
+
* The line never includes secrets — only site IDs, auth methods, hostnames,
|
|
25
|
+
* tildified file paths, and counts.
|
|
26
|
+
*
|
|
27
|
+
* Copyright (C) 2026 Influencentricity | Wicked Evolutions
|
|
28
|
+
* @license GPL-2.0-or-later
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Replace a leading $HOME prefix with `~/` so logs don't leak the operator's
|
|
33
|
+
* full username path. No-op for paths outside $HOME.
|
|
34
|
+
*/
|
|
35
|
+
function tildify(p) {
|
|
36
|
+
if (!p) return p;
|
|
37
|
+
const home = os.homedir();
|
|
38
|
+
if (home && (p === home || p.startsWith(home + '/'))) {
|
|
39
|
+
return '~' + p.slice(home.length);
|
|
40
|
+
}
|
|
41
|
+
return p;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Per-site short auth label: prefer auth.method (v2 schema), fall back to
|
|
46
|
+
* transport (v1 schema), 'unknown' if neither is set.
|
|
47
|
+
*/
|
|
48
|
+
function siteAuthLabel(site) {
|
|
49
|
+
if (site && site.auth && site.auth.method) return site.auth.method;
|
|
50
|
+
if (site && site.transport) return site.transport;
|
|
51
|
+
return 'unknown';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build the Config-source line.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} config Output of `loadConfig` — must carry `_configSource`
|
|
58
|
+
* and `_configSourceLabel`.
|
|
59
|
+
* @returns {string} One-line operator diagnostic, no trailing newline.
|
|
60
|
+
*/
|
|
61
|
+
function formatConfigSourceLine(config) {
|
|
62
|
+
const source = config && config._configSource;
|
|
63
|
+
const rawLabel = (config && config._configSourceLabel) || '';
|
|
64
|
+
|
|
65
|
+
if (source === 'env-var') {
|
|
66
|
+
const site = config.sites && config.sites[config.defaultSite];
|
|
67
|
+
const username = (site && site.http && site.http.username) || '?';
|
|
68
|
+
return `Config source: ABILITIES_MCP_URL env var (single-site basic auth: ${rawLabel} as ${username})`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (source === 'legacy-cli') {
|
|
72
|
+
return `Config source: --host/--path legacy CLI (single-site SSH: ${rawLabel})`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// File-based: explicit-config / script-adjacent / home-dir
|
|
76
|
+
const label = tildify(rawLabel);
|
|
77
|
+
const siteEntries = Object.entries(config.sites || {}).map(
|
|
78
|
+
([id, site]) => `${id} ${siteAuthLabel(site)}`
|
|
79
|
+
);
|
|
80
|
+
const sitesHeader = siteEntries.length === 1 ? '1 site' : `${siteEntries.length} sites`;
|
|
81
|
+
const sourcePrefix = source ? `[${source}] ` : '';
|
|
82
|
+
return `Config source: ${sourcePrefix}${label} (${sitesHeader}: ${siteEntries.join(', ')})`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { formatConfigSourceLine, tildify, siteAuthLabel };
|
package/lib/config.js
CHANGED
|
@@ -75,25 +75,39 @@ async function resolveConfigFilePath(args) {
|
|
|
75
75
|
async function loadConfig(args) {
|
|
76
76
|
// Explicit config path
|
|
77
77
|
if (args.config) {
|
|
78
|
-
return loadConfigFile(args.config);
|
|
78
|
+
return loadConfigFile(args.config, 'explicit-config');
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// Check alongside script (lib/ → package root)
|
|
82
82
|
const scriptDir = path.resolve(__dirname, '..');
|
|
83
83
|
const scriptConfig = path.join(scriptDir, 'wp-sites.json');
|
|
84
84
|
if (await _exists(scriptConfig)) {
|
|
85
|
-
return loadConfigFile(scriptConfig);
|
|
85
|
+
return loadConfigFile(scriptConfig, 'script-adjacent');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// Check home directory
|
|
89
89
|
const homeConfig = path.join(os.homedir(), '.abilities-mcp', 'wp-sites.json');
|
|
90
90
|
if (await _exists(homeConfig)) {
|
|
91
|
-
return loadConfigFile(homeConfig);
|
|
91
|
+
return loadConfigFile(homeConfig, 'home-dir');
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// Env-var single-site config — covers the .mcpb install path and any
|
|
95
95
|
// env-var-based MCP client configuration (claude mcp add, Docker, etc.)
|
|
96
|
+
//
|
|
97
|
+
// First-launch seed (#34): if the .mcpb just installed and no home-dir
|
|
98
|
+
// wp-sites.json exists yet, seed one from the env vars so subsequent CLI
|
|
99
|
+
// commands (`list-sites`, `upgrade-auth`, `add-site`) operate on a single
|
|
100
|
+
// source of truth that already includes the site Claude Desktop is
|
|
101
|
+
// connected to. On success the bridge loads the freshly seeded file —
|
|
102
|
+
// same shape it would read on next restart — so the runtime path stays
|
|
103
|
+
// identical regardless of whether seeding just happened. On failure
|
|
104
|
+
// (keytar unavailable, file-already-exists, write error) the seed is a
|
|
105
|
+
// graceful no-op and we fall back to env-var-only mode below.
|
|
96
106
|
if (process.env.ABILITIES_MCP_URL) {
|
|
107
|
+
const seedResult = await _seedFromEnvIfMissing(homeConfig, process.env);
|
|
108
|
+
if (seedResult && seedResult.seeded) {
|
|
109
|
+
return loadConfigFile(homeConfig, 'home-dir');
|
|
110
|
+
}
|
|
97
111
|
return buildEnvConfig(process.env);
|
|
98
112
|
}
|
|
99
113
|
|
|
@@ -173,14 +187,15 @@ function buildEnvConfig(env) {
|
|
|
173
187
|
return {
|
|
174
188
|
defaultSite: 'default',
|
|
175
189
|
_isMultiSite: false,
|
|
176
|
-
_configSource: 'env',
|
|
190
|
+
_configSource: 'env-var',
|
|
191
|
+
_configSourceLabel: parsedUrl.hostname,
|
|
177
192
|
sites: {
|
|
178
193
|
default: siteConfig,
|
|
179
194
|
},
|
|
180
195
|
};
|
|
181
196
|
}
|
|
182
197
|
|
|
183
|
-
async function loadConfigFile(filePath) {
|
|
198
|
+
async function loadConfigFile(filePath, source = 'explicit-config') {
|
|
184
199
|
const raw = await fsp.readFile(filePath, 'utf8');
|
|
185
200
|
|
|
186
201
|
// Warn if config file is readable by group or world
|
|
@@ -216,6 +231,8 @@ async function loadConfigFile(filePath) {
|
|
|
216
231
|
config._isMultiSite = Object.keys(config.sites).length > 1 ||
|
|
217
232
|
Object.values(config.sites).some(s => s.multisite);
|
|
218
233
|
config._configPath = filePath;
|
|
234
|
+
config._configSource = source;
|
|
235
|
+
config._configSourceLabel = filePath;
|
|
219
236
|
|
|
220
237
|
return config;
|
|
221
238
|
}
|
|
@@ -301,6 +318,8 @@ function buildLegacyConfig(args) {
|
|
|
301
318
|
return {
|
|
302
319
|
defaultSite: 'default',
|
|
303
320
|
_isMultiSite: false,
|
|
321
|
+
_configSource: 'legacy-cli',
|
|
322
|
+
_configSourceLabel: args.host,
|
|
304
323
|
sites: {
|
|
305
324
|
default: {
|
|
306
325
|
label: args.host,
|
|
@@ -428,6 +447,17 @@ async function resolveSitePassword(site, secretStore) {
|
|
|
428
447
|
throw new Error('No password source configured for site');
|
|
429
448
|
}
|
|
430
449
|
|
|
450
|
+
/**
|
|
451
|
+
* Lazy wrapper around `lib/cli/config-store.js#seedFromEnvIfMissing`. Only
|
|
452
|
+
* loads the CLI config-store + KeychainSecretStore modules when the env-var
|
|
453
|
+
* branch of `loadConfig` actually fires — so SSH-only, explicit-config, and
|
|
454
|
+
* legacy-CLI install paths never pay the keytar import cost.
|
|
455
|
+
*/
|
|
456
|
+
async function _seedFromEnvIfMissing(configPath, env) {
|
|
457
|
+
const { seedFromEnvIfMissing } = require('./cli/config-store');
|
|
458
|
+
return seedFromEnvIfMissing(configPath, env);
|
|
459
|
+
}
|
|
460
|
+
|
|
431
461
|
module.exports = {
|
|
432
462
|
loadConfig,
|
|
433
463
|
resolveConfigFilePath,
|
|
@@ -436,4 +466,5 @@ module.exports = {
|
|
|
436
466
|
resolveSiteKey,
|
|
437
467
|
buildSiteKeyEnum,
|
|
438
468
|
buildEnvConfig,
|
|
469
|
+
validateSiteConfig,
|
|
439
470
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wickedevolutions/abilities-mcp",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
4
4
|
"description": "Open-source MCP bridge connecting AI clients to WordPress through the Abilities API — multi-site routing, zero dependencies",
|
|
5
5
|
"main": "abilities-mcp.js",
|
|
6
6
|
"bin": {
|
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"test": "node --test test/*.test.js test/auth/*.test.js test/cli/*.test.js test/transports/*.test.js",
|
|
19
19
|
"validate:mcpb": "npx --yes @anthropic-ai/mcpb validate manifest.json",
|
|
20
|
-
"pack:mcpb": "
|
|
20
|
+
"pack:mcpb": "node scripts/pack-mcpb.js",
|
|
21
|
+
"verify:pack-isolation": "node scripts/verify-pack-isolation.js"
|
|
21
22
|
},
|
|
22
23
|
"engines": {
|
|
23
24
|
"node": ">=18.0.0"
|
|
24
25
|
},
|
|
25
26
|
"optionalDependencies": {
|
|
26
|
-
"keytar": "
|
|
27
|
+
"keytar": "~7.9.0"
|
|
27
28
|
}
|
|
28
29
|
}
|