MCP vs Function Calling: Two Layers, Not Two Choices

They are not competitors, and the question conceals a category error. Function calling is a calling convention between a model and the program running it: you show the model a set of JSON Schemas, and instead of prose it emits a structured request naming one schema and supplying arguments. The Model Context Protocol is a resolution and transport layer: it decides which schemas exist in the first place, where the implementation lives, and whose credentials it runs under. Every MCP tool invocation still terminates in an ordinary function call. The model never learns that the schema arrived over JSON-RPC instead of being typed into an array in your source file.

A concrete way to feel the difference: imagine a company that clips long videos into vertical shorts. If that company wants its clipping available inside other people's agents, hardcoding means writing one integration per customer application, forever. Publishing one MCP endpoint replaces the whole matrix. Flip it around and the calculus inverts. If you have a single application, a single model vendor, and six functions you wrote yourself, MCP adds a handshake, a process or a network hop, and a config file per client, in exchange for reuse you will never claim.

What follows is the code-level version of that trade: the two wire encodings side by side, why runtime listing is the only genuine capability difference, where credentials actually sit, a corrected worked example against a live endpoint, and the cases in both directions where the obvious choice is the wrong one. If you want the concept before the wire format, start at What Is MCP? Model Context Protocol Explained. A single request traced from handshake to result lives in How MCP Servers Work: Architecture and Request Flow.

On this pageCompile Time vs Load Time: Where the Catalog Comes FromWhat the Model Actually ReceivesThe JSON-RPC Surface MCP AddsOne Tool, Two EncodingsRuntime Listing Is the Only Genuine Capability DifferenceWhose Credentials Run the CodeLong Jobs Are Nobody's Solved ProblemPodcast to Clips, Written Both WaysThe Differences, Row by RowCases Where the Protocol Is Pure OverheadCases Where a Hardcoded Array Becomes a LiabilityClaims That Break When You Read the Wire LogPorting an Existing Dispatch TableSix Questions, Then Three Commands

Compile Time vs Load Time: Where the Catalog Comes From

Programmers already carry a precise mental model for this distinction and mostly do not notice they have it. A statically linked binary knows every symbol it can call before it starts. The linker resolved them at build time, the set is frozen, and adding one means a rebuild and a redeploy. A program that calls dlopen resolves symbols at load time instead: it asks something outside itself what functions exist, receives names and signatures, and binds to them while already running.

Plain function calling is the static case. Your tool array is a literal in your source tree. It is fully known at deploy time, it cannot change while the process is alive, and the compiler — or at least your test suite — can see the whole surface at once.

MCP is the dynamic case. The host connects to a server and asks what it exports. The server answers with names, human-readable descriptions and JSON Schemas. The host binds those into whatever tool format its model expects and hands them to the model. Nothing about the set is known until runtime, and a server may legitimately answer differently on Tuesday than it did on Monday.

The analogy survives further than it deserves to. Dynamic loading buys distribution: a plugin author ships an update without rebuilding the host. It costs a resolution step that can fail in ways static linking cannot — the library is missing, the version is wrong, the symbol moved. MCP inherits exactly that failure class. A hardcoded function cannot fail to connect, fail a handshake, or return 401. An MCP tool can fail all three before your business logic runs a single line.

Where the analogy breaks is address space, and the break is the interesting part. A dynamically loaded library runs inside your process with your permissions. An MCP server usually runs in another process, frequently on another machine, often at another company, holding its own credentials for its own upstream APIs. That relocation of execution — not the JSON-RPC framing — is what makes MCP structurally different rather than merely fancier.

WHO SPEAKS WHAT

  MODEL  <--[ vendor tool-call format ]-->  HOST / AGENT  <--[ MCP: JSON-RPC 2.0 ]-->  SERVER
         schemas in, tool_use blocks out    (Claude Code,     initialize
                                             Claude Desktop,  tools/list
                                             your own loop)   tools/call

  The host is the only component fluent in both dialects.
  The model never sees JSON-RPC. The server never sees a tool_use block.
  Neither end knows the other exists.

Read that left to right and the claim "MCP replaces function calling" collapses immediately. Delete the left arrow and the model cannot request anything at all. Delete the right arrow and the host simply reverts to a hardcoded array. They are stacked, not opposed, and a single host can present three servers' tools alongside four built-in local functions as one flat catalog that the model cannot disambiguate.

