Remote MCP vs Local MCP Servers: A Developer's Guide

Take one tool — say a tool that fetches finished video clips for a job id. Give it a name, a JSON Schema for its arguments, and a description the model can read. You can now ship that tool two ways. In the first, your MCP client starts a program on your laptop and writes the call into its standard input. In the second, your client opens a TLS connection to a URL and puts the identical call in an HTTP request body. Nothing about the tool changed. The bytes took a different road.

That is the entire local-versus-remote distinction, and stating it that flatly saves a lot of argument. Model Context Protocol splits cleanly into a message layer (JSON-RPC 2.0, with initialize, tools/list, tools/call and friends) and a transport layer that decides how those frames physically move. Local servers use stdio. Remote servers use streamable HTTP. The message layer does not know or care which one is underneath.

What does change is everything the protocol deliberately leaves alone: who the server runs as, how a new user gets it, how long a unit of work is allowed to live, what hardware it needs, who finds out first when it breaks, and where the data is permitted to sit. Those six questions decide the answer far more reliably than any feature comparison, because on features there is nothing to compare. This page works through them with wire frames, real SDK code, a curl session against a live endpoint, and one extended case study of a hosted server — ClipSpeedAI's — where the reasoning is unusually legible. If the protocol vocabulary here is new, What Is MCP? Model Context Protocol Explained is the gentler entry point, and the request-flow diagrams in How MCP Servers Work: Architecture and Request Flow pair well with the traces below.

On this pageWhere the Line Is Actually DrawnOne Session, Traced Down Both PathsSix Questions That Decide ItThe Subprocess ContractOne URL, Two Response ShapesAmbient Identity Versus Presented IdentityWriting the Server Once and Flipping the TransportProbing a Hosted Endpoint With curlCase Study: Why ClipSpeedAI's Server Sits on the NetworkClient Coverage Is a Tier, Not a Yes or NoWho Holds the StateThe Wrapper That Looks Local and Behaves RemoteFailure Taxonomy: What Breaks and Who NoticesTwo Different Risks, Neither of Them ZeroWrong Answers That Sound RightPromoting a Local Server to a Hosted OneDeciding in Under a Minute

Where the Line Is Actually Drawn

A tool call is a JSON-RPC request object. It has a method, a params object, and an id the client uses to match the reply. Here is one, and it is byte-identical whether the server lives in a subprocess or in a data center:

{"jsonrpc":"2.0","id":7,"method":"tools/call", "params":{"name":"check_clips","arguments":{"projectId":"proj_abc123"}}}

Now the only part that differs:

LOCAL that JSON, on one line, written to the child process's stdin; the reply arrives on the child's stdout REMOTE that JSON as the body of an HTTP POST to a single URL; the reply arrives as a JSON response body, or as a sequence of Server-Sent Events on the same connection

Everything a model sees — the tool list, the schemas, the descriptions it reasons over, the result content blocks it reads back — is produced by the message layer and is untouched by the choice. A model cannot tell you which transport it just used, because that information never reaches it.

Two practical consequences fall straight out. First, a capability that can be expressed as a tool can be shipped either way, so "which is more powerful" is not a real question. Second, since the same handler code can sit behind either transport, most SDKs let you register tools once and pick the transport at startup. That is a good indication this is a deployment decision rather than an architectural one.

One Session, Traced Down Both Paths

Sequence first, because the sequence is shared. The client sends initialize, the server replies with its protocol version and capabilities, the client sends an initialized notification, and only then does normal traffic start. Every MCP session begins this way regardless of transport.

Over stdio the trace looks like a pipe conversation, and there is no addressing of any kind — the child process has exactly one peer:

