[go: up one dir, main page]

0% found this document useful (0 votes)
78 views57 pages

Creatingmcpserverswithoauth

This document is a preview of the book 'Creating MCP Servers with OAuth' by Zach Silveira, which aims to guide readers from the basics of MCP to deployment within a weekend. It covers various topics such as environment setup, building the first MCP server, and integrating OAuth. The book is currently a work in progress, allowing for reader feedback and updates as it develops.

Uploaded by

rosamariarod74
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
78 views57 pages

Creatingmcpserverswithoauth

This document is a preview of the book 'Creating MCP Servers with OAuth' by Zach Silveira, which aims to guide readers from the basics of MCP to deployment within a weekend. It covers various topics such as environment setup, building the first MCP server, and integrating OAuth. The book is currently a work in progress, allowing for reader feedback and updates as it develops.

Uploaded by

rosamariarod74
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 57

MCP Servers with OAuth

A full introduction to MCP, from zero to deployment in one


weekend

Zach Silveira
This book is available at https://leanpub.com/creatingmcpserverswithoauth

This version was published on 2025-07-09

This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and
many iterations to get reader feedback, pivot until you have the right book and build traction once
you do.

© 2025 Zach Silveira


Tweet This Book!
Please help Zach Silveira by spreading the word about this book on Twitter!
The suggested tweet for this book is:
I just started reading “Building MCP Servers with OAuth” from @zachcodes!
https://leanpub.com/creatingmcpserverswithoauth
Contents

Work In Progress Changelog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1


Book Changelog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1

Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Library Choice Is Critical . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Sharing data across AI Providers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Integration into your system . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
It’s REST, but for LLMs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Why are they needed . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4

Environment Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
VS Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Installing Bun . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Initialize our first project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Bun init . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6

Our First MCP Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7


Installing the official implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Adding our first feature . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Testing with the official MCP Inspector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14

MCP Deep Dive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19


Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Sampling (Completions) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Elicitation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
Choosing the right features to use . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Dynamic Prompts and Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Upgrading to HTTP Streaming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
Other MCP Clients . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Security Concerns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Deployment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50

OAuth . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
OAuth Server Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
Adding the OAuth Proxy to our MCP Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
Checking if users are paying before tool use . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
Optional Logins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
Quirks for Claude.ai integration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51

MCP Clients . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
Gemini CLI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
Claude Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
Claude Desktop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
Claude Website . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
Cline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
Work In Progress Changelog
Thank you for buying this early while it is still in progress.
As a thank you, I can add you as a paid subscriber for 3 months to my newsletter on zach.codes1 .
You will be able to access my chat section2 to leave comments, questions, and other feedback for
this book.
I will address each concern as I near completion, and clarify everything I can.
Please email me@zach.codes if you’d like to be added and provide feedback in my substack chat.
Leanpub does not give me your email by default, so I cannot do it proactively.

Book Changelog
Until completion of the book, I will be sending out an update each Friday afternoon. My goal is to
complete this by mid July. I moved back another week due to the holiday
July 18th, 2025 (in progress)

• OAuth section initial draft


• Revamp introduction section

2025-07-11

• Address feedback from main book reviewer, fixes and clarifications


• Added diagram of what happens when using our first server prompt
• Added “Dynamic Prompts and Tools”
• Added “Async Tool Registration”
• Added “Adding an MCP tool that utilizes a REST api”

2025-06-27

• Add tool calling section rough draft near the top of “MCP Deep Dive”
• “Tools Over Prompts?” section added
• Add section for using the official MCP insepctor
• Setup source code repo for each section as complete reference
1 https://zach.codes
2 https://open.substack.com/chat/posts/609275e9-7d19-46d8-99be-cf92e86c361c
Work In Progress Changelog 2

• Add “Library Choice Is Critical” section

2025-06-20

• Added Sampling section rough draft to “MCP Deep Dive” Chapter


• Added new “Elicitation” section rough draft, from the 2025-06-18 spec, to “MCP Deep Dive”
Chapter
• Updated all examples to use new @modelcontextprotocol/sdk@1.13.0

2025-06-18

• Initial public release, around 30-40% completion


• Environment Setup and Building an MCP Server chapters are functional and 85% ready to read.
Introduction
In one weekend this book will help you become an expert with the latest MCP spec and be able to
use every possible feature MCP provides. That is the goal of this book.
I’ll use this section to talk about a few reasons you may want to build MCP servers and what they
are useful for. If you already understand this, you can skip ahead to the next chapter.
It’s important to note, once you finish the Environment Setup chapter, you can accomplish this book
offline, in case you want to do it on an airplane or other remote place…. without starlink :)
Here’s a few reasons to make MCP Servers, and I plan to revamp this before final release:

Library Choice Is Critical


One common theme I have seen lately as this specification changes rapidly, is every cloud provider
rushing to make their own implementation libaries. They want to gain marketshare and it’s a bit
unfortunate. Sometimes cloudflare or vercel implement parts of it faster and better documented than
the official implementation. However, I think it’s important in this book to lay a strong foundation,
accomplishing every task, using only Anthropic’s official implementation, and doing it all inside VS
Code.
We are able to test and build in one place. You can connect your server to other clients later, and
we do briefly do this at the end of the book as well.
In my entire career I’ve always hated vendor lock-in, so I see no reason to use a library from a
branded cloud provider. I hope you enjoy the direction of this approach, bringing portability and
simplification to what could otherwise become a mess of code on top of many other libraries that
may not last long term.

