@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.
Files changed (106) hide show
  1. package/.claude/settings.local.json +18 -0
  2. package/.claude/skills/mcpx.md +165 -0
  3. package/.claude/worktrees/elastic-jennings/.claude/settings.local.json +18 -0
  4. package/.claude/worktrees/elastic-jennings/.claude/skills/mcpcli.md +93 -0
  5. package/.claude/worktrees/elastic-jennings/.github/workflows/auto-release.yml +117 -0
  6. package/.claude/worktrees/elastic-jennings/.github/workflows/ci.yml +18 -0
  7. package/.claude/worktrees/elastic-jennings/.prettierignore +4 -0
  8. package/.claude/worktrees/elastic-jennings/.prettierrc +7 -0
  9. package/.claude/worktrees/elastic-jennings/CLAUDE.md +19 -0
  10. package/.claude/worktrees/elastic-jennings/LICENSE +21 -0
  11. package/.claude/worktrees/elastic-jennings/README.md +487 -0
  12. package/.claude/worktrees/elastic-jennings/bun.lock +381 -0
  13. package/.claude/worktrees/elastic-jennings/install.sh +55 -0
  14. package/.claude/worktrees/elastic-jennings/package.json +56 -0
  15. package/.claude/worktrees/elastic-jennings/src/cli.ts +39 -0
  16. package/.claude/worktrees/elastic-jennings/src/client/http.ts +100 -0
  17. package/.claude/worktrees/elastic-jennings/src/client/manager.ts +266 -0
  18. package/.claude/worktrees/elastic-jennings/src/client/oauth.ts +299 -0
  19. package/.claude/worktrees/elastic-jennings/src/client/stdio.ts +12 -0
  20. package/.claude/worktrees/elastic-jennings/src/commands/add.ts +155 -0
  21. package/.claude/worktrees/elastic-jennings/src/commands/auth.ts +114 -0
  22. package/.claude/worktrees/elastic-jennings/src/commands/exec.ts +91 -0
  23. package/.claude/worktrees/elastic-jennings/src/commands/index.ts +62 -0
  24. package/.claude/worktrees/elastic-jennings/src/commands/info.ts +38 -0
  25. package/.claude/worktrees/elastic-jennings/src/commands/list.ts +30 -0
  26. package/.claude/worktrees/elastic-jennings/src/commands/remove.ts +67 -0
  27. package/.claude/worktrees/elastic-jennings/src/commands/search.ts +45 -0
  28. package/.claude/worktrees/elastic-jennings/src/commands/skill.ts +70 -0
  29. package/.claude/worktrees/elastic-jennings/src/config/env.ts +41 -0
  30. package/.claude/worktrees/elastic-jennings/src/config/loader.ts +156 -0
  31. package/.claude/worktrees/elastic-jennings/src/config/schemas.ts +137 -0
  32. package/.claude/worktrees/elastic-jennings/src/context.ts +53 -0
  33. package/.claude/worktrees/elastic-jennings/src/output/formatter.ts +316 -0
  34. package/.claude/worktrees/elastic-jennings/src/output/logger.ts +114 -0
  35. package/.claude/worktrees/elastic-jennings/src/search/index.ts +69 -0
  36. package/.claude/worktrees/elastic-jennings/src/search/indexer.ts +92 -0
  37. package/.claude/worktrees/elastic-jennings/src/search/keyword.ts +86 -0
  38. package/.claude/worktrees/elastic-jennings/src/search/semantic.ts +75 -0
  39. package/.claude/worktrees/elastic-jennings/src/search/staleness.ts +8 -0
  40. package/.claude/worktrees/elastic-jennings/src/validation/schema.ts +77 -0
  41. package/.claude/worktrees/elastic-jennings/test/cli.test.ts +51 -0
  42. package/.claude/worktrees/elastic-jennings/test/client/manager.test.ts +249 -0
  43. package/.claude/worktrees/elastic-jennings/test/client/oauth.test.ts +328 -0
  44. package/.claude/worktrees/elastic-jennings/test/commands/add-remove.test.ts +253 -0
  45. package/.claude/worktrees/elastic-jennings/test/commands/exec.test.ts +105 -0
  46. package/.claude/worktrees/elastic-jennings/test/commands/info.test.ts +48 -0
  47. package/.claude/worktrees/elastic-jennings/test/commands/list.test.ts +39 -0
  48. package/.claude/worktrees/elastic-jennings/test/commands/skill.test.ts +98 -0
  49. package/.claude/worktrees/elastic-jennings/test/config/env.test.ts +61 -0
  50. package/.claude/worktrees/elastic-jennings/test/config/loader.test.ts +139 -0
  51. package/.claude/worktrees/elastic-jennings/test/fixtures/.keep +0 -0
  52. package/.claude/worktrees/elastic-jennings/test/fixtures/auth.json +10 -0
  53. package/.claude/worktrees/elastic-jennings/test/fixtures/mock-config/.keep +0 -0
  54. package/.claude/worktrees/elastic-jennings/test/fixtures/mock-config/servers.json +8 -0
  55. package/.claude/worktrees/elastic-jennings/test/fixtures/mock-server.ts +113 -0
  56. package/.claude/worktrees/elastic-jennings/test/fixtures/search.json +15 -0
  57. package/.claude/worktrees/elastic-jennings/test/fixtures/servers.json +18 -0
  58. package/.claude/worktrees/elastic-jennings/test/integration/stdio-server.test.ts +149 -0
  59. package/.claude/worktrees/elastic-jennings/test/output/formatter.test.ts +54 -0
  60. package/.claude/worktrees/elastic-jennings/test/output/logger.test.ts +89 -0
  61. package/.claude/worktrees/elastic-jennings/test/search/indexer.test.ts +32 -0
  62. package/.claude/worktrees/elastic-jennings/test/search/keyword.test.ts +80 -0
  63. package/.claude/worktrees/elastic-jennings/test/search/semantic.test.ts +32 -0
  64. package/.claude/worktrees/elastic-jennings/test/validation/schema.test.ts +113 -0
  65. package/.claude/worktrees/elastic-jennings/tsconfig.json +29 -0
  66. package/.cursor/rules/mcpx.mdc +165 -0
  67. package/LICENSE +21 -0
  68. package/README.md +627 -0
  69. package/package.json +58 -0
  70. package/src/cli.ts +72 -0
  71. package/src/client/browser.ts +24 -0
  72. package/src/client/debug-fetch.ts +81 -0
  73. package/src/client/elicitation.ts +368 -0
  74. package/src/client/http.ts +25 -0
  75. package/src/client/manager.ts +566 -0
  76. package/src/client/oauth.ts +314 -0
  77. package/src/client/sse.ts +17 -0
  78. package/src/client/stdio.ts +12 -0
  79. package/src/client/trace.ts +184 -0
  80. package/src/commands/add.ts +179 -0
  81. package/src/commands/auth.ts +114 -0
  82. package/src/commands/exec.ts +156 -0
  83. package/src/commands/index.ts +62 -0
  84. package/src/commands/info.ts +63 -0
  85. package/src/commands/list.ts +64 -0
  86. package/src/commands/ping.ts +69 -0
  87. package/src/commands/prompt.ts +60 -0
  88. package/src/commands/remove.ts +67 -0
  89. package/src/commands/resource.ts +46 -0
  90. package/src/commands/search.ts +49 -0
  91. package/src/commands/servers.ts +66 -0
  92. package/src/commands/skill.ts +112 -0
  93. package/src/commands/task.ts +82 -0
  94. package/src/config/env.ts +41 -0
  95. package/src/config/loader.ts +156 -0
  96. package/src/config/schemas.ts +152 -0
  97. package/src/context.ts +62 -0
  98. package/src/lib/input.ts +36 -0
  99. package/src/output/formatter.ts +884 -0
  100. package/src/output/logger.ts +173 -0
  101. package/src/search/index.ts +69 -0
  102. package/src/search/indexer.ts +92 -0
  103. package/src/search/keyword.ts +86 -0
  104. package/src/search/semantic.ts +75 -0
  105. package/src/search/staleness.ts +8 -0
  106. package/src/validation/schema.ts +103 -0
