@evantahler/mcpx 0.15.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/.claude/settings.local.json +18 -0
- package/.claude/skills/mcpx.md +165 -0
- package/.claude/worktrees/elastic-jennings/.claude/settings.local.json +18 -0
- package/.claude/worktrees/elastic-jennings/.claude/skills/mcpcli.md +93 -0
- package/.claude/worktrees/elastic-jennings/.github/workflows/auto-release.yml +117 -0
- package/.claude/worktrees/elastic-jennings/.github/workflows/ci.yml +18 -0
- package/.claude/worktrees/elastic-jennings/.prettierignore +4 -0
- package/.claude/worktrees/elastic-jennings/.prettierrc +7 -0
- package/.claude/worktrees/elastic-jennings/CLAUDE.md +19 -0
- package/.claude/worktrees/elastic-jennings/LICENSE +21 -0
- package/.claude/worktrees/elastic-jennings/README.md +487 -0
- package/.claude/worktrees/elastic-jennings/bun.lock +381 -0
- package/.claude/worktrees/elastic-jennings/install.sh +55 -0
- package/.claude/worktrees/elastic-jennings/package.json +56 -0
- package/.claude/worktrees/elastic-jennings/src/cli.ts +39 -0
- package/.claude/worktrees/elastic-jennings/src/client/http.ts +100 -0
- package/.claude/worktrees/elastic-jennings/src/client/manager.ts +266 -0
- package/.claude/worktrees/elastic-jennings/src/client/oauth.ts +299 -0
- package/.claude/worktrees/elastic-jennings/src/client/stdio.ts +12 -0
- package/.claude/worktrees/elastic-jennings/src/commands/add.ts +155 -0
- package/.claude/worktrees/elastic-jennings/src/commands/auth.ts +114 -0
- package/.claude/worktrees/elastic-jennings/src/commands/exec.ts +91 -0
- package/.claude/worktrees/elastic-jennings/src/commands/index.ts +62 -0
- package/.claude/worktrees/elastic-jennings/src/commands/info.ts +38 -0
- package/.claude/worktrees/elastic-jennings/src/commands/list.ts +30 -0
- package/.claude/worktrees/elastic-jennings/src/commands/remove.ts +67 -0
- package/.claude/worktrees/elastic-jennings/src/commands/search.ts +45 -0
- package/.claude/worktrees/elastic-jennings/src/commands/skill.ts +70 -0
- package/.claude/worktrees/elastic-jennings/src/config/env.ts +41 -0
- package/.claude/worktrees/elastic-jennings/src/config/loader.ts +156 -0
- package/.claude/worktrees/elastic-jennings/src/config/schemas.ts +137 -0
- package/.claude/worktrees/elastic-jennings/src/context.ts +53 -0
- package/.claude/worktrees/elastic-jennings/src/output/formatter.ts +316 -0
- package/.claude/worktrees/elastic-jennings/src/output/logger.ts +114 -0
- package/.claude/worktrees/elastic-jennings/src/search/index.ts +69 -0
- package/.claude/worktrees/elastic-jennings/src/search/indexer.ts +92 -0
- package/.claude/worktrees/elastic-jennings/src/search/keyword.ts +86 -0
- package/.claude/worktrees/elastic-jennings/src/search/semantic.ts +75 -0
- package/.claude/worktrees/elastic-jennings/src/search/staleness.ts +8 -0
- package/.claude/worktrees/elastic-jennings/src/validation/schema.ts +77 -0
- package/.claude/worktrees/elastic-jennings/test/cli.test.ts +51 -0
- package/.claude/worktrees/elastic-jennings/test/client/manager.test.ts +249 -0
- package/.claude/worktrees/elastic-jennings/test/client/oauth.test.ts +328 -0
- package/.claude/worktrees/elastic-jennings/test/commands/add-remove.test.ts +253 -0
- package/.claude/worktrees/elastic-jennings/test/commands/exec.test.ts +105 -0
- package/.claude/worktrees/elastic-jennings/test/commands/info.test.ts +48 -0
- package/.claude/worktrees/elastic-jennings/test/commands/list.test.ts +39 -0
- package/.claude/worktrees/elastic-jennings/test/commands/skill.test.ts +98 -0
- package/.claude/worktrees/elastic-jennings/test/config/env.test.ts +61 -0
- package/.claude/worktrees/elastic-jennings/test/config/loader.test.ts +139 -0
- package/.claude/worktrees/elastic-jennings/test/fixtures/.keep +0 -0
- package/.claude/worktrees/elastic-jennings/test/fixtures/auth.json +10 -0
- package/.claude/worktrees/elastic-jennings/test/fixtures/mock-config/.keep +0 -0
- package/.claude/worktrees/elastic-jennings/test/fixtures/mock-config/servers.json +8 -0
- package/.claude/worktrees/elastic-jennings/test/fixtures/mock-server.ts +113 -0
- package/.claude/worktrees/elastic-jennings/test/fixtures/search.json +15 -0
- package/.claude/worktrees/elastic-jennings/test/fixtures/servers.json +18 -0
- package/.claude/worktrees/elastic-jennings/test/integration/stdio-server.test.ts +149 -0
- package/.claude/worktrees/elastic-jennings/test/output/formatter.test.ts +54 -0
- package/.claude/worktrees/elastic-jennings/test/output/logger.test.ts +89 -0
- package/.claude/worktrees/elastic-jennings/test/search/indexer.test.ts +32 -0
- package/.claude/worktrees/elastic-jennings/test/search/keyword.test.ts +80 -0
- package/.claude/worktrees/elastic-jennings/test/search/semantic.test.ts +32 -0
- package/.claude/worktrees/elastic-jennings/test/validation/schema.test.ts +113 -0
- package/.claude/worktrees/elastic-jennings/tsconfig.json +29 -0
- package/.cursor/rules/mcpx.mdc +165 -0
- package/LICENSE +21 -0
- package/README.md +627 -0
- package/package.json +58 -0
- package/src/cli.ts +72 -0
- package/src/client/browser.ts +24 -0
- package/src/client/debug-fetch.ts +81 -0
- package/src/client/elicitation.ts +368 -0
- package/src/client/http.ts +25 -0
- package/src/client/manager.ts +566 -0
- package/src/client/oauth.ts +314 -0
- package/src/client/sse.ts +17 -0
- package/src/client/stdio.ts +12 -0
- package/src/client/trace.ts +184 -0
- package/src/commands/add.ts +179 -0
- package/src/commands/auth.ts +114 -0
- package/src/commands/exec.ts +156 -0
- package/src/commands/index.ts +62 -0
- package/src/commands/info.ts +63 -0
- package/src/commands/list.ts +64 -0
- package/src/commands/ping.ts +69 -0
- package/src/commands/prompt.ts +60 -0
- package/src/commands/remove.ts +67 -0
- package/src/commands/resource.ts +46 -0
- package/src/commands/search.ts +49 -0
- package/src/commands/servers.ts +66 -0
- package/src/commands/skill.ts +112 -0
- package/src/commands/task.ts +82 -0
- package/src/config/env.ts +41 -0
- package/src/config/loader.ts +156 -0
- package/src/config/schemas.ts +152 -0
- package/src/context.ts +62 -0
- package/src/lib/input.ts +36 -0
- package/src/output/formatter.ts +884 -0
- package/src/output/logger.ts +173 -0
- package/src/search/index.ts +69 -0
- package/src/search/indexer.ts +92 -0
- package/src/search/keyword.ts +86 -0
- package/src/search/semantic.ts +75 -0
- package/src/search/staleness.ts +8 -0
- package/src/validation/schema.ts +103 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { program } from "commander";
|
|
4
|
+
import { registerListCommand } from "./commands/list.ts";
|
|
5
|
+
import { registerInfoCommand } from "./commands/info.ts";
|
|
6
|
+
import { registerSearchCommand } from "./commands/search.ts";
|
|
7
|
+
import { registerExecCommand } from "./commands/exec.ts";
|
|
8
|
+
import { registerAuthCommand, registerDeauthCommand } from "./commands/auth.ts";
|
|
9
|
+
import { registerIndexCommand } from "./commands/index.ts";
|
|
10
|
+
import { registerAddCommand } from "./commands/add.ts";
|
|
11
|
+
import { registerRemoveCommand } from "./commands/remove.ts";
|
|
12
|
+
import { registerSkillCommand } from "./commands/skill.ts";
|
|
13
|
+
import { registerPingCommand } from "./commands/ping.ts";
|
|
14
|
+
import { registerResourceCommand } from "./commands/resource.ts";
|
|
15
|
+
import { registerPromptCommand } from "./commands/prompt.ts";
|
|
16
|
+
import { registerServersCommand } from "./commands/servers.ts";
|
|
17
|
+
import { registerTaskCommand } from "./commands/task.ts";
|
|
18
|
+
|
|
19
|
+
import pkg from "../package.json";
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.name("mcpx")
|
|
23
|
+
.description("A command-line interface for MCP servers. curl for MCP.")
|
|
24
|
+
.version(pkg.version)
|
|
25
|
+
.option("-c, --config <path>", "config directory path")
|
|
26
|
+
.option("-d, --with-descriptions", "include tool descriptions in output")
|
|
27
|
+
.option("-j, --json", "force JSON output")
|
|
28
|
+
.option("-v, --verbose", "show HTTP details and JSON-RPC protocol messages")
|
|
29
|
+
.option("-S, --show-secrets", "show full auth tokens in verbose output")
|
|
30
|
+
.option("-N, --no-interactive", "decline server elicitation requests")
|
|
31
|
+
.option(
|
|
32
|
+
"-l, --log-level <level>",
|
|
33
|
+
"minimum server log level (debug|info|notice|warning|error|critical|alert|emergency)",
|
|
34
|
+
"warning",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
registerListCommand(program);
|
|
38
|
+
registerInfoCommand(program);
|
|
39
|
+
registerSearchCommand(program);
|
|
40
|
+
registerExecCommand(program);
|
|
41
|
+
registerAuthCommand(program);
|
|
42
|
+
registerDeauthCommand(program);
|
|
43
|
+
registerIndexCommand(program);
|
|
44
|
+
registerAddCommand(program);
|
|
45
|
+
registerRemoveCommand(program);
|
|
46
|
+
registerSkillCommand(program);
|
|
47
|
+
registerPingCommand(program);
|
|
48
|
+
registerResourceCommand(program);
|
|
49
|
+
registerPromptCommand(program);
|
|
50
|
+
registerServersCommand(program);
|
|
51
|
+
registerTaskCommand(program);
|
|
52
|
+
|
|
53
|
+
// Detect unknown subcommands before commander misreports them as "too many arguments"
|
|
54
|
+
const knownCommands = new Set(program.commands.map((c) => c.name()));
|
|
55
|
+
const cliArgs = process.argv.slice(2);
|
|
56
|
+
let firstCommand: string | undefined;
|
|
57
|
+
for (let i = 0; i < cliArgs.length; i++) {
|
|
58
|
+
const a = cliArgs[i];
|
|
59
|
+
if (a === "-c" || a === "--config" || a === "-l" || a === "--log-level") {
|
|
60
|
+
i++; // skip the option's value argument
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (a.startsWith("-")) continue;
|
|
64
|
+
firstCommand = a;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
if (firstCommand && !knownCommands.has(firstCommand)) {
|
|
68
|
+
console.error(`error: unknown command '${firstCommand}'. See 'mcpx --help'.`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
program.parse();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Open a URL in the default browser (macOS/Windows/Linux).
|
|
5
|
+
* Falls back to printing the URL to stderr if no browser is available
|
|
6
|
+
* (e.g., headless servers, Docker containers).
|
|
7
|
+
*/
|
|
8
|
+
export function openBrowser(url: string): Promise<void> {
|
|
9
|
+
const cmd =
|
|
10
|
+
process.platform === "darwin"
|
|
11
|
+
? `open "${url}"`
|
|
12
|
+
: process.platform === "win32"
|
|
13
|
+
? `start "${url}"`
|
|
14
|
+
: `xdg-open "${url}"`;
|
|
15
|
+
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
exec(cmd, (err) => {
|
|
18
|
+
if (err) {
|
|
19
|
+
process.stderr.write(`Could not open browser. Please visit:\n ${url}\n`);
|
|
20
|
+
}
|
|
21
|
+
resolve();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { dim } from "ansis";
|
|
2
|
+
import { logger } from "../output/logger.ts";
|
|
3
|
+
|
|
4
|
+
export type FetchLike = (url: string | URL, init?: RequestInit) => Promise<Response>;
|
|
5
|
+
|
|
6
|
+
export function createDebugFetch(showSecrets: boolean): FetchLike {
|
|
7
|
+
const isTTY = process.stderr.isTTY ?? false;
|
|
8
|
+
const fmt = (s: string) => (isTTY ? dim(s) : s);
|
|
9
|
+
|
|
10
|
+
return async (url, init) => {
|
|
11
|
+
const start = performance.now();
|
|
12
|
+
|
|
13
|
+
// Request
|
|
14
|
+
log("");
|
|
15
|
+
log(fmt(`> ${init?.method ?? "GET"} ${url}`));
|
|
16
|
+
logHeaders(">", init?.headers, fmt, showSecrets);
|
|
17
|
+
log(fmt(">"));
|
|
18
|
+
if (init?.body) {
|
|
19
|
+
logBody(String(init.body), fmt);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const response = await fetch(url, init);
|
|
23
|
+
const elapsed = Math.round(performance.now() - start);
|
|
24
|
+
|
|
25
|
+
// Response
|
|
26
|
+
log(fmt(`< ${response.status} ${response.statusText} (${elapsed}ms)`));
|
|
27
|
+
logHeaders("<", response.headers, fmt, showSecrets);
|
|
28
|
+
log(fmt("<"));
|
|
29
|
+
log("");
|
|
30
|
+
|
|
31
|
+
return response;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function log(line: string) {
|
|
36
|
+
logger.writeRaw(line + "\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function logHeaders(
|
|
40
|
+
prefix: string,
|
|
41
|
+
headers: HeadersInit | Headers | undefined,
|
|
42
|
+
fmt: (s: string) => string,
|
|
43
|
+
showSecrets: boolean,
|
|
44
|
+
) {
|
|
45
|
+
if (!headers) return;
|
|
46
|
+
|
|
47
|
+
const format = (key: string, value: string) =>
|
|
48
|
+
fmt(`${prefix} ${key}: ${showSecrets ? value : maskSensitive(key, value)}`);
|
|
49
|
+
|
|
50
|
+
if (headers instanceof Headers) {
|
|
51
|
+
headers.forEach((value, key) => log(format(key, value)));
|
|
52
|
+
} else if (Array.isArray(headers)) {
|
|
53
|
+
for (const [key, value] of headers) {
|
|
54
|
+
log(format(key, value));
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
58
|
+
log(format(key, value));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function logBody(body: string, fmt: (s: string) => string) {
|
|
64
|
+
try {
|
|
65
|
+
const formatted = JSON.stringify(JSON.parse(body), null, 2);
|
|
66
|
+
for (const line of formatted.split("\n")) {
|
|
67
|
+
log(fmt(line));
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
log(fmt(body));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function maskSensitive(key: string, value: string): string {
|
|
75
|
+
const lower = key.toLowerCase();
|
|
76
|
+
if (lower === "authorization" || lower === "cookie" || lower === "set-cookie") {
|
|
77
|
+
if (value.length <= 12) return value;
|
|
78
|
+
return value.slice(0, 12) + "...";
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import type {
|
|
3
|
+
ElicitRequest,
|
|
4
|
+
ElicitResult,
|
|
5
|
+
ElicitRequestFormParams,
|
|
6
|
+
ElicitRequestURLParams,
|
|
7
|
+
PrimitiveSchemaDefinition,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { openBrowser } from "./browser.ts";
|
|
10
|
+
import { validateElicitationResponse } from "../validation/schema.ts";
|
|
11
|
+
import ansis from "ansis";
|
|
12
|
+
|
|
13
|
+
export interface ElicitationOptions {
|
|
14
|
+
noInteractive: boolean;
|
|
15
|
+
json: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type ElicitAction = "accept" | "cancel" | "decline";
|
|
19
|
+
|
|
20
|
+
/** Top-level elicitation request handler, registered on the MCP Client */
|
|
21
|
+
export async function handleElicitation(
|
|
22
|
+
request: ElicitRequest,
|
|
23
|
+
options: ElicitationOptions,
|
|
24
|
+
): Promise<ElicitResult> {
|
|
25
|
+
if (options.noInteractive) {
|
|
26
|
+
return { action: "decline" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const params = request.params;
|
|
30
|
+
const mode = (params as { mode?: string }).mode ?? "form";
|
|
31
|
+
|
|
32
|
+
if (mode === "url") {
|
|
33
|
+
return handleUrlElicitation(params as ElicitRequestURLParams, options);
|
|
34
|
+
}
|
|
35
|
+
return handleFormElicitation(params as ElicitRequestFormParams, options);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Form mode
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
async function handleFormElicitation(
|
|
43
|
+
params: ElicitRequestFormParams,
|
|
44
|
+
options: ElicitationOptions,
|
|
45
|
+
): Promise<ElicitResult> {
|
|
46
|
+
if (options.json) {
|
|
47
|
+
return handleFormJson(params);
|
|
48
|
+
}
|
|
49
|
+
return handleFormInteractive(params);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** JSON mode: write request to stdout, read ElicitResult from stdin */
|
|
53
|
+
async function handleFormJson(params: ElicitRequestFormParams): Promise<ElicitResult> {
|
|
54
|
+
const request = {
|
|
55
|
+
type: "elicitation",
|
|
56
|
+
mode: "form",
|
|
57
|
+
message: params.message,
|
|
58
|
+
requestedSchema: params.requestedSchema,
|
|
59
|
+
};
|
|
60
|
+
console.log(JSON.stringify(request));
|
|
61
|
+
|
|
62
|
+
const response = await readStdinLine();
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(response);
|
|
65
|
+
return {
|
|
66
|
+
action: parsed.action ?? "cancel",
|
|
67
|
+
content: parsed.content,
|
|
68
|
+
};
|
|
69
|
+
} catch {
|
|
70
|
+
return { action: "cancel" };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Interactive TTY: prompt user for each field */
|
|
75
|
+
async function handleFormInteractive(params: ElicitRequestFormParams): Promise<ElicitResult> {
|
|
76
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
77
|
+
const question = (prompt: string): Promise<string> =>
|
|
78
|
+
new Promise((resolve) => rl.question(prompt, resolve));
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
process.stderr.write(`\n${ansis.bold("Server requests input:")} ${params.message}\n`);
|
|
82
|
+
|
|
83
|
+
const schema = params.requestedSchema;
|
|
84
|
+
const properties = schema.properties ?? {};
|
|
85
|
+
const required = new Set((schema as { required?: string[] }).required ?? []);
|
|
86
|
+
const content: Record<string, string | number | boolean | string[]> = {};
|
|
87
|
+
|
|
88
|
+
for (const [key, fieldSchema] of Object.entries(properties)) {
|
|
89
|
+
const isRequired = required.has(key);
|
|
90
|
+
const value = await promptField(key, fieldSchema, isRequired, question);
|
|
91
|
+
if (value === undefined) {
|
|
92
|
+
if (isRequired) {
|
|
93
|
+
process.stderr.write(ansis.yellow("Cancelled.\n"));
|
|
94
|
+
return { action: "cancel" };
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
content[key] = value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Validate collected values against the full schema
|
|
102
|
+
const validation = validateElicitationResponse(
|
|
103
|
+
schema as unknown as Record<string, unknown>,
|
|
104
|
+
content,
|
|
105
|
+
);
|
|
106
|
+
if (!validation.valid) {
|
|
107
|
+
const msgs = validation.errors.map((e) => ` ${e.path}: ${e.message}`).join("\n");
|
|
108
|
+
process.stderr.write(ansis.red(`Validation failed:\n${msgs}\n`));
|
|
109
|
+
return { action: "cancel" };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { action: "accept", content };
|
|
113
|
+
} finally {
|
|
114
|
+
rl.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function promptField(
|
|
119
|
+
key: string,
|
|
120
|
+
schema: PrimitiveSchemaDefinition,
|
|
121
|
+
isRequired: boolean,
|
|
122
|
+
question: (prompt: string) => Promise<string>,
|
|
123
|
+
): Promise<string | number | boolean | string[] | undefined> {
|
|
124
|
+
const label = (schema as { title?: string }).title ?? key;
|
|
125
|
+
const desc = (schema as { description?: string }).description;
|
|
126
|
+
const marker = isRequired ? ansis.red("*") : "";
|
|
127
|
+
const type = (schema as { type?: string }).type;
|
|
128
|
+
|
|
129
|
+
// Show description if present
|
|
130
|
+
if (desc) {
|
|
131
|
+
process.stderr.write(ansis.dim(` ${desc}\n`));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Enum (single-select)
|
|
135
|
+
if (type === "string" && "enum" in schema) {
|
|
136
|
+
return promptEnum(label, schema, marker, question);
|
|
137
|
+
}
|
|
138
|
+
if (type === "string" && "oneOf" in schema) {
|
|
139
|
+
return promptOneOfEnum(label, schema, marker, question);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Multi-select enum (array type)
|
|
143
|
+
if (type === "array") {
|
|
144
|
+
return promptMultiSelect(label, schema, marker, question);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Boolean
|
|
148
|
+
if (type === "boolean") {
|
|
149
|
+
return promptBoolean(label, schema, marker, question);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Number / integer
|
|
153
|
+
if (type === "number" || type === "integer") {
|
|
154
|
+
return promptNumber(label, schema, marker, question);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// String (default)
|
|
158
|
+
return promptString(label, schema, marker, question);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function promptString(
|
|
162
|
+
label: string,
|
|
163
|
+
schema: PrimitiveSchemaDefinition,
|
|
164
|
+
marker: string,
|
|
165
|
+
question: (prompt: string) => Promise<string>,
|
|
166
|
+
): Promise<string | undefined> {
|
|
167
|
+
const def = (schema as { default?: string }).default;
|
|
168
|
+
const defHint = def !== undefined ? ` [${def}]` : "";
|
|
169
|
+
const answer = await question(` ${marker}${label} (string)${defHint}: `);
|
|
170
|
+
if (!answer && def !== undefined) return def;
|
|
171
|
+
if (!answer) return undefined;
|
|
172
|
+
return answer;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function promptNumber(
|
|
176
|
+
label: string,
|
|
177
|
+
schema: PrimitiveSchemaDefinition,
|
|
178
|
+
marker: string,
|
|
179
|
+
question: (prompt: string) => Promise<string>,
|
|
180
|
+
): Promise<number | undefined> {
|
|
181
|
+
const def = (schema as { default?: number }).default;
|
|
182
|
+
const defHint = def !== undefined ? ` [${def}]` : "";
|
|
183
|
+
const answer = await question(
|
|
184
|
+
` ${marker}${label} (${(schema as { type: string }).type})${defHint}: `,
|
|
185
|
+
);
|
|
186
|
+
if (!answer && def !== undefined) return def;
|
|
187
|
+
if (!answer) return undefined;
|
|
188
|
+
const num = Number(answer);
|
|
189
|
+
if (Number.isNaN(num)) {
|
|
190
|
+
process.stderr.write(ansis.red(` Invalid number: ${answer}\n`));
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
return num;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function promptBoolean(
|
|
197
|
+
label: string,
|
|
198
|
+
schema: PrimitiveSchemaDefinition,
|
|
199
|
+
marker: string,
|
|
200
|
+
question: (prompt: string) => Promise<string>,
|
|
201
|
+
): Promise<boolean | undefined> {
|
|
202
|
+
const def = (schema as { default?: boolean }).default;
|
|
203
|
+
const defHint = def !== undefined ? ` [${def ? "Y/n" : "y/N"}]` : " [y/n]";
|
|
204
|
+
const answer = await question(` ${marker}${label}${defHint}: `);
|
|
205
|
+
if (!answer && def !== undefined) return def;
|
|
206
|
+
if (!answer) return undefined;
|
|
207
|
+
return ["y", "yes", "true", "1"].includes(answer.toLowerCase());
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function promptEnum(
|
|
211
|
+
label: string,
|
|
212
|
+
schema: PrimitiveSchemaDefinition,
|
|
213
|
+
marker: string,
|
|
214
|
+
question: (prompt: string) => Promise<string>,
|
|
215
|
+
): Promise<string | undefined> {
|
|
216
|
+
const values = (schema as { enum: string[] }).enum;
|
|
217
|
+
const def = (schema as { default?: string }).default;
|
|
218
|
+
process.stderr.write(` ${marker}${label}:\n`);
|
|
219
|
+
values.forEach((v, i) => {
|
|
220
|
+
const defMark = v === def ? ansis.dim(" (default)") : "";
|
|
221
|
+
process.stderr.write(` [${i + 1}] ${v}${defMark}\n`);
|
|
222
|
+
});
|
|
223
|
+
const answer = await question(" > ");
|
|
224
|
+
if (!answer && def !== undefined) return def;
|
|
225
|
+
const idx = parseInt(answer, 10) - 1;
|
|
226
|
+
if (idx >= 0 && idx < values.length) return values[idx];
|
|
227
|
+
// Try matching by value directly
|
|
228
|
+
if (values.includes(answer)) return answer;
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function promptOneOfEnum(
|
|
233
|
+
label: string,
|
|
234
|
+
schema: PrimitiveSchemaDefinition,
|
|
235
|
+
marker: string,
|
|
236
|
+
question: (prompt: string) => Promise<string>,
|
|
237
|
+
): Promise<string | undefined> {
|
|
238
|
+
const options = (schema as { oneOf: { const: string; title: string }[] }).oneOf;
|
|
239
|
+
const def = (schema as { default?: string }).default;
|
|
240
|
+
process.stderr.write(` ${marker}${label}:\n`);
|
|
241
|
+
options.forEach((opt, i) => {
|
|
242
|
+
const defMark = opt.const === def ? ansis.dim(" (default)") : "";
|
|
243
|
+
process.stderr.write(` [${i + 1}] ${opt.title} (${opt.const})${defMark}\n`);
|
|
244
|
+
});
|
|
245
|
+
const answer = await question(" > ");
|
|
246
|
+
if (!answer && def !== undefined) return def;
|
|
247
|
+
const idx = parseInt(answer, 10) - 1;
|
|
248
|
+
if (idx >= 0 && idx < options.length) return options[idx]!.const;
|
|
249
|
+
// Try matching by const value directly
|
|
250
|
+
const match = options.find((o) => o.const === answer);
|
|
251
|
+
if (match) return match.const;
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function promptMultiSelect(
|
|
256
|
+
label: string,
|
|
257
|
+
schema: PrimitiveSchemaDefinition,
|
|
258
|
+
marker: string,
|
|
259
|
+
question: (prompt: string) => Promise<string>,
|
|
260
|
+
): Promise<string[] | undefined> {
|
|
261
|
+
const items = (
|
|
262
|
+
schema as { items?: { enum?: string[]; anyOf?: { const: string; title: string }[] } }
|
|
263
|
+
).items;
|
|
264
|
+
const def = (schema as { default?: string[] }).default;
|
|
265
|
+
|
|
266
|
+
let values: string[];
|
|
267
|
+
let titles: string[] | undefined;
|
|
268
|
+
|
|
269
|
+
if (items?.anyOf) {
|
|
270
|
+
values = items.anyOf.map((o) => o.const);
|
|
271
|
+
titles = items.anyOf.map((o) => o.title);
|
|
272
|
+
} else if (items?.enum) {
|
|
273
|
+
values = items.enum;
|
|
274
|
+
} else {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
process.stderr.write(` ${marker}${label} (select multiple, comma-separated):\n`);
|
|
279
|
+
values.forEach((v, i) => {
|
|
280
|
+
const display = titles ? `${titles[i]} (${v})` : v;
|
|
281
|
+
process.stderr.write(` [${i + 1}] ${display}\n`);
|
|
282
|
+
});
|
|
283
|
+
const answer = await question(" > ");
|
|
284
|
+
if (!answer && def !== undefined) return def;
|
|
285
|
+
if (!answer) return undefined;
|
|
286
|
+
|
|
287
|
+
const indices = answer.split(",").map((s) => parseInt(s.trim(), 10) - 1);
|
|
288
|
+
const selected = indices.filter((i) => i >= 0 && i < values.length).map((i) => values[i]!);
|
|
289
|
+
return selected.length > 0 ? selected : undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// URL mode
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
async function handleUrlElicitation(
|
|
297
|
+
params: ElicitRequestURLParams,
|
|
298
|
+
options: ElicitationOptions,
|
|
299
|
+
): Promise<ElicitResult> {
|
|
300
|
+
if (options.json) {
|
|
301
|
+
return handleUrlJson(params);
|
|
302
|
+
}
|
|
303
|
+
return handleUrlInteractive(params);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function handleUrlJson(params: ElicitRequestURLParams): Promise<ElicitResult> {
|
|
307
|
+
const request = {
|
|
308
|
+
type: "elicitation",
|
|
309
|
+
mode: "url",
|
|
310
|
+
message: params.message,
|
|
311
|
+
url: params.url,
|
|
312
|
+
elicitationId: params.elicitationId,
|
|
313
|
+
};
|
|
314
|
+
console.log(JSON.stringify(request));
|
|
315
|
+
|
|
316
|
+
const response = await readStdinLine();
|
|
317
|
+
try {
|
|
318
|
+
const parsed = JSON.parse(response);
|
|
319
|
+
return { action: (parsed.action as ElicitAction) ?? "cancel" };
|
|
320
|
+
} catch {
|
|
321
|
+
return { action: "cancel" };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function handleUrlInteractive(params: ElicitRequestURLParams): Promise<ElicitResult> {
|
|
326
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
327
|
+
const question = (prompt: string): Promise<string> =>
|
|
328
|
+
new Promise((resolve) => rl.question(prompt, resolve));
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const domain = (() => {
|
|
332
|
+
try {
|
|
333
|
+
return new URL(params.url).hostname;
|
|
334
|
+
} catch {
|
|
335
|
+
return "unknown";
|
|
336
|
+
}
|
|
337
|
+
})();
|
|
338
|
+
|
|
339
|
+
process.stderr.write(`\n${ansis.bold("Server requests URL interaction:")}\n`);
|
|
340
|
+
process.stderr.write(` ${params.message}\n`);
|
|
341
|
+
process.stderr.write(` ${ansis.yellow("Domain:")} ${domain}\n`);
|
|
342
|
+
process.stderr.write(` ${ansis.yellow("URL:")} ${params.url}\n`);
|
|
343
|
+
|
|
344
|
+
const answer = await question(` Open in browser? [y/n]: `);
|
|
345
|
+
if (["y", "yes"].includes(answer.toLowerCase())) {
|
|
346
|
+
await openBrowser(params.url);
|
|
347
|
+
return { action: "accept" };
|
|
348
|
+
}
|
|
349
|
+
return { action: "decline" };
|
|
350
|
+
} finally {
|
|
351
|
+
rl.close();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
// Helpers
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
function readStdinLine(): Promise<string> {
|
|
360
|
+
return new Promise((resolve) => {
|
|
361
|
+
const rl = createInterface({ input: process.stdin });
|
|
362
|
+
rl.once("line", (line) => {
|
|
363
|
+
rl.close();
|
|
364
|
+
resolve(line);
|
|
365
|
+
});
|
|
366
|
+
rl.once("close", () => resolve(""));
|
|
367
|
+
});
|
|
368
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
2
|
+
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
3
|
+
import type { HttpServerConfig } from "../config/schemas.ts";
|
|
4
|
+
import pkg from "../../package.json";
|
|
5
|
+
import { createDebugFetch } from "./debug-fetch.ts";
|
|
6
|
+
|
|
7
|
+
export function createHttpTransport(
|
|
8
|
+
config: HttpServerConfig,
|
|
9
|
+
authProvider?: OAuthClientProvider,
|
|
10
|
+
verbose = false,
|
|
11
|
+
showSecrets = false,
|
|
12
|
+
): StreamableHTTPClientTransport {
|
|
13
|
+
const requestInit: RequestInit = {};
|
|
14
|
+
const userAgent = `${pkg.name}/${pkg.version}`;
|
|
15
|
+
requestInit.headers = {
|
|
16
|
+
"User-Agent": userAgent,
|
|
17
|
+
...config.headers,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return new StreamableHTTPClientTransport(new URL(config.url), {
|
|
21
|
+
authProvider,
|
|
22
|
+
requestInit,
|
|
23
|
+
fetch: verbose ? createDebugFetch(showSecrets) : undefined,
|
|
24
|
+
});
|
|
25
|
+
}
|