client: spawn ["node", "./server.js"] (pid 48213)
client -> stdin {"jsonrpc":"2.0","id":1,"method":"initialize", ...}
server -> stdout {"jsonrpc":"2.0","id":1,"result":{"protocolVersion": ...}}
client -> stdin {"jsonrpc":"2.0","method":"notifications/initialized"}
client -> stdin {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
server -> stdout {"jsonrpc":"2.0","id":2,"result":{"tools":[ ... ]}}
server -> stderr "warm cache loaded" (logs go here, never stdout)

Over streamable HTTP the same five messages become four independent HTTP requests, each of which must carry its own identity and may land on a different server instance:

POST /mcp POST /mcp
Authorization: Bearer <key> Authorization: Bearer <key>
Content-Type: application/json Mcp-Session-Id: <id from initialize>
Accept: application/json, Accept: application/json, text/event-stream text/event-stream {"id":1,"method":"initialize"} {"id":2,"method":"tools/list"} -> 200 application/json -> 200 application/json -> Mcp-Session-Id: 9f2c... or text/event-stream frames

Read the two traces side by side and the shape of the difference appears without any editorializing. The stdio version has a process id and no credential. The HTTP version has a credential on every line and no process id. Local servers inherit an identity; remote servers are handed one. Nearly every downstream property — who can call the thing, what it can touch, how you cut someone off — is a restatement of that sentence.

Six Questions That Decide It

Feature lists are useless here because the feature set is identical. These questions are not.

Who is the server running as? A stdio server runs as you, with your file permissions, your SSH agent, your cloud credential files, your loopback ports. A hosted server runs as a service account inside someone's infrastructure and has no path to your disk at all.

How does a new user get it? Locally: install a runtime, install a package at the right version, edit a config file, provision any secrets it needs — per client application, if they use more than one. Remotely: a URL and a credential.

How long does a unit of work get to live? A subprocess dies when the client quits. If your work outlasts the editor window, a stdio server is the wrong container for it, and no amount of engineering inside the handler changes that.

What hardware does the work need? Transcoding video, running large models, or holding tens of gigabytes resident is not laptop work. Reading a git worktree is not data-center work. This one is usually decided before you start.

Who sees the failure? A local crash is one person's problem and one person's stderr. A hosted outage is everyone's problem, and you find out from your own metrics before the reports arrive.

Where is the data allowed to be? Tool arguments go wherever the server is. For a stdio server they go nowhere. For a hosted server they cross a boundary into someone else's system and are probably written to a log there.

PropertyLocal (stdio)Remote (streamable HTTP)
Process lifetimeOwned by the client, one launch at a timeOwned by the operator, continuous
Cost to onboard a personRuntime, package, config file, secretsA URL and a credential
Identity presentedThe OS user account, implicitlyA token, explicitly, on every request
Disk it can readYoursIts own; never yours
With no networkWorks, unless the tool itself calls outNothing works
Overhead per callLocal pipe overhead; negligible next to the work itselfTLS plus a network round trip
Compute ceilingThe machine it is sitting onWhatever the operator provisions
Shipping a fixEvery user reinstalls, on their own scheduleOne deploy
Durable stateOnly what it writes to diskServer-side storage, by default
Ten usersTen processes, ten versionsOne service, ten principals
Where logs landOn the user's machine, in stderrCentralized, with the operator
Radius of one bad releaseA single userEveryone connected, at once
Cutting off accessHope the config gets cleaned upRevoke one credential

The Subprocess Contract

A stdio server is an ordinary program that happens to speak JSON-RPC on its standard streams. No port, no listener, no URL, nothing to curl. It is also not a daemon you start — the client starts it, and that ownership drives the rules.

Consider a plausible local server: one that answers questions about the git repository you are sitting in. It shells out to git log, reads working-tree files, and knows which branch you are on. Every one of those abilities exists only because the process is on your machine, running as you, with your working directory. Hosting that server somewhere else would not make it slower; it would make it meaningless.

Three rules follow from the subprocess relationship, and breaking any of them accounts for most first-week failures:

Client-specific mechanics for spawning a server — which file, which keys, which directory — vary by application and are not part of the MCP specification. Follow the setup documentation for whichever client you use rather than copying a block between them; ClipSpeedAI MCP for Claude Desktop: Complete Setup Guide covers that ground for one of them, and How to Build an MCP Server (Practical Guide) covers writing the server that config points at.

One URL, Two Response Shapes

Streamable HTTP collapses a remote MCP server down to a single endpoint. ClipSpeedAI's is https://api.clipspeed.ai/mcp, and every message in every direction goes through it.

The flexibility is in the response. A fast call can be answered with a plain JSON body that closes immediately. A slower or chattier call can be answered with Content-Type: text/event-stream, after which the server pushes a sequence of events — progress notifications, log messages, and finally the result — down the open connection. The server picks per request. That is why a correct client always sends Accept: application/json, text/event-stream: it is declaring that either shape is fine, and a server that sees only one of the two may legitimately refuse.

Sessions are optional and worth understanding before you assume anything about affinity. If a server wants continuity it returns an Mcp-Session-Id header on the initialize response; the client then echoes that header on subsequent requests, letting the server route you back to the right state. A stateless server omits the header and treats every request as self-contained. Both are valid, and the failure mode of assuming otherwise is the classic one: behind a load balancer, two consecutive calls hit two different instances, and in-memory state you were counting on is simply not there. Either keep the server stateless with shared storage behind it, or use the session header deliberately and make sure your routing honours it. An older two-endpoint HTTP+SSE transport predates streamable HTTP and still turns up in the wild; the FAQ at the end explains how the two relate.

Ambient Identity Versus Presented Identity

Local servers usually have no authentication, and that is the right design. The trust boundary is the operating-system account. A process that already runs as you gains no new authority by speaking MCP. What a local server needs is not authentication but credentials for the things it calls — a database URL, a repository token — which is why local config almost always carries an environment block.

That environment block is the real hazard of the local model. Secrets sitting in plaintext inside a JSON file, often in a directory that gets synced or backed up, and trivially pasted into a bug report by someone trying to be helpful. Where your client supports indirection — a keychain reference, or a wrapper script that fetches the value at spawn time — take it.

Hosted servers have no ambient trust to inherit, so identity has to travel with each request. Two patterns cover almost everything in production:

ClipSpeedAI runs both against the same endpoint: OAuth for graphical clients, and a Bearer key for CLI clients. The key mechanics are worth reading as a concrete instance of what a hosted server owes its users. Keys are issued as csai_live_ followed by 48 hexadecimal characters, generated from 24 random bytes. You create one with POST /auth/api-keys, or from the app: open Account, find the API & Integrations panel, press Generate API Key. The server stores only the key's prefix for display purposes — the first 18 characters plus an ellipsis — so the full string is shown once, at creation, and cannot be recovered afterwards. Losing it means issuing a new one.

Listing keys with GET /auth/api-keys returns the metadata that makes a static credential manageable rather than mysterious: an id and name, the stored prefix, the plan, the key's rate limit, requests today, total requests, the last request timestamp, whether it is still active, and when it was created. Revocation is real: DELETE /auth/api-keys/:id flips the key inactive and stamps a revocation time. That last property is the one to look for in any hosted MCP service you adopt, because it is what turns "someone pasted a key into a screenshot" from an incident into a chore. The flows themselves get a fuller treatment in MCP Authentication: OAuth and Bearer Keys, and the question of what a token should be allowed to do once it exists belongs to MCP Security: Scopes, Keys and Safe Tool Design.

Writing the Server Once and Flipping the Transport

The fastest way to internalize the split is to write one server and change one line. In Python, using the official SDK's FastMCP helper:

from mcp.server.fastmcp import FastMCP mcp = FastMCP("line-tools") @mcp.tool()
def count_lines(path: str) -> int: """Count the lines in a UTF-8 text file.""" with open(path, encoding="utf-8") as fh: return sum(1 for _ in fh) if __name__ == "__main__": # Local: the client spawns this process and pipes JSON-RPC. mcp.run(transport="stdio") # Remote: identical tools, served over HTTP instead. # mcp.run(transport="streamable-http")

Look at what the swap does to the meaning of count_lines. Over stdio, path refers to the user's own disk and the tool is useful. Over HTTP, path refers to the server's disk: the tool is useless to the caller and has become a directory-traversal probe against the operator. The transport flag is one line; the semantics of every filesystem-touching tool inverted underneath it. Going remote is a redesign of what your tools mean, not a configuration change.

The Node equivalent, using the TypeScript SDK's stdio transport. The registration call below is the three-argument (name, schema, handler) form; later SDK releases changed that signature, so check the typings in the version you actually installed before pasting this anywhere:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod"; const server = new McpServer({ name: "line-tools", version: "1.0.0" }); server.tool( "count_lines", { path: z.string().describe("Absolute path to a UTF-8 text file") }, async ({ path }) => { const fs = await import("node:fs/promises"); const text = await fs.readFile(path, "utf8"); return { content: [{ type: "text", text: String(text.split("\n").length) }] }; }
); // console.log() here would corrupt the stream. Use console.error().
await server.connect(new StdioServerTransport());

