@wickedevolutions/abilities-mcp 1.6.2 → 1.6.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 +14 -0
- package/lib/bridge-tools.js +6 -3
- package/lib/router.js +34 -4
- package/lib/sanitizer.js +13 -1
- package/lib/tool-catalog.js +20 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Abilities MCP are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.6.3] - 2026-05-12
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Sanitizer normalizes array-shaped `inputSchema.properties` (Issue [#83](https://github.com/Wicked-Evolutions/abilities-mcp/issues/83), Sprint C 2026-05-11).** Extends `validateToolSchema` in `lib/sanitizer.js` so that when a tool's `inputSchema.properties` is an array (PHP `'properties' => array()` JSON-encodes to `[]`), `null`, a string, or any other non-plain-object value, it is normalized to `{}` before client emission. The previous v1.6.2 fix (#78/#79) normalized broken top-level `inputSchema` but left malformed nested `properties` untouched — the value passed `typeof === 'object'` (arrays are objects in JS) and reached the `Object.entries()` loop unchanged. Anthropic's draft 2020-12 validator rejects the entire `tools/list` payload on the first invalid schema (`/properties must be object`), so a single vendor-registered `properties: []` shape breaks the whole catalog. Defends against the entire class of vendor-registered ability schema slips that share the `properties: []` shape; SureCart's `surecart/get-store-info` was the trigger case (live capture from helenawillow.com 2026-05-11, 1 of 789 tools failing) but the fix is name-agnostic. Boundary discipline binding: `inputSchema: { type: "object" }` shapes that omit the `properties` key entirely remain byte-unchanged (early-return on `schema.properties === undefined` before the malformed-shape normalize). Already-valid object-shaped `properties` pass through byte-identical (regression-guarded with reference-equality assertion in `test/sanitizer.test.js`).
|
|
10
|
+
|
|
11
|
+
- **Bridge orientation: `mcp-adapter-*` reliably visible as the cross-site discovery path when filtering is enabled; `wp_browse_tools` / `wp_load_tools` describe themselves honestly (Issue [#85](https://github.com/Wicked-Evolutions/abilities-mcp/issues/85), Sprint C-follow-up 2026-05-12).** Surfaced during v1.6.3 release validation: AI clients were orienting around the bridge-local meta-tools first, treating their default-site catalog as the full ability surface and missing abilities registered only on non-default sites (e.g., `surecart/get-store-info` on helenawillow when `defaultSite=wickedevolutions`). The cross-site execution path via `mcp-adapter-discover-abilities` / `mcp-adapter-get-ability-info` / `mcp-adapter-execute-ability` already worked correctly — it just wasn't the path clients reached for. Three minimal changes restore orientation without changing architecture: (1) `lib/tool-catalog.js` — when `toolFilter.enabled === true` and the operator has not set their own `alwaysIncludeCategories`, default to `['mcp-adapter']` so the five `mcp-adapter-*` tools are always callable without a prior `wp_load_tools` step. Explicit operator override (including `[]`) is honored — guarded against the `[] || ['mcp-adapter']` truthy-array pitfall with an explicit `undefined`/`null` check. (2) `lib/bridge-tools.js` — `wp_browse_tools` and `wp_load_tools` descriptions now explicitly state they operate on the direct-tool catalog scoped to the default site, not full cross-site ability discovery. (3) `lib/router.js` — `wp_browse_tools` response leads with a `Scope:` line; `wp_load_tools` computes which requested categories are missing from the catalog and returns a structured pointer ("Not in the direct-tool catalog (scoped to defaultSite `<site>`): `<category>`. To call abilities in this category on another site, use `mcp-adapter-discover-abilities` with `{site, category}` then `mcp-adapter-execute-ability` with `{site, ability_name, parameters}`") instead of the prior silent "No changes — categories may already be active" zero-activation. No union-catalog architecture introduced; no `defaultSite`-switching workaround required; adapter unchanged. Acceptance pin: with `defaultSite = wickedevolutions` and `toolFilter.enabled = true`, a fresh session can discover and execute `surecart/get-store-info` on `helenawillow` through the adapter meta-tool path without changing `defaultSite` (empirically verified, MD5-pinned behavior-reversal cycle, 415/415 tests passing).
|
|
12
|
+
|
|
13
|
+
### Compatibility & operator notes
|
|
14
|
+
|
|
15
|
+
- **No protocol semantics change** for valid schemas. Healthy operators see no behavioral difference. The fix activates only on the malformed-`properties` shape it closes.
|
|
16
|
+
- **No operator action required** — passive defensive normalization on next bridge restart.
|
|
17
|
+
- **Coordinated release wave** — held for joint release with abilities-mcp-adapter v1.4.8 per the cross-sprint coupling decision.
|
|
18
|
+
|
|
5
19
|
## [1.6.2] - 2026-05-10
|
|
6
20
|
|
|
7
21
|
Schema validity polish + boot fragility MVP + multisite topology gate. Four Bridge fixes ship together as a bundled release closing the operator-side schema-400 + the silent-bridge-death-on-expired-refresh-token (both connect-time and request-time refresh boundaries) + the misplaced-multisite-block-cross-product issues.
|
package/lib/bridge-tools.js
CHANGED
|
@@ -23,7 +23,8 @@ const BRIDGE_TOOLS = [
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
name: 'wp_browse_tools',
|
|
26
|
-
description:
|
|
26
|
+
description:
|
|
27
|
+
'List categories in the bridge\'s direct-tool catalog (scoped to defaultSite) with tool counts and load-state. This is the local-catalog control surface — not full cross-site ability discovery. For abilities on another site, or in a category not listed here, use mcp-adapter-discover-abilities / mcp-adapter-execute-ability with { site }.',
|
|
27
28
|
inputSchema: {
|
|
28
29
|
type: 'object',
|
|
29
30
|
properties: {},
|
|
@@ -31,14 +32,16 @@ const BRIDGE_TOOLS = [
|
|
|
31
32
|
},
|
|
32
33
|
{
|
|
33
34
|
name: 'wp_load_tools',
|
|
34
|
-
description:
|
|
35
|
+
description:
|
|
36
|
+
'Activate categories in the bridge\'s direct-tool catalog (scoped to defaultSite) to expose their tools in tools/list. This is the local-catalog control surface — not full cross-site ability discovery. If a requested category is not in the local catalog, the response points to mcp-adapter-discover-abilities + mcp-adapter-execute-ability for the cross-site path.',
|
|
35
37
|
inputSchema: {
|
|
36
38
|
type: 'object',
|
|
37
39
|
properties: {
|
|
38
40
|
categories: {
|
|
39
41
|
type: 'array',
|
|
40
42
|
items: { type: 'string' },
|
|
41
|
-
description:
|
|
43
|
+
description:
|
|
44
|
+
'Category names to activate from the direct-tool catalog (e.g. ["fluent-crm", "content", "media"]). Use wp_browse_tools to see available categories. Categories not in the local catalog return a pointer to mcp-adapter-discover-abilities instead of silently activating nothing.',
|
|
42
45
|
},
|
|
43
46
|
deactivate: {
|
|
44
47
|
type: 'array',
|
package/lib/router.js
CHANGED
|
@@ -463,14 +463,26 @@ class McpRouter {
|
|
|
463
463
|
}
|
|
464
464
|
|
|
465
465
|
const summary = this.catalog.getCategorySummary();
|
|
466
|
-
const lines =
|
|
467
|
-
|
|
466
|
+
const lines = [];
|
|
467
|
+
lines.push(
|
|
468
|
+
`Scope: direct-tool catalog scoped to defaultSite (${this.config.defaultSite}). ` +
|
|
469
|
+
`Not full cross-site ability discovery.`
|
|
468
470
|
);
|
|
469
471
|
lines.push('');
|
|
472
|
+
for (const c of summary) {
|
|
473
|
+
lines.push(`${c.active ? '[LOADED]' : ' '} ${c.name} (${c.toolCount} tools)`);
|
|
474
|
+
}
|
|
475
|
+
lines.push('');
|
|
470
476
|
lines.push(`Total: ${this.catalog.fullTools.length} tools in ${summary.length} categories`);
|
|
471
477
|
lines.push(`Loaded: ${summary.filter(c => c.active).reduce((n, c) => n + c.toolCount, 0)} tools`);
|
|
472
478
|
lines.push('');
|
|
473
479
|
lines.push('Use wp_load_tools with categories array to activate.');
|
|
480
|
+
lines.push('');
|
|
481
|
+
lines.push(
|
|
482
|
+
'For abilities on another site, or in a category not listed here, use ' +
|
|
483
|
+
'mcp-adapter-discover-abilities { site, category } and ' +
|
|
484
|
+
'mcp-adapter-execute-ability { site, ability_name, parameters }.'
|
|
485
|
+
);
|
|
474
486
|
|
|
475
487
|
this.sendToClient(JSON.stringify({
|
|
476
488
|
jsonrpc: '2.0', id: msg.id,
|
|
@@ -496,6 +508,7 @@ class McpRouter {
|
|
|
496
508
|
}
|
|
497
509
|
|
|
498
510
|
const activated = this.catalog.activateCategories(toActivate);
|
|
511
|
+
const missing = toActivate.filter((c) => !this.catalog.categories[c]);
|
|
499
512
|
const lines = [];
|
|
500
513
|
|
|
501
514
|
if (activated.length > 0) {
|
|
@@ -504,8 +517,25 @@ class McpRouter {
|
|
|
504
517
|
if (toDeactivate.length > 0) {
|
|
505
518
|
lines.push(`Deactivated: ${toDeactivate.join(', ')}`);
|
|
506
519
|
}
|
|
507
|
-
if (
|
|
508
|
-
lines.push(
|
|
520
|
+
if (missing.length > 0) {
|
|
521
|
+
lines.push(
|
|
522
|
+
`Not in the direct-tool catalog (scoped to defaultSite "${this.config.defaultSite}"): ` +
|
|
523
|
+
missing.join(', ') +
|
|
524
|
+
'.'
|
|
525
|
+
);
|
|
526
|
+
lines.push(
|
|
527
|
+
'For abilities on another site, or in a category not in this catalog, use the ' +
|
|
528
|
+
'adapter meta-tools instead:'
|
|
529
|
+
);
|
|
530
|
+
for (const cat of missing) {
|
|
531
|
+
lines.push(` mcp-adapter-discover-abilities { site: "<site>", category: "${cat}" }`);
|
|
532
|
+
}
|
|
533
|
+
lines.push(
|
|
534
|
+
' mcp-adapter-execute-ability { site: "<site>", ability_name: "<ability>", parameters: { ... } }'
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
if (activated.length === 0 && toDeactivate.length === 0 && missing.length === 0) {
|
|
538
|
+
lines.push('No changes — categories may already be active.');
|
|
509
539
|
}
|
|
510
540
|
|
|
511
541
|
const filtered = this.catalog.getFilteredTools();
|
package/lib/sanitizer.js
CHANGED
|
@@ -29,7 +29,19 @@ function validateToolSchema(toolName, schema, log) {
|
|
|
29
29
|
log(`SCHEMA WARN [${toolName}]: invalid top-level type "${schema.type}"`);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
// Boundary: properties key absent — leave inputSchema byte-unchanged.
|
|
33
|
+
if (schema.properties === undefined) return;
|
|
34
|
+
|
|
35
|
+
// Normalize malformed `properties` shapes (PHP `array()` JSON-encodes to `[]`,
|
|
36
|
+
// strings, null, primitives) to `{}` before downstream validation. Anthropic's
|
|
37
|
+
// draft 2020-12 validator rejects the entire tools/list payload on the first
|
|
38
|
+
// invalid schema, so a single vendor-registered `properties: []` breaks the
|
|
39
|
+
// whole catalog. Mirror of the top-level inputSchema normalize one frame up.
|
|
40
|
+
if (schema.properties === null || typeof schema.properties !== 'object' || Array.isArray(schema.properties)) {
|
|
41
|
+
log(`SCHEMA NORMALIZE [${toolName}]: inputSchema.properties not a plain object — normalized to {}`);
|
|
42
|
+
schema.properties = {};
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
33
45
|
|
|
34
46
|
for (const [prop, def] of Object.entries(schema.properties)) {
|
|
35
47
|
if (!def || typeof def !== 'object') {
|
package/lib/tool-catalog.js
CHANGED
|
@@ -32,11 +32,22 @@ class ToolCatalog {
|
|
|
32
32
|
// Active (loaded) categories
|
|
33
33
|
this.activeCategories = new Set();
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
// Resolve "always-include" categories once. Explicit operator config wins
|
|
36
|
+
// (including an explicit empty list, which preserves the operator's choice).
|
|
37
|
+
// When filtering is enabled and the operator hasn't set their own list,
|
|
38
|
+
// default to ['mcp-adapter'] so the cross-site adapter meta-tools
|
|
39
|
+
// (mcp-adapter-discover-abilities / -get-ability-info / -execute-ability)
|
|
40
|
+
// are always visible without requiring a prior wp_load_tools call.
|
|
41
|
+
const explicit = this.filterConfig && this.filterConfig.alwaysIncludeCategories;
|
|
42
|
+
const resolved =
|
|
43
|
+
explicit !== undefined && explicit !== null
|
|
44
|
+
? explicit
|
|
45
|
+
: this.isEnabled()
|
|
46
|
+
? ['mcp-adapter']
|
|
47
|
+
: [];
|
|
48
|
+
this.effectiveAlwaysInclude = new Set(resolved);
|
|
49
|
+
for (const cat of this.effectiveAlwaysInclude) {
|
|
50
|
+
this.activeCategories.add(cat);
|
|
40
51
|
}
|
|
41
52
|
}
|
|
42
53
|
|
|
@@ -104,12 +115,11 @@ class ToolCatalog {
|
|
|
104
115
|
* Deactivate categories (return to compact mode).
|
|
105
116
|
*/
|
|
106
117
|
deactivateCategories(categoryNames) {
|
|
107
|
-
// Don't deactivate always-included categories
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
);
|
|
118
|
+
// Don't deactivate always-included categories (single source of truth:
|
|
119
|
+
// the set resolved at construction, so the default mcp-adapter inclusion
|
|
120
|
+
// is treated the same as an explicit operator-configured entry).
|
|
111
121
|
for (const name of categoryNames) {
|
|
112
|
-
if (!
|
|
122
|
+
if (!this.effectiveAlwaysInclude.has(name)) {
|
|
113
123
|
this.activeCategories.delete(name);
|
|
114
124
|
}
|
|
115
125
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wickedevolutions/abilities-mcp",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.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": {
|