MCP vs Plugins: How AI Tool Integration Evolved
Why "connecting an LLM to your tools" used to be a nightmare
If you built anything with large language models before 2024, you probably remember the pain of "integrations." Every model provider had its own idea of what a tool call should look like, every vendor shipped its own SDK, and every plugin you wrote for one platform was basically useless anywhere else. You'd write a function schema for OpenAI's function calling, then rewrite it for Anthropic's tool use format, then rewrite it again for some open-source framework's idea of an "agent tool." Multiply that by every data source you wanted to connect — a database, a ticketing system, a file store, a search API — and you had a combinatorial mess. N models times M tools meant N×M integrations, each slightly different, each maintained separately.
This is the problem the industry has spent the last couple of years trying to solve, and it's why the conversation around "MCP vs plugins" keeps coming up in engineering teams right now. It's not an academic distinction. It's the difference between an integration that dies the moment you swap models and one that survives a stack change. In this article we'll walk through what plugins actually were, why they hit a ceiling, how the Model Context Protocol (MCP) approaches the same problem differently, and where each pattern still makes sense today. By the end you should be able to look at a project brief and know, concretely, which approach to reach for.
What "plugins" meant in the LLM world
Before MCP existed, "plugin" was the umbrella term for any mechanism that let a model call out to external functionality. This took a few concrete forms:
- ChatGPT plugins (2023) — a manifest-based system where a plugin exposed an OpenAPI spec and a
ai-plugin.jsonmanifest describing what it did. The model would read the spec, decide which endpoint to call, and the platform would make the HTTP request on its behalf. - Function calling / tool use — instead of a full plugin ecosystem, providers let developers define JSON schemas for functions the model could request. The developer's own application code was responsible for actually executing the function and feeding the result back into the conversation.
- Framework-specific tools — libraries like LangChain or LlamaIndex defined their own
Toolabstractions, each with a name, description, and a Python or JS function to run. These were popular because they let you compose tools quickly, but the abstraction only worked inside that framework.
The common thread across all of these: the "plugin" was defined per platform. A ChatGPT plugin manifest didn't work in Claude. A LangChain tool wrapper didn't work if you switched to a different orchestration library. A function-calling schema written against one model's calling convention often needed adjustment for another, especially around how arguments were typed, how errors were reported back, or how multi-step tool chains were represented in the conversation history.
Here's a simplified example of what a plugin-style tool definition looked like in a typical function-calling setup:
# A "plugin" tool defined for one specific framework/model
tools = [
{
"type": "function",
"function": {
"name": "get_course_progress",
"description": "Fetch a student's progress percentage for a course",
"parameters": {
"type": "object",
"properties": {
"student_id": {"type": "string"},
"course_id": {"type": "string"}
},
"required": ["student_id", "course_id"]
}
}
}
]
def get_course_progress(student_id, course_id):
# your actual business logic lives here, glued to this one app
return db.query_progress(student_id, course_id)This works fine in isolation. The problem shows up the moment you want to reuse get_course_progress somewhere else — a different chat client, a different agent framework, a teammate's local dev environment. You end up copy-pasting the schema and rewriting the glue code, hoping the two copies don't drift.
Enter MCP: a protocol instead of a plugin format
The Model Context Protocol, introduced by Anthropic in late 2024 and since adopted broadly across the industry, reframes the problem. Instead of asking "how does this specific model call this specific function," MCP asks "what does a standard client-server contract for tools, data, and prompts look like, independent of any one model or app?"
MCP defines a client-server architecture:
- An MCP server exposes capabilities — tools (actions the model can invoke), resources (data the model can read), and prompts (reusable prompt templates) — over a standard protocol, typically JSON-RPC over stdio or HTTP/SSE.
- An MCP client (built into an app like Claude Desktop, Claude Code, or any compatible IDE/agent runtime) connects to one or more MCP servers, discovers what they offer, and lets the model use them during a conversation.
The critical shift is that the server doesn't know or care which model is on the other end. You write the MCP server once, and it works with any MCP-compatible client — Claude, other assistants that adopt the protocol, custom agent runtimes, IDE integrations. The N×M integration problem collapses into N servers plus M clients, because the protocol itself is the shared contract.
Here's a minimal MCP server written in Python using the official SDK, exposing that same course-progress lookup as a standard MCP tool:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("teachyou-progress")
@mcp.tool()
def get_course_progress(student_id: str, course_id: str) -> dict:
"""Fetch a student's progress percentage for a course."""
progress = db.query_progress(student_id, course_id)
return {
"student_id": student_id,
"course_id": course_id,
"percent_complete": progress.percent,
"last_active": progress.last_active_at.isoformat(),
}
if __name__ == "__main__":
mcp.run(transport="stdio")Notice what's missing compared to the plugin example: there's no model-specific schema formatting, no framework-specific decorator tied to one vendor's SDK. The @mcp.tool() decorator generates the JSON schema automatically from the function signature and docstring, and any MCP client that connects to this server can discover and call get_course_progress without knowing anything about how it's implemented internally.
The three building blocks: tools, resources, and prompts
A lot of the "MCP vs plugins" confusion comes from people assuming MCP is just "function calling with extra steps." It's broader than that, and understanding the three primitives makes the comparison clearer.
- Tools are actions with side effects or computation — the closest analogue to a plugin function. Send an email, query a database, run a calculation, trigger a deployment.
- Resources are read-only data the client can attach to context, similar to how a plugin might expose a document store, except resources are addressable by URI and can be listed, browsed, and subscribed to for updates.
- Prompts are reusable, parameterized prompt templates that a server can expose so that a client (or a human user, in Claude Desktop's UI) can invoke a well-tested prompt rather than reinventing it every time.
Old-style plugins mostly only covered the "tools" case, and even then inconsistently. ChatGPT's plugin manifest, for instance, conflated tool-calling with a REST API description, which meant a lot of manual translation work for the model to figure out multi-step flows. MCP's separation of concerns means a server can say "here's a piece of static data you might want in context" (a resource) versus "here's an action you can trigger" (a tool) versus "here's a prompt template a user might select from a menu" (a prompt) — and clients can render each of these differently in their UI.
A side-by-side comparison
Let's put this plainly, since the practical differences are what actually matter when you're deciding what to build.
- Portability: Plugins were tied to a specific platform's manifest format or a specific framework's tool abstraction. MCP servers are portable across any MCP-compliant client.
- Discovery: Plugin ecosystems relied on centralized directories (the ChatGPT plugin store, a framework's tool registry) that a specific vendor controlled. MCP servers are discovered by clients at connection time via a standard
list_tools/list_resourceshandshake — no central gatekeeper required, though public registries do exist for convenience. - Transport: Plugins usually meant "the platform calls your HTTP endpoint." MCP explicitly supports both local transport (stdio, meaning the server runs as a subprocess on the user's machine) and remote transport (HTTP with server-sent events), which matters enormously for tools that need filesystem or local process access — think a code editor's MCP server for running tests locally.
- Statefulness and session semantics: MCP defines a proper initialization handshake, capability negotiation, and lifecycle events. Older plugin systems mostly treated each call as a stateless HTTP request with no shared session context.
- Multi-vendor adoption: This is the big one. MCP has been adopted as an open specification with SDKs in Python, TypeScript, Java, C#, and Kotlin, and it's been picked up by tooling well beyond Anthropic's own products. Plugin formats, by contrast, tended to live and die with the platform that invented them — ChatGPT's original plugin store was largely deprecated in favor of GPTs, which shows how quickly a closed, vendor-specific format can become a dead end.
None of this means plugins were a bad idea. They were the right solution for the constraints of the moment — a single-vendor ecosystem trying to bootstrap third-party functionality quickly. MCP is what you get when the industry has had a couple of years to notice the interoperability problem and decide it's worth solving with a shared standard rather than another walled garden.
Where framework-native "tools" still fit
It's worth being honest that MCP doesn't replace every tool-calling mechanism, and you shouldn't feel obligated to wrap every function in an MCP server. If you're building a single application with a single model integration — say, a Python backend that calls the Anthropic API directly and needs to look up a row in your own database — plain old function calling (tool use) inside that one call is simpler and has less overhead. You don't need a subprocess, a JSON-RPC handshake, or a discovery step for a function that only ever gets called from one codebase.
Here's the same functionality as a direct tool-use call against the Claude API, no MCP involved:
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=[{
"name": "get_course_progress",
"description": "Fetch a student's progress percentage for a course",
"input_schema": {
"type": "object",
"properties": {
"student_id": {"type": "string"},
"course_id": {"type": "string"}
},
"required": ["student_id", "course_id"]
}
}],
messages=[{"role": "user", "content": "How far along is student S42 in course C7?"}]
)This is the right call when the tool is genuinely one-off and internal. MCP earns its complexity when you need one of these things: reuse across multiple clients or teams, a tool that should work identically whether it's invoked from Claude Desktop, Claude Code, or a custom agent; local system access that a remote API can't provide (reading files, running a linter, controlling a browser); or a growing catalog of tools where you want clients to discover capabilities dynamically instead of hardcoding a fixed list at build time.
A good rule of thumb: if you're writing the schema once and calling it from one place, plain tool use is fine. If you find yourself copying the same tool definition into a second project, that's the signal to promote it into an MCP server.
Real-world patterns teams are using today
A few patterns have become common enough that they're worth naming directly, because they show what MCP actually unlocks in practice rather than in theory.
- Local dev-tool servers. Editors and CLIs expose MCP servers for things like running the test suite, querying git history, or inspecting a running process. Because these run as local subprocesses over stdio, the model gets access to your actual filesystem and shell state without you having to build a hosted API for it.
- Data platform connectors. Companies that host structured data — ticketing systems, CRMs, analytics warehouses — ship an MCP server once, and every downstream AI client (internal chat tools, IDE assistants, automation agents) can connect to the same server rather than each team writing bespoke API glue.
- Composable agent stacks. Teams building multi-step agents connect several MCP servers at once — one for search, one for a proprietary database, one for sending notifications — and let the client's orchestration layer decide which tool to call at each step, without needing a single monolithic integration layer that knows about all of them.
- Prompt and resource sharing across a team. Because MCP servers can expose curated prompts and resources, not just tools, teams use them to standardize how everyone queries a shared knowledge base, rather than each engineer maintaining their own copy-pasted prompt template.
What all four have in common is reuse. The value of MCP isn't that it makes any single tool call faster or smarter — it's that it makes the investment in building that tool call pay off across many more contexts than a one-off plugin ever could.
It's also worth noting how this changes the economics of building integrations for a small team. Under the old plugin model, if you wanted your internal tool to work inside three different AI-assisted workflows — say, a chat-based support tool, an IDE assistant, and a CI automation script — you effectively maintained three separate integrations, each with its own auth handling, its own schema, and its own failure modes to debug. With an MCP server, you maintain one codebase, and the auth handling, input validation, and business logic live in exactly one place. When the underlying business logic changes (say, the progress calculation in our example above starts factoring in quiz scores as well as lesson completion), you fix it once and every client that talks to that server picks up the change automatically, without anyone needing to redeploy a chatbot plugin or bump a framework version pin. That single-codebase property is easy to undervalue when you're building your first integration and hard to overstate once you're maintaining your tenth.
Common mistakes when adopting MCP
A few things trip up teams moving from ad hoc tool use to MCP, worth flagging so you don't repeat them.
- Treating every function as tool-worthy. Not everything needs to be exposed to the model. If a piece of logic is deterministic and doesn't need judgment (like formatting a date), just do it in code before or after the model call — don't make the model round-trip through a tool call for something it doesn't need to reason about.
- Skipping input validation on the server. Because MCP tools are described with JSON schemas, it's tempting to assume the schema is enough protection. It isn't — a model can still send malformed or adversarial input, especially in agentic loops where the model is chaining several tool calls together. Validate on the server side exactly as you would for any other externally-facing input.
- Ignoring authentication and scope. An MCP server that touches real data (a production database, an email account, billing records) needs the same access controls you'd put on any API — scoped credentials, rate limiting, and audit logging. The protocol doesn't provide security for you; it standardizes the interface, not the trust model.
- Overloading a single server with unrelated tools. It's tempting to build one giant MCP server that does everything. In practice, smaller servers scoped to a single domain (one for calendar, one for search, one for internal docs) are easier to reason about, easier to permission separately, and easier for a client to selectively enable.
- Forgetting that resources and prompts exist. Many early MCP servers only implement tools because that's the most familiar concept coming from function calling. If your server exposes a lot of read-only reference data, model it as a resource — clients can cache and browse resources more efficiently than if you force everything through a tool call.
What this means if you're building AI products right now
If you're an engineer deciding how to wire an LLM into a product today, the practical takeaway is this: default to plain tool/function calling for anything scoped to a single application, and reach for MCP the moment you need that capability to be reusable outside of one codebase, or the moment you need local system access that a hosted API can't give you. The "plugins" era taught the industry a real lesson — proprietary, single-vendor integration formats don't survive platform shifts, and every team that bet heavily on ChatGPT's original plugin store had to redo that work when the platform moved on. MCP is a direct response to that lesson: a protocol, not a product, which is exactly why it's had an easier time getting adopted across competing tools instead of being tied to one company's roadmap.
That said, adopting a protocol is not the same as adopting it well. Writing a correct MCP server that handles authentication properly, validates input defensively, exposes the right primitives (tools versus resources versus prompts), and behaves predictably inside a multi-step agent loop takes real engineering discipline — it's a different skill from writing a single function-calling schema for one app. This is exactly the gap our course Building & Integrating MCP Servers is built to close: you'll go from a basic stdio server like the one above to a properly authenticated, production-shaped MCP integration, and understand not just the syntax but the judgment calls — when to build an MCP server at all, and when a simple tool-use call is the better engineering decision.
BootcampA 30-day guided bootcamp: build, harden and ship a production autonomous agent from scratch.
AI AgentsUnderstand how AI agents really work: the loop, the tools, the memory, and why most agent projects fail.