Keep handler bodies free of transport assumptions from day one — no reading process.stdin, no writing process.stdout, no reliance on the current working directory — and the eventual move to HTTP stays mechanical. Bake those assumptions in and the migration turns into a rewrite. Tool shape matters at least as much as transport here, and MCP Tool Design: Writing Tools an Agent Can Actually Use is the companion piece on getting schemas and descriptions right.

Probing a Hosted Endpoint With curl

A hosted MCP server is HTTP all the way down, which gives you a diagnostic that has no local equivalent: you can drive it by hand and find out, in one command, whether the problem is the server, the credential, or your client's configuration. Start with the handshake:

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": "initialize", "params": { "protocolVersion": "<PROTOCOL_VERSION>", "capabilities": {}, "clientInfo": { "name": "curl-probe", "version": "0.0.1" } } }'

protocolVersion is a dated spec-revision string, of the form 2025-06-18. Send whichever revision your client library targets rather than a value copied off a web page — the string moves forward, the page does not — and read the version the server echoes back, which may be older than the one you offered.

Then enumerate what the server exposes:

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":2,"method":"tools/list","params":{}}'

The status code is the whole diagnosis. A 401 or 403 means the credential is wrong, revoked, or missing its Bearer prefix — compare the start of the key you pasted against the prefix the key-list endpoint reports for the key you think you are using. A 404 means the path is wrong, usually a truncated URL. A 429 means you have hit the per-key rate limit rather than anything being broken. A clean tool list means the server and your credential are both fine and the fault is in the client configuration, which narrows the search enormously.

