@evantahler/mcpx 0.18.5 → 0.18.7
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/.claude/skills/mcpx.md +14 -14
- package/.cursor/rules/mcpx.mdc +14 -14
- package/README.md +23 -19
- package/package.json +1 -1
- package/src/client/browser.ts +29 -8
- package/src/client/debug-fetch.ts +15 -4
- package/src/commands/add.ts +39 -23
- package/src/config/loader.ts +5 -2
- package/src/output/formatter.ts +7 -4
- package/src/validation/schema.ts +3 -2
package/.claude/skills/mcpx.md
CHANGED
|
@@ -199,20 +199,20 @@ mcpx deauth <server> # remove stored auth
|
|
|
199
199
|
|
|
200
200
|
## `add` options
|
|
201
201
|
|
|
202
|
-
| Flag
|
|
203
|
-
|
|
|
204
|
-
| `--command <cmd>`
|
|
205
|
-
| `--args <
|
|
206
|
-
| `--env <KEY=VAL
|
|
207
|
-
| `--cwd <dir>`
|
|
208
|
-
| `--url <url>`
|
|
209
|
-
| `--header <Key:Value>`
|
|
210
|
-
| `--transport <type>`
|
|
211
|
-
| `--allowed-tools <
|
|
212
|
-
| `--disabled-tools <
|
|
213
|
-
| `-f, --force`
|
|
214
|
-
| `--no-auth`
|
|
215
|
-
| `--no-index`
|
|
202
|
+
| Flag | Purpose |
|
|
203
|
+
| ------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
|
204
|
+
| `--command <cmd>` | Command to run (stdio server) |
|
|
205
|
+
| `--args <arg>` | Argument for the command. Repeatable, or comma-separated. Tokens after `--` are also appended (stdio only). |
|
|
206
|
+
| `--env <KEY=VAL>` | Environment variable. Repeatable, or comma-separated. |
|
|
207
|
+
| `--cwd <dir>` | Working directory for the command |
|
|
208
|
+
| `--url <url>` | Server URL (HTTP server) |
|
|
209
|
+
| `--header <Key:Value>` | HTTP header. Repeatable. |
|
|
210
|
+
| `--transport <type>` | Transport: `sse` or `streamable-http` |
|
|
211
|
+
| `--allowed-tools <pat>` | Allowed tool pattern. Repeatable, or comma-separated. |
|
|
212
|
+
| `--disabled-tools <pat>` | Disabled tool pattern. Repeatable, or comma-separated. |
|
|
213
|
+
| `-f, --force` | Overwrite if server already exists |
|
|
214
|
+
| `--no-auth` | Skip automatic OAuth after adding |
|
|
215
|
+
| `--no-index` | Skip rebuilding the search index |
|
|
216
216
|
|
|
217
217
|
## `remove` options
|
|
218
218
|
|
package/.cursor/rules/mcpx.mdc
CHANGED
|
@@ -193,20 +193,20 @@ mcpx deauth <server> # remove stored auth
|
|
|
193
193
|
|
|
194
194
|
## `add` options
|
|
195
195
|
|
|
196
|
-
| Flag
|
|
197
|
-
|
|
|
198
|
-
| `--command <cmd>`
|
|
199
|
-
| `--args <
|
|
200
|
-
| `--env <KEY=VAL
|
|
201
|
-
| `--cwd <dir>`
|
|
202
|
-
| `--url <url>`
|
|
203
|
-
| `--header <Key:Value>`
|
|
204
|
-
| `--transport <type>`
|
|
205
|
-
| `--allowed-tools <
|
|
206
|
-
| `--disabled-tools <
|
|
207
|
-
| `-f, --force`
|
|
208
|
-
| `--no-auth`
|
|
209
|
-
| `--no-index`
|
|
196
|
+
| Flag | Purpose |
|
|
197
|
+
| ------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
|
198
|
+
| `--command <cmd>` | Command to run (stdio server) |
|
|
199
|
+
| `--args <arg>` | Argument for the command. Repeatable, or comma-separated. Tokens after `--` are also appended (stdio only). |
|
|
200
|
+
| `--env <KEY=VAL>` | Environment variable. Repeatable, or comma-separated. |
|
|
201
|
+
| `--cwd <dir>` | Working directory for the command |
|
|
202
|
+
| `--url <url>` | Server URL (HTTP server) |
|
|
203
|
+
| `--header <Key:Value>` | HTTP header. Repeatable. |
|
|
204
|
+
| `--transport <type>` | Transport: `sse` or `streamable-http` |
|
|
205
|
+
| `--allowed-tools <pat>` | Allowed tool pattern. Repeatable, or comma-separated. |
|
|
206
|
+
| `--disabled-tools <pat>` | Disabled tool pattern. Repeatable, or comma-separated. |
|
|
207
|
+
| `-f, --force` | Overwrite if server already exists |
|
|
208
|
+
| `--no-auth` | Skip automatic OAuth after adding |
|
|
209
|
+
| `--no-index` | Skip rebuilding the search index |
|
|
210
210
|
|
|
211
211
|
## `remove` options
|
|
212
212
|
|
package/README.md
CHANGED
|
@@ -138,20 +138,24 @@ Server log messages (`notifications/message`) are displayed on stderr with level
|
|
|
138
138
|
Add and remove servers from the CLI — no manual JSON editing required.
|
|
139
139
|
|
|
140
140
|
```bash
|
|
141
|
-
# Add a stdio server
|
|
141
|
+
# Add a stdio server (anything after `--` is passed to the command verbatim)
|
|
142
|
+
mcpx add filesystem --command npx -- -y @modelcontextprotocol/server-filesystem /tmp
|
|
143
|
+
|
|
144
|
+
# Equivalent forms: repeatable --args, or a single comma-separated --args
|
|
145
|
+
mcpx add filesystem --command npx --args -y --args @modelcontextprotocol/server-filesystem --args /tmp
|
|
142
146
|
mcpx add filesystem --command npx --args "-y,@modelcontextprotocol/server-filesystem,/tmp"
|
|
143
147
|
|
|
144
148
|
# Add an HTTP server with headers
|
|
145
149
|
mcpx add my-api --url https://api.example.com/mcp --header "Authorization:Bearer tok123"
|
|
146
150
|
|
|
147
|
-
# Add with tool filtering
|
|
148
|
-
mcpx add github --url https://mcp.github.com --allowed-tools "search_
|
|
151
|
+
# Add with tool filtering (repeatable, or comma-separated)
|
|
152
|
+
mcpx add github --url https://mcp.github.com --allowed-tools "search_*" --allowed-tools "get_*"
|
|
149
153
|
|
|
150
154
|
# Add a legacy SSE server (explicit transport)
|
|
151
155
|
mcpx add legacy-api --url https://api.example.com/sse --transport sse
|
|
152
156
|
|
|
153
|
-
# Add with environment variables
|
|
154
|
-
mcpx add my-server --command node --args
|
|
157
|
+
# Add with environment variables (repeatable, or comma-separated)
|
|
158
|
+
mcpx add my-server --command node --args server.js --env API_KEY=sk-123 --env DEBUG=true
|
|
155
159
|
|
|
156
160
|
# Overwrite an existing server
|
|
157
161
|
mcpx add filesystem --command echo --force
|
|
@@ -168,20 +172,20 @@ mcpx remove my-api --dry-run
|
|
|
168
172
|
|
|
169
173
|
**`add` options:**
|
|
170
174
|
|
|
171
|
-
| Flag | Purpose
|
|
172
|
-
| -------------------------- |
|
|
173
|
-
| `--command <cmd>` | Command to run (stdio server)
|
|
174
|
-
| `--args <
|
|
175
|
-
| `--env <KEY=VAL
|
|
176
|
-
| `--cwd <dir>` | Working directory for the command
|
|
177
|
-
| `--url <url>` | Server URL (HTTP server)
|
|
178
|
-
| `--header <Key:Value>` | HTTP header
|
|
179
|
-
| `--transport <type>` | Transport: `sse` or `streamable-http`
|
|
180
|
-
| `--allowed-tools <
|
|
181
|
-
| `--disabled-tools <
|
|
182
|
-
| `-f, --force` | Overwrite if server already exists
|
|
183
|
-
| `--no-auth` | Skip automatic OAuth after adding
|
|
184
|
-
| `--no-index` | Skip rebuilding the search index
|
|
175
|
+
| Flag | Purpose |
|
|
176
|
+
| -------------------------- | ---------------------------------------------------------------------- |
|
|
177
|
+
| `--command <cmd>` | Command to run (stdio server) |
|
|
178
|
+
| `--args <arg>` | Argument for the command. Repeatable, or comma-separated. Tokens after `--` are also appended (stdio only). |
|
|
179
|
+
| `--env <KEY=VAL>` | Environment variable. Repeatable, or comma-separated. |
|
|
180
|
+
| `--cwd <dir>` | Working directory for the command |
|
|
181
|
+
| `--url <url>` | Server URL (HTTP server) |
|
|
182
|
+
| `--header <Key:Value>` | HTTP header. Repeatable. |
|
|
183
|
+
| `--transport <type>` | Transport: `sse` or `streamable-http` |
|
|
184
|
+
| `--allowed-tools <pat>` | Allowed tool pattern. Repeatable, or comma-separated. |
|
|
185
|
+
| `--disabled-tools <pat>` | Disabled tool pattern. Repeatable, or comma-separated. |
|
|
186
|
+
| `-f, --force` | Overwrite if server already exists |
|
|
187
|
+
| `--no-auth` | Skip automatic OAuth after adding |
|
|
188
|
+
| `--no-index` | Skip rebuilding the search index |
|
|
185
189
|
|
|
186
190
|
**`remove` options:**
|
|
187
191
|
|
package/package.json
CHANGED
package/src/client/browser.ts
CHANGED
|
@@ -1,20 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Open a URL in the default browser (macOS/Windows/Linux).
|
|
5
5
|
* Falls back to printing the URL to stderr if no browser is available
|
|
6
6
|
* (e.g., headless servers, Docker containers).
|
|
7
|
+
*
|
|
8
|
+
* Uses execFile (not exec) to avoid shell injection via malicious URLs.
|
|
7
9
|
*/
|
|
8
10
|
export function openBrowser(url: string): Promise<void> {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
// Validate URL scheme to prevent non-HTTP protocols
|
|
12
|
+
try {
|
|
13
|
+
const parsed = new URL(url);
|
|
14
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
15
|
+
process.stderr.write(`Refusing to open non-HTTP URL: ${url}\n`);
|
|
16
|
+
return Promise.resolve();
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
process.stderr.write(`Invalid URL: ${url}\n`);
|
|
20
|
+
return Promise.resolve();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let cmd: string;
|
|
24
|
+
let args: string[];
|
|
25
|
+
|
|
26
|
+
if (process.platform === "darwin") {
|
|
27
|
+
cmd = "open";
|
|
28
|
+
args = [url];
|
|
29
|
+
} else if (process.platform === "win32") {
|
|
30
|
+
cmd = "cmd";
|
|
31
|
+
args = ["/c", "start", "", url];
|
|
32
|
+
} else {
|
|
33
|
+
cmd = "xdg-open";
|
|
34
|
+
args = [url];
|
|
35
|
+
}
|
|
15
36
|
|
|
16
37
|
return new Promise((resolve) => {
|
|
17
|
-
|
|
38
|
+
execFile(cmd, args, (err) => {
|
|
18
39
|
if (err) {
|
|
19
40
|
process.stderr.write(`Could not open browser. Please visit:\n ${url}\n`);
|
|
20
41
|
}
|
|
@@ -68,11 +68,22 @@ function logBody(body: string, fmt: (s: string) => string) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
const SENSITIVE_HEADERS = new Set([
|
|
72
|
+
"authorization",
|
|
73
|
+
"cookie",
|
|
74
|
+
"set-cookie",
|
|
75
|
+
"proxy-authorization",
|
|
76
|
+
"x-api-key",
|
|
77
|
+
"api-key",
|
|
78
|
+
"x-auth-token",
|
|
79
|
+
"x-token",
|
|
80
|
+
"token",
|
|
81
|
+
]);
|
|
82
|
+
|
|
71
83
|
export function maskSensitive(key: string, value: string): string {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return `${value.slice(0, 12)}...`;
|
|
84
|
+
if (SENSITIVE_HEADERS.has(key.toLowerCase())) {
|
|
85
|
+
if (value.length <= 6) return "***";
|
|
86
|
+
return `${value.slice(0, 4)}...`;
|
|
76
87
|
}
|
|
77
88
|
return value;
|
|
78
89
|
}
|
package/src/commands/add.ts
CHANGED
|
@@ -6,33 +6,34 @@ import { runIndex } from "./index.ts";
|
|
|
6
6
|
|
|
7
7
|
export function registerAddCommand(program: Command) {
|
|
8
8
|
program
|
|
9
|
-
.command("add <name>")
|
|
9
|
+
.command("add <name> [passthroughArgs...]")
|
|
10
10
|
.description("add an MCP server to your config")
|
|
11
11
|
.option("--command <cmd>", "command to run (stdio server)")
|
|
12
|
-
.option("--args <
|
|
13
|
-
.option("--env <
|
|
12
|
+
.option("--args <arg>", "argument for the command (repeatable, comma-separated, or pass after --)", collect, [])
|
|
13
|
+
.option("--env <KEY=VAL>", "environment variable (repeatable or comma-separated)", collect, [])
|
|
14
14
|
.option("--cwd <dir>", "working directory for the command")
|
|
15
15
|
.option("--url <url>", "server URL (HTTP server)")
|
|
16
16
|
.option("--header <h>", "header in Key:Value format (repeatable)", collect, [])
|
|
17
17
|
.option("--transport <type>", 'transport for HTTP servers: "sse" or "streamable-http"')
|
|
18
|
-
.option("--allowed-tools <
|
|
19
|
-
.option("--disabled-tools <
|
|
18
|
+
.option("--allowed-tools <pattern>", "allowed tool pattern (repeatable or comma-separated)", collect, [])
|
|
19
|
+
.option("--disabled-tools <pattern>", "disabled tool pattern (repeatable or comma-separated)", collect, [])
|
|
20
20
|
.option("-f, --force", "overwrite if server already exists")
|
|
21
21
|
.option("--no-auth", "skip automatic OAuth authentication after adding an HTTP server")
|
|
22
22
|
.option("--no-index", "skip rebuilding the search index after adding")
|
|
23
23
|
.action(
|
|
24
24
|
async (
|
|
25
25
|
name: string,
|
|
26
|
+
passthroughArgs: string[],
|
|
26
27
|
options: {
|
|
27
28
|
command?: string;
|
|
28
|
-
args
|
|
29
|
-
env
|
|
29
|
+
args: string[];
|
|
30
|
+
env: string[];
|
|
30
31
|
cwd?: string;
|
|
31
32
|
url?: string;
|
|
32
|
-
header
|
|
33
|
+
header: string[];
|
|
33
34
|
transport?: string;
|
|
34
|
-
allowedTools
|
|
35
|
-
disabledTools
|
|
35
|
+
allowedTools: string[];
|
|
36
|
+
disabledTools: string[];
|
|
36
37
|
force?: boolean;
|
|
37
38
|
auth?: boolean;
|
|
38
39
|
index?: boolean;
|
|
@@ -49,6 +50,10 @@ export function registerAddCommand(program: Command) {
|
|
|
49
50
|
console.error("Cannot specify both --command and --url");
|
|
50
51
|
process.exit(1);
|
|
51
52
|
}
|
|
53
|
+
if (!hasCommand && passthroughArgs.length > 0) {
|
|
54
|
+
console.error("Positional arguments after -- only apply to stdio servers (--command)");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
52
57
|
|
|
53
58
|
const configFlag = program.opts().config;
|
|
54
59
|
const { configDir, servers } = await loadRawServers(configFlag);
|
|
@@ -61,7 +66,7 @@ export function registerAddCommand(program: Command) {
|
|
|
61
66
|
let config: ServerConfig;
|
|
62
67
|
|
|
63
68
|
if (hasCommand) {
|
|
64
|
-
config = buildStdioConfig(options);
|
|
69
|
+
config = buildStdioConfig(options, passthroughArgs);
|
|
65
70
|
} else {
|
|
66
71
|
config = buildHttpConfig(options);
|
|
67
72
|
}
|
|
@@ -74,12 +79,13 @@ export function registerAddCommand(program: Command) {
|
|
|
74
79
|
(config as { transport: string }).transport = options.transport;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
config.allowedTools =
|
|
82
|
+
const allowedTools = splitCommaList(options.allowedTools);
|
|
83
|
+
if (allowedTools.length > 0) {
|
|
84
|
+
config.allowedTools = allowedTools;
|
|
80
85
|
}
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
const disabledTools = splitCommaList(options.disabledTools);
|
|
87
|
+
if (disabledTools.length > 0) {
|
|
88
|
+
config.disabledTools = disabledTools;
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
// For HTTP servers, resolve the canonical resource URL before saving.
|
|
@@ -127,16 +133,26 @@ function collect(value: string, previous: string[]): string[] {
|
|
|
127
133
|
return previous.concat([value]);
|
|
128
134
|
}
|
|
129
135
|
|
|
130
|
-
|
|
136
|
+
// Flatten a list of repeated CLI values, splitting each on commas and trimming.
|
|
137
|
+
// Supports both `--flag a --flag b` and `--flag "a,b"` forms.
|
|
138
|
+
function splitCommaList(values: string[]): string[] {
|
|
139
|
+
return values.flatMap((v) => v.split(",").map((s) => s.trim())).filter((s) => s.length > 0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildStdioConfig(
|
|
143
|
+
options: { command?: string; args: string[]; env: string[]; cwd?: string },
|
|
144
|
+
passthroughArgs: string[],
|
|
145
|
+
): ServerConfig {
|
|
131
146
|
const config: Record<string, unknown> = { command: options.command! };
|
|
132
147
|
|
|
133
|
-
|
|
134
|
-
|
|
148
|
+
const args = [...splitCommaList(options.args), ...passthroughArgs];
|
|
149
|
+
if (args.length > 0) {
|
|
150
|
+
config.args = args;
|
|
135
151
|
}
|
|
136
152
|
|
|
137
|
-
if (options.env) {
|
|
153
|
+
if (options.env.length > 0) {
|
|
138
154
|
const env: Record<string, string> = {};
|
|
139
|
-
for (const pair of options.env
|
|
155
|
+
for (const pair of splitCommaList(options.env)) {
|
|
140
156
|
const eqIdx = pair.indexOf("=");
|
|
141
157
|
if (eqIdx === -1) {
|
|
142
158
|
console.error(`Invalid env format "${pair}", expected KEY=VAL`);
|
|
@@ -154,10 +170,10 @@ function buildStdioConfig(options: { command?: string; args?: string; env?: stri
|
|
|
154
170
|
return config as unknown as ServerConfig;
|
|
155
171
|
}
|
|
156
172
|
|
|
157
|
-
function buildHttpConfig(options: { url?: string; header
|
|
173
|
+
function buildHttpConfig(options: { url?: string; header: string[] }): ServerConfig {
|
|
158
174
|
const config: Record<string, unknown> = { url: options.url! };
|
|
159
175
|
|
|
160
|
-
if (options.header
|
|
176
|
+
if (options.header.length > 0) {
|
|
161
177
|
const headers: Record<string, string> = {};
|
|
162
178
|
for (const h of options.header) {
|
|
163
179
|
const colonIdx = h.indexOf(":");
|
package/src/config/loader.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { chmod } from "node:fs/promises";
|
|
1
2
|
import { join, resolve } from "node:path";
|
|
2
3
|
import { DEFAULT_CONFIG_DIR, ENV } from "../constants.ts";
|
|
3
4
|
import { interpolateEnv } from "./env.ts";
|
|
@@ -63,6 +64,7 @@ export async function loadConfig(options: LoadConfigOptions = {}): Promise<Confi
|
|
|
63
64
|
const cwd = process.cwd();
|
|
64
65
|
if (await hasServersFile(cwd)) {
|
|
65
66
|
configDir = cwd;
|
|
67
|
+
process.stderr.write(`Note: using servers.json from current directory (${cwd})\n`);
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -109,9 +111,10 @@ async function saveJsonFile(configDir: string, filename: string, data: unknown):
|
|
|
109
111
|
await Bun.write(join(configDir, filename), `${JSON.stringify(data, null, 2)}\n`);
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
/** Save auth.json to the config directory */
|
|
114
|
+
/** Save auth.json to the config directory with restrictive permissions */
|
|
113
115
|
export async function saveAuth(configDir: string, auth: AuthFile): Promise<void> {
|
|
114
|
-
|
|
116
|
+
await saveJsonFile(configDir, "auth.json", auth);
|
|
117
|
+
await chmod(join(configDir, "auth.json"), 0o600).catch(() => {});
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
/** Load search.json from the config directory */
|
package/src/output/formatter.ts
CHANGED
|
@@ -619,20 +619,23 @@ export function renderMarkdownToAnsi(input: string): string {
|
|
|
619
619
|
return restored;
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
+
const MAX_NESTED_JSON_DEPTH = 10;
|
|
623
|
+
|
|
622
624
|
/** Recursively parse JSON strings inside MCP content blocks */
|
|
623
|
-
function parseNestedJson(value: unknown): unknown {
|
|
625
|
+
function parseNestedJson(value: unknown, depth = 0): unknown {
|
|
626
|
+
if (depth > MAX_NESTED_JSON_DEPTH) return value;
|
|
624
627
|
if (typeof value === "string") {
|
|
625
628
|
try {
|
|
626
|
-
return parseNestedJson(JSON.parse(value));
|
|
629
|
+
return parseNestedJson(JSON.parse(value), depth + 1);
|
|
627
630
|
} catch {
|
|
628
631
|
return value;
|
|
629
632
|
}
|
|
630
633
|
}
|
|
631
634
|
if (Array.isArray(value)) {
|
|
632
|
-
return value.map(parseNestedJson);
|
|
635
|
+
return value.map((v) => parseNestedJson(v, depth + 1));
|
|
633
636
|
}
|
|
634
637
|
if (typeof value === "object" && value !== null) {
|
|
635
|
-
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, parseNestedJson(v)]));
|
|
638
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, parseNestedJson(v, depth + 1)]));
|
|
636
639
|
}
|
|
637
640
|
return value;
|
|
638
641
|
}
|
package/src/validation/schema.ts
CHANGED
|
@@ -28,8 +28,9 @@ function validateWithSchema(
|
|
|
28
28
|
try {
|
|
29
29
|
validate = ajv.compile(schema);
|
|
30
30
|
validatorCache.set(cacheKey, validate);
|
|
31
|
-
} catch {
|
|
32
|
-
|
|
31
|
+
} catch (err) {
|
|
32
|
+
const msg = err instanceof Error ? err.message : "unknown error";
|
|
33
|
+
return { valid: false, errors: [{ path: "(schema)", message: `schema compilation failed: ${msg}` }] };
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|