Free account = 1 chapter of every course unlocked
No credit card · Google sign-in in 30 seconds · 20+ free chapters across 25 courses
Start free →
All Courses/MCP Servers and Tool Ecosystems
🔌

MCP Servers and Tool Ecosystems

Build, secure, and ship Model Context Protocol servers. Tools, resources, transport, observability, and production deployment patterns.

12 chaptersFirst chapter free to preview

Who this is for

  • Backend engineers asked to expose an internal API, database or service to an AI assistant without hand-writing a bespoke integration for each client.
  • Platform and developer-experience teams building shared internal tooling that multiple AI clients need to reach.
  • Engineers who already wrote a small MCP server from the quickstart and now face authentication, hosting and review from a security team.
  • Product engineers evaluating third-party MCP servers who need to judge whether one is safe to install.
  • Skip it if you only need one model, in one application, to call one function. Direct tool calling is simpler and you gain nothing from a protocol.
  • Skip it if what you want is prompt-writing technique. Nothing here is about phrasing; it is about wire formats, schemas, transports and access control.

What you need first

  • ·Comfortable in TypeScript or Python. Both have official SDKs and the course material tracks the concepts rather than one language.
  • ·You understand client-server architecture, JSON, and what a schema is for.
  • ·Familiarity with process management and standard input and output, since local servers run as subprocesses.
  • ·Some exposure to HTTP authentication. OAuth is explained from first principles, but knowing what a bearer token is will help.
  • ·Having used an AI client that supports tool calling is useful context, though not strictly required.

The integration problem MCP was built to remove

Before any protocol existed, connecting an AI application to an external system meant writing a bespoke integration. Your assistant needed to read from a ticketing system, so you wrote code that described the ticketing API to that specific model in that specific application's tool format. Then a second application needed the same capability and the work was repeated. Then a third model with a slightly different tool schema arrived, and it was repeated again.

That is the classic N by M problem. With a handful of clients and a handful of systems, the number of integrations multiplies, and every one of them is separately maintained, separately broken and separately out of date. It is the same shape as the problem language server protocols solved for editors: every editor once needed its own integration with every language toolchain, until a common protocol collapsed that into one implementation per side.

The Model Context Protocol, published by Anthropic as an open specification in late 2024, takes the same approach for AI applications. A server describes what it offers once, in a standard form. Any compatible client can discover and use it. Write a server for your internal document store and every MCP-capable client can talk to it without knowing anything about your API.

Three roles are worth keeping distinct, because conversations about MCP frequently muddle them. The host is the application the user interacts with: an assistant, an IDE, an agent runtime. Inside the host, a client maintains a connection to exactly one server. A server exposes capabilities. The one-to-one client-server pairing matters because it is what keeps servers isolated from one another and gives the host a single place to enforce policy.

Underneath, the wire format is JSON-RPC 2.0: requests with identifiers, responses, and notifications with no reply. Nothing exotic. The connection opens with an initialize handshake in which both sides declare protocol version and capabilities, which is how the ecosystem tolerates clients and servers built against different revisions. Understanding that handshake is the single most useful debugging asset when a server appears in a client but none of its tools do.

The thing MCP is not is a model API. It does not run inference, does not decide which tool to call, and has no opinion about prompting. It standardizes the boundary between an AI application and the systems it needs to reach. Everything on the model side stays where it was.

Tools, resources and prompts: the distinction that trips people up

A server can expose three kinds of capability, and the difference between them is not what they contain but who decides when they are used.

Tools are model-controlled. The model sees the list, decides one is relevant, and calls it with arguments it constructs. Tools are the primitive that performs actions and the reason most servers exist. They are also the primitive with consequences, since a tool call is the model reaching into a real system.

Resources are application-controlled. They are addressable, read-only context that the host application chooses to include: a file, a database record, a document, a log. The host, or the user through the host's interface, decides what gets attached. A resource is identified by a URI and can be listed, read, subscribed to for updates, and templated so that a family of items shares one definition.

