#!/bin/sh
# lmcp-tool — call a LOCAL lmcp server's tool over a stateless HTTP tools/call.
#
# Companion client for lmcp servers (the Lua MCP server): https://git.reauktion.de/marfrit/lmcp
# Canonical source: marfrit/lmcp bin/lmcp-tool, shipped as /usr/bin/lmcp-tool by the lmcp
# package (since 2.1.0). Moved here from marfrit/sic contrib/ (d576886); edit it HERE.
# Meant to be run through sic — `sic <host> lmcp-tool ...` — so a model drives a
# remote host's MCP tools without a persistent MCP session. Three forms:
#
#   lmcp-tool list                              # DISCOVERY: available tools + descriptions
#   lmcp-tool list --json                       # same, but full tools/list incl. inputSchema
#   lmcp-tool --json <tool> key=value ...       # full result object (content[] + isError)
#   lmcp-tool <tool> key=value [key=value ...]  # args as pairs — NO json-in-shell (apostrophe-safe)
#   lmcp-tool fetch_url https://example.com       # a bare http(s):// arg is auto-mapped to url=
#   lmcp-tool <tool> '{"k":"v"}'                # raw JSON (single arg starting with '{')
#
# Prefer key=value: `sic host lmcp-tool web_search "query=today's news"` — an apostrophe
# inside double quotes is fine, and lmcp-tool JSON-encodes the pairs for you.
#
# host/port/token come from, in order: $LMCP_HOST/$LMCP_PORT/$LMCP_TOKEN → the lmcp systemd
# unit's Environment= → the file it points at via LMCP_CONF (sudo -n if root-only). Defaults to
# 127.0.0.1:8080. The unit is $LMCP_UNIT if set, else lmcp.service, else lmcp-<hostname>.service,
# else the one active lmcp*.service (the lmcp-hub broker excluded). The token is the whole value
# of the conf's `.godparticle = ...` line, read the way lmcp.lua's read_conf reads it (base64
# padding and all); a godparticle|token|bearer key is the fallback for other conf layouts.
# --json darf vorne stehen (maschinelle Verbraucher) oder bei `list` hinten.
json_out=0
if [ "$1" = "--json" ]; then json_out=1; shift; fi

tool="$1"
[ -n "$tool" ] || { echo "usage: lmcp-tool [--json] <tool> [ key=value ... | '{json}' ]   |   lmcp-tool list [--json]" >&2; exit 2; }
shift
[ "$1" = "--json" ] && { json_out=1; shift; }

