Creatingmcpserverswithoauth
Creatingmcpserverswithoauth
Zach Silveira
This book is available at https://leanpub.com/creatingmcpserverswithoauth
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.
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
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)
2025-07-11
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
2025-06-20
2025-06-18
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
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.
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
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.
1 https://www.jsonrpc.org/
2 https://modelcontextprotocol.io/docs/concepts/architecture
Our First MCP Server 8
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.
STDIO Server
We will replace it with the following code:
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
Hit enter.
Choose 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
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.
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
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:
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
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:
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
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”
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:
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
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.
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
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
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:
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!
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
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}”
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.
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:
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:
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.
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:
13 }
14 );
15 const transport = new StdioServerTransport();
16 server.connect(transport);
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
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.
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.
Next we will replace the entire index.ts file with the code below, be sure to put your existing code
in the commented area:
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.
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
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.
Optional Logins
COMING SOON
not possible yet, see open mcp issue
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