Prompts are user-controlled. They are named, parameterized templates the host surfaces as an explicit user action, typically a slash command or a menu item. A prompt is the server saying "here is a well-constructed way to use me", which is genuinely valuable when a server's capabilities are non-obvious.

That control axis, model versus application versus user, is the design question to ask about every capability you build. If a model should be able to decide autonomously to pull something, it is a tool. If the user or host should decide what context to attach, it is a resource. If it is a canned workflow a person triggers, it is a prompt. Servers that make everything a tool put the whole surface under model discretion, which is both a reliability and a security decision made by accident.

The protocol also defines capabilities that run in the other direction, from server to client, and these are less well known. Sampling lets a server ask the host to run a model completion on its behalf, so a server can use intelligence without holding its own API key or choosing a model. Roots let the client tell the server which filesystem or URI boundaries it is permitted to operate within, which is a meaningful containment mechanism for anything touching local files. Elicitation lets a server pause and ask the user for additional input mid-operation, rather than failing or guessing.

Notifications matter too. A server can tell the client that its tool list has changed, or that a subscribed resource was updated, which is what allows a server to be dynamic rather than a fixed manifest declared at startup.

Not every client implements every part of the specification. Capability negotiation exists precisely because of this, and building a server that degrades sensibly when a client lacks sampling or elicitation is part of writing one that works outside your own machine.

Transports: where your server actually runs

The protocol defines the messages; the transport defines how they travel. There are two that matter, and choosing between them is mostly a question about where the code executes and who it serves.

Standard input and output is the local transport. The host launches your server as a child process and exchanges JSON-RPC messages over stdin and stdout. There is no network, no port, no authentication layer, and no deployment. The server inherits the trust and the filesystem access of the user who started it.

That simplicity is why almost every local integration uses it, and it has one operational trap worth stating plainly: stdout belongs to the protocol. Any stray print statement or library banner written to stdout corrupts the message stream, and the symptom is a server that fails to connect with an unhelpful parse error. Logging goes to stderr. This is the most common first bug for anyone writing an MCP server, and it costs people hours.

Streamable HTTP is the remote transport. The server is an ordinary web service exposing a single endpoint that accepts POSTed JSON-RPC messages and can stream responses back using server-sent events when a long-running operation needs to report progress. This replaced an earlier two-endpoint HTTP and SSE design, and modern servers should target the newer form while being aware that older clients may still expect the previous one.

Remote hosting changes everything about the problem. Now you have deployment, TLS, availability, sessions, scaling and authentication, none of which existed in the stdio case. Sessions are the subtle one: a stateful server that keeps per-connection state must either pin a session to an instance or externalize that state, or a load balancer will route the second request of a conversation to a process that has never heard of it. Stateless request handling avoids the whole category and is worth designing for deliberately.

The practical decision comes down to who the server is for. A developer tool that touches local files, local repositories or local credentials belongs on stdio, because moving it to a network adds risk and solves nothing. A capability that a whole team or an organization needs, backed by a central system, belongs on HTTP with real authentication. A useful middle path during development is running a remote-style server locally over HTTP, which lets you exercise the authentication code long before deployment.

Distribution matters as much as transport. Local servers are typically launched through a package runner so users do not clone and build anything, and containers give you a reproducible environment when dependencies are heavy. An official registry exists to make servers discoverable, which is a meaningful improvement over passing configuration snippets around, and it also raises the question of how a user judges whether an unfamiliar server is safe to run.

Designing tools a model can use without hand-holding

A working server is not the same as a good one, and the difference shows up in a place server authors rarely control: a context window they do not own, shared with tools from every other server the user happens to have connected. Your surface is not evaluated on its own. It competes for the model's attention alongside a filesystem server, a ticketing server and whatever else is loaded, which is the constraint that should drive most of the decisions below.

That competition is the real argument against mechanically wrapping an existing REST API route by route. Thirty tools whose names all begin with the same product noun are hard to tell apart from the inside, and a name that reads unambiguously in your codebase can collide conceptually with a tool from an unrelated server. Curate to the operations a user would actually name out loud, and let one tool orchestrate several internal calls where that is what the task requires.