For Claude Code the configuration is a single command, and it is the one config incantation on this page you can copy verbatim:

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

ClipSpeedAI MCP for Claude Code: Complete Setup Guide walks through the rest of that flow, including what to do when the command succeeds but the tools do not appear.

Case Study: Why ClipSpeedAI's Server Sits on the Network

Abstract trade-offs get slippery, so here is a hosted server whose reasoning can be checked against its own tool list.

The unit of work is a render, not a query. Analysing a video and producing captioned vertical cuts is sustained compute against large files. That is not a thing you ask a laptop to do underneath an editor session, and it is not a thing you want dying because someone closed a window.

The work outlives the client. Look at the live-mode tools. clip_livestream starts clipping a stream in real time and hands back a subscriptionId. check_livestream polls that subscription — a status of monitoring means the stream is still live and still being clipped. extend_livestream keeps an active session going, and stop_livestream ends it, with clips already produced kept and still downloadable. A subscription that survives independently of any one client is only possible when something other than a client-owned subprocess is holding it.

The catalogue is defined once. Ten tools live behind that URL. discover_trending finds the fastest-growing recent video in a niche worth turning into shorts, searching only videos published in roughly the last three weeks. submit_to_clipspeed drops a video URL or file in — the clip button. check_clips returns the finished, scored, captioned 9:16 clips for a project id, each with a title, a viral score and a download URL. creator_pack returns per-clip suggested titles, hooks and best posting times. list_templates enumerates the caption styles, whose real ids are karaoke, hormozi, beasty, fire, youshaei and cinematic — you pass the one you want as captionStyle. publish_to_youtube pushes a finished clip up and defaults to private, taking a project id plus an optional clip id, title and privacy status. The four livestream tools above complete the set. Amending any of them is a deploy, and the next tools/list anywhere picks it up.

The interaction pattern is submit-then-fetch, which is the shape hosted servers handle well and stdio servers handle badly. submit_to_clipspeed accepts the video and returns a project rather than holding a connection open for the duration of a render; check_clips retrieves the finished clips with their titles, viral scores and download URLs once the work is done. Note what that pattern buys structurally: no request timeout to negotiate, no keep-alive to babysit, and something sensible for the agent to do in between.

agent hosted MCP endpoint render workers | submit_to_clipspeed(url) | | | --------------------------> | enqueue ---------> | analyse | { project accepted } | | score | <-------------------------- | | caption | | | reframe 9:16 | ... the render proceeds on its own ... | | | | | check_clips(projectId) | <----------------- | done | --------------------------> | | | clips: title, score, url | | | <-------------------------- | |

