a11y-devkit-deploy 0.9.3 → 0.9.4

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/README.md CHANGED
@@ -218,14 +218,21 @@ Add an object to the `hostApplications` array with the host application's config
218
218
  **Host Application Configuration Properties:**
219
219
  - `id` - Unique identifier for the host application
220
220
  - `displayName` - Human-readable name shown in prompts
221
- - `mcpServerKey` - MCP config key name (`"servers"` or `"mcpServers"`)
221
+ - `mcpServerKey` - MCP config key name (`"servers"`, `"mcpServers"`, or `"mcp_servers"` for TOML)
222
222
  - `skillsFolder` - Path to skills directory (relative to home/project root)
223
- - `mcpConfigFile` - Path to MCP config file (relative to home/project root)
223
+ - `mcpConfigFile` - Path to MCP config file (relative to home/project root). Supports both JSON (`.json`) and TOML (`.toml`) formats. TOML format is auto-detected by file extension (used by Codex).
224
224
  - `globalMcpConfigFile` - (Optional) Path to global MCP config relative to AppData/Application Support instead of home directory. Used for hosts like VSCode that store configs in platform-specific app directories:
225
225
  - Windows: `%APPDATA%` (e.g., `C:\Users\name\AppData\Roaming`)
226
226
  - macOS: `~/Library/Application Support`
227
227
  - Linux: `$XDG_CONFIG_HOME` or `~/.config`
228
228
 
229
+ **Note:** Codex uses TOML format for its MCP configuration (`~/.codex/config.toml`), which requires the `mcpServerKey` to be `"mcp_servers"` and generates config entries like:
230
+ ```toml
231
+ [mcp_servers.magentaa11y]
232
+ command = "npx"
233
+ args = ["-y", "magentaa11y-mcp"]
234
+ ```
235
+
229
236
  ### Config Structure
230
237
 
231
238
  - `skillsFolder` - Subfolder name to bundle skills under (e.g., "a11y")
@@ -255,7 +262,7 @@ your-project/
255
262
  │ ├── mcp.json # Cursor MCP config
256
263
  │ └── skills/ # Cursor skills
257
264
  ├── .codex/
258
- │ ├── mcp.json # Codex MCP config
265
+ │ ├── config.toml # Codex MCP config
259
266
  │ └── skills/ # Codex skills
260
267
  ├── .github/
261
268
  │ ├── mcp.json # VSCode MCP config
@@ -277,7 +284,7 @@ your-project/
277
284
  ├── mcp.json # Cursor global MCP config
278
285
  └── skills/ # Cursor global skills
279
286
  ~/.codex/
280
- ├── mcp.json # Codex global MCP config
287
+ ├── config.toml # Codex global MCP config
281
288
  └── skills/ # Codex global skills
282
289
  ~/.github/
283
290
  └── skills/ # VSCode global skills
@@ -63,9 +63,9 @@
63
63
  {
64
64
  "id": "codex",
65
65
  "displayName": "Codex",
66
- "mcpServerKey": "servers",
66
+ "mcpServerKey": "mcp_servers",
67
67
  "skillsFolder": ".codex/skills",
68
- "mcpConfigFile": ".codex/mcp.json"
68
+ "mcpConfigFile": ".codex/config.toml"
69
69
  },
70
70
  {
71
71
  "id": "vscode",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a11y-devkit-deploy",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "CLI to deploy a11y skills and MCP servers across IDEs",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,6 +10,10 @@ async function pathExists(target) {
10
10
  }
11
11
  }
12
12
 
