MCP Protocol
Bowire and the Model Context Protocol intersect in four distinct roles. They live in different packages with different CLI / DI entry points; pick the one that matches what you want to build:
| Role | What it does | Package | Mount / Entry |
|---|---|---|---|
| 1. MCP client | Bowire connects to any MCP server URL and surfaces its tools / resources / prompts. | Kuestenlogik.Bowire.Protocol.Mcp |
bowire --url http://…/mcp |
| 2. MCP adapter | Bowire wraps every API method it can discover (gRPC, REST, SignalR, …) as an MCP tool so AI agents can call them. Opt-in. | Kuestenlogik.Bowire.Protocol.Mcp |
.WithMcpAdapter() (embedded) or --enable-mcp-adapter (standalone) |
| 3. Bowire as MCP server — HTTP | Bowire exposes itself (its discovery / invoke / record / mock / replay surface) as an MCP server over HTTP so AI agents can drive Bowire. | Kuestenlogik.Bowire.Mcp |
services.AddBowireMcp() + endpoints.MapBowireMcp() |
| 4. Bowire as MCP server — stdio | Same toolset as role 3, but spoken over stdio for agents that prefer process-level transport (Claude Desktop, Cursor stdin/stdout). | Kuestenlogik.Bowire.Mcp |
bowire mcp serve |
Roles 1 and 2 are about other people's APIs (or your own gRPC/REST/SignalR APIs) flowing through Bowire. Roles 3 and 4 turn Bowire itself into something AI agents can drive — list discovered services, invoke methods, start/stop mocks, inspect recordings.
MCP Client — discovering an external MCP server
Standalone:
bowire --url http://localhost:5003/mcp
The URL you supply is the MCP message endpoint itself. The plugin uses the streamable HTTP transport: it POSTs JSON-RPC requests directly to that URL and reads the response (either application/json or text/event-stream framing). No /sse event stream is required.
After the handshake (initialize + notifications/initialized), Bowire calls tools/list, resources/list, and prompts/list and surfaces each non-empty category as its own service in the sidebar:
| Service | Methods | Invocation |
|---|---|---|
| Tools | one per discovered tool | tools/call with the tool arguments |
| Resources | one per resource (method name = URI) | resources/read |
| Prompts | one per prompt | prompts/get with the prompt arguments |
Tool input schemas (inputSchema, JSON Schema with type: "object") are mapped to the standard form-based UI: strings become text inputs, numbers become number fields, booleans become checkboxes, arrays become repeated fields, and nested objects become message fields. Required fields are marked with an asterisk.
Sample target
samples/Kuestenlogik.Bowire.Sample.Mcp is a combined sample — one project that both serves MCP over the official C# SDK's streamable-HTTP transport (message endpoint http://localhost:5190/mcp) and mounts the embedded workbench at /bowire. It exposes two tools:
- Tools —
echo(echoes the input text) andadd(adds two integers)
Because it plays both roles at once, you can reach it either way:
- Embedded — open http://localhost:5190/bowire; the bundled catalogue already seeds the Sources rail with this host's own
/mcpendpoint. - Separate — point a standalone Bowire client at the same server with
bowire --url mcp@….
dotnet run --project samples/Kuestenlogik.Bowire.Sample.Mcp
bowire --url mcp@http://localhost:5190/mcp
Transport notes
The plugin supports the modern MCP streamable HTTP transport (MCP 2025-03-26): a single endpoint that accepts POSTed JSON-RPC requests and returns either application/json or framed text/event-stream. The older SSE+POST split is not supported.
MCP Adapter — exposing Bowire as an MCP server
The adapter goes the other direction: it wraps every service Bowire can discover (gRPC, REST, SignalR) as an MCP tool. AI agents connect to Bowire as if it were an MCP server, list the tools, and invoke them — the adapter dispatches each call to the matching protocol plugin.
This is opt-in because it would otherwise expose any API the host happens to discover.
Embedded mode — WithMcpAdapter() chained on MapBowire()
using Kuestenlogik.Bowire;
using Kuestenlogik.Bowire.Protocol.Mcp;
app.MapBowire(options => options.Title = "My API")
.WithMcpAdapter("https://localhost:5005");
The .WithMcpAdapter() extension only exists when the Kuestenlogik.Bowire.Protocol.Mcp package is referenced — projects that don't depend on it cannot accidentally activate the adapter.
Standalone mode — --enable-mcp-adapter flag
bowire --url https://localhost:5005 --enable-mcp-adapter
Without the flag, bowire runs as an MCP client only. With it, the adapter is mapped at POST /mcp (the workbench itself mounts at / in standalone, so there's no /bowire prefix).
Adapter endpoints
| Endpoint | Description |
|---|---|
POST {basePath}/mcp |
MCP streamable HTTP transport (MCP 2025-03-26): a single endpoint, JSON-RPC 2.0 in, JSON out. Handles initialize, tools/list, tools/call, ping. |
{basePath} is empty in the standalone bowire CLI (workbench at site root → adapter at /mcp) and /bowire (or whatever MapBowire("/your/prefix") you passed) in embedded mode.
The legacy SSE+POST split (/sse event stream + /messages POST) from the older MCP 2024-11-05 spec is intentionally not supported here.
Agent configuration
Claude Desktop (claude_desktop_config.json), standalone CLI:
{
"mcpServers": {
"bowire": {
"url": "http://localhost:5080/mcp"
}
}
}
Embedded mode with the default prefix:
{
"mcpServers": {
"your-api": {
"url": "https://your-host/bowire/mcp"
}
}
}
Cursor uses the same mcpServers shape. Other MCP clients accept the message endpoint URL directly.
Tool naming
Each discovered method becomes one tool: {service-name-with-underscores}_{method}. For example, calculator.CalculatorService/Add becomes calculator_CalculatorService_Add. Tool input schemas are derived from the discovered BowireMessageInfo (protobuf for gRPC, CLR types for REST/SignalR).
Only unary methods are exposed — streaming methods don't fit MCP's request/response shape.
Security warning
Enabling the adapter lets any MCP client invoke any discovered API method. Don't use it on a server whose API surface should not be reachable from arbitrary local MCP clients. The adapter is intended for development-time AI integration, not production exposure.
Sample
Bowire.Samples/SimpleMcpAdapter is a gRPC CalculatorService with the adapter chained in:
app.MapBowire(options => options.Title = "Simple MCP Adapter")
.WithMcpAdapter("https://localhost:5005");
Run it on port 5005 and Bowire standalone can browse it both ways:
bowire --url https://localhost:5005 # gRPC reflection
bowire --url https://localhost:5005/bowire/mcp # MCP client (embedded sample keeps the /bowire prefix)
Bowire as an MCP server — controlling Bowire from an AI agent
Kuestenlogik.Bowire.Mcp is a separate package that goes the other way: it exposes the Bowire workbench itself — discovery, invocation, recording, mocking, replay — as an MCP server so an AI agent can drive Bowire end-to-end.
This is distinct from the MCP adapter above: the adapter wraps the discovered APIs (your gRPC / REST / SignalR methods); the MCP server wraps Bowire's own tools (bowire.discover, bowire.invoke, bowire.record, bowire.mock.start, bowire.recordings.list, …). An agent uses it to ask Bowire "what's running at this URL, invoke method X with payload Y, capture the response, mock the next call, replay it".
Two transports ship in the box; pick the one your agent prefers.
Role 3 — HTTP transport (embedded)
Mount it next to your existing Bowire endpoint:
using Kuestenlogik.Bowire;
using Kuestenlogik.Bowire.Mcp;
builder.Services.AddBowire();
builder.Services.AddBowireMcp(opts =>
{
// Without an allowlist, all server URLs are reachable. In production
// host code you almost always want to constrain this.
opts.AllowedServerUrls.Add("https://my-trusted-api.example.com");
});
var app = builder.Build();
app.MapBowire(); // workbench UI + REST/gRPC discovery
app.MapBowireMcp(); // MCP server for AI agents
AddBowireMcp() registers the tools and a singleton BowireMockHandleRegistry (for the start-mock / stop-mock tools). MapBowireMcp() attaches the streamable-HTTP endpoint. Default URL: POST /mcp next to your Bowire workbench. Allowlist enforcement lives in BowireMcpOptions.AllowedServerUrls — empty means "trust any URL the agent asks about", which is convenient for local dev but should be tightened in production hosts.
Role 4 — stdio transport (bowire mcp serve)
For agents that prefer process-level transport (Claude Desktop's command: flavour, Cursor's stdio mode), invoke the CLI:
bowire mcp serve --allow-arbitrary-urls
Same toolset, JSON-RPC over stdin/stdout. The process exits when the agent closes the stream. --allow-arbitrary-urls is the stdio equivalent of leaving AllowedServerUrls empty.
Claude Desktop config:
{
"mcpServers": {
"bowire": {
"command": "bowire",
"args": ["mcp", "serve", "--allow-arbitrary-urls"]
}
}
}
Standing the same server up over HTTP (--bind http)
bowire mcp serve speaks stdio by default. --bind http runs the identical
toolset behind a Kestrel of its own, for agents that connect over the network
rather than over a pipe:
bowire mcp serve --bind http --token "$MCP_SECRET"
The endpoint is POST /mcp on port 5081 — no /bowire/ prefix, because the prefix follows the host's idea of where Bowire lives and here Bowire is the host. --token requires
Authorization: Bearer <secret> on every inbound request.
Serve it over TLS whenever it leaves the machine. --token puts a bearer
token on the wire, and a bearer token over plaintext is readable by anything
on the path. There is no Bowire flag for this — the listener takes its address
from ASP.NET's own configuration, exactly like the workbench:
ASPNETCORE_URLS=https://localhost:5443 bowire mcp serve --bind http --token "$MCP_SECRET"
or Kestrel:Endpoints with Kestrel:Certificates in appsettings.json for a
real certificate. Leave --port off when you do: it is a command-line
argument, so it outranks the environment and would give you the port in
plaintext instead of the endpoint you configured. Bowire logs a line when that
happens rather than doing it quietly.
Tool surface (roles 3 + 4)
Both transports expose the same toolset. Top-level tools include:
| Tool | Purpose |
|---|---|
bowire.discover |
List services + methods at a target URL via the appropriate protocol plugin. |
bowire.invoke |
Call a unary method with a JSON payload. |
bowire.subscribe |
Sample a streaming method for a bounded window and return collected frames. |
bowire.env.list / bowire.env.get |
Read environments stored under ~/.bowire/environments.json. |
bowire.recordings.list / bowire.recording.get |
Browse captured recordings. |
bowire.mock.start / bowire.mock.stop / bowire.mock.list |
Spin up an in-process mock server from a recording, stop it, list active handles. Mutators run behind a two-step confirmation gate (--no-confirm to disable). |
bowire.har.import |
Convert a HAR 1.2 trace into a Bowire recording — optionally writes it to disk for use with bowire.mock.start. |
bowire.assert |
Append a Newman-style assertion ({ path, op, expected }) onto a step inside a recording. Ops: eq, ne, gt, gte, lt, lte, contains, matches, exists, notexists, type. |
bowire.allowlist.show / bowire.allowlist.permit |
Diagnose the active URL allowlist + extend it at runtime (the latter also persists to ~/.bowire/typed-urls.json). |
bowire.lint |
Discover a URL and run the design-time linter over the API surface — the same rules as bowire lint and the Lint rail, honouring .bowire/rules.json. |
bowire.contract.matrix |
Roll the stored contract-verification results up into the consumer × provider matrix (see contract testing). Local-only: it reads what bowire contract verify stored and never contacts a provider. |
bowire.report.rollup |
Roll the reports Bowire writes (lint, contract, benchmark, scan, test) up into one row per service across a portfolio — see report rollup. Local-only: it reads existing files and never calls a service. |
The full tool list is generated from the discovered BowireMcpTools class via the ModelContextProtocol C# SDK — call tools/list on the running server to see the current schemas.
Resource surface (roles 3 + 4)
Tools do things; resources are things an agent can read. Call resources/list on the running server for the live set.
| Resource | What it holds |
|---|---|
bowire://workspaces |
Every workspace this identity has — id, name, gitNative, storageRoot. The index the workspace-scoped resources are addressed through. |
bowire://workspaces/{workspaceId}/collections |
Saved request collections in that workspace. |
bowire://workspaces/{workspaceId}/recordings |
Recordings captured in that workspace. |
bowire://workspaces/{workspaceId}/flows |
Visual flows saved in that workspace. |
bowire://collections, bowire://recordings, bowire://flows |
The workspace-less files, for a host that never adopted workspaces. |
bowire://collections/{id}, bowire://recordings/{id}, bowire://flows/{id} |
One item by id, from the workspace-less files. |
bowire://plugins |
Sibling plugins in the host's plugin directory. |
Why a workspace has to be named
An agent asking for bowire://flows sends a URI and nothing else. The workbench sends ?workspaceId= on every call it makes, but a resource read carries no such context, and Bowire has no server-side notion of "the workspace that is currently open" — the active selection lives in the browser, deliberately, because two windows may honestly be looking at different workspaces.
So the workspace is named in the URI, and bowire://workspaces exists to make the ids discoverable. That index only became possible once the workspace list itself moved out of the browser and into the identity's storage slot; before that, nothing on the server knew a workspace existed.
The consequence worth knowing: on a host that uses workspaces, the workspace-less resources are the pre-workspace files — usually stale, often empty. They still answer, because a host that never adopted workspaces has its data exactly there and breaking that would trade one wrong answer for another. Read bowire://workspaces first; an empty list means the workspace-less resources are the right ones.
An unknown workspace id answers with a message naming the index rather than with an empty document. "No data" and "wrong id" are indistinguishable to whoever reads the result, and that ambiguity is what this surface previously got wrong.
Security warning (roles 3 + 4)
The MCP server lets an agent drive any URL that's allowlisted (or any URL at all if no allowlist is configured). Treat it the same way you'd treat a CLI with shell access: only run it against trusted target systems, and prefer the AllowedServerUrls allowlist for non-localhost production hosts.
Three CLI flags layer the allowlist:
- (default) — the allowlist seeds from
~/.bowire/environments.json(every saved environment'sserverUrl). Add URLs explicitly viaBowireMcpOptions.AllowedServerUrlswhen scripting. --allow-invoke— also seed from~/.bowire/typed-urls.json, which tracks every URL the user has typed into the workbench. Widens the allowlist without dropping the gate; agents can also callbowire.allowlist.permitto append the URL the user just typed.--allow-arbitrary-urls— drop the gate entirely. Sandboxed CI only.
--no-confirm disables the two-step pending-confirmation pattern on mutator tools (bowire.mock.start, bowire.record.start). Without it, the first call returns { pending: true, confirmationToken, plan } and the agent must echo the token (or pass confirm: true) on a second call to execute. Pending tokens expire after five minutes.
See also: Quick Start, Plugin System