The protocol gives you two naming fields for a reason. The programmatic name is the identifier the model selects on and should be specific enough to survive the company of strangers. The human-readable title is what a host displays in a permission dialog or a tool list, and it should read as a sentence to a person deciding whether to allow something. Writing one and letting the other default wastes half the mechanism.

Descriptions deserve particular care because clients cache them. The list a host holds was fetched at connection time, and although the protocol defines a notification for announcing that the tool list has changed, not every client acts on it promptly. A description is therefore closer to a published interface than to an internal docstring: assume some consumer is working from a slightly stale copy.

Output is where MCP gives you options that a plain function-calling integration does not, and where servers most often waste them. A tool result lands in the model context, so returning an upstream JSON payload verbatim spends context budget on fields nobody will read and keeps paying for them on every later turn. Structured content with a declared output schema lets a client parse a result programmatically instead of asking the model to interpret prose. A resource link lets a tool point at something large rather than inline it, handing the decision about whether to load it back to the host and the user, which is where a decision about a fifty-page document belongs. Cursor-based pagination on list operations lets a caller walk a large collection without any single response becoming unmanageable.

Failure has a similar split that is specific to this protocol and worth getting right. A JSON-RPC error means the request itself was malformed or could not be dispatched, and it is generally handled by the client rather than shown to the model. An execution failure inside a tool that ran is returned as a normal result flagged as an error, which puts the message where the model can read it and respond. Collapsing everything into protocol errors hides the information most likely to help; a message naming what went wrong and what to try instead is what converts a dead end into a recovered turn.

Finally, annotations. A tool can be marked read-only, destructive, idempotent or open-world, and hosts use those hints to decide what to confirm before running. They are hints, not enforcement, which is precisely why honesty about them matters: labelling a deletion read-only to reduce friction disables the confirmation a user was relying on. Where an argument is genuinely missing rather than wrong, elicitation is the better answer than a guess, because it asks the person instead of the model.

Authorization and trust boundaries

Local servers running over stdio have a simple authorization story: they run as the user, with whatever the user can already reach. The scoping question is what credentials you hand them and what filesystem roots the client permits. That is not nothing, but it is familiar.

Remote servers are where the real design work is, and the specification is prescriptive about it. An MCP server exposed over HTTP acts as an OAuth 2.1 resource server. It does not issue tokens and it should not be in the business of authenticating users directly. It validates tokens issued by an authorization server, which in most organizations is the identity provider that already exists.

A few mechanisms make this workable in practice. Protected resource metadata, standardized as RFC 9728, lets a server that receives an unauthenticated request respond with a pointer to the authorization server a client should use, so clients can discover how to authenticate rather than being configured with it in advance. Resource indicators, standardized as RFC 8707, let a client request a token explicitly bound to a particular resource, which prevents a token minted for one service from being replayed against another.

Two antipatterns are called out in the specification's security guidance, and both are easy to commit.

Token passthrough is accepting a token that was issued for some other service and forwarding it upstream, or accepting a token that was not issued for you at all. It destroys the audience restriction that makes tokens safe, removes your ability to enforce your own policy, and means a token stolen from anywhere in the chain works against your server. Validate that a token was issued for you, and obtain your own credentials for anything downstream.

The confused deputy appears when a server holds privileged credentials and acts on behalf of a caller without checking whether that caller is entitled to the specific action. The server has permission; the user does not; the server does it anyway. Static client identifiers combined with consent that is silently reused across sessions make this worse, because a malicious party can ride an existing approval. Authorization decisions must be made against the identity of the requester, not the identity of the server.

Session identifiers deserve a similar caution. They are for correlating requests, not for proving who someone is. A session token used as an authentication credential is a session token that can be guessed, replayed or fixed by an attacker.

The broader principle is that a server is a boundary, and boundaries need explicit policy. Which users may call which tools. Which records a given caller may see. What gets written to the audit log. Whether a destructive tool requires elevated entitlement. None of that emerges from the protocol; it is application logic you have to write, and it is what separates a demo server from one a security review will pass.

