@softspark/ai-toolkit 4.26.0 → 4.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/README.md +16 -15
- package/app/.claude-plugin/plugin.json +1 -1
- package/benchmarks/ecosystem-doctor-snapshot.json +13 -12
- package/kb/reference/global-install-model.md +5 -2
- package/kb/reference/mcp-editor-compatibility.md +27 -3
- package/kb/reference/mcp-templates.md +5 -3
- package/kb/reference/supported-tools-registry.md +4 -3
- package/kb/troubleshooting/README.md +5 -3
- package/kb/troubleshooting/plugin-double-load.md +97 -0
- package/llms-full.txt +149 -14
- package/llms.txt +1 -0
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/doctor.py +82 -0
- package/scripts/ecosystem_tools.json +9 -4
- package/scripts/mcp_editors.py +112 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,83 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## v4.28.0 — MCP servers reach Chat and Cowork (2026-08-21)
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`claude-app` MCP adapter.** `ai-toolkit mcp install --editor claude-app
|
|
15
|
+
--scope global <template>` writes `claude_desktop_config.json` — macOS
|
|
16
|
+
`~/Library/Application Support/Claude/`, Windows `%APPDATA%/Claude/`, Linux
|
|
17
|
+
`~/.config/Claude/`, with `CLAUDE_USER_DATA_DIR` overriding the root the same
|
|
18
|
+
way the app itself honors it. Until now the Claude app was reachable only
|
|
19
|
+
through an uploaded plugin ZIP, so a user with seven MCP servers configured for
|
|
20
|
+
Claude Code still had an empty `mcpServers` block in Chat and Cowork. The app
|
|
21
|
+
parses this file on startup and reports invalid entries in its own warning
|
|
22
|
+
dialog, so it was always a supported surface — ai-toolkit just never wrote to
|
|
23
|
+
it. Note that `--editor claude` targets Claude Code (`.mcp.json`,
|
|
24
|
+
`~/.claude.json`); the two are different runtimes and different files.
|
|
25
|
+
- **HTTP and SSE templates are bridged through `mcp-remote`.** The app validates
|
|
26
|
+
each entry as `{command, args, env}`; remote endpoints live in a separate
|
|
27
|
+
`remoteMcpServers` surface managed from its Connectors UI and are not
|
|
28
|
+
file-configurable. Writing a portable `{"type":"http","url":…}` entry verbatim
|
|
29
|
+
would produce a config the app silently skips, so the adapter wraps remote
|
|
30
|
+
servers as `npx -y mcp-remote <url>`, forwarding `headers` as `--header`
|
|
31
|
+
arguments. `mcp-remote` negotiates the transport itself, so one bridge shape
|
|
32
|
+
covers both the `http` and `sse` spellings. This also applies to
|
|
33
|
+
`ai-toolkit inject-mcp`, which propagates to every editor exposing a global
|
|
34
|
+
path and therefore now reaches the Claude app too.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- `_resolve_global_config_root` gained a `_validate_configured_config_root`
|
|
39
|
+
helper, shared with the new adapter. `COPILOT_HOME` and `CODEX_HOME` keep
|
|
40
|
+
their existing semantics; the Claude app root is validated only when set
|
|
41
|
+
explicitly, because its platform default need not exist yet.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **Two docs described the `claude` adapter's config path wrongly.**
|
|
46
|
+
`kb/reference/mcp-editor-compatibility.md` and `kb/reference/mcp-templates.md`
|
|
47
|
+
both claimed it writes `.claude/settings.local.json` and
|
|
48
|
+
`~/.claude/settings.json`. It writes `.mcp.json` and `~/.claude.json`, and has
|
|
49
|
+
for as long as the adapter has existed. Both scopes were wrong in both files.
|
|
50
|
+
|
|
51
|
+
### Ecosystem
|
|
52
|
+
|
|
53
|
+
- `claude-app` gains three `config_paths`, the `scripts/mcp_editors.py`
|
|
54
|
+
generator, and an `MCP` capability marker. Re-baseline with
|
|
55
|
+
`python3 scripts/ecosystem_doctor.py --update --tool claude-app`.
|
|
56
|
+
|
|
57
|
+
### Tests
|
|
58
|
+
|
|
59
|
+
- 1645 → 1659. Thirteen cover the adapter (path resolution, parent-tree
|
|
60
|
+
creation, preservation of `preferences`/`coworkUserFilesPath`/user-owned
|
|
61
|
+
servers, byte-identical re-install, the bridge shape, rejection of entries
|
|
62
|
+
with neither or both transports, four `CLAUDE_USER_DATA_DIR` validation cases,
|
|
63
|
+
removal, and transactional rollback). One covers `inject-mcp` propagation,
|
|
64
|
+
where per-editor failures are downgraded to warnings and a regression would
|
|
65
|
+
otherwise be silent.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## v4.27.0 — Doctor sees the Claude app plugin (2026-08-21)
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- **Doctor check 11: plugin double-load.** `ai-toolkit doctor` now reads the
|
|
74
|
+
Claude Code plugin registry (`~/.claude/plugins/installed_plugins.json`) and
|
|
75
|
+
`enabledPlugins` in `~/.claude/settings.json`. When an uploaded Claude app
|
|
76
|
+
plugin is active next to the global install, both feed the same session:
|
|
77
|
+
Claude Code merges plugin hooks with user hooks without deduplication, so every
|
|
78
|
+
toolkit hook fires twice per event and skills and agents load twice. The check
|
|
79
|
+
warns with the hook count, and `--fix` disables the plugin for Claude Code
|
|
80
|
+
while leaving the global install authoritative. Previously this passed as a
|
|
81
|
+
healthy install with no signal at all.
|
|
82
|
+
- `kb/troubleshooting/plugin-double-load.md` documents the symptom, the debug-log
|
|
83
|
+
evidence, and the fix.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
10
87
|
## v4.26.0 — Python floor is declared and enforced (2026-08-21)
|
|
11
88
|
|
|
12
89
|
### Added
|
package/README.md
CHANGED
|
@@ -6,21 +6,22 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](app/skills/)
|
|
8
8
|
[](app/agents/)
|
|
9
|
-
[](tests/)
|
|
10
10
|
|
|
11
|
-
## What's New in v4.
|
|
11
|
+
## What's New in v4.28.0
|
|
12
12
|
|
|
13
|
-
**v4.
|
|
13
|
+
**v4.28.0** gives Claude Chat and Cowork their MCP servers:
|
|
14
14
|
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- `
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
- The Claude app parses `claude_desktop_config.json` on startup and reports bad
|
|
16
|
+
entries in its own warning dialog, but ai-toolkit never wrote to it. A user
|
|
17
|
+
with seven servers configured for Claude Code still had an empty `mcpServers`
|
|
18
|
+
block in Cowork.
|
|
19
|
+
- `ai-toolkit mcp install --editor claude-app --scope global <template>` now
|
|
20
|
+
writes it, on macOS, Windows, and Linux, honoring `CLAUDE_USER_DATA_DIR` the
|
|
21
|
+
way the app does. `--editor claude` remains Claude Code -- different runtime,
|
|
22
|
+
different file.
|
|
23
|
+
- The app accepts stdio entries only, so HTTP/SSE templates are bridged as
|
|
24
|
+
`npx -y mcp-remote <url>` rather than written verbatim and silently skipped.
|
|
24
25
|
|
|
25
26
|
## Table of Contents
|
|
26
27
|
|
|
@@ -139,7 +140,7 @@ See [CLI Reference](kb/reference/cli-reference.md) for all commands and options.
|
|
|
139
140
|
| Platform | Config Files | Hooks | Scope |
|
|
140
141
|
|----------|-------------|:-----:|-------|
|
|
141
142
|
| Claude Code | `~/.claude/agents`, `~/.claude/skills`, `~/.claude/rules/*.md`, `~/.claude/settings.json` | ✅ | global |
|
|
142
|
-
| Claude Chat / Cowork | uploaded plugin ZIP + UI global/folder instructions | Cowork only | account/app |
|
|
143
|
+
| Claude Chat / Cowork | uploaded plugin ZIP + UI global/folder instructions + `claude_desktop_config.json` (MCP) | Cowork only | account/app |
|
|
143
144
|
| Cursor | `.cursor/rules/*.mdc` + `.cursor/mcp.json` + `.cursor/skills/*` | ✅ | project (`~/.cursor/mcp.json` for MCP only) |
|
|
144
145
|
| Windsurf (Devin Desktop) | `~/.config/devin/AGENTS.md` + `.devin/rules/*.md` + `.devin/hooks.v1.json` + `.windsurf/skills/*` | ✅ | global + project |
|
|
145
146
|
| Gemini CLI | `~/.gemini/GEMINI.md` + `.gemini/settings.json` + `.gemini/{commands,skills,agents}/` | ✅ | project + user |
|
|
@@ -152,7 +153,7 @@ See [CLI Reference](kb/reference/cli-reference.md) for all commands and options.
|
|
|
152
153
|
| Codex CLI | Project: `AGENTS.md` + `.agents/skills/*` + `.codex/{agents,hooks}/` + `.codex/{hooks.json,config.toml}`; user: `$CODEX_HOME/{AGENTS.md,agents,hooks.json,config.toml}` + `$HOME/.agents/skills/*` | ✅ | project + user |
|
|
153
154
|
| opencode | `AGENTS.md` + `.opencode/{agents,commands,plugins,skills}/*` + `opencode.{json,jsonc}` | ✅ | project + global (`~/.config/opencode/`) |
|
|
154
155
|
|
|
155
|
-
> Claude Code is always installed (primary platform). Other editors are selected with `--editors`; the Claude app uses the separate `claude-app export` flow because its customization store is UI/plugin-managed. The **Hooks** column marks platforms with lifecycle enforcement. Platforms marked — receive guidance without blocking hooks.
|
|
156
|
+
> Claude Code is always installed (primary platform). Other editors are selected with `--editors`; the Claude app uses the separate `claude-app export` flow because its customization store is UI/plugin-managed, except for MCP servers, which `ai-toolkit mcp install --editor claude-app --scope global` writes straight to `claude_desktop_config.json`. The **Hooks** column marks platforms with lifecycle enforcement. Platforms marked — receive guidance without blocking hooks.
|
|
156
157
|
|
|
157
158
|
---
|
|
158
159
|
|
|
@@ -188,7 +189,7 @@ ai-toolkit/
|
|
|
188
189
|
│ └── ARCHITECTURE.md # Full system design
|
|
189
190
|
├── kb/ # Reference docs, procedures, plans
|
|
190
191
|
├── scripts/ # Validation, install, evaluation scripts
|
|
191
|
-
├── tests/ # Bats and Python test suite (
|
|
192
|
+
├── tests/ # Bats and Python test suite (1659 tests)
|
|
192
193
|
└── CHANGELOG.md
|
|
193
194
|
```
|
|
194
195
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "ai-toolkit",
|
|
4
4
|
"displayName": "AI Toolkit",
|
|
5
5
|
"description": "Professional-grade engineering skills, agents, rules, and lifecycle guardrails for Claude Code, Claude Chat, and Cowork.",
|
|
6
|
-
"version": "4.
|
|
6
|
+
"version": "4.28.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "SoftSpark",
|
|
9
9
|
"url": "https://github.com/softspark"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"last_run": "2026-08-
|
|
2
|
+
"last_run": "2026-08-21T18:54:11Z",
|
|
3
3
|
"schema_version": 1,
|
|
4
4
|
"tools": {
|
|
5
5
|
"aider": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"augment": {
|
|
27
|
-
"docs_hash": "
|
|
27
|
+
"docs_hash": "fda8f5ca2c114b30",
|
|
28
28
|
"headings": [
|
|
29
29
|
"Admin",
|
|
30
30
|
"Auggie CLI",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
"claude-app": {
|
|
69
|
-
"docs_hash": "
|
|
69
|
+
"docs_hash": "73b44d89c7b771d9",
|
|
70
70
|
"headings": [
|
|
71
71
|
"Add global and folder instructions",
|
|
72
72
|
"Availability",
|
|
@@ -101,13 +101,14 @@
|
|
|
101
101
|
"markers": {
|
|
102
102
|
"Folder instructions": true,
|
|
103
103
|
"Global instructions": true,
|
|
104
|
+
"MCP": true,
|
|
104
105
|
"plugins": true,
|
|
105
106
|
"skills": true,
|
|
106
107
|
"sub-agents": true
|
|
107
108
|
}
|
|
108
109
|
},
|
|
109
110
|
"claude-code": {
|
|
110
|
-
"docs_hash": "
|
|
111
|
+
"docs_hash": "1666675fbb24aaad",
|
|
111
112
|
"headings": [
|
|
112
113
|
"Core concepts",
|
|
113
114
|
"Documentation Index",
|
|
@@ -176,7 +177,7 @@
|
|
|
176
177
|
"version": "2.1.238 (Claude Code)"
|
|
177
178
|
},
|
|
178
179
|
"cline": {
|
|
179
|
-
"docs_hash": "
|
|
180
|
+
"docs_hash": "a0753bb220f610ba",
|
|
180
181
|
"headings": [
|
|
181
182
|
"API Reference",
|
|
182
183
|
"Best Practices",
|
|
@@ -223,7 +224,7 @@
|
|
|
223
224
|
}
|
|
224
225
|
},
|
|
225
226
|
"codex-cli": {
|
|
226
|
-
"docs_hash": "
|
|
227
|
+
"docs_hash": "61d85bae5c0cf885",
|
|
227
228
|
"headings": [
|
|
228
229
|
"API",
|
|
229
230
|
"API Reference",
|
|
@@ -365,7 +366,7 @@
|
|
|
365
366
|
"version": "codex-cli 0.148.0"
|
|
366
367
|
},
|
|
367
368
|
"cursor": {
|
|
368
|
-
"docs_hash": "
|
|
369
|
+
"docs_hash": "189e666436c73107",
|
|
369
370
|
"headings": [
|
|
370
371
|
"Agent",
|
|
371
372
|
"CLI",
|
|
@@ -397,7 +398,7 @@
|
|
|
397
398
|
}
|
|
398
399
|
},
|
|
399
400
|
"gemini-cli": {
|
|
400
|
-
"docs_hash": "
|
|
401
|
+
"docs_hash": "547969f433375d0c",
|
|
401
402
|
"headings": [
|
|
402
403
|
"Breadcrumbs",
|
|
403
404
|
"Directory actions",
|
|
@@ -438,7 +439,7 @@
|
|
|
438
439
|
"version": "0.55.1"
|
|
439
440
|
},
|
|
440
441
|
"github-copilot": {
|
|
441
|
-
"docs_hash": "
|
|
442
|
+
"docs_hash": "a313606be316fada",
|
|
442
443
|
"headings": [
|
|
443
444
|
"About Copilot auto model selection",
|
|
444
445
|
"About Copilot automations",
|
|
@@ -475,7 +476,7 @@
|
|
|
475
476
|
}
|
|
476
477
|
},
|
|
477
478
|
"google-antigravity": {
|
|
478
|
-
"docs_hash": "
|
|
479
|
+
"docs_hash": "8cbf9aaffb6b57bf",
|
|
479
480
|
"headings": [],
|
|
480
481
|
"markers": {
|
|
481
482
|
"AGENTS.md": false,
|
|
@@ -502,7 +503,7 @@
|
|
|
502
503
|
}
|
|
503
504
|
},
|
|
504
505
|
"opencode": {
|
|
505
|
-
"docs_hash": "
|
|
506
|
+
"docs_hash": "3e9f45631e71a28c",
|
|
506
507
|
"headings": [
|
|
507
508
|
"Add features",
|
|
508
509
|
"Ask questions",
|
|
@@ -564,7 +565,7 @@
|
|
|
564
565
|
}
|
|
565
566
|
},
|
|
566
567
|
"windsurf": {
|
|
567
|
-
"docs_hash": "
|
|
568
|
+
"docs_hash": "8bf9d2e8b6d36350",
|
|
568
569
|
"headings": [
|
|
569
570
|
"Accounts",
|
|
570
571
|
"Advanced",
|
|
@@ -26,8 +26,11 @@ active `CODEX_HOME` (default `~/.codex`) and shared user skills use
|
|
|
26
26
|
Codex user surface.
|
|
27
27
|
|
|
28
28
|
Claude Chat/Desktop/Cowork is a separate runtime. It does not read the
|
|
29
|
-
filesystem surfaces under `~/.claude`; it receives ai-toolkit
|
|
30
|
-
uploaded plugin plus app-managed global/folder
|
|
29
|
+
filesystem surfaces under `~/.claude`; it receives ai-toolkit skills, agents,
|
|
30
|
+
hooks, and rules through an uploaded plugin plus app-managed global/folder
|
|
31
|
+
instructions. Its one file-based surface is `claude_desktop_config.json`, which
|
|
32
|
+
the app parses for local MCP servers -- `ai-toolkit mcp install --editor
|
|
33
|
+
claude-app --scope global` targets it directly, without a plugin.
|
|
31
34
|
|
|
32
35
|
## Command Responsibilities
|
|
33
36
|
|
|
@@ -3,9 +3,9 @@ title: "AI Toolkit - MCP Editor Compatibility"
|
|
|
3
3
|
category: reference
|
|
4
4
|
service: ai-toolkit
|
|
5
5
|
tags: [mcp, editors, compatibility, codex, cursor, antigravity]
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.4.0"
|
|
7
7
|
created: "2026-04-12"
|
|
8
|
-
last_updated: "2026-08-
|
|
8
|
+
last_updated: "2026-08-21"
|
|
9
9
|
description: "Official MCP support matrix and native config targets for editors supported by ai-toolkit."
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -19,7 +19,8 @@ ai-toolkit keeps `.mcp.json` as the project-level canonical template format and
|
|
|
19
19
|
|
|
20
20
|
| Editor | Scope | Native Config Path | Adapter Behavior |
|
|
21
21
|
|--------|-------|--------------------|------------------|
|
|
22
|
-
| Claude Code | project + global | `.
|
|
22
|
+
| Claude Code | project + global | `.mcp.json`, `~/.claude.json` | Merges `mcpServers` while preserving other top-level keys |
|
|
23
|
+
| Claude Chat / Cowork | global | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS), `%APPDATA%/Claude/claude_desktop_config.json` (Windows), `~/.config/Claude/claude_desktop_config.json` (Linux); `CLAUDE_USER_DATA_DIR` overrides the root | Emits the app's stdio-only `{command, args, env}` schema and bridges HTTP/SSE servers through `mcp-remote`; preserves `preferences`, `coworkUserFilesPath`, and user-owned servers |
|
|
23
24
|
| Cursor | project + global | `.cursor/mcp.json`, `~/.cursor/mcp.json` | Mirrors `mcpServers` directly |
|
|
24
25
|
| GitHub Copilot | project + global | `.github/mcp.json`, `$COPILOT_HOME/mcp-config.json` (default `~/.copilot/mcp-config.json`) | Adds Copilot-required `type` and `tools` fields |
|
|
25
26
|
| Gemini CLI | project + global | `.gemini/settings.json`, `~/.gemini/settings.json` | Merges `mcpServers` into settings JSON |
|
|
@@ -46,9 +47,32 @@ ai-toolkit mcp install --editor cursor --scope project github --target .
|
|
|
46
47
|
ai-toolkit mcp install --editor antigravity --scope project context7 --target .
|
|
47
48
|
ai-toolkit mcp install --editor codex --scope project context7 --target .
|
|
48
49
|
ai-toolkit mcp install --editor codex context7
|
|
50
|
+
ai-toolkit mcp install --editor claude-app --scope global context7
|
|
49
51
|
ai-toolkit mcp remove github --editor cursor --scope project --target .
|
|
50
52
|
```
|
|
51
53
|
|
|
54
|
+
## Claude Chat / Cowork
|
|
55
|
+
|
|
56
|
+
The Claude app is the one target whose MCP surface is file-based while everything
|
|
57
|
+
else about it is not. Skills, agents, hooks, and rules reach Chat/Cowork only
|
|
58
|
+
through the uploaded plugin ZIP (`ai-toolkit claude-app export`), because the app
|
|
59
|
+
scans no filesystem location for them. Local MCP servers are different: the app
|
|
60
|
+
parses `claude_desktop_config.json` on startup and reports invalid entries in its
|
|
61
|
+
own warning dialog.
|
|
62
|
+
|
|
63
|
+
Two constraints shape the adapter:
|
|
64
|
+
|
|
65
|
+
- **stdio only.** Each entry is validated as `{command, args, env}`. Remote
|
|
66
|
+
endpoints live in a separate `remoteMcpServers` surface that the app manages
|
|
67
|
+
through its Connectors UI and does not read from this file. Portable HTTP/SSE
|
|
68
|
+
templates are therefore wrapped in `mcp-remote`, which negotiates the transport
|
|
69
|
+
itself, so one bridge shape covers both spellings.
|
|
70
|
+
- **The file is not ours.** It also holds `preferences` and `coworkUserFilesPath`,
|
|
71
|
+
which the app rewrites on its own. The adapter merges only `mcpServers` and
|
|
72
|
+
leaves every other key untouched.
|
|
73
|
+
|
|
74
|
+
The app must be restarted for a config change to take effect.
|
|
75
|
+
|
|
52
76
|
## Install Flow Integration
|
|
53
77
|
|
|
54
78
|
When `.mcp.json` exists in a project, `ai-toolkit install --local` mirrors its servers into:
|
|
@@ -3,9 +3,9 @@ title: "MCP Server Templates"
|
|
|
3
3
|
category: reference
|
|
4
4
|
service: ai-toolkit
|
|
5
5
|
tags: [mcp, templates, servers, configuration, editors, inject-mcp, external-templates]
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.5.0"
|
|
7
7
|
created: "2026-04-07"
|
|
8
|
-
last_updated: "2026-08-
|
|
8
|
+
last_updated: "2026-08-21"
|
|
9
9
|
description: "Reference for 26 built-in MCP server templates, external template injection via inject-mcp, and native editor MCP installation support."
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -39,6 +39,7 @@ The `add` command merges the `mcpServers` block from the template into `.mcp.jso
|
|
|
39
39
|
The `install` command renders the same canonical template into an editor-native config format:
|
|
40
40
|
- JSON clients with `mcpServers` blocks: Claude Code, Cursor, Gemini CLI, Google Antigravity, Roo Code, Windsurf, Cline, Augment
|
|
41
41
|
- JSON clients with additional transport metadata: GitHub Copilot
|
|
42
|
+
- JSON clients restricted to stdio: Claude Chat/Cowork (`claude-app`) -- remote servers are bridged through `mcp-remote`
|
|
42
43
|
- TOML clients: Codex CLI (`[mcp_servers.<name>]`)
|
|
43
44
|
|
|
44
45
|
When `install` runs with `--scope project`, ai-toolkit also updates the project's `.mcp.json` so it remains the source of truth for later syncs.
|
|
@@ -47,7 +48,8 @@ When `install` runs with `--scope project`, ai-toolkit also updates the project'
|
|
|
47
48
|
|
|
48
49
|
| Editor | Scope | Native Config Path | Notes |
|
|
49
50
|
|--------|-------|--------------------|-------|
|
|
50
|
-
| `claude` | project + global | `.
|
|
51
|
+
| `claude` | project + global | `.mcp.json`, `~/.claude.json` | Preserves unrelated top-level keys |
|
|
52
|
+
| `claude-app` | global | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS), `%APPDATA%/Claude/claude_desktop_config.json` (Windows), `~/.config/Claude/claude_desktop_config.json` (Linux) | Claude Chat/Cowork; stdio-only schema, bridges HTTP/SSE through `mcp-remote`, preserves `preferences` and `coworkUserFilesPath`; `CLAUDE_USER_DATA_DIR` overrides the root |
|
|
51
53
|
| `cursor` | project + global | `.cursor/mcp.json`, `~/.cursor/mcp.json` | Mirrors canonical `mcpServers` |
|
|
52
54
|
| `copilot` | project + global | `.github/mcp.json`, `$COPILOT_HOME/mcp-config.json` (default `~/.copilot/mcp-config.json`) | Adds `type` and `tools: ["*"]` automatically |
|
|
53
55
|
| `gemini` | project + global | `.gemini/settings.json`, `~/.gemini/settings.json` | Uses Gemini CLI `mcpServers` format |
|
|
@@ -49,11 +49,12 @@ The canonical data lives in **`scripts/ecosystem_tools.json`** and is consumed b
|
|
|
49
49
|
| ID | `claude-app` |
|
|
50
50
|
| Docs | https://support.claude.com/en/articles/13345190-get-started-with-claude-cowork |
|
|
51
51
|
| Plugin docs | https://support.claude.com/en/articles/13837440-use-plugins-in-claude |
|
|
52
|
-
| Config surfaces | `Settings > Cowork > Global instructions`, Cowork folder instructions, `Customize > Skills`,
|
|
52
|
+
| Config surfaces | `Settings > Cowork > Global instructions`, Cowork folder instructions, `Customize > Skills`, `Customize > Plugins`, plus the one file-based surface: `claude_desktop_config.json` (macOS `~/Library/Application Support/Claude/`, Windows `%APPDATA%/Claude/`, Linux `~/.config/Claude/`; `CLAUDE_USER_DATA_DIR` overrides the root) |
|
|
53
53
|
| Plugin layout | `.claude-plugin/plugin.json`, `skills/*/SKILL.md`, `agents/*.md`, `hooks/hooks.json`; ai-toolkit uses manifest paths under `claude-app/` for its generated app-only rules and hooks |
|
|
54
|
-
| Our generator | `scripts/claude_app.py` (`ai-toolkit claude-app export`) |
|
|
54
|
+
| Our generator | `scripts/claude_app.py` (`ai-toolkit claude-app export`); `scripts/mcp_editors.py` for the `claude-app` MCP adapter |
|
|
55
55
|
| Runtime split | Skills work in Chat (web/Desktop) and Cowork. Hooks and sub-agents run only in Cowork. Claude app does **not** scan Claude Code's `~/.claude/rules/`, `CLAUDE.md`, or `~/.claude/settings.json`. |
|
|
56
|
-
|
|
|
56
|
+
| MCP | `ai-toolkit mcp install --editor claude-app --scope global <template>` writes `claude_desktop_config.json` directly -- no plugin involved. Entries are validated by the app as `{command, args, env}`; remote endpoints belong to the separate UI-managed `remoteMcpServers` surface, so HTTP/SSE templates are bridged through `mcp-remote`. Restart the app to load a change. |
|
|
57
|
+
| Install/update | Export the ZIP, upload it from `Customize > Plugins`, then paste the generated global-instructions file into `Settings > Cowork > Global instructions`. Re-export/re-upload after toolkit updates. MCP config is the exception -- it updates from the CLI like any other editor. |
|
|
57
58
|
|
|
58
59
|
---
|
|
59
60
|
|
|
@@ -3,10 +3,12 @@ title: "Troubleshooting"
|
|
|
3
3
|
service: ai-toolkit
|
|
4
4
|
category: troubleshooting
|
|
5
5
|
tags: [troubleshooting, debugging]
|
|
6
|
-
last_updated: "2026-
|
|
7
|
-
description: "Section index for ai-toolkit troubleshooting
|
|
6
|
+
last_updated: "2026-08-21"
|
|
7
|
+
description: "Section index for ai-toolkit troubleshooting, covering the Claude app plugin double-load collision."
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Troubleshooting
|
|
11
11
|
|
|
12
|
-
Problem resolution guides.
|
|
12
|
+
Problem resolution guides.
|
|
13
|
+
|
|
14
|
+
- [Claude App Plugin Loads Twice in Claude Code](plugin-double-load.md) -- hooks fire twice after the plugin ZIP is uploaded next to a global install.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Claude App Plugin Loads Twice in Claude Code"
|
|
3
|
+
category: troubleshooting
|
|
4
|
+
service: ai-toolkit
|
|
5
|
+
tags: [plugin, claude-app, hooks, duplication, doctor]
|
|
6
|
+
created: "2026-08-21"
|
|
7
|
+
last_updated: "2026-08-21"
|
|
8
|
+
description: "Every toolkit hook fires twice and skills and agents load twice after the Claude app plugin ZIP is uploaded on a machine that already has the global install. Cause: the app registers the plugin under ~/.claude/plugins, which Claude Code also reads."
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Claude App Plugin Loads Twice in Claude Code
|
|
12
|
+
|
|
13
|
+
## Symptom
|
|
14
|
+
|
|
15
|
+
After uploading `ai-toolkit-claude-app.zip` through `Customize > Plugins`, Claude
|
|
16
|
+
Code sessions get slower and every hook side effect appears twice: duplicate rows
|
|
17
|
+
in `governance.log`, duplicate session state writes, two Stop gates per turn.
|
|
18
|
+
`ai-toolkit doctor` reports a healthy install because it only inspected
|
|
19
|
+
`~/.claude` and `app/plugins`.
|
|
20
|
+
|
|
21
|
+
## Cause
|
|
22
|
+
|
|
23
|
+
The Claude app writes uploaded plugins into
|
|
24
|
+
`~/.claude/plugins/marketplaces/local-desktop-app-uploads/` and sets
|
|
25
|
+
`enabledPlugins` in `~/.claude/settings.json`. Both paths belong to Claude Code
|
|
26
|
+
as well, so Claude Code loads the plugin on top of the global install. The
|
|
27
|
+
bundle carries the same catalog as `~/.claude`, and plugin hooks merge with user
|
|
28
|
+
hooks without deduplication.
|
|
29
|
+
|
|
30
|
+
Confirmed in a Claude Code debug log (`claude --debug -p ...`, then read
|
|
31
|
+
`~/.claude/debug/latest`):
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Loaded 111 unique skills (... user: 111 ...)
|
|
35
|
+
Total plugin skills loaded: 111 (0 duplicate/user-owned entries skipped)
|
|
36
|
+
Total plugin agents loaded: 44
|
|
37
|
+
Read manifest hooks for plugin ai-toolkit (enabled=true): ./claude-app/hooks/hooks.json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The `skills` manifest field adds to the default `skills/` directory instead of
|
|
41
|
+
replacing it, so the plugin contributes its 109 catalog skills plus the 2
|
|
42
|
+
app-only rule skills.
|
|
43
|
+
|
|
44
|
+
## Diagnosis
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
ai-toolkit doctor
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Check 11 reports the collision:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
## 11. Plugin Double-Load
|
|
54
|
+
WARN: ai-toolkit@local-desktop-app-uploads is active next to the global install: 28 toolkit hooks fire twice per event and skills/agents load twice (run: ai-toolkit doctor --fix)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
To see the duplication directly, count hook invocations per source in a fresh
|
|
58
|
+
session log:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
grep -oE "[^\"' ]*hooks/[a-z0-9._-]+\.sh" ~/.claude/debug/latest | sort | uniq -c
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Fix
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
ai-toolkit doctor --fix
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
That sets the plugin to `false` in `enabledPlugins` and leaves the global
|
|
71
|
+
install authoritative. Claude Code then logs
|
|
72
|
+
`enabled=false; will NOT register, plugin is disabled`, and plugin skills and
|
|
73
|
+
agents drop to 0.
|
|
74
|
+
|
|
75
|
+
The global install is the richer surface for Claude Code: it delivers rules as
|
|
76
|
+
real files under `~/.claude/rules/`, which are always in context, while the
|
|
77
|
+
plugin exposes them as an `ai-toolkit-rules` skill the model has to load. The
|
|
78
|
+
plugin also has no `session-context.sh` hook.
|
|
79
|
+
|
|
80
|
+
Keep the plugin enabled only when Claude Code has no global install, for example
|
|
81
|
+
a machine that runs the Claude app alone. In that case the plugin is the single
|
|
82
|
+
source and check 11 stays quiet.
|
|
83
|
+
|
|
84
|
+
## Verification
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
claude --debug -p "ok"
|
|
88
|
+
grep -E "Total plugin (skills|agents) loaded|enabled=" ~/.claude/debug/latest
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Expect `Total plugin skills loaded: 0`, `Total plugin agents loaded: 0`, and the
|
|
92
|
+
`plugin is disabled` line.
|
|
93
|
+
|
|
94
|
+
## Related
|
|
95
|
+
|
|
96
|
+
- `kb/reference/global-install-model.md`
|
|
97
|
+
- `kb/procedures/maintenance-sop.md`
|
package/llms-full.txt
CHANGED
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
- [Unique Features & Differentiators](kb/reference/unique-features.md)
|
|
74
74
|
- [Windows Support](kb/reference/windows-support.md)
|
|
75
75
|
- [Troubleshooting](kb/troubleshooting/README.md)
|
|
76
|
+
- [Claude App Plugin Loads Twice in Claude Code](kb/troubleshooting/plugin-double-load.md)
|
|
76
77
|
|
|
77
78
|
## Skills
|
|
78
79
|
|
|
@@ -11729,8 +11730,11 @@ active `CODEX_HOME` (default `~/.codex`) and shared user skills use
|
|
|
11729
11730
|
Codex user surface.
|
|
11730
11731
|
|
|
11731
11732
|
Claude Chat/Desktop/Cowork is a separate runtime. It does not read the
|
|
11732
|
-
filesystem surfaces under `~/.claude`; it receives ai-toolkit
|
|
11733
|
-
uploaded plugin plus app-managed global/folder
|
|
11733
|
+
filesystem surfaces under `~/.claude`; it receives ai-toolkit skills, agents,
|
|
11734
|
+
hooks, and rules through an uploaded plugin plus app-managed global/folder
|
|
11735
|
+
instructions. Its one file-based surface is `claude_desktop_config.json`, which
|
|
11736
|
+
the app parses for local MCP servers -- `ai-toolkit mcp install --editor
|
|
11737
|
+
claude-app --scope global` targets it directly, without a plugin.
|
|
11734
11738
|
|
|
11735
11739
|
## Command Responsibilities
|
|
11736
11740
|
|
|
@@ -13531,9 +13535,9 @@ title: "AI Toolkit - MCP Editor Compatibility"
|
|
|
13531
13535
|
category: reference
|
|
13532
13536
|
service: ai-toolkit
|
|
13533
13537
|
tags: [mcp, editors, compatibility, codex, cursor, antigravity]
|
|
13534
|
-
version: "1.
|
|
13538
|
+
version: "1.4.0"
|
|
13535
13539
|
created: "2026-04-12"
|
|
13536
|
-
last_updated: "2026-08-
|
|
13540
|
+
last_updated: "2026-08-21"
|
|
13537
13541
|
description: "Official MCP support matrix and native config targets for editors supported by ai-toolkit."
|
|
13538
13542
|
---
|
|
13539
13543
|
|
|
@@ -13547,7 +13551,8 @@ ai-toolkit keeps `.mcp.json` as the project-level canonical template format and
|
|
|
13547
13551
|
|
|
13548
13552
|
| Editor | Scope | Native Config Path | Adapter Behavior |
|
|
13549
13553
|
|--------|-------|--------------------|------------------|
|
|
13550
|
-
| Claude Code | project + global | `.
|
|
13554
|
+
| Claude Code | project + global | `.mcp.json`, `~/.claude.json` | Merges `mcpServers` while preserving other top-level keys |
|
|
13555
|
+
| Claude Chat / Cowork | global | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS), `%APPDATA%/Claude/claude_desktop_config.json` (Windows), `~/.config/Claude/claude_desktop_config.json` (Linux); `CLAUDE_USER_DATA_DIR` overrides the root | Emits the app's stdio-only `{command, args, env}` schema and bridges HTTP/SSE servers through `mcp-remote`; preserves `preferences`, `coworkUserFilesPath`, and user-owned servers |
|
|
13551
13556
|
| Cursor | project + global | `.cursor/mcp.json`, `~/.cursor/mcp.json` | Mirrors `mcpServers` directly |
|
|
13552
13557
|
| GitHub Copilot | project + global | `.github/mcp.json`, `$COPILOT_HOME/mcp-config.json` (default `~/.copilot/mcp-config.json`) | Adds Copilot-required `type` and `tools` fields |
|
|
13553
13558
|
| Gemini CLI | project + global | `.gemini/settings.json`, `~/.gemini/settings.json` | Merges `mcpServers` into settings JSON |
|
|
@@ -13574,9 +13579,32 @@ ai-toolkit mcp install --editor cursor --scope project github --target .
|
|
|
13574
13579
|
ai-toolkit mcp install --editor antigravity --scope project context7 --target .
|
|
13575
13580
|
ai-toolkit mcp install --editor codex --scope project context7 --target .
|
|
13576
13581
|
ai-toolkit mcp install --editor codex context7
|
|
13582
|
+
ai-toolkit mcp install --editor claude-app --scope global context7
|
|
13577
13583
|
ai-toolkit mcp remove github --editor cursor --scope project --target .
|
|
13578
13584
|
```
|
|
13579
13585
|
|
|
13586
|
+
## Claude Chat / Cowork
|
|
13587
|
+
|
|
13588
|
+
The Claude app is the one target whose MCP surface is file-based while everything
|
|
13589
|
+
else about it is not. Skills, agents, hooks, and rules reach Chat/Cowork only
|
|
13590
|
+
through the uploaded plugin ZIP (`ai-toolkit claude-app export`), because the app
|
|
13591
|
+
scans no filesystem location for them. Local MCP servers are different: the app
|
|
13592
|
+
parses `claude_desktop_config.json` on startup and reports invalid entries in its
|
|
13593
|
+
own warning dialog.
|
|
13594
|
+
|
|
13595
|
+
Two constraints shape the adapter:
|
|
13596
|
+
|
|
13597
|
+
- **stdio only.** Each entry is validated as `{command, args, env}`. Remote
|
|
13598
|
+
endpoints live in a separate `remoteMcpServers` surface that the app manages
|
|
13599
|
+
through its Connectors UI and does not read from this file. Portable HTTP/SSE
|
|
13600
|
+
templates are therefore wrapped in `mcp-remote`, which negotiates the transport
|
|
13601
|
+
itself, so one bridge shape covers both spellings.
|
|
13602
|
+
- **The file is not ours.** It also holds `preferences` and `coworkUserFilesPath`,
|
|
13603
|
+
which the app rewrites on its own. The adapter merges only `mcpServers` and
|
|
13604
|
+
leaves every other key untouched.
|
|
13605
|
+
|
|
13606
|
+
The app must be restarted for a config change to take effect.
|
|
13607
|
+
|
|
13580
13608
|
## Install Flow Integration
|
|
13581
13609
|
|
|
13582
13610
|
When `.mcp.json` exists in a project, `ai-toolkit install --local` mirrors its servers into:
|
|
@@ -13620,9 +13648,9 @@ title: "MCP Server Templates"
|
|
|
13620
13648
|
category: reference
|
|
13621
13649
|
service: ai-toolkit
|
|
13622
13650
|
tags: [mcp, templates, servers, configuration, editors, inject-mcp, external-templates]
|
|
13623
|
-
version: "1.
|
|
13651
|
+
version: "1.5.0"
|
|
13624
13652
|
created: "2026-04-07"
|
|
13625
|
-
last_updated: "2026-08-
|
|
13653
|
+
last_updated: "2026-08-21"
|
|
13626
13654
|
description: "Reference for 26 built-in MCP server templates, external template injection via inject-mcp, and native editor MCP installation support."
|
|
13627
13655
|
---
|
|
13628
13656
|
|
|
@@ -13656,6 +13684,7 @@ The `add` command merges the `mcpServers` block from the template into `.mcp.jso
|
|
|
13656
13684
|
The `install` command renders the same canonical template into an editor-native config format:
|
|
13657
13685
|
- JSON clients with `mcpServers` blocks: Claude Code, Cursor, Gemini CLI, Google Antigravity, Roo Code, Windsurf, Cline, Augment
|
|
13658
13686
|
- JSON clients with additional transport metadata: GitHub Copilot
|
|
13687
|
+
- JSON clients restricted to stdio: Claude Chat/Cowork (`claude-app`) -- remote servers are bridged through `mcp-remote`
|
|
13659
13688
|
- TOML clients: Codex CLI (`[mcp_servers.<name>]`)
|
|
13660
13689
|
|
|
13661
13690
|
When `install` runs with `--scope project`, ai-toolkit also updates the project's `.mcp.json` so it remains the source of truth for later syncs.
|
|
@@ -13664,7 +13693,8 @@ When `install` runs with `--scope project`, ai-toolkit also updates the project'
|
|
|
13664
13693
|
|
|
13665
13694
|
| Editor | Scope | Native Config Path | Notes |
|
|
13666
13695
|
|--------|-------|--------------------|-------|
|
|
13667
|
-
| `claude` | project + global | `.
|
|
13696
|
+
| `claude` | project + global | `.mcp.json`, `~/.claude.json` | Preserves unrelated top-level keys |
|
|
13697
|
+
| `claude-app` | global | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS), `%APPDATA%/Claude/claude_desktop_config.json` (Windows), `~/.config/Claude/claude_desktop_config.json` (Linux) | Claude Chat/Cowork; stdio-only schema, bridges HTTP/SSE through `mcp-remote`, preserves `preferences` and `coworkUserFilesPath`; `CLAUDE_USER_DATA_DIR` overrides the root |
|
|
13668
13698
|
| `cursor` | project + global | `.cursor/mcp.json`, `~/.cursor/mcp.json` | Mirrors canonical `mcpServers` |
|
|
13669
13699
|
| `copilot` | project + global | `.github/mcp.json`, `$COPILOT_HOME/mcp-config.json` (default `~/.copilot/mcp-config.json`) | Adds `type` and `tools: ["*"]` automatically |
|
|
13670
13700
|
| `gemini` | project + global | `.gemini/settings.json`, `~/.gemini/settings.json` | Uses Gemini CLI `mcpServers` format |
|
|
@@ -15490,11 +15520,12 @@ The canonical data lives in **`scripts/ecosystem_tools.json`** and is consumed b
|
|
|
15490
15520
|
| ID | `claude-app` |
|
|
15491
15521
|
| Docs | https://support.claude.com/en/articles/13345190-get-started-with-claude-cowork |
|
|
15492
15522
|
| Plugin docs | https://support.claude.com/en/articles/13837440-use-plugins-in-claude |
|
|
15493
|
-
| Config surfaces | `Settings > Cowork > Global instructions`, Cowork folder instructions, `Customize > Skills`,
|
|
15523
|
+
| Config surfaces | `Settings > Cowork > Global instructions`, Cowork folder instructions, `Customize > Skills`, `Customize > Plugins`, plus the one file-based surface: `claude_desktop_config.json` (macOS `~/Library/Application Support/Claude/`, Windows `%APPDATA%/Claude/`, Linux `~/.config/Claude/`; `CLAUDE_USER_DATA_DIR` overrides the root) |
|
|
15494
15524
|
| Plugin layout | `.claude-plugin/plugin.json`, `skills/*/SKILL.md`, `agents/*.md`, `hooks/hooks.json`; ai-toolkit uses manifest paths under `claude-app/` for its generated app-only rules and hooks |
|
|
15495
|
-
| Our generator | `scripts/claude_app.py` (`ai-toolkit claude-app export`) |
|
|
15525
|
+
| Our generator | `scripts/claude_app.py` (`ai-toolkit claude-app export`); `scripts/mcp_editors.py` for the `claude-app` MCP adapter |
|
|
15496
15526
|
| Runtime split | Skills work in Chat (web/Desktop) and Cowork. Hooks and sub-agents run only in Cowork. Claude app does **not** scan Claude Code's `~/.claude/rules/`, `CLAUDE.md`, or `~/.claude/settings.json`. |
|
|
15497
|
-
|
|
|
15527
|
+
| MCP | `ai-toolkit mcp install --editor claude-app --scope global <template>` writes `claude_desktop_config.json` directly -- no plugin involved. Entries are validated by the app as `{command, args, env}`; remote endpoints belong to the separate UI-managed `remoteMcpServers` surface, so HTTP/SSE templates are bridged through `mcp-remote`. Restart the app to load a change. |
|
|
15528
|
+
| Install/update | Export the ZIP, upload it from `Customize > Plugins`, then paste the generated global-instructions file into `Settings > Cowork > Global instructions`. Re-export/re-upload after toolkit updates. MCP config is the exception -- it updates from the CLI like any other editor. |
|
|
15498
15529
|
|
|
15499
15530
|
---
|
|
15500
15531
|
|
|
@@ -16130,13 +16161,117 @@ title: "Troubleshooting"
|
|
|
16130
16161
|
service: ai-toolkit
|
|
16131
16162
|
category: troubleshooting
|
|
16132
16163
|
tags: [troubleshooting, debugging]
|
|
16133
|
-
last_updated: "2026-
|
|
16134
|
-
description: "Section index for ai-toolkit troubleshooting
|
|
16164
|
+
last_updated: "2026-08-21"
|
|
16165
|
+
description: "Section index for ai-toolkit troubleshooting, covering the Claude app plugin double-load collision."
|
|
16135
16166
|
---
|
|
16136
16167
|
|
|
16137
16168
|
# Troubleshooting
|
|
16138
16169
|
|
|
16139
|
-
Problem resolution guides.
|
|
16170
|
+
Problem resolution guides.
|
|
16171
|
+
|
|
16172
|
+
- [Claude App Plugin Loads Twice in Claude Code](plugin-double-load.md) -- hooks fire twice after the plugin ZIP is uploaded next to a global install.
|
|
16173
|
+
|
|
16174
|
+
---
|
|
16175
|
+
|
|
16176
|
+
## kb/troubleshooting/plugin-double-load.md
|
|
16177
|
+
|
|
16178
|
+
---
|
|
16179
|
+
title: "Claude App Plugin Loads Twice in Claude Code"
|
|
16180
|
+
category: troubleshooting
|
|
16181
|
+
service: ai-toolkit
|
|
16182
|
+
tags: [plugin, claude-app, hooks, duplication, doctor]
|
|
16183
|
+
created: "2026-08-21"
|
|
16184
|
+
last_updated: "2026-08-21"
|
|
16185
|
+
description: "Every toolkit hook fires twice and skills and agents load twice after the Claude app plugin ZIP is uploaded on a machine that already has the global install. Cause: the app registers the plugin under ~/.claude/plugins, which Claude Code also reads."
|
|
16186
|
+
---
|
|
16187
|
+
|
|
16188
|
+
# Claude App Plugin Loads Twice in Claude Code
|
|
16189
|
+
|
|
16190
|
+
## Symptom
|
|
16191
|
+
|
|
16192
|
+
After uploading `ai-toolkit-claude-app.zip` through `Customize > Plugins`, Claude
|
|
16193
|
+
Code sessions get slower and every hook side effect appears twice: duplicate rows
|
|
16194
|
+
in `governance.log`, duplicate session state writes, two Stop gates per turn.
|
|
16195
|
+
`ai-toolkit doctor` reports a healthy install because it only inspected
|
|
16196
|
+
`~/.claude` and `app/plugins`.
|
|
16197
|
+
|
|
16198
|
+
## Cause
|
|
16199
|
+
|
|
16200
|
+
The Claude app writes uploaded plugins into
|
|
16201
|
+
`~/.claude/plugins/marketplaces/local-desktop-app-uploads/` and sets
|
|
16202
|
+
`enabledPlugins` in `~/.claude/settings.json`. Both paths belong to Claude Code
|
|
16203
|
+
as well, so Claude Code loads the plugin on top of the global install. The
|
|
16204
|
+
bundle carries the same catalog as `~/.claude`, and plugin hooks merge with user
|
|
16205
|
+
hooks without deduplication.
|
|
16206
|
+
|
|
16207
|
+
Confirmed in a Claude Code debug log (`claude --debug -p ...`, then read
|
|
16208
|
+
`~/.claude/debug/latest`):
|
|
16209
|
+
|
|
16210
|
+
```
|
|
16211
|
+
Loaded 111 unique skills (... user: 111 ...)
|
|
16212
|
+
Total plugin skills loaded: 111 (0 duplicate/user-owned entries skipped)
|
|
16213
|
+
Total plugin agents loaded: 44
|
|
16214
|
+
Read manifest hooks for plugin ai-toolkit (enabled=true): ./claude-app/hooks/hooks.json
|
|
16215
|
+
```
|
|
16216
|
+
|
|
16217
|
+
The `skills` manifest field adds to the default `skills/` directory instead of
|
|
16218
|
+
replacing it, so the plugin contributes its 109 catalog skills plus the 2
|
|
16219
|
+
app-only rule skills.
|
|
16220
|
+
|
|
16221
|
+
## Diagnosis
|
|
16222
|
+
|
|
16223
|
+
```bash
|
|
16224
|
+
ai-toolkit doctor
|
|
16225
|
+
```
|
|
16226
|
+
|
|
16227
|
+
Check 11 reports the collision:
|
|
16228
|
+
|
|
16229
|
+
```
|
|
16230
|
+
## 11. Plugin Double-Load
|
|
16231
|
+
WARN: ai-toolkit@local-desktop-app-uploads is active next to the global install: 28 toolkit hooks fire twice per event and skills/agents load twice (run: ai-toolkit doctor --fix)
|
|
16232
|
+
```
|
|
16233
|
+
|
|
16234
|
+
To see the duplication directly, count hook invocations per source in a fresh
|
|
16235
|
+
session log:
|
|
16236
|
+
|
|
16237
|
+
```bash
|
|
16238
|
+
grep -oE "[^\"' ]*hooks/[a-z0-9._-]+\.sh" ~/.claude/debug/latest | sort | uniq -c
|
|
16239
|
+
```
|
|
16240
|
+
|
|
16241
|
+
## Fix
|
|
16242
|
+
|
|
16243
|
+
```bash
|
|
16244
|
+
ai-toolkit doctor --fix
|
|
16245
|
+
```
|
|
16246
|
+
|
|
16247
|
+
That sets the plugin to `false` in `enabledPlugins` and leaves the global
|
|
16248
|
+
install authoritative. Claude Code then logs
|
|
16249
|
+
`enabled=false; will NOT register, plugin is disabled`, and plugin skills and
|
|
16250
|
+
agents drop to 0.
|
|
16251
|
+
|
|
16252
|
+
The global install is the richer surface for Claude Code: it delivers rules as
|
|
16253
|
+
real files under `~/.claude/rules/`, which are always in context, while the
|
|
16254
|
+
plugin exposes them as an `ai-toolkit-rules` skill the model has to load. The
|
|
16255
|
+
plugin also has no `session-context.sh` hook.
|
|
16256
|
+
|
|
16257
|
+
Keep the plugin enabled only when Claude Code has no global install, for example
|
|
16258
|
+
a machine that runs the Claude app alone. In that case the plugin is the single
|
|
16259
|
+
source and check 11 stays quiet.
|
|
16260
|
+
|
|
16261
|
+
## Verification
|
|
16262
|
+
|
|
16263
|
+
```bash
|
|
16264
|
+
claude --debug -p "ok"
|
|
16265
|
+
grep -E "Total plugin (skills|agents) loaded|enabled=" ~/.claude/debug/latest
|
|
16266
|
+
```
|
|
16267
|
+
|
|
16268
|
+
Expect `Total plugin skills loaded: 0`, `Total plugin agents loaded: 0`, and the
|
|
16269
|
+
`plugin is disabled` line.
|
|
16270
|
+
|
|
16271
|
+
## Related
|
|
16272
|
+
|
|
16273
|
+
- `kb/reference/global-install-model.md`
|
|
16274
|
+
- `kb/procedures/maintenance-sop.md`
|
|
16140
16275
|
|
|
16141
16276
|
---
|
|
16142
16277
|
|
package/llms.txt
CHANGED
|
@@ -73,3 +73,4 @@
|
|
|
73
73
|
- [Unique Features & Differentiators](kb/reference/unique-features.md)
|
|
74
74
|
- [Windows Support](kb/reference/windows-support.md)
|
|
75
75
|
- [Troubleshooting](kb/troubleshooting/README.md)
|
|
76
|
+
- [Claude App Plugin Loads Twice in Claude Code](kb/troubleshooting/plugin-double-load.md)
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softspark/ai-toolkit",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.28.0",
|
|
4
4
|
"description": "AI coding toolkit: 109 skills, 44 agents, 12 developer-tool integrations, recoverable native tool-output filtering, Claude Chat/Cowork export, safety constitution, SARIF audit, and signed npm provenance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/scripts/doctor.py
CHANGED
|
@@ -15,6 +15,8 @@ Checks:
|
|
|
15
15
|
7. Benchmark freshness
|
|
16
16
|
8. Stale rules
|
|
17
17
|
9. URL hook sources
|
|
18
|
+
10. Language rules drift (project-local)
|
|
19
|
+
11. Plugin double-load (Claude app plugin vs global install)
|
|
18
20
|
|
|
19
21
|
Exit codes:
|
|
20
22
|
0 all checks pass
|
|
@@ -44,6 +46,7 @@ HOOKS_DIR = _HOOKS_DIR
|
|
|
44
46
|
RULES_DIR = _RULES_DIR
|
|
45
47
|
EXTERNAL_HOOKS_DIR = _EXTERNAL_HOOKS_DIR
|
|
46
48
|
BENCHMARK_DASHBOARD = toolkit_dir / "benchmarks" / "ecosystem-dashboard.json"
|
|
49
|
+
PLUGIN_REGISTRY = CLAUDE_DIR / "plugins" / "installed_plugins.json"
|
|
47
50
|
|
|
48
51
|
VALID_EVENTS = frozenset({
|
|
49
52
|
"SessionStart", "Notification", "PreToolUse", "PostToolUse", "Stop",
|
|
@@ -668,6 +671,84 @@ def check_language_drift(dr: DiagResult) -> None:
|
|
|
668
671
|
dr.warn(f"{lang} detected but {skill} not injected — run: ai-toolkit install --local --lang {lang}")
|
|
669
672
|
|
|
670
673
|
|
|
674
|
+
# ---------------------------------------------------------------------------
|
|
675
|
+
# Check 11: Plugin Double-Load
|
|
676
|
+
# ---------------------------------------------------------------------------
|
|
677
|
+
|
|
678
|
+
def _registered_toolkit_plugins(registry: dict) -> list[str]:
|
|
679
|
+
"""Return ai-toolkit plugin keys from the Claude Code plugin registry."""
|
|
680
|
+
plugins = registry.get("plugins", {})
|
|
681
|
+
if not isinstance(plugins, dict):
|
|
682
|
+
return []
|
|
683
|
+
return [key for key in plugins if str(key).split("@", 1)[0] == "ai-toolkit"]
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def check_plugin_double_load(dr: DiagResult, fix_mode: bool) -> None:
|
|
687
|
+
"""Warn when the Claude app plugin and the global install both feed Claude Code.
|
|
688
|
+
|
|
689
|
+
Uploading the ``claude-app export`` ZIP from the Claude app registers it under
|
|
690
|
+
``~/.claude/plugins``, which Claude Code reads as well. The plugin carries the
|
|
691
|
+
same skills, agents, and hooks as the global install, and Claude Code merges
|
|
692
|
+
plugin hooks with user hooks without deduplication, so every toolkit hook runs
|
|
693
|
+
twice per event.
|
|
694
|
+
"""
|
|
695
|
+
print()
|
|
696
|
+
print("## 11. Plugin Double-Load")
|
|
697
|
+
|
|
698
|
+
if not PLUGIN_REGISTRY.is_file():
|
|
699
|
+
dr.skip("no Claude Code plugin registry")
|
|
700
|
+
return
|
|
701
|
+
try:
|
|
702
|
+
registry = json.loads(PLUGIN_REGISTRY.read_text(encoding="utf-8"))
|
|
703
|
+
except (OSError, json.JSONDecodeError):
|
|
704
|
+
dr.warn(f"{PLUGIN_REGISTRY} is not valid JSON")
|
|
705
|
+
return
|
|
706
|
+
|
|
707
|
+
keys = _registered_toolkit_plugins(registry)
|
|
708
|
+
if not keys:
|
|
709
|
+
dr.ok("no ai-toolkit plugin registered in Claude Code")
|
|
710
|
+
return
|
|
711
|
+
|
|
712
|
+
settings_json = CLAUDE_DIR / "settings.json"
|
|
713
|
+
try:
|
|
714
|
+
settings = json.loads(settings_json.read_text(encoding="utf-8"))
|
|
715
|
+
except (OSError, json.JSONDecodeError):
|
|
716
|
+
settings = {}
|
|
717
|
+
enabled = settings.get("enabledPlugins", {})
|
|
718
|
+
if not isinstance(enabled, dict):
|
|
719
|
+
enabled = {}
|
|
720
|
+
|
|
721
|
+
active = [key for key in keys if enabled.get(key, True)]
|
|
722
|
+
if not active:
|
|
723
|
+
dr.ok("ai-toolkit plugin registered but disabled for Claude Code")
|
|
724
|
+
return
|
|
725
|
+
|
|
726
|
+
hook_count, _ = _installed_toolkit_hook_count(settings)
|
|
727
|
+
if hook_count == 0:
|
|
728
|
+
dr.ok("ai-toolkit plugin active without global hooks (single source)")
|
|
729
|
+
return
|
|
730
|
+
|
|
731
|
+
for key in active:
|
|
732
|
+
dr.warn(
|
|
733
|
+
f"{key} is active next to the global install: {hook_count} toolkit hooks "
|
|
734
|
+
"fire twice per event and skills/agents load twice "
|
|
735
|
+
"(run: ai-toolkit doctor --fix)"
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
if not fix_mode:
|
|
739
|
+
return
|
|
740
|
+
|
|
741
|
+
enabled.update({key: False for key in active})
|
|
742
|
+
settings["enabledPlugins"] = enabled
|
|
743
|
+
try:
|
|
744
|
+
settings_json.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
|
|
745
|
+
except OSError as exc:
|
|
746
|
+
dr.fail(f"could not disable plugin in settings.json: {exc}")
|
|
747
|
+
return
|
|
748
|
+
for key in active:
|
|
749
|
+
dr.fixed(f"disabled {key} for Claude Code (global install stays authoritative)")
|
|
750
|
+
|
|
751
|
+
|
|
671
752
|
# ---------------------------------------------------------------------------
|
|
672
753
|
# Main
|
|
673
754
|
# ---------------------------------------------------------------------------
|
|
@@ -691,6 +772,7 @@ def main() -> None:
|
|
|
691
772
|
check_stale_rules(dr, fix_mode)
|
|
692
773
|
check_url_hooks(dr, fix_mode)
|
|
693
774
|
check_language_drift(dr)
|
|
775
|
+
check_plugin_double_load(dr, fix_mode)
|
|
694
776
|
|
|
695
777
|
# Summary
|
|
696
778
|
print("========================")
|
|
@@ -110,18 +110,23 @@
|
|
|
110
110
|
"UI: Customize > Plugins",
|
|
111
111
|
".claude-plugin/plugin.json",
|
|
112
112
|
"claude-app/hooks/hooks.json",
|
|
113
|
-
"claude-app/skills/*/SKILL.md"
|
|
113
|
+
"claude-app/skills/*/SKILL.md",
|
|
114
|
+
"~/Library/Application Support/Claude/claude_desktop_config.json",
|
|
115
|
+
"%APPDATA%/Claude/claude_desktop_config.json",
|
|
116
|
+
"~/.config/Claude/claude_desktop_config.json"
|
|
114
117
|
],
|
|
115
118
|
"our_generators": [
|
|
116
|
-
"scripts/claude_app.py"
|
|
119
|
+
"scripts/claude_app.py",
|
|
120
|
+
"scripts/mcp_editors.py"
|
|
117
121
|
],
|
|
118
|
-
"status_note": "Claude Chat/Desktop/Cowork does not scan ~/.claude/rules, CLAUDE.md, or Claude Code's user settings
|
|
122
|
+
"status_note": "Claude Chat/Desktop/Cowork does not scan ~/.claude/rules, CLAUDE.md, or Claude Code's user settings, so skills, agents, hooks, and rules ship only as an uploadable plugin ZIP plus Cowork global instructions. Local MCP servers are the exception: the app reads claude_desktop_config.json directly, so `ai-toolkit mcp install --editor claude-app --scope global` configures them without a plugin. That file accepts stdio entries only; HTTP/SSE templates are bridged through mcp-remote. Skills work in Chat and Cowork; plugin hooks and sub-agents run only in Cowork.",
|
|
119
123
|
"capability_markers": [
|
|
120
124
|
"Global instructions",
|
|
121
125
|
"Folder instructions",
|
|
122
126
|
"plugins",
|
|
123
127
|
"skills",
|
|
124
|
-
"sub-agents"
|
|
128
|
+
"sub-agents",
|
|
129
|
+
"MCP"
|
|
125
130
|
],
|
|
126
131
|
"version_probe": null
|
|
127
132
|
},
|
package/scripts/mcp_editors.py
CHANGED
|
@@ -13,6 +13,7 @@ import json
|
|
|
13
13
|
import math
|
|
14
14
|
import os
|
|
15
15
|
import re
|
|
16
|
+
import sys
|
|
16
17
|
import tempfile
|
|
17
18
|
from dataclasses import dataclass
|
|
18
19
|
from pathlib import Path
|
|
@@ -23,6 +24,18 @@ except ModuleNotFoundError: # pragma: no cover - Python 3.11+ should have tomll
|
|
|
23
24
|
tomllib = None
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
def _claude_app_config_relpath() -> str:
|
|
28
|
+
"""Home-relative Claude app MCP config path for the running platform."""
|
|
29
|
+
if sys.platform == "darwin":
|
|
30
|
+
return "Library/Application Support/Claude/claude_desktop_config.json"
|
|
31
|
+
if os.name == "nt":
|
|
32
|
+
return "AppData/Roaming/Claude/claude_desktop_config.json"
|
|
33
|
+
return ".config/Claude/claude_desktop_config.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
CLAUDE_APP_CONFIG_RELPATH = _claude_app_config_relpath()
|
|
37
|
+
|
|
38
|
+
|
|
26
39
|
EDITOR_SPECS: dict[str, dict[str, str | None]] = {
|
|
27
40
|
"claude": {
|
|
28
41
|
"label": "Claude Code",
|
|
@@ -94,6 +107,13 @@ EDITOR_SPECS: dict[str, dict[str, str | None]] = {
|
|
|
94
107
|
"format": "toml",
|
|
95
108
|
"doc_scope": "project + global",
|
|
96
109
|
},
|
|
110
|
+
"claude-app": {
|
|
111
|
+
"label": "Claude Chat / Cowork",
|
|
112
|
+
"project_path": None,
|
|
113
|
+
"global_path": CLAUDE_APP_CONFIG_RELPATH,
|
|
114
|
+
"format": "json",
|
|
115
|
+
"doc_scope": "global",
|
|
116
|
+
},
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
|
|
@@ -202,6 +222,11 @@ def _resolve_global_config_root(
|
|
|
202
222
|
if not configured:
|
|
203
223
|
return (Path.home() / default_dir).absolute()
|
|
204
224
|
|
|
225
|
+
return _validate_configured_config_root(env_name, configured)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _validate_configured_config_root(env_name: str, configured: str) -> Path:
|
|
229
|
+
"""Validate an operator-supplied config root without resolving symlinks."""
|
|
205
230
|
config_root = Path(configured).expanduser()
|
|
206
231
|
if not config_root.is_absolute():
|
|
207
232
|
raise ValueError(f"Configured {env_name} must be absolute: {configured}")
|
|
@@ -219,6 +244,30 @@ def _resolve_global_config_root(
|
|
|
219
244
|
return config_root
|
|
220
245
|
|
|
221
246
|
|
|
247
|
+
def _resolve_claude_app_config_path(*, home: Path | None) -> Path:
|
|
248
|
+
"""Resolve ``claude_desktop_config.json`` for the Claude app.
|
|
249
|
+
|
|
250
|
+
The Claude app honors ``CLAUDE_USER_DATA_DIR`` for its user-data root, so we
|
|
251
|
+
honor it too. Unlike ``COPILOT_HOME``/``CODEX_HOME`` the default directory is
|
|
252
|
+
platform-specific and need not exist yet -- ``_atomic_write_bytes`` creates it
|
|
253
|
+
-- so only an explicitly configured root is validated.
|
|
254
|
+
"""
|
|
255
|
+
if home is not None:
|
|
256
|
+
return (home / CLAUDE_APP_CONFIG_RELPATH).expanduser().absolute()
|
|
257
|
+
|
|
258
|
+
configured = os.environ.get("CLAUDE_USER_DATA_DIR", "").strip()
|
|
259
|
+
if configured:
|
|
260
|
+
root = _validate_configured_config_root("CLAUDE_USER_DATA_DIR", configured)
|
|
261
|
+
return root / "claude_desktop_config.json"
|
|
262
|
+
|
|
263
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows only
|
|
264
|
+
appdata = os.environ.get("APPDATA", "").strip()
|
|
265
|
+
if appdata:
|
|
266
|
+
return (Path(appdata) / "Claude" / "claude_desktop_config.json").absolute()
|
|
267
|
+
|
|
268
|
+
return (Path.home() / CLAUDE_APP_CONFIG_RELPATH).absolute()
|
|
269
|
+
|
|
270
|
+
|
|
222
271
|
def resolve_editor_path(
|
|
223
272
|
editor: str,
|
|
224
273
|
scope: str,
|
|
@@ -256,6 +305,8 @@ def resolve_editor_path(
|
|
|
256
305
|
default_dir=".codex",
|
|
257
306
|
)
|
|
258
307
|
return codex_home / "config.toml"
|
|
308
|
+
if editor == "claude-app":
|
|
309
|
+
return _resolve_claude_app_config_path(home=home)
|
|
259
310
|
return (home or Path.home()) / str(rel)
|
|
260
311
|
raise ValueError(f"Unsupported scope: {scope}")
|
|
261
312
|
|
|
@@ -509,6 +560,8 @@ def _rollback_config_update(update: ConfigUpdate) -> None:
|
|
|
509
560
|
def _normalize_server(editor: str, server: dict) -> dict:
|
|
510
561
|
if editor == "antigravity":
|
|
511
562
|
return _normalize_antigravity_server(server)
|
|
563
|
+
if editor == "claude-app":
|
|
564
|
+
return _normalize_claude_app_server(server)
|
|
512
565
|
data = copy.deepcopy(server)
|
|
513
566
|
if editor == "copilot":
|
|
514
567
|
if "url" in data:
|
|
@@ -519,6 +572,65 @@ def _normalize_server(editor: str, server: dict) -> dict:
|
|
|
519
572
|
return data
|
|
520
573
|
|
|
521
574
|
|
|
575
|
+
MCP_REMOTE_PACKAGE = "mcp-remote"
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _normalize_claude_app_server(server: dict) -> dict:
|
|
579
|
+
"""Emit the Claude app's stdio-only MCP schema, bridging remote endpoints.
|
|
580
|
+
|
|
581
|
+
``claude_desktop_config.json`` validates each entry as ``{command, args,
|
|
582
|
+
env}``; remote servers live in a separate app-managed ``remoteMcpServers``
|
|
583
|
+
surface that is not file-configurable. Portable HTTP/SSE templates are
|
|
584
|
+
therefore wrapped in ``mcp-remote``, which negotiates the transport itself,
|
|
585
|
+
so one bridge shape covers both the ``http`` and ``sse`` spellings.
|
|
586
|
+
"""
|
|
587
|
+
if not isinstance(server, dict):
|
|
588
|
+
raise ValueError("Claude app MCP server configuration must be an object")
|
|
589
|
+
|
|
590
|
+
data = copy.deepcopy(server)
|
|
591
|
+
for key in ("_source", "transport", "type", "tools"):
|
|
592
|
+
data.pop(key, None)
|
|
593
|
+
|
|
594
|
+
url = data.pop("url", None)
|
|
595
|
+
server_url = data.pop("serverUrl", None)
|
|
596
|
+
if url is not None and server_url is not None:
|
|
597
|
+
raise ValueError(
|
|
598
|
+
"Claude app MCP server requires exactly one of 'url' or 'serverUrl'"
|
|
599
|
+
)
|
|
600
|
+
url = url if url is not None else server_url
|
|
601
|
+
|
|
602
|
+
if "command" in data and url is not None:
|
|
603
|
+
raise ValueError(
|
|
604
|
+
"Claude app MCP server requires either 'command' or a URL, not both"
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
if url is not None:
|
|
608
|
+
if not isinstance(url, str) or not url.strip():
|
|
609
|
+
raise ValueError("Claude app MCP server URL must be a non-empty string")
|
|
610
|
+
headers = data.pop("headers", None)
|
|
611
|
+
args = ["-y", MCP_REMOTE_PACKAGE, url.strip()]
|
|
612
|
+
if headers is not None:
|
|
613
|
+
if not isinstance(headers, dict):
|
|
614
|
+
raise ValueError("Claude app MCP 'headers' must be an object")
|
|
615
|
+
for name, value in headers.items():
|
|
616
|
+
if not isinstance(name, str) or not isinstance(value, str):
|
|
617
|
+
raise ValueError(
|
|
618
|
+
"Claude app MCP 'headers' must map strings to strings"
|
|
619
|
+
)
|
|
620
|
+
args.extend(["--header", f"{name}: {value}"])
|
|
621
|
+
bridged: dict = {"command": "npx", "args": args}
|
|
622
|
+
if "env" in data:
|
|
623
|
+
bridged["env"] = data["env"]
|
|
624
|
+
return bridged
|
|
625
|
+
|
|
626
|
+
command = data.get("command")
|
|
627
|
+
if not isinstance(command, str) or not command.strip():
|
|
628
|
+
raise ValueError(
|
|
629
|
+
"Claude app MCP server requires a non-empty 'command' or a URL"
|
|
630
|
+
)
|
|
631
|
+
return {key: data[key] for key in ("command", "args", "env") if key in data}
|
|
632
|
+
|
|
633
|
+
|
|
522
634
|
def _normalize_antigravity_server(server: dict) -> dict:
|
|
523
635
|
"""Validate and emit Antigravity's current ``serverUrl`` MCP schema."""
|
|
524
636
|
if not isinstance(server, dict):
|