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:

  • Toolsecho (echoes the input text) and add (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 /mcp endpoint.
  • 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's serverUrl). Add URLs explicitly via BowireMcpOptions.AllowedServerUrls when 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 call bowire.allowlist.permit to 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