Architecture
Bowire is a modular API browser built on ASP.NET. The core library provides the UI shell and a protocol-agnostic dispatch layer. Every protocol plugin — gRPC, REST, GraphQL, SignalR, WebSocket, SSE, MQTT, Socket.IO, OData, MCP, plus your own — handles the specifics behind a common IBowireProtocol contract.
High-level shape
graph TB
subgraph "Browser"
UI["Bowire UI<br/>HTML/CSS/JS"]
end
subgraph "ASP.NET host"
API["Bowire API endpoints"]
REG["Protocol registry"]
API --> REG
REG --> GRPC["gRPC"]
REG --> REST["REST"]
REG --> GQL["GraphQL"]
REG --> SIG["SignalR"]
REG --> WS["WebSocket"]
REG --> SSE["SSE"]
REG --> MQTT["MQTT"]
REG --> SIO["Socket.IO"]
REG --> OD["OData"]
REG --> MCP["MCP"]
end
subgraph "Target services"
TGT["Any protocol<br/>the plugins speak"]
end
UI -->|HTTP / SSE| API
GRPC --> TGT
REST --> TGT
GQL --> TGT
SIG --> TGT
WS --> TGT
SSE --> TGT
MQTT --> TGT
SIO --> TGT
OD --> TGT
MCP -.->|wraps| REG
Request flow
When the user invokes a method, the request crosses three layers:
sequenceDiagram
Browser->>API: POST /bowire/api/invoke
API->>Registry: GetProtocol(id)
Registry->>Plugin: InvokeAsync()
Plugin->>Target: protocol-native call
Target->>Plugin: response
Plugin->>API: InvokeResult
API->>Browser: JSON response
Streaming and duplex calls open an SSE connection to /bowire/api/invoke/stream or a channel endpoint. The plugin yields messages as they arrive; each one is forwarded as an SSE event.
No CORS proxy needed
The request flow above is the reason Bowire talks to any API without a CORS shim:
- The browser only ever calls
localhost:5080/bowire/api/*— same origin as the UI, so same-origin policy is satisfied trivially and no preflight is issued. - The target service is reached by the Bowire host process, not by the browser. Server-to-server traffic isn't subject to the same-origin policy, so endpoints without permissive
Access-Control-Allow-Originheaders still work.
In other words, Bowire's architecture is the CORS proxy. There's no separate proxy layer to configure, no *-origin hole to poke in the target service, and no dev-tunnel / nginx rewrite to maintain. The one scenario that would invalidate this — upgrading a streaming channel directly from the browser to a non-Bowire endpoint — isn't how any current protocol plugin works.
API endpoints
All endpoints are prefixed with the configured RoutePrefix (default: bowire).
| Endpoint | Purpose |
|---|---|
GET /{prefix} |
Serves the browser UI |
GET /{prefix}/api/protocols |
Lists registered protocol plugins |
GET /{prefix}/api/services |
Lists all services across protocols |
POST /{prefix}/api/invoke |
Invokes unary / client-streaming calls |
GET /{prefix}/api/invoke/stream |
SSE endpoint for streaming calls |
POST /{prefix}/api/channels/open |
Opens a duplex channel |
POST /{prefix}/api/channels/{id}/send |
Sends a message to a channel |
POST /{prefix}/api/channels/{id}/close |
Closes a channel |
GET /{prefix}/api/channels/{id}/stream |
SSE endpoint for channel responses |
GET /{prefix}/mcp/sse |
MCP SSE transport |
POST /{prefix}/mcp/message |
MCP JSON-RPC message endpoint |
Embedded vs. standalone
- Embedded —
MapBowire()adds Bowire to an existing ASP.NET app. Plugins receive the host'sIServiceProvider, so endpoint-metadata-based discovery (SignalR, REST viaIApiDescriptionGroupCollectionProvider, SSE attributes) works out of the box. - Standalone — the global .NET tool runs Bowire in its own ASP.NET process and connects to remote URLs. Every protocol plugin is available; discovery uses network protocols (gRPC reflection, OpenAPI fetch, GraphQL introspection, SDL upload, MCP listing) rather than the host's metadata.
UI
The browser UI is a single-page application packaged as static assets. Pure HTML, CSS, and JavaScript — no framework dependency. It talks to the API via standard HTTP and SSE.
See also: Plugin architecture · Packages