@@ -0,0 +1,69 @@
1
+ import { green, red } from "ansis";
2
+ import type { Command } from "commander";
3
+ import { getContext } from "../context.ts";
4
+ import { formatError } from "../output/formatter.ts";
5
+ import { logger } from "../output/logger.ts";
6
+
7
+ interface PingResult {
8
+ server: string;
9
+ success: boolean;
10
+ latencyMs?: number;
11
+ error?: string;
12
+ }
13
+
14
+ export function registerPingCommand(program: Command) {
15
+ program
16
+ .command("ping [servers...]")
17
+ .description("Check connectivity to MCP servers")
18
+ .action(async (servers: string[]) => {
19
+ const { manager, formatOptions } = await getContext(program);
20
+
21
+ const targetServers = servers.length > 0 ? servers : manager.getServerNames();
22
+
23
+ if (targetServers.length === 0) {
24
+ console.error(formatError("No servers configured", formatOptions));
25
+ await manager.close();
26
+ process.exit(1);
27
+ }
28
+
29
+ const spinner = logger.startSpinner(
30
+ `Pinging ${targetServers.length} server(s)...`,
31
+ formatOptions,
32
+ );
33
+
34
+ const results: PingResult[] = [];
35
+
36
+ try {
37
+ await Promise.all(
38
+ targetServers.map(async (serverName) => {
39
+ const start = Date.now();
40
+ try {
41
+ await manager.getClient(serverName);
42
+ results.push({ server: serverName, success: true, latencyMs: Date.now() - start });
43
+ } catch (err) {
44
+ results.push({ server: serverName, success: false, error: String(err) });
45
+ }
46
+ }),
47
+ );
48
+
49
+ spinner.stop();
50
+
51
+ if (formatOptions.json) {
52
+ console.log(JSON.stringify(results, null, 2));
53
+ } else {
54
+ for (const r of results) {
55
+ if (r.success) {
56
+ console.log(`${green("✔")} ${r.server} connected (${r.latencyMs}ms)`);
57
+ } else {
58
+ console.log(`${red("✖")} ${r.server} failed: ${r.error}`);
59
+ }
60
+ }
61
+ }
62
+ } finally {
63
+ await manager.close();
64
+ }
65
+
66
+ const anyFailed = results.some((r) => !r.success);
67
+ if (anyFailed) process.exit(1);
68
+ });
69
+ }
@@ -0,0 +1,60 @@
1
+ import type { Command } from "commander";
2
+ import { getContext } from "../context.ts";
3
+ import {
4
+ formatPromptList,
5
+ formatServerPrompts,
6
+ formatPromptMessages,
7
+ formatError,
8
+ } from "../output/formatter.ts";
9
+ import { logger } from "../output/logger.ts";
10
+ import { parseJsonArgs, readStdin } from "../lib/input.ts";
11
+
12
+ export function registerPromptCommand(program: Command) {
13
+ program
14
+ .command("prompt [server] [name] [args]")
15
+ .description("list prompts for a server, or get a specific prompt")
16
+ .action(
17
+ async (server: string | undefined, name: string | undefined, argsStr: string | undefined) => {
18
+ const { manager, formatOptions } = await getContext(program);
19
+ const spinner = logger.startSpinner(
20
+ server ? `Connecting to ${server}...` : "Connecting to servers...",
21
+ formatOptions,
22
+ );
23
+ try {
24
+ if (server && name) {
25
+ let args: Record<string, string> | undefined;
26
+
27
+ if (argsStr) {
28
+ args = parseJsonArgs(argsStr, { coerceToString: true }) as Record<string, string>;
29
+ } else if (!process.stdin.isTTY) {
30
+ const stdin = await readStdin();
31
+ if (stdin.trim()) {
32
+ args = parseJsonArgs(stdin, { coerceToString: true }) as Record<string, string>;
33
+ }
34
+ }
35
+
36
+ const result = await manager.getPrompt(server, name, args);
37
+ spinner.stop();
38
+ console.log(formatPromptMessages(server, name, result, formatOptions));
39
+ } else if (server) {
40
+ const prompts = await manager.listPrompts(server);
41
+ spinner.stop();
42
+ console.log(formatServerPrompts(server, prompts, formatOptions));
43
+ } else {
44
+ const { prompts, errors } = await manager.getAllPrompts();
45
+ spinner.stop();
46
+ console.log(formatPromptList(prompts, formatOptions));
47
+ for (const err of errors) {
48
+ console.error(formatError(`${err.server}: ${err.message}`, formatOptions));
49
+ }
50
+ }
51
+ } catch (err) {
52
+ spinner.error("Failed");
53
+ console.error(formatError(String(err), formatOptions));
54
+ process.exit(1);
55
+ } finally {
56
+ await manager.close();
57
+ }
58
+ },
59
+ );
60
+ }
@@ -0,0 +1,67 @@
1
+ import type { Command } from "commander";
2
+ import {
3
+ loadRawServers,
4
+ loadRawAuth,
5
+ loadSearchIndex,
6
+ saveServers,
7
+ saveAuth,
8
+ saveSearchIndex,
9
+ } from "../config/loader.ts";
10
+
11
+ export function registerRemoveCommand(program: Command) {
12
+ program
13
+ .command("remove <name>")
14
+ .description("remove an MCP server from your config")
15
+ .option("--keep-auth", "keep stored authentication credentials")
16
+ .option("--dry-run", "show what would be removed without changing files")
17
+ .action(async (name: string, options: { keepAuth?: boolean; dryRun?: boolean }) => {
18
+ const configFlag = program.opts().config;
19
+ const { configDir, servers } = await loadRawServers(configFlag);
20
+
21
+ if (!servers.mcpServers[name]) {
22
+ console.error(`Unknown server: "${name}"`);
23
+ process.exit(1);
24
+ }
25
+
26
+ if (options.dryRun) {
27
+ console.log(`Would remove server "${name}" from ${configDir}/servers.json`);
28
+ if (!options.keepAuth) {
29
+ const auth = await loadRawAuth(configDir);
30
+ if (auth[name]) {
31
+ console.log(`Would remove auth for "${name}" from ${configDir}/auth.json`);
32
+ }
33
+ }
34
+ const searchIndex = await loadSearchIndex(configDir);
35
+ const indexedCount = searchIndex.tools.filter((t) => t.server === name).length;
36
+ if (indexedCount > 0) {
37
+ console.log(
38
+ `Would remove ${indexedCount} tool(s) for "${name}" from ${configDir}/search.json`,
39
+ );
40
+ }
41
+ return;
42
+ }
43
+
44
+ delete servers.mcpServers[name];
45
+ await saveServers(configDir, servers);
46
+ console.log(`Removed server "${name}" from ${configDir}/servers.json`);
47
+
48
+ if (!options.keepAuth) {
49
+ const auth = await loadRawAuth(configDir);
50
+ if (auth[name]) {
51
+ delete auth[name];
52
+ await saveAuth(configDir, auth);
53
+ console.log(`Removed auth for "${name}" from ${configDir}/auth.json`);
54
+ }
55
+ }
56
+
57
+ // Remove tools for this server from the search index
58
+ const searchIndex = await loadSearchIndex(configDir);
59
+ const before = searchIndex.tools.length;
60
+ searchIndex.tools = searchIndex.tools.filter((t) => t.server !== name);
61
+ const removed = before - searchIndex.tools.length;
62
+ if (removed > 0) {
63
+ await saveSearchIndex(configDir, searchIndex);
64
+ console.log(`Removed ${removed} tool(s) for "${name}" from ${configDir}/search.json`);
65
+ }
66
+ });
67
+ }
@@ -0,0 +1,46 @@
1
+ import type { Command } from "commander";
2
+ import { getContext } from "../context.ts";
3
+ import {
4
+ formatResourceList,
5
+ formatServerResources,
6
+ formatResourceContents,
7
+ formatError,
8
+ } from "../output/formatter.ts";
9
+ import { logger } from "../output/logger.ts";
10
+
11
+ export function registerResourceCommand(program: Command) {
12
+ program
13
+ .command("resource [server] [uri]")
14
+ .description("list resources for a server, or read a specific resource")
15
+ .action(async (server: string | undefined, uri: string | undefined) => {
16
+ const { manager, formatOptions } = await getContext(program);
17
+ const spinner = logger.startSpinner(
18
+ server ? `Connecting to ${server}...` : "Connecting to servers...",
19
+ formatOptions,
20
+ );
21
+ try {
22
+ if (server && uri) {
23
+ const result = await manager.readResource(server, uri);
24
+ spinner.stop();
25
+ console.log(formatResourceContents(server, uri, result, formatOptions));
26
+ } else if (server) {
27
+ const resources = await manager.listResources(server);
28
+ spinner.stop();
29
+ console.log(formatServerResources(server, resources, formatOptions));
30
+ } else {
31
+ const { resources, errors } = await manager.getAllResources();
32
+ spinner.stop();
33
+ console.log(formatResourceList(resources, formatOptions));
34
+ for (const err of errors) {
35
+ console.error(formatError(`${err.server}: ${err.message}`, formatOptions));
36
+ }
37
+ }
38
+ } catch (err) {
39
+ spinner.error("Failed");
40
+ console.error(formatError(String(err), formatOptions));
41
+ process.exit(1);
42
+ } finally {
43
+ await manager.close();
44
+ }
45
+ });
46
+ }
@@ -0,0 +1,49 @@
1
+ import type { Command } from "commander";
2
+ import { getContext } from "../context.ts";
3
+ import { search } from "../search/index.ts";
4
+ import { getStaleServers } from "../search/staleness.ts";
5
+ import { formatError, formatSearchResults } from "../output/formatter.ts";
6
+ import { logger } from "../output/logger.ts";
7
+
8
+ export function registerSearchCommand(program: Command) {
9
+ program
10
+ .command("search <terms...>")
11
+ .description("search tools by keyword and/or semantic similarity")
12
+ .option("-k, --keyword", "keyword/glob search only")
13
+ .option("-q, --query", "semantic search only")
14
+ .option("-n, --limit <number>", "max results to return", "10")
15
+ .action(
16
+ async (terms: string[], options: { keyword?: boolean; query?: boolean; limit: string }) => {
17
+ const query = terms.join(" ");
18
+ const { config, formatOptions } = await getContext(program);
19
+
20
+ if (config.searchIndex.tools.length === 0) {
21
+ console.error(formatError("No search index found. Run: mcpx index", formatOptions));
22
+ process.exit(1);
23
+ }
24
+
25
+ const stale = getStaleServers(config.searchIndex, config.servers);
26
+ if (stale.length > 0) {
27
+ logger.warn(
28
+ `Warning: index has tools for removed servers: ${stale.join(", ")}. Run: mcpx index`,
29
+ );
30
+ }
31
+
32
+ const spinner = logger.startSpinner("Searching...", formatOptions);
33
+
34
+ try {
35
+ const results = await search(query, config.searchIndex, {
36
+ keywordOnly: options.keyword,
37
+ semanticOnly: options.query,
38
+ topK: parseInt(options.limit, 10),
39
+ });
40
+ spinner.stop();
41
+ console.log(formatSearchResults(results, formatOptions));
42
+ } catch (err) {
43
+ spinner.stop();
44
+ console.error(formatError(String(err), formatOptions));
45
+ process.exit(1);
46
+ }
47
+ },
48
+ );
49
+ }
@@ -0,0 +1,66 @@
1
+ import { cyan, dim, green, yellow } from "ansis";
2
+ import type { Command } from "commander";
3
+ import { getContext } from "../context.ts";
4
+ import { isStdioServer, isHttpServer } from "../config/schemas.ts";
5
+ import { formatError, isInteractive } from "../output/formatter.ts";
6
+
7
+ export function registerServersCommand(program: Command) {
8
+ program
9
+ .command("servers")
10
+ .description("List configured MCP servers")
11
+ .action(async () => {
12
+ const { manager, config, formatOptions } = await getContext(program);
13
+ try {
14
+ const servers = Object.entries(config.servers.mcpServers);
15
+
16
+ if (!isInteractive(formatOptions)) {
17
+ console.log(
18
+ JSON.stringify(
19
+ servers.map(([name, cfg]) => ({
20
+ name,
21
+ type: isStdioServer(cfg) ? "stdio" : "http",
22
+ ...(isHttpServer(cfg) ? { transport: cfg.transport ?? "http" } : {}),
23
+ ...(isStdioServer(cfg)
24
+ ? { command: cfg.command, args: cfg.args ?? [] }
25
+ : { url: cfg.url }),
26
+ })),
27
+ null,
28
+ 2,
29
+ ),
30
+ );
31
+ return;
32
+ }
33
+
34
+ if (servers.length === 0) {
35
+ console.log(dim("No servers configured"));
36
+ return;
37
+ }
38
+
39
+ const maxName = Math.max(...servers.map(([n]) => n.length));
40
+
41
+ function typeLabel(cfg: (typeof servers)[number][1]): string {
42
+ if (isStdioServer(cfg)) return "stdio";
43
+ if (isHttpServer(cfg) && cfg.transport === "sse") return "http/sse";
44
+ if (isHttpServer(cfg) && cfg.transport === "streamable-http") return "http/streamable";
45
+ return "http";
46
+ }
47
+ const maxType = Math.max(...servers.map(([, cfg]) => typeLabel(cfg).length));
48
+
49
+ for (const [name, cfg] of servers) {
50
+ const n = cyan(name.padEnd(maxName));
51
+ const type = isStdioServer(cfg)
52
+ ? green(typeLabel(cfg).padEnd(maxType))
53
+ : yellow(typeLabel(cfg).padEnd(maxType));
54
+ const detail = isStdioServer(cfg)
55
+ ? dim([cfg.command, ...(cfg.args ?? [])].join(" "))
56
+ : dim(cfg.url);
57
+ console.log(`${n} ${type} ${detail}`);
58
+ }
59
+ } catch (err) {
60
+ console.error(formatError(String(err), formatOptions));
61
+ process.exit(1);
62
+ } finally {
63
+ await manager.close();
64
+ }
65
+ });
66
+ }
@@ -0,0 +1,112 @@
1
+ import type { Command } from "commander";
2
+ import { resolve, dirname, join } from "path";
3
+ import { readFile, mkdir, writeFile, access } from "fs/promises";
4
+ import { homedir } from "os";
5
+
6
+ interface SkillTarget {
7
+ label: string;
8
+ dir: string;
9
+ filename: string;
10
+ }
11
+
12
+ export function registerSkillCommand(program: Command) {
13
+ const skill = program.command("skill").description("manage mcpx skills");
14
+
15
+ skill
16
+ .command("install")
17
+ .description("install the mcpx skill for an AI agent")
18
+ .option("--claude", "install for Claude Code")
19
+ .option("--cursor", "install for Cursor")
20
+ .option("--global", "install to global location (e.g. ~/.claude/skills/)")
21
+ .option("--project", "install to project location (default)")
22
+ .option("-f, --force", "overwrite if file already exists")
23
+ .action(
24
+ async (options: {
25
+ claude?: boolean;
26
+ cursor?: boolean;
27
+ global?: boolean;
28
+ project?: boolean;
29
+ force?: boolean;
30
+ }) => {
31
+ if (!options.claude && !options.cursor) {
32
+ console.error("error: specify at least one agent target: --claude, --cursor");
33
+ process.exit(1);
34
+ }
35
+
36
+ const agents: {
37
+ name: string;
38
+ sourcePath: string;
39
+ globalDir: string;
40
+ projectDir: string;
41
+ filename: string;
42
+ }[] = [];
43
+
44
+ if (options.claude) {
45
+ agents.push({
46
+ name: "Claude Code",
47
+ sourcePath: resolve(dirname(Bun.main), "..", ".claude", "skills", "mcpx.md"),
48
+ globalDir: join(homedir(), ".claude", "skills"),
49
+ projectDir: resolve(".claude", "skills"),
50
+ filename: "mcpx.md",
51
+ });
52
+ }
53
+
54
+ if (options.cursor) {
55
+ agents.push({
56
+ name: "Cursor",
57
+ sourcePath: resolve(dirname(Bun.main), "..", ".cursor", "rules", "mcpx.mdc"),
58
+ globalDir: join(homedir(), ".cursor", "rules"),
59
+ projectDir: resolve(".cursor", "rules"),
60
+ filename: "mcpx.mdc",
61
+ });
62
+ }
63
+
64
+ for (const agent of agents) {
65
+ let content: string;
66
+ try {
67
+ content = await readFile(agent.sourcePath, "utf-8");
68
+ } catch {
69
+ console.error(`Could not read skill file: ${agent.sourcePath}`);
70
+ process.exit(1);
71
+ }
72
+
73
+ // Determine targets — default to project if neither flag is set
74
+ const targets: SkillTarget[] = [];
75
+
76
+ if (options.global) {
77
+ targets.push({
78
+ label: "global",
79
+ dir: agent.globalDir,
80
+ filename: agent.filename,
81
+ });
82
+ }
83
+ if (options.project || !options.global) {
84
+ targets.push({
85
+ label: "project",
86
+ dir: agent.projectDir,
87
+ filename: agent.filename,
88
+ });
89
+ }
90
+
91
+ for (const target of targets) {
92
+ const dest = join(target.dir, target.filename);
93
+
94
+ // Check if file already exists
95
+ if (!options.force) {
96
+ try {
97
+ await access(dest);
98
+ console.error(`${dest} already exists (use --force to overwrite)`);
99
+ process.exit(1);
100
+ } catch {
101
+ // File doesn't exist — good
102
+ }
103
+ }
104
+
105
+ await mkdir(target.dir, { recursive: true });
106
+ await writeFile(dest, content, "utf-8");
107
+ console.log(`Installed mcpx skill for ${agent.name} (${target.label}): ${dest}`);
108
+ }
109
+ }
110
+ },
111
+ );
112
+ }
@@ -0,0 +1,82 @@
1
+ import type { Command } from "commander";
2
+ import { getContext } from "../context.ts";
3
+ import {
4
+ formatCallResult,
5
+ formatError,
6
+ formatTaskStatus,
7
+ formatTasksList,
8
+ } from "../output/formatter.ts";
9
+ import { logger } from "../output/logger.ts";
10
+
11
+ export function registerTaskCommand(program: Command) {
12
+ program
13
+ .command("task <action> <server> [taskId]")
14
+ .description("manage tasks (actions: get, list, result, cancel)")
15
+ .action(async (action: string, server: string, taskId: string | undefined) => {
16
+ const { manager, formatOptions } = await getContext(program);
17
+ const spinner = logger.startSpinner(`Connecting to ${server}...`, formatOptions);
18
+
19
+ try {
20
+ switch (action) {
21
+ case "list": {
22
+ const result = await manager.listTasks(server);
23
+ spinner.stop();
24
+ console.log(formatTasksList(result.tasks, result.nextCursor, formatOptions));
25
+ break;
26
+ }
27
+ case "get": {
28
+ if (!taskId) {
29
+ spinner.error("Missing task ID");
30
+ console.error(formatError("Usage: mcpx task get <server> <taskId>", formatOptions));
31
+ process.exit(1);
32
+ }
33
+ const task = await manager.getTask(server, taskId);
34
+ spinner.stop();
35
+ console.log(formatTaskStatus(task, formatOptions));
36
+ break;
37
+ }
38
+ case "result": {
39
+ if (!taskId) {
40
+ spinner.error("Missing task ID");
41
+ console.error(
42
+ formatError("Usage: mcpx task result <server> <taskId>", formatOptions),
43
+ );
44
+ process.exit(1);
45
+ }
46
+ const result = await manager.getTaskResult(server, taskId);
47
+ spinner.stop();
48
+ console.log(formatCallResult(result, formatOptions));
49
+ break;
50
+ }
51
+ case "cancel": {
52
+ if (!taskId) {
53
+ spinner.error("Missing task ID");
54
+ console.error(
55
+ formatError("Usage: mcpx task cancel <server> <taskId>", formatOptions),
56
+ );
57
+ process.exit(1);
58
+ }
59
+ const cancelled = await manager.cancelTask(server, taskId);
60
+ spinner.stop();
61
+ console.log(formatTaskStatus(cancelled, formatOptions));
62
+ break;
63
+ }
64
+ default:
65
+ spinner.error("Unknown action");
66
+ console.error(
67
+ formatError(
68
+ `Unknown task action: "${action}". Use: get, list, result, cancel`,
69
+ formatOptions,
70
+ ),
71
+ );
72
+ process.exit(1);
73
+ }
74
+ } catch (err) {
75
+ spinner.error("Failed");
76
+ console.error(formatError(String(err), formatOptions));
77
+ process.exit(1);
78
+ } finally {
79
+ await manager.close();
80
+ }
81
+ });
82
+ }
@@ -0,0 +1,41 @@
1
+ const ENV_VAR_PATTERN = /\$\{([^}]+)\}/g;
2
+
3
+ /** Whether to throw on missing env vars (default: true) */
4
+ function isStrictEnv(): boolean {
5
+ return process.env.MCP_STRICT_ENV !== "false";
6
+ }
7
+
8
+ /** Replace ${VAR_NAME} in a string with the corresponding env var value */
9
+ export function interpolateEnvString(value: string): string {
10
+ return value.replace(ENV_VAR_PATTERN, (_match, varName: string) => {
11
+ const envValue = process.env[varName];
12
+ if (envValue === undefined) {
13
+ if (isStrictEnv()) {
14
+ throw new Error(
15
+ `Environment variable "${varName}" is not set (set MCP_STRICT_ENV=false to warn instead)`,
16
+ );
17
+ }
18
+ console.warn(`Warning: environment variable "${varName}" is not set`);
19
+ return "";
20
+ }
21
+ return envValue;
22
+ });
23
+ }
24
+
25
+ /** Recursively interpolate env vars in all string values of an object */
26
+ export function interpolateEnv<T>(obj: T): T {
27
+ if (typeof obj === "string") {
28
+ return interpolateEnvString(obj) as T;
29
+ }
30
+ if (Array.isArray(obj)) {
31
+ return obj.map((item) => interpolateEnv(item)) as T;
32
+ }
33
+ if (typeof obj === "object" && obj !== null) {
34
+ const result: Record<string, unknown> = {};
35
+ for (const [key, value] of Object.entries(obj)) {
36
+ result[key] = interpolateEnv(value);
37
+ }
38
+ return result as T;
39
+ }
40
+ return obj;
41
+ }