Security: a server is a funnel for untrusted input

The security story for MCP is not mainly about the protocol. It is about what happens when a probabilistic system is handed the ability to act.

The dominant risk is indirect prompt injection. Anything your server returns becomes text in the model's context, and the model has no reliable way to distinguish data it was given from instructions it was issued. A ticket description, a web page, a code comment, a file in a repository, a row in a database written by an untrusted user: all of these can contain text crafted to redirect the assistant's behaviour. The server did nothing wrong; it faithfully returned content. The attack rides in on the content.

Risk concentrates when three conditions coincide: access to private data, exposure to untrusted content, and a way to send information outward. Simon Willison named this combination the lethal trifecta, and it is a good checklist to run against any deployment. An assistant that can read your internal wiki, browse an attacker-influenced page, and post to an external endpoint has everything required for exfiltration, and no individual capability looks alarming on its own.

As a server author you cannot fix that at the prompt level, but you have levers a client does not. Whether a given capability exists at all is your decision, and a server that only reads is a server that cannot be turned into an exfiltration path. Splitting read and write into separate tools, rather than one tool with a mode argument, lets a host approve them independently. Marking the destructive ones accurately is what causes a confirmation dialog to appear in front of a user at the moment it matters. And where a tool necessarily returns content authored by someone outside the organization, saying so in the result gives the host something to act on, instead of presenting an attacker-supplied paragraph in the same undifferentiated form as a record from your own database. The composition problem sits above all of this and belongs to whoever configures the client: capabilities from several connected servers combine in ways no single author designed or reviewed.

Then there are risks specific to the ecosystem's openness. Tool descriptions are themselves model-visible text, so a malicious server can embed instructions in a description, which is sometimes called tool poisoning. A server can change its tool definitions after a user has approved them, so approval given once may not describe what runs later. When several servers are connected, one can describe its tools in a way that captures calls intended for another, an issue usually described as tool shadowing. Pinning versions, reviewing what a server actually exposes, and treating installation of a third-party server as a supply-chain decision are all reasonable responses.

On the implementation side, the ordinary rules still apply and are often forgotten because the caller is a model rather than a person. Validate every argument, because a schema is a hint to the model and not a guarantee to your handler. Never interpolate arguments into shell commands or SQL. Constrain file paths against the permitted roots and resolve them before use, since path traversal works exactly as it always has. Rate limit, because an agent in a loop generates traffic no human would. And keep secrets in the server's environment rather than anywhere the model can read them, because anything returned to the client is now in a transcript.

Testing, observability and shipping

The development loop for a server has an awkward property: the thing consuming your work is a model inside somebody else's application, which makes the usual write-run-inspect cycle slower than it should be.

The reference debugging tool, the MCP Inspector, addresses the worst of that. It connects to your server directly and lets you list tools and resources, invoke them with chosen arguments, and read the raw protocol traffic. Working there rather than inside a full AI client removes the model from the loop entirely, which is what you want when the question is whether your server behaves correctly. Bring the model back in only when you are testing whether the tools are usable, which is a different question.

Below that, ordinary testing applies. Handlers are functions and deserve unit tests, especially around argument validation and error paths, since those are what the model exercises when it guesses. Integration tests should drive the server through the protocol itself, including the initialize handshake, so that schema and capability regressions are caught. Contract tests over your tool schemas are worth having because a schema change is a breaking change to a client you do not control.

Then there is the evaluation layer that has no equivalent in traditional API work: whether a model can actually use the server to accomplish a task. A tool can be flawlessly implemented and consistently misused because its description is ambiguous or its output is unwieldy. Assemble a set of realistic requests, run them against a client, and check whether the right tools were selected with sensible arguments. Track that as tools evolve. Descriptions are effectively part of the interface, and changing one is a change that can regress behaviour.