Sharing data across AI Providers


One valid use for an MCP Server stems from the fact that we need to share data across many AI
providers. You may already use ChatGPT for certain tasks, and Claude for others. Even if these
providers add memory recall features that work perfectly, this won’t help you work across different
providers, without duplicated setup.

Integration into your system


If you have a REST api already, or you want to expose your system’s data when you or your clients
are talking to AI bots, this is the perfect reason to get up to speed on MCPs.
Introduction 4

It’s REST, but for LLMs


Anything you want to expose through LLM’s, you’ll need to setup an MCP Server going forward.
This book will explain every feature of the specification, and include the crucial information needed
in order to authenticate users into your system.
If you want to charge money for your MCP, you will be able to. If you want to ensure users have
active subscriptions when they request information, that will be covered as well.

Why are they needed


Ultimately, they are needed because other approaches that provide LLMs with ways to interact with
outside systems, are not standardized. Maybe your model is trained to use the github command line
tool, like Claude does. This requires specific training, and for every user to manually install a local
command line tool.
MCP’s provide standardization and consistency, in defining actions that can be taken by LLMs, like
an explict tool name, description, and schema validation for required arguments. An example of
this would be a tool for “web_search” with a nice description of “Search the web for a given query”
and finally, requiring a “query” argument that must be a string in order to use this tool.
Environment Setup
Please ensure you have a solid understanding of TypeScript, REST apis, and basic programming
skills before attempting to follow this book. With that out of the way, we will dive right in to setup.
We only need two things to work on this book:

1. Have the latest version of VS Code installed.


2. We will be utilizing bun.sh for this entire book.

If you would rather use node you can replace all bun commands with “node” and you will just lose
out on a —watch mode built in feature we use when building some of the sections in the book.
Otherwise you should be fine, although you may need additional configuration1 to parse typescript.
This is why I preferred using bun for the book as it’s nearly zero config.
If you already have both of those things, you can skip to the next chapter. Otherwise, continue
below

VS Code
First thing we are going to do, is download the latest version of VS Code2
Why must we use this editor? As I started writing this book, they added official support of the entire
MCP Protocol3
This lets us edit our code, implement each major feature of the MCP spec, and test it, all in one
place. It makes it faster for you to learn, and easier for me to teach. Less time jumping around
to different places to test your MCP server, since you can connect to it in your same editor.

Installing Bun
bun.sh4 is simple to install, and faster than the default Node runtime.
The main reason we are using it today, is to minimize extra configuration and setup work that Node
sometimes requires. This lets us run one setup command, and TypeScript “just works.”
This is critical when we are trying to learn MCP’s, not deal with NodeJS configuration problems.
To install it, we will run the following command:
1 https://nodejs.org/api/typescript.html#enabling
2 https://code.visualstudio.com/
3 https://code.visualstudio.com/blogs/2025/06/12/full-mcp-spec-support
4 https://bun.sh/
Environment Setup 6

1 curl -fsSL https://bun.sh/install | bash

If you are using windows, it is supported, just go to their website directly for setup instructions. We
will be using MacOS and everything should work across the major platforms.
If you prefer, you may also pull the oven/bun official docker container instead.
To keep it simple we will install it using curl so it’s available globally on our system. Be sure to
source your bash or zsh profile afterwards. If you are not sure what that means, just open a new
terminal window after the install.

Initialize our first project


With bun installed, we will create a new folder to start building inside of.
Make this wherever you’d like on your machine:

1 mkdir mcp-book
2 cd mcp-book

Bun init
Make sure you are inside of the folder we just created.
Run the following command:

1 bun init

We will choose “Blank” as the default template:

Figure 1. Bun init output

Finally, open up this folder in VS Code, our environment setup is complete!


Our First MCP Server
In this chapter we will start small and fast, making an MCP server that serves dynamic prompts.
Then we will gradually add more features to it.
At this point, we have Bun installed, and we have our mcp-book folder opened inside of VS Code.
In this chapter we will:

• create an STDIO Server


• make it sharable across our dev team
• Expose a prompt that accepts a variable
• Access and run this prompt inside of VS Code (our MCP client)

By the end of this chapter, you’ll already have a basic understanding of how an MCP server works.
If you are interested in the lower level specification understanding. I will sprinkle it in along the
way, however, we are more focused on building than we are discussing every low-level detail.
Here’s a simple explainer of the spec for those who want to start building and skip the fluff:

1. All transport protocols use json-rpc1 . This is a simple way to call functions remotely. Think
of it like passing “function name: addition” and params “1 and 2” through a well defined
json schema. The server grabs a function by that name, and calls it with the arguments, then
responds back with the result in a specified manner again. It’s very straight forward.
2. You can pass these json rpc messages using the stdio transport. This is just a fancy term
meaning that an MCP client (such as Claude Desktop or VS Code) executes your mcp server
on-device, and sends json rpc over the command line.
3. You can pass these json rpc messages using http requests. More specifically HTTP Streaming.

Consider reading the core specification2 for detailed understanding of every action taking place with
each feature. It’s a short read.

Installing the official implementation


Inside our mcp-book folder, we will run:

1 https://www.jsonrpc.org/
2 https://modelcontextprotocol.io/docs/concepts/architecture
Our First MCP Server 8

1 bun add @modelcontextprotocol/sdk@1.15.0