13
+ function isTomlFile(filePath) {
14
+ return filePath.endsWith('.toml');
15
+ }
16
+
13
17
  async function loadJson(filePath) {
14
18
  if (!(await pathExists(filePath))) {
15
19
  return {};
@@ -25,6 +29,120 @@ async function loadJson(filePath) {
25
29
  }
26
30
  }
27
31
 
32
+ // Simple TOML parser for MCP server configs
33
+ function parseSimpleToml(content) {
34
+ const result = {};
35
+ const lines = content.split('\n');
36
+ let currentSection = null;
37
+ let currentTable = null;
38
+
39
+ for (const line of lines) {
40
+ const trimmed = line.trim();
41
+
42
+ // Skip empty lines and comments
43
+ if (!trimmed || trimmed.startsWith('#')) continue;
44
+
45
+ // Parse table headers like [mcp_servers.name]
46
+ const tableMatch = trimmed.match(/^\[([^\]]+)\]$/);
47
+ if (tableMatch) {
48
+ const parts = tableMatch[1].split('.');
49
+ if (parts.length === 2) {
50
+ const [section, table] = parts;
51
+ if (!result[section]) result[section] = {};
52
+ if (!result[section][table]) result[section][table] = {};
53
+ currentSection = section;
54
+ currentTable = table;
55
+ }
56
+ continue;
57
+ }
58
+
59
+ // Parse key-value pairs
60
+ const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
61
+ if (kvMatch && currentSection && currentTable) {
62
+ const [, key, value] = kvMatch;
63
+
64
+ // Parse arrays like ["a", "b"]
65
+ if (value.startsWith('[')) {
66
+ const arrayMatch = value.match(/\[(.*)\]/);
67
+ if (arrayMatch) {
68
+ const items = arrayMatch[1].split(',').map(item => {
69
+ const trimmedItem = item.trim();
70
+ // Remove quotes
71
+ if (trimmedItem.startsWith('"') && trimmedItem.endsWith('"')) {
72
+ return trimmedItem.slice(1, -1);
73
+ }
74
+ return trimmedItem;
75
+ });
76
+ result[currentSection][currentTable][key] = items;
77
+ }
78
+ }
79
+ // Parse strings
80
+ else if (value.startsWith('"') && value.endsWith('"')) {
81
+ result[currentSection][currentTable][key] = value.slice(1, -1);
82
+ }
83
+ // Parse other values
84
+ else {
85
+ result[currentSection][currentTable][key] = value;
86
+ }
87
+ }
88
+ }
89
+
90
+ return result;
91
+ }
92
+
93
+ // Simple TOML stringifier for MCP server configs
94
+ function stringifySimpleToml(obj) {
95
+ const lines = [];
96
+
97
+ for (const [section, tables] of Object.entries(obj)) {
98
+ if (typeof tables !== 'object' || tables === null) continue;
99
+
100
+ for (const [tableName, config] of Object.entries(tables)) {
101
+ if (typeof config !== 'object' || config === null) continue;
102
+
103
+ // Write table header [section.tableName]
104
+ lines.push(`[${section}.${tableName}]`);
105
+
106
+ // Write key-value pairs
107
+ for (const [key, value] of Object.entries(config)) {
108
+ if (Array.isArray(value)) {
109
+ // Format arrays
110
+ const arrayStr = value.map(v => `"${v}"`).join(', ');
111
+ lines.push(`${key} = [${arrayStr}]`);
112
+ } else if (typeof value === 'string') {
113
+ lines.push(`${key} = "${value}"`);
114
+ } else {
115
+ lines.push(`${key} = ${value}`);
116
+ }
117
+ }
118
+
119
+ // Add blank line between tables
120
+ lines.push('');
121
+ }
122
+ }
123
+
124
+ return lines.join('\n');
125
+ }
126
+
127
+ async function loadToml(filePath) {
128
+ if (!(await pathExists(filePath))) {
129
+ return {};
130
+ }
131
+
132
+ try {
133
+ const raw = await fs.readFile(filePath, "utf8");
134
+ return raw.trim() ? parseSimpleToml(raw) : {};
135
+ } catch (error) {
136
+ const backupPath = `${filePath}.bak`;
137
+ await fs.copyFile(filePath, backupPath);
138
+ return {};
139
+ }
140
+ }
141
+
142
+ async function loadConfig(filePath) {
143
+ return isTomlFile(filePath) ? loadToml(filePath) : loadJson(filePath);
144
+ }
145
+
28
146
  function mergeServers(existing, incoming, serverKey = "servers") {
29
147
  const existingServers = existing[serverKey] && typeof existing[serverKey] === "object"
30
148
  ? existing[serverKey]
@@ -84,9 +202,14 @@ function removeServers(existing, removeNames, serverKey = "servers") {
84
202
 
85
203
  async function installMcpConfig(configPath, servers, serverKey = "servers") {
86
204
  await fs.mkdir(path.dirname(configPath), { recursive: true });
87
- const existing = await loadJson(configPath);
205
+ const existing = await loadConfig(configPath);
88
206
  const updated = mergeServers(existing, servers, serverKey);
89
- await fs.writeFile(configPath, `${JSON.stringify(updated, null, 2)}\n`, "utf8");
207
+
208
+ if (isTomlFile(configPath)) {
209
+ await fs.writeFile(configPath, stringifySimpleToml(updated), "utf8");
210
+ } else {
211
+ await fs.writeFile(configPath, `${JSON.stringify(updated, null, 2)}\n`, "utf8");
212
+ }
90
213
  }
91
214
 
92
215
  async function removeMcpConfig(configPath, serverNames, serverKey = "servers") {
@@ -94,14 +217,18 @@ async function removeMcpConfig(configPath, serverNames, serverKey = "servers") {
94
217
  return { removed: 0, changed: false };
95
218
  }
96
219
 
97
- const existing = await loadJson(configPath);
220
+ const existing = await loadConfig(configPath);
98
221
  const { updated, removed } = removeServers(existing, serverNames, serverKey);
99
222
 
100
223
  if (removed === 0) {
101
224
  return { removed: 0, changed: false };
102
225
  }
103
226
 
104
- await fs.writeFile(configPath, `${JSON.stringify(updated, null, 2)}\n`, "utf8");
227
+ if (isTomlFile(configPath)) {
228
+ await fs.writeFile(configPath, stringifySimpleToml(updated), "utf8");
229
+ } else {
230
+ await fs.writeFile(configPath, `${JSON.stringify(updated, null, 2)}\n`, "utf8");
231
+ }
105
232
  return { removed, changed: true };
106
233
  }
107
234