From b298c534061dc3ada0ed69644988bd1dc7cf303b Mon Sep 17 00:00:00 2001 From: Luxferre Date: Mon, 17 Aug 2026 09:53:02 +0300 Subject: [PATCH] added some extra sample tool scripts --- README.md | 47 ++++++++++++ extras/weather | 190 +++++++++++++++++++++++++++++++++++++++++++++++ extras/websearch | 106 ++++++++++++++++++++++++++ 3 files changed, 343 insertions(+) create mode 100755 extras/weather create mode 100755 extras/websearch diff --git a/README.md b/README.md index d684e17..11ad42e 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,53 @@ MicroBantam (`mb`) is a compressed Perl 5 reference implementation of the same a - `model.cfg`, `system.txt` — shared configuration and system prompt - `README.md` — this document +## Extra tools + +The `extras/` directory contains small, dependency-light shell scripts that extend Bantam without changing its core. Because Bantam's only built-in tools are `shell_exec` and `run_subagent`, these helpers can be invoked directly by the agent through `shell_exec` to give it real-world capabilities (web search, live weather) that the base model alone does not have. They are plain `/bin/sh` scripts depending only on `curl` (and `jq` where noted), so the agent can discover and run them just like any other command. + +### `extras/websearch` + +A simple web search tool for Bantam built on [Exa's MCP server](https://mcp.exa.ai/mcp) +(Streamable HTTP). It speaks the JSON-RPC MCP protocol (`initialize` -> +`notifications/initialized` -> `tools/call` with `web_search_exa`) and prints +the formatted result text. Usage: + +```sh +extras/websearch "your query" # default 5 results +extras/websearch "your query" 10 # custom number of results +``` + +Environment: + +- `EXA_MCP_ENDPOINT` — endpoint URL (default `https://mcp.exa.ai/mcp`). +- `EXA_API_KEY` — optional; when set, it is passed as an `?api_key=` query + parameter so no extra HTTP headers are added beyond `Content-Type`. + +Dependencies: `curl`, `jq`. + +### `extras/weather` + +A wrapper around the [wttr.in](https://github.com/chubin/wttr.in) weather API +that queries current conditions and forecasts for any location. It supports the +graphical ANSI terminal view (default), one-line presets (`1`-`4`), custom +`%`-notation formats, and the rich JSON document (`?format=j1`). Usage: + +```sh +extras/weather # auto-detect location from request IP +extras/weather London # graphical report +extras/weather -f 3 "New York" # one-line preset +extras/weather -f "%l: %c %t" Paris # custom one-line format +extras/weather -u u -L de Berlin # USCS units, German output +extras/weather -j Tokyo # raw JSON (pretty-printed via jq) +``` + +Options include `-l/--location`, `-u/--units` (`m`/`u`/`M`), `-L/--lang`, +`-f/--format`, `-j/--json`, `-0`/`-1`/`-2` (view depth), `-q`/`--quiet`, +`-A` (force ANSI) and `-h/--help`. Environment overrides: `WTTRAPI` (default +`https://wttr.in`) and `WEATHER_TIMEOUT` (default `20`s). + +Dependencies: `curl`, `jq` (only required for the JSON format). + ## FAQ ### Does Bantam support `AGENTS.md` etc? diff --git a/extras/weather b/extras/weather new file mode 100755 index 0000000..f856148 --- /dev/null +++ b/extras/weather @@ -0,0 +1,190 @@ +#!/bin/sh +# weather - query current weather and forecasts for any location via wttr.in. +# +# wttr.in (https://github.com/chubin/wttr.in) is a console-oriented weather +# service backed by World Weather Online data. It supports several output +# formats: a graphical ANSI view for terminals (the default), one-line text +# formats (built-in presets 1-4 or a custom %-notation string), a rich JSON +# document (?format=j1 / j2) for scripts and APIs, plus PNG / HTML / +# Prometheus metrics. +# +# This script is a thin, friendly wrapper around the wttr.in HTTP API. It +# builds the request URL from the supplied location and options, fetches the +# data with curl and (when JSON is requested) pretty-prints it with jq. +# +# Usage: +# weather [options] [location] +# +# Options: +# -l, --location LOC Location: city, airport code, domain, IP, +# "lat,lon" coordinates, or "~Name" for a custom label. +# Default: auto-detect from the request IP. +# -u, --units U Unit system: m (metric, default), u (USCS/imperial), +# M (metric with wind speed in m/s). +# -L, --lang LANG Output language code, e.g. de, fr, ru, zh-cn. +# -f, --format FMT Output format: +# j1 -> rich JSON document (default for --json) +# j2 -> JSON document, imperial units +# 1-4 -> built-in one-line presets +# "%..." -> custom one-line %-notation format +# Omit to get the default graphical terminal view. +# -j, --json Alias for --format j1 (emit JSON). +# -0 Current weather only. +# -1 Current weather + today's forecast. +# -2 Current weather + today's + tomorrow's forecast. +# -q, --quiet Quiet: no "Weather report" header / city name. +# -A Force ANSI output (ignore User-Agent detection). +# -h, --help Show this help text. +# +# Environment: +# WTTRAPI Base endpoint. Default: https://wttr.in +# WEATHER_TIMEOUT curl connect/read timeout in seconds. Default: 20 +# +# Dependencies: curl, jq (jq only needed for the JSON format). +# +# Examples: +# weather +# weather London +# weather -u u -L de Berlin +# weather -f 3 "New York" +# weather -f "%l: %c %t (feels %f), wind %w %m" Paris +# weather -j Tokyo + +set -eu + +WTTRAPI="${WTTRAPI:-https://wttr.in}" +TIMEOUT="${WEATHER_TIMEOUT:-20}" + +# Single-letter options (view/quiet/units) concatenated into one run. +SHORT="" +LANG_CODE="" +FORMAT="" +LOCATION="" + +usage() { + sed -n '/^# Usage:/,/^# Examples:/p' "$0" | sed 's/^# \{0,1\}//' +} + +# --- Parse arguments. --- +while [ "$#" -gt 0 ]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + -l|--location) + [ "$#" -ge 2 ] || { echo "weather: $1 requires an argument" >&2; exit 1; } + LOCATION="$2" + shift 2 + ;; + -u|--units) + [ "$#" -ge 2 ] || { echo "weather: $1 requires an argument" >&2; exit 1; } + case "$2" in + m|u|M) SHORT="${SHORT}$2" ;; + *) echo "weather: unknown unit '$2' (use m, u or M)" >&2; exit 1 ;; + esac + shift 2 + ;; + -L|--lang) + [ "$#" -ge 2 ] || { echo "weather: $1 requires an argument" >&2; exit 1; } + LANG_CODE="$2" + shift 2 + ;; + -f|--format) + [ "$#" -ge 2 ] || { echo "weather: $1 requires an argument" >&2; exit 1; } + FORMAT="$2" + shift 2 + ;; + -j|--json) + FORMAT="j1" + shift + ;; + -0|-1|-2|-q|-A) + SHORT="${SHORT}${1#-}" + shift + ;; + --) + shift + LOCATION="${LOCATION:+$LOCATION }$(printf '%s' "$*" | sed 's/^ //')" + break + ;; + -*) + echo "weather: unknown option '$1' (try --help)" >&2 + exit 1 + ;; + *) + # Positional: append to location (preserves multi-word locations). + LOCATION="${LOCATION:+$LOCATION }$1" + shift + ;; + esac +done + +# --- Assemble the query string. --- +# wttr.in accepts a leading run of single-letter options, then '&'-joined +# long options (key=value). We build both parts and join them. +QUERY="$SHORT" + +# Long options: format and lang. +LONG="" +if [ -n "$FORMAT" ]; then + case "$FORMAT" in + j1|j2) + LONG="${LONG:+$LONG&}format=${FORMAT}" + ;; + [1-4]) + LONG="${LONG:+$LONG&}format=${FORMAT}" + ;; + *) + # Custom %-notation one-line format -> URL-encode it. + ENC=$(printf '%s' "$FORMAT" | jq -sRr @uri) + LONG="${LONG:+$LONG&}format=${ENC}" + ;; + esac +fi +if [ -n "$LANG_CODE" ]; then + LONG="${LONG:+$LONG&}lang=$(printf '%s' "$LANG_CODE" | jq -sRr @uri)" +fi + +# Combine short + long parts into a single query string. +if [ -n "$QUERY" ] && [ -n "$LONG" ]; then + QUERY="${QUERY}&${LONG}" +elif [ -z "$QUERY" ] && [ -n "$LONG" ]; then + QUERY="$LONG" +fi + +# URL-encode the location (spaces -> %20, etc.). Empty means auto-detect. +if [ -n "$LOCATION" ]; then + LOC_ENC=$(printf '%s' "$LOCATION" | jq -sRr @uri) +else + LOC_ENC="" +fi + +URL="${WTTRAPI}/${LOC_ENC}" +if [ -n "$QUERY" ]; then + URL="${URL}?${QUERY}" +fi + +# --- Fetch. --- +# wttr.in returns the graphical ANSI art whenever the request looks like it +# comes from a console client (curl's default User-Agent), and HTML for +# browsers. So we keep curl's default identity. The "-A" flag above is a +# wttr.in URL option ("force ANSI") and is already part of the query string. + +RESP=$(curl -s -L --max-time "$TIMEOUT" "$URL") || { + echo "weather: failed to reach $WTTRAPI" >&2 + exit 1 +} + +# --- Emit. --- +if [ "$FORMAT" = "j1" ] || [ "$FORMAT" = "j2" ]; then + if command -v jq >/dev/null 2>&1; then + printf '%s\n' "$RESP" | jq . 2>/dev/null || printf '%s\n' "$RESP" + else + printf '%s\n' "$RESP" + fi +else + printf '%s\n' "$RESP" +fi + +exit 0 diff --git a/extras/websearch b/extras/websearch new file mode 100755 index 0000000..32a4cd4 --- /dev/null +++ b/extras/websearch @@ -0,0 +1,106 @@ +#!/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