Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lessons/03-setup-mcp-clients/B-tome-and-ollama.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ You can also run models locally on your computer using [Ollama][ollama]. Ollama

Keep in mind that the larger, "smarter" models typically require beefy GPUs to run with any sort of speed. That said, there are some models that run on even just CPUs and can still do tools calling.

> New models are coming out at break-neck pace. By the time you read this versus when I wrote it, I guarantee there will be several cycle of new models available. You will have to select one based on what's available. The good news is that the new ones are only getting more performant and "smarter".
> New models are coming out at break-neck pace. By the time you read this versus when I wrote it, I guarantee there will be several cycles of new models available. You will have to select one based on what's available. The good news is that the new ones are only getting more performant and "smarter".

What's important is that you select a model that can handle [tools calling][tools]. As of writing, a few good options that run on less-capable hardware

Expand Down
4 changes: 2 additions & 2 deletions lessons/04-lets-build-mcp/A-my-first-mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "ad

## JSON RPC 2.0

You should see the MCP server respond with an answer of 8! This feels just like writing API endpoints, but the advantage here is that we get to give these tools to LLMs and they can call into code we generate for them. Let's talk a bit about JSON RPC 2.0 which is all this is. [JSON RPC][rpc] is ancient in computing terms with the first version of the spec coming out in 2005. The revised 2.0 version came out and in 2010 and that's what this is using – we're not doing anything wild here, just relying on a very proven set of technology.
You should see the MCP server respond with an answer of 8! This feels just like writing API endpoints, but the advantage here is that we get to give these tools to LLMs and they can call into code we generate for them. Let's talk a bit about JSON RPC 2.0 which is all this is. [JSON RPC][rpc] is ancient in computing terms with the first version of the spec coming out in 2005. The revised 2.0 version came out in 2010 and that's what this is using – we're not doing anything wild here, just relying on a very proven set of technology.

So what _is_ JSON RPC? You can think of it as an alternative to REST. With REST you call endpoints that are based around a thing - e.g. you call a PATCH to /users/123 to update user 123. Your URLs are based things and the semantics of manipulating those things. JSON RPC (and XML RPC before it) is based around calling remote functions - that's it. It's literally a remote procedure call. So in this we're just giving an MCP server direction on what procedures (or functions) we want them to do. That's it!

Let's see it initializes itself!
Let's see it initialize itself!

```bash
echo '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "test-client", "version": "1.0.0"}}}' | node mcp.js | jq
Expand Down
2 changes: 1 addition & 1 deletion lessons/04-lets-build-mcp/B-using-our-first-mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ I'll show you how for both Claude Desktop and Tome but just Google "how do I use

## Claude Desktop

Open settings on your Claude Desktop client and go the developers tab. You should see something that will edit your config. It will then probably just point you at the file you need to edit, so open that with VS Code or whatever. Put this in there:
Open settings on your Claude Desktop client and go to the developers tab. You should see something that will edit your config. It will then probably just point you at the file you need to edit, so open that with VS Code or whatever. Put this in there:

```json
{
Expand Down
8 changes: 4 additions & 4 deletions lessons/04-lets-build-mcp/D-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,17 @@ const transport = new StdioServerTransport();
await server.connect(transport);
```

- We'll reuse as this our general MCP server for the issue project so keep this one around
- We'll reuse this as our general MCP server for the issue project so keep this one around
- Most of it should look familiar to you already
- `database-schema` is the name of the resource
- `schema://database` is the URI. this gives it a unique identifier that can be used by programs and LLMs to refer to specific resources.
- `schema://database` is the URI. This gives it a unique identifier that can be used by programs and LLMs to refer to specific resources.
- After that it's mostly just like a tool.
- Resource templates (which are dynamic) are really similar. The biggest difference is their URIs have something like `schema://database/{table}` or something like that and `{table}` becomes the name of the parameter that can be passed in.

Add this MCP server to Claude Desktop and restart it.

Now, click the ➕ button in the text box. You should see the ability to attach a resource. Attach database schema and then ask the LLM `explain my database schema for my issue to me in plain english` or something like that.
Now, click the ➕ button in the text box. You should see the ability to attach a resource. Attach the database schema and then ask the LLM `explain my database schema for my issue to me in plain english` or something like that.

That's a resource! This more useful for things like Google Docs where you can easily attach a doc to a request or something of that nature. But they're there and you add them to your MCP servers as you deem necessary!
That's a resource! This is more useful for things like Google Docs where you can easily attach a doc to a request or something of that nature. But they're there and you add them to your MCP servers as you deem necessary!

[gh]: https://github.com/btholt/mcp-issue-tracker
6 changes: 3 additions & 3 deletions lessons/04-lets-build-mcp/E-prompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ keywords:
- Airbnb style guide
- Claude
---
This one ends working fairly similarly to a resource but the idea is that it's a template for a prompt. Imagine you have common prompt you use a lot and it just needs a little tweak here and there to fit what you're doing. Or you have a team of people that need to share templates between them and you want one place to update it and automatically push it to your colleagues. Or maybe even a company could ship some canned prompts that work well with their tools (e.g. here's a good prompt for working with Neon.com). That's what prompts are for.
This one ends up working fairly similarly to a resource but the idea is that it's a template for a prompt. Imagine you have a common prompt you use a lot and it just needs a little tweak here and there to fit what you're doing. Or you have a team of people that need to share templates between them and you want one place to update it and automatically push it to your colleagues. Or maybe even a company could ship some canned prompts that work well with their tools (e.g. here's a good prompt for working with Neon.com). That's what prompts are for.

> Resources are meant more to be context given to an LLM while prompts are meant more to be commands for the LLM to do. The line is a bit blurry here so don't worry too much about it – I think there are still some iterations left before we land on the exact what MCP servers will work.

Expand All @@ -28,7 +28,7 @@ Feel free to use any style guide you want, but here are a few:
- [Airbnb][airbnb]
- [Standard][standard]

Just copy the entire file into an style-guide.md file in your directory of MCP servers. I think idiomatic is the shortest one if you're looking to save on tokens. Airbnb is certainly the longest, being 4x longer than Standard.
Just copy the entire file into a style-guide.md file in your directory of MCP servers. I think idiomatic is the shortest one if you're looking to save on tokens. Airbnb is certainly the longest, being 4x longer than Standard.

Then let's make a little MCP server for it. Make style-checker.js

Expand Down Expand Up @@ -70,7 +70,7 @@ const transport = new StdioServerTransport();
await server.connect(transport);
```

Add your MCP server to Claude and/or Tome, and restart Claude if necessary. Click the ➕, click code-review-server, and paste some code in the box that you want Claude to review. LLM code review! Now, we could just have ESLint do this (and 100% you should do that first) but I could see this being useful giving to an LLM up front that you want it follow certain patterns and rules or if you want LLM to give you advice on how best to fit code to a style guide. The plain English feedback is super nice.
Add your MCP server to Claude and/or Tome, and restart Claude if necessary. Click the ➕, click code-review-server, and paste some code in the box that you want Claude to review. LLM code review! Now, we could just have ESLint do this (and 100% you should do that first) but I could see this being useful giving to an LLM up front that you want it to follow certain patterns and rules or if you want LLM to give you advice on how best to fit code to a style guide. The plain English feedback is super nice.

That's it for prompts!

Expand Down
2 changes: 1 addition & 1 deletion lessons/04-lets-build-mcp/F-future-features-of-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ A critical part is that there is still human in the loop here because we don't w

Elicitation is a just a fancy word that your MCP server can ask follow up questions.

Think of an a tool that the MCP server could call to ask you to get your full address so it could fill out a form for you or one where it could ask for your specific GitHub username before it generates a report on GitHub usage. There's all sorts of reasons that a tool could need more information from the user. As of now we just have to hope that the LLM is smart enough to correlate this data for us, but this way we can just make it deterministic that correct information gets gathered as specified by the tool.
Think of it like a tool that the MCP server could call to ask you to get your full address so it could fill out a form for you or one where it could ask for your specific GitHub username before it generates a report on GitHub usage. There's all sorts of reasons that a tool could need more information from the user. As of now we just have to hope that the LLM is smart enough to correlate this data for us, but this way we can just make it deterministic that correct information gets gathered as specified by the tool.

[clients]: https://modelcontextprotocol.io/clients
[roots]: https://modelcontextprotocol.io/specification/2025-06-18/client/roots
Expand Down
4 changes: 2 additions & 2 deletions lessons/05-our-project/A-mcp-server-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ npm run dev # run dev script in root directory to start both frontend and backen
1. In the mcp directory run `npm init -y`
1. Then run `npm i @modelcontextprotocol/[email protected] [email protected]`
1. Add `"type": "module"` to the package.json
1. Finally, create a file called called main.js and put this in there.
1. Finally, create a file called main.js and put this in there.

```javascript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
Expand Down Expand Up @@ -114,7 +114,7 @@ Ideally, we can connect our app to Claude Desktop via MCP server so that we can
So what all do we need to worry about?

- Auth. Claude needs to be able to act on behalf of us.
- Order of operations. We need an issue to an exist before we can update it.
- Order of operations. We need an issue to exist before we can update it.
- Correct tags/people/etc. to assign to issues. We don't want "bug", "bugs", "issues", "prod-issue", and a trillion variations. We want one "bug" tag.

As you can see, there's a lot to juggle here, and it's trusting an LLM a lot to just say "here Claude, use the API directly". Inevitably it's going to mess up a lot. Instead of just wrapping our API directly, we're going to make an MCP server that covers entire "jobs to do" instead of API steps. So we're going to make a tool that "creates a ticket, assigns a user, and gives a correct label to it" instead of just hoping that Claude can get the sequence of API calls right.
Expand Down
2 changes: 1 addition & 1 deletion lessons/05-our-project/B-api-based-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ Create a new issue in my issue tracker that says "Add Microsoft login to my app"

Now, if you're using Claude Desktop, this will probably work. If you're using Qwen3:0.6B, well, flip a coin. I've had a hard time to get the smallest Qwen 3 model to do anything more than one step.

Once you feel okay with that, go head and change the import in main.js to import from the complete file
Once you feel okay with that, go ahead and change the import in main.js to import from the complete file

```javascript
import apiBasedTools from "./api-based-tools-complete.js"; // add complete at the end
Expand Down
2 changes: 1 addition & 1 deletion lessons/05-our-project/c-jobs-based-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ This is where just mapping your API server to an MCP server runs into trouble.

I'm going to call this "jobs oriented" MCP server as opposed to "API oriented". I made up these terms. But it suits what we're trying to get done today.

Instead of having our infinitely flexible API server and hoping the LLM chooses right, let's say we know we really only want our MCP server to do three thinks
Instead of having our infinitely flexible API server and hoping the LLM chooses right, let's say we know we really only want our MCP server to do three things:

- Create new bugs and mark them as high priority
- Create new feature request and mark them as low priority
Expand Down
4 changes: 2 additions & 2 deletions lessons/06-sses-and-streaming-html/A-server-side-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ We're not going to dwell here too much because this was a pretty short lived ide

So the problem we have with the stdio transport (transport is what we call the way info is exchanged - think HTTP vs WebSockets, different ways of communicating) we've been using so far is that it only works locally. You have to download an MCP server from somewhere and run it yourself. This offers a lot of advantages - it will have access to local files, USB, terminals, etc. and you can choose how to scale it. It also could be a bit of a security liability - you're letting arbitrary code run locally and the "person" in charge is an agent. This is one of the biggest criticisms in general about MCP servers. You also have to be sure to update it yourself - if the MCP maker pushes an update, you need to go grab it yourself.

So enter the need for a "remote" MCP server - something an LLM can use like a a local stdio MCP server, but acts more like an API server where it can make those MCP calls to a remote location.
So enter the need for a "remote" MCP server - something an LLM can use like a local stdio MCP server, but acts more like an API server where it can make those MCP calls to a remote location.

The first attempt of this was a "server-side event" (nearly always written as SSE) protocol where you'd call an SSE endpoint and be given back a second endpoint that has a unique identifier for this session. The client (like Claude Desktop) will keep an open connection to this main endpoint and receive messages from the server as HTTP messages sent. If the client needs to send something to the server, the client POSTs a message to the unique endpoint that it got initially.
The first attempt of this was a "server-side event" (nearly always written as SSE) protocol where you'd call an SSE endpoint and be given back a second endpoint that has a unique identifier for this session. The client (like Claude Desktop) will keep an open connection to this main endpoint and receive messages from the server as HTTP messages are sent. If the client needs to send something to the server, the client POSTs a message to the unique endpoint that it got initially.

![SSE diagram](/images/sse.png)
<small>Source: [modelcontextprotocol.io](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse)</small>
Expand Down
8 changes: 4 additions & 4 deletions lessons/06-sses-and-streaming-html/B-streamable-http.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ So let's talk about how streamable HTTP is different than SSEs. For one, there's

The topography of handshakes here is a bit more complicated but at the end you get a resumable session, one endpoint to deal with instead of two, and a server that can be stateless as long you architect it well.

But yeah, the key here is that the server gives the session a UUID as a session ID and then the client refers to that using an HTTP header to make sure that the server and client both understand the ongoing context. That's really it. The idea of SSEs still happen inside of this, but it's only part of the architecture instead of all of it.
But yeah, the key here is that the server gives the session a UUID as a session ID and then the client refers to that using an HTTP header to make sure that the server and client both understand the ongoing context. That's really it. The idea of SSEs still happens inside of this, but it's only part of the architecture instead of all of it.

We're going to implement our MCP main.js again but instead in `streamable.js` and using Express as our Node.js server. Express is chosen because we really just need minimal HTTP helpers and it's the one everyone gets. You can use Fastify, Next.js or whatever you want here.

Expand Down Expand Up @@ -138,12 +138,12 @@ app.listen(PORT, () => {
});
```

- Every sessions needs a UUID to keep track of which session is ongoing. I'm just using node:crypto for this and JS object to keep track of it. This wouldn't scale - every client would need to hit the same client which makes it hard to scale. You'd probaby use Redis or something to share state amongst stateless servers to scale this better.
- Every session needs a UUID to keep track of which session is ongoing. I'm just using node:crypto for this and JS object to keep track of it. This wouldn't scale - every client would need to hit the same client which makes it hard to scale. You'd probably use Redis or something to share state amongst stateless servers to scale this better.
- We need to handle POST for client-to-server messages, GET for server-to-client messages, and DELETE for ending sessions.
- I turned off the DNS rebinding protection so we can use the MCP inspector but this is something you'd leave on in prod. Bascially you don't want people to be able to jack other people's sessions if they're able to guess the UUID, that would be a huge vulnerability. But locally it doesn't matter.
- Beyond this, this should just look like a normal ol' web server which it is. We definitely could have (and probably should have) just built this into our backend.

Let's try in it now.
Let's try it now.

```
npx @modelcontextprotocol/inspector
Expand All @@ -153,7 +153,7 @@ Then in the UI put in `localhost:3100/mcp` to connect to your server. Make sure

Now you should see our three jobs-based tools in the inspector.

So what are limitations here?
So what are the limitations here?

- Obviously we can't just shell out CLI commands - we're constrained to only what we can do on our server and pass back to the user.
- We have to worry a lot more about security - we don't want to leak other users' data because we did something wrong.
Expand Down
Loading