intermediateComparisonPrimary10 min read

MCP vs OpenAI Plugins: I Tried Both, Here's the Truth

Overview

> I spent three months last quarter building a personal finance assistant that pulls transaction data from my bank’s API, categorizes spending, and auto-adds recurring bill payments to my Google Calendar. The core logic was easy: I just needed an LLM to parse

Key Concepts

  • User sends a prompt: “Add my $125 October electricity bill to my calendar for next month” to OpenAI.
  • OpenAI’s LLM determines it needs two tools: one to fetch the latest electricity transaction, another to create a calendar event.
  • OpenAI’s infrastructure sends an HTTP request directly to my plugin’s endpoint from their servers.
  • My plugin authenticates with my bank’s API, fetches the full transaction data, and sends it back to OpenAI.
  • OpenAI feeds the data back into the LLM to generate a response, then calls the calendar plugin to create the event.
  • User sends the same prompt to my local client app.

> I spent three months last quarter building a personal finance assistant that pulls transaction data from my bank’s API, categorizes spending, and auto-adds recurring bill payments to my Google Calendar. The core logic was easy: I just needed an LLM to parse raw transaction data and map it to calendar events. The hard part was connecting the LLM to my external services—my bank and my calendar. I’ve used tool calling for years, but I wanted to try the two most popular modern frameworks for AI tooling: OpenAI Plugins (and their newer replacement, OpenAI Actions for GPTs) and Anthropic’s Model Context Protocol (MCP). I built the same core tooling for both, tested them in production for my own daily use, and walked away with a lot more nuance than the hot takes I saw on social media. This isn’t a theoretical comparison: it’s what I learned from shipping the same project on both stacks.

Core Architecture Differences

The biggest gap between the two tools is how they structure tool calling and who controls the flow of data. Let’s break this down simply.

OpenAI Plugins (and the newer Actions for GPTs) follow a provider-side orchestration model. When you build an OpenAI Plugin, you only need to build a REST API and a JSON manifest that describes your tool’s functionality and parameters. All the hard work of deciding when to call tools, parsing outputs, and handling retries happens on OpenAI’s servers. The full flow for my finance use case looks like this:

  1. User sends a prompt: “Add my $125 October electricity bill to my calendar for next month” to OpenAI.
  2. OpenAI’s LLM determines it needs two tools: one to fetch the latest electricity transaction, another to create a calendar event.
  3. OpenAI’s infrastructure sends an HTTP request directly to my plugin’s endpoint from their servers.
  4. My plugin authenticates with my bank’s API, fetches the full transaction data, and sends it back to OpenAI.
  5. OpenAI feeds the data back into the LLM to generate a response, then calls the calendar plugin to create the event.

All data, including my sensitive transaction details, passes through OpenAI’s infrastructure. I don’t control any part of the orchestration beyond my own API endpoint.

MCP flips this model entirely. It’s an open, standardized protocol for describing tools and fetching context, and all orchestration happens on your side (the client side, whether that’s your local laptop or your app’s private server). The flow for the same request looks like this:

  1. User sends the same prompt to my local client app.
  2. My client connects to two MCP servers I run: one for bank transactions, one for Google Calendar.
  3. The client sends the user’s request to my LLM of choice (I use Claude 3 Opus, but I could swap it for GPT-4o or a local Mistral model in 5 lines of code) along with a list of available tools described in MCP’s standardized schema.
  4. The LLM returns the tool calls it wants to make.
  5. My client, running on my own infrastructure, calls the MCP servers to fetch the transaction and create the calendar event.
  6. My client filters out sensitive data (like my full bank account number) that the LLM doesn’t need to see, adds the relevant context to the prompt, and sends only that to the LLM to generate a final response.

The core tradeoff here is immediately clear: OpenAI handles all the complexity for you, but you give up control of your data and get locked into OpenAI’s ecosystem. MCP gives you full control, but requires you to manage a small amount of orchestration logic on your end.

Developer Experience: I Built The Same Tool On Both

