Skip to content

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.

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.

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.

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.

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.

Failure is a response, not a transport error, so the response pane shows it and an assertion can read it:

OutcomeStatusBody
The endpoint answered non-2xx (401, 404, 500…)The server’s ownThe server’s own
JSON-RPC error — unknown tool, invalid params500{ code, message, data? }
result.isError: true — the tool ran and failed500The whole result
Success200The 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.

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.

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 /sse that 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.
- 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-agent

method: 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.

As with gRPC, how a request is sent and how its catalogue is described are different fields:

FieldValues
How it is sentprotocolmcp
How it is describedSpec source kindmcp

A server can be one, the other, or both. They are not the same setting and neither implies the other.