@wickedevolutions/abilities-mcp 1.3.1 → 1.5.1
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 +61 -0
- package/README.md +88 -17
- package/abilities-mcp.js +182 -113
- package/lib/auth/bridge-identity-provider.js +34 -0
- package/lib/auth/browser-launcher.js +67 -0
- package/lib/auth/config-migration.js +322 -0
- package/lib/auth/dcr-client.js +123 -0
- package/lib/auth/discovery-client.js +273 -0
- package/lib/auth/errors.js +114 -0
- package/lib/auth/events.js +55 -0
- package/lib/auth/fresh-each-time-identity.js +101 -0
- package/lib/auth/http-json.js +151 -0
- package/lib/auth/index.js +88 -0
- package/lib/auth/keychain-secret-store.js +98 -0
- package/lib/auth/loopback-server.js +249 -0
- package/lib/auth/memory-secret-store.js +0 -0
- package/lib/auth/oauth-client.js +357 -0
- package/lib/auth/pkce.js +93 -0
- package/lib/auth/schema-v2.js +110 -0
- package/lib/auth/secret-store.js +78 -0
- package/lib/auth/token-manager.js +378 -0
- package/lib/cli/commands/add-site.js +226 -0
- package/lib/cli/commands/force-downgrade.js +93 -0
- package/lib/cli/commands/list-sites.js +93 -0
- package/lib/cli/commands/reauth.js +108 -0
- package/lib/cli/commands/revoke.js +127 -0
- package/lib/cli/commands/self-check.js +158 -0
- package/lib/cli/commands/test.js +174 -0
- package/lib/cli/commands/upgrade-auth.js +259 -0
- package/lib/cli/config-store.js +161 -0
- package/lib/cli/context.js +102 -0
- package/lib/cli/errors.js +227 -0
- package/lib/cli/index.js +173 -0
- package/lib/cli/output.js +175 -0
- package/lib/cli/parse-args.js +80 -0
- package/lib/config.js +248 -19
- package/lib/connection-pool.js +214 -11
- package/lib/router.js +29 -11
- package/lib/transports/oauth-http-transport.js +601 -0
- package/package.json +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,67 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Abilities MCP are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.5.1] - 2026-05-02
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
Companion releases: [abilities-mcp-adapter v1.4.4](https://github.com/Wicked-Evolutions/abilities-mcp-adapter/releases/tag/v1.4.4) and [abilities-for-ai v1.9.1](https://github.com/Wicked-Evolutions/abilities-for-ai/releases/tag/v1.9.1) — coordinated multi-repo release per the Stretch to Stable sprint plan.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Schema v1→v2 auto-migration is now actually wired into boot** (PR #25, closes #23). `migrateFile()` shipped in v1.5.0 but the Phase 4/5 OAuth handoff missed the call sites — the migration code existed but never ran. Now invoked at two points before any consumer touches the config: (a) in `abilities-mcp.js` startup before the MCP server reads `wp-sites.json`, and (b) in `lib/cli/index.js`'s `runCommand()` before any subcommand parses the file. Both call sites share a single `KeychainSecretStore` instance. CLI subcommands no longer error with `v<unknown> but CLI expects v2` against legacy v1 configs; the OAuth `add-site` / `upgrade-auth` flows can reach the auth code path on a fresh install. Idempotent — second-run on an already-v2 file is a no-op.
|
|
14
|
+
- **Post-migration v2 apppassword sites validate and connect** (PR #27, closes #26). Helena's Phase B run surfaced a regression: when PR #25's wired migration converted a multi-site v1 config to v2, every non-OAuth site moved to `auth.method: 'apppassword'` with `auth.password_ref`, and the legacy `http.password` / `http.passwordEnv` / `http.passwordCommand` fields were stripped per Appendix F.5 — keychain becomes the sole source of truth. The runtime side still spoke v1 only: `validateSiteConfig()` had no apppassword branch, so v2 sites fell through to the legacy http validator which rejected them with `requires one of http.password, http.passwordEnv, or http.passwordCommand`, and `ConnectionPool._createTransport` had no resolver for `auth.password_ref`. Two parallel branches added (validator + async `resolveSitePassword(site, secretStore)` helper that reads keychain via the SecretStore), pool dispatches on `auth.method === 'apppassword'`, and `KeychainSecretStore` is constructed lazily so SSH-only / v1-only setups still avoid loading keytar. Multi-site acceptance test (1 oauth + 2 apppassword + 1 ssh-carrier) pins the routing.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Async config loading** (PR #28, closes #5). The boot chain — `resolveConfigFilePath`, `loadConfig`, `loadConfigFile`, `validateSiteConfig`, `resolvePassword` — is async. File reads use `fs.promises`; the `passwordCommand` shell-out goes through `util.promisify(exec)` rather than `execFile` so existing operator configs that rely on shell features (pipes, redirects, `$()` substitution — e.g. `op read 'op://Vault/foo' | tr -d '\n'`) keep working unchanged. `loadConfig` now returns a `Promise`; `abilities-mcp.js`'s bootstrap awaits it (the IIFE was already async per the migration wiring above). Pure-dispatch helpers (`resolveSiteKey`, `buildSiteKeyEnum`, `buildEnvConfig`, `buildLegacyConfig`) stayed synchronous — they have no I/O and converting them would touch every call site for no runtime benefit.
|
|
19
|
+
|
|
20
|
+
### Internal
|
|
21
|
+
|
|
22
|
+
- Test count: 237 (+27 since 1.5.0). Node CI matrix: 18, 20, 22.
|
|
23
|
+
- Validator coverage extended: 8 acceptance/reject cases for v2 apppassword (http and ssh carriers, hand-edited reject paths), 9 cases for the async surface (`loadConfig` Promise return, `resolveConfigFilePath` async, `resolvePassword` env / command / shell-feature regression).
|
|
24
|
+
|
|
25
|
+
## [1.5.0] - 2026-04-28
|
|
26
|
+
|
|
27
|
+
OAuth 2.1 release. The bridge is now a full OAuth client: it discovers the authorization server via RFC 9728, performs Dynamic Client Registration (RFC 7591), drives the authorization-code grant with PKCE S256 (RFC 7636) through a loopback browser flow (RFC 8252), persists tokens in the OS keychain, refreshes them automatically, and sends Bearer tokens through the runtime MCP transport.
|
|
28
|
+
|
|
29
|
+
Companion release: [abilities-mcp-adapter v1.4.3](https://github.com/Wicked-Evolutions/abilities-mcp-adapter/releases/tag/v1.4.3) — the OAuth resource server + authorization server.
|
|
30
|
+
|
|
31
|
+
### Added — OAuth 2.1 client core (#15)
|
|
32
|
+
|
|
33
|
+
- **`lib/auth/` module** — full OAuth 2.1 client: `oauth-client.js` (state machine), `discovery-client.js` (RFC 9728 + RFC 8414, refuses HTTP, refuses redirects on `.well-known/*`, throws `CapabilityPinningError` on pinned 404), `dcr-client.js` (RFC 7591), `pkce.js` (32-byte verifier, S256, 16-byte hex state, `crypto.timingSafeEqual` with length-mismatch CPU-burn defense), `loopback-server.js` (RFC 8252 loopback callback), `browser-launcher.js` (cross-platform `open`), `token-manager.js` (refresh window, retry semantics, expired/revoked state machine).
|
|
34
|
+
- **`SecretStore` interface** — three implementations: `keychain-secret-store.js` (libsecret on Linux, Keychain on macOS, Credential Manager on Windows), `memory-secret-store.js` (testing), `secret-store.js` (interface). All token persistence flows through this interface.
|
|
35
|
+
- **`BridgeIdentityProvider` interface** + **`FreshEachTimeIdentityProvider`** (v1.0 implementation per Appendix H.3.2 binding amendment): `getClientId()` always returns null → fresh DCR on every flow. `persistClientId()` is a documented no-op pending v1.1's persistent-identity upgrade contract.
|
|
36
|
+
- **`schema-v2.js`** — keychain references replace inline secrets in `wp-sites.json`. `schema_version: 2` with `auth.method`, `access_token_ref`, `refresh_token_ref`, `oauth_capability_pinned`, `apppassword_fallback`. `config-migration.js` upgrades v1 configs in place.
|
|
37
|
+
- **`OAuthHttpTransport`** (#18) — runtime transport that uses `TokenManager.getAccessToken()` before each request, builds `Authorization: Bearer ...`, handles 401 → `forceRefresh` → retry-once, surfaces terminal auth failure via `onAuthStatusChange('expired')`. `ConnectionPool._create()` dispatches to this when `siteConfig.auth.method === 'oauth'`.
|
|
38
|
+
|
|
39
|
+
### Added — OAuth CLI subcommands (#16)
|
|
40
|
+
|
|
41
|
+
- **`abilities-mcp <subcommand>`** dispatches to eight new subcommands wrapping `lib/auth/`: the six in the sprint plan — `add-site`, `reauth`, `revoke`, `list-sites`, `test`, `upgrade-auth` — plus two design-doc extensions: `force-downgrade` (J.1, escape hatch for the H.2.3 capability-pin failure) and `self-check` (J.2, the H.2.6 Authorization-header probe). Each subcommand subscribes to the OAuth state machine and prints operator-facing progress lines. Error messages name the exact next action. Bare `node abilities-mcp.js` (no subcommand) still starts the MCP STDIO server unchanged.
|
|
42
|
+
- Exit-code table (`0`/`1`/`2`/`3`/`4`/`5`) mapping success / generic / usage / config / auth / capability-pinning failures, documented in `abilities-mcp --help`.
|
|
43
|
+
- `--debug` flag includes the `cause` stack on errors for troubleshooting.
|
|
44
|
+
- `force-downgrade` audit lives on the site config (`force_downgrade.{at, expires_at, reason}`) and is surfaced in `list-sites` for 30 days.
|
|
45
|
+
|
|
46
|
+
### Security
|
|
47
|
+
|
|
48
|
+
- **H-7: removed dead refresh-intent keychain code** (#21). `TokenManager.refresh()` previously wrote a `${siteId}/refresh-intent` keychain entry before sending the refresh request and deleted it on every exit path. The marker had no reader — the original H.2.1 mid-flight crash-recovery semantics were never implemented. With the adapter's C-2 fix shipping encrypt-at-rest grace-window retry on the server (adapter PR #61), the bridge no longer needs an in-flight intent marker. Pure deletion of dead code that paid a keychain write per refresh.
|
|
49
|
+
- **H-8: client_id port guard in `_runRegister`** (#22). `OAuthClient._runRegister` previously returned a persisted `client_id` from `identityProvider.getClientId()` without verifying that the registered loopback redirect_uri's port matched the live loopback port. v1.0 was safe by accident because `FreshEachTimeIdentityProvider.getClientId()` always returns null. v1.1's persistent-identity upgrade (per Appendix H.3.2) would have surfaced the bug: a stale persisted client_id whose registered port no longer matched would fail server-side `redirect_uri_valid()`. Defensive fix: `_runRegister` now always calls `identityProvider.clearClientId()` before DCR. v1.1+ designs that want to short-circuit DCR on persisted client_id must do so at the `OAuthClient.run()` level after verifying the loopback port matches the registration. See follow-up [abilities-mcp-adapter#73](https://github.com/Wicked-Evolutions/abilities-mcp-adapter/issues/73) for the spec amendment.
|
|
50
|
+
|
|
51
|
+
### Internal
|
|
52
|
+
|
|
53
|
+
- Test count: 210 (+34 since 1.4.0). Node CI matrix: 18, 20, 22.
|
|
54
|
+
- `infra: retarget project automation` (#14) for the OAuth sprint workflow.
|
|
55
|
+
|
|
56
|
+
## [1.4.0] - 2026-04-26
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
- `.mcpb` distribution bundle for one-click install in Claude Desktop (#8). `manifest.json` against MCPB spec v0.3, `.mcpbignore`, and `npm run pack:mcpb` script. Application Password is stored encrypted in the OS keychain via `sensitive: true`. Published as a GitHub Release asset.
|
|
60
|
+
- `ABILITIES_MCP_URL` / `ABILITIES_MCP_USERNAME` / `ABILITIES_MCP_PASSWORD` environment-variable config fallback in `lib/config.js`. When no `wp-sites.json` exists, the bridge builds a single-site config from the env vars and auto-derives the MCP adapter endpoint as `<URL>/wp-json/mcp/mcp-adapter-default-server`. Covers the `.mcpb` install path and any env-var-based MCP client (`claude mcp add`, Docker, etc.).
|
|
61
|
+
- `npm run validate:mcpb` — validates `manifest.json` against the MCPB schema.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
- README restructured around three install paths: `.mcpb` bundle (recommended for Claude Desktop), env-vars + npm install (Claude Code / Cursor / Docker), and `wp-sites.json` (multi-site / power users). Existing `wp-sites.json` users are unaffected.
|
|
65
|
+
|
|
5
66
|
## [1.3.1] - 2026-03-19
|
|
6
67
|
|
|
7
68
|
### Fixed
|
package/README.md
CHANGED
|
@@ -16,22 +16,26 @@ Open-source MCP bridge that connects any AI client to your WordPress sites throu
|
|
|
16
16
|
|
|
17
17
|
## What You Can Do
|
|
18
18
|
|
|
19
|
-
The abilities available to your AI agent depend on which ability plugins you install. With [Abilities for AI](https://wickedevolutions.com/abilities-for-ai) installed, your agent gets access to:
|
|
19
|
+
The abilities available to your AI agent depend on which ability plugins you install. With [Abilities for AI](https://community.wickedevolutions.com/item/abilities-for-ai/) installed, your agent gets access to:
|
|
20
20
|
|
|
21
21
|
**Content & Publishing** — content, blocks, patterns, media, menus, taxonomies, comments, revisions
|
|
22
22
|
**Site Management** — plugins, themes, settings, users, site health, cache, cron, rewrite rules
|
|
23
23
|
**Infrastructure** — filesystem, meta, REST discovery, knowledge layer
|
|
24
24
|
**Third-party integrations** — auto-detected modules for supported plugins (Astra, Spectra, SureCart, Presto Player, and more)
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
**[Abilities for Fluent Plugins](https://github.com/Wicked-Evolutions/abilities-for-fluent-plugins)** is our continuously-enhanced third-party translator — bringing AI control to FluentCRM, FluentCommunity, FluentForms, FluentBooking, FluentSupport, FluentBoards, FluentSMTP, FluentAuth, FluentSnippets, FluentMessaging, FluentCart, and FluentAffiliate. We build and maintain it because we use Fluent's plugins ourselves and wanted them AI-native.
|
|
27
|
+
|
|
28
|
+
Beyond Fluent, the bridge is plugin-agnostic by design. Any plugin that registers abilities through the WordPress Abilities API becomes available automatically — no configuration in this bridge required. We urge every WordPress plugin developer to prioritize native Abilities API support over anything else.
|
|
27
29
|
|
|
28
30
|
Every ability enforces `current_user_can()` at execution time — your WordPress role is the security boundary.
|
|
29
31
|
|
|
30
|
-
> **Sign up for the Abilities for AI alpha release:** https://wickedevolutions.com/abilities-for-ai
|
|
32
|
+
> **Sign up for the Abilities for AI alpha release:** https://community.wickedevolutions.com/item/abilities-for-ai/
|
|
33
|
+
|
|
34
|
+
## Install
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
There are three install paths. Pick the one that matches how you use AI clients.
|
|
33
37
|
|
|
34
|
-
###
|
|
38
|
+
### Set up WordPress (required for all paths)
|
|
35
39
|
|
|
36
40
|
Create a dedicated WordPress user for AI access and generate an Application Password.
|
|
37
41
|
|
|
@@ -59,24 +63,77 @@ Go to **Users → Edit (your mcp-agent user) → Application Passwords**, enter
|
|
|
59
63
|
|
|
60
64
|
Install both on your WordPress site:
|
|
61
65
|
|
|
62
|
-
1. **[Abilities for AI](https://wickedevolutions.com/abilities-for-ai)** — registers WordPress abilities across content, site management, infrastructure, and third-party integration modules
|
|
63
|
-
2. **[Abilities MCP Adapter](https://
|
|
66
|
+
1. **[Abilities for AI](https://community.wickedevolutions.com/item/abilities-for-ai/)** — registers WordPress abilities across content, site management, infrastructure, and third-party integration modules
|
|
67
|
+
2. **[Abilities MCP Adapter](https://community.wickedevolutions.com/item/abilities-mcp-adapter/)** — exposes abilities as MCP tools via REST API
|
|
68
|
+
|
|
69
|
+
Both are available as free downloads from our store, or install from GitHub: [abilities-for-ai](https://github.com/Wicked-Evolutions/abilities-for-ai) and [abilities-mcp-adapter](https://github.com/Wicked-Evolutions/abilities-mcp-adapter).
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### Path 1 — `.mcpb` bundle for Claude Desktop (recommended)
|
|
74
|
+
|
|
75
|
+
Single-click install for Claude Desktop on macOS and Windows. The Application Password is stored encrypted in your OS keychain (macOS Keychain / Windows Credential Manager).
|
|
76
|
+
|
|
77
|
+
1. Download `abilities-mcp.mcpb` from the [latest GitHub Release](https://github.com/Wicked-Evolutions/abilities-mcp/releases/latest).
|
|
78
|
+
2. Double-click the file. Claude Desktop opens an "Install Extension" dialog.
|
|
79
|
+
3. Type three things:
|
|
80
|
+
- **WordPress Site URL** — `https://example.com`
|
|
81
|
+
- **WordPress Username** — `mcp-agent`
|
|
82
|
+
- **Application Password** — paste the password from the previous step
|
|
83
|
+
4. Click **Install**. The connection is live.
|
|
84
|
+
|
|
85
|
+
The bundle covers the single-site case. For multi-site (one bridge connected to several WordPress sites at once), use Path 3.
|
|
86
|
+
|
|
87
|
+
---
|
|
64
88
|
|
|
65
|
-
### 2
|
|
89
|
+
### Path 2 — Env vars (Claude Code, Cursor, Docker, any MCP client)
|
|
66
90
|
|
|
67
|
-
|
|
91
|
+
Install the bridge from npm, then point your client at it with three environment variables:
|
|
68
92
|
|
|
69
93
|
```bash
|
|
70
|
-
|
|
94
|
+
npm install -g @wickedevolutions/abilities-mcp
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
In your client's MCP config:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"mcpServers": {
|
|
102
|
+
"wordpress": {
|
|
103
|
+
"command": "abilities-mcp",
|
|
104
|
+
"env": {
|
|
105
|
+
"ABILITIES_MCP_URL": "https://example.com",
|
|
106
|
+
"ABILITIES_MCP_USERNAME": "mcp-agent",
|
|
107
|
+
"ABILITIES_MCP_PASSWORD": "xxxx xxxx xxxx xxxx xxxx xxxx"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
71
112
|
```
|
|
72
113
|
|
|
73
|
-
|
|
114
|
+
The endpoint is auto-derived as `<URL>/wp-json/mcp/mcp-adapter-default-server`. Single-site only — for multi-site, use Path 3.
|
|
115
|
+
|
|
116
|
+
For `claude mcp add` users:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
claude mcp add wordpress \
|
|
120
|
+
--env ABILITIES_MCP_URL=https://example.com \
|
|
121
|
+
--env ABILITIES_MCP_USERNAME=mcp-agent \
|
|
122
|
+
--env ABILITIES_MCP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx' \
|
|
123
|
+
-- abilities-mcp
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### Path 3 — `wp-sites.json` (multi-site, power users)
|
|
74
129
|
|
|
75
|
-
|
|
130
|
+
Use this when you connect one bridge to multiple WordPress sites, when you want passwords sourced from a keychain or shell command, or when you're targeting WordPress multisite networks via dot-notation routing.
|
|
76
131
|
|
|
77
|
-
|
|
132
|
+
```bash
|
|
133
|
+
cp wp-sites.example.json wp-sites.json
|
|
134
|
+
```
|
|
78
135
|
|
|
79
|
-
|
|
136
|
+
Edit `wp-sites.json` with your sites, then add the server to your client's MCP config:
|
|
80
137
|
|
|
81
138
|
```json
|
|
82
139
|
{
|
|
@@ -126,11 +183,15 @@ node abilities-mcp.js --register
|
|
|
126
183
|
}
|
|
127
184
|
```
|
|
128
185
|
|
|
129
|
-
### Config
|
|
186
|
+
### Config search order
|
|
130
187
|
|
|
131
188
|
1. `--config=/path/to/wp-sites.json` (explicit)
|
|
132
|
-
2.
|
|
189
|
+
2. `wp-sites.json` in the same directory as `abilities-mcp.js`
|
|
133
190
|
3. `~/.abilities-mcp/wp-sites.json`
|
|
191
|
+
4. `ABILITIES_MCP_URL` / `ABILITIES_MCP_USERNAME` / `ABILITIES_MCP_PASSWORD` env vars (single-site, used by the `.mcpb` bundle)
|
|
192
|
+
5. `--host=<ssh-host> --path=<wp-path>` (legacy SSH single-site)
|
|
193
|
+
|
|
194
|
+
The first one found wins. If a `wp-sites.json` exists, env vars are ignored.
|
|
134
195
|
|
|
135
196
|
### WordPress Multisite
|
|
136
197
|
|
|
@@ -313,9 +374,19 @@ See [docs/architecture.md](docs/architecture.md) for the full technical deep div
|
|
|
313
374
|
## Requirements
|
|
314
375
|
|
|
315
376
|
- Node.js >= 18
|
|
316
|
-
- WordPress 6.9+ with [Abilities for AI](https://wickedevolutions.com/abilities-for-ai) and [Abilities MCP Adapter](https://github.com/Wicked-Evolutions/abilities-mcp-adapter) installed
|
|
377
|
+
- WordPress 6.9+ with [Abilities for AI](https://community.wickedevolutions.com/item/abilities-for-ai/) and [Abilities MCP Adapter](https://github.com/Wicked-Evolutions/abilities-mcp-adapter) installed
|
|
317
378
|
- Application Passwords enabled (default in WordPress 5.6+)
|
|
318
379
|
|
|
380
|
+
## Evolving Knowledge
|
|
381
|
+
|
|
382
|
+
We continuously add knowledge docs, skills, and agent patterns to [knowledge.wickedevolutions.com](https://knowledge.wickedevolutions.com).
|
|
383
|
+
|
|
384
|
+
## Disclaimer
|
|
385
|
+
|
|
386
|
+
Humans make mistakes — as we know from the present day and history. Humans trained AI. AI acts accordingly. AI predicts probability based on the context window it holds. It is trained to sound certain, as if everything is truth, and to "fix" everything so the human becomes satisfied.
|
|
387
|
+
|
|
388
|
+
Learn how to communicate with AI. You are fully responsible for using AI in your life, business, and projects. Using these products is your personal responsibility to learn and own.
|
|
389
|
+
|
|
319
390
|
## License
|
|
320
391
|
|
|
321
392
|
GPL-2.0-or-later
|
package/abilities-mcp.js
CHANGED
|
@@ -23,147 +23,216 @@
|
|
|
23
23
|
'use strict';
|
|
24
24
|
|
|
25
25
|
const { createLogger } = require('./lib/logger');
|
|
26
|
-
const { loadConfig, buildSiteKeyEnum } = require('./lib/config');
|
|
26
|
+
const { loadConfig, buildSiteKeyEnum, resolveConfigFilePath } = require('./lib/config');
|
|
27
27
|
const { ConnectionPool } = require('./lib/connection-pool');
|
|
28
28
|
const { ToolCatalog } = require('./lib/tool-catalog');
|
|
29
29
|
const { McpRouter } = require('./lib/router');
|
|
30
30
|
const { SshTransport } = require('./lib/transports/ssh-transport');
|
|
31
|
+
const { migrateFile } = require('./lib/auth/config-migration');
|
|
32
|
+
const { KeychainSecretStore } = require('./lib/auth/keychain-secret-store');
|
|
31
33
|
|
|
32
34
|
// ---------------------------------------------------------------------------
|
|
33
|
-
//
|
|
35
|
+
// Subcommand routing (Phase 5 OAuth CLI)
|
|
34
36
|
// ---------------------------------------------------------------------------
|
|
37
|
+
// `abilities-mcp <subcommand>` (e.g. add-site, list-sites) dispatches to the
|
|
38
|
+
// OAuth CLI in lib/cli/. With no subcommand the bridge starts the MCP server
|
|
39
|
+
// as before — this preserves backwards compatibility for every existing
|
|
40
|
+
// invocation path (Claude Desktop, .mcpb bundle, bare `node abilities-mcp.js`).
|
|
41
|
+
const { isKnownSubcommand, runCommand, HELP_TEXT, isHelpToken } = require('./lib/cli');
|
|
35
42
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
if (arg.startsWith('--')) {
|
|
39
|
-
const [key, ...rest] = arg.slice(2).split('=');
|
|
40
|
-
args[key] = rest.length ? rest.join('=') : true;
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const debug = !!args.debug;
|
|
45
|
-
const register = !!args.register;
|
|
46
|
-
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
// Registration mode (--register)
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
43
|
+
const rawArgs = process.argv.slice(2);
|
|
44
|
+
const firstToken = rawArgs[0];
|
|
50
45
|
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
registerClaudeDesktop({ name: args.name || 'wordpress', configPath: args.config });
|
|
46
|
+
if (firstToken && isHelpToken(firstToken)) {
|
|
47
|
+
process.stdout.write(HELP_TEXT.join('\n') + '\n');
|
|
54
48
|
process.exit(0);
|
|
55
49
|
}
|
|
56
50
|
|
|
51
|
+
const isSubcommandInvocation = firstToken && isKnownSubcommand(firstToken);
|
|
52
|
+
|
|
53
|
+
if (isSubcommandInvocation) {
|
|
54
|
+
(async () => {
|
|
55
|
+
try {
|
|
56
|
+
const { exitCode, lines, errLines } = await runCommand({
|
|
57
|
+
subcommand: firstToken,
|
|
58
|
+
argv: rawArgs.slice(1),
|
|
59
|
+
});
|
|
60
|
+
if (lines.length) process.stdout.write(lines.join('\n') + '\n');
|
|
61
|
+
if (errLines.length) process.stderr.write(errLines.join('\n') + '\n');
|
|
62
|
+
process.exit(exitCode);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
// Last-resort safety net — runCommand normally catches everything.
|
|
65
|
+
process.stderr.write(`abilities-mcp: ${err.message}\n`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
// ---------------------------------------------------------------------------
|
|
58
|
-
//
|
|
72
|
+
// MCP server mode — the original CLI argument parsing (no subcommand).
|
|
73
|
+
// Skipped when a subcommand was dispatched above; otherwise we'd race the
|
|
74
|
+
// IIFE's process.exit() against the bootstrap that awaits loadConfig() and
|
|
75
|
+
// connectDefault().
|
|
59
76
|
// ---------------------------------------------------------------------------
|
|
60
77
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
78
|
+
if (!isSubcommandInvocation) {
|
|
79
|
+
const args = {};
|
|
80
|
+
rawArgs.forEach(arg => {
|
|
81
|
+
if (arg.startsWith('--')) {
|
|
82
|
+
const [key, ...rest] = arg.slice(2).split('=');
|
|
83
|
+
args[key] = rest.length ? rest.join('=') : true;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
66
86
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
config = loadConfig(args);
|
|
70
|
-
} catch (err) {
|
|
71
|
-
process.stderr.write(`abilities-mcp: ${err.message}\n`);
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
87
|
+
const debug = !!args.debug;
|
|
88
|
+
const register = !!args.register;
|
|
74
89
|
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
if (register) {
|
|
91
|
+
const { registerClaudeDesktop } = require('./lib/register');
|
|
92
|
+
registerClaudeDesktop({ name: args.name || 'wordpress', configPath: args.config });
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
79
95
|
|
|
80
|
-
const
|
|
81
|
-
|
|
96
|
+
const log = createLogger(debug);
|
|
97
|
+
log('abilities-mcp v1.0.0 starting');
|
|
98
|
+
|
|
99
|
+
// Ensure SSH agent is available (macOS launchd discovery)
|
|
100
|
+
SshTransport.ensureSshAuthSock();
|
|
101
|
+
|
|
102
|
+
// -------------------------------------------------------------------------
|
|
103
|
+
// Async startup — schema v1→v2 migration must complete BEFORE loadConfig.
|
|
104
|
+
// -------------------------------------------------------------------------
|
|
105
|
+
// Per Appendix F.5 (binding): the migration is "Triggered on first bridge
|
|
106
|
+
// launch after upgrade. One-shot, non-destructive." `migrateFile` is
|
|
107
|
+
// idempotent — second-run on a v2 file is a no-op. Called before
|
|
108
|
+
// `loadConfig` so that when v1 is on disk, `loadConfig` reads the freshly
|
|
109
|
+
// rewritten v2 file (with secrets lifted into the keychain).
|
|
110
|
+
//
|
|
111
|
+
// The MCP server uses a fresh `KeychainSecretStore` instance here. The
|
|
112
|
+
// store is a stateless wrapper over keytar — entry identity is determined
|
|
113
|
+
// entirely by (service, account), so a freshly constructed instance writes
|
|
114
|
+
// to the same keychain entries the runtime/CLI later read.
|
|
115
|
+
//
|
|
116
|
+
// Env-var single-site mode (.mcpb path) and legacy --host/--path mode have
|
|
117
|
+
// no on-disk wp-sites.json; `resolveConfigFilePath` returns null and we
|
|
118
|
+
// skip migration entirely.
|
|
119
|
+
(async function bootstrap() {
|
|
120
|
+
const filePath = await resolveConfigFilePath(args);
|
|
121
|
+
if (filePath) {
|
|
122
|
+
try {
|
|
123
|
+
const result = await migrateFile({
|
|
124
|
+
filePath,
|
|
125
|
+
secretStore: new KeychainSecretStore(),
|
|
126
|
+
});
|
|
127
|
+
if (result.migrated) {
|
|
128
|
+
log(`Migrated wp-sites.json v1 → v2 (${result.liftedCount} secret(s) lifted; backup: ${result.backupPath})`);
|
|
129
|
+
}
|
|
130
|
+
} catch (err) {
|
|
131
|
+
process.stderr.write(`abilities-mcp: schema migration failed: ${err.message}\n`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
82
135
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
136
|
+
let config;
|
|
137
|
+
try {
|
|
138
|
+
config = await loadConfig(args);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
process.stderr.write(`abilities-mcp: ${err.message}\n`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
88
143
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
144
|
+
const isMultiSite = config._isMultiSite;
|
|
145
|
+
const siteKeys = buildSiteKeyEnum(config);
|
|
146
|
+
log(`Config loaded: ${siteKeys.length} site(s): ${siteKeys.join(', ')} (default: ${config.defaultSite})`);
|
|
147
|
+
log(`Multi-site mode: ${isMultiSite}`);
|
|
92
148
|
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
siteKeys,
|
|
96
|
-
isMultiSite,
|
|
97
|
-
pool,
|
|
98
|
-
catalog,
|
|
99
|
-
sendToClient,
|
|
100
|
-
log,
|
|
101
|
-
});
|
|
149
|
+
const pool = new ConnectionPool(config, log);
|
|
150
|
+
const catalog = new ToolCatalog(config, log);
|
|
102
151
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
process.stdin.on('data', (chunk) => {
|
|
110
|
-
inputBuffer += chunk.toString();
|
|
152
|
+
if (catalog.isEnabled()) {
|
|
153
|
+
log('Tool filtering enabled');
|
|
154
|
+
} else {
|
|
155
|
+
log('Tool filtering disabled (no toolFilter in config or enabled: false)');
|
|
156
|
+
}
|
|
111
157
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const line = inputBuffer.slice(0, newlineIdx);
|
|
115
|
-
inputBuffer = inputBuffer.slice(newlineIdx + 1);
|
|
116
|
-
if (line.trim()) {
|
|
117
|
-
let msg;
|
|
118
|
-
try {
|
|
119
|
-
msg = JSON.parse(line.trim());
|
|
120
|
-
} catch (e) {
|
|
121
|
-
log(`Non-JSON from client (dropped): ${line.substring(0, 200)}`);
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
router.handleClientMessage(msg, line.trim());
|
|
158
|
+
function sendToClient(data) {
|
|
159
|
+
process.stdout.write(data + '\n');
|
|
125
160
|
}
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
161
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
162
|
+
const router = new McpRouter({
|
|
163
|
+
config,
|
|
164
|
+
siteKeys,
|
|
165
|
+
isMultiSite,
|
|
166
|
+
pool,
|
|
167
|
+
catalog,
|
|
168
|
+
sendToClient,
|
|
169
|
+
log,
|
|
170
|
+
});
|
|
133
171
|
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
172
|
+
// -----------------------------------------------------------------------
|
|
173
|
+
// Client STDIO processing
|
|
174
|
+
// -----------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
let inputBuffer = '';
|
|
177
|
+
|
|
178
|
+
process.stdin.on('data', (chunk) => {
|
|
179
|
+
inputBuffer += chunk.toString();
|
|
180
|
+
|
|
181
|
+
let newlineIdx;
|
|
182
|
+
while ((newlineIdx = inputBuffer.indexOf('\n')) !== -1) {
|
|
183
|
+
const line = inputBuffer.slice(0, newlineIdx);
|
|
184
|
+
inputBuffer = inputBuffer.slice(newlineIdx + 1);
|
|
185
|
+
if (line.trim()) {
|
|
186
|
+
let msg;
|
|
187
|
+
try {
|
|
188
|
+
msg = JSON.parse(line.trim());
|
|
189
|
+
} catch (e) {
|
|
190
|
+
log(`Non-JSON from client (dropped): ${line.substring(0, 200)}`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
router.handleClientMessage(msg, line.trim());
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
137
197
|
|
|
138
|
-
(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
router.handleTransportMessage(parsedMsg, rawLine);
|
|
198
|
+
process.stdin.on('end', () => {
|
|
199
|
+
log('Client stdin closed — shutting down');
|
|
200
|
+
shutdown();
|
|
142
201
|
});
|
|
143
|
-
router.setDefaultTransport(transport);
|
|
144
|
-
log(`Default transport connected: ${config.defaultSite}`);
|
|
145
|
-
router.drainEarlyQueue();
|
|
146
|
-
} catch (err) {
|
|
147
|
-
process.stderr.write(`abilities-mcp: Failed to connect to default site: ${err.message}\n`);
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
})();
|
|
151
202
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
203
|
+
// -----------------------------------------------------------------------
|
|
204
|
+
// Startup — connect to default site
|
|
205
|
+
// -----------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const transport = await pool.connectDefault((parsedMsg, rawLine) => {
|
|
209
|
+
router.handleTransportMessage(parsedMsg, rawLine);
|
|
210
|
+
});
|
|
211
|
+
router.setDefaultTransport(transport);
|
|
212
|
+
log(`Default transport connected: ${config.defaultSite}`);
|
|
213
|
+
router.drainEarlyQueue();
|
|
214
|
+
} catch (err) {
|
|
215
|
+
process.stderr.write(`abilities-mcp: Failed to connect to default site: ${err.message}\n`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
155
218
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
219
|
+
// -----------------------------------------------------------------------
|
|
220
|
+
// Signal handling
|
|
221
|
+
// -----------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
function shutdown() {
|
|
224
|
+
log('Shutting down');
|
|
225
|
+
pool.shutdownAll().then(() => {
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}).catch(() => {
|
|
228
|
+
process.exit(1);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
164
231
|
|
|
165
|
-
process.on('SIGTERM', shutdown);
|
|
166
|
-
process.on('SIGINT', shutdown);
|
|
167
|
-
process.on('unhandledRejection', (reason) => {
|
|
168
|
-
|
|
169
|
-
});
|
|
232
|
+
process.on('SIGTERM', shutdown);
|
|
233
|
+
process.on('SIGINT', shutdown);
|
|
234
|
+
process.on('unhandledRejection', (reason) => {
|
|
235
|
+
log(`Unhandled rejection: ${reason}`);
|
|
236
|
+
});
|
|
237
|
+
})();
|
|
238
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BridgeIdentityProvider — interface contract.
|
|
5
|
+
*
|
|
6
|
+
* Specified in design doc Appendix F.4 (interface) and Appendix H.3.2 (the
|
|
7
|
+
* v1.0 binding amendment that REPLACES the v1.0 implementation shown in F.4).
|
|
8
|
+
*
|
|
9
|
+
* The provider decides whether the bridge presents a stable identity to the
|
|
10
|
+
* adapter across sessions / installs / machines. v1.0 ships the
|
|
11
|
+
* fresh-each-time implementation (force fresh DCR on every flow); v1.1+ may
|
|
12
|
+
* persist client_id same-machine, v1.2+ may export/import across machines.
|
|
13
|
+
*
|
|
14
|
+
* @typedef {object} IdentityBundle
|
|
15
|
+
* @property {1} version
|
|
16
|
+
* @property {string} siteId
|
|
17
|
+
* @property {string} clientId
|
|
18
|
+
* @property {string} exportedAt ISO 8601
|
|
19
|
+
* @property {string} [signature] future: HMAC for tamper-detection
|
|
20
|
+
*
|
|
21
|
+
* @typedef {object} BridgeIdentityProvider
|
|
22
|
+
* @property {(siteId: string) => Promise<string|null>} getClientId
|
|
23
|
+
* @property {(siteId: string, clientId: string) => Promise<void>} persistClientId
|
|
24
|
+
* @property {(siteId: string) => Promise<void>} clearClientId
|
|
25
|
+
* @property {(siteId: string) => Promise<IdentityBundle|null>} exportIdentity
|
|
26
|
+
* @property {(siteId: string, bundle: IdentityBundle) => Promise<void>} importIdentity
|
|
27
|
+
*
|
|
28
|
+
* Copyright (C) 2026 Influencentricity | Wicked Evolutions
|
|
29
|
+
* @license GPL-2.0-or-later
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const IDENTITY_BUNDLE_VERSION = 1;
|
|
33
|
+
|
|
34
|
+
module.exports = { IDENTITY_BUNDLE_VERSION };
|