CLI Mode
A command-line interface for scripting, automation, and quick exploration without opening the browser UI. Commands follow the shape bowire <verb> --url <server> [args].
Commands
Discover Services (all protocols)
bowire discover --url https://api.example.com
bowire discover --url https://api.example.com -v # verbose: list methods too
bowire discover --url rest@https://api.example.com # pin one plugin, skip the rest
Probes the URL with every loaded protocol plugin in parallel and prints what each one found — or why it didn't:
petstore.Pets (7 methods, via rest)
12 plugins probed · 2 failed
gRPC error 2011 ms connection refused
MQTT timeout 8003 ms probe exceeded the 8 s ceiling
GraphQL empty 311 ms returned no services
REST ok 142 ms 1 service
…
The attempt table always prints — the whole point of running the command is
the diagnosis, so there is no collapsed / expanded tradeoff the way there is
in the UI. Exit code is 0 when at least one service was found and 1
otherwise, so CI can gate on it.
This is the same BowireDiscoveryProbe fan-out the /bowire/api/services
endpoint and the bowire.discover MCP tool use, so the terminal and the
workbench can never disagree about what happened. See
Auto-discovery → When discovery finds nothing
for the outcome vocabulary.
List Services
bowire list --url https://server:443
bowire list --url https://server:443 -v # verbose: show methods
Lists all discovered services via gRPC server reflection only — it talks
straight to GrpcReflectionClient and never touches the multi-protocol
fan-out. That is deliberate: scripts depend on its output shape. Reach for
bowire discover when you want every plugin's verdict. With -v, shows each
method with its call type.
Describe a Service or Method
bowire describe --url https://server:443 weather.WeatherService
bowire describe --url https://server:443 weather.WeatherService/GetCurrentWeather
Shows method signatures and input/output schemas. For gRPC, this includes protobuf field numbers and types.
Invoke a Method
# Unary call
bowire call --url https://server:443 \
weather.WeatherService/GetCurrentWeather -d '{"city":"Berlin"}'
# Server streaming (prints each message as it arrives)
bowire call --url https://server:443 \
weather.WeatherService/SubscribeWeather -d '{"city":"Tokyo"}'
# Compact JSON output (one line per message, pipe-friendly)
bowire call --url https://server:443 \
weather.WeatherService/SubscribeWeather -d '{"city":"Tokyo"}' --compact
# Read request body from file
bowire call --url https://server:443 \
weather.WeatherService/GetCurrentWeather -d @request.json
# With metadata headers
bowire call --url https://server:443 \
weather.WeatherService/GetCurrentWeather -d '{}' -H "authorization: Bearer token123"
# Plaintext (no TLS)
bowire call --url http://server:5000 -plaintext \
weather.WeatherService/GetCurrentWeather -d '{}'
Any protocol, not just gRPC
call invokes through whichever protocol plugin owns the URL, so every
request the workbench can send has a terminal equivalent. Pin the plugin
either with the protocol@url hint form (the same one discover and the
sidebar accept) or with an explicit --protocol:
# REST — the hint form
bowire call --url rest@https://petstore3.swagger.io/api/v3 \
pet/getPetById -d '{"petId":1}'
# GraphQL
bowire call --url graphql@https://countries.trevorblades.com/graphql \
Query/country -d '{"query":"query($c:ID!){country(code:$c){name}}","variables":{"c":"DE"}}'
# MQTT — the broker address carries no scheme, so name the plugin instead
bowire call --url broker.example.com:1883 --protocol mqtt \
sensors/sensors/temperature -d '{"celsius":21.5}'
Without a hint or a --protocol, call assumes gRPC and takes a fast path
that skips loading the plugin registry — existing gRPC scripts pay nothing
for the widening. With one, the URL is probed by that plugin (the same
BowireDiscoveryProbe fan-out discover uses) before the invocation, so a
wrong URL reports which plugin said what rather than a bare transport error.
All first-party protocol plugins ship inside the bowire tool, so nothing
needs installing for the protocols listed above. A plugin that is not
loaded produces an error naming the ones that are, plus the
bowire plugin install Kuestenlogik.Bowire.Protocol.<Name> line that would
add it.
Following a stream
# SSE — one JSON document per event until Ctrl+C
bowire call --url sse@https://stream.example.com 'SSE Endpoints//events' --stream
# WebSocket — send one frame, then print what comes back
bowire call --url websocket@https://echo.example.com 'WebSocket endpoints//chat' \
-d '{"text":"hello"}' --stream
--stream routes the call through the plugin's streaming entry point.
gRPC server-streaming methods are detected automatically and don't need
it; every other protocol does, because only the caller knows whether an
SSE / WebSocket / broker target should be read once or followed. A plugin
with no streaming support answers with a one-line explanation rather than
a stack trace.
Ctrl+C is the normal way to stop a subscription and exits 0. A stream
that ends without ever delivering a frame exits 1 with an explanation
instead — in a pipeline, "printed nothing, exited 0" is indistinguishable
from success.
Variables
-d, -H and --url all run through the same {{name}} / ${name}
resolver bowire test uses, so one recorded request works against several
environments:
bowire call --url rest@https://{{host}}/api/v3 pet/getPetById \
-d '{"petId":{{petId}}}' \
--var host=petstore3.swagger.io --var petId=1
# Or from a dotenv-style file; --var repeats win over file entries
bowire call --url rest@https://{{host}}/api/v3 pet/getPetById \
-d '{"petId":1}' --env-file staging.env
The built-in {{uuid}} / {{now}} / {{timestamp}} / {{random}}
variables resolve without being declared. Unknown names are left intact so
a typo shows up in the request rather than as an empty value.
Copy a request out of the workbench
The workbench's Code tab (and the response pane's Copy ▾ dropdown)
offers Bowire CLI alongside curl / grpcurl / wscat / fetch: it renders
the request you are looking at as a runnable bowire call … line, with a
shell-flavour toggle (bash/zsh or PowerShell) and a Keep {{variables}}
pill that leaves the refs in place and pairs them with --var.
Two things it will not do. It never resolves {{secret.*}} or
{{keyring.*}} into the copied text — those stay as refs, with a note
saying so. And for the auth types whose token is fetched at request time
(session, OAuth client-credentials / auth-code, custom token, signed JWT)
it emits a # note instead of an Authorization header, because the
exchange happens in the browser and no static header can stand in for it.
A golden fixture parses every command shape that generator can emit through this command's real grammar on each build, so the copied line cannot quietly stop being runnable.
Options
| Option | Applies to | Description |
|---|---|---|
--url <url> |
all | Target server URL (required). discover and call also accept the protocol@url hint form |
--protocol <id> |
call |
Protocol plugin to invoke through (grpc / rest / graphql / mqtt / …). Overrides a protocol@url prefix |
--stream |
call |
Consume the method as a stream: one JSON document per frame until the stream ends or Ctrl+C |
-d, --data <json> |
call |
Request body (JSON string or @filename). Repeatable — one frame per repeat for client-streaming |
-H <key:value> |
call |
Add metadata header (repeatable) |
--var, --env <K=V> |
call |
Variable for the {{name}} / ${name} resolver (repeatable) |
--env-file <path> |
call |
dotenv-style KEY=VALUE file for the resolver (repeatable; --var wins) |
--compact |
call |
One-line JSON output for piping |
-plaintext |
all | Use plaintext (no TLS) |
-v, --verbose |
list, discover |
Verbose output |
Exit Codes
| Code | Meaning |
|---|---|
0 |
OK -- call succeeded (for discover: at least one service found) |
1 |
Connection or runtime error (for discover: no service found) |
2 |
Protocol-level error (a gRPC status, a 4xx/5xx HTTP response, an unknown --protocol, a URL no plugin recognised) or invalid usage |
CI/CD Usage
CLI mode is designed for automated pipelines:
# Health check in CI
bowire call --url https://staging:443 \
health.HealthService/Check -d '{}' --compact \
|| exit 1
The --compact flag produces one-line JSON output suitable for piping to jq or other tools.
Argument validation
Common mistakes are caught at parse time -- before any server binds a
socket -- and reported on stderr with a one-line pointer at the
relevant --help:
--port(and--api-port) must be in1..65535.--recording <path>(and the positionalbowire mock <file>form) must point at an existing file.--chaosis parsed eagerly, so a malformed spec such as--chaos bogusfails immediately instead of mid-boot.
$ bowire mock --port 70000
✗ --port: port must be between 1 and 65535 (got 70000).
Run 'bowire mock --help' for usage.
Error output is colourised on an interactive terminal and plain when redirected (pipes, CI logs), so captured output stays ANSI-free.
Tab completion
Bowire answers the standard dotnet-suggest
completion protocol, so bash / zsh / PowerShell users get completion for
sub-commands, options, and enumerated values (e.g. fuzz --payloads
offers sqli / xss / pathtrav / cmdinj; --map-basemap offers
osm / satellite / demotiles / none).
One-time setup:
# 1. Install the completion broker (once per machine)
dotnet tool install -g dotnet-suggest
# 2. Add the shell shim to your profile, then reload:
# bash/zsh -> https://github.com/dotnet/command-line-api/blob/main/src/System.CommandLine.Suggest/dotnet-suggest-shim.bash
# PowerShell:
# Add-Content $PROFILE (dotnet-suggest script powershell)
# 3. Register the bowire executable with the broker
dotnet-suggest register --command-path "$(command -v bowire)"
After reloading the shell, bowire mo<Tab> completes to mock, and
bowire fuzz --payloads <Tab> lists the payload categories.
See also: Setup -- Standalone