pick_unit() {
    if [ -n "$LMCP_UNIT" ]; then echo "$LMCP_UNIT"; return; fi
    command -v systemctl >/dev/null 2>&1 || return 0
    for u in lmcp.service "lmcp-$(hostname -s 2>/dev/null || hostname).service"; do
        [ "$(systemctl show "$u" -p LoadState --value 2>/dev/null)" = loaded ] && { echo "$u"; return; }
    done
    set -- $(systemctl list-units --type=service --state=active --no-legend --plain 'lmcp*.service' 2>/dev/null \
             | awk '{print $1}' | grep -v '^lmcp-hub\.service$')
    [ $# -eq 1 ] && echo "$1"
    return 0
}

# Value of `key = value` lines, as lmcp.lua read_conf parses them: key up to the first '=',
# value = the rest, trimmed. `.godparticle` wins; a key containing godparticle|token|bearer
# is the fallback.
conf_token() {
    { cat "$1" 2>/dev/null || sudo -n cat "$1" 2>/dev/null; } | awk '
        { line = $0; sub(/^[ \t]+/, "", line) }
        line ~ /^#/ { next }
        { i = index(line, "="); if (!i) next
          k = substr(line, 1, i - 1); v = substr(line, i + 1)
          sub(/[ \t]+$/, "", k); sub(/^[ \t]+/, "", v); sub(/[ \t\r]+$/, "", v) }
        k == ".godparticle" && v != "" { print v; found = 1; exit }
        tolower(k) ~ /godparticle|token|bearer/ && v != "" && fb == "" { fb = v }
        END { if (!found && fb != "") print fb }'
}

host="$LMCP_HOST"; port="$LMCP_PORT"; tok="$LMCP_TOKEN"
unit=$(pick_unit)
env=
[ -n "$unit" ] && env=$(systemctl show "$unit" -p Environment 2>/dev/null | tr ' ' '\n')
[ -z "$host" ] && host=$(printf '%s\n' "$env" | sed -n 's/^LMCP_HOST=//p'  | head -1)
[ -z "$port" ] && port=$(printf '%s\n' "$env" | sed -n 's/^LMCP_PORT=//p'  | head -1)
[ -z "$tok"  ] && tok=$(printf  '%s\n' "$env" | sed -n 's/^LMCP_TOKEN=//p' | head -1)
if [ -z "$tok" ]; then
    conf=$(printf '%s\n' "$env" | sed -n 's/^LMCP_CONF=//p' | head -1)
    [ -n "$conf" ] && tok=$(conf_token "$conf")
fi
[ -n "$host" ] || host=127.0.0.1
[ -n "$port" ] || port=8080

post() {
    curl -s -m 120 -X POST "http://${host}:${port}/mcp" \
      -H "Authorization: Bearer ${tok}" -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" -d "$1" | sed -n 's/^data: //p'
}

if [ "$tool" = "list" ] || [ "$tool" = "tools" ] || [ "$tool" = "--list" ]; then
    post '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | python3 -c '
import sys, json
raw = sys.stdin.read().strip()
if not raw: sys.stderr.write("lmcp-tool: empty response (unreachable or auth failed)\n"); sys.exit(1)
d = json.loads(raw.splitlines()[-1])
if "error" in d: sys.stderr.write("lmcp error: %s\n" % d["error"]); sys.exit(1)
res = d.get("result", {})
if sys.argv[1] == "1":
    # Vollstaendig, inklusive inputSchema - das braucht ein Modell, um die
    # Parameter richtig zu setzen. Die Prosa-Beschreibung allein reicht nicht.
    json.dump(res, sys.stdout, ensure_ascii=False)
    sys.stdout.write("\n")
else:
    for t in res.get("tools", []):
        desc = (t.get("description") or "").splitlines()[0] if t.get("description") else ""
        print("%-16s %s" % (t.get("name","?"), desc))
' "$json_out"
    exit $?
fi

# build the arguments JSON
if [ $# -eq 0 ]; then
    args='{}'
elif [ $# -eq 1 ] && [ "${1#\{}" != "$1" ]; then
    args="$1"                                   # single arg starting with '{' = raw JSON
else
    args=$(python3 -c 'import sys, json
args = {}
for a in sys.argv[1:]:
    if "=" in a:
        k, _, v = a.partition("=")
        args[k] = v
    elif a.startswith(("http://", "https://")):
        args["url"] = a          # bare URL -> url= (agents keep passing it positionally)
    else:
        args[a] = ""
print(json.dumps(args))' "$@") \
        || { echo "lmcp-tool: could not build args from key=value pairs" >&2; exit 2; }
fi

body=$(printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"%s","arguments":%s}}' "$tool" "$args")
post "$body" | python3 -c '
import sys, json
raw = sys.stdin.read().strip()
if not raw: sys.stderr.write("lmcp-tool: empty response (unreachable or auth failed)\n"); sys.exit(1)
d = json.loads(raw.splitlines()[-1])
if "error" in d:
    sys.stderr.write("lmcp error: %s\n" % d["error"])
    sys.stderr.write("hint: run  lmcp-tool list  to see valid tool names\n"); sys.exit(1)
r = d.get("result", {})
if sys.argv[1] == "1":
    # MCP-Semantik: der Inhalt ist auch bei isError massgeblich und gehoert
    # an das Modell durchgereicht. Rueckgabewert 0, sobald eine gueltige
    # Antwort vorliegt - 1 bleibt dem Transportfehler vorbehalten.
    json.dump(r, sys.stdout, ensure_ascii=False)
    sys.stdout.write("\n")
    sys.exit(0)
print("\n".join(c["text"] for c in r.get("content", []) if c.get("type") == "text"))
sys.exit(1 if r.get("isError") else 0)
' "$json_out"