You may attempt to install the latest version, but we want to ensure the rest of the code shown in
this book works in case breaking changes are added to the SDK before I can update it.
The official implementation already comes with zod and a few other sub dependencies that we will
be able to import through it. If you prefer you can pin zod yourself, but this can lead to having more
than one copy in your app. If the @modelcontextprotocol/sdk changes anything in a future update,
I will update the book after.

Adding our first feature


We’re going to build a server that exposes some custom prompts. There’s many great reasons to
do this. For example, you may want a prompt that says “Start the given task, and once complete,
commit it for me with a message that makes sense” and then whatever you pass in, gets attempted
and committed all in one shot.
Another example might be, “rewrite my long message into a short, viral tweet form.” The ideas are
endless, and there’s many reason you’d want reusable prompts that can be pulled it at any time.
A more advanced example of this, would be a prompt that takes an issue number on GitHub, pulls
the information, works on it, commits it, and makes a pull request, all in one shot.
This is just one feature of the MCP Server specification. Which lets us access our server inside any
AI client that supports the model context protocol. Prompts are a very useful feature that anyone
can quickly understand the value of having. Without it, you need to configure prompts for every
tool you use individually, and they must be static files that you setup, they cannot contain dynamic
information.
We’ll start building our a simple prompt server now.

Creating the Server and a Prompt


Your index.ts file at this point should contain

1 console.log("Hello via Bun!");


Our First MCP Server 9

STDIO Server
We will replace it with the following code:

1 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


2 import { z } from "zod";
3 import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
5 const server = new McpServer({
6 name: "My MCP",
7 version: "1.0.0",
8 });
9
10 server.prompt("one-shot-task", { task: z.string() }, ({ task }) => ({
11 messages: [
12 {
13 role: "user",
14 content: {
15 type: "text",
16 text: `Pleast attempt the following task, and once done, commit it for me wi\
17 th a message that makes sense:\n\n${task}`,
18 },
19 },
20 ],
21 }));
22 const transport = new StdioServerTransport();
23 server.connect(transport);

This creates a basic server, and registers a single prompt. This prompt uses Zod to validate the inputs
from the language model and ensures a task string is passed in to the prompt. This is how the official
protocol from Anthropic ensures required inputs are passed in from the MCP client.
Essentially, anyone who installs our MCP server, can choose the “one-shot-task” prompt and pass
in a task, and then the user’s AI model will run the full prompt with our task.
This can be much more powerful, maybe you decide to make an api request based on some argument,
and your prompt changes based on that result.

Adding it to VS Code
You’re going to start by bringing up the “Command Palette” in VS Code. Search the “Help” bar at
the top if you are not sure how.
We will then search for the following:
Our First MCP Server 10

Figure 2. Add Server Palette

Hit enter.
Choose stdio:

Figure 3. Add Stdio

Choosing to install the server as user or workspace is up to you. I tend to install them under my
user, meaning all VS Code folders that I use. If there is an MCP server you are making for a specific
project only, then workspace can be a good option.
For the command to run, type in bun run --watch ~/projects/books/mcp-with-oauth/mcp-book/index.ts
You must correct the path to where you created this file on your computer
On Linux or Mac, you should be able to type echo $PWD to get the full path when inside of your
mcp-book folder.

Name it “my-stdio”:
Our First MCP Server 11

Finally, open the “Chat” tab (use the help bar on top) and start typing /one

When you hit enter, you will see a prompt for the “task” string value we added in our code:

I inserted “finish my book” at which point, the Chat content is replaced by the prompt generated
from our server!
Our First MCP Server 12

Here’s a nice visualization to see what happened:


Our First MCP Server 13

This is really awesome, as we can add this same server to Claude Code, Claude Desktop, Cline, and
many other tools. There’s more AI tools by the day that support this specification.
We are able to bring our prompts to any tool, without having to define prompt files according to
every tool’s own specification.
Our First MCP Server 14

Not to mention, we can make very powerful ones. Imagine one that fetches the current stock price
within the prompt to let you know if you should take action today. The possibilities are endless
compared to hard-coded prompt files.

Making this sharable with your Dev Team


Since we are using bun… we can do something really neat now.
Let’s say we want to share these prompts with our dev team. There’s two problems with using
the STDIO transport. What if one team member adds a new prompt. You will not see it until you
manually pull in their code and attempt to restart your MCP connection.
We could setup an HTTP Streaming transport for our internal team, but that would require us to
deploy to a shared server and make sure any updates are released to it automatically. That’s too
much work in this scenario.
There’s a much nicer solution. First, we already added our server using the --watch flag. This means
if any code changes, bun will reload the process for us.
Next, all we need to add is a few lines of code at the bottom… like this:

1 import { $ } from "bun";


2
3 setInterval(async () => {
4 try {
5 console.log(await $`git pull`.text());
6 } catch (err) {
7 console.error("Failed to run git pull:", err);
8 }
9 }, 60 * 5000);

This will run a git pull every 60s. Bun will reload the process, and we will have the latest code.
Now you can git init and push a repo up to github. Ask your teammates to pull the repo and add
the stdio command just like we did.
Anyone on your team can add and modify prompts, push to the main branch in git, and within 5
minutes, your teammates will have the latest code, no server required
This is a good point for you to play around, make a few more prompts, and get ready for the next
section, where we discuss using the official MCP Insepctor, before we move on to working with
more features that MCP provides.
If you add another prompt, even with our watch mode running, you will need to restart the server
by Cmd + Shift + P (opening command palatte) and using MCP: List Servers. This is due to VS Code
only querying a server for its prompt list when it is first started. Here’s the full path:
Cmd+shift+p “MCP: List servers” -> “my-stdio” -> “Restart server”
Our First MCP Server 15

