MCP Tool Design: Writing a Tool Surface an Agent Can Read

Nearly every "the model called my tool wrong" bug is a reading bug. The model did not misunderstand your service; it read the only thing you gave it — a name, a paragraph, and a JSON Schema — and filled in the rest with something plausible. It cannot open your README. It cannot inspect your database. It cannot ask you a clarifying question and wait. It reads once, quickly, under context pressure, and then commits.

So the useful way to approach tool design is as an audit rather than a rulebook. Take a surface that is actually deployed, read it the way the model reads it — names first, descriptions second, required fields third — and ask at each tool: could a competent stranger produce one correct call from this alone? Where the answer is no, you have found a defect, and the fix is almost always a word in a description or a constraint in a schema rather than a change to the handler underneath.

This page runs that audit against ClipSpeedAI's ten-tool MCP server, quoting the schemas it actually ships — including the places where the shipping surface is looser than the advice here, because a page that only shows tidy hypotheticals teaches nothing about the trade-offs. Along the way: how to name so two tools stay distinguishable, how to write a description that survives being read once, when a closed value set belongs in an enum and when it deliberately does not, how to name the handle for asynchronous work, how to spend a return-size budget, and how to write errors that fix the next turn. If you have not met the protocol yet, What Is MCP? Model Context Protocol Explained is the place to start.

On this pageWhat the Model Has in Front of It at Decision TimeRead the Shipping Surface Before You Read Any AdviceNames Are a Retrieval Index, Not a LabelA Description Is an Instruction Delivered Exactly OnceWhat ClipSpeedAI's Real Schema Constrains, and What It Leaves OpenHandles: projectId, subscriptionId, and Why the Name Carries WeightThe Split-or-Merge TestEvery Byte You Return Is Spent TwiceErrors Are Your Only Mid-Task Instruction ChannelReference Implementation, Node and PythonThe Blast Radius of Every Verb You ExposeTest Selection, Not Just ExecutionWhere a Tool Surface Is the Wrong ShapeBeliefs That Reliably Produce Bad Tool SurfacesThe Pre-Publish Pass

What the Model Has in Front of It at Decision Time

The design constraints only make sense once you are precise about the moment of decision. When a client connects, it issues a tools/list call and folds the returned names, descriptions and schemas into the model's context — typically once, at session start. Every tool call for the rest of that session is chosen against that frozen snapshot plus whatever the conversation has accumulated since.

