#!/usr/bin/env bash

set -euo pipefail

URL="http://127.0.0.1:8000/mcp"
TOKEN=""
PRETTY=true
MODE="mcp"   # mcp | openapi

usage() {
cat <<EOF
Usage:

  MCP — Discovery
    $0 --token TOKEN list
  MCP — Discovery (custom URL)
    $0 --url URL --token TOKEN list
  MCP — Tool call
    $0 --token TOKEN call TOOL_NAME key=value ...

  OpenAPI — Discovery
    $0 --openapi --url URL --token TOKEN list
  OpenAPI — Tool call
    $0 --openapi --url URL --token TOKEN call OPERATION_ID key=value ...

  Help (MCP or OpenAPI)
    $0 --token TOKEN list --help
        Table of all tools: name, title, description
    $0 --token TOKEN call TOOL_NAME --help
        Python-style doc of one tool: signature, name, title, description,
        arguments and output (type, title, description)

Options:

  --url URL
      Endpoint base URL
      Default (MCP): http://127.0.0.1:8000/mcp
  --token TOKEN
      Bearer token (required)
  --openapi
      Switch to OpenAPI mode (fetches /openapi.json, calls operations directly)
  --brut
      No pretty-print — raw JSON output
  -h, --help
      Alone: show this help.
      After 'list': table of tools.  After 'call TOOL_NAME': doc of that tool.
      (with --brut: raw JSON definition instead of formatted text)

Examples:

  MCP — Discover tools
    $0 --token xxx list
  MCP — Call a tool
    $0 --token xxx call get_weather city=Paris

  OpenAPI — Discover operations
    $0 --openapi --url https://api.example.com --token xxx list
  OpenAPI — Call an operation
    $0 --openapi --url https://api.example.com --token xxx call get_weather city=Paris
  Brut output
    $0 --openapi --url https://api.example.com --token xxx --brut call get_weather city=Paris

  Help
    $0 --token xxx list --help
    $0 --token xxx call get_weather --help
    $0 --openapi --url https://api.example.com --token xxx call get_weather --help

EOF
}

pretty_print() {
    if [[ "$PRETTY" != true ]]; then
        cat
        return
    fi

    if ! command -v jq >/dev/null 2>&1; then
        echo "ERROR: jq is required for pretty-print" >&2
        exit 1
    fi

    local RESPONSE
    RESPONSE=$(cat)

    # MCP SSE responses carry a "data: " prefix
    local JSON
    JSON=$(echo "$RESPONSE" | sed -n 's/^data: //p')

    if [[ -z "$JSON" ]]; then
        # Plain JSON (OpenAPI responses) — just pretty-print as-is
        echo "$RESPONSE" | jq '.'
        return
    fi

    echo "$JSON" | jq '
        if .result.structuredContent then
            .result.structuredContent
        elif .result then
            .result
        else
            .
        end
    '
}

# ---------------------------------------------------------------------------
# MCP helpers
# ---------------------------------------------------------------------------

create_session() {
    local headers
    headers=$(mktemp)

    curl -s \
      -X POST "$URL" \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -H "Authorization: Bearer $TOKEN" \
      -D "$headers" \
      -o /dev/null \
      -d '{
            "jsonrpc":"2.0",
            "method":"initialize",
            "id":0,
            "params":{
              "protocolVersion":"2024-11-05",
              "capabilities":{},
              "clientInfo":{
                "name":"mcp.sh",
                "version":"1.0"
              }
            }
          }'

    local HTTP_CODE
    HTTP_CODE=$(awk 'NR==1 {print $2}' "$headers")
    SESSION=$(
        grep -i '^mcp-session-id:' "$headers" |
        awk '{print $2}' |
        tr -d '\r'
    ) || true

    rm -f "$headers"

    if [[ "$HTTP_CODE" != "200" ]]; then
        echo "ERROR: initialize failed (HTTP ${HTTP_CODE:-no response}) on $URL" >&2
        exit 1
    fi

    # Stateless servers (stateless_http=True) return no session id: that is expected
    SESSION_HEADER=()
    if [[ -n "${SESSION:-}" ]]; then
        SESSION_HEADER=(-H "mcp-session-id: $SESSION")
    fi
}

mcp_list() {
    create_session

    curl -s \
      -X POST "$URL" \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -H "Authorization: Bearer $TOKEN" \
      ${SESSION_HEADER[@]+"${SESSION_HEADER[@]}"} \
      -d '{
            "jsonrpc":"2.0",
            "method":"tools/list",
            "id":1
          }' \
    | pretty_print

    echo
}