Testing with the official MCP Inspector


Throughout this book we will use every MCP feature inside of VS Code. However, it is great to
understand that an official inspector exists for debugging. This is provided by the team at Anthropic.
When I first wrote this book, it was half-baked. At this point it’s gotten a lot better and can really
provide a lot of insight if you run into any bugs or things not working as you would expect.
You can continue on, and come back to this later if you want to stay in VS Code, but I recommend
running through this before we get into OAuth at the end of the book, as it provides a lot of helpful
tests to ensure everything is correct with our server.
To use the inspector, you can launch it this way:

1 bunx @modelcontextprotocol/inspector@latest

It has support for all the main transport protocols we discussed at the beginning of this chapter.
Open it up in your browser at the provided url, and choose “stdio” as the transport type for now.
Enter “bun” as the command, and then put “run path/to/file.ts” in the second part.
It needs to be the full path to the server file you created, for example mine is:

1 bun run /Users/z/projects/books/mcp-with-oauth/mcp-book/chapters/STDIOServer.ts

You can find the path by running echo $PWD in a unix based OS. This is the path to the reference file
in the source code repo that is optionally included with purchase.
Here’s how it looks in the inspector web view after pressing connect:
Our First MCP Server 16

You can go over to the prompts tab, and select list prompts to see the prompt we made:
Our First MCP Server 17

You can click “one-shot-task”


After clicking it, you can enter something in for our task argument, as seen here:

Pressing get prompt will return the result from our server, just like we did in VS Code earlier:
Our First MCP Server 18

Now you understand how simple this tool is, feel free to use it after building things in the next
chapter!
MCP Deep Dive
At this point, we understand how to make an STDIO MCP Server. We also understand how to
register prompts that are served by our server to any MCP client. We know how to connect using
VS Code as our MCP Client, and we know how to use the official MCP Inspector. We really learned
a lot already.
Now, we’ll take a look at the other features of the specification.
We will learn about resources, tools, sampling, and finally, the http streaming transport.
By the end of this chapter, the only thing left to learn will be authentication logic.

Resources
Okay, so we completely understand how simple and powerful custom prompts are for our server.
What about resources?
I am semi-confident you won’t be using this feature very often, but there are some neat ways to use
it, and we will explore one of those ways now.
Resources are supposed to represent static assets that you may want to pull in during chats with AI.
We’re going to add another method to our server, which will screenshot our website every 15
minutes.
First, we need to add this capability to the MCP Server instantiation:

1 const server = new McpServer(


2 {
3 name: "My MCP",
4 version: "1.0.0",
5 },
6 {
7 capabilities: {
8 resources: {},
9 },
10 }
11 );

For some reason, Anthropic decided you need to add an empty object for resources in order to use
them, but not for the other features.
Next, let’s install puppeteer so we can take screenshots of webpages:
MCP Deep Dive 20

1 bun add puppeteer

Finally, lets make a resource for our website’s screenshot:

1 // in memory storage of the latest screenshot


2 let latestScreenshot: string = "";
3
4 // function that screenshots the site
5 const getWebsiteScreenshot = async () => {
6 const browser = await puppeteer.launch();
7 const page = await browser.newPage();
8 await page.goto("https://zach.codes");
9 latestScreenshot = await page.screenshot({ encoding: "base64", type: "png" });
10 await browser.close();
11 };
12
13 // screenshot it every 15 minutes
14 getWebsiteScreenshot();
15 setInterval(() => {
16 getWebsiteScreenshot();
17 }, 1000 * 60 * 15); // every 15 minutes
18
19 server.resource(
20 "zach.codes",
21 "screenshots://zach.codes",
22 {
23 title: "Latest homepage screenshot",
24 mimeType: "image/png",
25 },
26 async (uri) => {
27 return {
28 contents: [
29 {
30 uri: uri.href,
31 mimeType: "image/png",
32 blob: latestScreenshot,
33 },
34 ],
35 };
36 }
37 );

We are doing something a little hacky. Because puppeteer can take 10-30s to finish its work, we
MCP Deep Dive 21

moved it outside of the resource call, and just keep a reference to the latest image in memory. I
wouldn’t recommend this in production.
After this, we use server.resource give it a name, resource URI, in this case
screenshots://zach.codes, and then define its mime type and title to display when an MCP client
chooses to display the list of resources available.
Finally, in the function, we return the latest screenshot. Let’s try using it now.
In your chat window in VS Code, click “Add Context…” then choose “MCP Resources”

You should see the one we just created:

Click it, and you should see it in your chat context. If you hover over it, you can even preview the
image:
MCP Deep Dive 22

If we ask Claude Opus what is currently on our website, it can see all of the information:
MCP Deep Dive 23
MCP Deep Dive 24

Use your imagination to register more resources on your server! Most of the time these will be
basic, static things. I like to think outside the box and provide dynamic, interesting uses for these
resources.
If you do QA often, you could have a resource take in a variable, like the page path, so it’s more
automated and dynamic, and then ask your AI something about it, if it looks correct, or to fix an
area of the design.

