Connect AI to Anything - Build your First MCP Server (Model Context Protocol)
Timestamps
- - Introduction
2:49- What does MCP stand for?3:19- How does MCP work?4:21- A demo using Claude7:46- How I use MCP- - 3 Parts of MCP
- - stdio vs http
- - Features
- - Resources
- - Prompts
- - Tools
- - Calling Tools
- - MCP vs AI features
- - Building an MCP Server
What is MCP
MCP is just a standard way to plug tools in to an AI.
Out of the box a model can only talk. It can tell you all about your email or your database, but it has no way to actually send the email or run the query. Model context protocol closes that gap: you run a small server that exposes tools, your AI connects to it, and now it can go do things in your apps instead of just chatting with you about them.
Anthropic designed it (the company behind Claude), but it's a standard now, not a Claude-only thing. If you're on OpenAI or Gemini or some other tool you're going to run in to the same idea, sometimes under a more general name like connectors or integrations.
There are two sides to this, and which one you are changes why you'd care:
- If you're a USER of AI, MCP is how you wire your different apps and services in to your AI so it can work across all of them.
- If you're a DEVELOPER building something, you'll probably end up creating the MCP server for your app, so your users can connect to it through their own LLMs and get abilities specific to whatever your app does.
We'll touch both. The plan for this one: a quick definition and a demo so you can see what it actually looks like, then back to the chalkboard for the concepts you need, then we build our own server to wrap it up. You don't really need any prerequisites, though if you've seen my API videos the idea is very similar, just adjusted to be more AI friendly.
The spec is open and genuinely readable if you want the source of truth: https://modelcontextprotocol.io/docs/getting-started/intro
Course Catalyst Demo
Before any theory, let's just watch it work.
Course Catalyst is my own course platform, and it exposes an MCP server. In Claude you go to connectors, add a custom connector, and paste the MCP URL. That kicks you to an approval page, you hit approve, and it's connected. From the connector you can manage it and see the tools it gives you, and they're split in to read-only tools and writing tools: listing page blocks, broadcasts, contacts, coupons, courses, forms, lessons, offers. Pretty much anything you'd do inside the app itself you can now do through the MCP.
First something small, I just ask about my platform and it pulls the answer straight from the connection (in my case info about a free Claude Code crash course, day two). No leaving the chat.
Then something with more moving parts. Here's the actual prompt I ran:
Create a coupon for 30% off the current list price of Agentic AI tier 2 for loyal customers, which would be anyone inside of Software Engineering Mastery. Draft a thank you email to anyone in this bundle, excluding anyone who has already bought Agentic AI. Send me the preview and confirm the recipient count. Coupon expiration is end of day tomorrow. Specify that it's limited time and the subject of the email. Once I approve, we can send it.
One prompt, and it's creating a coupon, building an audience with an exclusion, drafting the email, and sending me a preview with the recipient count before anything goes out. I switch it to auto mode so it stops asking permission on each step (if you want to know more about auto mode or bypassing permissions, that's in my one hour Claude Code intro). It hands back the preview, I approve, and it's sent.
Here's another one:
Generate an example landing page with the Agentic AI course tier 2, with the course content block for tier one and two. Default vibe is fine to get something started. Use my synced bio and testimonial blocks.
The processing can take a bit depending on the ask and the model, so I run it in the background and come back. About 5 minutes with Fable 5, and there's a link to the page. First iteration and it's already got an overview of what the course covers, the actual course structure, the tiers with enrollment options and buy buttons, testimonials, an about me, a Q&A, and another buy button at the bottom. Obviously I'd go through and review and update it, but that saves so much time.
That's the point of MCP: once your apps expose tools, the AI can go operate them instead of just talking to you about them. Now let's understand what actually happened.
Migrating My Business
Quick story so you know this isn't a toy demo I built for YouTube.
For the last 6 months or so I've been building my own platform to host everything for my business. It's multi-tenant, so multiple customers run from the same platform, each with their own landing pages, domains, and customers, all separated out. When I talk about MCP here, one customer connects to just their own environment, a lot like getting an API key scoped to your data on some platform.
The thing I wanted MCP for first was imports. You can always write scripts to move data around through APIs, and I started down that path, some of those in-app import tools still exist. But what I actually wanted was a full migration across multiple platforms, multiple years of data, thousands of transactions, tens of thousands of contacts, and every course people had bought so they don't lose access. That's a big one, and it's one of the biggest reasons people get stuck on a platform, moving off is just hard.
So I tried building it through skills and LLMs using MCP. Because it was a bigger effort, I had the LLM keep all its progress in a markdown file, I just call that the vault, and one of the files in there was the migration report, the progress checker. Then I had it scan the other services, Notion, Kit, Kajabi, giving it access either through their MCP or API or just handing it my login token to go grab everything.
The first thing I'd have it do was write out what needed done, not actually do it, just spec out the work with checkboxes, one box per lesson or video that needed migrated. Then a second box for a separate verifying agent, a fresh context window with no influence from the previous work, just there to catch errors. LLMs aren't flawless so I'd still do a thorough look myself, but you'd be surprised how well it holds up when the work is tracked and each piece is easily verified. Especially with scripts in the mix, so instead of going 100% LLM, it's the LLM plus scripts the LLM writes.
Through that we imported hundreds of videos and hundreds of thousands of words of notes (videos re-uploaded to the new platform, not just linked), scraped and re-uploaded images from the landing pages and notes, formatted all the code properly in markdown so it renders in the notes, and imported transactions and contacts. I also had it keep a separate section for any conflicts or issues and prompt me for how I wanted them resolved. Quick example, one platform had a course called AI Mastery and I'd renamed it to AI Engineering on the new one, there's no clean one to one match there, so it would just list the conflicts and let me pick the resolution. That mattered for things like old transactions where the product had been archived, what do we even grant those people. The scripts I did my best to make idempotent and I leaned heavily on transactions when appropriate, so a contact import was pass or fail for integrity instead of half applying.
All of it went in to a skill. Skills are kind of the functions or libraries of the LLM world now, they document and contain the scripts, instructions, and the verifying agent definition, so any time I need to import again I just run the skill.
That's not really the point of this video, but it's a good example of how far MCP goes for big messy automations where a clean hand-written script isn't quite enough. Ok, now let's learn how the machine actually works.
Server, Client, Host
There are three parts to MCP, and people mix the terms up constantly, so let's go through them.
The server is the thing exposing tools, sitting in front of your data or your API. Notion runs one, GitHub runs one, Course Catalyst runs one, and later you'll run your own.
The host is the actual AI application, the thing powering the LLM and running the show, Claude, ChatGPT, Cursor, whatever.
The client is an individual connection from the host to one server. The difference between client and host is a little gray, but that's how I see it, the host is the app, the client is one connection inside it. And one host can hold many clients, which is how a single chat reaches Notion and GitHub and your own app all at once (that's also how the migration worked).
A nice property of it being a standard is the server has no idea which AI is on the other end and doesn't care. Before MCP every app built one-off integrations per AI platform, now you build one server and every MCP-speaking app can use it.
One more thing about the server: where it lives is irrelevant. Most people picture some external service out there, their email or whatever, but it can just as easily be local tools on your own computer. That gives you two main ways the client and server talk:
- stdio, for local apps running in your own environment, communicating over standard in and standard out
- HTTP, for talking over the web
We'll use stdio for our build since it's just us on one machine, and I'll point at HTTP as the next step.
The Spec
Since I keep saying "it's a standard", it's worth knowing there's an actual spec behind it. It's a versioned document that covers the message format (it's JSON-RPC, more on that in a second), the lifecycle of a connection, and all the capabilities the server and client can negotiate.
Do you need to read it to build something? Not really. If you understand the pieces above you're going to get pretty far, and you can read in to the spec once you get in to the more advanced stuff or when something breaks between a client and server. It's a good read though, shorter than most API docs: https://modelcontextprotocol.io/specification
Resources, Prompts, and Tools
Going a bit more in to the spec, there are three things the server can provide to the client: resources, prompts, and tools. This is basically everything the server hands over to teach the LLM how to use the service, and I'm going to spend almost all of our time on tools, because I think that gets you 90% of the way there, possibly all you'll ever need.
Technically the client can give the server some stuff back too. The three I've memorized are sampling, roots, and elicitation. For my own MCP I've never had to touch any of them, everything I do is wrapped through tools, so you can build a fully working server and only worry about that one piece. Do your own research on the client side if you get curious, we might do a dedicated video on it down the road.
Quick pass on the other two first.
Resources are a way to read data from the server. If you've got an API background you've heard "resource" used for an entity, the actual thing you're getting from a database, and it's the same flavor here, read-only data addressed so the client can pull it in. In practice you can just make read-only get tools instead, so I don't lean on resources much.
Prompts are pre-canned messages the server hands to the client to guide it, so instead of just handing over a pile of tools you can also give the client prompts on how to work with your server. It's really similar to a skill or a slash command, a prepackaged message that brings context in to the thread. The only real difference is where it lives and how much it carries, a skill usually has more data and larger workflows, but in theory a skill and a server prompt can be the exact same thing.
A Tool Call on the Wire
Now tools. You can think of these as endpoints the client invokes on the server.
The messaging is JSON-RPC. RPC stands for remote procedure call, and it's very close in idea to invoking an endpoint on a server with an API. A request has a jsonrpc version, an id, and a method. The first call just lists what's available:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }
That gives the AI a high level view of what tools it could use, stuff like get_coupon, and very often each one goes one to one with an API endpoint. If you look at any particular tool you'll find a description attribute. Think of it like a comment, except it's written specifically to tell the LLM invoking it how to use it and what to expect. So instead of writing comments for other developers, we're writing comments for LLMs so they use the tool correctly and don't screw things up. The description is the context we're infusing in to the client, that's what I want you to see it as.
Then the client makes another call, but the method is tools/call instead of tools/list:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "create_coupon",
"arguments": { "code": "YOUTUBE20", "percentOff": 20 }
}
}
Notice params does double duty. The name says which tool we're actually calling, and arguments is the payload the server defined for that tool. Then, just like with an API, the server responds with something the consuming LLM might need for later work, the created coupon's id, a confirmation, whatever. That's where we'd confirm the coupon was actually made.
We could talk theory forever, but let's wrap the chalkboard with one more thing before we build.
MCP vs AI Features
Sometimes a thing could live in either an MCP or a built-in AI feature, and you have to pick, so here's how I think about it.
If you're building an app you could put an AI button right on the page, click it and the user generates something, reviews something, does some AI stuff. That hits your server, works with your data, and costs YOU tokens, but it's a nice native experience, the user doesn't have to connect to Claude or anything, they just click.
But those buttons are often an alternative to an API. If the button creates a page by adding blocks, an API could add those same blocks, and anything you can do through an API you can do through MCP. So you could skip the built-in button and just expose the same tools over MCP. In that version the user runs it from their own LLM, so THEY pay the tokens, and your server basically acts like an API, no AI compute on your side. That means no worrying about credits or people abusing your AI features, it's all delegated to the user.
The downside is they have to connect their LLM to your service, and you know how users are, an extra step they don't understand is a step a lot of them just won't take. A built-in button is less scary, they're already logged in.
So you have to think about what belongs in the MCP. My take: if you have enough tools to fully operate the app through AI, push people toward the MCP and go lighter on the built-in AI features, because at that point AI is the interface. But if you just want one AI thing, like summarizing some notes, making them connect an MCP and invoke a tool for that is not the way, just build the feature. I'd only really invest in the MCP layer if you're building a whole new way of working with the app.
Realistically you'll see apps do both, AI-enhanced features and search and summaries built in, plus an MCP for the action stuff, creating, updating, connecting, analyzing. Right now it's kind of the wild west, there's some standardization but one platform's MCP can take a totally different approach than another's. Over time I think that settles down, to where working with an MCP is predictable because everyone's used to running apps through their LLM. Ok, enough chalkboard, let's build one.
Build Step 1 - The API
We're going to build a tiny API first, then wrap it with an MCP server, so you can see exactly where the MCP layer sits.
Fresh folder, a node project, and the packages:
mkdir mcp-example
cd mcp-example
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript tsx
npm install express
I like starting with TypeScript even when I'm not sure I'll use much of it. Make a server.ts, and this is just a normal little Express API with the coupons kept in memory:
import express from "express";
const app = express();
const coupons = [
{ id: 1, code: "YouTube20", percentOff: 20 },
{ id: 2, code: "launch", percentOff: 10 },
];
app.get("/", (req, res) => {
res.json({ coupons });
});
app.listen(3000, () => {
console.log("Running on http://localhost:3000");
});
Try to run it with node server.ts and you'll get an error:
SyntaxError: Cannot use import statement outside a module
Node's telling you to treat these files as ES modules, so pop open package.json and set the type (mine was commonjs, switch it):
"type": "module"
Run it again, open localhost:3000, and there's the JSON. That's basically a mocked API, in a real app that data comes from a database. Point is we now have an app server.
Build Step 2 - The MCP Server
New file, mcp-server.ts. This is where we do the same stuff as the API, but through the MCP layer. Start with the imports:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
StdioServerTransport is the local transport from the chalkboard, it communicates over standard in and standard out, so you can kind of think of anything we'd normally console log as what's getting sent. Now the server itself, and the connect line at the very end:
const server = new McpServer({ name: "product-server", version: "1.0.0" });
// tools go here
await server.connect(new StdioServerTransport());
It's a bit of setup to get going, but you only do it once. After that, adding a tool is the same shape every time. Here's our first one:
server.registerTool(
"list_coupons",
{
description: "Lists out available coupons and % discount",
},
async () => {
return {
content: [{ type: "text", text: JSON.stringify(getCoupons()) }],
};
}
);
The name is list_coupons, then a settings object where the description is the message that goes to the LLM. There's also an optional input and output schema you can define here, we'll skip that for a plain get. Then the function that actually runs, returning an object with content, which is an array of items that each have a type and a text.
For the text I'm calling a getCoupons() function instead of touching the array directly. Since the API and the MCP server are running in the same place, I don't need to make an HTTP call, I can just share the code. Over in server.ts:
export const getCoupons = () => {
// any logic here
return coupons;
};
And the endpoint can return getCoupons() too. So right now the tool is literally a one to one with the API endpoint, doing the exact same thing.
Build Step 3 - Splitting Out the Shared Code
If you import getCoupons straight from server.ts you drag the whole Express app along with it, app.listen is a top level line, so importing the file starts the server. That was a mistake in how I structured it. The fix is to pull the shared stuff in to its own file so neither side has to run Express.
Make coupons.ts and move the data and the functions there:
const coupons = [
{ id: 1, code: "YouTube20", percentOff: 20 },
{ id: 2, code: "launch", percentOff: 10 },
];
export const getCoupons = () => {
// any logic here
return coupons;
};
Now server.ts imports getCoupons from ./coupons, and mcp-server.ts imports it from ./coupons too, and the MCP server runs on its own without the API server running at all.
Before this compiles cleanly, generate a tsconfig and give it a few settings:
npx tsc --init
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true
}
}
Then compile and run:
tsc
node mcp-server.js
Build Step 4 - Connecting It to Claude Code
We understand the server from the consuming end, now let's actually connect to it. This time we'll do it through Claude Code. From inside the folder:
claude mcp add coupon-server -- node /Users/codebreakthrough/cs/mcp-example/mcp-server.ts
Use the absolute path to your file (right click, copy path). That command is just how Claude launches your server as a local process, the stdio transport again. Launch claude, trust the folder, and run /mcp, and it lists your connections. The very first section is local MCPs, so go in to the coupon server and you can disable, reconnect, or view tools, where you'll see that description we wrote.
Now a prompt. Even when you don't have to name the server it's sometimes smart to, so it doesn't grab the wrong thing when you've got multiple connections that could have coupons:
use local stdio MCP server to get all coupons and show which is the best
It calls coupon-server, comes back with the two coupons, YouTube20 and launch10, and picks YouTube20 as the best, your own server getting called by the AI.
Build Step 5 - A Write Tool
The get is nice, let's add one that changes data. Back in coupons.ts:
export const createCoupon = (code: string, percentOff: number) => {
const coupon = { id: coupons.length + 1, code, percentOff };
coupons.push(coupon);
return coupon;
};
Then register a matching tool in mcp-server.ts:
server.registerTool(
"create_coupon",
{
description: "creates a coupon",
},
async ({ code, percentOff }) => {
const coupon = createCoupon(code, percentOff);
return {
content: [{ type: "text", text: JSON.stringify(coupon) }],
};
}
);
The arguments come in as a single object that we destructure and pass through to createCoupon. That gives us a schema-expectation problem though, we never told the tool what shape those arguments are. That's what the input schema is for, and we'll define it with zod:
import { z } from "zod";
server.registerTool(
"create_coupon",
{
description: "creates a coupon",
inputSchema: {
code: z.string(),
percentOff: z.number().min(1).max(100),
},
},
async ({ code, percentOff }) => {
const coupon = createCoupon(code, percentOff);
return {
content: [{ type: "text", text: JSON.stringify(coupon) }],
};
}
);
Now it knows code is a string and percentOff is a number between 1 and 100. Compile again (always recompile to get the latest JavaScript if you're on TypeScript), and back in Claude you might see a failed connection if the old server shut down, so just reconnect. Then:
create a new coupon code for 60% off
It calls the server, creates it, and hands back Save60.
Where to Go Next
That's your introduction. There's obviously so much more you could do.
First thing I'd do is get rid of the in-memory array, that's going to bite you the moment you're running through both the API and the MCP, so move it to a database, then build out more endpoints and more tools to work with that data. At some point you'll want to break away from stdio and look in to HTTP so other people (or your phone) can reach the server, and the process is pretty similar. Maybe we'll get in to that in an upcoming episode.
The fastest way to make all of this stick is to wrap something you already own. Pick an API you use, expose a couple tools, and go ask your AI to do something with it.