The publish_to_youtube default is worth pausing on for reasons that generalize beyond this product. Publishing is the one irreversible action in the set, and it defaults to private — the destructive-adjacent operation carries the conservative default. When you design tools for a hosted server, that is the pattern to copy, because a hosted server executes whatever an agent decides to call, and the agent's decision is downstream of text it read somewhere.

Access is paid. A single demo run is available without a card, limited to one video under 30 minutes; past that, a one-time $1 charge opens a three-day trial that rolls onto whichever plan you chose unless you cancel first. The plans are Starter at $15 a month, Pro at $29 and Ultra at $49, with annual billing cutting the rate in half. No permanently free tier exists. The workflow-level view of all this, rather than the transport-level one, is in MCP for Video Editing and Clipping Workflows, and live mode gets its own deeper treatment in Livestream Clipping API: Clip While You Stream.

Client Coverage Is a Tier, Not a Yes or No

Here is a distinction that gets flattened constantly, including by vendors: "our server speaks streamable HTTP" and "we have verified that client X works" are different claims, and only the second one helps you. Any client that implements the transport correctly should work against any compliant endpoint. Should is doing real work in that sentence.

ClipSpeedAI publishes its coverage in three honest tiers, and the shape is worth copying:

Read that middle tier the way its author intended: a well-founded expectation, not a promise. If you are standardizing a team on Cursor, budget an afternoon to prove the connection yourself instead of treating the tier as a guarantee — ClipSpeedAI MCP for Cursor: Complete Setup Guide is the starting point, and ClipSpeedAI MCP for Windsurf: Complete Setup Guide covers a verified sibling if you want a known-good comparison to test against.

For any client not named in the first tier, resist the urge to copy a configuration block from elsewhere on the internet. The outer key names, the field that holds the URL, the way headers are expressed and the location of the file are all client-specific, and none of them are part of the MCP specification. The durable instruction is the shape, not the syntax: register ClipSpeedAI as an HTTP MCP server pointing at https://api.clipspeed.ai/mcp, with your key in an Authorization: Bearer header, following that client's own MCP documentation for exactly where those two values go. For graphical clients that support the connector flow, ClipSpeedAI MCP for Claude (claude.ai): Complete Setup Guide shows the OAuth path, where no key is typed anywhere.

Who Holds the State

Statefulness is where the two models stop being symmetrical, and it catches people from both directions.

A stdio server is ephemeral by construction. Whatever it holds in memory evaporates when the client exits, so anything durable has to hit the disk — SQLite, a JSON file, a directory of artefacts — and the moment it does, you have inherited the concurrency question of two client applications running two copies against the same files. There is no coordination layer; you are writing one.

A hosted server is stateful by default, which is usually what you want and occasionally a trap. The trap is assuming request affinity the transport never promised you. Two calls from one client can land on two instances. Either hold nothing in process memory and put every piece of state in shared storage, or use Mcp-Session-Id deliberately and make your routing respect it. Choosing implicitly is how you get a bug that only reproduces under load.

For anything slow, prefer an explicit job model over a long-held request, whichever transport you are on. Return an identifier immediately, expose a second tool that fetches results, and let the client poll. It survives restarts, network blips and proxy timeouts, and it gives an agent something coherent to reason about while waiting. Progress notifications over SSE complement that design; they do not replace it, because a notification stream that drops takes your only completion signal with it.

The Wrapper That Looks Local and Behaves Remote

There is a third arrangement that confuses the taxonomy: a small stdio server on your machine that forwards every call it receives to a hosted HTTP service. Your configuration file says local. Your data says remote.

The pattern exists for good reasons. It gives a one-line install to clients whose remote support is immature or absent, it can hold an OAuth flow and token refresh on the local side where a browser exists, and it lets a vendor bridge an older transport to a newer one without asking users to change anything. Adapters of exactly this kind are common wherever a client's transport support lags the rest of the ecosystem.

Evaluate one as remote in every dimension that matters. Your tool arguments still cross the network. Your uptime is still the provider's uptime. Your latency still includes the round trip. And you have added a component that can be out of date independently of both ends. The one genuine advantage is credential handling: a local wrapper can pull a key from the OS keychain at spawn time and keep it out of any file on disk, which beats pasting a static key into JSON.

client --stdio--> local wrapper --HTTPS--> hosted service (holds the token, forwards the frames) Local in your config file.
Remote for security, privacy, uptime and latency —
which is to say, remote.