What the Model Actually Receives

Anthropic calls it tool use, OpenAI calls it function calling, and the mechanics are close enough that the same code shape works for both after a rename. Each tool definition carries three things: a name, a natural-language description, and a JSON Schema for the arguments. All three go into the model's context alongside the conversation, which means all three consume tokens and all three compete for attention.

The description is doing far more work than newcomers expect. The schema constrains what a valid call looks like; the description is the only thing that tells the model when to make one. A schema-perfect tool with a vague description gets called at the wrong moments, or not at all, and no amount of validation catches that because every call is technically well-formed.

The model does not execute anything. It emits an intent. Your code receives that intent, decides whether to honour it, performs the work, and returns the outcome as a tool-result message keyed to the original request id. The conversation continues until the model stops asking for tools and answers in prose.

ONE TURN OF THE LOOP  (no MCP anywhere in this picture)

  build   tools = [ {name, description, schema}, ... ]
  send    messages + tools                    ----->  model
  recv    tool_use { id, name, input }         <-----
  run     YOUR process: dispatch[name](input)
  send    tool_result { tool_use_id, content } ----->  model
  loop    until the model replies with text instead

  Nothing here is networked. Nothing here is discovered. Nothing here is authenticated.

Two consequences fall out of that loop and account for most of what appears later on this page. The catalog is frozen at the moment you construct the request, because it is an array literal in a running process. And the executing code sits inside your application, reaching for your application's credentials, with your application's blast radius when a tool misbehaves.

Neither consequence is a flaw. For one team shipping one product, a dictionary mapping tool names to callables is a legitimate production design, trivially unit-testable, and free of every operational concern described in the rest of this page.

The JSON-RPC Surface MCP Adds

MCP is an open specification built on JSON-RPC 2.0. An MCP host — Claude Code, Claude Desktop, an IDE, or an agent loop you wrote — runs one MCP client per connected MCP server. Tools are the most visible capability, but a server may also expose resources (readable content addressed by URI) and prompts (parameterised templates the user can invoke).

A connection opens with an initialize exchange that pins a protocol version and swaps capability declarations, so each side learns what the other supports before anything is called. After that the client can list and invoke.

THE METHODS WORTH MEMORISING

  initialize        version negotiation + capability declarations
  tools/list        -> [ { name, description, inputSchema }, ... ]
  tools/call        execute one tool by name with an arguments object
  resources/list    enumerate readable content
  resources/read    fetch one resource by URI
  prompts/list      enumerate prompt templates
  prompts/get       render one template with parameters

  notifications/tools/list_changed   server -> client, unsolicited:
                                     "the catalog moved, list again"

Two transports are standard. Under stdio the host spawns the server as a child process and speaks over stdin and stdout — no network surface, no listener, and the server can touch the local filesystem. Under streamable HTTP the server is a web service at a URL; the host POSTs JSON-RPC to it and the response arrives either as a JSON body or as a Server-Sent Events stream, which is how a server pushes progress during a long call. The operational consequences of that choice are large enough to have their own page: Remote MCP vs Local MCP Servers.

Notice what is absent from the method list. There is no scheduling, no retry policy, no durable queue, no rate limiter. MCP standardises how a catalog is described and how a call is framed. Everything about making the underlying work reliable remains the server author's problem, which is why so many MCP integration failures turn out to be ordinary distributed-systems failures wearing a new acronym.

One Tool, Two Encodings

Here is a real tool from a live server, in both encodings. ClipSpeedAI's submit_to_clipspeed takes one required argument, videoUrl, plus optional captionStyle, orientation, count and videoId. It returns a projectId. First, the shape you would hand a model directly if you had written the tool yourself:

// Anthropic-style definition, sent inline with the messages request
{
  "name": "submit_to_clipspeed",
  "description": "Submit a video URL for clipping. Returns a projectId.",
  "input_schema": {
    "type": "object",
    "properties": {
      "videoUrl":     { "type": "string" },
      "captionStyle": { "type": "string",
                        "description": "karaoke | hormozi | beasty | fire | youshaei | cinematic" },
      "orientation":  { "type": "string", "enum": ["vertical", "landscape"] },
      "count":        { "type": "number" }
    },
    "required": ["videoUrl"]
  }
}