Observability follows the same shape as any service, with a few additions. Log every tool invocation with its arguments, its outcome, its duration and the identity of the caller, redacting sensitive fields. That record is your audit trail, your debugging aid and your evidence when someone asks what an assistant did. Structured logging to stderr on stdio, and to your normal pipeline on HTTP. Health checks and metrics matter for remote servers exactly as they would for any other endpoint.

Operationally, plan for how a server changes over time. Removing a tool or tightening a schema breaks clients silently, because the failure surfaces as a model doing the wrong thing rather than a stack trace. Additive changes first, deprecation notices in descriptions, and version pinning where clients support it. And test against more than one client if you intend the server to be used widely, because implementations differ in which parts of the specification they support and a server that assumes sampling or elicitation is available will fail in ways you never see locally.

Common questions

What is MCP in plain terms?

It is an open protocol, published by Anthropic, that standardizes how an AI application connects to external tools and data. A server describes what it offers once, in a common format built on JSON-RPC, and any compatible client can discover and use it. The point is to stop every AI application needing a hand-written integration with every system. It does not run models or decide which tool to call; it defines the boundary between the application and the systems it reaches.

Do I need MCP if my model already supports function calling?

Not necessarily. If one application calls a fixed set of functions you control, direct tool calling is simpler and the protocol buys you nothing. MCP earns its place when the same capability must be reachable from several clients, when you want an integration that survives changing frameworks or models, or when you are publishing something for others to install. The dividing line is reuse: one client and one integration does not need a protocol.

Should my server use stdio or HTTP?

Use stdio when the server does work on the machine the user is sitting at, such as touching local files, repositories or local credentials. It runs as a subprocess with no network, no authentication and nothing to deploy. Use HTTP when the capability is shared across a team or backed by a central system, and accept that this brings deployment, TLS, sessions and real authorization with it. Many servers are only ever needed locally, and putting those on a network adds risk without benefit.

Is MCP only for Claude?

No. The specification is open and has been adopted by a range of AI applications, editors and agent frameworks, with official SDKs in several languages. That is the reason to target it rather than the tool format of one vendor: an integration written against the protocol keeps working when the client or the model underneath changes. Support is not uniform, though, so build servers that degrade sensibly when a client does not implement every optional capability.

How do I stop an MCP server becoming a security hole?

Most of the answer is server-side craft rather than protocol configuration. Validate every argument in the handler, because a schema is guidance to the model and not a guarantee to your code; resolve file paths against the roots the client granted before touching anything; rate limit, since a model in a loop generates traffic no person would. Keep destructive operations in their own tools and annotate them honestly, so the host raises a confirmation instead of running them silently. Keep credentials in the process environment and out of anything you return, because a tool result becomes part of a transcript. On remote servers, check that a presented token was issued for your server, obtain your own credentials for anything downstream instead of forwarding what the caller sent, and decide authorization from the identity making the request rather than the privileges the server happens to hold.

Why do my tools not appear in the client?

The usual cause on stdio is that something wrote to standard output. That channel carries the protocol, so a stray print statement or a library banner corrupts the message stream and the connection fails during setup. Send all logging to stderr. The next most common causes are a crash during the initialize handshake, an invalid tool schema rejected by the client, or the client and server negotiating different protocol versions. Connecting with the Inspector rather than a full client shows you the raw traffic and usually identifies which of these it is within a minute.

Related reading

Full syllabus

1

Foundations of MCP

Free preview
Read free →
2

Protocol: Tools, Resources, and Prompts

3

Local, Remote, and Transport Architectures

4

Building Your First MCP Server

5

Designing High-Quality Tools and Resources

6

Authentication, Authorization, and Trust Boundaries

7

MCP for Agents, Workflows, and Enterprise Systems

8

Testing, Debugging, Observability, and Reliability

9

Security Risks, Guardrails, and Safe Execution

10

Deployment, Hosting, Registry, and Discovery

11

Case Studies, Patterns, and Antipatterns

12

Capstones, Production Readiness, and Career Use

Unlock all 12 chapters

Plus 24 other courses — 549 more chapters included.

Compare all plans

Free Tools & Calculators