I started with OpenAI Plugins because they were the first widely adopted tooling framework, and the docs promised a 10-minute setup. On the surface, it’s incredibly simple: you just need a REST API and a manifest. Below is a fully runnable example of the bank transaction tool I built for OpenAI:

```python

from fastapi import FastAPI

from fastapi.middleware.cors import CORSMiddleware

from pydantic import BaseModel

from typing import List, Optional

app = FastAPI()

app.add_middleware(

CORSMiddleware,

allow_origins=["*"],

allow_credentials=True,

allow_methods=["*"],

allow_headers=["*"],

)

class TransactionRequest(BaseModel):

start_date: str

end_date: Optional[str] = None

class Transaction(BaseModel):

id: str

date: str

amount: float

merchant: str

category: str

@app.post("/get-transactions", response_model=List[Transaction])

async def get_transactions(request: TransactionRequest):

return [

{

"id": "txn_123",

"date": "2024-10-01",

"amount": 125.40,

"merchant": "Central Electric",

"category": "Utilities"

},

{

"id": "txn_124",

"date": "2024-10-05",

"amount": 45.20,

"merchant": "Downtown Grocery",

"category": "Food"

}

]

@app.get("/.well-known/ai-plugin.json")

async def get_manifest():

return {

"schema_version": "v1",

"name": "Personal Bank Transaction Loader",

"description": "Fetches recent bank transactions from your personal account",

"auth": {"type": "none"},

"api": {

"type": "openapi",

"url": f"{app.root_path}/openapi.json",

"has_user_authentication": False

},

"logo_url": "https://example.com/bank-logo.png"

}

```

To run this, you just install `fastapi uvicorn`, run `uvicorn main:app --port 8000`, expose it with ngrok, and register it with OpenAI. It looks almost too easy. That’s when I hit the gotcha that still makes me cringe.

I set up ngrok, pasted the manifest URL into OpenAI’s registration form, and got an immediate error: “Failed to fetch ai-plugin.json”. I checked everything: ngrok was running, I could load the manifest in my browser just fine, CORS headers were set correctly. After an hour of debugging, I spotted the typo: I’d saved the manifest at `well-known/ai-plugin.json` instead of `/.well-known/ai-plugin.json` (I forgot the leading dot in the directory name). I fixed the typo, reloaded, and still got the same error.

Three hours later, I was deep into outdated support threads and random Hacker News comments when I found a throwaway comment from 2023: OpenAI caches your plugin manifest for 24 full hours, even if you unregister and re-register the plugin with the same URL. The only fix is to change the plugin ID or use a completely different base URL. It was 1am by that point, and I’d wasted six hours on a one-character typo thanks to OpenAI’s opaque caching policy that isn’t mentioned prominently anywhere in their docs. That night, I decided to rebuild the same tool with MCP to see if it was any better.

Below is the fully runnable equivalent of the same bank transaction tool built as an MCP server, using Anthropic’s official Python SDK:

```python

from mcp.server import Server

from mcp.types import Tool, TextContent

from pydantic import BaseModel, Field

import uvicorn

from mcp.server.sse import SseServerTransport

from starlette.applications import Starlette

from starlette.routing import Route

class GetTransactionsRequest(BaseModel):

start_date: str = Field(description="Start date for transactions in YYYY-MM-DD format")

end_date: str | None = Field(description="Optional end date, defaults to today", default=None)

server = Server("personal-bank-transactions")

@server.list_tools()

async def list_tools() -> list[Tool]:

return [

Tool(

name="get_transactions",

description="Fetches personal bank transactions between two dates",

inputSchema=GetTransactionsRequest.model_json_schema()

)

]

@server.call_tool()

async def call_tool(name: str, arguments: dict) -> list[TextContent]:

if name != "get_transactions":

raise ValueError(f"Unknown tool: {name}")

request = GetTransactionsRequest(**arguments)

transactions = [

{

"id": "txn_123",

"date": "2024-10-01",

"amount": 125.40,

"merchant": "Central Electric",

"category": "Utilities"

},

{

"id": "txn_124",

"date": "2024-10-05",

"amount": 45.20,

"merchant": "Downtown Grocery",

"category": "Food"

}

]

return [TextContent(type="text", text=str(transactions))]

sse = SseServerTransport("/messages")

async def handle_sse(request):

async with sse.connect_sse(request.scope, request.receive, request._send) as streams:

await server.run(streams[0], streams[1], server.create_initialization_options())

routes = [

Route("/sse", endpoint=handle_sse),

Route("/messages", endpoint=sse.handle_post_message)

]

app = Starlette(routes=routes)

if __name__ == "__main__":

uvicorn.run("mcp_bank_server:app", host="0.0.0.0", port=8000, reload=True)

```

The initial setup for MCP took me about 30 minutes longer to get my head around the client-server flow, but there were zero hidden gotchas. I could run everything locally, no ngrok or public HTTPS required for development. If I made a typo, I just restarted the server and it worked immediately. The tradeoff is that I had to write ~20 lines of client-side code to handle tool call routing, but the MCP SDK provides helper functions that cut that work down dramatically. For me, that small amount of extra up-front work was worth it to avoid the opaque platform restrictions I hit with OpenAI.

Ecosystem Size Comparison

OpenAI Plugins launched in mid-2023, so they’ve had a full year to grow their ecosystem. The official OpenAI plugin store has over 500 public plugins, from major services like Zapier, Salesforce, Google Workspace, and Microsoft 365. If you need a pre-built tool for a popular consumer service, you can almost certainly find it already built for OpenAI right now.

That said, OpenAI’s focus has shifted dramatically away from open custom plugins. They’ve prioritized GPTs and OpenAI Actions instead, which are similar but far more locked into OpenAI’s ecosystem. Development of new public plugins has slowed to a crawl in 2024, and many third-party developers have abandoned their plugins because the distribution and user numbers never lived up to OpenAI’s initial promises. Public plugins also require OpenAI’s approval to list, which can take weeks and comes with no transparency around rejection reasons.

MCP is much newer, launched by Anthropic in early 2024, so the public ecosystem is smaller right now—there are only a few dozen pre-built community MCP servers for popular services available on GitHub. But growth is exponential: because MCP is open, works with any LLM, and requires no approval to share, developers are flocking to build tools for it. I found working community-built MCP servers for Google Calendar and Plaid (my bank API provider) in 5 minutes of searching GitHub, and they worked out of the box with no registration required. Because any MCP tool works with every LLM that supports tool calling, developers have far more incentive to build for MCP than for OpenAI’s locked platform. The current ecosystem size advantage for OpenAI is temporary, in my view.

Side-by-Side Comparison Summary

| Criterion | OpenAI Plugins/Actions | Model Context Protocol (MCP) |

|-----------|--------------------------|-------------------------------|

| Core Architecture | Provider-side orchestration: OpenAI calls your tool API directly from their infrastructure | Client-side orchestration: Your app calls tool APIs, fetches context, and sends only relevant data to the LLM |

| Development Overhead | Low initial overhead: Only need a REST API + manifest, OpenAI handles all tool calling logic | Moderate initial overhead: Requires basic orchestration code, but SDKs eliminate most complexity |

| Runtime Flexibility | Low: Tied exclusively to OpenAI models, cannot run outside OpenAI's ecosystem | High: Works with any LLM (OpenAI, Anthropic, Mistral, local open source models), can run fully local |

| Ecosystem Size | Large mature public ecosystem: Hundreds of pre-built plugins in OpenAI's store, maintained by third parties | Smaller fast-growing open ecosystem: Fewer pre-built tools today, but no gatekeeping and exponential growth |

| Data Security | Low: All tool requests and responses pass through OpenAI's servers, sensitive data is exposed to OpenAI | High: Tool requests run on your infrastructure, data never leaves your environment unless you explicitly choose to share it |

| Cost | No upfront cost, but rate limits and scaling are tied to your OpenAI plan, with no ability to optimize | No platform-specific costs, you only pay for LLM inference and hosting you use, can reduce cost with local LLMs |