// What the model emits
{ "type": "tool_use",
  "id": "toolu_01ABC",
  "name": "submit_to_clipspeed",
  "input": { "videoUrl": "https://example.com/podcast.mp4",
             "captionStyle": "hormozi",
             "orientation": "vertical" } }

// What you send back after running it
{ "type": "tool_result",
  "tool_use_id": "toolu_01ABC",
  "content": "projectId=... status=queued" }

Now the same tool travelling over MCP. The server publishes it; the host is the component that rewrites it into the block above before the model ever sees it.

// Request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }

// Response (abridged to one tool)
{ "jsonrpc": "2.0", "id": 1, "result": {
    "tools": [
      { "name": "submit_to_clipspeed",
        "description": "Submit a video URL for clipping. Returns a projectId.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "videoUrl":     { "type": "string" },
            "captionStyle": { "type": "string" },
            "orientation":  { "type": "string", "enum": ["vertical", "landscape"] },
            "count":        { "type": "number" }
          },
          "required": ["videoUrl"] } }
    ] } }

// Invocation
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
  "params": { "name": "submit_to_clipspeed",
              "arguments": { "videoUrl": "https://example.com/podcast.mp4",
                             "captionStyle": "hormozi" } } }

// Result: content blocks, plus a flag that is NOT a JSON-RPC error
{ "jsonrpc": "2.0", "id": 2, "result": {
    "content": [ { "type": "text", "text": "projectId=... status=queued" } ],
    "isError": false } }

Three details repay attention. The schema field is inputSchema in camelCase under MCP and input_schema in snake_case in Anthropic's API — a papercut that everyone writing their own host hits exactly once and remembers permanently. Arguments live under params.arguments, not at the top level of params, and a nested object one level off is the single most common malformed call.

The third detail is isError, and it is a design decision rather than a formatting quirk. A tool that fails for domain reasons — the URL is unreachable, the account has no quota left, the video is longer than the plan allows — should return a normal result with isError: true and a text block explaining what happened. JSON-RPC error objects are reserved for protocol failures: unknown method, malformed parameters, a broken frame. The distinction matters because the model can read an isError result and adapt, correcting an argument or choosing a different approach, whereas a protocol error is generally swallowed by the host as plumbing. Writing descriptions and failure text that a model can act on is a discipline of its own, and MCP Tool Design: Writing Tools an Agent Can Actually Use is the page devoted to it.

Runtime Listing Is the Only Genuine Capability Difference

Strip away the framing and the transport and exactly one thing MCP does cannot be replicated by a sufficiently determined engineer with a hardcoded array: the catalog is fetched at runtime and can change mid-session.

With a static array, adding a tool means shipping a release of every consumer. Gating a tool behind a paid plan means writing that branch yourself, in every consumer. With MCP, the client asks the server what exists each session, and the server is free to answer according to who is asking. A key belonging to one plan tier can see a different catalog from a key belonging to another, with no branching logic anywhere in the client. When the catalog moves, the server sends notifications/tools/list_changed and connected clients re-list without restarting.

That flexibility is not free, and the costs are underrated. Tool descriptions written by someone else now share context with your carefully tuned system prompt, in wording you do not control and which may change on the server author's release schedule. Those descriptions are also untrusted text landing directly in your model's context — treat them as data, never as instructions, particularly in an agent holding shell or filesystem access. Scoping, key hygiene and the rest of that surface are covered in MCP Security: Scopes, Keys and Safe Tool Design.

You can inspect any remote server's catalog with nothing but curl, no client and no config file:

# What does this server actually expose, right now, to this key?
curl -sS https://api.clipspeed.ai/mcp \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Run that first whenever a server misbehaves. A catalog in the response proves the key, the network path and the server process in one shot, which means every remaining problem is client-side configuration. A 401 or a hang proves the opposite and saves you from debugging a config file that was never the issue. The Accept header matters: streamable HTTP servers may answer with an event stream, and omitting text/event-stream is a common cause of a request that looks rejected but was merely unacceptable.

Whose Credentials Run the Code

Under plain function calling there is no authentication step, because there is no boundary to authenticate across. Your function runs in your process and reaches for whatever that process already holds — an environment variable, an instance role, an open connection pool. Identity is ambient and usually invisible.

