Skip to content

MCP CLI Adapter uses field names instead of option names for tool parameters #576

@thomasmhofmann

Description

@thomasmhofmann

MCP CLI Adapter Bug Report: Parameter Name Mapping Issue

Summary

The MCP CLI adapter has two related bugs that cause parameter mapping failures:

  1. Primary Bug: Uses Java field names instead of Picocli option names for MCP tool parameters
  2. Secondary Issue: Uses the first option name (often short form like -c) instead of the longest/preferred option name (like --cluster)

Bug Locations

Primary Bug

File: repo/quarkus-mcp-server/cli-adapter/deployment/src/main/java/io/quarkiverse/mcp/server/cli/adapter/deployment/McpServerCliAdapterProcessor.java

Lines: 314-325 (in wrapCommand method)

Secondary Issue

File: repo/quarkus-mcp-server/cli-adapter/deployment/src/main/java/io/quarkiverse/mcp/server/cli/adapter/deployment/CommandUtil.java

Lines: 97-99 (in listOptions method)

Root Cause Analysis

Current Implementation (Lines 314-325)

for (Entry<String, FieldInfo> entry : options.entrySet()) {
    FieldInfo option = entry.getValue();
    String optionName = option.name().toString();  // ← BUG: Uses field name
    String optionDescription = String.join("\n",
            option.annotation(DotNames.OPTION).value("description").asStringArray());

    executeMethod.getParameterAnnotations(optionIndex).addAnnotation(ToolArg.class)
            .add("name", optionName.replaceAll("^-*", ""))  // ← Uses field name
            .add("description", optionDescription)
            .add("required", false);
    optionIndex++;
}

The Problem

  1. Line 316: option.name().toString() returns the Java field name (e.g., clusterNameOrId)
    • option is a FieldInfo object (from Jandex bytecode analysis)
    • FieldInfo.name() returns the Java field identifier, NOT the Picocli option name
  2. Line 321: This field name is used as the MCP tool parameter name
  3. Line 352: When calling the command, it correctly uses entry.getKey() which contains the Picocli option name (e.g., --cluster)

Parameter Flow

MCP Tool Call:
  {"cluster": "eip-reflup"}  ← User provides this
       ↓
MCP Tool Parameter Name: "clusterNameOrId"  ← Generated from field name (WRONG)
       ↓
Command Line Argument: "--cluster"  ← From entry.getKey() (CORRECT)
       ↓
Result: Parameter mismatch - MCP expects "clusterNameOrId" but should expect "cluster"

Affected Code Example

NamespaceNodeInfoCommand.java

@Option(names = { "-c", "--cluster" }, 
        description = "Name or ID of the IBM Cloud Kubernetes cluster...")
private String clusterNameOrId;  // ← Field name differs from option name

Generated MCP Tool (Current - WRONG)

public String namespace_namespacenodeinfocommand(
    @ToolArg(name = "namespaces", ...) String namespaces,
    @ToolArg(name = "clusterNameOrId", ...) String clusterNameOrId,  // ← WRONG: Uses field name
    @ToolArg(name = "jsonOutput", ...) String jsonOutput,
    @ToolArg(name = "dryRun", ...) String dryRun) {
    
    HashMap map = new HashMap();
    map.put("--cluster", clusterNameOrId);  // ← Maps to correct CLI option
    // ...
}

What Should Be Generated (CORRECT)

public String namespace_namespacenodeinfocommand(
    @ToolArg(name = "namespaces", ...) String namespaces,
    @ToolArg(name = "cluster", ...) String cluster,  // ← CORRECT: Uses option name
    @ToolArg(name = "json", ...) String json,
    @ToolArg(name = "dry-run", ...) String dryRun) {
    
    HashMap map = new HashMap();
    map.put("--cluster", cluster);  // ← Maps correctly
    // ...
}

Fixes Required

Fix 1: CommandUtil.listOptions() - Use Longest Option Name

Location: CommandUtil.java, lines 97-99

Current Code:

String[] names = o.value("names").asStringArray();
if (names.length != 0) {
    options.put(names[0], a);  // ← BUG: Uses first option (often short form)
}

Problem: For @Option(names = { "-c", "--cluster" }), this puts "-c" in the map, which becomes parameter name "c" after removing dashes.

Proposed Fix:

String[] names = o.value("names").asStringArray();
if (names.length != 0) {
    // Use the longest option name (typically the --long-form)
    String longestName = names[0];
    for (String name : names) {
        if (name.length() > longestName.length()) {
            longestName = name;
        }
    }
    options.put(longestName, a);
}

Result: For @Option(names = { "-c", "--cluster" }), this puts "--cluster" in the map.

Fix 2: McpServerCliAdapterProcessor.wrapCommand() - Use Option Name

Location: McpServerCliAdapterProcessor.java, lines 314-325

Current Code:

for (Entry<String, FieldInfo> entry : options.entrySet()) {
    FieldInfo option = entry.getValue();
    String optionName = option.name().toString();  // ← BUG: Uses field name

Proposed Fix:

for (Entry<String, FieldInfo> entry : options.entrySet()) {
    FieldInfo option = entry.getValue();
    // Use the CLI option name from the map key (now the longest option after Fix 1)
    String optionName = entry.getKey().replaceAll("^-*", "");

Result: With both fixes, @Option(names = { "-c", "--cluster" }) becomes parameter name "cluster".

Impact

Affected Commands

All Picocli commands with options are affected by at least one of these bugs:

Both Bugs (field name mismatch + short option used)

  • @Option(names = { "-c", "--cluster" }) private String clusterNameOrId;
    • Current: Uses field name "clusterNameOrId"
    • After Fix 1 only: Uses "-c""c"
    • After both fixes: Uses "--cluster""cluster"

Primary Bug Only (field name mismatch)

  • @Option(names = "--json") private boolean jsonOutput;
    • Current: Uses field name "jsonOutput"
    • After fixes: Uses "--json""json"

Secondary Issue Only (short option preferred)

  • @Option(names = { "-n", "--namespace" }) private List<String> namespaces;
    • Current: Uses field name "namespaces" (happens to work) ✓
    • But if field matched option: Would use "-n""n"
    • After both fixes: Uses "--namespace""namespace"

Workaround

Rename all Java fields to match their primary Picocli option names:

// Before (fails with MCP)
@Option(names = { "-c", "--cluster" })
private String clusterNameOrId;

// After (works with MCP)
@Option(names = { "-c", "--cluster" })
private String cluster;

Test Case

Command

ops-tools namespace node-info --namespace tt2-eip-all --cluster eip-reflup

MCP Tool Call (Current - Fails)

{
  "tool": "namespace_namespacenodeinfocommand",
  "arguments": {
    "namespaces": "tt2-eip-all",
    "clusterNameOrId": "eip-reflup"  // ← Wrong parameter name
  }
}

MCP Tool Call (After Fix - Should Work)

{
  "tool": "namespace_namespacenodeinfocommand",
  "arguments": {
    "namespaces": "tt2-eip-all",
    "cluster": "eip-reflup"  // ← Correct parameter name
  }
}

Additional Notes

Why Both Fixes Are Needed

Line 352 in McpServerCliAdapterProcessor.wrapCommand() already uses entry.getKey() correctly:

ResultHandle paramKey = tryBlock.load(entry.getKey());

This proves that entry.getKey() is the right source for the CLI option name. However, the current implementation has two problems:

  1. Line 316 uses option.name().toString() (field name) instead of entry.getKey() (option name)
  2. CommandUtil.listOptions() puts names[0] (first/shortest option) in the map instead of the longest option

Understanding FieldInfo.name()

The FieldInfo class is from Jandex (JBoss bytecode indexing library):

  • FieldInfo.name() returns a DotName representing the Java field identifier
  • option.name().toString() converts this to a String like "clusterNameOrId"
  • This is the Java field name, not the Picocli @Option(names = ...) value

Why Use the Longest Option Name?

Picocli conventions typically use:

  • Short options: -c, -n, -v (single letter, less descriptive)
  • Long options: --cluster, --namespace, --verbose (descriptive, self-documenting)

For MCP tool parameters, the long form is preferable because:

  • More descriptive: "cluster" vs "c"
  • Better API documentation
  • Matches common CLI conventions
  • More intuitive for users

CommandUtil.listOptions() Behavior

The method returns a Map<String, FieldInfo> where:

  • Key: Currently the first CLI option name (e.g., "-c")
  • Value: The FieldInfo containing the Java field metadata

After Fix 1, the key will be the longest option name (e.g., "--cluster").

Severity

High - Affects all commands with options:

  • Commands with mismatched field/option names fail completely
  • Commands with short options get poor parameter names (e.g., "c" instead of "cluster")

Recommendations

  1. Fix both issues in a single PR for consistency
  2. Add unit tests to verify:
    • MCP parameter names match the longest CLI option name
    • Parameter names are properly stripped of leading dashes
    • Field names are NOT used for parameter names
  3. Document the behavior: MCP tool parameters use the longest option name from @Option(names = {...})
  4. Consider backward compatibility: This change improves parameter names but may break existing MCP clients that adapted to the buggy behavior

Testing Strategy

Test Case 1: Multiple Option Names

@Option(names = { "-c", "--cluster" })
private String clusterNameOrId;

Expected MCP parameter: "cluster" (from "--cluster")

Test Case 2: Single Long Option

@Option(names = "--namespace")
private List<String> namespaces;

Expected MCP parameter: "namespace"

Test Case 3: Field Name Matches Option

@Option(names = { "-d", "--dry-run" })
private boolean dryRun;

Expected MCP parameter: "dry-run" (NOT "dryRun" from field name)

Test Case 4: Hyphenated Options

@Option(names = "--json-output")
private boolean jsonOutput;

Expected MCP parameter: "json-output" (preserves hyphens)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions