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 |
Starting Bowire when Bowire is already running
bowire with no subcommand starts the workbench. If the port it wants is
already serving another Bowire, it does not start a second one: it prints
the running instance's URL, opens a browser at it (unless --no-browser), and
exits 0. Nothing went wrong -- you asked for a workbench and there is one, so
a script that starts Bowire idempotently keeps working.
Flags that shape the server cannot be applied to an instance that is already
up. If the second invocation carries any of --url, --enable-mcp-adapter,
--title, --disable-plugin, --plugin-dir, --map-basemap or
--auto-create-initial-workspace, they are named on stderr rather than
silently dropped. Stop the running instance first if you need them.
If the port is held by something that is not Bowire, you get one line
naming the port and the two ways out (--port <number>, or --port 0 together
with --port-file), and exit 1. Either way there is no stack trace.
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