MCP tools
The catalogue exposed over agent access at v0.1.0-preview.10: twenty-one tools and four resources — plus any tools you publish from your own API, which are listed after them and marked your API. The app itself lists them under Settings → AI → Agent access (MCP) → Tools, generated from the same source as the server, so it never drifts.
Asks first means an approval card appears in the app and the agent waits. A denial returns
the error denied by user. Five of Tinspec’s own tools ask: send_request, run_chain,
save_chain, save_request, and set_environment.
Sessions
Section titled “Sessions”Every task an agent does is a session. It opens one before sending anything —
send_request, run_chain and your published tools are refused without one — and everything it
does is filed under it: in the app’s Agent view it shows up as one titled session, and the agent
can list exactly its own requests without wading through yours.
start_session
Section titled “start_session”Asks first: no. Opens a session and returns its sessionId.
| Argument | Type | Notes |
|---|---|---|
title | string | Required. What the session is for |
goal | string | What the agent is trying to find out |
environment | string | Default environment for every send in the session |
provider, sender, org | string | A default identity — applied when a request authenticates with that provider |
A call’s own arguments always win over the session’s defaults. Sends take a sessionId; it may be
left out while the agent has exactly one session open. “Allow all” on the approval card lasts for
the session, and so do the agent budgets. A session nobody used for 30 minutes ends as idle.
get_session
Section titled “get_session”Asks first: no. The session’s own requests, in order — runId, method, path, status, who it
went out as (as), tests — with totals, paged by limit/offset. A body is one get_run away.
list_sessions
Section titled “list_sessions”Asks first: no. The agent’s sessions, newest first (allClients: true for everyone’s).
end_session
Section titled “end_session”Asks first: no. Closes the session with an optional summary, which you see in the Agent view,
and returns a recap: totals and the last runs.
Orientation
Section titled “Orientation”get_active_project
Section titled “get_active_project”Asks first: no. The project the app currently has open. With none open, every tool answers
no project is open in Tinspec.
list_environments
Section titled “list_environments”Asks first: no. Environment names and, per environment, the keys of its variables. Values are never returned.
set_environment
Section titled “set_environment”Asks first: yes. Switches the project’s active environment — the variable set every
later send resolves {{vars}} against, the same switch the app’s environment picker makes.
| Argument | Type | Notes |
|---|---|---|
name | string | An environment name from list_environments |
Returns the new active environment, its source, and its variable keys. Values do not cross here either. An unknown name fails before the prompt, and the error lists the names that exist.
It is read-shaped but it changes the app for you too, so it prompts every time — it is never covered by the “allow safe methods” setting. It is not flagged as destructive, because switching an environment neither destroys nor re-does anything, and an agent told otherwise would over-warn.
list_providers
Section titled “list_providers”Asks first: no. The API providers in the project — id, name, kind, url,
endpointCount, status. The id is what list_endpoints takes.
Identities
Section titled “Identities”These two answer “who can I send as?” without ever naming a credential. Provider files hold non-secret config plus secret names; neither reaches the agent.
list_auth_providers
Section titled “list_auth_providers”Asks first: no. The project’s auth providers: name, kind
(script, request, integration, oauth2, awsSigV4), the IdP integration when the kind
is integration, a human source label, defaultSender, senderCount, and
perSenderPassword. This is what a request’s auth.provider refers to.
No config, no secret refs, no script source, no token.
list_senders
Section titled “list_senders”Asks first: no. The senders — the identities a request can go out as — of one provider or of all of them.
| Argument | Type | Notes |
|---|---|---|
provider | string | A name from list_auth_providers; omit for every provider |
query | string | Free-text match on id, label, or tenant |
includeOrgs | boolean | Also list each sender’s organization memberships (Clerk). At most 25 senders per call |
Each sender comes back as provider, id, label, tenant, default, usable, and
varKeys (names only, like environments). usable: false means the engine would refuse to
authenticate as that identity — a password-grant identity with no password stored, say — and
issue says why in a sentence. Pass a sender’s id as send_request’s sender. With
includeOrgs, each also carries orgs: [{ id, slug, name, role }] — or orgsError from a
provider that has no directory to ask — and an org’s id is what send_request’s org takes.
Sending the same endpoint as several senders and comparing the statuses is the authorization matrix; the bundled skill walks an agent through it.
Spec exploration
Section titled “Spec exploration”list_endpoints
Section titled “list_endpoints”Asks first: no. Search the spec-derived endpoints.
| Argument | Type | Notes |
|---|---|---|
provider | string | Provider id from list_providers |
query | string | Free-text match on path, summary, or operationId |
tag | string | |
method | string | e.g. GET |
limit | integer | 1–500, default 50 |
Returns the (limited) page plus total. Endpoint ids handed to agents are
<providerId>:<endpointId>; a bare endpoint id is accepted when it is unique across
providers, and is otherwise rejected as ambiguous rather than guessed.
Settings → Endpoints → Show internal endpoints is a display setting: hiding
x-internal operations affects the app’s own views, not this tool. Publishing is the
opposite — it refuses them outright (see below).
describe_endpoint
Section titled “describe_endpoint”Asks first: no. One endpoint in full: protocol, resolved URL, parameters, body schema,
documented responses, an example body, and which auth provider applies (or "none"). The
example body may come from your own successful sends (secret-scrubbed, with
{{placeholders}}), from the endpoint’s saved body, or from its schema — exampleSource says
which, so the agent knows whether it is looking at something the server accepted or a guess.
Saved requests
Section titled “Saved requests”list_requests
Section titled “list_requests”Asks first: no. The hand-authored requests saved in the project’s collections.
get_request
Section titled “get_request”Asks first: no. One saved request in full.
Sending
Section titled “Sending”send_request
Section titled “send_request”Asks first: yes. Send one request. Either a spec endpoint with overrides, or an ad-hoc request object.
| Argument | Type | Notes |
|---|---|---|
endpointId | string | A spec-derived endpoint from list_endpoints |
request | object | An ad-hoc request — { name?, protocol?, method, url, headers?, query?, body?, auth?, prerunChain? }. method and url required |
overrides | object | query, headers, body, pathVars — applied to endpointId |
environment | string | Defaults to the active one; an unknown name is an error |
sender | string | The sender to authenticate as |
org | string | The organization the token is obtained for, when the sender is in several — see senders |
select | string | A jq filter over the JSON body, applied before anything is returned — .data[] | {id, name} |
bodyMode | full · summary · none | summary returns the body’s structure (keys, array lengths, the first item’s keys); none no body |
maxItems | integer | Cut every array to its first N items; each cut is reported in truncations |
maxStringLength | integer | Clip long strings, marked …[+n chars] |
expect | object | { "<jq>": expected value } — pass/fail instead of the body |
verbose | boolean | Also return response headers, the sent request, and the token’s identity claims. Off by default |
waitUntil | string | A jq condition. Re-sends until it holds — safe methods and GraphQL queries only |
timeoutSecs, intervalSecs | integer | The poll’s limits: up to 300 s (default 60), every 1–60 s (default 2) |
An ad-hoc request declares http, grpc, or graphql — the chainable, single-response
protocols. A spec endpoint goes out as whatever protocol it is, which includes a tool from
an MCP catalogue provider: that leaves as an MCP call.
sender and org pin the identity on the auth provider the request would have used anyway;
with no provider to pin them to, the call errors rather than sending unauthenticated.
Every jq expression is compiled before anything is sent — and before the approval card — so a
typo costs nothing. A waitUntil send asks once for the whole poll, and the card says it repeats;
every attempt is still its own history row.
The send is the same one the editor’s Send button makes: the collection’s defaults and
the provider’s send settings layer in, so the agent’s request runs on the
resolved timeout, proxy, and TLS you configured. A spec endpoint belongs to a provider and to
no collection, and an agent’s ad-hoc request belongs to neither — so neither inherits whatever
collection you happen to have selected in the UI.
Returns:
| Field | Notes |
|---|---|
status, durationMs, runId | The response, and the history row it became — pass it to get_run |
body | The raw body text when no shaping was asked for; the selected / clipped / summarised value when it was. bodyTruncated: true when cut to the response-size limit (256 KB when unlimited) |
selectError, truncations, stringsClipped, bodyChars | What shaping did: why a select could not run, every array cut, how many strings were clipped, the original size |
expect | { passed, total, failed: [{ path, expected, actual }] } |
tests | Counts plus failures of the request’s assertions: { passed, total, failed } (the full list with verbose) |
poll | With waitUntil: { attempts, satisfied, elapsedMs } |
as | Who it went out as — provider, sender, org, and tokenOrg, the organization the token itself names. Absent for an unauthenticated send |
headers, sentRequest, auth, claims | Only with verbose: response headers and the sent request with credentials masked (Bearer ****), and the token’s identity claims |
The sent body is deliberately omitted. With auth injected into the body rather than a header, it would carry the raw token, and masking a body is a harder problem than masking a header.
If the request has pre/post hooks, anything they printed goes to the app’s Agent activity panel, not into the tool result: those lines are your code’s output, and an agent that asked for a status code has no claim on them. The durable copy is in run history.
Unless shaping was asked for, what crosses is the raw body text, truncated — not the response pane’s rendering. Preview, find, filter, and run-to-run diff are the app’s surfaces, and an agent gets the bytes instead.
An agent’s send never streams. SSE auto-streaming belongs to the editor, so a
text/event-stream endpoint sent this way is buffered — it returns when the server closes the
stream or the timeout fires.
Chains
Section titled “Chains”list_chains
Section titled “list_chains”Asks first: no. The chains in the project.
run_chain
Section titled “run_chain”Asks first: yes. Run one.
| Argument | Type |
|---|---|
name | string |
inputs | object — chain input variables |
The chain is validated before the approval prompt, so an invalid chain fails with the reason —
code: message (node) lines — rather than asking you to approve something that cannot run.
A run that stops partway is not a tool error: it comes back as a result, so the agent can see how far it got.
| Field | Notes |
|---|---|
ok | True only when the engine said the run passed |
status | passed, failed, or timedOut |
error | A one-line reason, when ok is false. failedNode names the node it is attributed to, when it is a node’s |
failures | Each { node, kind, message, region, handled }, in order. handled: true means the run carried on past it |
stopped | true when the run ended where it failed, so the later nodes never ran |
nodes | Per node: id, kind, status (done/error/skipped), httpStatus, durationMs, plus region when it ran in teardown and iteration when it is a loop pass |
outputs | The chain’s variables |
Read ok, never the absence of an error. A node with onError: continue, a failure an
error edge caught, a failed assert, and a broken teardown all come back resolved and
failed — an agent that treats “the call returned” as success reports every one of them green.
plan_prerequisites
Section titled “plan_prerequisites”Asks first: no — it is read-only and writes nothing.
| Argument | Type | Notes |
|---|---|---|
endpointId | string | The target to set up prerequisites for |
reuse | prefer | never | prefer (default) references matching saved requests; never generates every step from the spec |
Returns a ready-to-save chain plus the steps, anything unresolved, notes, and validation
issues. Pass the returned chain to save_chain, edited as needed.
save_chain
Section titled “save_chain”Asks first: yes.
| Argument | Type | Notes |
|---|---|---|
chain | object | The chain to write |
overwrite | boolean | Default false; required to replace an existing name |
Validated strictly before the approval prompt — against the workspace’s collections and
the active environment’s keys, with extraction paths checked against the spec endpoint each
node maps back to. A failure lists code: message (node) lines and writes nothing.
History
Section titled “History”get_history
Section titled “get_history”Asks first: no.
| Argument | Type | Notes |
|---|---|---|
limit | integer | 1–500, default 50 |
method | string | |
status | integer | |
since | string | ISO-8601 timestamp |
get_run
Section titled “get_run”Asks first: no. One run in full. The request shown is the pre-resolution snapshot.
For a send the agent made in the current app session, the full body is still in memory and
bodySource is "full": select, bodyMode, maxItems, maxStringLength and expect work
exactly as on send_request, so a body can be re-read another way without re-sending. Anything
older has only what history stores — the first 2,000 characters (bodySource: "historyPrefix"),
paged with body_offset / body_limit. Full bodies are never written to disk.
Spec changes and drift
Section titled “Spec changes and drift”The two answer different questions, so they are two tools: one compares two fetches of the spec document, the other compares a live response against what the spec declared.
get_spec_changes
Section titled “get_spec_changes”Asks first: no. Returns three things.
changes— endpoints added, removed, or modified since you last reviewed the spec, the same data as the Spec Changes view.breaking— the engine’s classified diff joined to this workspace. Each row carries the operation, itschanges(each with a stable machine-readablerule—requiredInputAdded,outputBecameNullable,typeNarrowed, … — and abreakingflag), andimpact: the saved requests, chain nodes, and endpoint overlay that call it. Branch onrule, never ondetail, which is prose written for a human.classified— how many providers actually produced a verdict.
That last field is there because an empty breaking list is not proof of safety: a source
polled for the first time this session seeds a baseline instead of reporting, and a
.proto-backed gRPC provider produces no verdict at all. classified is how an agent tells
“nothing broke” from “nothing was checked”.
endpointId is null for a removed operation — there is nothing left to open.
get_drift
Section titled “get_drift”Asks first: no. The recorded contract drift the app’s Contract Drift view lists — responses that did not match what the bound spec declared, collected as sends happen, newest first and excluding findings you dismissed.
| Argument | Type | Notes |
|---|---|---|
provider | string | Provider id or name |
kind | string | undeclaredStatus, missingRequired, undocumentedField, typeMismatch |
limit | integer | 1–500, default 50 |
A finding is { id, at, provider, providerName, method, path, status, kind, fieldPath, expected, actual } — compact shape facts. The response body is never stored and never
returned. A project with no folder has no local database, so this is empty for one.
Authoring
Section titled “Authoring”import_curl
Section titled “import_curl”Asks first: no. Turns a curl command into a request object. Nothing is saved.
save_request
Section titled “save_request”Asks first: yes.
| Argument | Type |
|---|---|
request | object |
folder | string |
Writes the request into the project as YAML.
Your own API as tools
Section titled “Your own API as tools”The twenty-one above drive Tinspec. The other direction is new in v0.1.0-preview.10: your own
API, handed to your agent as tools — createOrder, listInvoices — so it never learns a URL,
a header, or a token.
Publishing is off by default and turned on per API provider, in that provider’s settings under Agent → Publish to your agent. An agent gaining the ability to call your API should be an act, not a setting that drifts on. The resulting catalogue is the Agent tools view (command palette: Show Published Agent Tools), which lists each tool, the endpoint behind it, and the identity it runs as.
Scope is explicit and small. Publish one tag, only the read-only verbs (GET/HEAD/
OPTIONS), or only what one auth rule’s sender can reach. A prefix (billing_) is there for
when two providers both want to publish a createOrder.
Naming. An operationId that is already an identifier is used verbatim, because
createOrder reads as something an agent can reason about where post_orders reads as
plumbing. Anything else is slugified from the verb and the path template (get_pets_petId),
which is stable across polls. Names are capped at 64 characters.
Arguments come from the endpoint’s parameters and request body. Cookie parameters are not
published — the engine’s cookie jar already carries one. The body rides as body (or
requestBody, if a parameter is already called body), and its default is a body that
actually produced a 2xx against this endpoint, secret-scrubbed, rather than a draft
synthesized from types. That lookup is bounded to 120 endpoints per rebuild; the rest publish
with their schema alone.
Never published, and the view says so rather than dropping them quietly: deprecated
endpoints, x-internal endpoints, a name that collides with one of Tinspec’s own tools, a name
two endpoints both resolved to, and a schema that is not an object. A scope decision — “you
asked for read-only and this is a POST” — is the filter working and is not listed as a refusal.
Each tool carries a resolved provider and sender, and the agent is never told which. The
resolution is held in the app alone; the process that serves the catalogue holds only names,
descriptions, and schemas. So a published tool has no credential field of any kind, the
approval card names the identity (as tenant-admin), and the agent sees the response.
A published call is an ordinary send. Same engine, same auth injection, same pre-run chain,
same settings layers, same gateway rewrite, same cookie jar, same run history, same drift
check — and the same approval card, per-client serialization, and Agent activity row. A
published write always prompts; a published read follows the “allow GET/HEAD/OPTIONS without
asking” setting, exactly as an agent-driven GET does.
The catalogue is recomputed when you change what a provider publishes, and the Agent tools view
has a Republish button for everything else. Tinspec does not emit a tools/list_changed
notification, so the list result tells clients not to cache it and a change shows up on the
agent’s next tools/list.
Resources
Section titled “Resources”| URI | Contents |
|---|---|
tinspec://spec/<providerId> | The provider’s OpenAPI document as YAML |
tinspec://collection/<file> | A collection file |
tinspec://response/last | The most recent response |
tinspec://skill | The bundled agent skill — the server’s instructions point here first |
The spec resource is offered only for OpenAPI-backed providers. A gRPC, GraphQL, or MCP provider has a live projection and no document, and an agent reading a resource that says “this API has no endpoints” is worse off than one that was never offered it.
What never crosses
Section titled “What never crosses”- Environment values. Keys only — including from
set_environmentafter it switches. - Auth tokens. Headers in the returned
sentRequestare masked asBearer ****, and the sent body is omitted entirely. - Secret names. The identity tools return neither a secret ref nor a provider’s config.
- The keychain. Not reachable through any tool.
- Script output. Hook
consolelines go to the app’s Agent activity panel, not the agent.
Response bodies do reach the agent — that is the point of the surface — truncated to the response-size limit, with the agent told when truncation happened.
Bridged sends land in run history with source: "mcp" and a source_detail naming the client
and tool; chain runs and published-tool calls likewise. The Agent activity panel
additionally shows every tool call and resource read with timing, outcome, and the approval
decision, session-only — SQLite history is the durable record.
Not in this release
Section titled “Not in this release”No stdio transport, no remote or hosted MCP, no tools/list_changed notification, no sampling
or elicitation, no tools for WebSocket, streaming gRPC, or LangGraph runs, and no cloud tools.
Switching the active environment is the only app state an agent can change: it cannot edit an
environment’s values, an auth provider, or a sender.