107 lines
3.4 KiB
Bash
107 lines
3.4 KiB
Bash
#!/bin/sh
|
|||
|
|
# websearch - simple web search for Bantam via Exa's MCP server (Streamable HTTP).
|
||
|
|
#
|
||
|
|
# Talks to https://mcp.exa.ai/mcp using the JSON-RPC MCP protocol:
|
||
|
|
# 1. initialize -> obtains an Mcp-Session-Id from response headers
|
||
|
|
# 2. notifications/initialized -> no reply
|
||
|
|
# 3. tools/call (web_search_exa) -> prints the formatted result text
|
||
|
|
#
|
||
|
|
# Usage:
|
||
|
|
# websearch "your query"
|
||
|
|
# websearch "your query" [num_results]
|
||
|
|
#
|
||
|
|
# Environment:
|
||
|
|
# EXA_MCP_ENDPOINT optional; default https://mcp.exa.ai/mcp
|
||
|
|
# EXA_API_KEY optional; if set, sent as an URL query parameter (?api_key=)
|
||
|
|
# so that NO extra HTTP headers are added beyond Content-Type.
|
||
|
|
#
|
||
|
|
# Dependencies: curl, jq.
|
||
|
|
|
||
|
|
set -eu
|
||
|
|
|
||
|
|
if [ "$#" -lt 1 ]; then
|
||
|
|
echo "Usage: websearch \"query\" [num_results]" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
QUERY="$1"
|
||
|
|
NUM="${2:-5}"
|
||
|
|
|
||
|
|
ENDPOINT="${EXA_MCP_ENDPOINT:-https://mcp.exa.ai/mcp}"
|
||
|
|
|
||
|
|
# Append API key as a query parameter (no extra headers).
|
||
|
|
if [ -n "${EXA_API_KEY:-}" ]; then
|
||
|
|
URL="${ENDPOINT}?api_key=${EXA_API_KEY}"
|
||
|
|
else
|
||
|
|
URL="${ENDPOINT}"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Build the tools/call arguments JSON safely (proper query escaping).
|
||
|
|
ARGS=$(jq -n --arg q "$QUERY" --argjson n "$NUM" \
|
||
|
|
'{query:$q, numResults:$n}')
|
||
|
|
|
||
|
|
# --- Step 1: initialize, capture headers (for Mcp-Session-Id) and body. ---
|
||
|
|
INIT_HEADERS=$(mktemp)
|
||
|
|
INIT_BODY=$(mktemp)
|
||
|
|
curl -s -D "$INIT_HEADERS" -X POST "$URL" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-H "Accept: application/json, text/event-stream" \
|
||
|
|
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"websearch","version":"1.0"}}}' \
|
||
|
|
> "$INIT_BODY"
|
||
|
|
|
||
|
|
SID=$(grep -i '^mcp-session-id:' "$INIT_HEADERS" | tr -d '\r' | awk '{print $2}')
|
||
|
|
if [ -z "$SID" ]; then
|
||
|
|
echo "websearch: failed to obtain MCP session id" >&2
|
||
|
|
cat "$INIT_BODY" >&2
|
||
|
|
rm -f "$INIT_HEADERS" "$INIT_BODY"
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- Step 2: send initialized notification (fire and forget). ---
|
||
|
|
curl -s -X POST "$URL" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-H "Accept: application/json, text/event-stream" \
|
||
|
|
-H "Mcp-Session-Id: $SID" \
|
||
|
|
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}' > /dev/null
|
||
|
|
|
||
|
|
# --- Step 3: call web_search_exa and extract the text result. ---
|
||
|
|
CALL_BODY=$(mktemp)
|
||
|
|
curl -s -X POST "$URL" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-H "Accept: application/json, text/event-stream" \
|
||
|
|
-H "Mcp-Session-Id: $SID" \
|
||
|
|
-d "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"web_search_exa\",\"arguments\":$ARGS}}" \
|
||
|
|
> "$CALL_BODY"
|
||
|
|
|
||
|
|
# Parse the response: take the last "data:" SSE payload (or the whole body
|
||
|
|
# if it is plain JSON) and print the text blocks from the result.
|
||
|
|
DATA=$(grep -E '^data:[[:space:]]' "$CALL_BODY" | tail -n1 | sed 's/^data:[[:space:]]*//')
|
||
|
|
if [ -z "$DATA" ]; then
|
||
|
|
DATA=$(cat "$CALL_BODY")
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ -z "$DATA" ]; then
|
||
|
|
echo "websearch: no response from MCP server" >&2
|
||
|
|
rm -f "$INIT_HEADERS" "$INIT_BODY" "$CALL_BODY"
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Surface server/protocol errors, then emit text content.
|
||
|
|
ERROR_MSG=$(printf '%s' "$DATA" | jq -r 'if .error then (.error|tostring) else empty end')
|
||
|
|
if [ -n "$ERROR_MSG" ]; then
|
||
|
|
echo "websearch error: $ERROR_MSG" >&2
|
||
|
|
rm -f "$INIT_HEADERS" "$INIT_BODY" "$CALL_BODY"
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
printf '%s' "$DATA" | jq -r '
|
||
|
|
(.result.content // [])
|
||
|
|
| map(select(.type == "text"))
|
||
|
|
| map(.text)
|
||
|
|
| .[]
|
||
|
|
'
|
||
|
|
|
||
|
|
rm -f "$INIT_HEADERS" "$INIT_BODY" "$CALL_BODY"
|
||
|
|
|
||
|
|
exit 0
|