mcp_call() {
    local TOOL_NAME="$1"; shift

    create_session

    local ARGS_JSON
    ARGS_JSON=$(build_args_json "$@")

    local PAYLOAD
    PAYLOAD=$(jq -nc --arg n "$TOOL_NAME" --argjson a "$ARGS_JSON" \
        '{jsonrpc:"2.0", method:"tools/call", id:2, params:{name:$n, arguments:$a}}')

    curl -s \
      -X POST "$URL" \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -H "Authorization: Bearer $TOKEN" \
      ${SESSION_HEADER[@]+"${SESSION_HEADER[@]}"} \
      -d "$PAYLOAD" \
    | pretty_print

    echo
}

# ---------------------------------------------------------------------------
# OpenAPI helpers
# ---------------------------------------------------------------------------

# Fetch and cache the OpenAPI spec (stored in a temp file for the session)
OPENAPI_SPEC_FILE=""

fetch_openapi_spec() {
    if [[ -n "$OPENAPI_SPEC_FILE" && -f "$OPENAPI_SPEC_FILE" ]]; then
        return
    fi

    if ! command -v jq >/dev/null 2>&1; then
        echo "ERROR: jq is required for OpenAPI mode" >&2
        exit 1
    fi

    OPENAPI_SPEC_FILE=$(mktemp)
    # Try /openapi.json first, then /openapi.yaml (json only — yaml requires extra tooling)
    local BASE_URL="${URL%/}"   # strip trailing slash

    local HTTP_CODE
    HTTP_CODE=$(curl -s -o "$OPENAPI_SPEC_FILE" -w "%{http_code}" \
      -H "Authorization: Bearer $TOKEN" \
      "${BASE_URL}/openapi.json")

    if [[ "$HTTP_CODE" != "200" ]]; then
        echo "ERROR: could not fetch OpenAPI spec from ${BASE_URL}/openapi.json (HTTP $HTTP_CODE)" >&2
        rm -f "$OPENAPI_SPEC_FILE"
        exit 1
    fi

    # Validate it looks like JSON
    if ! jq empty "$OPENAPI_SPEC_FILE" 2>/dev/null; then
        echo "ERROR: ${BASE_URL}/openapi.json did not return valid JSON" >&2
        rm -f "$OPENAPI_SPEC_FILE"
        exit 1
    fi
}

