Serve tools over MCP

Put a vendor's read operations, your stores and your runs behind one MCP address your engineers and applications attach to
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/agents/guides/serve-tools-over-mcp.md](https://docs.speechify.ai/agents/guides/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 tool route 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 tool already exists; Connect a REST API builds one.

1. Create the API

POST
/v1/apis
curl -X POST https://api.speechify.ai/v1/apis \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"slug": "acme-news",
"name": "Acme News"
}'
{
"slug": "acme-tools",
"name": "Acme security tools",
"description": "Read Acme's open security issues from the vendor. Use list_open_issues to find issues by severity and get_issue for one issue's detail.",
"auth_mode": "consumer_key",
"mcp_enabled": true,
"project_id": "proj_01arz3ndektsv4rrffq69g5fav"
}
  • Create it in the tool’s project. A tool route refuses a tool from another project with 409 cross_project_reference.
  • Pick an audience that names its callers. consumer_key (a ck_ key you mint), workspace or owner (a Speechify API key of your workspace) and user_token (a JWT your backend signs) all work. public is refused with mcp_enabled, and it never serves a tool route.
  • Write name and description for a model. On the MCP face, name is the server’s title and description is 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

POST
/v1/apis/:api_id/routes
curl -X POST https://api.speechify.ai/v1/apis/api_01kc4q3r9s6t8v0w2x4y6z8a0b/routes \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"method": "POST",
"path": "/issues/open",
"resolver": {
"type": "tool",
"tool_id": "tool_01kc4m8r2t6v9x3z5b7d1f3h5k",
"operation": "list_open_issues"
},
"name": "list_open_issues",
"description": "List the vendor'\''s open security issues, newest first. Pass severity to narrow them."
}'
{
"method": "POST",
"path": "/issues/open",
"name": "list_open_issues",
"description": "List the vendor's open security issues, newest first. Pass severity to narrow them.",
"resolver": {
"type": "tool",
"tool_id": "tool_01kc4m8r2t6v9x3z5b7d1f3h5k",
"operation": "list_open_issues"
}
}
  • operation is the id of one of the tool’s operations.
  • 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’s approval must be null or auto, because a route has nobody to approve a call. A route write that breaks either rule is refused with 400 validation_failed, and the message names the fix. Every call checks both again: reclassifying the operation, or giving the tool any other approval, turns the route into a 403 route_tool_not_readable until 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 and /_runs, 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 response mapping, or {"text": ...} when the vendor answered text.
  • Every call counts against the API’s daily_read_cap and the tool’s max_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.

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

POST
/v1/apis/:api_id/keys
curl -X POST https://api.speechify.ai/v1/apis/api_01jqr8x9zg5k2m3n4p5q6r7s8t/keys \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "string"
}'

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.

claude mcp add --transport http acme-tools https://acme-tools.apis.speechify.ai/mcp \
--header "Authorization: Bearer $ACME_TOOLS_KEY"

A client that finds the API some other way can read the address from the API’s OpenAPI document: GET https://acme-tools.apis.speechify.ai/openapi.json carries it as x-speechify-mcp.url whenever the face is on.

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: true and the error envelope as its text, such as route_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_key argument, sent as the route’s Idempotency-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

CodeStatusWhereWhat to do
hosted_mcp_not_enabled404/mcpSet mcp_enabled: true with PATCH /v1/apis/{api_id}; the face is never served on a public API
validation_failed on mcp_enabled400/v1/apisThe API is, or is being made, public; pick an audience that names its callers
validation_failed400Route create or updateThe tool is not an openapi tool you can reach, has no such operation, the operation is not read, or the tool’s approval is not null or auto; the message names which
validation_failed on path400Route create or updateThe path is reserved (/mcp, /openapi.json, /_runs); pick another
cross_project_reference409Route createCreate the API in the tool’s project
route_tool_not_readable403Tool callThe operation is no longer classified read, or the tool’s approval is no longer null or auto; set it back with PATCH /v1/agents/tool-definitions/{tool_definition_id}
route_upstream_rate_limited429Tool callThe tool’s max_requests_per_minute or the vendor throttled the call; wait for Retry-After
route_upstream_error502Tool callThe vendor answered an error (its status is in error.details.upstream_status), could not be reached, or the tool’s credential no longer resolves
route_tool_unavailable409Tool callThe tool was deleted, moved to another project, or no longer has the operation; retrying will not help, so point the route at a tool that has it
route_read_limit_reached429Tool callThe API has served its daily_read_cap today; raise it
rate_limited429Any requestThe key’s rate_per_minute is spent

Where the rest lives