Dynamic Resources
Let’s modify our code above to make it more dynamic, and pull in the data when we request it. You
probably don’t want to do this on a production server used by many people, as it would queue up
puppeteer a lot, but it would work fine for a personal server.
MCP Deep Dive 25

Remove all the resource code we added above, and replace it with this:

1 import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";


2
3 server.resource(
4 "zach.codes",
5 new ResourceTemplate("screenshots://zach.codes/{path}", { list: undefined }),
6 {
7 title: "Latest homepage screenshot",
8 mimeType: "image/png",
9 },
10 async (uri, { path }) => {
11 const browser = await puppeteer.launch();
12 const page = await browser.newPage();
13 await page.goto(`https://zach.codes/${path}`);
14 const blob = await page.screenshot({ encoding: "base64", type: "png" });
15 await browser.close();
16 return {
17 contents: [
18 {
19 uri: uri.href,
20 mimeType: "image/png",
21 blob,
22 },
23 ],
24 };
25 }
26 );
27 const transport = new StdioServerTransport();
28 server.connect(transport);

The main changes include using new ResourceTemplate which lets us have variable paths, and then
doing the screenshot when the resource is requested.
What is interesting, is that we can now select this resource again, and type in “test” as the path
value. It will work! We can see this in our chat context now:
MCP Deep Dive 26

Correctly renders a 404 route image for us.


However, if we try to put a nested path, like test/test it will not work!
It appears there is not a way to do a catch all resource path, that gets nested route paths in the
parameter. I opened an issue1 and will update the book if I hear any news on this.

Tools
This will probably be your most used API interface of the MCP server specification.
Tool calling is a very important… tool in the MCP arsenal.
1 https://github.com/modelcontextprotocol/typescript-sdk/issues/650
MCP Deep Dive 27

A tool is essentially a function that any MCP client can call at anytime. Think of them like functions
in code, but for LLMs.
Let’s take a look at the basic setup, by making one that gets the current weather.

1 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


2 import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
4 const server = new McpServer({
5 name: "My MCP",
6 version: "1.0.0",
7 });
8
9 server.registerTool(
10 "fetch-weather",
11 {
12 title: "Weather Fetcher",
13 description: "Get the current weather",
14 },
15 async () => {
16 return {
17 content: [
18 {
19 type: "text",
20 text: "The weather is cloudy",
21 },
22 ],
23 };
24 }
25 );
26
27 const transport = new StdioServerTransport();
28 server.connect(transport);

Using this full example, if we connect to it in VS Code, we can add this new “fetch-weather” tool to
our chat tab context.
Click “Add Context…” in the chat tab.
MCP Deep Dive 28

Next, click “Tools…” at the bottom of the list.

Finally, search and click on “fetch-weather”

If we ask in our chat “can you fetch the current weather” we will be prompted to allow this tool call:
MCP Deep Dive 29

If we accept, you should see something like this:


MCP Deep Dive 30

We just made and executed our first tool call!


Now we need to make this more usable. Next step, we can add an argument for a city name. This
will help the LLM know what to place in order to call our tool. Let’s change registerTool to this:

1 server.registerTool(
2 "fetch-weather",
3 {
4 title: "Weather Fetcher",
5 description: "Get the current weather",
6 inputSchema: { city: z.string() },
7 },
8 async ({ city }) => {
9 return {
10 content: [
11 {
12 type: "text",
13 text: `The weather is cloudy in ${city}`,
14 },
15 ],
16 };
17 }
18 );

We use Zod to define what arguments this tool can take. In this case it requires a city string. If we
MCP Deep Dive 31

wanted to make something optional, you must put .optional() on the end when using Zod. Then
it passes it in to the resolver function for us to access.
If we ask the question again, about fetching the current weather, we will see this:

It won’t work and makes us mention a city name!


Now, please manually restart the server by opening the Command Palette again (see the help tab in
the top)
Search for > MCP: List Servers
Hit enter
Choose our server, hit enter, and choose restart.
Even with Bun’s fancy watch mode, we must do this, because VS Code caches tools and their
arguments, so it won’t work without doing this.
After that is complete, we can finally say “What’s the weather in paris?” and see the result:
MCP Deep Dive 32

Important Tool concepts


One thing you have to be careful of, is making your tool call names and descriptions not too generic,
and also not too specific.
If I want to provide the weather by city and by zip code. I could add a few arguments that make
city and zip code both optional.
However, this might start to confuse the LLM.
It may be better to have get_weather_by_zip and get_weather_by_city tool names that are separate.
This is a very new and hard problem currently. It requires experimenting with multiple models and
seeing what works well.

Massive JSON Responses

We don’t want to be like Linear’s MCP server (the v1 anyway). They have many arguments for
each tool, and I see Claude Opus get confused by it often. In addition, they are returning massive
json arrays with tons of extra data in response to different tool calls, making it hard for the LLM to
figure out information.
Instead of “get_issues” with 10 different arguments, it would be a lot better to have a specific, “get_-
issues_logged_in_user” for example.
MCP Deep Dive 33

Here’s why. If I ask Claude using Linear’s MCP to grab all my open issues. It will commonly get
an error on the first try, saying a “team_id” is missing. Then it ends up calling a get_teams tool and
then figuring out what the team is, before going back to the first task.
It’s a beautiful art of learning the types of queries people want to make, vs being overly generic,
when designing tool calling functions.
Here’s an example I just found today…. Adding the official Paypal MCP server and then just trying
something as simple as listing transactions, ends up in a loop that doesn’t work!

