@modeltoolsprotocol/mtpcli 0.1.0 → 0.2.1

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 (3) hide show
  1. package/README.md +38 -9
  2. package/dist/index.js +733 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@ The command-line interface for the [Model Tools Protocol](https://github.com/mod
6
6
 
7
7
  LLM agents need to discover and use tools. Right now there are two worlds:
8
8
 
9
- **CLI tools** are the backbone of software development. They're composable (`|`), scriptable, version-controlled, and work everywhere. But LLMs can't discover what a CLI does -they have to parse `--help` text, guess at arguments, and hope for the best.
9
+ **CLI tools** are the backbone of software development. They're composable (`|`), scriptable, version-controlled, and work everywhere. But LLMs can't discover what a CLI does - they have to parse `--help` text, guess at arguments, and hope for the best.
10
10
 
11
11
  **MCP (Model Context Protocol)** solves discovery beautifully. Tools declare typed schemas, and LLM hosts discover them via a structured handshake. But MCP requires running a server process, speaking JSON-RPC over stdio/SSE, and building within the MCP ecosystem. Your existing CLI tools don't get any of this for free.
12
12
 
@@ -55,12 +55,15 @@ mtpcli search --scan-path "git commit"
55
55
  # OAuth2 login
56
56
  mtpcli auth login mytool
57
57
 
58
- # API key login
59
- mtpcli auth login mytool --method apikey
58
+ # API key / bearer token login
59
+ mtpcli auth login mytool --token sk-xxx
60
60
 
61
61
  # Check auth status
62
62
  mtpcli auth status mytool
63
63
 
64
+ # Inject token into env
65
+ eval $(mtpcli auth env mytool)
66
+
64
67
  # Log out
65
68
  mtpcli auth logout mytool
66
69
  ```
@@ -69,17 +72,43 @@ mtpcli auth logout mytool
69
72
 
70
73
  ```bash
71
74
  # Start an MCP server that bridges describe-compatible tools
72
- mtpcli serve -- mytool anothertool
75
+ mtpcli serve --tool mytool --tool anothertool
76
+ ```
73
77
 
74
- # Serve with a specific transport
75
- mtpcli serve --transport sse -- mytool
78
+ ### Wrap an MCP server as a CLI
79
+
80
+ ```bash
81
+ # Describe an MCP server's tools
82
+ mtpcli wrap --server "npx @mcp/server-github" --describe
83
+
84
+ # Call a tool on an MCP server
85
+ mtpcli wrap --server "npx @mcp/server-github" search_repos -- --query mtpcli
76
86
  ```
77
87
 
78
- ### Wrap a tool as an MCP server
88
+ ### Validate a tool's --describe output
79
89
 
80
90
  ```bash
81
- # Wrap a single tool so it speaks MCP
82
- mtpcli wrap mytool
91
+ # Validate a tool against the MTP spec
92
+ mtpcli validate mytool
93
+
94
+ # Validate JSON from stdin (for CI)
95
+ cat describe.json | mtpcli validate --stdin
96
+
97
+ # JSON output
98
+ mtpcli validate mytool --json
99
+ ```
100
+
101
+ ### Generate shell completions
102
+
103
+ ```bash
104
+ # Bash
105
+ eval $(mtpcli completions bash mytool)
106
+
107
+ # Zsh
108
+ eval $(mtpcli completions zsh mytool)
109
+
110
+ # Fish
111
+ mtpcli completions fish mytool | source
83
112
  ```
84
113
 
85
114
  ### Describe self
package/dist/index.js CHANGED
@@ -7917,17 +7917,38 @@ function sendNotification(writer, method, params) {
7917
7917
  writer.write(JSON.stringify(req) + `
7918
7918
  `);
7919
7919
  }
7920
- async function readResponse(rl) {
7921
- for await (const line of rl) {
7922
- const trimmed = line.trim();
7923
- if (!trimmed)
7924
- continue;
7925
- const msg = JSON.parse(trimmed);
7926
- if ("id" in msg && msg.id !== undefined) {
7927
- return JsonRpcResponseSchema2.parse(msg);
7928
- }
7929
- }
7930
- throw new Error("MCP server closed connection");
7920
+ function readResponse(rl) {
7921
+ return new Promise((resolve, reject) => {
7922
+ const onLine = (line) => {
7923
+ const trimmed = line.trim();
7924
+ if (!trimmed)
7925
+ return;
7926
+ let msg;
7927
+ try {
7928
+ msg = JSON.parse(trimmed);
7929
+ } catch (e) {
7930
+ rl.off("line", onLine);
7931
+ rl.off("close", onClose);
7932
+ reject(e);
7933
+ return;
7934
+ }
7935
+ if ("id" in msg && msg.id !== undefined) {
7936
+ rl.off("line", onLine);
7937
+ rl.off("close", onClose);
7938
+ try {
7939
+ resolve(JsonRpcResponseSchema2.parse(msg));
7940
+ } catch (e) {
7941
+ reject(e);
7942
+ }
7943
+ }
7944
+ };
7945
+ const onClose = () => {
7946
+ rl.off("line", onLine);
7947
+ reject(new Error("MCP server closed connection"));
7948
+ };
7949
+ rl.on("line", onLine);
7950
+ rl.on("close", onClose);
7951
+ });
7931
7952
  }
7932
7953
  var init_mcp = __esm(() => {
7933
7954
  init_models();
@@ -8452,6 +8473,614 @@ var init_wrap = __esm(() => {
8452
8473
  init_mcp();
8453
8474
  });
8454
8475
 
8476
+ // src/validate.ts
8477
+ var exports_validate = {};
8478
+ __export(exports_validate, {
8479
+ validateTool: () => validateTool,
8480
+ validateJson: () => validateJson,
8481
+ run: () => run3,
8482
+ crossReferenceHelp: () => crossReferenceHelp
8483
+ });
8484
+ import { execFile as execFile10 } from "node:child_process";
8485
+ import { promisify as promisify10 } from "node:util";
8486
+ function validateJson(raw) {
8487
+ const diags = [];
8488
+ let parsed;
8489
+ try {
8490
+ parsed = JSON.parse(raw);
8491
+ } catch (e) {
8492
+ diags.push({
8493
+ level: "error",
8494
+ code: "INVALID_JSON",
8495
+ message: `Not valid JSON: ${e instanceof Error ? e.message : e}`
8496
+ });
8497
+ return diags;
8498
+ }
8499
+ const result = ToolSchemaSchema2.safeParse(parsed);
8500
+ if (!result.success) {
8501
+ for (const issue of result.error.issues) {
8502
+ diags.push({
8503
+ level: "error",
8504
+ code: "SCHEMA_VIOLATION",
8505
+ message: issue.message,
8506
+ path: issue.path.join(".")
8507
+ });
8508
+ }
8509
+ return diags;
8510
+ }
8511
+ const schema = result.data;
8512
+ for (let ci = 0;ci < schema.commands.length; ci++) {
8513
+ const cmd = schema.commands[ci];
8514
+ const cmdPath = `commands.${ci}`;
8515
+ if (cmd.examples.length === 0) {
8516
+ diags.push({
8517
+ level: "warning",
8518
+ code: "MISSING_EXAMPLES",
8519
+ message: `Command "${cmd.name}" has no examples`,
8520
+ path: `${cmdPath}.examples`
8521
+ });
8522
+ }
8523
+ for (let ai = 0;ai < cmd.args.length; ai++) {
8524
+ const arg = cmd.args[ai];
8525
+ const argPath = `${cmdPath}.args.${ai}`;
8526
+ if (!VALID_ARG_TYPES.has(arg.type)) {
8527
+ diags.push({
8528
+ level: "error",
8529
+ code: "INVALID_ARG_TYPE",
8530
+ message: `Arg "${arg.name}" has invalid type "${arg.type}"`,
8531
+ path: `${argPath}.type`
8532
+ });
8533
+ }
8534
+ if (arg.type === "enum" && (!arg.values || arg.values.length === 0)) {
8535
+ diags.push({
8536
+ level: "warning",
8537
+ code: "ENUM_NO_VALUES",
8538
+ message: `Enum arg "${arg.name}" has no values`,
8539
+ path: `${argPath}.values`
8540
+ });
8541
+ }
8542
+ if (!arg.description) {
8543
+ diags.push({
8544
+ level: "warning",
8545
+ code: "MISSING_DESCRIPTION",
8546
+ message: `Arg "${arg.name}" has no description`,
8547
+ path: `${argPath}.description`
8548
+ });
8549
+ }
8550
+ }
8551
+ }
8552
+ return diags;
8553
+ }
8554
+ function crossReferenceHelp(schema, helpText) {
8555
+ const diags = [];
8556
+ const helpFlags = new Set;
8557
+ const flagRe = /--([\w][\w-]*)/g;
8558
+ let m;
8559
+ while ((m = flagRe.exec(helpText)) !== null) {
8560
+ helpFlags.add(`--${m[1]}`);
8561
+ }
8562
+ helpFlags.delete("--help");
8563
+ helpFlags.delete("--version");
8564
+ helpFlags.delete("--describe");
8565
+ const declaredFlags = new Set;
8566
+ for (const cmd of schema.commands) {
8567
+ for (const arg of cmd.args) {
8568
+ if (arg.name.startsWith("--")) {
8569
+ declaredFlags.add(arg.name);
8570
+ }
8571
+ }
8572
+ }
8573
+ for (const flag of declaredFlags) {
8574
+ if (!helpFlags.has(flag)) {
8575
+ diags.push({
8576
+ level: "info",
8577
+ code: "HELP_ARG_MISMATCH",
8578
+ message: `Flag "${flag}" in --describe but not found in --help`
8579
+ });
8580
+ }
8581
+ }
8582
+ for (const flag of helpFlags) {
8583
+ if (!declaredFlags.has(flag)) {
8584
+ diags.push({
8585
+ level: "info",
8586
+ code: "HELP_ARG_MISMATCH",
8587
+ message: `Flag "${flag}" in --help but not found in --describe`
8588
+ });
8589
+ }
8590
+ }
8591
+ return diags;
8592
+ }
8593
+ async function validateTool(toolName, opts) {
8594
+ const diags = [];
8595
+ let toolPath;
8596
+ try {
8597
+ toolPath = await import_which3.default(toolName);
8598
+ } catch {
8599
+ diags.push({
8600
+ level: "error",
8601
+ code: "NOT_FOUND",
8602
+ message: `Tool "${toolName}" not found in PATH`
8603
+ });
8604
+ return buildResult(toolName, diags);
8605
+ }
8606
+ let describeOutput;
8607
+ try {
8608
+ const { stdout } = await execFileAsync8(toolPath, ["--describe"], {
8609
+ timeout: 1e4
8610
+ });
8611
+ describeOutput = stdout.trim();
8612
+ if (!describeOutput) {
8613
+ diags.push({
8614
+ level: "error",
8615
+ code: "DESCRIBE_FAILED",
8616
+ message: `"${toolName} --describe" produced empty output`
8617
+ });
8618
+ return buildResult(toolName, diags);
8619
+ }
8620
+ } catch (e) {
8621
+ diags.push({
8622
+ level: "error",
8623
+ code: "DESCRIBE_FAILED",
8624
+ message: `"${toolName} --describe" failed: ${e instanceof Error ? e.message : e}`
8625
+ });
8626
+ return buildResult(toolName, diags);
8627
+ }
8628
+ diags.push(...validateJson(describeOutput));
8629
+ const hasErrors = diags.some((d) => d.level === "error");
8630
+ if (!hasErrors && !opts?.skipHelp) {
8631
+ try {
8632
+ const { stdout: helpText } = await execFileAsync8(toolPath, ["--help"], {
8633
+ timeout: 5000
8634
+ });
8635
+ const schema = ToolSchemaSchema2.parse(JSON.parse(describeOutput));
8636
+ diags.push(...crossReferenceHelp(schema, helpText));
8637
+ } catch {}
8638
+ }
8639
+ return buildResult(toolName, diags);
8640
+ }
8641
+ function buildResult(tool, diagnostics) {
8642
+ const errors2 = diagnostics.filter((d) => d.level === "error").length;
8643
+ const warnings = diagnostics.filter((d) => d.level === "warning").length;
8644
+ const info = diagnostics.filter((d) => d.level === "info").length;
8645
+ return {
8646
+ tool,
8647
+ valid: errors2 === 0,
8648
+ diagnostics,
8649
+ summary: { errors: errors2, warnings, info }
8650
+ };
8651
+ }
8652
+ async function run3(toolOrStdin, opts) {
8653
+ let result;
8654
+ if (opts.stdin) {
8655
+ const chunks = [];
8656
+ for await (const chunk of process.stdin) {
8657
+ chunks.push(chunk);
8658
+ }
8659
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
8660
+ const diags = validateJson(raw);
8661
+ result = buildResult(opts.stdin ? "stdin" : "unknown", diags);
8662
+ } else if (toolOrStdin) {
8663
+ result = await validateTool(toolOrStdin, { skipHelp: opts.skipHelp });
8664
+ } else {
8665
+ process.stderr.write(`error: provide a tool name or use --stdin
8666
+ `);
8667
+ process.exit(1);
8668
+ }
8669
+ if (opts.json) {
8670
+ console.log(JSON.stringify(result, null, 2));
8671
+ } else {
8672
+ printHuman(result);
8673
+ }
8674
+ if (!result.valid)
8675
+ process.exit(1);
8676
+ }
8677
+ function printHuman(result) {
8678
+ const icon = result.valid ? "PASS" : "FAIL";
8679
+ console.log(`${icon} ${result.tool}`);
8680
+ for (const d of result.diagnostics) {
8681
+ const tag = d.level === "error" ? "ERR" : d.level === "warning" ? "WRN" : "INF";
8682
+ const path2 = d.path ? ` (${d.path})` : "";
8683
+ console.log(` ${tag} [${d.code}] ${d.message}${path2}`);
8684
+ }
8685
+ const parts = [];
8686
+ if (result.summary.errors)
8687
+ parts.push(`${result.summary.errors} error(s)`);
8688
+ if (result.summary.warnings)
8689
+ parts.push(`${result.summary.warnings} warning(s)`);
8690
+ if (result.summary.info)
8691
+ parts.push(`${result.summary.info} info`);
8692
+ if (parts.length)
8693
+ console.log(` ${parts.join(", ")}`);
8694
+ }
8695
+ var import_which3, execFileAsync8, VALID_ARG_TYPES;
8696
+ var init_validate = __esm(() => {
8697
+ init_models();
8698
+ import_which3 = __toESM(require_lib(), 1);
8699
+ execFileAsync8 = promisify10(execFile10);
8700
+ VALID_ARG_TYPES = new Set([
8701
+ "string",
8702
+ "integer",
8703
+ "number",
8704
+ "boolean",
8705
+ "array",
8706
+ "enum",
8707
+ "path"
8708
+ ]);
8709
+ });
8710
+
8711
+ // src/completions.ts
8712
+ var exports_completions = {};
8713
+ __export(exports_completions, {
8714
+ schemaToCompletionContext: () => schemaToCompletionContext,
8715
+ run: () => run4,
8716
+ generateZsh: () => generateZsh,
8717
+ generateFish: () => generateFish,
8718
+ generateBash: () => generateBash,
8719
+ generate: () => generate
8720
+ });
8721
+ function extractFlags(args) {
8722
+ const flags = [];
8723
+ for (const arg of args) {
8724
+ if (arg.name.startsWith("--")) {
8725
+ flags.push({
8726
+ name: arg.name,
8727
+ description: arg.description ?? "",
8728
+ type: arg.type,
8729
+ values: arg.values
8730
+ });
8731
+ }
8732
+ }
8733
+ return flags;
8734
+ }
8735
+ function schemaToCompletionContext(schema) {
8736
+ let rootFlags = [];
8737
+ const groups = new Map;
8738
+ for (const cmd of schema.commands) {
8739
+ if (cmd.name === "_root") {
8740
+ rootFlags = extractFlags(cmd.args);
8741
+ continue;
8742
+ }
8743
+ const parts = cmd.name.split(/\s+/);
8744
+ const key = parts[0];
8745
+ const entry = { parts, description: cmd.description, flags: extractFlags(cmd.args) };
8746
+ let group = groups.get(key);
8747
+ if (!group) {
8748
+ group = [];
8749
+ groups.set(key, group);
8750
+ }
8751
+ group.push(entry);
8752
+ }
8753
+ const commands = [];
8754
+ for (const [key, entries] of groups) {
8755
+ if (entries.length === 1 && entries[0].parts.length === 1) {
8756
+ commands.push({
8757
+ name: key,
8758
+ description: entries[0].description,
8759
+ flags: entries[0].flags,
8760
+ children: []
8761
+ });
8762
+ } else {
8763
+ const children = [];
8764
+ let groupDesc = "";
8765
+ for (const entry of entries) {
8766
+ const childName = entry.parts.slice(1).join(" ");
8767
+ if (!childName) {
8768
+ groupDesc = entry.description;
8769
+ continue;
8770
+ }
8771
+ children.push({
8772
+ name: childName,
8773
+ description: entry.description,
8774
+ flags: entry.flags,
8775
+ children: []
8776
+ });
8777
+ }
8778
+ if (!groupDesc && children.length > 0) {
8779
+ groupDesc = `${key} commands`;
8780
+ }
8781
+ commands.push({
8782
+ name: key,
8783
+ description: groupDesc,
8784
+ flags: [],
8785
+ children
8786
+ });
8787
+ }
8788
+ }
8789
+ return { toolName: schema.name, rootFlags, commands };
8790
+ }
8791
+ function generateBash(toolName, ctx) {
8792
+ const funcName = `_${toolName.replace(/[^a-zA-Z0-9]/g, "_")}_completions`;
8793
+ const topNames = ctx.commands.map((c) => c.name);
8794
+ const lines = [];
8795
+ lines.push(`${funcName}() {`);
8796
+ lines.push(` local cur prev words cword`);
8797
+ lines.push(` _get_comp_words_by_ref -n : cur prev words cword 2>/dev/null || {`);
8798
+ lines.push(` cur="\${COMP_WORDS[COMP_CWORD]}"`);
8799
+ lines.push(` prev="\${COMP_WORDS[COMP_CWORD-1]}"`);
8800
+ lines.push(` words=("\${COMP_WORDS[@]}")`);
8801
+ lines.push(` cword=$COMP_CWORD`);
8802
+ lines.push(` }`);
8803
+ lines.push(``);
8804
+ lines.push(` if ((cword == 1)); then`);
8805
+ const toplevel = [...topNames];
8806
+ if (ctx.rootFlags.length > 0) {
8807
+ toplevel.push(...ctx.rootFlags.map((f) => f.name));
8808
+ }
8809
+ lines.push(` COMPREPLY=($(compgen -W "${toplevel.join(" ")}" -- "$cur"))`);
8810
+ lines.push(` return`);
8811
+ lines.push(` fi`);
8812
+ lines.push(``);
8813
+ lines.push(` local subcmd="\${words[1]}"`);
8814
+ lines.push(``);
8815
+ lines.push(` case "$subcmd" in`);
8816
+ for (const cmd of ctx.commands) {
8817
+ if (cmd.children.length > 0) {
8818
+ const childNames = cmd.children.map((c) => c.name);
8819
+ lines.push(` ${cmd.name})`);
8820
+ lines.push(` if ((cword == 2)); then`);
8821
+ lines.push(` COMPREPLY=($(compgen -W "${childNames.join(" ")}" -- "$cur"))`);
8822
+ lines.push(` return`);
8823
+ lines.push(` fi`);
8824
+ lines.push(` local childcmd="\${words[2]}"`);
8825
+ for (const child of cmd.children) {
8826
+ for (const flag of child.flags) {
8827
+ if (flag.type === "enum" && flag.values && flag.values.length > 0) {
8828
+ lines.push(` if [[ "$childcmd" == "${child.name}" && "$prev" == "${flag.name}" ]]; then`);
8829
+ lines.push(` COMPREPLY=($(compgen -W "${flag.values.join(" ")}" -- "$cur"))`);
8830
+ lines.push(` return`);
8831
+ lines.push(` fi`);
8832
+ }
8833
+ if (flag.type === "path") {
8834
+ lines.push(` if [[ "$childcmd" == "${child.name}" && "$prev" == "${flag.name}" ]]; then`);
8835
+ lines.push(` compopt -o default`);
8836
+ lines.push(` COMPREPLY=()`);
8837
+ lines.push(` return`);
8838
+ lines.push(` fi`);
8839
+ }
8840
+ }
8841
+ }
8842
+ lines.push(` case "$childcmd" in`);
8843
+ for (const child of cmd.children) {
8844
+ const flags = child.flags.map((f) => f.name).join(" ");
8845
+ lines.push(` ${child.name}) COMPREPLY=($(compgen -W "${flags}" -- "$cur"));;`);
8846
+ }
8847
+ lines.push(` *) ;;`);
8848
+ lines.push(` esac`);
8849
+ lines.push(` ;;`);
8850
+ } else {
8851
+ lines.push(` ${cmd.name})`);
8852
+ for (const flag of cmd.flags) {
8853
+ if (flag.type === "enum" && flag.values && flag.values.length > 0) {
8854
+ lines.push(` if [[ "$prev" == "${flag.name}" ]]; then`);
8855
+ lines.push(` COMPREPLY=($(compgen -W "${flag.values.join(" ")}" -- "$cur"))`);
8856
+ lines.push(` return`);
8857
+ lines.push(` fi`);
8858
+ }
8859
+ if (flag.type === "path") {
8860
+ lines.push(` if [[ "$prev" == "${flag.name}" ]]; then`);
8861
+ lines.push(` compopt -o default`);
8862
+ lines.push(` COMPREPLY=()`);
8863
+ lines.push(` return`);
8864
+ lines.push(` fi`);
8865
+ }
8866
+ }
8867
+ const flags = cmd.flags.map((f) => f.name).join(" ");
8868
+ lines.push(` COMPREPLY=($(compgen -W "${flags}" -- "$cur"))`);
8869
+ lines.push(` ;;`);
8870
+ }
8871
+ }
8872
+ lines.push(` *) ;;`);
8873
+ lines.push(` esac`);
8874
+ lines.push(`}`);
8875
+ lines.push(``);
8876
+ lines.push(`complete -F ${funcName} ${toolName}`);
8877
+ return lines.join(`
8878
+ `) + `
8879
+ `;
8880
+ }
8881
+ function zshEsc(s) {
8882
+ return s.replace(/'/g, "'\\''");
8883
+ }
8884
+ function zshFlagLine(flag) {
8885
+ const desc = zshEsc(flag.description);
8886
+ if (flag.type === "enum" && flag.values && flag.values.length > 0) {
8887
+ return `'${flag.name}[${desc}]:value:(${flag.values.join(" ")})'`;
8888
+ } else if (flag.type === "path") {
8889
+ return `'${flag.name}[${desc}]:file:_files'`;
8890
+ } else if (flag.type === "boolean") {
8891
+ return `'${flag.name}[${desc}]'`;
8892
+ }
8893
+ return `'${flag.name}[${desc}]:value:'`;
8894
+ }
8895
+ function generateZsh(toolName, ctx) {
8896
+ const funcName = `_${toolName.replace(/[^a-zA-Z0-9]/g, "_")}`;
8897
+ const lines = [];
8898
+ lines.push(`#compdef ${toolName}`);
8899
+ lines.push(``);
8900
+ for (const cmd of ctx.commands) {
8901
+ if (cmd.children.length === 0)
8902
+ continue;
8903
+ const subFunc = `${funcName}_${cmd.name.replace(/[^a-zA-Z0-9]/g, "_")}`;
8904
+ lines.push(`${subFunc}() {`);
8905
+ lines.push(` local -a subcmds`);
8906
+ lines.push(` subcmds=(`);
8907
+ for (const child of cmd.children) {
8908
+ lines.push(` '${child.name}:${zshEsc(child.description)}'`);
8909
+ }
8910
+ lines.push(` )`);
8911
+ lines.push(``);
8912
+ lines.push(` _arguments -C \\`);
8913
+ lines.push(` '1:command:->subcmd' \\`);
8914
+ lines.push(` '*::arg:->args'`);
8915
+ lines.push(``);
8916
+ lines.push(` case "$state" in`);
8917
+ lines.push(` subcmd)`);
8918
+ lines.push(` _describe '${cmd.name} command' subcmds`);
8919
+ lines.push(` ;;`);
8920
+ lines.push(` args)`);
8921
+ lines.push(` case "\${words[1]}" in`);
8922
+ for (const child of cmd.children) {
8923
+ lines.push(` ${child.name})`);
8924
+ lines.push(` _arguments \\`);
8925
+ for (const flag of child.flags) {
8926
+ lines.push(` ${zshFlagLine(flag)} \\`);
8927
+ }
8928
+ lines.push(` ;;`);
8929
+ }
8930
+ lines.push(` esac`);
8931
+ lines.push(` ;;`);
8932
+ lines.push(` esac`);
8933
+ lines.push(`}`);
8934
+ lines.push(``);
8935
+ }
8936
+ lines.push(`${funcName}() {`);
8937
+ lines.push(` local -a subcmds`);
8938
+ if (ctx.commands.length > 0) {
8939
+ lines.push(` subcmds=(`);
8940
+ for (const cmd of ctx.commands) {
8941
+ lines.push(` '${cmd.name}:${zshEsc(cmd.description)}'`);
8942
+ }
8943
+ lines.push(` )`);
8944
+ }
8945
+ lines.push(``);
8946
+ lines.push(` _arguments -C \\`);
8947
+ for (const flag of ctx.rootFlags) {
8948
+ lines.push(` ${zshFlagLine(flag)} \\`);
8949
+ }
8950
+ lines.push(` '1:command:->subcmd' \\`);
8951
+ lines.push(` '*::arg:->args'`);
8952
+ lines.push(``);
8953
+ lines.push(` case "$state" in`);
8954
+ lines.push(` subcmd)`);
8955
+ lines.push(` _describe 'command' subcmds`);
8956
+ lines.push(` ;;`);
8957
+ lines.push(` args)`);
8958
+ lines.push(` case "\${words[1]}" in`);
8959
+ for (const cmd of ctx.commands) {
8960
+ if (cmd.children.length > 0) {
8961
+ const subFunc = `${funcName}_${cmd.name.replace(/[^a-zA-Z0-9]/g, "_")}`;
8962
+ lines.push(` ${cmd.name}) ${subFunc};;`);
8963
+ } else {
8964
+ lines.push(` ${cmd.name})`);
8965
+ lines.push(` _arguments \\`);
8966
+ for (const flag of cmd.flags) {
8967
+ lines.push(` ${zshFlagLine(flag)} \\`);
8968
+ }
8969
+ lines.push(` ;;`);
8970
+ }
8971
+ }
8972
+ lines.push(` esac`);
8973
+ lines.push(` ;;`);
8974
+ lines.push(` esac`);
8975
+ lines.push(`}`);
8976
+ lines.push(``);
8977
+ lines.push(`${funcName} "$@"`);
8978
+ return lines.join(`
8979
+ `) + `
8980
+ `;
8981
+ }
8982
+ function fishEsc(s) {
8983
+ return s.replace(/'/g, "\\'");
8984
+ }
8985
+ function fishFlagLines(toolName, condition, flags) {
8986
+ const lines = [];
8987
+ for (const flag of flags) {
8988
+ const flagName = flag.name.replace(/^--/, "");
8989
+ const desc = fishEsc(flag.description);
8990
+ if (flag.type === "enum" && flag.values && flag.values.length > 0) {
8991
+ lines.push(`complete -c ${toolName} -n '${condition}' -l ${flagName} -d '${desc}' -f -a '${flag.values.join(" ")}'`);
8992
+ } else if (flag.type === "path") {
8993
+ lines.push(`complete -c ${toolName} -n '${condition}' -l ${flagName} -d '${desc}' -F`);
8994
+ } else if (flag.type === "boolean") {
8995
+ lines.push(`complete -c ${toolName} -n '${condition}' -l ${flagName} -d '${desc}'`);
8996
+ } else {
8997
+ lines.push(`complete -c ${toolName} -n '${condition}' -l ${flagName} -d '${desc}' -x`);
8998
+ }
8999
+ }
9000
+ return lines;
9001
+ }
9002
+ function generateFish(toolName, ctx) {
9003
+ const lines = [];
9004
+ const groupCmds = ctx.commands.filter((c) => c.children.length > 0);
9005
+ if (groupCmds.length > 0) {
9006
+ for (const group of groupCmds) {
9007
+ const childNames = group.children.map((c) => c.name).join(" ");
9008
+ const funcName = `__${toolName.replace(/[^a-zA-Z0-9]/g, "_")}_needs_${group.name}_subcmd`;
9009
+ lines.push(`function ${funcName}`);
9010
+ lines.push(` set -l cmd (commandline -opc)`);
9011
+ lines.push(` if not contains -- ${group.name} $cmd`);
9012
+ lines.push(` return 1`);
9013
+ lines.push(` end`);
9014
+ lines.push(` for subcmd in ${childNames}`);
9015
+ lines.push(` if contains -- $subcmd $cmd`);
9016
+ lines.push(` return 1`);
9017
+ lines.push(` end`);
9018
+ lines.push(` end`);
9019
+ lines.push(` return 0`);
9020
+ lines.push(`end`);
9021
+ lines.push(``);
9022
+ }
9023
+ }
9024
+ for (const flag of ctx.rootFlags) {
9025
+ const flagName = flag.name.replace(/^--/, "");
9026
+ const desc = fishEsc(flag.description);
9027
+ lines.push(`complete -c ${toolName} -n '__fish_use_subcommand' -l ${flagName} -d '${desc}'`);
9028
+ }
9029
+ for (const cmd of ctx.commands) {
9030
+ const desc = fishEsc(cmd.description);
9031
+ lines.push(`complete -c ${toolName} -n '__fish_use_subcommand' -a ${cmd.name} -d '${desc}'`);
9032
+ }
9033
+ for (const cmd of ctx.commands) {
9034
+ if (cmd.children.length > 0) {
9035
+ const funcName = `__${toolName.replace(/[^a-zA-Z0-9]/g, "_")}_needs_${cmd.name}_subcmd`;
9036
+ for (const child of cmd.children) {
9037
+ const desc = fishEsc(child.description);
9038
+ lines.push(`complete -c ${toolName} -n '${funcName}' -a ${child.name} -d '${desc}'`);
9039
+ }
9040
+ for (const child of cmd.children) {
9041
+ const condition = `__fish_seen_subcommand_from ${child.name}`;
9042
+ lines.push(...fishFlagLines(toolName, condition, child.flags));
9043
+ }
9044
+ } else {
9045
+ const condition = `__fish_seen_subcommand_from ${cmd.name}`;
9046
+ lines.push(...fishFlagLines(toolName, condition, cmd.flags));
9047
+ }
9048
+ }
9049
+ return lines.join(`
9050
+ `) + `
9051
+ `;
9052
+ }
9053
+ function generate(shell, toolName, schema) {
9054
+ const ctx = schemaToCompletionContext(schema);
9055
+ switch (shell) {
9056
+ case "bash":
9057
+ return generateBash(toolName, ctx);
9058
+ case "zsh":
9059
+ return generateZsh(toolName, ctx);
9060
+ case "fish":
9061
+ return generateFish(toolName, ctx);
9062
+ default:
9063
+ throw new Error(`Unknown shell: ${shell}`);
9064
+ }
9065
+ }
9066
+ async function run4(shell, toolName) {
9067
+ if (!["bash", "zsh", "fish"].includes(shell)) {
9068
+ process.stderr.write(`error: unsupported shell "${shell}". Use bash, zsh, or fish.
9069
+ `);
9070
+ process.exit(1);
9071
+ }
9072
+ const schema = await getToolSchema2(toolName);
9073
+ if (!schema) {
9074
+ process.stderr.write(`error: could not get --describe from "${toolName}"
9075
+ `);
9076
+ process.exit(1);
9077
+ }
9078
+ process.stdout.write(generate(shell, toolName, schema));
9079
+ }
9080
+ var init_completions = __esm(() => {
9081
+ init_search2();
9082
+ });
9083
+
8455
9084
  // node_modules/commander/esm.mjs
8456
9085
  var import__ = __toESM(require_commander(), 1);
8457
9086
  var {
@@ -8579,7 +9208,7 @@ function cleanJson(obj) {
8579
9208
  }
8580
9209
 
8581
9210
  // src/index.ts
8582
- var VERSION3 = "0.1.0";
9211
+ var VERSION3 = "0.2.1";
8583
9212
  function selfDescribe() {
8584
9213
  const schema = {
8585
9214
  name: "mtpcli",
@@ -8777,6 +9406,82 @@ function selfDescribe() {
8777
9406
  command: 'mtpcli wrap --server "npx @mcp/server-github" search_repos -- --query mtpcli'
8778
9407
  }
8779
9408
  ]
9409
+ },
9410
+ {
9411
+ name: "validate",
9412
+ description: "Validate a tool's --describe output against the MTP spec",
9413
+ args: [
9414
+ {
9415
+ name: "tool",
9416
+ type: "string",
9417
+ description: "Tool name to validate"
9418
+ },
9419
+ {
9420
+ name: "--json",
9421
+ type: "boolean",
9422
+ default: false,
9423
+ description: "Output as JSON"
9424
+ },
9425
+ {
9426
+ name: "--stdin",
9427
+ type: "boolean",
9428
+ default: false,
9429
+ description: "Read JSON from stdin instead of running tool"
9430
+ },
9431
+ {
9432
+ name: "--skip-help",
9433
+ type: "boolean",
9434
+ default: false,
9435
+ description: "Skip --help cross-reference check"
9436
+ }
9437
+ ],
9438
+ examples: [
9439
+ {
9440
+ description: "Validate a tool",
9441
+ command: "mtpcli validate mytool"
9442
+ },
9443
+ {
9444
+ description: "Validate from stdin",
9445
+ command: "cat describe.json | mtpcli validate --stdin"
9446
+ },
9447
+ {
9448
+ description: "JSON output for CI",
9449
+ command: "mtpcli validate mytool --json"
9450
+ }
9451
+ ]
9452
+ },
9453
+ {
9454
+ name: "completions",
9455
+ description: "Generate shell completions for a --describe-compatible tool",
9456
+ args: [
9457
+ {
9458
+ name: "shell",
9459
+ type: "enum",
9460
+ required: true,
9461
+ values: ["bash", "zsh", "fish"],
9462
+ description: "Target shell"
9463
+ },
9464
+ {
9465
+ name: "tool",
9466
+ type: "string",
9467
+ required: true,
9468
+ description: "Tool name"
9469
+ }
9470
+ ],
9471
+ examples: [
9472
+ {
9473
+ description: "Install bash completions",
9474
+ command: "eval $(mtpcli completions bash mytool)"
9475
+ },
9476
+ {
9477
+ description: "Install zsh completions",
9478
+ command: "eval $(mtpcli completions zsh mytool)"
9479
+ },
9480
+ {
9481
+ description: "Install fish completions",
9482
+ command: "mtpcli completions fish mytool | source"
9483
+ }
9484
+ ]
8780
9485
  }
8781
9486
  ]
8782
9487
  };