MCP makes the boundary explicit and therefore forces you to have an answer. Remote servers authenticate the connection, by one of two routes. GUI clients run an OAuth authorization-code flow: the user clicks through a consent screen and the client stores tokens. CLI and headless clients send a static Bearer token in the Authorization header, because there is no browser to redirect. Local stdio servers typically receive secrets as environment variables from the host's configuration.

ClipSpeedAI supports both routes against one endpoint, https://api.clipspeed.ai/mcp. Its API keys are worth describing concretely, because they illustrate what per-connection identity looks like when a vendor implements it properly rather than handing out one shared secret:

The consequence is that credentials become per-user rather than per-application. The server sees a distinct principal on every connection and can scope its catalog, its quotas and its data access to that principal. Per-key counters and rate limits mean a leaked or misbehaving key is a bounded, observable, revocable problem instead of a diffuse one. That is strictly better for a multi-tenant product and strictly more work for a weekend project. The mechanics of both flows are laid out in MCP Authentication: OAuth and Bearer Keys.

Registering the server with a CLI client is one command, and this is the canonical form:

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

For GUI clients the equivalent is the OAuth flow rather than a header. Verified end-to-end support today covers Claude, Claude Code, Claude Desktop and Windsurf. Cursor, Codex, OpenClaw and Hermes speak the same protocol and verification is in progress. ChatGPT support is vendor-gated and rolling out. Those three statements are deliberately different claims, and a server author should never collapse them into one.

Long Jobs Are Nobody's Solved Problem

Neither mechanism gives you long-running work for free, and the mistake is so common it deserves its own section. A tool call is a request and a response. If the underlying job takes eight minutes, you cannot hold the model's turn open for eight minutes; you need a handle and a second tool that reads it.

The correct pattern is identical on both sides of the comparison. One tool starts the work and returns an identifier immediately. Another takes that identifier and reports status or results. The agent loop supplies the waiting behaviour on its own, because the model calls the status tool, reads "still processing", and decides to call it again.

START AND POLL

  start(input)        -> { id, status: "queued" }
        |
        v
  status(id)          -> { status: "processing" }      model waits, calls again
        |
        v
  status(id)          -> { status: "done", results: [...] }

  ANTI-PATTERN: one tool that blocks until the job finishes.
  It hits the client timeout, and the retry starts a SECOND job.

ClipSpeedAI's live-clipping tools are a worked instance of the pattern with an extra wrinkle: the job has no natural end. clip_livestream starts a real-time session against a stream URL and returns a subscriptionId. check_livestream polls that id; a status of monitoring means the stream is still live and still being clipped. Because the session runs until something stops it, the design needs two more verbs — one to extend an active session and one to end it, with clips already produced kept and still downloadable. A recorded video does not need that; an open-ended one does. If live sessions are your actual use case rather than a thought experiment, Livestream Clipping API: Clip While You Stream goes into the operational detail.

What MCP contributes here is a session concept: a streamable HTTP connection can carry a session identifier so successive calls are recognisably one conversation, and SSE lets the server stream progress notifications while a call is in flight. What MCP does not contribute is durability. If the server process restarts, the handle has to remain resolvable from the server's own storage or the work is orphaned. Make the start tool idempotent wherever the domain allows, so that a retried start does not quietly create a duplicate job and a duplicate bill.

Podcast to Clips, Written Both Ways

Take a job specific enough to be checkable: submit a recorded podcast for clipping with a chosen caption style, then fetch the finished clips. The corresponding real tools are submit_to_clipspeed and check_clips, so the two implementations can be compared honestly rather than in the abstract.

Hardcoded. You author both schemas, both implementations and the dispatch table. You hold the API key inside your process. Every other application in your company that wants clipping repeats the whole exercise, and each copy drifts independently when the upstream API changes.

# Python -- the function-calling approach, sketched.
TOOLS = [
  {"name": "submit_to_clipspeed",
   "description": "Submit a video URL for clipping. Returns a projectId.",
   "input_schema": {"type": "object",
                    "properties": {"videoUrl":     {"type": "string"},
                                   "captionStyle": {"type": "string"},
                                   "orientation":  {"type": "string"}},
                    "required": ["videoUrl"]}},
  {"name": "check_clips",
   "description": "Fetch the finished clips for a projectId, each with a title, "
                  "viral score and download URL.",
   "input_schema": {"type": "object",
                    "properties": {"projectId": {"type": "string"}},
                    "required": ["projectId"]}},
]

