@modeltoolsprotocol/mtpcli 0.1.0 → 0.2.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 (3) hide show
  1. package/README.md +37 -8
  2. package/dist/index.js +701 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -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
@@ -8452,6 +8452,614 @@ var init_wrap = __esm(() => {
8452
8452
  init_mcp();
8453
8453
  });
8454
8454
 
8455
+ // src/validate.ts
8456
+ var exports_validate = {};
8457
+ __export(exports_validate, {
8458
+ validateTool: () => validateTool,
8459
+ validateJson: () => validateJson,
8460
+ run: () => run3,
8461
+ crossReferenceHelp: () => crossReferenceHelp
8462
+ });
8463
+ import { execFile as execFile10 } from "node:child_process";
8464
+ import { promisify as promisify10 } from "node:util";
8465
+ function validateJson(raw) {
8466
+ const diags = [];
8467
+ let parsed;
8468
+ try {
8469
+ parsed = JSON.parse(raw);
8470
+ } catch (e) {
8471
+ diags.push({
8472
+ level: "error",
8473
+ code: "INVALID_JSON",
8474
+ message: `Not valid JSON: ${e instanceof Error ? e.message : e}`
8475
+ });
8476
+ return diags;
8477
+ }
8478
+ const result = ToolSchemaSchema2.safeParse(parsed);
8479
+ if (!result.success) {
8480
+ for (const issue of result.error.issues) {
8481
+ diags.push({
8482
+ level: "error",
8483
+ code: "SCHEMA_VIOLATION",
8484
+ message: issue.message,
8485
+ path: issue.path.join(".")
8486
+ });
8487
+ }
8488
+ return diags;
8489
+ }
8490
+ const schema = result.data;
8491
+ for (let ci = 0;ci < schema.commands.length; ci++) {
8492
+ const cmd = schema.commands[ci];
8493
+ const cmdPath = `commands.${ci}`;
8494
+ if (cmd.examples.length === 0) {
8495
+ diags.push({
8496
+ level: "warning",
8497
+ code: "MISSING_EXAMPLES",
8498
+ message: `Command "${cmd.name}" has no examples`,
8499
+ path: `${cmdPath}.examples`
8500
+ });
8501
+ }
8502
+ for (let ai = 0;ai < cmd.args.length; ai++) {
8503
+ const arg = cmd.args[ai];
8504
+ const argPath = `${cmdPath}.args.${ai}`;
8505
+ if (!VALID_ARG_TYPES.has(arg.type)) {
8506
+ diags.push({
8507
+ level: "error",
8508
+ code: "INVALID_ARG_TYPE",
8509
+ message: `Arg "${arg.name}" has invalid type "${arg.type}"`,
8510
+ path: `${argPath}.type`
8511
+ });
8512
+ }
8513
+ if (arg.type === "enum" && (!arg.values || arg.values.length === 0)) {
8514
+ diags.push({
8515
+ level: "warning",
8516
+ code: "ENUM_NO_VALUES",
8517
+ message: `Enum arg "${arg.name}" has no values`,
8518
+ path: `${argPath}.values`
8519
+ });
8520
+ }
8521
+ if (!arg.description) {
8522
+ diags.push({
8523
+ level: "warning",
8524
+ code: "MISSING_DESCRIPTION",
8525
+ message: `Arg "${arg.name}" has no description`,
8526
+ path: `${argPath}.description`
8527
+ });
8528
+ }
8529
+ }
8530
+ }
8531
+ return diags;
8532
+ }
8533
+ function crossReferenceHelp(schema, helpText) {
8534
+ const diags = [];
8535
+ const helpFlags = new Set;
8536
+ const flagRe = /--([\w][\w-]*)/g;
8537
+ let m;
8538
+ while ((m = flagRe.exec(helpText)) !== null) {
8539
+ helpFlags.add(`--${m[1]}`);
8540
+ }
8541
+ helpFlags.delete("--help");
8542
+ helpFlags.delete("--version");
8543
+ helpFlags.delete("--describe");
8544
+ const declaredFlags = new Set;
8545
+ for (const cmd of schema.commands) {
8546
+ for (const arg of cmd.args) {
8547
+ if (arg.name.startsWith("--")) {
8548
+ declaredFlags.add(arg.name);
8549
+ }
8550
+ }
8551
+ }
8552
+ for (const flag of declaredFlags) {
8553
+ if (!helpFlags.has(flag)) {
8554
+ diags.push({
8555
+ level: "info",
8556
+ code: "HELP_ARG_MISMATCH",
8557
+ message: `Flag "${flag}" in --describe but not found in --help`
8558
+ });
8559
+ }
8560
+ }
8561
+ for (const flag of helpFlags) {
8562
+ if (!declaredFlags.has(flag)) {
8563
+ diags.push({
8564
+ level: "info",
8565
+ code: "HELP_ARG_MISMATCH",
8566
+ message: `Flag "${flag}" in --help but not found in --describe`
8567
+ });
8568
+ }
8569
+ }
8570
+ return diags;
8571
+ }
8572
+ async function validateTool(toolName, opts) {
8573
+ const diags = [];
8574
+ let toolPath;
8575
+ try {
8576
+ toolPath = await import_which3.default(toolName);
8577
+ } catch {
8578
+ diags.push({
8579
+ level: "error",
8580
+ code: "NOT_FOUND",
8581
+ message: `Tool "${toolName}" not found in PATH`
8582
+ });
8583
+ return buildResult(toolName, diags);
8584
+ }
8585
+ let describeOutput;
8586
+ try {
8587
+ const { stdout } = await execFileAsync8(toolPath, ["--describe"], {
8588
+ timeout: 1e4
8589
+ });
8590
+ describeOutput = stdout.trim();
8591
+ if (!describeOutput) {
8592
+ diags.push({
8593
+ level: "error",
8594
+ code: "DESCRIBE_FAILED",
8595
+ message: `"${toolName} --describe" produced empty output`
8596
+ });
8597
+ return buildResult(toolName, diags);
8598
+ }
8599
+ } catch (e) {
8600
+ diags.push({
8601
+ level: "error",
8602
+ code: "DESCRIBE_FAILED",
8603
+ message: `"${toolName} --describe" failed: ${e instanceof Error ? e.message : e}`
8604
+ });
8605
+ return buildResult(toolName, diags);
8606
+ }
8607
+ diags.push(...validateJson(describeOutput));
8608
+ const hasErrors = diags.some((d) => d.level === "error");
8609
+ if (!hasErrors && !opts?.skipHelp) {
8610
+ try {
8611
+ const { stdout: helpText } = await execFileAsync8(toolPath, ["--help"], {
8612
+ timeout: 5000
8613
+ });
8614
+ const schema = ToolSchemaSchema2.parse(JSON.parse(describeOutput));
8615
+ diags.push(...crossReferenceHelp(schema, helpText));
8616
+ } catch {}
8617
+ }
8618
+ return buildResult(toolName, diags);
8619
+ }
8620
+ function buildResult(tool, diagnostics) {
8621
+ const errors2 = diagnostics.filter((d) => d.level === "error").length;
8622
+ const warnings = diagnostics.filter((d) => d.level === "warning").length;
8623
+ const info = diagnostics.filter((d) => d.level === "info").length;
8624
+ return {
8625
+ tool,
8626
+ valid: errors2 === 0,
8627
+ diagnostics,
8628
+ summary: { errors: errors2, warnings, info }
8629
+ };
8630
+ }
8631
+ async function run3(toolOrStdin, opts) {
8632
+ let result;
8633
+ if (opts.stdin) {
8634
+ const chunks = [];
8635
+ for await (const chunk of process.stdin) {
8636
+ chunks.push(chunk);
8637
+ }
8638
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
8639
+ const diags = validateJson(raw);
8640
+ result = buildResult(opts.stdin ? "stdin" : "unknown", diags);
8641
+ } else if (toolOrStdin) {
8642
+ result = await validateTool(toolOrStdin, { skipHelp: opts.skipHelp });
8643
+ } else {
8644
+ process.stderr.write(`error: provide a tool name or use --stdin
8645
+ `);
8646
+ process.exit(1);
8647
+ }
8648
+ if (opts.json) {
8649
+ console.log(JSON.stringify(result, null, 2));
8650
+ } else {
8651
+ printHuman(result);
8652
+ }
8653
+ if (!result.valid)
8654
+ process.exit(1);
8655
+ }
8656
+ function printHuman(result) {
8657
+ const icon = result.valid ? "PASS" : "FAIL";
8658
+ console.log(`${icon} ${result.tool}`);
8659
+ for (const d of result.diagnostics) {
8660
+ const tag = d.level === "error" ? "ERR" : d.level === "warning" ? "WRN" : "INF";
8661
+ const path2 = d.path ? ` (${d.path})` : "";
8662
+ console.log(` ${tag} [${d.code}] ${d.message}${path2}`);
8663
+ }
8664
+ const parts = [];
8665
+ if (result.summary.errors)
8666
+ parts.push(`${result.summary.errors} error(s)`);
8667
+ if (result.summary.warnings)
8668
+ parts.push(`${result.summary.warnings} warning(s)`);
8669
+ if (result.summary.info)
8670
+ parts.push(`${result.summary.info} info`);
8671
+ if (parts.length)
8672
+ console.log(` ${parts.join(", ")}`);
8673
+ }
8674
+ var import_which3, execFileAsync8, VALID_ARG_TYPES;
8675
+ var init_validate = __esm(() => {
8676
+ init_models();
8677
+ import_which3 = __toESM(require_lib(), 1);
8678
+ execFileAsync8 = promisify10(execFile10);
8679
+ VALID_ARG_TYPES = new Set([
8680
+ "string",
8681
+ "integer",
8682
+ "number",
8683
+ "boolean",
8684
+ "array",
8685
+ "enum",
8686
+ "path"
8687
+ ]);
8688
+ });
8689
+
8690
+ // src/completions.ts
8691
+ var exports_completions = {};
8692
+ __export(exports_completions, {
8693
+ schemaToCompletionContext: () => schemaToCompletionContext,
8694
+ run: () => run4,
8695
+ generateZsh: () => generateZsh,
8696
+ generateFish: () => generateFish,
8697
+ generateBash: () => generateBash,
8698
+ generate: () => generate
8699
+ });
8700
+ function extractFlags(args) {
8701
+ const flags = [];
8702
+ for (const arg of args) {
8703
+ if (arg.name.startsWith("--")) {
8704
+ flags.push({
8705
+ name: arg.name,
8706
+ description: arg.description ?? "",
8707
+ type: arg.type,
8708
+ values: arg.values
8709
+ });
8710
+ }
8711
+ }
8712
+ return flags;
8713
+ }
8714
+ function schemaToCompletionContext(schema) {
8715
+ let rootFlags = [];
8716
+ const groups = new Map;
8717
+ for (const cmd of schema.commands) {
8718
+ if (cmd.name === "_root") {
8719
+ rootFlags = extractFlags(cmd.args);
8720
+ continue;
8721
+ }
8722
+ const parts = cmd.name.split(/\s+/);
8723
+ const key = parts[0];
8724
+ const entry = { parts, description: cmd.description, flags: extractFlags(cmd.args) };
8725
+ let group = groups.get(key);
8726
+ if (!group) {
8727
+ group = [];
8728
+ groups.set(key, group);
8729
+ }
8730
+ group.push(entry);
8731
+ }
8732
+ const commands = [];
8733
+ for (const [key, entries] of groups) {
8734
+ if (entries.length === 1 && entries[0].parts.length === 1) {
8735
+ commands.push({
8736
+ name: key,
8737
+ description: entries[0].description,
8738
+ flags: entries[0].flags,
8739
+ children: []
8740
+ });
8741
+ } else {
8742
+ const children = [];
8743
+ let groupDesc = "";
8744
+ for (const entry of entries) {
8745
+ const childName = entry.parts.slice(1).join(" ");
8746
+ if (!childName) {
8747
+ groupDesc = entry.description;
8748
+ continue;
8749
+ }
8750
+ children.push({
8751
+ name: childName,
8752
+ description: entry.description,
8753
+ flags: entry.flags,
8754
+ children: []
8755
+ });
8756
+ }
8757
+ if (!groupDesc && children.length > 0) {
8758
+ groupDesc = `${key} commands`;
8759
+ }
8760
+ commands.push({
8761
+ name: key,
8762
+ description: groupDesc,
8763
+ flags: [],
8764
+ children
8765
+ });
8766
+ }
8767
+ }
8768
+ return { toolName: schema.name, rootFlags, commands };
8769
+ }
8770
+ function generateBash(toolName, ctx) {
8771
+ const funcName = `_${toolName.replace(/[^a-zA-Z0-9]/g, "_")}_completions`;
8772
+ const topNames = ctx.commands.map((c) => c.name);
8773
+ const lines = [];
8774
+ lines.push(`${funcName}() {`);
8775
+ lines.push(` local cur prev words cword`);
8776
+ lines.push(` _get_comp_words_by_ref -n : cur prev words cword 2>/dev/null || {`);
8777
+ lines.push(` cur="\${COMP_WORDS[COMP_CWORD]}"`);
8778
+ lines.push(` prev="\${COMP_WORDS[COMP_CWORD-1]}"`);
8779
+ lines.push(` words=("\${COMP_WORDS[@]}")`);
8780
+ lines.push(` cword=$COMP_CWORD`);
8781
+ lines.push(` }`);
8782
+ lines.push(``);
8783
+ lines.push(` if ((cword == 1)); then`);
8784
+ const toplevel = [...topNames];
8785
+ if (ctx.rootFlags.length > 0) {
8786
+ toplevel.push(...ctx.rootFlags.map((f) => f.name));
8787
+ }
8788
+ lines.push(` COMPREPLY=($(compgen -W "${toplevel.join(" ")}" -- "$cur"))`);
8789
+ lines.push(` return`);
8790
+ lines.push(` fi`);
8791
+ lines.push(``);
8792
+ lines.push(` local subcmd="\${words[1]}"`);
8793
+ lines.push(``);
8794
+ lines.push(` case "$subcmd" in`);
8795
+ for (const cmd of ctx.commands) {
8796
+ if (cmd.children.length > 0) {
8797
+ const childNames = cmd.children.map((c) => c.name);
8798
+ lines.push(` ${cmd.name})`);
8799
+ lines.push(` if ((cword == 2)); then`);
8800
+ lines.push(` COMPREPLY=($(compgen -W "${childNames.join(" ")}" -- "$cur"))`);
8801
+ lines.push(` return`);
8802
+ lines.push(` fi`);
8803
+ lines.push(` local childcmd="\${words[2]}"`);
8804
+ for (const child of cmd.children) {
8805
+ for (const flag of child.flags) {
8806
+ if (flag.type === "enum" && flag.values && flag.values.length > 0) {
8807
+ lines.push(` if [[ "$childcmd" == "${child.name}" && "$prev" == "${flag.name}" ]]; then`);
8808
+ lines.push(` COMPREPLY=($(compgen -W "${flag.values.join(" ")}" -- "$cur"))`);
8809
+ lines.push(` return`);
8810
+ lines.push(` fi`);
8811
+ }
8812
+ if (flag.type === "path") {
8813
+ lines.push(` if [[ "$childcmd" == "${child.name}" && "$prev" == "${flag.name}" ]]; then`);
8814
+ lines.push(` compopt -o default`);
8815
+ lines.push(` COMPREPLY=()`);
8816
+ lines.push(` return`);
8817
+ lines.push(` fi`);
8818
+ }
8819
+ }
8820
+ }
8821
+ lines.push(` case "$childcmd" in`);
8822
+ for (const child of cmd.children) {
8823
+ const flags = child.flags.map((f) => f.name).join(" ");
8824
+ lines.push(` ${child.name}) COMPREPLY=($(compgen -W "${flags}" -- "$cur"));;`);
8825
+ }
8826
+ lines.push(` *) ;;`);
8827
+ lines.push(` esac`);
8828
+ lines.push(` ;;`);
8829
+ } else {
8830
+ lines.push(` ${cmd.name})`);
8831
+ for (const flag of cmd.flags) {
8832
+ if (flag.type === "enum" && flag.values && flag.values.length > 0) {
8833
+ lines.push(` if [[ "$prev" == "${flag.name}" ]]; then`);
8834
+ lines.push(` COMPREPLY=($(compgen -W "${flag.values.join(" ")}" -- "$cur"))`);
8835
+ lines.push(` return`);
8836
+ lines.push(` fi`);
8837
+ }
8838
+ if (flag.type === "path") {
8839
+ lines.push(` if [[ "$prev" == "${flag.name}" ]]; then`);
8840
+ lines.push(` compopt -o default`);
8841
+ lines.push(` COMPREPLY=()`);
8842
+ lines.push(` return`);
8843
+ lines.push(` fi`);
8844
+ }
8845
+ }
8846
+ const flags = cmd.flags.map((f) => f.name).join(" ");
8847
+ lines.push(` COMPREPLY=($(compgen -W "${flags}" -- "$cur"))`);
8848
+ lines.push(` ;;`);
8849
+ }
8850
+ }
8851
+ lines.push(` *) ;;`);
8852
+ lines.push(` esac`);
8853
+ lines.push(`}`);
8854
+ lines.push(``);
8855
+ lines.push(`complete -F ${funcName} ${toolName}`);
8856
+ return lines.join(`
8857
+ `) + `
8858
+ `;
8859
+ }
8860
+ function zshEsc(s) {
8861
+ return s.replace(/'/g, "'\\''");
8862
+ }
8863
+ function zshFlagLine(flag) {
8864
+ const desc = zshEsc(flag.description);
8865
+ if (flag.type === "enum" && flag.values && flag.values.length > 0) {
8866
+ return `'${flag.name}[${desc}]:value:(${flag.values.join(" ")})'`;
8867
+ } else if (flag.type === "path") {
8868
+ return `'${flag.name}[${desc}]:file:_files'`;
8869
+ } else if (flag.type === "boolean") {
8870
+ return `'${flag.name}[${desc}]'`;
8871
+ }
8872
+ return `'${flag.name}[${desc}]:value:'`;
8873
+ }
8874
+ function generateZsh(toolName, ctx) {
8875
+ const funcName = `_${toolName.replace(/[^a-zA-Z0-9]/g, "_")}`;
8876
+ const lines = [];
8877
+ lines.push(`#compdef ${toolName}`);
8878
+ lines.push(``);
8879
+ for (const cmd of ctx.commands) {
8880
+ if (cmd.children.length === 0)
8881
+ continue;
8882
+ const subFunc = `${funcName}_${cmd.name.replace(/[^a-zA-Z0-9]/g, "_")}`;
8883
+ lines.push(`${subFunc}() {`);
8884
+ lines.push(` local -a subcmds`);
8885
+ lines.push(` subcmds=(`);
8886
+ for (const child of cmd.children) {
8887
+ lines.push(` '${child.name}:${zshEsc(child.description)}'`);
8888
+ }
8889
+ lines.push(` )`);
8890
+ lines.push(``);
8891
+ lines.push(` _arguments -C \\`);
8892
+ lines.push(` '1:command:->subcmd' \\`);
8893
+ lines.push(` '*::arg:->args'`);
8894
+ lines.push(``);
8895
+ lines.push(` case "$state" in`);
8896
+ lines.push(` subcmd)`);
8897
+ lines.push(` _describe '${cmd.name} command' subcmds`);
8898
+ lines.push(` ;;`);
8899
+ lines.push(` args)`);
8900
+ lines.push(` case "\${words[1]}" in`);
8901
+ for (const child of cmd.children) {
8902
+ lines.push(` ${child.name})`);
8903
+ lines.push(` _arguments \\`);
8904
+ for (const flag of child.flags) {
8905
+ lines.push(` ${zshFlagLine(flag)} \\`);
8906
+ }
8907
+ lines.push(` ;;`);
8908
+ }
8909
+ lines.push(` esac`);
8910
+ lines.push(` ;;`);
8911
+ lines.push(` esac`);
8912
+ lines.push(`}`);
8913
+ lines.push(``);
8914
+ }
8915
+ lines.push(`${funcName}() {`);
8916
+ lines.push(` local -a subcmds`);
8917
+ if (ctx.commands.length > 0) {
8918
+ lines.push(` subcmds=(`);
8919
+ for (const cmd of ctx.commands) {
8920
+ lines.push(` '${cmd.name}:${zshEsc(cmd.description)}'`);
8921
+ }
8922
+ lines.push(` )`);
8923
+ }
8924
+ lines.push(``);
8925
+ lines.push(` _arguments -C \\`);
8926
+ for (const flag of ctx.rootFlags) {
8927
+ lines.push(` ${zshFlagLine(flag)} \\`);
8928
+ }
8929
+ lines.push(` '1:command:->subcmd' \\`);
8930
+ lines.push(` '*::arg:->args'`);
8931
+ lines.push(``);
8932
+ lines.push(` case "$state" in`);
8933
+ lines.push(` subcmd)`);
8934
+ lines.push(` _describe 'command' subcmds`);
8935
+ lines.push(` ;;`);
8936
+ lines.push(` args)`);
8937
+ lines.push(` case "\${words[1]}" in`);
8938
+ for (const cmd of ctx.commands) {
8939
+ if (cmd.children.length > 0) {
8940
+ const subFunc = `${funcName}_${cmd.name.replace(/[^a-zA-Z0-9]/g, "_")}`;
8941
+ lines.push(` ${cmd.name}) ${subFunc};;`);
8942
+ } else {
8943
+ lines.push(` ${cmd.name})`);
8944
+ lines.push(` _arguments \\`);
8945
+ for (const flag of cmd.flags) {
8946
+ lines.push(` ${zshFlagLine(flag)} \\`);
8947
+ }
8948
+ lines.push(` ;;`);
8949
+ }
8950
+ }
8951
+ lines.push(` esac`);
8952
+ lines.push(` ;;`);
8953
+ lines.push(` esac`);
8954
+ lines.push(`}`);
8955
+ lines.push(``);
8956
+ lines.push(`${funcName} "$@"`);
8957
+ return lines.join(`
8958
+ `) + `
8959
+ `;
8960
+ }
8961
+ function fishEsc(s) {
8962
+ return s.replace(/'/g, "\\'");
8963
+ }
8964
+ function fishFlagLines(toolName, condition, flags) {
8965
+ const lines = [];
8966
+ for (const flag of flags) {
8967
+ const flagName = flag.name.replace(/^--/, "");
8968
+ const desc = fishEsc(flag.description);
8969
+ if (flag.type === "enum" && flag.values && flag.values.length > 0) {
8970
+ lines.push(`complete -c ${toolName} -n '${condition}' -l ${flagName} -d '${desc}' -f -a '${flag.values.join(" ")}'`);
8971
+ } else if (flag.type === "path") {
8972
+ lines.push(`complete -c ${toolName} -n '${condition}' -l ${flagName} -d '${desc}' -F`);
8973
+ } else if (flag.type === "boolean") {
8974
+ lines.push(`complete -c ${toolName} -n '${condition}' -l ${flagName} -d '${desc}'`);
8975
+ } else {
8976
+ lines.push(`complete -c ${toolName} -n '${condition}' -l ${flagName} -d '${desc}' -x`);
8977
+ }
8978
+ }
8979
+ return lines;
8980
+ }
8981
+ function generateFish(toolName, ctx) {
8982
+ const lines = [];
8983
+ const groupCmds = ctx.commands.filter((c) => c.children.length > 0);
8984
+ if (groupCmds.length > 0) {
8985
+ for (const group of groupCmds) {
8986
+ const childNames = group.children.map((c) => c.name).join(" ");
8987
+ const funcName = `__${toolName.replace(/[^a-zA-Z0-9]/g, "_")}_needs_${group.name}_subcmd`;
8988
+ lines.push(`function ${funcName}`);
8989
+ lines.push(` set -l cmd (commandline -opc)`);
8990
+ lines.push(` if not contains -- ${group.name} $cmd`);
8991
+ lines.push(` return 1`);
8992
+ lines.push(` end`);
8993
+ lines.push(` for subcmd in ${childNames}`);
8994
+ lines.push(` if contains -- $subcmd $cmd`);
8995
+ lines.push(` return 1`);
8996
+ lines.push(` end`);
8997
+ lines.push(` end`);
8998
+ lines.push(` return 0`);
8999
+ lines.push(`end`);
9000
+ lines.push(``);
9001
+ }
9002
+ }
9003
+ for (const flag of ctx.rootFlags) {
9004
+ const flagName = flag.name.replace(/^--/, "");
9005
+ const desc = fishEsc(flag.description);
9006
+ lines.push(`complete -c ${toolName} -n '__fish_use_subcommand' -l ${flagName} -d '${desc}'`);
9007
+ }
9008
+ for (const cmd of ctx.commands) {
9009
+ const desc = fishEsc(cmd.description);
9010
+ lines.push(`complete -c ${toolName} -n '__fish_use_subcommand' -a ${cmd.name} -d '${desc}'`);
9011
+ }
9012
+ for (const cmd of ctx.commands) {
9013
+ if (cmd.children.length > 0) {
9014
+ const funcName = `__${toolName.replace(/[^a-zA-Z0-9]/g, "_")}_needs_${cmd.name}_subcmd`;
9015
+ for (const child of cmd.children) {
9016
+ const desc = fishEsc(child.description);
9017
+ lines.push(`complete -c ${toolName} -n '${funcName}' -a ${child.name} -d '${desc}'`);
9018
+ }
9019
+ for (const child of cmd.children) {
9020
+ const condition = `__fish_seen_subcommand_from ${child.name}`;
9021
+ lines.push(...fishFlagLines(toolName, condition, child.flags));
9022
+ }
9023
+ } else {
9024
+ const condition = `__fish_seen_subcommand_from ${cmd.name}`;
9025
+ lines.push(...fishFlagLines(toolName, condition, cmd.flags));
9026
+ }
9027
+ }
9028
+ return lines.join(`
9029
+ `) + `
9030
+ `;
9031
+ }
9032
+ function generate(shell, toolName, schema) {
9033
+ const ctx = schemaToCompletionContext(schema);
9034
+ switch (shell) {
9035
+ case "bash":
9036
+ return generateBash(toolName, ctx);
9037
+ case "zsh":
9038
+ return generateZsh(toolName, ctx);
9039
+ case "fish":
9040
+ return generateFish(toolName, ctx);
9041
+ default:
9042
+ throw new Error(`Unknown shell: ${shell}`);
9043
+ }
9044
+ }
9045
+ async function run4(shell, toolName) {
9046
+ if (!["bash", "zsh", "fish"].includes(shell)) {
9047
+ process.stderr.write(`error: unsupported shell "${shell}". Use bash, zsh, or fish.
9048
+ `);
9049
+ process.exit(1);
9050
+ }
9051
+ const schema = await getToolSchema2(toolName);
9052
+ if (!schema) {
9053
+ process.stderr.write(`error: could not get --describe from "${toolName}"
9054
+ `);
9055
+ process.exit(1);
9056
+ }
9057
+ process.stdout.write(generate(shell, toolName, schema));
9058
+ }
9059
+ var init_completions = __esm(() => {
9060
+ init_search2();
9061
+ });
9062
+
8455
9063
  // node_modules/commander/esm.mjs
8456
9064
  var import__ = __toESM(require_commander(), 1);
8457
9065
  var {
@@ -8579,7 +9187,7 @@ function cleanJson(obj) {
8579
9187
  }
8580
9188
 
8581
9189
  // src/index.ts
8582
- var VERSION3 = "0.1.0";
9190
+ var VERSION3 = "0.2.0";
8583
9191
  function selfDescribe() {
8584
9192
  const schema = {
8585
9193
  name: "mtpcli",
@@ -8777,6 +9385,82 @@ function selfDescribe() {
8777
9385
  command: 'mtpcli wrap --server "npx @mcp/server-github" search_repos -- --query mtpcli'
8778
9386
  }
8779
9387
  ]
9388
+ },
9389
+ {
9390
+ name: "validate",
9391
+ description: "Validate a tool's --describe output against the MTP spec",
9392
+ args: [
9393
+ {
9394
+ name: "tool",
9395
+ type: "string",
9396
+ description: "Tool name to validate"
9397
+ },
9398
+ {
9399
+ name: "--json",
9400
+ type: "boolean",
9401
+ default: false,
9402
+ description: "Output as JSON"
9403
+ },
9404
+ {
9405
+ name: "--stdin",
9406
+ type: "boolean",
9407
+ default: false,
9408
+ description: "Read JSON from stdin instead of running tool"
9409
+ },
9410
+ {
9411
+ name: "--skip-help",
9412
+ type: "boolean",
9413
+ default: false,
9414
+ description: "Skip --help cross-reference check"
9415
+ }
9416
+ ],
9417
+ examples: [
9418
+ {
9419
+ description: "Validate a tool",
9420
+ command: "mtpcli validate mytool"
9421
+ },
9422
+ {
9423
+ description: "Validate from stdin",
9424
+ command: "cat describe.json | mtpcli validate --stdin"
9425
+ },
9426
+ {
9427
+ description: "JSON output for CI",
9428
+ command: "mtpcli validate mytool --json"
9429
+ }
9430
+ ]
9431
+ },
9432
+ {
9433
+ name: "completions",
9434
+ description: "Generate shell completions for a --describe-compatible tool",
9435
+ args: [
9436
+ {
9437
+ name: "shell",
9438
+ type: "enum",
9439
+ required: true,
9440
+ values: ["bash", "zsh", "fish"],
9441
+ description: "Target shell"
9442
+ },
9443
+ {
9444
+ name: "tool",
9445
+ type: "string",
9446
+ required: true,
9447
+ description: "Tool name"
9448
+ }
9449
+ ],
9450
+ examples: [
9451
+ {
9452
+ description: "Install bash completions",
9453
+ command: "eval $(mtpcli completions bash mytool)"
9454
+ },
9455
+ {
9456
+ description: "Install zsh completions",
9457
+ command: "eval $(mtpcli completions zsh mytool)"
9458
+ },
9459
+ {
9460
+ description: "Install fish completions",
9461
+ command: "mtpcli completions fish mytool | source"
9462
+ }
9463
+ ]
8780
9464
  }
8781
9465
  ]
8782
9466
  };
@@ -8837,12 +9521,24 @@ authCmd.command("refresh").description("Force-refresh the stored OAuth token for
8837
9521
  await runRefresh2(tool);
8838
9522
  });
8839
9523
  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);
9524
+ const { run: run5 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
9525
+ await run5(opts.tool);
8842
9526
  });
8843
9527
  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);
9528
+ const { run: run5 } = await Promise.resolve().then(() => (init_wrap(), exports_wrap));
9529
+ await run5(opts.server, opts.describe, toolName, args);
9530
+ });
9531
+ 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) => {
9532
+ const { run: run5 } = await Promise.resolve().then(() => (init_validate(), exports_validate));
9533
+ await run5(tool, {
9534
+ json: opts.json,
9535
+ stdin: opts.stdin,
9536
+ skipHelp: opts.skipHelp
9537
+ });
9538
+ });
9539
+ 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) => {
9540
+ const { run: run5 } = await Promise.resolve().then(() => (init_completions(), exports_completions));
9541
+ await run5(shell, tool);
8846
9542
  });
8847
9543
  try {
8848
9544
  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.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "bin": {