Serve tools over MCP
Serve my workspace's tools over MCP
Put my Speechify OpenAPI tool behind a hosted API with the MCP face switched on: create the API with a keyed audience in the tool’s project, add a tool route for each read operation with a clear name and description, switch on mcp\_enabled, issue a consumer key, then attach the API’s /mcp address to this coding agent and list its tools.
Follow [https://docs.speechify.ai/build/guides/hosted-apis/serve-tools-over-mcp.md](https://docs.speechify.ai/build/guides/hosted-apis/serve-tools-over-mcp.md).Hosted APIs are in beta and enabled per workspace.
Without the grant every /v1/apis endpoint here answers 402 hosted_apis_not_in_plan.
A hosted API can serve its routes as an MCP server at POST https://<slug>.apis.speechify.ai/mcp.
One address gives an MCP client the connectors, stores and runs your workspace assembled, under the same audience, keys, limits and daily caps as the routes themselves.
Every tool call runs through the same path an ordinary HTTP request to the route takes, so nothing the route enforces can be skipped over MCP.
What you end up with
- A hosted API,
acme-tools.apis.speechify.ai, that only callers holding its key can reach. - A
toolroute for each vendor read operation, and any store or run routes you want beside them. - An MCP client, such as Claude Code or Cursor, listing those routes as tools.
This page assumes an openapi or mcp tool already exists; Connect a REST API builds an openapi one, and Add tools an mcp one.
1. Create the API
- Create it in the tool’s project. A
toolroute refuses a tool from another project with409 cross_project_reference. - Pick an audience that names its callers.
consumer_key(ack_key you mint),workspaceorowner(a Speechify API key of your workspace) anduser_token(a JWT your backend signs) all work.publicis refused withmcp_enabled, and it never serves atoolroute. Pickworkspaceorownerfor your own people: each attaches with their own Speechify API key. - Write
nameanddescriptionfor a model. On the MCP face,nameis the server’s title anddescriptionis the instructions the client’s model reads before it chooses a tool.
mcp_enabled can also be switched on later with PATCH /v1/apis/{api_id}.
Allow up to 15 seconds for the switch to reach every server.
2. Add a tool route
operationis theidof one of the tool’soperations.- A tool route is always a
POST: its JSON body is the operation’s arguments, checked against the operation’s own argument schema. - The operation must be classified
read, and the tool’sapprovalmust be null orauto, because a route has nobody to approve a call. A route write that breaks either rule is refused with400 validation_failed, and the message names the fix. Every call checks both again: reclassifying the operation, or giving the tool any otherapproval, turns the route into a403 route_tool_not_readableuntil you change it back. - Deleting the tool, moving it to another project, or removing the operation turns the route into a
409 route_tool_unavailable, which no retry clears: point the route at a tool that has the operation. - The paths
/mcp,/openapi.json,/_runsand/.well-known/oauth-protected-resource, and everything under them, are reserved for the platform, so a route cannot use them. - The response is the vendor’s answer after the operation’s
responsemapping, or{"text": ...}when the vendor answered text. - Every call counts against the API’s
daily_read_capand the tool’smax_requests_per_minute.
On the MCP face the tool’s name comes from the route’s name: every run of characters other than letters, digits, _ and - becomes _, leading and trailing _ are dropped, and the result is cut to 64 characters.
A route with no name is listed under a name built from its method and path, such as post_issues_open, and a name two routes would share takes _2, _3 in route order.
The face’s tools/list is the authority, so read names from it rather than deriving them.
The route’s description is what the model reads to choose the tool, falling back to the operation’s summary when empty.
A vague route name costs you more here than anywhere else, because a model picks from names and descriptions alone.
Mount a whole connector at once
Instead of one route per operation, mount the connector: one call writes a tool route for each operation it offers, named <name_prefix>__<operation> so two connectors’ tools never collide on the MCP face.
- Preview first. With
dry_run: truenothing is written; each entry inoperationssays what a mount would do (create,update,unchanged,skiporstale), and everyskipandstalecarries thereasonand what fixes it. - Apply by sending the same body without
dry_run, with the preview’splan_digest. Every create and update lands in one transaction, and each entry carries the writtenroute. The apply writes only the plan the preview showed: if the connector or the API’s routes changed since, it writes nothing and answers409 mount_plan_changedwith the current plan inerror.details.plan, to review and apply with its ownplan_digest. - Pick with
operations. Omit it to create a route for every operation a route can serve. - Refresh by mounting again. Routes you already have are compared with what the connector offers now: an MCP tool whose input schema changed is an
update, and a route whose operation is gone or can no longer be served isstale. A stale route is never deleted, since a client may still call it; delete it yourself when you are ready. A route you renamed or moved keeps its name and path. - An API holds at most 200 routes.
Let people write through the gateway
A route can serve a tool that is not a read, such as filing an issue, when three things hold:
- The route has
"allow_write": true(mount with"include_writes": trueto create write routes for every write tool at once). - The API’s
auth_modenames a person:owner,workspaceoruser_token. Aconsumer_keyorpublicAPI never serves a write, because a key names no person and the write would act for nobody. - The tool’s effective
approvalisauto. A tool that needs approval stays refused, since nobody on the route can approve it.
A write acts for the person calling: your connector receives their id in Speechify-User-Identity, and the request log names them.
Each write counts against the API’s daily_write_cap, and a client that retries with the same Idempotency-Key gets the first answer back instead of a second issue.
A service account key cannot write through a route.
While a route serves writes, the API cannot be switched to consumer_key or public.
Put an MCP server’s tool behind a route
A tool route can front one tool of an mcp tool definition, so the MCP servers your team already uses sit beside your REST vendors on one address.
operation is the server’s own tool name, as POST /v1/agents/tool-definitions/test-mcp-connection lists it.
- The route write lists the server’s tools, so the server has to be reachable with the tool’s credential at that moment, and it has to speak streamable HTTP; a server that cannot be listed is refused with the server’s own reason.
- The tool must be classified
readin the definition’saction_classes(or declaredreadby the server under the definition’saction_class), and its effectiveapprovalmust beauto. - The response carries
resolver.input_schema: the tool’s input schema as the server listed it, pinned on the route. The MCP face lists the pin and every call is checked against it, so a change the server ships reaches no client until you write the route again. - Calls reuse one warm session to the server, carry the tool’s vault credential, and send your consumer’s
Idempotency-KeyasSpeechify-Idempotency-Key. - The response is the tool’s structured content, its text when that text is JSON,
{"text": ...}for plain text, or{"content": [...]}with every block as the server sent it when one is not text. The definition’sresponse_mappingsentry for the tool applies first. - When the tool reports an error, the route answers
502 route_upstream_errorwith the tool’s own message inerror.details.tool_error, so the model can correct its arguments.
Every other enabled route is a tool too: a store query, an aggregate, a write or a run, each taking the parameters the route binds from a request.
A file route is not listed, since a tool result cannot carry a file’s bytes.
3. Issue a key
The response carries the ck_ secret once; store it now.
rate_per_minute bounds each key, and a client over it gets 429 on its next request.
On a workspace or owner API skip this step: a Speechify API key of your workspace is the credential.
4. Attach an MCP client
The face speaks MCP over stateless streamable HTTP and takes POST only.
Every read of the API carries the address as mcp_url while the face is on, and a client that finds the API some other way can read it from the API’s OpenAPI document: GET https://acme-tools.apis.speechify.ai/openapi.json carries it as x-speechify-mcp.url.
Sign-in is not available yet
A hosted API’s /mcp takes a key or a token, never a sign-in, in every workspace: attach a workspace or owner API with a Speechify API key of its workspace, as above.
A client that can only sign in and cannot send an Authorization header, such as a claude.ai or ChatGPT connector, cannot attach to a hosted API yet.
See how each connector is doing
GET /v1/apis/{api_id}/usage reports, per route and for today, tool_calls, upstream_errors, upstream_throttles and upstream_latency_ms_mean.
When an engineer says the gateway is slow, this is where a slow or failing vendor shows up as that vendor.
What a call looks like to the client
- A route that answers successfully is a tool result whose text is the route’s JSON, with the same JSON as structured content when it is an object.
- A route that refuses answers a tool result with
isError: trueand the error envelope as its text, such asroute_upstream_rate_limited. The client’s model reads the code and can wait or change its arguments. - A run or write tool takes an optional
idempotency_keyargument, sent as the route’sIdempotency-Key, so a client retry replays the first answer instead of acting twice. - A tool route is annotated read-only, and tool and run routes are annotated as reaching beyond the API, so a careful client can decide what to confirm with its user.
Errors
Where the rest lives
- Putting every connector your team uses behind one gateway your people attach once: Run a company MCP gateway.
- Building the tool the route calls: Connect a REST API.
- Store, write and run routes, audiences and caps: Build a dashboard on the platform alone.
- Every error code: Error handling.