CONTEXT AT THE MOMENT A TOOL IS CHOSEN
-------------------------------------------------------
  system prompt         (the client's, often not yours)
  conversation so far   (grows every turn)
  tools/list snapshot   <-- fetched ONCE, at connect
      name              <-- scanned first
      description       <-- read second
      inputSchema       <-- consulted while filling args
  earlier tool results  <-- whatever you returned before
-------------------------------------------------------
        |
        v   model emits { name, arguments }
   tools/call ---> your handler ---> result
        |                              |
        +------------------------------+
           the result text is appended and becomes
           the input to the NEXT decision

Three consequences fall out of that picture, and they drive everything below.

  1. Your description is competing, not presenting. It sits in a list next to every other server the user has connected. It is read in a scan, not a study.
  2. The schema is not validation, it is guidance. By the time your validator rejects a bad argument, the model has already spent a turn. Constraints that appear in the schema shape the argument before it is written.
  3. Your return value is not an ending. It is the opening of the next reasoning step. A 40 KB response is not "a big response"; it is 40 KB of working memory some later step no longer has.

The transport and lifecycle mechanics underneath that exchange — initialize, capability negotiation, notifications — are covered in How MCP Servers Work: Architecture and Request Flow. This page assumes them and stays on the payload.

Read the Shipping Surface Before You Read Any Advice

Start with a real surface rather than a hypothetical one. ClipSpeedAI exposes ten tools over streamable HTTP at https://api.clipspeed.ai/mcp, authenticated with OAuth for GUI clients or an Authorization: Bearer header for CLI clients. Grouped by intent rather than alphabetically, they are:

You can read any server's surface the same way, and you should read it directly rather than trusting its marketing page. Over streamable HTTP a single JSON-RPC request is enough — note that the Accept header has to allow both JSON and SSE:

curl -sS https://api.clipspeed.ai/mcp \
  -H "Authorization: Bearer $CLIPSPEED_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

ClipSpeedAI keys are csai_live_ followed by 48 hexadecimal characters, generated in the account area under API & Integrations, or over the API with POST /auth/api-keys. Only the key's prefix — the first 18 characters plus an ellipsis — is retained for display, so the full value is shown once at creation and cannot be recovered afterwards. Keep it in an environment variable rather than a file you might commit. Why a remote server takes its credential in a header at all, where a local stdio server would read the process environment, is the subject of Remote MCP vs Local MCP Servers.

Now read the tools/list output the way the model will. Scan the names. If two of them could answer the same request and nothing states the tiebreak, you have found a defect. Read the required fields. If any required field has no obvious source — not from the conversation, not from another tool's output — the surface has a hole, and the model will fill it by inventing a value.

Names Are a Retrieval Index, Not a Label

Names do the first pass of the work. Under context pressure a model may effectively select on the name alone, which makes naming the cheapest correctness lever available and the most expensive one to change later.

Treat names as public API from the first release. A rename invalidates saved workflows, the prompts users wrote around your tools, and every eval case you recorded. It is the one change in this whole discipline that cannot be made quietly.

A Description Is an Instruction Delivered Exactly Once

Write for a competent contractor who will read the brief once, immediately before acting, and never again. That framing settles most arguments about what belongs in the text.

Three things earn their place. First, what the tool does, in one sentence, first — some client surfaces truncate long descriptions, so front-load rather than building to a conclusion. Second, when to choose this tool over the nearest alternative; this is the highest-value sentence in the whole description and the one most often missing. Third, what comes back and what to do with it — if the result is a handle that another tool consumes, name that other tool explicitly.

Three things do not: implementation trivia, changelogs, and a prose restatement of the schema. The schema is already in context, so repeating it burns tokens and creates a second source of truth that will drift away from the first.

WEAK
  "Submits a video."

WEAK (schema restated, no decision guidance)
  "Submits a video. Takes videoUrl (string, required),
   captionStyle (string, optional), orientation (string,
   optional), count (number, optional)."

STRONG
  "THE clip button - drops a video URL into ClipSpeed's engine
   and returns ready-to-post clips with the spoken words burned
   in as captions. Ask the creator two things first: orientation
   (vertical 9:16 default, or landscape 16:9) and caption colour
   - call list_templates and show the palette. Returns a
   projectId; then poll check_clips for the finished clips.
   For a stream that is currently live, use clip_livestream."

That third version is close to what the server actually ships, and it is doing four jobs at once: stating the outcome, prescribing a two-question pre-flight, naming the lookup tool that supplies a valid argument, and routing the live case elsewhere. Every one of those is a mistake it prevents rather than a fact it conveys.

A cheap test before you publish: delete the implementation, hand a colleague only the name, description and schema, and ask for one correct call for a realistic request. Any clarifying question they ask is a place the model would have guessed silently instead. The same test works for provider-native function definitions, since the model is reading the same three surfaces either way — MCP vs Function Calling: What Actually Differs covers where the two diverge, which is mostly everything around the definition rather than the definition itself.

What ClipSpeedAI's Real Schema Constrains, and What It Leaves Open

Here is the input schema submit_to_clipspeed actually advertises. It is worth showing verbatim, including the parts a purist would tighten:

{
  "name": "submit_to_clipspeed",
  "inputSchema": {
    "type": "object",
    "properties": {
      "videoUrl":     { "type": "string" },
      "videoId":      { "type": "string" },
      "captionStyle": { "type": "string" },
      "orientation":  {
        "type": "string",
        "enum": ["vertical", "landscape"],
        "description": "vertical = 9:16 (default), landscape = 16:9"
      },
      "count":        { "type": "number" }
    },
    "required": ["videoUrl"]
  }
}

Read what it gets right. required holds exactly one field — the URL, which always comes from the user's own message, so there is nothing to fabricate. orientation is a two-value enum with the aspect ratios spelled out in the field description, which is the difference between a model guessing "portrait" and a model being unable to. Everything else is optional and has a server-side default, so a minimal call with just a URL is a valid call.

Now read what it leaves open, honestly. captionStyle is a free-form string even though the valid set is closed and small: list_templates returns exactly karaoke, hormozi, beasty, fire, youshaei and cinematic. Nothing in the schema stops a model from passing bold-yellow. The description compensates by telling the model to call list_templates first, and the handler has to cope with an unknown value at runtime. That is a real trade: an open string means a new caption style ships without a schema change, at the cost of moving one class of error from impossible to merely handled.

The same server shows the other side of the trade on the live path. clip_livestream declares layout as an enum of square, vertical, rectangle and split, with the default and its behaviour written into the field description, while listing the caption-style ids in that field's description rather than an enum. Two closed sets, two different decisions, on one server. That is what the choice looks like in practice rather than in a checklist.

When you do want a tightly constrained example, here is the shape, on a deliberately fictional tool so nobody copies it into a real call:

{
  "name": "queue_transcript",
  "description": "Queue a recorded audio file for transcription. …",
  "inputSchema": {
    "type": "object",
    "properties": {
      "sourceUrl": {
        "type": "string",
        "format": "uri",
        "description": "Public URL of the audio file."
      },
      "language": {
        "type": "string",
        "enum": ["en", "es", "de", "auto"],
        "default": "auto",
        "description": "Omit unless the user named a language."
      },
      "maxSegments": {
        "type": "integer",
        "minimum": 1,
        "maximum": 50,
        "default": 10,
        "description": "Omit unless the user asked for a count."
      }
    },
    "required": ["sourceUrl"],
    "additionalProperties": false
  }
}

Field-level descriptions are not decoration. They are the only place to write "omit unless asked", "must come from list_templates", or "seconds, not milliseconds". Two habits are worth adopting alongside them: keep required as short as the tool genuinely allows, because every required field is an invitation to invent a value rather than ask; and prefer flat argument lists, which tend to be filled more reliably than three-level nested objects. Treat that second one as a design heuristic to test in your own evals, not as a measured result.

A REST API sitting behind all this is fine — often correct. But the tool layer is a separate artifact with a different reader, and the mapping is rarely one-for-one. MCP vs REST API: When to Use Each works through where the two models genuinely differ; the narrow point here is that forty-three endpoints should not become forty-three tools.

Handles: projectId, subscriptionId, and Why the Name Carries Weight

Anything slower than a few seconds should not block a tool call. Agent clients have timeouts, and a model waiting on a hung call has no way to report progress or make a decision. The durable pattern is submit-then-poll: one tool starts the work and returns a handle immediately, a second reports status or results for that handle.

  submit_* ---> handle ---+
                          |
            +-------------v--------------+
            |   check_* (poll)           |
            +---+---------+----------+---+
   queued /     |         |          |
   running -----+         |          +--> failed --> reason, and
     ("wait Ns, call      |                          whether a retry
      again with the      |                          can help
      same handle")       +--> done --> results + ids for next tools

The handle's name is a design decision, not a formality. ClipSpeedAI uses two, and they are deliberately not interchangeable. The recorded path returns a projectId, which is the required argument of check_clips, and the same id is what creator_pack and publish_to_youtube take. The live path returns a subscriptionId, which is the required argument of check_livestream, stop_livestream and extend_livestream. Because the two names differ, a model holding one of them can see at a glance which family of tools it unlocks. Had both been called job_id, the surface would have offered no signal at all and the first mismatched call would have been a silent empty result.

Three properties make polling safe in practice:

Live work adds lifecycle that recorded work does not have — start, observe, extend, stop — which is exactly why it gets its own verbs instead of borrowing the recorded ones. Livestream Clipping API: Clip While You Stream walks that shape end to end.

The Split-or-Merge Test

There is genuine tension between few coarse tools and many fine ones, and no universal answer. Coarse tools are easy to select and hard to fill correctly, because they grow mode enums and mutually exclusive fields. Fine tools are easy to fill and harder to select, because the list grows and the names start to crowd each other. Context cost rises with the surface; round trips rise with the granularity.

Few, coarse toolsMany, fine tools
SelectionEasy — little to confuseHarder as names crowd
Argument fillingHarder — conditional fieldsEasier — every field applies
Context costLowGrows with the surface
Round tripsFewerMore, each a chance to derail
Typical failureRight tool, wrong mode, quiet nonsenseWrong tool, obvious immediately

Rather than picking a number, apply two mechanical tests.

Split when a field is meaningless unless another field holds a particular value. Imagine collapsing the live lifecycle into one manage_livestream tool with an action enum. Immediately you need an addMinutes field that means something only when action is extend — and indeed the real extend_livestream is the one live tool with two required arguments, subscriptionId and addMinutes, where check and stop need only the id. The conditional field is the tell. Keeping the verbs separate also keeps them separately approvable, which matters when one of them ends a session and another merely reads it.

Merge when two tools always fire in the same order with no decision between them. If a caller must always call create_job, then attach_source, then set_options, then start_job, the model is not making four decisions — it is performing your internal state machine, with four chances to stop halfway. That is one intent and belongs in one tool. "Get me clips from this video" is a single intent, which is why the recorded path is one submit plus one retrieval and not a four-step ceremony.

The working rule underneath both tests: one tool per user intent, not per backend operation and not per resource.

Every Byte You Return Is Spent Twice

Your return value costs tokens once to produce and again every turn it stays in context. Design it for a reader who has to decide something next, not for a parser that will pull three fields and discard the rest.

The classic anti-pattern is a search tool that returns the full body of every match. Return ids, titles and a one-line snippet, then offer a second tool that fetches one full record by id. You pay one extra round trip and get a context budget that survives the rest of the session.

Format matters less than volume. Small structured JSON is good when the model must extract a specific id reliably; compact line-oriented text is often cheaper and just as readable for lists. Either is fine. Neither survives being twenty kilobytes.

Errors Are Your Only Mid-Task Instruction Channel

Once the session is running, an error message is the only text you can inject into the model's context at exactly the moment it is about to make a decision. A status code teaches it nothing. A sentence naming the cause and the corrective action usually produces a correct retry on the very next turn.

WEAK   400 Bad Request
WEAK   {"error":"E_INVALID_ARG","field":"captionStyle"}

STRONG Unknown captionStyle "bold-yellow". Call list_templates to
       get the valid caption-style ids, then retry
       submit_to_clipspeed with one of those values.

STRONG This project is still processing. Wait ~60 seconds and call
       check_clips again with the same projectId. Do NOT call
       submit_to_clipspeed again - that starts a second project.

Look closely at the second one. Besides naming the fix, it forbids the specific wrong action the model is most likely to take. "It didn't work, start over" is a powerful prior in agent loops, and it is exactly how duplicate jobs, double charges and double-posted clips happen. A negative instruction aimed at that reflex is worth the extra line every time.

Also distinguish the two error channels, because they reach different readers. A malformed request or an unknown tool name is a protocol failure and belongs in a JSON-RPC error. A tool that ran correctly and failed for a domain reason — unknown caption style, project not ready, no YouTube account connected — should return a normal result marked as an error, which in the official SDKs means isError: true with explanatory content. The distinction matters because the model sees the text of the second kind and can act on it, whereas the first kind is more likely to be surfaced as a transport fault.

One more habit: when a mutation succeeds, state what changed in concrete terms. "Published clip 3 to YouTube as private" lets both the model and the human verify the outcome. "OK" does not.

Reference Implementation, Node and Python

The shape is the same in every SDK: advertise schemas, then dispatch on name. Note that this example defines both tools it mentions — a description that points at a tool which does not exist is the same hole as a required field with no source.

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

const server = new Server(
  { name: "reports", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

const TOOLS = [
  {
    name: "find_report",
    description:
      "Search published reports by title or body text. Returns up to `limit` " +
      "matches as id + title + one-line snippet. This tool never returns full " +
      "bodies - pass an id to read_report for the full text.",
    inputSchema: {
      type: "object",
      properties: {
        query: { type: "string", description: "Keywords, not a full sentence." },
        limit: { type: "integer", minimum: 1, maximum: 25, default: 5 },
      },
      required: ["query"],
      additionalProperties: false,
    },
  },
  {
    name: "read_report",
    description:
      "Return the full text of one report. The id comes from find_report; " +
      "do not guess ids.",
    inputSchema: {
      type: "object",
      properties: { id: { type: "string", description: "An id from find_report." } },
      required: ["id"],
      additionalProperties: false,
    },
  },
];

const fail = (text) => ({ isError: true, content: [{ type: "text", text }] });

server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  const args = req.params.arguments ?? {};

  if (req.params.name === "find_report") {
    const { query, limit = 5 } = args;
    const hits = await search(query, limit);
    if (hits.length === 0) {
      return fail(`No reports matched "${query}". Try fewer, broader keywords.`);
    }
    const lines = hits.map((h) => `${h.id}  ${h.title}\n    ${h.snippet}`);
    return { content: [{ type: "text", text: lines.join("\n") }] };
  }

  if (req.params.name === "read_report") {
    const doc = await load(args.id);
    if (!doc) {
      return fail(
        `No report with id ${args.id}. Call find_report first and use an id ` +
        `from its results - do not construct ids.`
      );
    }
    return { content: [{ type: "text", text: doc.body }] };
  }

  return fail(`Unknown tool ${req.params.name}.`);
});

In Python the decorator style derives the schema from type hints and the docstring, which makes both of those load-bearing rather than cosmetic:

from typing import Literal
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("reports")

@mcp.tool()
def find_report(query: str, limit: int = 5,
                scope: Literal["published", "draft"] = "published") -> str:
    """Search reports by title or body text.

    Returns id + title + snippet, one per line. Never returns full
    bodies - pass an id to read_report. Keywords beat full sentences.
    """
    hits = search(query, limit, scope)
    if not hits:
        return f'No {scope} reports matched "{query}". Try broader keywords.'
    return "\n".join(f"{h.id}  {h.title}\n    {h.snippet}" for h in hits)


@mcp.tool()
def read_report(id: str) -> str:
    """Return the full text of one report. The id comes from find_report."""
    doc = load(id)
    if doc is None:
        return f"No report with id {id}. Call find_report and use an id from its results."
    return doc.body

The Literal is the detail to copy: it becomes an enum in the generated schema, so an invented scope value is rejected before your code runs. Everything around these handlers — transports, deployment, session handling — is covered in How to Build an MCP Server (Practical Guide).

The Blast Radius of Every Verb You Expose

A large share of your security posture is decided at tool-definition time, because the model will call anything in the list whenever the description sounds relevant. Assume every tool will eventually be invoked with arguments influenced by text you did not write — fetched pages, transcripts, earlier tool output — because all of that flows back into the same context that chooses the next call.

The full threat model — including how untrusted text arriving in the context window can chain two individually harmless tools into something neither was meant to do — is the subject of MCP Security: Scopes, Keys and Safe Tool Design. Key hygiene belongs in the same conversation: ClipSpeedAI keys can be listed with GET /auth/api-keys, which reports each key's name, prefix, plan, rate limit, request counters and last-used timestamp, and revoked with DELETE /auth/api-keys/:id, which deactivates the key and stamps a revocation time. Rotation being cheap is what makes a leaked key an incident rather than a catastrophe; the token-handling trade-offs between that and OAuth are laid out in MCP Authentication: OAuth and Bearer Keys.

Test Selection, Not Just Execution

Unit tests prove your handler behaves when called correctly. They say nothing about whether the model calls it correctly, which is the property that actually determines whether your server works. The unit of testing here is the decision.

  1. Collect real phrasings. Twenty to fifty ways people actually ask for what your tools do — including ambiguous requests, and including requests where the correct behaviour is to call nothing at all.
  2. Score selection and arguments separately. Wrong tool entirely points at names and descriptions. Right tool with wrong arguments points at the schema and the field descriptions. The two failures have different fixes, and averaging them hides both.
  3. Log every call in staging, including the rejected ones. Repeated validation failures on one field are a message about that field's description, not about your users. A field that keeps arriving as a string when you wanted a number needs a better description before it needs better validation.
  4. Force every error path and watch the next action. If the model retries identically, or resubmits from scratch, your error text failed. Rewrite it and re-run the case.
  5. Re-run after wording changes. Expect description edits to move selection behaviour; that expectation is the entire reason the eval exists. Treat description text with the same change discipline you apply to code.

Run the suite against the clients you actually support, because the surrounding harness shapes tool use as much as your schema does. ClipSpeedAI's server is verified end to end on Claude at claude.ai, Claude Code, Claude Desktop and Windsurf; Cursor, Codex, OpenClaw and Hermes speak the same protocol with verification still in progress; ChatGPT support is vendor-gated and rolling out. For a sense of how much the harness matters, Claude Code vs Cursor for MCP Workflows compares two of them directly, and per-client walkthroughs like ClipSpeedAI MCP for Claude Code: Complete Setup Guide cover the mechanics of getting connected in the first place.

Adding a CLI client is one command, and it is the only configuration on this page worth memorising:

claude mcp add --transport http clipspeed https://api.clipspeed.ai/mcp \
  --header "Authorization: Bearer <API_KEY>"

GUI clients store the same two facts — an endpoint and a credential — through their own settings UI or config file. The names and locations differ by product, so follow each client's own MCP documentation; guides such as ClipSpeedAI MCP for Claude Desktop: Complete Setup Guide and ClipSpeedAI MCP for Windsurf: Complete Setup Guide cover the specifics per client. An npm package, clipspeed-mcp, also exists. The structural point for this page is that there is one server and one tool surface behind all of them; clients differ in where they keep the URL and the key, not in what the model sees.

Where a Tool Surface Is the Wrong Shape

A tool surface is not the right container for every capability, and knowing the boundary saves more time than any naming rule.

The inverse describes where the shape fits well: variable intent, small structured results, and work a person would otherwise stitch together by hand across three browser tabs. MCP for Video Editing and Clipping Workflows shows that pattern in one domain end to end. If you want to see how other teams have drawn the same lines, Best MCP Servers for Video and Content Workflows is a survey worth skimming before you commit to a layout.

Beliefs That Reliably Produce Bad Tool Surfaces

Every one of these sounds sensible and produces a worse surface than the alternative.

The Pre-Publish Pass

Run this before you publish a surface, and again after every tool you add.

  1. Every name is a verb plus an object, in the user's vocabulary, and stable enough to treat as public API.
  2. Every description states what the tool does, when to prefer it over its nearest neighbour, and what to do with the result — in that order, with the first sentence carrying the load.
  3. No two tools could plausibly be chosen for the same request without a stated tiebreak.
  4. Every required field is obtainable from the conversation or from another tool named explicitly in the description.
  5. Closed and stable value sets are enums; numeric bounds use minimum/maximum; any value set you deliberately left open is handled at runtime and mentioned in the description.
  6. Nothing slower than a few seconds blocks a call; slow work returns a named handle plus an explicit instruction on when and what to poll.
  7. Handles for different lifecycles have different names, so the model can tell which family of tools a given id unlocks.
  8. Submits are idempotent, or duplicates are harmless and cheap.
  9. Every error names the cause, the fix, and — where the obvious retry is wrong — the action not to take.
  10. Results carry only decision-relevant fields, truncation is announced, and reusable ids sit on their own labelled line.
  11. Read and write tools are separate, irreversible actions require exact identifiers, defaults are the safe setting, and no tool executes arbitrary input.
  12. An eval set of realistic phrasings scores selection and argument accuracy separately, exercises every error path, and re-runs on every wording change.

None of this is exotic. It is the ordinary discipline of writing an interface for a reader who is fast, literal, forgetful, and unable to ask you a question — which describes the agent on the other end of the wire, and occasionally the developer too. If you want to see the finished version of that discipline operating across a whole pipeline rather than a single call, AI Agent Video Automation: End-to-End Workflows follows one from source video to published clip.

Frequently asked questions

How many tools should one MCP server expose?
There is no hard limit, but both selection accuracy and context cost work against you as the list grows. A focused server in the range of roughly five to fifteen tools, each mapped to a distinct user intent, is a comfortable target — ClipSpeedAI ships ten. If you need substantially more, split by domain into separate servers so users load only the surface they need, and check that near-neighbour names stay distinguishable at a glance.
Should a tool description repeat what is already in the input schema?
No. The schema is injected into the model's context alongside the description, so restating field types spends tokens twice and creates a second source of truth that will drift. Use the description for decision guidance — when to pick this tool over its neighbour, what it returns, what to call next — and use per-field descriptions inside the schema for anything specific to one argument, such as units, valid sources, or when to omit it.
Does every closed value set belong in an enum?
Not automatically. An enum makes an invalid value unrepresentable, which is the right call when the set is both closed and stable. The trade is that adding a value means changing the schema. ClipSpeedAI makes that choice both ways on one server: clip_livestream declares its layout options as an enum, while captionStyle stays a free-form string whose valid ids come from list_templates and are validated at runtime. If you leave a set open, say so in the field description and name the lookup tool that supplies valid values.
What is the right way to design a tool that takes minutes to finish?
Return immediately with a named handle, and provide a second tool that reports status or results for it. Make every non-terminal status say how long to wait and which tool to call next, and explicitly warn against resubmitting, because retrying from the top is a strong default in agent loops. Make the submit idempotent so a retry cannot create duplicate work. ClipSpeedAI uses exactly this shape: submit_to_clipspeed returns a projectId, and check_clips takes that projectId and returns the finished clips with their titles, viral scores and download URLs.
Why should the async handle have a specific name rather than just job_id?
Because the handle's name is the model's only clue about which tools it unlocks. ClipSpeedAI runs two lifecycles: the recorded path returns a projectId, consumed by check_clips, creator_pack and publish_to_youtube, while the live path returns a subscriptionId, consumed by check_livestream, extend_livestream and stop_livestream. Distinct names make a mismatched call visibly wrong before it is made. If both were called job_id, the first mismatch would produce a silent empty result and the model would have no way to reason about why.
How do I test whether my tool descriptions are good enough?
Build a small eval set of realistic user phrasings, including ambiguous ones and cases where no tool should be called, then score two things separately: did the model pick the right tool, and did it fill the arguments correctly. A wrong tool points at naming and descriptions; the right tool with wrong arguments points at the schema and field descriptions. Also force each error path and check the model's next action, since a self-correcting error message is usually the cheapest reliability improvement available.
Is JSON or plain text better for tool return values?
Either works, and volume matters far more than format. Small structured JSON helps when the model must extract a specific field such as an id. Compact line-oriented text is often cheaper and just as readable for lists. In both cases: drop response envelopes, drop internal fields, announce truncation explicitly, and put any identifier the next call needs where it cannot be overlooked.
What does a ClipSpeedAI API key look like, and can I revoke one?
Keys are the prefix csai_live_ followed by 48 hexadecimal characters. Create one in the account area under API & Integrations, or with POST /auth/api-keys. Only the first 18 characters are stored for display, so the full value is shown once at creation and cannot be retrieved later — treat it like a password and keep it in an environment variable. GET /auth/api-keys lists your keys with their plan, rate limit, request counters and last-used time, and DELETE /auth/api-keys/:id revokes a key, deactivating it and recording when it was revoked.
Do I need a paid ClipSpeedAI account to try its MCP tools?
You need an account and an API key. There is no free plan: $1 starts a 3-day trial that converts to your chosen plan after three days unless you cancel, with Starter at $15/mo, Pro at $29/mo and Ultra at $49/mo, and annual billing saving 50%. A free demo is available for a single video under 30 minutes, which is enough to see the submit-then-poll shape work end to end.

Related reading

ClipSpeedAI MCP on claude.ai: The No-Config ConnectorClipSpeedAI MCP for Claude Code: Terminal Setup, Keys and Tool ReferenceClipSpeedAI MCP for Claude Desktop: Complete Setup GuideClipSpeedAI MCP in Windsurf: Bearer Key Setup for CascadeClipSpeedAI MCP in Cursor: Connect the Clipping Engine to Your Editor AgentClipSpeedAI MCP for Codex CLI: HTTP Setup and Tool ReferenceClipSpeedAI MCP for OpenClaw: Complete Setup GuideClipSpeedAI MCP on Hermes Agent: Connect and Verify
Start clipping for $1 →