@@ -8837,12 +9542,24 @@ authCmd.command("refresh").description("Force-refresh the stored OAuth token for
8837
9542
  await runRefresh2(tool);
8838
9543
  });
8839
9544
  program2.command("serve").description("Serve CLI tools as an MCP server (cli2mcp bridge)").requiredOption("--tool <names...>", "Tool(s) to serve").action(async (opts) => {
8840
- const { run: run3 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
8841
- await run3(opts.tool);
9545
+ const { run: run5 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
9546
+ await run5(opts.tool);
8842
9547
  });
8843
9548
  program2.command("wrap").description("Wrap an MCP server as a CLI tool (mcp2cli bridge)").requiredOption("--server <cmd>", "MCP server command to run").option("--describe", "Output --describe JSON instead of invoking", false).argument("[tool_name]", "Tool name to invoke").argument("[args...]", "Arguments for the tool (after --)").action(async (toolName, args, opts) => {
8844
- const { run: run3 } = await Promise.resolve().then(() => (init_wrap(), exports_wrap));
8845
- await run3(opts.server, opts.describe, toolName, args);
9549
+ const { run: run5 } = await Promise.resolve().then(() => (init_wrap(), exports_wrap));
9550
+ await run5(opts.server, opts.describe, toolName, args);
9551
+ });
9552
+ program2.command("validate").description("Validate a tool's --describe output against the MTP spec").argument("[tool]", "Tool name to validate").option("--json", "Output as JSON", false).option("--stdin", "Read JSON from stdin", false).option("--skip-help", "Skip --help cross-reference", false).action(async (tool, opts) => {
9553
+ const { run: run5 } = await Promise.resolve().then(() => (init_validate(), exports_validate));
9554
+ await run5(tool, {
9555
+ json: opts.json,
9556
+ stdin: opts.stdin,
9557
+ skipHelp: opts.skipHelp
9558
+ });
9559
+ });
9560
+ program2.command("completions").description("Generate shell completions from --describe output").argument("<shell>", "Shell type (bash, zsh, fish)").argument("<tool>", "Tool name").action(async (shell, tool) => {
9561
+ const { run: run5 } = await Promise.resolve().then(() => (init_completions(), exports_completions));
9562
+ await run5(shell, tool);
8846
9563
  });
8847
9564
  try {
8848
9565
  await program2.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modeltoolsprotocol/mtpcli",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "bin": {