DISPATCH = {
  "submit_to_clipspeed": lambda a: my_client.submit(a["videoUrl"],
                                                    a.get("captionStyle"),
                                                    a.get("orientation")),
  "check_clips":         lambda a: my_client.clips(a["projectId"]),
}

# ...then the loop: send TOOLS, read tool_use blocks, call DISPATCH[name],
# send tool_result back, repeat. Every line of that is yours to maintain.

Over MCP. You author none of it. You connect, list and call — and the same connection also carries the caption-template lookup, the trending-video search, the per-clip title and posting-time pack, the YouTube publisher and the four live-session verbs, none of which you defined, versioned or tested.

# Python MCP client over streamable HTTP (official mcp SDK).
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

URL     = "https://api.clipspeed.ai/mcp"
HEADERS = {"Authorization": "Bearer " + API_KEY}

async def main():
    async with streamablehttp_client(URL, headers=HEADERS) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()

            tools = await session.list_tools()
            print([t.name for t in tools.tools])      # catalog discovered at runtime

            styles = await session.call_tool("list_templates", {})
            print(styles.content)                     # karaoke, hormozi, beasty,
                                                      # fire, youshaei, cinematic

            started = await session.call_tool(
                "submit_to_clipspeed",
                {"videoUrl": "https://example.com/podcast.mp4",
                 "captionStyle": "karaoke",
                 "orientation": "vertical"},
            )
            print(started.content)                    # contains the projectId

            # later, with that projectId in hand:
            # clips = await session.call_tool("check_clips", {"projectId": pid})

asyncio.run(main())

The instructive part is what stayed constant. The model's behaviour is byte-for-byte comparable across both versions: it sees two tools with the same names and the same argument shapes, and it emits the same tool-use blocks. Nothing about the model's reasoning improved because a protocol appeared underneath. What changed is ownership — who authored the schema, whose process executes, whose credentials are in play, and who ships a fix when the upstream API adds a field. That is the entire trade, stated without decoration.

Two neighbouring pages pick up from either end of this example. The client-side wiring for an interactive agent is walked through in ClipSpeedAI MCP for Claude Code: Complete Setup Guide. What the clipping tools do once called, independent of protocol, is the subject of AI Clipping API: Programmatic Short-Form Video.

The Differences, Row by Row

Having seen both encodings, the compressed comparison reads correctly rather than as a list of assertions. Each row restates something demonstrated above.

DimensionPlain function callingMCP
Layer it occupiesModel <-> applicationApplication <-> tool provider
Where the catalog comes fromArray literal in your codetools/list, at runtime
When the catalog can changeDeploy timeMid-session, via list_changed
Who authors the schemaYouThe server author, possibly a third party
Whose process executesYoursThe server's
TransportNone; in-processstdio or streamable HTTP
IdentityAmbient — your app's own credentialsPer-connection — OAuth or Bearer key
Reuse in a second appCopy the codePoint another client at the URL
Changing model vendorReshape every schema and parserServer is neutral; the host reshapes
Integration costN clients × M toolsN clients + M servers
Ways it can failExceptions in your processPlus connection, handshake, auth, timeout
Per-call costA function callPlus serialisation and at least one hop

Only one row is a capability difference. The rest describe where responsibility sits. MCP does not enable a model to do anything it could not do before; it relocates the work of building and maintaining the plumbing, and relocation is worth paying for precisely when the plumbing would otherwise be built many times.

Cases Where the Protocol Is Pure Overhead

An honest reference has to argue against itself, because the ecosystem's marketing will not.

There is a prior question underneath all of these: whether the caller is a model at all. If a deterministic program is doing the calling, an ordinary HTTP interface is the better fit and by far the easier thing to version. That comparison has its own page — MCP vs REST API: When to Use Each — and it is the right place to start if you are not certain a model belongs in the loop.

Cases Where a Hardcoded Array Becomes a Liability

The converse cases are equally concrete, and each one is a boundary the array cannot cross.