Failure Taxonomy: What Breaks and Who Notices

The two transports do not fail in comparable ways, and knowing which list you are debugging saves most of the time.

Local servers mostly fail before they run. Command not found. Wrong interpreter version. A package installed for a different Python. A PATH that exists in your terminal but not in the environment a desktop application inherits. Then, at runtime: a stray write to stdout, a crash during initialize that manifests as silence, or version drift where the user's copy is four weeks behind yours. Work the list in this order — run the exact command from your configuration by hand in a terminal; switch to absolute paths for interpreters and scripts, which resolves a surprising share of "works in my terminal" reports; grep the source for prints to stdout; then drive the server with the MCP Inspector to confirm it responds at all before blaming the client.

Hosted servers fail in HTTP terms, so debug in HTTP terms. Reproduce with the curl handshake above before anything else, because it partitions the problem in one command. Then check the mundane causes in order of frequency: a key pasted without its Bearer prefix, a URL missing its path, a corporate proxy or TLS-inspecting middlebox sitting in the connection, a key that was revoked, and rate limiting. The per-key counters exposed by the key-list endpoint — requests today, total requests, last request time — are unusually useful here, because they answer the first real question directly: is the server seeing my calls at all?

The asymmetry worth internalizing is about telemetry, not frequency. When a local server breaks on someone else's machine, your entire diagnostic surface is whatever that person is willing to copy out of a log. When a hosted server breaks, you have structured logs, request ids and traces, and you likely knew before they told you. Neither list is shorter than the other. They are owned by different people.

Two Different Risks, Neither of Them Zero

Nobody should claim one transport is safer. They relocate the risk.

A stdio server runs with your full user privileges and no sandbox. It can read your SSH keys, your browser profile and your cloud credentials, because you can. Installing an MCP server from an unfamiliar source is exactly as consequential as running an unfamiliar binary as yourself, and the friendly tool list at the end does nothing to change that. Pin versions instead of floating on latest, read the source when the server is small enough to read, and scope any file-access server to specific directories rather than the root of your disk.

A hosted server puts a third party inside your workflow. Every tool argument you send is transmitted and probably logged. A compromised credential affects the account rather than one laptop, which cuts both ways: larger blast radius, but a single revocation closes it — more than can be said for a key sitting in a config file on a laptop that left the building. Scope keys narrowly, rotate them on a schedule, prefer OAuth where the client supports it, and keep keys out of repositories and issue trackers.

Both share the risk that actually shows up in incident reports: an agent calling a consequential tool because of text it read somewhere. Tool descriptions and returned content are untrusted input to a model. Design irreversible operations to require explicit confirmation, default them to the conservative setting the way publish_to_youtube defaults to private, and keep read and write capability in separately scoped credentials so a compromise of one is not a compromise of both.

Wrong Answers That Sound Right

Promoting a Local Server to a Hosted One

Most hosted servers began as stdio prototypes, and the promotion is mechanical or brutal depending on decisions made months earlier.

  1. Audit every filesystem and localhost reference first. This is the step that actually costs time, so do it before estimating anything. Each reference becomes one of three things: an uploaded input, a hosted resource, or a tool that stays local forever. Anything in the third bucket means you are shipping two servers, which is fine — just decide it deliberately.
  2. Replace ambient identity with an explicit principal. Every call now has to name who is making it. Decide what a token is scoped to before you issue the first one, because retrofitting scopes onto keys already in circulation is its own project.
  3. Add authorization per tool, not per connection. Authenticated is not the same as permitted to call the destructive one.
  4. Introduce a job model for anything slow. Submit returns an identifier; a second tool fetches results. The submit-then-fetch shape described earlier exists precisely because request-response over the public internet is a poor container for minutes of work.
  5. Build the operational layer. Structured logs with request ids, rate limits, timeouts, health checks, and a versioning policy for tool schemas. Treat descriptions as an API surface — agents depend on their exact wording, so changing a description is a behavioural change, not a docs edit.
  6. Run both for a while. Point a handful of users at the endpoint, diff behaviour against the local build, then cut over.

If a capability genuinely needs both local files and hosted compute, split it rather than compromising: a local server that gathers and uploads, a hosted server that processes. Two servers with one clear boundary beats one server straddling it. That composition is what AI Agent Video Automation: End-to-End Workflows demonstrates end to end, and the hosted half is covered from the API side in AI Clipping API: Programmatic Short-Form Video.