Adding an MCP tool that utilizes a REST api


We can use the same code we wrote above to do this. The only change will be making a real api
request inside of our tool call resolver function:

1 server.registerTool(
2 "fetch-weather",
3 {
4 title: "Weather Fetcher",
5 description: "Get the current weather",
6 inputSchema: { city: z.string() },
7 },
8 async ({ city }) => {
9 const response = await fetch(
10 `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitu\
11 de}&appid=YOURKEY`
12 );
13 return {
14 content: [
15 {
16 type: "text",
17 text: `The weather is: ${JSON.stringify(await response.json())}`,
MCP Deep Dive 34

18 },
19 ],
20 };
21 }
22 );

In this example we make a fetch request to openweathermap. If you are interested in trying this out
yourself, you’ll need to sign up to get access to their api.
If you ask the tool what the weather is at the White House… You’ll see something like this happen:

It’s easy to do any async operation when making MCP tools, since your function just needs to end
up returning a content array for the LLM at the end.
MCP Deep Dive 35

Async Tool Registration


Another feature that can be quite useful later on, is dynamic registration of tools.
Imagine a user is logged out, and you want to offer a few free tools. Once logged in, more become
available.
You may also have users who have different roles. Maybe the administration can “delete_todos”
when they’re logged in, but a normal user cannot. All of these are great uses for registering tools
dynamically.
Interestingly enough, the api from anthropic is once again, less than ideal… You must register a
tool, immediately mark it as removed, if you want it initially disabled. This feels weird but it works
simply… an odd combination for sure!
Here’s a full example of how it looks:

1 // From "Tools" section


2
3 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4 import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5 import { set, z } from "zod";
6
7 const server = new McpServer({
8 name: "My MCP",
9 version: "1.0.0",
10 });
11
12 // Async tool with external API call
13 const tool = server.registerTool(
14 "fetch-weather",
15 {
16 title: "Weather Fetcher",
17 description: "Get the current weather",
18 inputSchema: { city: z.string() },
19 },
20 async ({ city }) => {
21 return {
22 content: [
23 {
24 type: "text",
25 text: `The weather is cloudy in ${city}`,
26 },
27 ],
28 };
MCP Deep Dive 36

29 }
30 );
31
32 tool.disable(); // Disable the tool initially
33 setTimeout(() => {
34 tool.enable(); // Enable the tool after 5 seconds
35 }, 10000);
36
37 const transport = new StdioServerTransport();
38 server.connect(transport);

Instead of registering a tool and moving on, we assign the tool to a variable. After that, we disable
it.
Sometime later we can re-enable it, in this case, 10 seconds after the server starts running.
If you add this to your server that is already registered in VS Code, restart it (Command Palette ->
MCP: List Servers -> Select yours -> Restart) and you should see this output if you open up the log
viewer, under that same section where “Restart” is listed:

Later on when we add OAuth, you’ll understand how we can disable and re-enable when people
login.

Sampling (Completions)
Sampling is a neat feature that was added in the June 2025 specification update. It allows your server
to request an LLM prompt completion on the connected client. Imagine this as a way to provide
certain features on your server, without having to host or pay for an LLM provider to perform every
action server side, instead it can run in the user’s own AI client.
Here’s an example: Let’s say our server wants to generate a friendly greeting message based on what
time of day it is.
Without sampling, our server would send a prompt to its own internal LLM, and ask “please generate
a friendly greeting depending on the current time of the day, the current time is {date}”

Example using official OpenAI Library


That is totally fine to do, but lets imagine you have 2000 users on this MCP and they all use your
greeting service to make nice greetings.
MCP Deep Dive 37

You must pay for every invocation or have local compute to handle these LLM requests.
Let’s try one more example using code, to really solidify how this works.

1 import { OpenAI } from "openai";


2
3 const openai = new OpenAI({
4 apiKey: process.env.OPENAI_API_KEY, // Set your API key in environment variables
5 });
6
7 const response = await openai.chat.completions.create({
8 model: "gpt-4",
9 messages: [
10 { role: "system", content: "You are a helpful assistant." },
11 { role: "user", content: "Hello, who won the world series in 2020?" },
12 ],
13 });

Imagine I wrote the code above, inside of my MCP server. I’m assuming you know how code like
this works. It is requesting a chat completion using the official OpenAI library.
I could run this on my MCP Server, during a tool call, or other action. I can pay OpenAI… for every
use of this. Or! I can pass this same exact info to the createMessage method call on our MCP
server, to trigger sampling on the connected client!
Let’s finally look at how this works.
We request language model responses from our connected clients! Hopefully this is all making sense
now.
Make sure you add the sampling capabilities object to your server to start:

1 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


2 import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
4 const server = new McpServer(
5 {
6 name: "My MCP",
7 version: "1.0.0",
8 },
9 {
10 capabilities: {
11 sampling: {},
12 },
13 }
MCP Deep Dive 38

14 );
15 const transport = new StdioServerTransport();
16 server.connect(transport);

Once this basic setup is in place, we can now request that the connected client uses one of its AI
models to process a request for us.
Place the following code below the lines above:

1 const test = await server.server.createMessage({


2 messages: [
3 {
4 role: "user",
5 content: {
6 type: "text",
7 text: "What is the capital of France?",
8 },
9 },
10 ],
11 modelPreferences: {
12 hints: [
13 {
14 name: "claude-3-sonnet",
15 },
16 ],
17 intelligencePriority: 0.8,
18 speedPriority: 0.5,
19 },
20 systemPrompt: "You are a helpful assistant.",
21 maxTokens: 100,
22 });
23
24 console.log("Test result:", test);

Interestingly, we can try to request a preferred model, speed, system prompt to use, and max number
of tokens.
If you save this file and add it to VS Code again, like we did in the first chapter, you will see this
appear:
MCP Deep Dive 39

VS Code sees that after it connects, the server is requesting a completion! You should be very careful
about accepting these if the MCP server isn’t trustworthy, as it can eat up your credits, or even try
to extract more information from you without your knowledge.
If we hit approve, we can then see the following in the output of our connected session:
MCP Deep Dive 40

Sampling, which was recently finalized, already fully works in VS Code! It can be a great way to
build more complex MCP Servers without any overhead of needing to pay for your own models to
run server side.

Samping inside a tool


Coming soon, we will sample inside a tool call for a more realistic test.

Elicitation
Elicitations are just a fancy way of saying “a request to the client to fill out a form.”
Imagine trying to perform some action on our MCP server, like “get_weather” and the server realizes,
we don’t have your zip code. We can request it during the tool call, by eliciting a request for more
information.
We will start with a server similar to the one above for Sampling:

1 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


2 import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
4 const server = new McpServer(
5 {
6 name: "My MCP",
7 version: "1.0.0",
8 },
9 {
10 capabilities: {
11 elicitation: {},
12 },
MCP Deep Dive 41

13 }
14 );
15 const transport = new StdioServerTransport();
16 server.connect(transport);

Next, we will add our request:

1 const result = await server.server.elicitInput({


2 message: `No tables available. Would you like to check alternative dates?`,
3 requestedSchema: {
4 type: "object",
5 properties: {
6 checkAlternatives: {
7 type: "boolean",
8 title: "Check alternative dates",
9 description: "Would you like me to check other dates?",
10 },
11 flexibleDates: {
12 type: "string",
13 title: "Date flexibility",
14 description: "How flexible are your dates?",
15 enum: ["next_day", "same_week", "next_week"],
16 enumNames: ["Next day", "Same week", "Next week"],
17 },
18 },
19 required: ["checkAlternatives"],
20 },
21 });

This example was taken from the latest implementation support added in June 2025. You can see it
supports enums, booleans, strings, etc.
We can test this inside VS Code (please note, only the Insiders release has support right now)
Coming soon, testing and photos

Elicit inside a tool call


Coming soon
MCP Deep Dive 42

Choosing the right features to use

When to Elicit
Elicitation can be mitigated by using arguments for tool calls as much as possible. There may still
be times you need it though. In the restaraunt example, we can’t know until making an internal
request, if the restuarant has tables at a requested time. So we could either return a response saying
“no tables, please try another time” OR we can use eliciting to return back some alternatives. This can
definitely be a better experience, but it’s something not every server needs. Many times, returning
a message with info can be enough.
If an LLM returned to me, in text, a list of alternative times, I can choose to say “ok do 4:45” vs
having an elicitation prompt pop up in my face.
It’s very interesting and I lean towards the simpler, no elicit approach for this type of thing.
Curious if you have any ideas on better reasons to use them, but it feels like you’re turning an LLM
chat into a web form instead of sticking with the natural languages interface.

Tools Over Prompts?


Recently I had a task given to me, to make a content generation MCP. The idea was, pull in some
internal data, and spit out a blog post.
There’s many ways to go about it, but two good options are using prompts, or tools.
The first iteration, I used a prompt that takesa variable, imagine I want to make a prompt to make
a blog post about a city. So we need to pass in a city and our system will grab information about it.
We can register a prompt like /create-city-blog-post and when attempting to run it, it can prompt
us for a city name, like in the “Our First MCP Server” chapter.
This is great, if you need something very specific, and you do not need variations on how this works.
Tools might be a better way if you want to do lots of other things.
Let’s say I want to use this city data for something else. Maybe I want to compare two cities and
see what the LLM thinks is a better vacation spot.
In this instance, it would be better to register a get_city tool that returns the same information our
prompt was getting internally.
Now users can get city information and accomplish more generic tasks.
Ultimately it’s up to you which route to take, I hope this helps you on your MCP Server journey!
MCP Deep Dive 43

Dynamic Prompts and Tools


This is a very powerful feature, and it can get very “meta” as you will understand soon.
Imagine an MCP Server that uses AI to generate custom tools to use… on the fly! This is completely
possible.
More practically, you may use this feature to register different tools based on different user roles.
Maybe a “user” can access “blog posts” but only an administrator can access the “edit blog post”
tool.
In this section we will show how to register prompts in real time and notify the MCP client of the
changes.
Coming soon

Upgrading to HTTP Streaming


Alright! So we have officially learned everything we can do with our server, except authentication,
and serving it over a network. This is what people call a “Remote MCP”
You can take any of the server code you made in the earlier part of this book, and then move it inside
the code we are about to create below.
This is neccesary if you are going to follow along with the OAuth section after, and if you want to
serve your MCP over the internet, or shared server.
If you only want to use the STDIO local transport, you can come back to this chapter later if you
end up needing this functionality. Let’s get started!

Setting up an HTTP Server