A useful shorthand: MCP earns its complexity at an organisational boundary. Inside one team's one codebase it is usually overhead. Across teams, across companies, or across client applications it is usually the cheapest option on the table.

Claims That Break When You Read the Wire Log

Each of the following is common, and each dissolves the moment you look at actual traffic between a host and a server.

"MCP replaces function calling." It feeds function calling. The model still receives schemas and still emits tool-use blocks; the host merely fetched those schemas over JSON-RPC rather than reading a local array. Both are present in every MCP session, at different layers.

"MCP is an Anthropic-only technology." It originated at Anthropic and was published as an open specification. A server has no idea which model sits on the other end — it answers tools/list and tools/call and nothing in either method mentions a vendor. What varies in practice is which client applications have shipped support and how complete that support is.

"MCP is faster." It is slower per call than an in-process function, unavoidably, because it adds serialisation and at least one hop. What it saves is engineering time, not milliseconds, and conflating the two leads to adopting it for the wrong reason and then being disappointed by the wrong metric.

"An MCP server is a REST API with different vocabulary." The differences are substantive: JSON-RPC framing instead of resource paths, a discovery method returning machine-readable schemas intended to be read by a model, a capability handshake, server-initiated notifications, and typed content blocks instead of arbitrary response bodies. Putting an MCP server in front of an existing HTTP API is a good idea and a common one; that does not make them the same interface.

"More connected servers means a smarter agent." More connected servers means a longer catalog. Descriptions compete for attention and overlapping ones make the model's choice harder, so adding servers indiscriminately works against the thing you were trying to improve.

"The model runs the tool." It never does, under either mechanism. The model emits a request and the host decides whether to execute it. Every approval prompt you have seen in an MCP-capable client lives in that gap; disabling those prompts is a policy choice, not a protocol feature.

"MCP handles retries, timeouts and long jobs." It handles none of them. Those belong to the server author, which is why the start-and-poll shape earlier on this page matters more than any protocol detail.

"The package installed, so it works." Installing a package registers a server with a client. It proves nothing about credentials, network reachability, or whether the server process is up. A tools/list request is the check that proves all three, and it takes one line.

Porting an Existing Dispatch Table

If you already have a working function-calling implementation and the boundary argument has convinced you, the migration is mostly mechanical. Your schemas were the hard part and they survive intact.

  1. Rename input_schema to inputSchema and return the array from tools/list.
  2. Move each dispatch entry behind tools/call, reading arguments from params.arguments and returning content blocks rather than bare strings.
  3. Convert domain failures into isError: true plus an explanatory text block. Reserve JSON-RPC errors for protocol-level problems.
  4. Replace ambient credentials with a per-connection principal derived from the Bearer token or OAuth session, and give that principal its own rate limit and counters.
  5. Split any tool that blocks for more than a few seconds into a start tool and a status tool.
  6. Re-read every name and description as if you were the model, not the author.

Step six is the one teams skip and later regret. In a hardcoded setup a vague description costs almost nothing, because you can observe the whole loop and fix it in an afternoon. Once the server is public, an ambiguous argument name becomes wrong tool selections inside agents you cannot see, reported to you as "it does not work" with no trace attached. Budget real time for the wording.

Before wiring a host to a server you just built, probe it directly. A twenty-line client tells you more than any log line:

// Minimal Node probe with the official SDK.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport }
  from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const transport = new StreamableHTTPClientTransport(
  new URL("https://api.clipspeed.ai/mcp"),
  { requestInit: { headers: { Authorization: `Bearer ${process.env.API_KEY}` } } }
);

const client = new Client({ name: "probe", version: "1.0.0" }, { capabilities: {} });
await client.connect(transport);

const { tools } = await client.listTools();
for (const t of tools) console.log(t.name, "-", t.description);

await client.close();

Building the server side rather than the client side is a larger topic, and How to Build an MCP Server (Practical Guide) takes it from an empty directory to a deployed endpoint.

Six Questions, Then Three Commands