Deciding in Under a Minute

Work down the list and stop at the first match. The questions are ordered by how often they turn out to be decisive, not by importance.

  1. Does it need the user's files, processes or loopback ports? → local, and no amount of hosting cleverness changes that.
  2. Is there a rule that this data must not leave the machine? → local.
  3. Does it need credentials you cannot hand to every user? → hosted. Distributing a production key is not solving key management, it is multiplying it.
  4. Does it need compute or storage a laptop does not have? → hosted.
  5. Must work continue after the client closes? → hosted.
  6. Do you need to ship fixes without an upgrade campaign? → hosted.
  7. Will browser-based clients need it? → hosted; there is no subprocess for them to spawn.
  8. Is the tool called constantly with tiny payloads? → local, or redesign it to do more per call.
  9. Still prototyping? → local first, whatever the eventual answer, for the reason given earlier: the loop is edit, restart, test.

One consideration sits outside the list: are you prepared to operate a service? A hosted MCP server is production software with TLS, uptime, authentication, versioning and someone on call, and a bad release reaches every user simultaneously. That cost is the honest argument for staying local longer than ambition suggests. If the capability you want already exists as somebody else's endpoint, read Best MCP Servers for Video and Content Workflows before writing one — and if you would rather not run anything at all, MCP for Creators: Automating Video Without Code is this same decision from the other end.

Frequently asked questions

Does the transport change what a tool is allowed to do?
Not at the protocol level, and completely at the semantic level. The schema, the description and the invocation are identical either way. What changes is the referent: a path argument means the user's disk on a stdio server and the operator's disk on a hosted one, so the same handler goes from useful to useless-and-dangerous with a one-line transport swap. Audit every filesystem and localhost reference in your handlers before hosting them.
Do I need OAuth for a hosted MCP server, or is a Bearer key enough?
Both appear in production and they serve different clients. OAuth suits graphical clients where a browser is available and yields short-lived, revocable tokens. A static Bearer key is the only workable option for CLIs, CI jobs and headless agents, because there is nowhere to redirect a consent screen. ClipSpeedAI accepts both against the same endpoint — OAuth for GUI clients, and an Authorization: Bearer header for CLI clients. MCP Authentication: OAuth and Bearer Keys covers the flows in detail.
My local MCP server connects but lists no tools. What is wrong?
Nine times out of ten, stdout pollution — a print or console.log writing non-JSON text into the stream the client is parsing. Send every log line to stderr. After that, check for a crash during initialize (visible only in stderr), a PATH mismatch between your shell and a GUI client launched from the dock, and an SDK whose registration signature differs from the snippet you copied.
Can a hosted MCP server read files on my computer?
No. It receives only what your client puts in the arguments of a tool call. There is no path from an HTTP endpoint back to your disk. When a workflow needs both local files and hosted processing, the standard answer is two servers — one local that reads and uploads, one hosted that processes — with an explicit boundary between them.
What happens to work in flight when I close my editor?
With stdio, the subprocess is killed and anything in memory is gone. With a hosted server it depends on the server's design, which is why the job model matters. ClipSpeedAI's live tools are built for exactly this: clip_livestream returns a subscriptionId, check_livestream polls it — a status of monitoring means the stream is still live and being clipped — and stop_livestream ends the session, with clips already produced kept and still downloadable.
If I revoke an API key, is access actually cut off?
On ClipSpeedAI, yes: DELETE /auth/api-keys/:id marks the key inactive and stamps a revocation timestamp. It is worth confirming this for any hosted service you adopt, because a static Bearer key without real revocation is a credential you can never take back. Note also that only the key's prefix is stored for display — the first 18 characters — so the full string is visible once, at creation, and cannot be retrieved later. A lost key is replaced, not recovered.
Is HTTP+SSE the same thing as streamable HTTP?
They are two revisions of the same idea. The older design used a separate SSE endpoint for server-to-client messages alongside a POST endpoint for client-to-server. Streamable HTTP consolidates both into one endpoint that answers with either a JSON body or an SSE stream, chosen per request. Streamable HTTP is current; some clients keep the older transport for backward compatibility, which is why you still encounter it.

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 →