#!/bin/sh
# context7 - query library/framework documentation via the Context7 public
#           MCP server (https://mcp.context7.com/mcp).
#
# Context7 keeps up-to-date docs and code examples for thousands of libraries
# and frameworks and exposes them through an MCP (Model Context Protocol)
# server. This script speaks the JSON-RPC MCP protocol over HTTP:
#   1. initialize                  -> obtains an Mcp-Session-Id (optional)
#   2. notifications/initialized   -> no reply
#   3. tools/call                  -> runs a Context7 tool and prints text
#
# Context7 provides two tools:
#   * resolve-library-id  maps a library name to a Context7 ID (/org/project).
#   * query-docs          fetches docs + examples for a resolved library ID.
#
# Usage:
#   context7 resolve <libraryName> [query]
#       Search for a library and print the candidate Context7 IDs with their
#       description, snippet count, reputation and benchmark score.
#
#   context7 query <libraryId> <query>
#       Fetch documentation for an explicit library ID (format /org/project
#       or /org/project/version). The ID usually comes from `resolve`.
#
#   context7 docs <libraryName> <query>
#       Convenience: resolve the library, auto-pick the top-ranked match and
#       immediately query its documentation.
#
#   context7 --help
#
# Environment:
#   CONTEXT7_MCP_ENDPOINT  optional; default https://mcp.context7.com/mcp
#   CONTEXT7_API_KEY       optional; if set, sent as the X-Context7-API-Key
#                          request header (for higher rate limits / private
#                          docs). No extra headers are added when unset.
#
# Dependencies: curl, jq.

set -eu

usage() {
  sed -n '/^# Usage:/,/^# Dependencies:/p' "$0" | sed 's/^# \{0,1\}//'
}

if [ "$#" -lt 1 ]; then
  usage >&2
  exit 1
fi

CMD="$1"
shift

case "$CMD" in
  -h|--help|help)
    usage
    exit 0
    ;;
  resolve|query|docs)
    ;;
  *)
    echo "context7: unknown command '$CMD' (try --help)" >&2
    exit 1
    ;;
esac

# --- Validate argument counts per command. ---
case "$CMD" in
  resolve)
    if [ "$#" -lt 1 ]; then
      echo "context7: resolve requires <libraryName> [query]" >&2
      exit 1
    fi
    LIB_NAME="$1"
    QUERY="${2:-$1}"
    ;;
  query)
    if [ "$#" -lt 2 ]; then
      echo "context7: query requires <libraryId> <query>" >&2
      exit 1
    fi
    LIB_ID="$1"
    QUERY="$2"
    ;;
  docs)
    if [ "$#" -lt 2 ]; then
      echo "context7: docs requires <libraryName> <query>" >&2
      exit 1
    fi
    LIB_NAME="$1"
    QUERY="$2"
    ;;
esac

ENDPOINT="${CONTEXT7_MCP_ENDPOINT:-https://mcp.context7.com/mcp}"

# Optional auth header (single token, no spaces expected in an API key).
AUTH_ARGS=""
if [ -n "${CONTEXT7_API_KEY:-}" ]; then
  AUTH_ARGS="-H X-Context7-API-Key:${CONTEXT7_API_KEY}"
fi

# --- MCP handshake + tool call helper. ---
# Args: $1 = tool name, $2 = arguments JSON. Prints the parsed text content.
mcp_call() {
  TOOL="$1"
  ARGS="$2"

  INIT_HEADERS=$(mktemp)
  INIT_BODY=$(mktemp)
  curl -s -D "$INIT_HEADERS" -X POST "$ENDPOINT" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    $AUTH_ARGS \
    -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"context7","version":"1.0"}}}' \
    > "$INIT_BODY"

  SID=$(grep -i '^mcp-session-id:' "$INIT_HEADERS" | tr -d '\r' | awk '{print $2}')

  if [ -n "$SID" ]; then
    curl -s -X POST "$ENDPOINT" \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -H "Mcp-Session-Id: $SID" \
      $AUTH_ARGS \
      -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' > /dev/null
    SID_ARGS="-H Mcp-Session-Id:$SID"
  else
    SID_ARGS=""
  fi

  CALL_BODY=$(mktemp)
  curl -s -X POST "$ENDPOINT" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    $SID_ARGS \
    $AUTH_ARGS \
    -d "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"$TOOL\",\"arguments\":$ARGS}}" \
    > "$CALL_BODY"

  # Take the last "data:" SSE payload (or the whole body if plain JSON).
  DATA=$(grep -E '^data:[[:space:]]' "$CALL_BODY" | tail -n1 | sed 's/^data:[[:space:]]*//')
  if [ -z "$DATA" ]; then
    DATA=$(cat "$CALL_BODY")
  fi

  rm -f "$INIT_HEADERS" "$INIT_BODY" "$CALL_BODY"

  if [ -z "$DATA" ]; then
    echo "context7: no response from MCP server" >&2
    return 1
  fi

  # Surface protocol errors.
  ERROR_MSG=$(printf '%s' "$DATA" | jq -r 'if .error then (.error|tostring) else empty end')
  if [ -n "$ERROR_MSG" ]; then
    echo "context7 error: $ERROR_MSG" >&2
    return 1
  fi

  printf '%s' "$DATA" | jq -r '
    (.result.content // [])
    | map(select(.type == "text"))
    | map(.text)
    | .[]
  '
}

# --- Dispatch. ---
case "$CMD" in
  resolve)
    ARGS=$(jq -n --arg n "$LIB_NAME" --arg q "$QUERY" \
      '{libraryName:$n, query:$q}')
    mcp_call "resolve-library-id" "$ARGS"
    ;;
  query)
    ARGS=$(jq -n --arg id "$LIB_ID" --arg q "$QUERY" \
      '{libraryId:$id, query:$q}')
    mcp_call "query-docs" "$ARGS"
    ;;
  docs)
    RARGS=$(jq -n --arg n "$LIB_NAME" --arg q "$QUERY" \
      '{libraryName:$n, query:$q}')
    RESOLVE_OUT=$(mcp_call "resolve-library-id" "$RARGS") || exit 1
    # Auto-pick the first candidate library ID from the resolve output.
    TOP_ID=$(printf '%s\n' "$RESOLVE_OUT" \
      | grep -m1 -oE '/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+' || true)
    if [ -z "$TOP_ID" ]; then
      echo "context7: could not extract a library ID from resolve output:" >&2
      printf '%s\n' "$RESOLVE_OUT" >&2
      exit 1
    fi
    echo "context7: using library ID $TOP_ID (from resolve of '$LIB_NAME')" >&2
    QARGS=$(jq -n --arg id "$TOP_ID" --arg q "$QUERY" \
      '{libraryId:$id, query:$q}')
    mcp_call "query-docs" "$QARGS"
    ;;
esac

exit 0
