MCP Explained: How the Model Context Protocol Could Replace Plugins for AI-Driven Developer Tools
If you’ve built developer tools or integrated AI assistants into workflows, you’ve probably wrestled with plugin ecosystems, brittle API wrappers, and the constant re-implementation of the same connectors across multiple clients. The Model Context Protocol (MCP) is an attempt to fix that: a standardized way for AI "clients" (assistants, IDE agents, chat apps) to discover and call "servers" (tool providers) that expose capabilities—think tools, resources, and prompts—without bespoke plugins per client.
That sounds ambitious, and it is. In this article, we’ll unpack MCP from first principles, compare it to existing standards like LSP/DAP and OpenAPI, cover how to build an MCP server with concrete examples, explain capability negotiation and security models, outline real-world integration patterns, and propose a pragmatic migration path. We’ll also be explicit about what MCP currently doesn’t solve.
The thesis: MCP can replace ad‑hoc plugins in AI-driven developer tooling because it cleanly separates capability providers (servers) from AI runtimes (clients), negotiates features, and supports secure local or remote transports. But its success depends on disciplined adoption, strong security defaults, and a clear story for enterprise governance.
What is the Model Context Protocol (MCP)?
At its core, MCP is a JSON-RPC–based protocol that standardizes how an AI client discovers and uses capabilities offered by a server:
- A server advertises capabilities such as:
- Tools: callable operations described via structured schemas.
- Resources: data surfaces (files, documents, HTTP endpoints) that can be listed, read, and sometimes observed for changes.
- Prompts: reusable prompt templates or system instructions.
- A client (e.g., an IDE agent, a desktop assistant, a chat application) connects to the server over a transport (stdio for local processes, WebSocket for networked ones, etc.), negotiates capabilities, and then calls tools or reads resources as needed during a session.
- All interactions are mediated through a machine-readable contract so models can be reliably guided to call tools with correct arguments and parse results.
This is not a model provider feature per se; it is a client–server protocol to expose external capabilities to any AI runtime that speaks MCP. Because the protocol is open and transport-agnostic, the same MCP server can be used by multiple clients without creating a new plugin per client.
A useful mental model:
- MCP is to AI tools what the Language Server Protocol (LSP) is to code intelligence: a standard interface that lets many clients talk to many servers in a composable way.
Why MCP now?
Three converging pressures make MCP timely:
- Tooling sprawl: Developers maintain separate plugins for each chat app, IDE, and orchestrator, all wrapping the same APIs.
- Security and privacy: You don’t want to give your cloud credentials to a third-party chat app just to use a plugin. You’d rather point the app at a local or enterprise-hosted broker under your control.
- Structured tool use: LLMs are increasingly about tool-augmented workflows. A protocol that enumerates tools/resources and negotiates capabilities is simpler and safer than unconstrained arbitrary HTTP calls.
MCP positions itself as the standard control plane for AI tools: pluggable, neutral, and portable.
MCP vs LSP/DAP: Similar bones, different purpose
MCP clearly borrows good ideas from the Language Server Protocol (LSP) and Debug Adapter Protocol (DAP):
- JSON-RPC over flexible transports
- An initialize/initialized handshake
- Capabilities negotiation so clients and servers can agree on what’s supported
- Notification vs request/response semantics
But the purpose and abstractions differ:
- LSP/DAP focus on editor features for a single programming workspace (diagnostics, hovers, stepping through code). MCP focuses on model-facing capabilities that might span many systems: GitHub, Jira, Docker, Kubernetes, secrets stores, cloud APIs, internal services.
- LSP methods are domain-specific to language intelligence and debugging. MCP methods are domain-neutral tool calls, resource access, and prompt catalogs.
- MCP is designed for a model-in-the-loop setting: the "caller" is often an AI agent that needs strict guardrails, auditability, and clear schemas.
If you’ve ever thought "I wish LSP-like tech existed for AI tools," MCP is that—but oriented around tool invocation, data access, and model-appropriate safety practices.
MCP vs OpenAPI (and plugin manifests)
OpenAPI describes HTTP APIs: endpoints, parameters, responses, and auth. It’s great for code generation, SDKs, and standard web integrations. But OpenAPI has limitations for AI-first integration:
- It doesn’t define a session protocol, capability negotiation, or push notifications.
- It’s transport-specific (HTTP), whereas MCP is protocol-first and transport-agnostic.
- It doesn’t capture the UI and consent semantics needed when an LLM calls tools that can affect the user’s environment.
- It’s not optimized for describing local resources (file systems, editors, terminals) that are not HTTP endpoints.
Chat plugins built on OpenAPI helped bootstrap the ecosystem, but they require per-platform manifests and hosting patterns. MCP aims to unify that with a single server implementation that any client can adopt.
This isn’t an either/or: you can wrap your existing OpenAPI-backed service behind an MCP server that presents model-friendly tools and resources. That also lets you standardize consent, logging, and policy in one place.
The MCP mental model: Clients, servers, and capabilities
- Client: the runtime that hosts an LLM or agent and wants to use external capabilities (e.g., a desktop assistant, an IDE, a notebook environment).
- Server: a process exposing tools/resources/prompts over MCP. This might be local (spawned via stdio), remote (WebSocket), or an enterprise gateway.
- Capabilities:
- Tools: Invocable operations with JSON-serializable arguments and structured results. Examples: "search_issues", "create_branch", "run_tests".
- Resources: Named data surfaces that can be listed and read. Examples: files in the current repo, a job log, a build artifact, or a curated knowledge base.
- Prompts: Reusable prompt templates or system instructions to improve consistency.
A quick look at the handshake and capability negotiation
MCP uses a handshake that lets both sides learn about each other and agree on supported features. The exact field names may evolve with the spec, but the conceptual shape is stable.
Example (illustrative JSON-RPC messages; refer to the spec for canonical names):
json{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "client": { "name": "my-assistant", "version": "0.1.0" }, "capabilities": { "tools": { "call": true }, "resources": { "list": true, "read": true }, "prompts": { "list": true, "get": true } } } }
Server responds with its identity and supported features:
json{ "jsonrpc": "2.0", "id": 1, "result": { "server": { "name": "acme-mcp", "version": "0.1.0" }, "capabilities": { "tools": { "list": true, "call": true }, "resources": { "list": true, "read": true, "subscribe": false }, "prompts": { "list": true, "get": true } } } }
Some protocols then send/receive an initialized
notification to confirm readiness:
json{ "jsonrpc": "2.0", "method": "initialized", "params": {} }
After that, the client can enumerate and call tools, or list/read resources.
Tool metadata typically includes:
- A name and description (important for model grounding)
- A JSON schema for arguments
- A JSON schema for results (if applicable)
- Whether the tool is idempotent or has side effects
- Optional auth/consent hints
Example tool call (illustrative):
json{ "jsonrpc": "2.0", "id": 42, "method": "tools/call", "params": { "name": "search_issues", "arguments": { "query": "is:open label:bug repo:acme/app" } } }
The server returns a structured result:
json{ "jsonrpc": "2.0", "id": 42, "result": { "items": [ { "id": 1834, "title": "Crash on startup", "url": "https://…" }, { "id": 1855, "title": "Null pointer in cache", "url": "https://…" } ], "nextPage": null } }
A resource listing/read flow might look like:
json{ "jsonrpc": "2.0", "id": 2, "method": "resources/list", "params": { "path": "/workspace" } }
json{ "jsonrpc": "2.0", "id": 2, "result": { "items": [ { "name": "README.md", "kind": "file" }, { "name": "src", "kind": "directory" } ]}}
json{ "jsonrpc": "2.0", "id": 3, "method": "resources/read", "params": { "path": "/workspace/README.md" } }
json{ "jsonrpc": "2.0", "id": 3, "result": { "content": "# Project\n…" } }
The exact method names and schemas may differ across MCP versions and SDKs, but the pattern is consistent: initialize → advertise capabilities → list/describe → call/read.
Building an MCP server: A minimal guide
You can write an MCP server in any language that can speak JSON-RPC. Official SDKs exist for popular languages (e.g., Python and TypeScript) to spare you from hand-rolling the message plumbing. The server can run as a local process spawned by a client (stdio transport) or as a network service (WebSocket/HTTP upgrade) behind your enterprise auth.
Below are two instructive sketches: one language-agnostic outline and one code-oriented pseudo-example.
Architecture outline
- Transport:
- Local desktop/IDE: spawn server as a child process and communicate over stdin/stdout for low latency and strong isolation.
- Remote/shared: serve a WebSocket endpoint and terminate TLS; authenticate clients with tokens or mTLS.
- Protocol core:
- Implement initialize/initialized handshake.
- Provide methods to list tools/resources/prompts.
- Implement invocation handlers for your tools and resource accessors.
- Tool schema discipline:
- Define clear JSON schemas for arguments and results.
- Provide descriptions models can use to select the right tool.
- Observability:
- Structured logs of tool calls, durations, and errors.
- Optional audit trail for side-effecting operations.
Minimal server pseudo-implementation (Python-like)
This example illustrates concepts. Use the official SDK for production-grade code; the method names are representative.
python# pseudo_mcp_server.py # Conceptual example; consult the MCP SDK for exact APIs. import json import sys import time SERVER_INFO = {"name": "acme-mcp", "version": "0.1.0"} TOOLS = [ { "name": "search_issues", "description": "Search issues by query string (supports provider-specific syntax).", "parameters": { "type": "object", "properties": { "query": {"type": "string", "description": "Issue query, e.g., 'is:open label:bug'"}, "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 20} }, "required": ["query"] }, "returns": { "type": "object", "properties": { "items": {"type": "array", "items": {"type": "object"}}, "nextPage": {"type": ["string", "null"]} } } }, { "name": "run_tests", "description": "Run unit tests and return summary.", "parameters": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": []}, "returns": {"type": "object", "properties": {"passed": {"type": "integer"}, "failed": {"type": "integer"}}} } ] WORKSPACE_ROOT = "/workspace" # demo path def handle_initialize(params): # Inspect client capabilities if needed return { "server": SERVER_INFO, "capabilities": { "tools": {"list": True, "call": True}, "resources": {"list": True, "read": True}, "prompts": {"list": True, "get": True} } } def handle_tools_list(params): return {"items": TOOLS} def handle_tools_call(params): name = params["name"] args = params.get("arguments", {}) if name == "search_issues": q = args["query"] limit = int(args.get("limit", 20)) # Fake results for demo items = [{"id": i, "title": f"Result for {q} #{i}", "url": f"https://example.com/{i}"} for i in range(limit)] return {"items": items, "nextPage": None} if name == "run_tests": # Fake test run time.sleep(0.2) return {"passed": 120, "failed": 3} raise Exception(f"Unknown tool: {name}") def handle_resources_list(params): path = params.get("path", WORKSPACE_ROOT) # Demo: static listing return {"items": [ {"name": "README.md", "kind": "file", "path": f"{path}/README.md"}, {"name": "src", "kind": "directory", "path": f"{path}/src"} ]} def handle_resources_read(params): path = params["path"] if path.endswith("README.md"): return {"content": "# Demo Project\nThis is a demo."} raise Exception("Not found") HANDLERS = { "initialize": handle_initialize, "tools/list": handle_tools_list, "tools/call": handle_tools_call, "resources/list": handle_resources_list, "resources/read": handle_resources_read, } def respond(id, result=None, error=None): msg = {"jsonrpc": "2.0", "id": id} if error is not None: msg["error"] = {"code": -32000, "message": str(error)} else: msg["result"] = result sys.stdout.write(json.dumps(msg) + "\n") sys.stdout.flush() for line in sys.stdin: try: req = json.loads(line) method = req.get("method") id_ = req.get("id") params = req.get("params", {}) if method in HANDLERS: result = HANDLERS[method](params) respond(id_, result) else: respond(id_, error=f"Method not found: {method}") except Exception as e: respond(req.get("id"), error=e)
With an SDK, you’ll typically register tools via decorators or builder patterns, and the library takes care of JSON-RPC framing, schema publication, and transport plumbing.
TypeScript shape with an SDK (conceptual)
ts// conceptual_mcp_server.ts // Conceptual shape; use the official MCP SDK for exact types and APIs. import { createServer, stdioTransport, tool } from "@mcp/server"; const srv = createServer({ name: "acme-mcp", version: "0.1.0" }); srv.registerTool(tool({ name: "search_issues", description: "Search issues by query.", parameters: { type: "object", properties: { query: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 100 } }, required: ["query"] }, handler: async ({ query, limit = 20 }) => { // Query GitHub/Jira/etc. return { items: [], nextPage: null }; } })); srv.registerResourceProvider({ list: async (path?: string) => [ { name: "README.md", kind: "file", path: "/workspace/README.md" } ], read: async (path: string) => ({ content: "# Demo" }) }); srv.listen(stdioTransport());
Again, consult the spec and SDK docs for precise names; this illustrates the intent: define tools/resources, wire handlers, and pick a transport.
Capability negotiation in practice
Capability negotiation lets clients and servers adapt without breaking each other.
- Versioning: Servers can expose a protocol version and optional feature flags. Clients may choose to degrade gracefully or refuse if a required feature is missing.
- Optional extensions: Servers can indicate experimental or optional APIs. Clients that don’t understand them simply ignore them.
- Resource shapes: Not all servers expose resources; some are tool-only. The handshake advertises what’s present so the client doesn’t guess.
- Limits and quotas: Servers can report constraints (max payload size, max concurrent calls) so clients avoid overdriving the system.
Pattern for robust negotiation:
- Client sends initialize with its supported features and a list of desired capabilities.
- Server responds with accepted and available capabilities and optional metadata.
- If needed, the client follows up with per-capability describe/list calls to gather schemas and enumerate resources.
- Both sides keep unknown fields for forward compatibility (don’t crash on extra data).
This is functionally similar to LSP but oriented around tool/resource semantics rather than editor events.
Security and sandboxing
The most important part of MCP is not JSON-RPC—it’s the trust boundary. You’re allowing a model to call tools that could access files, secrets, cloud resources, and production systems. Treat the server as a policy enforcement point with the following controls:
- Least privilege by design
- Split servers by trust tier: a local filesystem server vs a cloud admin server. Don’t co-host high-risk tools with low-risk ones.
- Capability scopes: Tools should be narrowly defined (e.g., "read_file" under a workspace root) rather than "read_any_path".
- User consent and UI gating
- Mark side-effecting tools and require explicit user approval per call or per session.
- Display arguments and target resources before execution.
- OS/container sandboxing
- Run servers as least-privileged users.
- Constrain system calls (seccomp), file access (chroot, namespaces), network egress (iptables), and memory/CPU.
- Containerize risky servers; use read-only filesystems where possible.
- Authentication and authorization
- For remote servers, use TLS, mTLS, or signed tokens. Bind tokens to device or user identity.
- Enforce per-user and per-tool policies (e.g., allow "deploy" only for SRE group).
- Rate limiting and quotas
- Per-client, per-user, and per-tool to prevent runaway loops.
- Audit logging and reproducibility
- Log who/what called which tool, with arguments, timestamps, and results (redact secrets).
- Keep a policy decision log when a call is allowed or denied.
- Secrets handling
- Never expose raw secrets to the model. Tools should accept short-lived tokens injected by the server, not by the client.
- Integrate with a vault (e.g., HashiCorp Vault, AWS KMS) and delegate ephemeral credentials via STS/assume-role mechanisms.
- Output filtering
- Validate and sanitize tool outputs before returning to the client to avoid prompt injection feedback loops.
- Resource isolation
- Mount per-workspace views: the model shouldn’t see your whole filesystem by default.
Remember, MCP centralizes tool access. That’s awesome for ergonomics—and a single place to get security right.
Real-world integration patterns
MCP becomes most useful when you standardize on it across local dev, CI, and internal tools. Here are patterns that work well in practice.
-
Local per-workspace server
- Spawn a server when an IDE or desktop assistant opens a repo.
- Expose tools like code search, test runner, linter, and resource access to the workspace files.
- Benefits: low latency, strong filesystem control, offline-friendly.
-
Enterprise gateway server
- Host a central MCP server (or a mesh of them) that fronts internal services: SCM, issue trackers, CI, artifact registries, Kubernetes.
- Enforce single sign-on, RBAC, and audit at the gateway.
- Benefits: one integration point for all AI clients; standardized policy.
-
Sidecar in dev containers
- When using remote dev containers (e.g., Codespaces, Dev Containers), run an MCP server in the container that exposes tools/resources bound to that environment.
- Benefits: environment-specific tools without leaking host credentials.
-
Hybrid local+remote
- Client connects to a local server for filesystem/tools and a remote enterprise server for cloud resources.
- Benefits: principle of least privilege, reduce blast radius.
-
Micro-servers composed via client
- Rather than a single monolith, run several small servers: "git server", "jira server", "k8s server". The client connects to all and composes them.
- Benefits: simpler blast-radius isolation and independent deployments.
-
BYOC (Bring Your Own Credentials) pattern
- For remote servers, avoid storing long-lived credentials in the client. Use device code flows and short-lived tokens retrieved on demand by the server.
- Benefits: minimal secret sprawl; consistent auth.
-
Observability and feedback loop
- Export metrics per tool: latency, error rate, usage. Feed back into retrieval and caching strategies (e.g., prefetch resources based on agent patterns).
Example tool inventory for a developer-focused MCP mesh:
- SCM: clone/list branches/create PRs
- Issues: search/create/update/comment
- CI: trigger builds, fetch logs/artifacts
- Containers: build, scan, run images
- Cloud: list services, deploy, view logs
- Security: run SAST/DAST scans, list findings
- Knowledge: search runbooks, ADRs, API catalogs
A practical migration path: from plugins and API wrappers to MCP
If you maintain a developer tool integration today, you likely support several clients: a ChatGPT-like app manifest, a VS Code extension, a JetBrains plugin, and a bespoke script for CI. Each wraps your API differently. MCP offers a consolidation path.
Step-by-step approach:
-
Inventory your capabilities
- List the meaningful operations as model-facing tools, with clear argument/result schemas.
- Identify resources you can surface safely (e.g., logs, docs, code, metadata).
-
Build a thin MCP server that wraps your existing API
- Map tools to underlying API calls. Add input validation, policy checks, and redaction.
- Start with stdio transport for local testing; add WebSocket for enterprise deployment.
-
Harden security from day one
- Define per-tool risk levels and consent requirements. Add RBAC and rate limits.
- Integrate with your auth provider; avoid embedding secrets in clients.
-
Dogfood across multiple clients
- Wire your MCP server to a desktop assistant, an IDE agent, and a notebook environment.
- Remove duplicated plugins where possible and keep a thin shim for UX.
-
Educate your users
- Document what tools exist, when consent is asked, and what’s logged.
- Provide examples and common workflows; publish JSON schemas for power users.
-
Gradually deprecate per-platform plugins
- Replace plugin logic with an MCP client that talks to the same server.
- Maintain compatibility layers during transition (e.g., your old VS Code command now calls the MCP client under the hood).
-
Iterate on schema quality
- Good tool names, succinct descriptions, and tight schemas dramatically improve LLM reliability.
- Publish changelogs and version your tools to avoid breaking clients.
This migration consolidates your logic, improves auditability, and reduces long-term maintenance. The trick is not to bolt MCP on as an afterthought—treat it as the primary interface for AI-driven access to your system.
Where MCP may fall short (today)
- Fragmentation risk
- MCP is still maturing. Competing or parallel protocols may emerge, and different clients may implement slightly different subsets.
- UI and consent aren’t fully standardized
- The protocol can mark tools as risky, but how a client prompts users varies. Enterprises will want stronger guarantees and policy pushdown.
- Not a silver bullet for remote execution
- Long-running jobs, streaming logs, and bidirectional streaming semantics need careful handling. Some SDKs provide patterns, but this isn’t as turnkey as simple REST.
- Still requires connectors
- Wrapping an API as an MCP tool is work. The difference is you do it once, not once per client. But you still own the integration lifecycle.
- Governance is your job
- MCP centralizes power. Without RBAC, quotas, and audit, you’ve just created a wider blast radius.
- Model variability
- Different LLMs prefer different tool selection strategies. Great schemas and descriptions help, but you may still need per-model prompt scaffolding.
None of these are deal-breakers, but they’re important to acknowledge. MCP is part of an architecture, not the entire solution.
Opinion: Why MCP can replace plugins for AI developer tools
- A universal server over many bespoke plugins
- One MCP server can serve multiple clients: desktop assistants, IDEs, terminals, notebooks, and CI automations. That’s a massive reduction in duplicated effort compared to per-platform plugins.
- Safer integration surface
- The server can be local and under user control, or remote and secured by enterprise policy. Either way, secrets stay closer to home and you avoid giving broad powers to third-party platforms.
- Better model ergonomics
- Tools and resources are enumerated with schemas and descriptions that models can use directly. You’re not hoping an LLM learns your REST docs from scratch.
- Extensible and composable
- You can run multiple servers and let the client compose them. You can version tools independently. You can add resources without changing a plugin manifest.
- Transport-agnostic future-proofing
- stdio for local, WebSocket for remote—swap transports without changing the model-facing semantics.
For developer tooling—where local context, file access, and CI/CD hooks dominate—MCP maps to reality better than HTTP-only plugin manifests. It takes the operational lessons from LSP/DAP and repurposes them for model tooling.
Testing and observability tips
- Contract tests
- Write tests that initialize the server, list tools/resources, call them with valid/invalid inputs, and assert structured results.
- Golden traces
- Capture JSON-RPC exchanges for known workflows and use them as regression tests.
- Fault injection
- Simulate timeouts, rate limiting, and auth failures. Ensure clients get actionable errors.
- Metrics that matter
- P50/P95 tool latency, error rates, per-user usage, consent declines vs accepts, and cache hit rates.
- Security drills
- Try path traversal on resources, prompt injection on outputs, mass-invocation loops, and credential exfiltration attempts. Verify defenses hold.
Example: Wrapping a CI service behind MCP
Imagine you operate an internal CI system. Today you have:
- A REST API documented in OpenAPI
- A VS Code extension to trigger builds
- A chat bot to fetch logs
- A notebook helper that downloads artifacts
Consolidate with MCP:
- Tools:
- trigger_build(project: string, branch: string, params?: object)
- get_build(build_id: string)
- list_artifacts(build_id: string)
- download_artifact(artifact_id: string)
- Resources:
- /projects -> metadata about projects user can see
- /builds/recent -> recent builds for current workspace
- /artifacts/{build_id} -> list of artifacts
- Prompts:
- "CI triage" template that guides the model through log analysis
- Policy:
- Only SREs can trigger builds on protected branches; require consent
- Rate limit downloads per user
- Transport:
- WebSocket behind SSO for enterprise; stdio for local testing
Clients now simply connect to your MCP server and get a consistent CI capability set, with security and auditing centralized.
Frequently asked implementation questions
- How do I handle streaming outputs (e.g., logs)?
- Use chunked responses or server-initiated notifications tied to a request id. If the SDK supports streaming content, adopt that; otherwise, fall back to polling via tools or resources.
- Can I expose both local and remote resources?
- Yes. Many servers multiplex: local workspace files and remote APIs. Keep clear namespaces and trust tiers.
- How do I reduce hallucinated tool calls?
- Provide concise tool names, tight argument schemas, and good descriptions. Consider few-shot examples or a "tool selection" prompt. Some clients can auto-hide tools until context demands them.
- What about backward compatibility?
- Version tool names or include a version field in tool metadata. Deprecate with grace periods and dual-publish.
References and further reading
- Model Context Protocol (official specification and SDKs). Search for the MCP spec and SDK repositories from the protocol’s maintainers; at the time of writing, popular implementations exist for Python and TypeScript.
- Language Server Protocol (LSP) and Debug Adapter Protocol (DAP) overviews to understand the initialize/initialized handshake and capability negotiation patterns.
- JSON-RPC 2.0 Specification for the underlying message framing and error semantics.
- Secure software supply chain guidance (e.g., NIST SP 800-204 series) for principles on auth, RBAC, and audit that translate well to MCP deployments.
Closing thoughts
MCP is a pragmatic evolution: take the battle-tested lessons of LSP/DAP, add model-friendly tool schemas and resource access, and package it in a transport-agnostic, security-conscious protocol. If you build or operate developer tools, it’s worth treating MCP as your first-class integration surface. You’ll replace a zoo of plugins with a consistent server that any AI client can use—under your policy, with your auditability, and without reimplementing the same connector five different ways.
It won’t solve everything, and you’ll still need to do the hard work of schema design, security, and governance. But as an organizing principle for AI-driven developer tooling, MCP is the most credible path away from plugin sprawl and toward a safer, faster, and more maintainable future.