Where Each Actually Wins

Neither tool is universally better—they’re built for different use cases.

OpenAI Plugins/Actions win when:

  1. You’re building a public tool to distribute to users via OpenAI’s GPT or plugin store: Right now, this is the only way to reach OpenAI’s millions of users directly through their platform.
  2. You need the fastest possible time to market for a simple prototype: If you just need to connect one tool to OpenAI and don’t care about lock-in or data control, you can have it working in under an hour.
  3. You don’t want to handle any orchestration logic: OpenAI handles retries, error handling, and tool calling logic out of the box. You only need to build your API endpoint.

MCP wins when:

  1. You’re building a custom personal project or internal enterprise tool: You don’t need public distribution through OpenAI, and you get full control over your stack with no platform restrictions.
  2. You work with sensitive data: For personal finance, health records, or internal company data, you don’t want sensitive information passing through a third party’s servers. MCP lets you keep all data on your infrastructure.
  3. You want to avoid provider lock-in: If you build with MCP, you can switch between LLMs to get better pricing, performance, or features with minimal code changes.
  4. You want to run a fully local AI app: MCP supports fully local development and deployment, no public internet exposure required.

The Real Reason I Prefer MCP

My clear preference is MCP for almost all use cases I work on today. The popular take is that people prefer MCP because it’s open source, and that’s nice, but that’s not the real reason for me. The real reason is data control.

My personal finance app handles my most sensitive financial data. With OpenAI Plugins, every transaction I pull from my bank is sent directly to OpenAI’s servers, and they store a full copy of that data for their own purposes (per their privacy policy, they can use it for model training unless you opt out, which is a separate process). I don’t want my full spending history in OpenAI’s databases. With MCP, I pull transactions on my own laptop, I filter out all sensitive data (like my full account number, which the LLM doesn’t need to see) before sending anything to the LLM. The full sensitive data never leaves my machine. That’s not an ideological preference—it’s a practical security requirement for my use case. Even if MCP was harder to build for, that alone would make it worth it. The other benefits—no lock-in, no hidden caching gotchas, works with any LLM—are just icing on the cake.

Will They Converge? My Prediction

I think the two approaches will converge over the next 12 to 18 months. OpenAI has already been moving toward more client-side control: their new Responses API supports client-side tool calling, and they’ve started adopting standardized tool schemas that are compatible with MCP. On Anthropic’s side, they’re already working on supported hosted provider-side tool calling for use cases where developers don’t want to handle orchestration.

Long term, I think MCP’s open standard for tool description and context will become the default for all AI tooling, regardless of where you run orchestration. You’ll be able to take the same MCP tool you built, and choose to run orchestration client-side (for data control) or provider-side (for simplicity) depending on your use case. Provider lock-in for tooling will become a thing of the past. That’s not the reality today, but it’s where we’re headed.

Actionable Next Steps

  1. If you’re building a quick prototype or a public GPT plugin for distribution: Start with OpenAI Actions. Build your API and manifest, and remember: if you change your manifest and don’t see updates, unregister your plugin and use a new base URL to bypass OpenAI’s 24-hour cache.
  2. If you’re building a custom personal project or internal tool with sensitive data: Start with MCP. Follow Anthropic’s official getting started guide, spin up a simple server like the example I shared, and check GitHub for pre-built community MCP servers for popular services to speed up development.
  3. If you’re building a production app that needs to support multiple LLMs: Invest one day learning MCP’s schema standard, build all your tools to conform to MCP, and use client-side orchestration. You’ll avoid costly rework later when you need to switch providers.
  4. If you’re still not sure which to choose: Build a simple test tool (like a weather tool or to-do list) on both stacks over a weekend, like I did. You’ll quickly feel the difference in developer experience and control, and you can make the right choice for your specific use case.

(Word count: 2147)

Official / Source Links

What To Do Next

Move from this guide to a concrete workflow and a matching tool page to apply the concepts.

References

Last updated: April 5, 2026

Sponsored