Work down this list and stop at the first clear answer. Most decisions resolve in the first three.

  1. Is the caller a model? If deterministic code will call this, write an ordinary API and stop. Neither mechanism applies.
  2. Will anything other than this application ever use these tools? If no, use plain function calling. Wrapping existing functions in a server later is a small job, and doing it prematurely is not.
  3. Do you control every consumer? If yes, hardcoding stays viable. If third parties or non-developers are consumers, MCP.
  4. Does the tool need local filesystem or device access? If yes, a local stdio server. If it only calls your API, remote HTTP, and your users install nothing.
  5. Does the catalog vary by user or change often? If yes, MCP — runtime listing is the whole point and the only thing you cannot reproduce cheaply.
  6. Is per-call latency the binding constraint? If yes, keep it in-process regardless of every answer above.

Once you have chosen and wired something up, verification is three commands rather than an opinion. Send tools/list over curl to prove the key, the route and the server. Make one real tools/call to prove the tool executes and returns what its description promised. Register the server in the client you actually use and drive it once end to end, because host wiring fails independently of everything else. Something that passes all three is integrated. Something that passes only the first is merely configured, and the difference tends to surface at the least convenient moment.

The compressed version, for anyone who skipped here: function calling is the model emitting a structured request; MCP is the machinery that determines which requests exist, who executes them, and under whose credentials. You always have the first. You adopt the second when tools have to cross a boundary — between processes, between applications, or between companies. If your boundary happens to involve video, MCP for Video Editing and Clipping Workflows is the applied version of everything above.

Frequently asked questions

Does MCP replace function calling?
No — it sits underneath it. MCP delivers tool definitions to a host, and the host presents them to the model in that model's native tool-calling format. The model still emits a tool-use block and the host still executes it. Remove function calling and nothing can be requested at all; remove MCP and you fall back to a hardcoded array of tools.
What is the actual difference between input_schema and inputSchema?
Only casing and context, but the mismatch is a real source of bugs. MCP's tools/list returns each tool with an inputSchema field in camelCase. Anthropic's messages API expects input_schema in snake_case. The JSON Schema inside is identical, so a host that bridges the two only has to rename the key — but forgetting to rename it produces a tool the model never calls, with no error to explain why.
Can one application use both MCP servers and hardcoded functions?
Yes, and most non-trivial agents do exactly that. A host can merge tools discovered from several MCP servers with its own built-in functions into one catalog before sending it to the model. The model sees a single flat list and has no way to tell which entries were discovered over JSON-RPC and which were compiled in.
Does MCP work with models from vendors other than Anthropic?
The protocol is model-agnostic by construction. An MCP server never learns which model is on the other end — it answers tools/list and tools/call, and the host translates into whatever format its model expects. What varies in practice is which client applications have shipped MCP support and how thoroughly that support has been verified.
What arguments do ClipSpeedAI's submit_to_clipspeed and check_clips actually take?
submit_to_clipspeed requires videoUrl and optionally accepts captionStyle, orientation (vertical or landscape), count and videoId. It returns a projectId. check_clips requires that projectId and returns the finished clips, each with a title, a viral score and a download URL. The argument names are camelCase — video_url and job_id do not exist and will fail validation.
How should an MCP tool report a failure the model can recover from?
Return a normal result with isError set to true and a text content block explaining what went wrong. Reserve JSON-RPC error objects for protocol-level failures such as an unknown method or malformed parameters. The distinction matters because an isError result reaches the model, which can then correct an argument or choose a different tool, whereas a protocol error is usually absorbed by the host.
How do long-running jobs work over MCP?
The same way they should work without it: split the work into two tools. One starts the job and immediately returns an identifier; the other takes that identifier and reports status or results. A single tool that blocks until completion will hit the client timeout, and the retry will typically start a duplicate job. MCP adds a session identifier and progress streaming, but it adds no durability — the handle must survive a server restart on the server's own storage.
What is the fastest way to tell whether an MCP server is genuinely working?
POST a tools/list JSON-RPC request with curl, including your Authorization header and an Accept header covering both application/json and text/event-stream. A catalog in the response proves the credential, the network path and the server process at once. A 401 or a timeout proves the problem is upstream of any client configuration, which saves you from editing config files that were never the cause.
Is a ClipSpeedAI API key recoverable if I lose it?
No. Only the key prefix is stored for display — the first 18 characters followed by an ellipsis — so the full csai_live_ value is shown once at creation and never again. Generate a replacement with POST /auth/api-keys and revoke the old one with DELETE /auth/api-keys/:id, which sets is_active to false and stamps revoked_at.

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 →