openapi_list() {
    fetch_openapi_spec

    # Résout un $ref "#/components/schemas/Foo" → l'objet schema correspondant
    # Extrait les propriétés du body en résolvant les $ref si nécessaire
    jq -r '
        . as $root |
        # Fonction de résolution de $ref
        def resolve_ref(schema):
            if schema | has("$ref") then
                schema["$ref"] |
                ltrimstr("#/") |
                split("/") |
                reduce .[] as $part ($root; .[$part])
            else
                schema
            end;

        .paths // {} |
        to_entries[] |
        .key as $path |
        .value | to_entries[] |
        select(.key | test("^(get|post|put|patch|delete|options|head)$"; "i")) |
        .key as $method |
        .value |
        . as $op |
        {
            operationId: (.operationId // ($method + " " + $path)),
            method: ($method | ascii_upcase),
            path: $path,
            summary: (.summary // "-"),
            parameters: (
                # Paramètres de chemin/query classiques
                [
                    ($op.parameters // [])[] |
                    .name + "=" + (.schema.type // "string") +
                    (if .required then " (required)" else "" end)
                ]
                +
                # Paramètres du requestBody (résolution du $ref si besoin)
                (
                    if $op.requestBody then
                        ($op.requestBody.content | to_entries[0].value.schema) |
                        resolve_ref(.) |
                        . as $resolved_schema |
                        ($resolved_schema.properties // {}) |
                        to_entries[] |
                        .key as $pname |
                        [
                            $pname + "=" + (.value.type // "?") +
                            (if (.value.default != null) then " [default: " + (.value.default | tostring) + "]" else "" end) +
                            (if ($resolved_schema.required // [] | contains([$pname])) then " (required)" else "" end)
                        ]
                    else
                        []
                    end
                )
            )
        }
    ' "$OPENAPI_SPEC_FILE" \
    | if [[ "$PRETTY" == true ]]; then
          jq -r '"─────────────────────────────────────────────\n" +
                 "operationId : " + .operationId + "\n" +
                 "method      : " + .method + "  " + .path + "\n" +
                 "summary     : " + .summary + "\n" +
                 "parameters  : " + (if (.parameters | length) > 0 then (.parameters | join(", ")) else "-" end)'
      else
          cat
      fi

    echo
}

openapi_call() {
    local OPERATION_ID="$1"; shift

    fetch_openapi_spec

    # Resolve the operation: find path + method + parameter locations
    # Résout les $ref pour les bodyParams
    local OP_INFO
    OP_INFO=$(jq -r --arg op "$OPERATION_ID" '
        . as $root |
        def resolve_ref(schema):
            if schema | has("$ref") then
                schema["$ref"] |
                ltrimstr("#/") |
                split("/") |
                reduce .[] as $part ($root; .[$part])
            else
                schema
            end;

        .paths // {} |
        to_entries[] |
        .key as $path |
        .value | to_entries[] |
        select(.key | test("^(get|post|put|patch|delete|options|head)$"; "i")) |
        .key as $method |
        .value |
        select((.operationId // "") == $op) |
        {
            method: ($method | ascii_upcase),
            path: $path,
            queryParams: [(.parameters // [])[] | select(.in == "query") | .name],
            pathParams:  [(.parameters // [])[] | select(.in == "path")  | .name],
            bodyParams:  (
                if .requestBody then
                    (.requestBody.content | to_entries[0].value.schema) |
                    resolve_ref(.) |
                    .properties // {} | keys
                else []
                end
            ),
            contentType: (
                if .requestBody then
                    (.requestBody.content | to_entries[0].key)
                else "application/json"
                end
            )
        }
    ' "$OPENAPI_SPEC_FILE")

    if [[ -z "$OP_INFO" ]]; then
        echo "ERROR: operationId '$OPERATION_ID' not found in OpenAPI spec" >&2
        exit 1
    fi

    local METHOD PATH_TMPL
    METHOD=$(echo "$OP_INFO"    | jq -r '.method')
    PATH_TMPL=$(echo "$OP_INFO" | jq -r '.path')
    local CONTENT_TYPE
    CONTENT_TYPE=$(echo "$OP_INFO" | jq -r '.contentType')

    # Parse key=value arguments
    declare -A KV
    for ARG in "$@"; do
        local KEY="${ARG%%=*}"
        local VALUE="${ARG#*=}"
        KV["$KEY"]="$VALUE"
    done

    # 1. Substitute path parameters
    local FINAL_PATH="$PATH_TMPL"
    while IFS= read -r PNAME; do
        [[ -z "$PNAME" ]] && continue
        local PVAL="${KV[$PNAME]:-}"
        if [[ -z "$PVAL" ]]; then
            echo "ERROR: missing required path parameter '$PNAME'" >&2
            exit 1
        fi
        FINAL_PATH="${FINAL_PATH//\{$PNAME\}/$PVAL}"
        unset "KV[$PNAME]"
    done < <(echo "$OP_INFO" | jq -r '.pathParams[]')

    # 2. Build query string
    local QUERY_STRING=""
    while IFS= read -r QNAME; do
        [[ -z "$QNAME" ]] && continue
        local QVAL="${KV[$QNAME]:-}"
        if [[ -n "$QVAL" ]]; then
            QUERY_STRING+="${QUERY_STRING:+&}${QNAME}=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$QVAL" 2>/dev/null || echo "$QVAL")"
            unset "KV[$QNAME]"
        fi
    done < <(echo "$OP_INFO" | jq -r '.queryParams[]')

    # 3. Remaining KV pairs → request body
    local BODY_JSON="{}"
    if [[ ${#KV[@]} -gt 0 ]]; then
        BODY_JSON=$(build_args_json_from_assoc)
    fi

    # 4. Assemble final URL
    local BASE_URL="${URL%/}"
    local FULL_URL="${BASE_URL}${FINAL_PATH}"
    [[ -n "$QUERY_STRING" ]] && FULL_URL+="?${QUERY_STRING}"

    # 5. Fire the request
    local CURL_ARGS=(
        -s
        -X "$METHOD"
        -H "Authorization: Bearer $TOKEN"
        -H "Accept: application/json"
    )

    if [[ "$METHOD" != "GET" && "$METHOD" != "HEAD" ]]; then
        CURL_ARGS+=(
            -H "Content-Type: ${CONTENT_TYPE}"
            -d "$BODY_JSON"
        )
    fi

    curl "${CURL_ARGS[@]}" "$FULL_URL" | pretty_print

    echo
}

# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------

build_args_json() {
    # key=value -> JSON object. A value that parses as JSON keeps its type
    # (top_k=5 -> 5, flag=true -> true, ids='["a","b"]' -> list); anything else is a string.
    local ARGS_JSON='{}' KEY VALUE
    for ARG in "$@"; do
        KEY="${ARG%%=*}"
        VALUE="${ARG#*=}"
        ARGS_JSON=$(jq -c --arg k "$KEY" --arg v "$VALUE" \
            '. + {($k): ($v | try fromjson catch $v)}' <<< "$ARGS_JSON")
    done
    echo "$ARGS_JSON"
}

# Build JSON body from the associative array KV (bash 4+)
build_args_json_from_assoc() {
    local JSON="{"
    local FIRST=true

    for KEY in "${!KV[@]}"; do
        local VALUE="${KV[$KEY]}"
        if [[ "$FIRST" == true ]]; then
            FIRST=false
        else
            JSON+=","
        fi
        local ESCAPED_VALUE
        ESCAPED_VALUE=$(printf '%s' "$VALUE" | sed 's/"/\\"/g')
        JSON+="\"$KEY\":\"$ESCAPED_VALUE\""
    done

    JSON+="}"
    echo "$JSON"
}

# ---------------------------------------------------------------------------
# Help (list --help / call TOOL --help)
# ---------------------------------------------------------------------------
#
# Both modes are normalised to the MCP tool shape:
#   { name, title, description, inputSchema, outputSchema, [endpoint] }
# then rendered by the same jq library below.

HELP_TMP=$(mktemp -d)
trap 'rm -rf "$HELP_TMP" ${OPENAPI_SPEC_FILE:+"$OPENAPI_SPEC_FILE"}' EXIT

JQ_LIB=$(cat <<'JQ'
def rep($s; $n): if $n <= 0 then "" else [range($n)] | map($s) | add end;
def ind($n): rep(" "; $n);
def pad($w): . + ind($w - length);
def trunc($w): if length > $w then .[0:($w - 1)] + "…" else . end;
def reindent($p):
    split("\n") | map(sub("\\s+$"; ""))
    | (.[0:1] + (.[1:] | map(if . == "" then . else $p + . end))) | join("\n");
def pyval:
    if . == null then "None" elif . == true then "True" elif . == false then "False"
    else tojson end;

# Resolve a local $ref ("#/..." relative to $root); sibling keys win
def resolve($root):
    if type == "object" and (.["$ref"] | type) == "string" and (.["$ref"] | startswith("#/")) then
        (.["$ref"] | ltrimstr("#/") | split("/") | map(gsub("~1"; "/") | gsub("~0"; "~"))) as $p
        | ((($root | getpath($p)) // {}) | resolve($root)) + del(.["$ref"])
    elif type == "object" and (.allOf | type) == "array" and (.allOf | length) == 1 then
        (.allOf[0] | resolve($root)) + del(.allOf)
    else . end;

# JSON schema -> Python-like type annotation
def basic:
    {"string":"str","integer":"int","number":"float","boolean":"bool",
     "null":"None","object":"dict","array":"list"}[.] // .;
def pytype($root; $d):
    if type != "object" or $d > 5 then "Any"
    elif has("$ref") then (.["$ref"] | split("/") | last)
    elif has("enum") then "Literal[" + (.enum | map(pyval) | join(", ")) + "]"
    elif has("const") then "Literal[" + (.const | pyval) + "]"
    elif has("anyOf") or has("oneOf") then [(.anyOf // .oneOf)[] | pytype($root; $d + 1)] | join(" | ")
    elif has("allOf") then [.allOf[] | pytype($root; $d + 1)] | join(" & ")
    elif (.type | type) == "array" then
        . as $s | [.type[] as $t | ($s + {type: $t}) | pytype($root; $d + 1)] | join(" | ")
    elif .type == "array" then
        if (.items | type) == "object" then "list[" + (.items | pytype($root; $d + 1)) + "]" else "list" end
    elif .type == "object" and (.additionalProperties | type) == "object" then
        "dict[str, " + (.additionalProperties | pytype($root; $d + 1)) + "]"
    elif .type then (.type | basic)
    else "Any" end;

# Object schema worth expanding (nested fields), following Optional[...] and list[...]
def expandable($root):
    resolve($root)
    | if has("anyOf") or has("oneOf") then
          [(.anyOf // .oneOf)[] | resolve($root) | select(.type != "null")]
          | if length == 1 then .[0] | expandable($root) else empty end
      elif .type == "array" and (.items | type) == "object" then .items | expandable($root)
      elif (.properties | type) == "object" and (.properties | length) > 0 then .
      else empty end;

# One block per property: "name : type = default  (required)" + title + description
def fields($root; $n; $d):
    resolve($root) as $s
    | ($s.required // []) as $req
    | ($s.properties // {}) | to_entries[]
    | .key as $k | .value as $raw | ($raw | resolve($root)) as $v
    | (if ($req | index($k)) != null then "required" else "optional" end) as $rq
    | (if ($v | type) == "object" and ($v | has("default")) then " = " + ($v.default | pyval) else "" end) as $def
    | (ind($n) + $k + " : " + ($raw | pytype($root; 0)) + $def
          + "  (" + $rq + (if $v["x-in"] then ", in " + $v["x-in"] else "" end) + ")"),
      (ind($n + 4) + "title       : " + (($v.title // "-") | tostring)),
      (ind($n + 4) + "description : " + (($v.description // "-") | tostring | reindent(ind($n + 18)))),
      (if $d < 3 then
           ($raw | expandable($root)) as $sub
           | (ind($n + 4) + "fields:"), ($sub | fields($root; $n + 8; $d + 1))
       else empty end);

def tool_title: (.title // .annotations.title // "-") | tostring;

def signature($root):
    (.inputSchema // {} | resolve($root)) as $s
    | ($s.required // []) as $req
    | [ ($s.properties // {}) | to_entries[]
        | .key as $k | .value as $raw | ($raw | resolve($root)) as $v
        | (($req | index($k)) != null) as $isreq
        | { r: $isreq,
            t: ($k + ": " + ($raw | pytype($root; 0))
                + (if ($v | type) == "object" and ($v | has("default")) then " = " + ($v.default | pyval)
                   elif $isreq then "" else " = None" end)) } ]
    | (map(select(.r)) + map(select(.r | not))) | map(.t) | join(", ");

def rettype($root):
    if (.outputSchema | type) != "object" then "Any"
    elif .outputSchema | has("$ref") then .outputSchema | pytype($root; 0)
    else .outputSchema | resolve($root)
         | if .["x-fastmcp-wrap-result"] and (.properties.result | type) == "object"
           then .properties.result | pytype($root; 0)
           else (.title // pytype($root; 0)) end
    end;

def tooldoc($inroot; $outroot):
    . as $t
    | (if (.outputSchema | type) == "object" then .outputSchema | resolve($outroot) else null end) as $o
    | "\(.name)(\(signature($inroot))) -> \(rettype($outroot))",
      "",
      "    Name        : \(.name)",
      "    Title       : \(tool_title)",
      (if .endpoint then "    Endpoint    : \(.endpoint)" else empty end),
      "    Description : " + ((.description // "-") | tostring | reindent(ind(18))),
      "",
      "    Args:",
      ([.inputSchema // {} | fields($inroot; 8; 0)] | if length == 0 then "        (none)" else .[] end),
      "",
      "    Returns:",
      (if $o == null then
           "        (no outputSchema declared: free-form content)"
       else
           "        type        : \($t | rettype($outroot))",
           "        title       : \(($o.title // "-") | tostring)",
           "        description : " + (($o.description // "-") | tostring | reindent(ind(22))),
           ([$t.outputSchema | fields($outroot; 12; 0)]
            | if length == 0 then empty else "        fields:", .[] end)
       end),
      "";

def table($width):
    map({ n: .name, t: tool_title,
          d: ((.description // "-") | tostring | split("\n") | map(select(test("\\S")))
              | (.[0] // "-") | gsub("^\\s+|\\s+$"; "")) })
    | (([.[].n | length] + [4]) | max) as $nw
    | ([(([.[].t | length] + [5]) | max), 40] | min) as $tw
    | ([$width - $nw - $tw - 6, 30] | max) as $dw
    | ("NAME" | pad($nw)) + "   " + ("TITLE" | pad($tw)) + "   DESCRIPTION",
      rep("─"; $nw) + "   " + rep("─"; $tw) + "   " + rep("─"; [$dw, 11] | min),
      (.[] | (.n | pad($nw)) + "   " + (.t | trunc($tw) | pad($tw)) + "   " + (.d | trunc($dw))),
      "",
      "\(length) tool(s) — details: call <name> --help";

# OpenAPI spec -> list of MCP-shaped tools
def oa_tools:
    . as $spec
    | [ (.paths // {}) | to_entries[]
        | .key as $path | .value as $pi | ($pi.parameters // []) as $common
        | $pi | to_entries[]
        | select(.key | test("^(get|post|put|patch|delete|options|head)$"; "i"))
        | .key as $m | .value as $op
        | (($common + ($op.parameters // [])) | map(resolve($spec))
           | map(select(.in != "header" and .in != "cookie"))) as $params
        | (if $op.requestBody then
               ($op.requestBody | resolve($spec) | (.content // {}) | to_entries
                | (.[0].value.schema // null) | if . then resolve($spec) else null end)
           else null end) as $body
        | { name: ($op.operationId // (($m | ascii_upcase) + " " + $path)),
            title: ($op.summary // null),
            description: ($op.description // $op.summary // null),
            endpoint: (($m | ascii_upcase) + " " + $path),
            inputSchema: {
                type: "object",
                properties: (
                    ([ $params[]
                       | { key: .name,
                           value: ((.schema // {})
                                   + ({description: .description} | with_entries(select(.value != null)))
                                   + {"x-in": .in}) } ] | from_entries)
                    + (if $body == null then {}
                       elif ($body.properties | type) == "object" then
                           ($body.properties | map_values(. + {"x-in": "body"}))
                       else {body: ($body + {"x-in": "body"})} end)),
                required: ([ $params[] | select(.required == true) | .name ]
                           + (if ($body | type) == "object" then ($body.required // []) else [] end))
            },
            outputSchema: (
                ($op.responses // {}) | to_entries | map(select(.key | test("^2")))
                | (.[0].value // null)
                | if . == null then null
                  else resolve($spec) | (.content // {})
                       | (.["application/json"] // (to_entries[0].value) // null)
                       | if . then (.schema // null) else null end
                  end)
          } ];
JQ
)

term_width() {
    local w="${COLUMNS:-}"
    [[ -z "$w" ]] && w=$(tput cols 2>/dev/null || true)
    [[ "$w" =~ ^[0-9]+$ ]] || w=120
    echo "$w"
}

# Writes the full tool list (all pages) as a JSON array to $1
mcp_fetch_tools() {
    local out="$1" cursor="" page=0 params resp json

    create_session

    while :; do
        page=$((page + 1))
        if [[ -n "$cursor" ]]; then
            params=$(jq -nc --arg c "$cursor" '{cursor: $c}')
        else
            params='{}'
        fi

        resp=$(curl -s \
          -X POST "$URL" \
          -H "Content-Type: application/json" \
          -H "Accept: application/json, text/event-stream" \
          -H "Authorization: Bearer $TOKEN" \
          ${SESSION_HEADER[@]+"${SESSION_HEADER[@]}"} \
          -d "$(jq -nc --argjson p "$params" '{jsonrpc:"2.0", method:"tools/list", id:1, params:$p}')")

        json=$(printf '%s\n' "$resp" | sed -n 's/^data: //p' | tail -n 1)
        [[ -z "$json" ]] && json="$resp"
        printf '%s\n' "$json" > "$HELP_TMP/page_$page.json"

        if ! jq -e '.result.tools | type == "array"' "$HELP_TMP/page_$page.json" >/dev/null 2>&1; then
            echo "ERROR: tools/list failed:" >&2
            printf '%s\n' "$json" >&2
            exit 1
        fi

        cursor=$(jq -r '.result.nextCursor // empty' "$HELP_TMP/page_$page.json")
        [[ -z "$cursor" ]] && break
    done

    jq -s 'map(.result.tools) | add' "$HELP_TMP"/page_*.json > "$out"
}

# Writes the OpenAPI operations, normalised as tools, to $1
openapi_fetch_tools() {
    fetch_openapi_spec
    jq "$JQ_LIB
        oa_tools" "$OPENAPI_SPEC_FILE" > "$1"
}

load_tools() {
    TOOLS_FILE="$HELP_TMP/tools.json"
    SPEC_FILE="$HELP_TMP/spec.json"
    if [[ "$MODE" == "openapi" ]]; then
        openapi_fetch_tools "$TOOLS_FILE"
        cp "$OPENAPI_SPEC_FILE" "$SPEC_FILE"
    else
        mcp_fetch_tools "$TOOLS_FILE"
        echo 'null' > "$SPEC_FILE"
    fi
}

help_list() {
    load_tools

    if [[ "$PRETTY" != true ]]; then
        jq '[.[] | {name, title: (.title // .annotations.title // null), description}]' "$TOOLS_FILE"
        return
    fi

    jq -r --argjson width "$(term_width)" "$JQ_LIB
        table(\$width)" "$TOOLS_FILE"
}

help_tool() {
    local NAME="$1"

    load_tools

    if ! jq -e --arg n "$NAME" 'any(.[]; .name == $n)' "$TOOLS_FILE" >/dev/null; then
        echo "ERROR: '$NAME' not found." >&2
        local close
        close=$(jq -r --arg n "$NAME" \
            '.[].name | select(ascii_downcase | contains($n | ascii_downcase))' "$TOOLS_FILE")
        if [[ -n "$close" ]]; then
            echo "Did you mean:" >&2
            sed 's/^/  - /' <<< "$close" >&2
        else
            echo "Use 'list --help' to see available tools." >&2
        fi
        exit 1
    fi

    if [[ "$PRETTY" != true ]]; then
        jq --arg n "$NAME" '.[] | select(.name == $n)' "$TOOLS_FILE"
        return
    fi

    jq -r --arg n "$NAME" --slurpfile specs "$SPEC_FILE" "$JQ_LIB
        \$specs[0] as \$spec
        | .[] | select(.name == \$n)
        | (if \$spec then \$spec else (.inputSchema  // {}) end) as \$in
        | (if \$spec then \$spec else (.outputSchema // {}) end) as \$out
        | tooldoc(\$in; \$out)" "$TOOLS_FILE"
}

# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------

COMMAND=""
HELP=false

while [[ $# -gt 0 ]]; do
    case "$1" in

        --url)
            URL="$2"
            shift 2
            ;;

        --token)
            TOKEN="$2"
            shift 2
            ;;

        --openapi)
            MODE="openapi"
            shift
            ;;

        --brut)
            PRETTY=false
            shift
            ;;

        list|--list)
            COMMAND="list"
            shift
            break
            ;;

        call)
            COMMAND="call"
            shift
            break
            ;;

        -h|--help)
            HELP=true
            shift
            ;;

        *)
            echo "Unknown argument: $1" >&2
            usage
            exit 1
            ;;
    esac
done

# --help may also come after the command: "list --help", "call TOOL --help"
REST=()
for ARG in "$@"; do
    case "$ARG" in
        -h|--help) HELP=true ;;
        *)         REST+=("$ARG") ;;
    esac
done
set -- ${REST[@]+"${REST[@]}"}

if [[ "$HELP" == true && -z "$COMMAND" ]]; then
    usage
    exit 0
fi

if [[ -z "$TOKEN" ]]; then
    echo "ERROR: --token is required" >&2
    exit 1
fi

if [[ -z "$COMMAND" ]]; then
    usage
    exit 1
fi

if [[ "$HELP" == true ]]; then
    if [[ "$COMMAND" == "call" && -n "${1:-}" ]]; then
        help_tool "$1"
    else
        help_list
    fi
    exit 0
fi

# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------

if [[ "$MODE" == "openapi" ]]; then

    if [[ "$COMMAND" == "list" ]]; then
        openapi_list
        exit 0
    fi

    if [[ "$COMMAND" == "call" ]]; then
        TOOL_NAME="${1:-}"
        if [[ -z "$TOOL_NAME" ]]; then
            echo "ERROR: missing operation id" >&2
            exit 1
        fi
        shift
        openapi_call "$TOOL_NAME" "$@"
        exit 0
    fi

else  # MCP mode

    if [[ "$COMMAND" == "list" ]]; then
        mcp_list
        exit 0
    fi

    if [[ "$COMMAND" == "call" ]]; then
        TOOL_NAME="${1:-}"
        if [[ -z "$TOOL_NAME" ]]; then
            echo "ERROR: missing tool name" >&2
            exit 1
        fi
        shift
        mcp_call "$TOOL_NAME" "$@"
        exit 0
    fi

fi

echo "ERROR: unsupported command" >&2
exit 1