For the next step, we will install express, and then listen to network requests for the code we wrote
in prior chapters.

Bun has its own HTTP server, but it is not compatible with the choices Anthropic made in
the official protocol implmentation. I had spent the time to convert back and forth from
Express request / responses to Bun’s Request / Response types prior to writing this book,
but it is too confusing and not worth it. So we will just use Express.

Let’s install express:


MCP Deep Dive 44

1 bun add express @types/express


MCP Deep Dive 45

Next we will replace the entire index.ts file with the code below, be sure to put your existing code
in the commented area:

1 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


2 import { z } from "zod";
3 import express, { type Request, type Response } from "express";
4 import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/stre\
5 amableHttp.js";
6
7 const app = express();
8 app.use(express.json());
9
10 app.post("/mcp", async (req: Request, res: Response) => {
11 try {
12 const server = new McpServer({
13 name: "My MCP",
14 version: "1.0.0",
15 });
16
17 // YOUR EXISTING SERVER CODE HERE
18
19 const transport: StreamableHTTPServerTransport =
20 new StreamableHTTPServerTransport({
21 sessionIdGenerator: undefined,
22 });
23
24 res.on("close", () => {
25 console.log("Request closed");
26 transport.close();
27 server.close();
28 });
29
30 await server.connect(transport);
31 await transport.handleRequest(req, res, req.body);
32 } catch (error) {
33 console.error("Error handling MCP request:", error);
34 if (!res.headersSent) {
35 res.status(500).json({
36 jsonrpc: "2.0",
37 error: {
38 code: -32603,
39 message: "Internal server error",
40 },
MCP Deep Dive 46

41 id: null,
42 });
43 }
44 }
45 });
46
47 app.listen(3000, () => {
48 console.log("MCP server listening on port 3000");
49 });

With the code above, we setup an http server, with a route on /mcp. Every time a request is made to
this route, a server instance is instantiated, and the mcp library handles the request. We also have
basic, top level error handling that throws a 500 error if something goes awry.
It’s important to understand you can have stateful sessions with MCP Servers, meaning a session id
header is attached and can be checked on the server. I find this to be unneccesary. Not only is the
code more complex, but we should be used to building stateless, serverless, applications. We will
receive user information later, and know who is logged in, but we don’t need to attach a session id
to each request in order to accomplish everything with MCPs. It’s worth mentioning this is possible
to add even if I do not agree it’s ever needed.
It’s more work to maintain an active session map using the official protocol, and I belive good,
isolated services, can be stateless when possible. It’s the same idea with utilizing JWT’s vs database
backed session storage, we will keep it simple in this book.
If you wish to track every action, you can do this without attaching a session id to every request.
These are the parts where my opinionated approach to a quick start cuts through the fluff that would
be more verbose, you’ll see similar decisions in the OAuth section.
I may expand or edit this section further depending on any feedback I receive as part of completing
this book.
Let’s move on and see how we can now add the server to VS Code again.

Adding our new Server to VS Code


We can start using our MCP server over the network now!
Ensure bun run index.ts is executed and running in a terminal window.
Now, bring up the command palette, by pressing CMD + Shift + P or Searching “Command Palette”
in the help bar at the top.
Search for MCP: Add Server:
MCP Deep Dive 47

Figure 4. Add Server Palette

Click “HTTP” as the type:

Figure 5. HTTP Type

Type in localhost:3000/mcp as the url:

Choose user settings, but this one is up to you:


MCP Deep Dive 48

Give it the name mcp-book:

Use our Server in VS Code


Now if you open up the copilot chat tab, you should be able to access any prompts, resources, or
tools you have registered. Below is an example using our original “one-shot-task” prompt from the
first chapter on building our server:
MCP Deep Dive 49

Hit enter now that it’s selected, and you are going to see this popup:

It’s asking what our task is, since we defined in our server a task string that MUST be passed in to
build the prompt.
I typed in finish writing the rest of this book for me and then hit enter:
MCP Deep Dive 50

Success! The prompt was returned to us and can now be used.


This may seem trivial, but imagine a server hosted publicly using this exact code.

Other MCP Clients


Coming soon, cover adding to other popular clients like claude desktop

Security Concerns
Coming soon

Deployment
Coming soon
OAuth
COMING SOON
In this chapter we will cover how to make it optional for users to login to your MCP server.
If they do not login, some tools can still work, and others can disable access.

OAuth Server Setup


This is optional, if you already have an OAuth server, you can skip to the next section. (better-auth,
I may provide a premade docker image)

Adding the OAuth Proxy to our MCP Server


COMING SOON
I highly recommend using the OAuth proxy setup no matter what. This lets our OAuth server live
on its own, not coupled fully to our MCP Server. Even if you plan to run it on the same server, the
proxy setup “just works” for any OAuth server, no matter if its hosted elsewhere. That means the
approach we are about to follow works no matter what way you run your server.

Checking if users are paying before tool use


COMING SOON

Optional Logins
COMING SOON
not possible yet, see open mcp issue

Quirks for Claude.ai integration


COMING SOON
If you want to take the code we have written, and be able to add it directly to Claude.ai remotely,
there are a couple quarks to be aware of.

1. claudeai scope
2. sse may be required
MCP Clients
In this chapter we will go over adding our server to many of the popular MCP clients.

Gemini CLI

Claude Code

Claude Desktop

Claude Website

Cline

You might also like