This document defines the HTTP interface implemented by MCP Workbench.
The API is versioned under /api/v1. Breaking changes require a new major path.
All request and response bodies use application/json; charset=utf-8.
The service accepts an optional X-Request-Id header. If absent, it creates one.
Every response includes X-Request-Id.
When Security:ApiKey is configured, all /api/v1/* endpoints require:
X-Mcp-Workbench-Key: <configured-secret>Health endpoints and static UI assets remain accessible unless explicitly configured otherwise. The API key is compared in constant time and is never logged.
{
"data": {},
"meta": {
"requestId": "01J...",
"timestampUtc": "2026-07-11T00:00:00Z"
}
}{
"error": {
"code": "server_not_found",
"message": "MCP server '...' was not found.",
"details": null
},
"meta": {
"requestId": "01J...",
"timestampUtc": "2026-07-11T00:00:00Z"
}
}details is optional and must not contain secrets, environment-variable values,
authorization headers, stack traces, process environment blocks, or raw exception text
that could expose sensitive data.
| HTTP | Meaning |
|---|---|
| 200 | Read/update succeeded, or an MCP tool returned a normal protocol response |
| 201 | Server definition created |
| 204 | Delete or disconnect completed with no response body |
| 400 | Invalid request or invalid server definition |
| 401 | API key missing or invalid |
| 404 | Server or tool not found |
| 405 | HTTP method is not supported by the API resource |
| 409 | Invalid lifecycle transition or duplicate normalized name |
| 413 | Request payload exceeds the configured limit |
| 415 | Request content type is unsupported |
| 422 | Tool arguments rejected before or by MCP invocation |
| 429 | Local invocation concurrency limit reached, when configured |
| 502 | MCP transport/protocol failure |
| 503 | Registry or required local service unavailable |
| 504 | MCP connect, ping, discovery, or call timeout |
| 500 | Unexpected application failure |
A tool result containing MCP isError: true is still a successfully transported MCP
response and therefore returns HTTP 200. The body reports isError: true.
stdio
http
auto
streamableHttp
legacySse
disconnected
connecting
connected
disconnecting
faulted
GET /api/v1/serversQuery parameters:
| Name | Type | Default | Notes |
|---|---|---|---|
includeRuntime |
boolean | true | Includes current in-memory status |
search |
string | empty | Case-insensitive name search |
Response:
{
"data": [
{
"id": "c5af558c-ef13-45f8-8cf5-a7d320292f72",
"name": "Demo stdio server",
"description": "Local deterministic MCP server",
"transport": "stdio",
"enabled": true,
"createdAtUtc": "2026-07-11T00:00:00Z",
"updatedAtUtc": "2026-07-11T00:00:00Z",
"runtime": {
"status": "connected",
"connectedAtUtc": "2026-07-11T00:01:00Z",
"lastOperationAtUtc": "2026-07-11T00:02:00Z",
"lastError": null,
"protocolVersion": "2025-11-25",
"serverName": "McpWorkbench.TestServer",
"serverVersion": "1.0.0",
"toolCount": 6
}
}
],
"meta": {
"requestId": "01J...",
"timestampUtc": "2026-07-11T00:00:00Z"
}
}Secrets and resolved environment references are never returned.
POST /api/v1/serversStdio request:
{
"name": "Local test server",
"description": "A local MCP process",
"enabled": true,
"transport": "stdio",
"stdio": {
"command": "dotnet",
"arguments": [
"./tools/McpWorkbench.TestServer.dll"
],
"workingDirectory": null,
"environment": {
"DEMO_TOKEN": "${ENV:DEMO_TOKEN}"
},
"shutdownTimeoutSeconds": 5
},
"http": null,
"operationTimeoutSeconds": 30
}HTTP request:
{
"name": "Remote MCP",
"description": null,
"enabled": true,
"transport": "http",
"stdio": null,
"http": {
"endpoint": "https://mcp.example.test/mcp",
"mode": "auto",
"headers": {},
"authorization": {
"kind": "bearer",
"credential": "${ENV:REMOTE_MCP_TOKEN}"
}
},
"operationTimeoutSeconds": 30
}Validation rules are specified in CONFIGURATION.md.
Response: HTTP 201 with the stored definition and a Location header.
GET /api/v1/servers/{serverId}Returns stored definition, redacted configuration, and current runtime snapshot.
PUT /api/v1/servers/{serverId}The body has the same editable fields as creation.
Rules:
id,createdAtUtc, and runtime fields are immutable.- A connected server is disconnected before the new definition replaces the old one.
- Validation happens before disconnecting.
- Persistence must succeed before the endpoint reports success.
- Failure to disconnect leaves the old definition unchanged and returns 409 or 502.
DELETE /api/v1/servers/{serverId}Rules:
- Cancel outstanding local operations for this server.
- Dispose the MCP client and transport.
- Terminate a managed stdio process using the configured graceful shutdown timeout.
- Remove the definition atomically from the registry.
- Return HTTP 204.
Deleting an unknown identifier returns 404. Deleting an already disconnected known server is valid.
POST /api/v1/servers/{serverId}/connectOptional body:
{
"forceReconnect": false
}Behavior:
- Resolves environment references.
- Creates the selected transport.
- Performs MCP initialization.
- Executes an MCP ping.
- Stores negotiated server metadata and capabilities.
- Optionally loads the initial tool catalog.
Response:
{
"data": {
"status": "connected",
"connectedAtUtc": "2026-07-11T00:01:00Z",
"connectDurationMilliseconds": 83,
"protocolVersion": "2025-11-25",
"server": {
"name": "Example",
"version": "1.0.0"
},
"capabilities": {
"tools": true,
"toolsListChanged": false
}
},
"meta": {
"requestId": "01J...",
"timestampUtc": "2026-07-11T00:01:00Z"
}
}Calling connect for an already connected server is idempotent unless
forceReconnect is true.
POST /api/v1/servers/{serverId}/disconnectReturn HTTP 204. Calling disconnect for a disconnected server is idempotent.
POST /api/v1/servers/{serverId}/pingA disconnected server returns 409. It is not implicitly connected by this endpoint.
Response:
{
"data": {
"success": true,
"durationMilliseconds": 7,
"timestampUtc": "2026-07-11T00:02:00Z"
},
"meta": {
"requestId": "01J...",
"timestampUtc": "2026-07-11T00:02:00Z"
}
}GET /api/v1/servers/{serverId}/runtimeThis endpoint never initiates a connection.
GET /api/v1/servers/{serverId}/toolsQuery:
| Name | Type | Default |
|---|---|---|
refresh |
boolean | false |
The server must be connected. refresh=true executes a new MCP tools/list request;
otherwise the cached catalog is returned when present.
Response:
{
"data": [
{
"name": "add",
"title": "Add numbers",
"description": "Adds two numbers.",
"inputSchema": {
"type": "object",
"properties": {
"a": { "type": "number" },
"b": { "type": "number" }
},
"required": ["a", "b"],
"additionalProperties": false
},
"outputSchema": null,
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": false
}
}
],
"meta": {
"requestId": "01J...",
"timestampUtc": "2026-07-11T00:03:00Z"
}
}Unknown schema members must be retained in the JSON returned to the UI.
POST /api/v1/servers/{serverId}/tools/refreshEquivalent to GET .../tools?refresh=true, but preferred for a UI command because it
clearly performs remote work.
GET /api/v1/servers/{serverId}/tools/{toolName}toolName is URL encoded. Tool lookup is ordinal and case-sensitive, matching MCP names.
POST /api/v1/servers/{serverId}/tools/{toolName}/invokeRequest:
{
"arguments": {
"a": 12,
"b": 30
},
"timeoutSeconds": 10
}arguments defaults to an empty object. Its values remain JSON values; the application
must not deserialize them into arbitrary CLR types.
Response:
{
"data": {
"serverId": "c5af558c-ef13-45f8-8cf5-a7d320292f72",
"toolName": "add",
"startedAtUtc": "2026-07-11T00:04:00Z",
"completedAtUtc": "2026-07-11T00:04:00.011Z",
"durationMilliseconds": 11,
"isError": false,
"content": [
{
"type": "text",
"text": "42"
}
],
"structuredContent": {
"result": 42
},
"raw": {
"content": [
{
"type": "text",
"text": "42"
}
],
"structuredContent": {
"result": 42
},
"isError": false
}
},
"meta": {
"requestId": "01J...",
"timestampUtc": "2026-07-11T00:04:00Z"
}
}The API returns normalized known content fields and a bounded raw result. Binary image data may be retained for direct preview only up to the configured response limit.
GET /health/liveReturns 200 whenever the process is running and the request pipeline is responsive.
GET /health/readyReturns 200 when:
- configuration loaded;
- the registry file was parsed or initialized successfully;
- the registry path is writable when persistence is enabled.
Readiness does not depend on any registered MCP server being reachable.
Defaults:
| Limit | Default |
|---|---|
| HTTP request body | 1 MiB |
| Tool arguments JSON | 256 KiB |
| Tool result retained by API | 4 MiB |
| Tool execution timeout | 30 seconds |
| Connect timeout | 15 seconds |
| Ping timeout | 5 seconds |
| Stdio shutdown timeout | 5 seconds |
| Execution history | 50 entries per server, memory only |
Limits are configuration driven. A response exceeding the limit is rejected or truncated according to the exact rule in CONFIGURATION.md; truncation is always reported explicitly.
- GET endpoints are side-effect free except
GET tools?refresh=true; the POST refresh command is preferred. - Connect and disconnect are idempotent.
- Create is not idempotent; normalized duplicate names return 409.
- Update is a full replacement of editable fields.
- Calls are serialized per server in version 1.
- Different servers may connect and invoke concurrently.
- Registry writes are serialized globally.