Call an MCP server
Tinspec speaks MCP in both directions, and they are different features:
- Outbound — this page. A saved request calls one tool on somebody else’s MCP server. The server is the thing under test.
- Inbound — Agent access. Your own agent drives the running Tinspec app. Tinspec is the thing being driven.
They share a protocol name and nothing else. Do not wire one expecting the other.
Why it is a protocol and not a viewer
Section titled “Why it is a protocol and not a viewer”An MCP server is the one API shape with no Swagger UI, no Postman collection, and no inspector that does much more than list-and-poke. Everything that makes an OpenAPI document useful here — a live binding, a diff across polls, a breaking-change verdict, a chain graph — is protocol-agnostic, so making MCP a protocol like any other buys all of it at once.
Underneath it is HTTP, and deliberately the same HTTP the rest of the app uses: the handler builds ordinary requests and hands them to the shared HTTP client. That is what makes the cookie jar, the layered send settings, proxies, mTLS, redirect recording and — above all — auth injection apply with no MCP-specific wiring.
The server is the URL
Section titled “The server is the URL”There is no MCP server entity in the collection format, and no token field.
Every other protocol puts its target in the request’s url and only the call in its own
shape, and MCP follows that. A bespoke token: field would have forked MCP away from the auth
automation this product exists for — and it would have been the wrong tool anyway, because
MCP’s own authorization spec is OAuth 2.1, which the generic
oauth2 auth provider already performs.
So: URL is the server, Auth is an ordinary provider, Headers are ordinary headers.
Bind a server as a source
Section titled “Bind a server as a source”Add API source → MCP server is the practical way in. Give it the server’s streamable-HTTP URL, an optional name, and a poll interval.
Its tools/list becomes the endpoint set — one endpoint per tool — and each generated
request arrives with the tool name already set and a starter arguments object built from that
tool’s own inputSchema, required properties first. An explicit default wins, then the first
enum member, then an example, and only then a blank of the right type; the blank is "" or
0 rather than a <placeholder> marker, because a marker that survives into a real call is
worse than an empty string.
The tools are tagged with the server’s own name. MCP has no grouping axis of its own — no tag, no service, no root operation — and inventing one from a name prefix would be a taxonomy the server never declared.
Because it is a spec source like any other, the catalogue is polled and diffed: a dropped tool reads as a removed endpoint, a newly required argument as a tightened parameter, and endpoint overlays keep your edits across polls.
What one send actually does
Section titled “What one send actually does”MCP is stateful: a client must initialize and then send notifications/initialized before it
may call anything. Each send runs that handshake, makes its one call, and DELETEs the session
on the way out when the server assigned one.
So a tool call is three or four HTTP round trips, and it leaves no server-side state behind — which is the property that makes it safe to run inside a chain, a loop or a poller without piling up sessions in somebody’s session manager. Caching a session between sends would save two round trips and raise questions (“when does it end”, “what happens when the token refreshes”) that a first cut should not answer badly, so it is not done.
The protocol revision advertised is 2025-06-18; a server on an older revision is negotiated
down rather than refused.
How a result reads
Section titled “How a result reads”Failure is a response, not a transport error, so the response pane shows it and an assertion can read it:
| Outcome | Status | Body |
|---|---|---|
| The endpoint answered non-2xx (401, 404, 500…) | The server’s own | The server’s own |
JSON-RPC error — unknown tool, invalid params | 500 | { code, message, data? } |
result.isError: true — the tool ran and failed | 500 | The whole result |
| Success | 200 | The whole result |
A tool that ran and failed is a 500 for the same reason a non-OK gRPC status is: green means
it worked is the contract of the whole UI. The two failure kinds stay distinguishable in the
body without inventing a header — a JSON-RPC error has a top-level code, a tool error has
isError and content.
A 401 from the endpoint passes straight through, including on the initialize leg, so an MCP
server behind a token rejects you visibly and the engine’s re-authenticate-and-retry path
applies with no MCP-specific wiring.
Chains
Section titled “Chains”MCP requests are chainable. One tool call is one request and one response, which is exactly what a chain step needs, and it dispatches through the same send path as HTTP — so extraction, assertions and variable passing work unchanged.
Transports
Section titled “Transports”Supported: streamable HTTP (MCP 2025-03-26 and later) — one POST per JSON-RPC message to a single endpoint. The reply may arrive as a lone JSON object or as an SSE stream carrying one; both are handled, because the spec lets the server choose.
Not supported, each a decision rather than an oversight:
- stdio. Many MCP servers are local processes, so this is the notable gap. It is also a
materially different shape — a spawned child with a lifetime, a stderr stream and a kill path
— and it would need the collection format to grow a command, its arguments and its
environment. Collection files are committed to git and pulled from teammates; a
command:in one is arbitrary code execution on clone. When it lands it needs a consent gate and a process supervisor. Until then, a local stdio server is reachable through any of the stdio↔HTTP bridges that already exist. - The deprecated HTTP+SSE transport (MCP 2024-11-05, a long-lived
GET /ssethat hands back a separate POST endpoint). It is superseded, and supporting it would mean a second connection model for servers being upgraded away from it. - Server-initiated traffic — sampling, elicitation, roots, and the notifications a server
may push, including
notifications/tools/list_changed. A request tester asks and reads the answer; honouring a server’s request to call your LLM is a product decision, not a transport detail. Catalogue changes are caught by the poll interval instead.
In the file format
Section titled “In the file format”- name: Create order protocol: mcp method: POST url: http://127.0.0.1:3000/mcp mcp: tool: create_order arguments: | { "customerId": "{{customerId}}", "sku": "SKU-1" } auth: provider: orders-agentmethod: POST is required by the format even though the verb is never yours to choose — an MCP
conversation is POSTs on the wire.
arguments is a JSON object encoded as a string, the same convention gRPC messages and
GraphQL variables use, and for the same two reasons: the argument names are the server’s
vocabulary rather than Tinspec’s, and a string diffs line by line in a committed YAML block
scalar. {{variables}} resolve inside it.
The two axes stay separate
Section titled “The two axes stay separate”As with gRPC, how a request is sent and how its catalogue is described are different fields:
| Field | Values | |
|---|---|---|
| How it is sent | protocol | mcp |
| How it is described | Spec source kind | mcp |
A server can be one, the other, or both. They are not the same setting and neither implies the other.