@bryan-thompson/inspector-assessment-cli 1.0.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/build/cli.js +277 -0
- package/build/client/connection.js +38 -0
- package/build/client/index.js +6 -0
- package/build/client/prompts.js +38 -0
- package/build/client/resources.js +30 -0
- package/build/client/tools.js +74 -0
- package/build/client/types.js +1 -0
- package/build/error-handler.js +18 -0
- package/build/index.js +232 -0
- package/build/transport.js +65 -0
- package/build/utils/awaitable-log.js +7 -0
- package/package.json +41 -0
package/build/cli.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { dirname, resolve } from "path";
|
|
6
|
+
import { spawnPromise } from "spawn-rx";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
function handleError(error) {
|
|
10
|
+
let message;
|
|
11
|
+
if (error instanceof Error) {
|
|
12
|
+
message = error.message;
|
|
13
|
+
}
|
|
14
|
+
else if (typeof error === "string") {
|
|
15
|
+
message = error;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
message = "Unknown error";
|
|
19
|
+
}
|
|
20
|
+
console.error(message);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
function delay(ms) {
|
|
24
|
+
return new Promise((resolve) => setTimeout(resolve, ms, true));
|
|
25
|
+
}
|
|
26
|
+
async function runWebClient(args) {
|
|
27
|
+
// Path to the client entry point
|
|
28
|
+
const inspectorClientPath = resolve(__dirname, "../../", "client", "bin", "start.js");
|
|
29
|
+
const abort = new AbortController();
|
|
30
|
+
let cancelled = false;
|
|
31
|
+
process.on("SIGINT", () => {
|
|
32
|
+
cancelled = true;
|
|
33
|
+
abort.abort();
|
|
34
|
+
});
|
|
35
|
+
// Build arguments to pass to start.js
|
|
36
|
+
const startArgs = [];
|
|
37
|
+
// Pass environment variables
|
|
38
|
+
for (const [key, value] of Object.entries(args.envArgs)) {
|
|
39
|
+
startArgs.push("-e", `${key}=${value}`);
|
|
40
|
+
}
|
|
41
|
+
// Pass transport type if specified
|
|
42
|
+
if (args.transport) {
|
|
43
|
+
startArgs.push("--transport", args.transport);
|
|
44
|
+
}
|
|
45
|
+
// Pass server URL if specified
|
|
46
|
+
if (args.serverUrl) {
|
|
47
|
+
startArgs.push("--server-url", args.serverUrl);
|
|
48
|
+
}
|
|
49
|
+
// Pass command and args (using -- to separate them)
|
|
50
|
+
if (args.command) {
|
|
51
|
+
startArgs.push("--", args.command, ...args.args);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await spawnPromise("node", [inspectorClientPath, ...startArgs], {
|
|
55
|
+
signal: abort.signal,
|
|
56
|
+
echoOutput: true,
|
|
57
|
+
// pipe the stdout through here, prevents issues with buffering and
|
|
58
|
+
// dropping the end of console.out after 8192 chars due to node
|
|
59
|
+
// closing the stdout pipe before the output has finished flushing
|
|
60
|
+
stdio: "inherit",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
if (!cancelled || process.env.DEBUG)
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function runCli(args) {
|
|
69
|
+
const projectRoot = resolve(__dirname, "..");
|
|
70
|
+
const cliPath = resolve(projectRoot, "build", "index.js");
|
|
71
|
+
const abort = new AbortController();
|
|
72
|
+
let cancelled = false;
|
|
73
|
+
process.on("SIGINT", () => {
|
|
74
|
+
cancelled = true;
|
|
75
|
+
abort.abort();
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
// Build CLI arguments
|
|
79
|
+
const cliArgs = [cliPath];
|
|
80
|
+
// Add target URL/command first
|
|
81
|
+
cliArgs.push(args.command, ...args.args);
|
|
82
|
+
// Add transport flag if specified
|
|
83
|
+
if (args.transport && args.transport !== "stdio") {
|
|
84
|
+
// Convert streamable-http back to http for CLI mode
|
|
85
|
+
const cliTransport = args.transport === "streamable-http" ? "http" : args.transport;
|
|
86
|
+
cliArgs.push("--transport", cliTransport);
|
|
87
|
+
}
|
|
88
|
+
// Add headers if specified
|
|
89
|
+
if (args.headers) {
|
|
90
|
+
for (const [key, value] of Object.entries(args.headers)) {
|
|
91
|
+
cliArgs.push("--header", `${key}: ${value}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
await spawnPromise("node", cliArgs, {
|
|
95
|
+
env: { ...process.env, ...args.envArgs },
|
|
96
|
+
signal: abort.signal,
|
|
97
|
+
echoOutput: true,
|
|
98
|
+
// pipe the stdout through here, prevents issues with buffering and
|
|
99
|
+
// dropping the end of console.out after 8192 chars due to node
|
|
100
|
+
// closing the stdout pipe before the output has finished flushing
|
|
101
|
+
stdio: "inherit",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
if (!cancelled || process.env.DEBUG) {
|
|
106
|
+
throw e;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function loadConfigFile(configPath, serverName) {
|
|
111
|
+
try {
|
|
112
|
+
const resolvedConfigPath = path.isAbsolute(configPath)
|
|
113
|
+
? configPath
|
|
114
|
+
: path.resolve(process.cwd(), configPath);
|
|
115
|
+
if (!fs.existsSync(resolvedConfigPath)) {
|
|
116
|
+
throw new Error(`Config file not found: ${resolvedConfigPath}`);
|
|
117
|
+
}
|
|
118
|
+
const configContent = fs.readFileSync(resolvedConfigPath, "utf8");
|
|
119
|
+
const parsedConfig = JSON.parse(configContent);
|
|
120
|
+
if (!parsedConfig.mcpServers || !parsedConfig.mcpServers[serverName]) {
|
|
121
|
+
const availableServers = Object.keys(parsedConfig.mcpServers || {}).join(", ");
|
|
122
|
+
throw new Error(`Server '${serverName}' not found in config file. Available servers: ${availableServers}`);
|
|
123
|
+
}
|
|
124
|
+
const serverConfig = parsedConfig.mcpServers[serverName];
|
|
125
|
+
return serverConfig;
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (err instanceof SyntaxError) {
|
|
129
|
+
throw new Error(`Invalid JSON in config file: ${err.message}`);
|
|
130
|
+
}
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function parseKeyValuePair(value, previous = {}) {
|
|
135
|
+
const parts = value.split("=");
|
|
136
|
+
const key = parts[0];
|
|
137
|
+
const val = parts.slice(1).join("=");
|
|
138
|
+
if (val === undefined || val === "") {
|
|
139
|
+
throw new Error(`Invalid parameter format: ${value}. Use key=value format.`);
|
|
140
|
+
}
|
|
141
|
+
return { ...previous, [key]: val };
|
|
142
|
+
}
|
|
143
|
+
function parseHeaderPair(value, previous = {}) {
|
|
144
|
+
const colonIndex = value.indexOf(":");
|
|
145
|
+
if (colonIndex === -1) {
|
|
146
|
+
throw new Error(`Invalid header format: ${value}. Use "HeaderName: Value" format.`);
|
|
147
|
+
}
|
|
148
|
+
const key = value.slice(0, colonIndex).trim();
|
|
149
|
+
const val = value.slice(colonIndex + 1).trim();
|
|
150
|
+
if (key === "" || val === "") {
|
|
151
|
+
throw new Error(`Invalid header format: ${value}. Use "HeaderName: Value" format.`);
|
|
152
|
+
}
|
|
153
|
+
return { ...previous, [key]: val };
|
|
154
|
+
}
|
|
155
|
+
function parseArgs() {
|
|
156
|
+
const program = new Command();
|
|
157
|
+
const argSeparatorIndex = process.argv.indexOf("--");
|
|
158
|
+
let preArgs = process.argv;
|
|
159
|
+
let postArgs = [];
|
|
160
|
+
if (argSeparatorIndex !== -1) {
|
|
161
|
+
preArgs = process.argv.slice(0, argSeparatorIndex);
|
|
162
|
+
postArgs = process.argv.slice(argSeparatorIndex + 1);
|
|
163
|
+
}
|
|
164
|
+
program
|
|
165
|
+
.name("inspector-bin")
|
|
166
|
+
.allowExcessArguments()
|
|
167
|
+
.allowUnknownOption()
|
|
168
|
+
.option("-e <env>", "environment variables in KEY=VALUE format", parseKeyValuePair, {})
|
|
169
|
+
.option("--config <path>", "config file path")
|
|
170
|
+
.option("--server <n>", "server name from config file")
|
|
171
|
+
.option("--cli", "enable CLI mode")
|
|
172
|
+
.option("--transport <type>", "transport type (stdio, sse, http)")
|
|
173
|
+
.option("--server-url <url>", "server URL for SSE/HTTP transport")
|
|
174
|
+
.option("--header <headers...>", 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)', parseHeaderPair, {});
|
|
175
|
+
// Parse only the arguments before --
|
|
176
|
+
program.parse(preArgs);
|
|
177
|
+
const options = program.opts();
|
|
178
|
+
const remainingArgs = program.args;
|
|
179
|
+
// Add back any arguments that came after --
|
|
180
|
+
const finalArgs = [...remainingArgs, ...postArgs];
|
|
181
|
+
// Validate config and server options
|
|
182
|
+
if (!options.config && options.server) {
|
|
183
|
+
throw new Error("--server requires --config to be specified");
|
|
184
|
+
}
|
|
185
|
+
// If config is provided without server, try to auto-select
|
|
186
|
+
if (options.config && !options.server) {
|
|
187
|
+
const configContent = fs.readFileSync(path.isAbsolute(options.config)
|
|
188
|
+
? options.config
|
|
189
|
+
: path.resolve(process.cwd(), options.config), "utf8");
|
|
190
|
+
const parsedConfig = JSON.parse(configContent);
|
|
191
|
+
const servers = Object.keys(parsedConfig.mcpServers || {});
|
|
192
|
+
if (servers.length === 1) {
|
|
193
|
+
// Use the only server if there's just one
|
|
194
|
+
options.server = servers[0];
|
|
195
|
+
}
|
|
196
|
+
else if (servers.length === 0) {
|
|
197
|
+
throw new Error("No servers found in config file");
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// Multiple servers, require explicit selection
|
|
201
|
+
throw new Error(`Multiple servers found in config file. Please specify one with --server.\nAvailable servers: ${servers.join(", ")}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// If config file is specified, load and use the options from the file. We must merge the args
|
|
205
|
+
// from the command line and the file together, or we will miss the method options (--method,
|
|
206
|
+
// etc.)
|
|
207
|
+
if (options.config && options.server) {
|
|
208
|
+
const config = loadConfigFile(options.config, options.server);
|
|
209
|
+
if (config.type === "stdio") {
|
|
210
|
+
return {
|
|
211
|
+
command: config.command,
|
|
212
|
+
args: [...(config.args || []), ...finalArgs],
|
|
213
|
+
envArgs: { ...(config.env || {}), ...(options.e || {}) },
|
|
214
|
+
cli: options.cli || false,
|
|
215
|
+
transport: "stdio",
|
|
216
|
+
headers: options.header,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
else if (config.type === "sse" || config.type === "streamable-http") {
|
|
220
|
+
return {
|
|
221
|
+
command: config.url,
|
|
222
|
+
args: finalArgs,
|
|
223
|
+
envArgs: options.e || {},
|
|
224
|
+
cli: options.cli || false,
|
|
225
|
+
transport: config.type,
|
|
226
|
+
serverUrl: config.url,
|
|
227
|
+
headers: options.header,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
// Backwards compatibility: if no type field, assume stdio
|
|
232
|
+
return {
|
|
233
|
+
command: config.command || "",
|
|
234
|
+
args: [...(config.args || []), ...finalArgs],
|
|
235
|
+
envArgs: { ...(config.env || {}), ...(options.e || {}) },
|
|
236
|
+
cli: options.cli || false,
|
|
237
|
+
transport: "stdio",
|
|
238
|
+
headers: options.header,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Otherwise use command line arguments
|
|
243
|
+
const command = finalArgs[0] || "";
|
|
244
|
+
const args = finalArgs.slice(1);
|
|
245
|
+
// Map "http" shorthand to "streamable-http"
|
|
246
|
+
let transport = options.transport;
|
|
247
|
+
if (transport === "http") {
|
|
248
|
+
transport = "streamable-http";
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
command,
|
|
252
|
+
args,
|
|
253
|
+
envArgs: options.e || {},
|
|
254
|
+
cli: options.cli || false,
|
|
255
|
+
transport: transport,
|
|
256
|
+
serverUrl: options.serverUrl,
|
|
257
|
+
headers: options.header,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
async function main() {
|
|
261
|
+
process.on("uncaughtException", (error) => {
|
|
262
|
+
handleError(error);
|
|
263
|
+
});
|
|
264
|
+
try {
|
|
265
|
+
const args = parseArgs();
|
|
266
|
+
if (args.cli) {
|
|
267
|
+
await runCli(args);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
await runWebClient(args);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
handleError(error);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
main();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const validLogLevels = [
|
|
2
|
+
"trace",
|
|
3
|
+
"debug",
|
|
4
|
+
"info",
|
|
5
|
+
"warn",
|
|
6
|
+
"error",
|
|
7
|
+
];
|
|
8
|
+
export async function connect(client, transport) {
|
|
9
|
+
try {
|
|
10
|
+
await client.connect(transport);
|
|
11
|
+
if (client.getServerCapabilities()?.logging) {
|
|
12
|
+
// default logging level is undefined in the spec, but the user of the
|
|
13
|
+
// inspector most likely wants debug.
|
|
14
|
+
await client.setLoggingLevel("debug");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
throw new Error(`Failed to connect to MCP server: ${error instanceof Error ? error.message : String(error)}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function disconnect(transport) {
|
|
22
|
+
try {
|
|
23
|
+
await transport.close();
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw new Error(`Failed to disconnect from MCP server: ${error instanceof Error ? error.message : String(error)}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Set logging level
|
|
30
|
+
export async function setLoggingLevel(client, level) {
|
|
31
|
+
try {
|
|
32
|
+
const response = await client.setLoggingLevel(level);
|
|
33
|
+
return response;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(`Failed to set logging level: ${error instanceof Error ? error.message : String(error)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// List available prompts
|
|
2
|
+
export async function listPrompts(client) {
|
|
3
|
+
try {
|
|
4
|
+
const response = await client.listPrompts();
|
|
5
|
+
return response;
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
throw new Error(`Failed to list prompts: ${error instanceof Error ? error.message : String(error)}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
// Get a prompt
|
|
12
|
+
export async function getPrompt(client, name, args) {
|
|
13
|
+
try {
|
|
14
|
+
// Convert all arguments to strings for prompt arguments
|
|
15
|
+
const stringArgs = {};
|
|
16
|
+
if (args) {
|
|
17
|
+
for (const [key, value] of Object.entries(args)) {
|
|
18
|
+
if (typeof value === "string") {
|
|
19
|
+
stringArgs[key] = value;
|
|
20
|
+
}
|
|
21
|
+
else if (value === null || value === undefined) {
|
|
22
|
+
stringArgs[key] = String(value);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
stringArgs[key] = JSON.stringify(value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const response = await client.getPrompt({
|
|
30
|
+
name,
|
|
31
|
+
arguments: stringArgs,
|
|
32
|
+
});
|
|
33
|
+
return response;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(`Failed to get prompt: ${error instanceof Error ? error.message : String(error)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// List available resources
|
|
2
|
+
export async function listResources(client) {
|
|
3
|
+
try {
|
|
4
|
+
const response = await client.listResources();
|
|
5
|
+
return response;
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
throw new Error(`Failed to list resources: ${error instanceof Error ? error.message : String(error)}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
// Read a resource
|
|
12
|
+
export async function readResource(client, uri) {
|
|
13
|
+
try {
|
|
14
|
+
const response = await client.readResource({ uri });
|
|
15
|
+
return response;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
throw new Error(`Failed to read resource ${uri}: ${error instanceof Error ? error.message : String(error)}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// List resource templates
|
|
22
|
+
export async function listResourceTemplates(client) {
|
|
23
|
+
try {
|
|
24
|
+
const response = await client.listResourceTemplates();
|
|
25
|
+
return response;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new Error(`Failed to list resource templates: ${error instanceof Error ? error.message : String(error)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export async function listTools(client) {
|
|
2
|
+
try {
|
|
3
|
+
const response = await client.listTools();
|
|
4
|
+
return response;
|
|
5
|
+
}
|
|
6
|
+
catch (error) {
|
|
7
|
+
throw new Error(`Failed to list tools: ${error instanceof Error ? error.message : String(error)}`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function convertParameterValue(value, schema) {
|
|
11
|
+
if (!value) {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
if (schema.type === "number" || schema.type === "integer") {
|
|
15
|
+
return Number(value);
|
|
16
|
+
}
|
|
17
|
+
if (schema.type === "boolean") {
|
|
18
|
+
return value.toLowerCase() === "true";
|
|
19
|
+
}
|
|
20
|
+
if (schema.type === "object" || schema.type === "array") {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(value);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function convertParameters(tool, params) {
|
|
31
|
+
const result = {};
|
|
32
|
+
const properties = tool.inputSchema.properties || {};
|
|
33
|
+
for (const [key, value] of Object.entries(params)) {
|
|
34
|
+
const paramSchema = properties[key];
|
|
35
|
+
if (paramSchema) {
|
|
36
|
+
result[key] = convertParameterValue(value, paramSchema);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// If no schema is found for this parameter, keep it as string
|
|
40
|
+
result[key] = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
export async function callTool(client, name, args) {
|
|
46
|
+
try {
|
|
47
|
+
const toolsResponse = await listTools(client);
|
|
48
|
+
const tools = toolsResponse.tools;
|
|
49
|
+
const tool = tools.find((t) => t.name === name);
|
|
50
|
+
let convertedArgs = args;
|
|
51
|
+
if (tool) {
|
|
52
|
+
// Convert parameters based on the tool's schema, but only for string values
|
|
53
|
+
// since we now accept pre-parsed values from the CLI
|
|
54
|
+
const stringArgs = {};
|
|
55
|
+
for (const [key, value] of Object.entries(args)) {
|
|
56
|
+
if (typeof value === "string") {
|
|
57
|
+
stringArgs[key] = value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (Object.keys(stringArgs).length > 0) {
|
|
61
|
+
const convertedStringArgs = convertParameters(tool, stringArgs);
|
|
62
|
+
convertedArgs = { ...args, ...convertedStringArgs };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const response = await client.callTool({
|
|
66
|
+
name: name,
|
|
67
|
+
arguments: convertedArgs,
|
|
68
|
+
});
|
|
69
|
+
return response;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
throw new Error(`Failed to call tool ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function formatError(error) {
|
|
2
|
+
let message;
|
|
3
|
+
if (error instanceof Error) {
|
|
4
|
+
message = error.message;
|
|
5
|
+
}
|
|
6
|
+
else if (typeof error === "string") {
|
|
7
|
+
message = error;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
message = "Unknown error";
|
|
11
|
+
}
|
|
12
|
+
return message;
|
|
13
|
+
}
|
|
14
|
+
export function handleError(error) {
|
|
15
|
+
const errorMessage = formatError(error);
|
|
16
|
+
console.error(errorMessage);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { callTool, connect, disconnect, getPrompt, listPrompts, listResources, listResourceTemplates, listTools, readResource, setLoggingLevel, validLogLevels, } from "./client/index.js";
|
|
5
|
+
import { handleError } from "./error-handler.js";
|
|
6
|
+
import { createTransport } from "./transport.js";
|
|
7
|
+
import { awaitableLog } from "./utils/awaitable-log.js";
|
|
8
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
9
|
+
function createTransportOptions(target, transport, headers) {
|
|
10
|
+
if (target.length === 0) {
|
|
11
|
+
throw new Error("Target is required. Specify a URL or a command to execute.");
|
|
12
|
+
}
|
|
13
|
+
const [command, ...commandArgs] = target;
|
|
14
|
+
if (!command) {
|
|
15
|
+
throw new Error("Command is required.");
|
|
16
|
+
}
|
|
17
|
+
const isUrl = command.startsWith("http://") || command.startsWith("https://");
|
|
18
|
+
if (isUrl && commandArgs.length > 0) {
|
|
19
|
+
throw new Error("Arguments cannot be passed to a URL-based MCP server.");
|
|
20
|
+
}
|
|
21
|
+
let transportType;
|
|
22
|
+
if (transport) {
|
|
23
|
+
if (!isUrl && transport !== "stdio") {
|
|
24
|
+
throw new Error("Only stdio transport can be used with local commands.");
|
|
25
|
+
}
|
|
26
|
+
if (isUrl && transport === "stdio") {
|
|
27
|
+
throw new Error("stdio transport cannot be used with URLs.");
|
|
28
|
+
}
|
|
29
|
+
transportType = transport;
|
|
30
|
+
}
|
|
31
|
+
else if (isUrl) {
|
|
32
|
+
const url = new URL(command);
|
|
33
|
+
if (url.pathname.endsWith("/mcp")) {
|
|
34
|
+
transportType = "http";
|
|
35
|
+
}
|
|
36
|
+
else if (url.pathname.endsWith("/sse")) {
|
|
37
|
+
transportType = "sse";
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
transportType = "sse";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
transportType = "stdio";
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
transportType,
|
|
48
|
+
command: isUrl ? undefined : command,
|
|
49
|
+
args: isUrl ? undefined : commandArgs,
|
|
50
|
+
url: isUrl ? command : undefined,
|
|
51
|
+
headers,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function callMethod(args) {
|
|
55
|
+
const transportOptions = createTransportOptions(args.target, args.transport, args.headers);
|
|
56
|
+
const transport = createTransport(transportOptions);
|
|
57
|
+
const [, name = packageJson.name] = packageJson.name.split("/");
|
|
58
|
+
const version = packageJson.version;
|
|
59
|
+
const clientIdentity = { name, version };
|
|
60
|
+
const client = new Client(clientIdentity);
|
|
61
|
+
try {
|
|
62
|
+
await connect(client, transport);
|
|
63
|
+
let result;
|
|
64
|
+
// Tools methods
|
|
65
|
+
if (args.method === "tools/list") {
|
|
66
|
+
result = await listTools(client);
|
|
67
|
+
}
|
|
68
|
+
else if (args.method === "tools/call") {
|
|
69
|
+
if (!args.toolName) {
|
|
70
|
+
throw new Error("Tool name is required for tools/call method. Use --tool-name to specify the tool name.");
|
|
71
|
+
}
|
|
72
|
+
result = await callTool(client, args.toolName, args.toolArg || {});
|
|
73
|
+
}
|
|
74
|
+
// Resources methods
|
|
75
|
+
else if (args.method === "resources/list") {
|
|
76
|
+
result = await listResources(client);
|
|
77
|
+
}
|
|
78
|
+
else if (args.method === "resources/read") {
|
|
79
|
+
if (!args.uri) {
|
|
80
|
+
throw new Error("URI is required for resources/read method. Use --uri to specify the resource URI.");
|
|
81
|
+
}
|
|
82
|
+
result = await readResource(client, args.uri);
|
|
83
|
+
}
|
|
84
|
+
else if (args.method === "resources/templates/list") {
|
|
85
|
+
result = await listResourceTemplates(client);
|
|
86
|
+
}
|
|
87
|
+
// Prompts methods
|
|
88
|
+
else if (args.method === "prompts/list") {
|
|
89
|
+
result = await listPrompts(client);
|
|
90
|
+
}
|
|
91
|
+
else if (args.method === "prompts/get") {
|
|
92
|
+
if (!args.promptName) {
|
|
93
|
+
throw new Error("Prompt name is required for prompts/get method. Use --prompt-name to specify the prompt name.");
|
|
94
|
+
}
|
|
95
|
+
result = await getPrompt(client, args.promptName, args.promptArgs || {});
|
|
96
|
+
}
|
|
97
|
+
// Logging methods
|
|
98
|
+
else if (args.method === "logging/setLevel") {
|
|
99
|
+
if (!args.logLevel) {
|
|
100
|
+
throw new Error("Log level is required for logging/setLevel method. Use --log-level to specify the log level.");
|
|
101
|
+
}
|
|
102
|
+
result = await setLoggingLevel(client, args.logLevel);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
throw new Error(`Unsupported method: ${args.method}. Supported methods include: tools/list, tools/call, resources/list, resources/read, resources/templates/list, prompts/list, prompts/get, logging/setLevel`);
|
|
106
|
+
}
|
|
107
|
+
await awaitableLog(JSON.stringify(result, null, 2));
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
try {
|
|
111
|
+
await disconnect(transport);
|
|
112
|
+
}
|
|
113
|
+
catch (disconnectError) {
|
|
114
|
+
throw disconnectError;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function parseKeyValuePair(value, previous = {}) {
|
|
119
|
+
const parts = value.split("=");
|
|
120
|
+
const key = parts[0];
|
|
121
|
+
const val = parts.slice(1).join("=");
|
|
122
|
+
if (val === undefined || val === "") {
|
|
123
|
+
throw new Error(`Invalid parameter format: ${value}. Use key=value format.`);
|
|
124
|
+
}
|
|
125
|
+
// Try to parse as JSON first
|
|
126
|
+
let parsedValue;
|
|
127
|
+
try {
|
|
128
|
+
parsedValue = JSON.parse(val);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// If JSON parsing fails, keep as string
|
|
132
|
+
parsedValue = val;
|
|
133
|
+
}
|
|
134
|
+
return { ...previous, [key]: parsedValue };
|
|
135
|
+
}
|
|
136
|
+
function parseHeaderPair(value, previous = {}) {
|
|
137
|
+
const colonIndex = value.indexOf(":");
|
|
138
|
+
if (colonIndex === -1) {
|
|
139
|
+
throw new Error(`Invalid header format: ${value}. Use "HeaderName: Value" format.`);
|
|
140
|
+
}
|
|
141
|
+
const key = value.slice(0, colonIndex).trim();
|
|
142
|
+
const val = value.slice(colonIndex + 1).trim();
|
|
143
|
+
if (key === "" || val === "") {
|
|
144
|
+
throw new Error(`Invalid header format: ${value}. Use "HeaderName: Value" format.`);
|
|
145
|
+
}
|
|
146
|
+
return { ...previous, [key]: val };
|
|
147
|
+
}
|
|
148
|
+
function parseArgs() {
|
|
149
|
+
const program = new Command();
|
|
150
|
+
// Find if there's a -- in the arguments and split them
|
|
151
|
+
const argSeparatorIndex = process.argv.indexOf("--");
|
|
152
|
+
let preArgs = process.argv;
|
|
153
|
+
let postArgs = [];
|
|
154
|
+
if (argSeparatorIndex !== -1) {
|
|
155
|
+
preArgs = process.argv.slice(0, argSeparatorIndex);
|
|
156
|
+
postArgs = process.argv.slice(argSeparatorIndex + 1);
|
|
157
|
+
}
|
|
158
|
+
program
|
|
159
|
+
.name("inspector-cli")
|
|
160
|
+
.allowUnknownOption()
|
|
161
|
+
.argument("<target...>", "Command and arguments or URL of the MCP server")
|
|
162
|
+
//
|
|
163
|
+
// Method selection
|
|
164
|
+
//
|
|
165
|
+
.option("--method <method>", "Method to invoke")
|
|
166
|
+
//
|
|
167
|
+
// Tool-related options
|
|
168
|
+
//
|
|
169
|
+
.option("--tool-name <toolName>", "Tool name (for tools/call method)")
|
|
170
|
+
.option("--tool-arg <pairs...>", "Tool argument as key=value pair", parseKeyValuePair, {})
|
|
171
|
+
//
|
|
172
|
+
// Resource-related options
|
|
173
|
+
//
|
|
174
|
+
.option("--uri <uri>", "URI of the resource (for resources/read method)")
|
|
175
|
+
//
|
|
176
|
+
// Prompt-related options
|
|
177
|
+
//
|
|
178
|
+
.option("--prompt-name <promptName>", "Name of the prompt (for prompts/get method)")
|
|
179
|
+
.option("--prompt-args <pairs...>", "Prompt arguments as key=value pairs", parseKeyValuePair, {})
|
|
180
|
+
//
|
|
181
|
+
// Logging options
|
|
182
|
+
//
|
|
183
|
+
.option("--log-level <level>", "Logging level (for logging/setLevel method)", (value) => {
|
|
184
|
+
if (!validLogLevels.includes(value)) {
|
|
185
|
+
throw new Error(`Invalid log level: ${value}. Valid levels are: ${validLogLevels.join(", ")}`);
|
|
186
|
+
}
|
|
187
|
+
return value;
|
|
188
|
+
})
|
|
189
|
+
//
|
|
190
|
+
// Transport options
|
|
191
|
+
//
|
|
192
|
+
.option("--transport <type>", "Transport type (sse, http, or stdio). Auto-detected from URL: /mcp → http, /sse → sse, commands → stdio", (value) => {
|
|
193
|
+
const validTransports = ["sse", "http", "stdio"];
|
|
194
|
+
if (!validTransports.includes(value)) {
|
|
195
|
+
throw new Error(`Invalid transport type: ${value}. Valid types are: ${validTransports.join(", ")}`);
|
|
196
|
+
}
|
|
197
|
+
return value;
|
|
198
|
+
})
|
|
199
|
+
//
|
|
200
|
+
// HTTP headers
|
|
201
|
+
//
|
|
202
|
+
.option("--header <headers...>", 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)', parseHeaderPair, {});
|
|
203
|
+
// Parse only the arguments before --
|
|
204
|
+
program.parse(preArgs);
|
|
205
|
+
const options = program.opts();
|
|
206
|
+
let remainingArgs = program.args;
|
|
207
|
+
// Add back any arguments that came after --
|
|
208
|
+
const finalArgs = [...remainingArgs, ...postArgs];
|
|
209
|
+
if (!options.method) {
|
|
210
|
+
throw new Error("Method is required. Use --method to specify the method to invoke.");
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
target: finalArgs,
|
|
214
|
+
...options,
|
|
215
|
+
headers: options.header, // commander.js uses 'header' field, map to 'headers'
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
async function main() {
|
|
219
|
+
process.on("uncaughtException", (error) => {
|
|
220
|
+
handleError(error);
|
|
221
|
+
});
|
|
222
|
+
try {
|
|
223
|
+
const args = parseArgs();
|
|
224
|
+
await callMethod(args);
|
|
225
|
+
// Explicitly exit to ensure process terminates in CI
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
handleError(error);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
main();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
2
|
+
import { getDefaultEnvironment, StdioClientTransport, } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
import { findActualExecutable } from "spawn-rx";
|
|
5
|
+
function createStdioTransport(options) {
|
|
6
|
+
let args = [];
|
|
7
|
+
if (options.args !== undefined) {
|
|
8
|
+
args = options.args;
|
|
9
|
+
}
|
|
10
|
+
const processEnv = {};
|
|
11
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
12
|
+
if (value !== undefined) {
|
|
13
|
+
processEnv[key] = value;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const defaultEnv = getDefaultEnvironment();
|
|
17
|
+
const env = {
|
|
18
|
+
...defaultEnv,
|
|
19
|
+
...processEnv,
|
|
20
|
+
};
|
|
21
|
+
const { cmd: actualCommand, args: actualArgs } = findActualExecutable(options.command ?? "", args);
|
|
22
|
+
return new StdioClientTransport({
|
|
23
|
+
command: actualCommand,
|
|
24
|
+
args: actualArgs,
|
|
25
|
+
env,
|
|
26
|
+
stderr: "pipe",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function createTransport(options) {
|
|
30
|
+
const { transportType } = options;
|
|
31
|
+
try {
|
|
32
|
+
if (transportType === "stdio") {
|
|
33
|
+
return createStdioTransport(options);
|
|
34
|
+
}
|
|
35
|
+
// If not STDIO, then it must be either SSE or HTTP.
|
|
36
|
+
if (!options.url) {
|
|
37
|
+
throw new Error("URL must be provided for SSE or HTTP transport types.");
|
|
38
|
+
}
|
|
39
|
+
const url = new URL(options.url);
|
|
40
|
+
if (transportType === "sse") {
|
|
41
|
+
const transportOptions = options.headers
|
|
42
|
+
? {
|
|
43
|
+
requestInit: {
|
|
44
|
+
headers: options.headers,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
: undefined;
|
|
48
|
+
return new SSEClientTransport(url, transportOptions);
|
|
49
|
+
}
|
|
50
|
+
if (transportType === "http") {
|
|
51
|
+
const transportOptions = options.headers
|
|
52
|
+
? {
|
|
53
|
+
requestInit: {
|
|
54
|
+
headers: options.headers,
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
: undefined;
|
|
58
|
+
return new StreamableHTTPClientTransport(url, transportOptions);
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`Unsupported transport type: ${transportType}`);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw new Error(`Failed to create transport: ${error instanceof Error ? error.message : String(error)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bryan-thompson/inspector-assessment-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for the Enhanced MCP Inspector with assessment capabilities",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Bryan Thompson <bryan@triepod.ai>",
|
|
7
|
+
"contributors": [
|
|
8
|
+
"Anthropic, PBC (original MCP Inspector)"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/triepod-ai/inspector-assessment",
|
|
11
|
+
"bugs": "https://github.com/triepod-ai/inspector-assessment/issues",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/triepod-ai/inspector-assessment.git"
|
|
15
|
+
},
|
|
16
|
+
"main": "build/cli.js",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"bin": {
|
|
19
|
+
"mcp-inspector-assess-cli": "build/cli.js"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"build"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"postbuild": "node scripts/make-executable.js",
|
|
30
|
+
"test": "node scripts/cli-tests.js && node scripts/cli-tool-tests.js && node scripts/cli-header-tests.js",
|
|
31
|
+
"test:cli": "node scripts/cli-tests.js",
|
|
32
|
+
"test:cli-tools": "node scripts/cli-tool-tests.js",
|
|
33
|
+
"test:cli-headers": "node scripts/cli-header-tests.js"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.18.0",
|
|
38
|
+
"commander": "^13.1.0",
|
|
39
|
+
"spawn-rx": "^5.1.2"
|
|
40
|
+
}
|
|
41
|
+
}
|