
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
  <channel>
    <title>Tobin South — Blog</title>
    <link>https://tobin.fyi/blog/</link>
    <description>Notes on AI agents, MCP, security, verifiability, and policy.</description>
    
    <item>
      <title>How to build an MCP server without going insane</title>
      <link>https://tobin.fyi/blog/mcp-faq/</link>
      <guid>https://tobin.fyi/blog/mcp-faq/</guid>
      <pubDate>Mon, 01 Sep 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[<p>I spend a lot of time building MCP servers with folks. It's a new protocol, frameworks are constantly changing, and it's often unclear what the right way to build or test things is. Most of the knowledge of how to make them really great is tacit and shared through word of mouth. This is my attempt at writing it down—so here are some thoughts and FAQs on how I like to build servers, up to date as of today.</p>
<h2>Where do I start? (to build a server)</h2>
<ul>
<li>If you like Python, <a href="https://gofastmcp.com/getting-started/welcome">FastMCP</a> is the best option, and it comes with its own <a href="https://www.fastmcp.cloud/">hosting platform</a>, which provides WorkOS Authkit support for authentication with zero additional work (simply click “Add Auth” in the cloud deploy). It also has easy <a href="https://gofastmcp.com/integrations/authkit">WorkOS Authkit setup</a>.</li>
<li>If you like TypeScript, use the <a href="https://github.com/vercel/mcp-adapter">Vercel mcpHandler</a> wrapper and host on Vercel. The Vercel wrapper is a thin layer on the TypeScript SDK that makes it better for Vercel hosting. You also get to create a website to accompany it. You can use our demo on <a href="http://mcp.shop">mcp.shop</a> (or see it on GitHub), which is built with this. You could use the core TypeScript SDK instead of the mcpHandler if you want.</li>
<li>If you want something very simple or standalone (e.g., you don't plan to integrate into an existing build stack), use <a href="https://smithery.ai/docs/getting_started/quickstart_build">Smithery</a> for a super-quick setup and remote deploy.</li>
</ul>
<h2>How do I know it worked? (the development testing cycle)</h2>
<ul>
<li>Do all the following steps, in order, with (1) your unprotected local MCP server, (2) your auth-protected localhost MCP server, and finally (3) your remote-hosted MCP server. Do this and I promise there will be no bugs (for now).</li>
<li>I always run <code>npx mcp-remote &lt;URL&gt;</code> first. This tests that the server connects correctly and will handle any auth flows (and give you better errors than the inspector). It's friendly and will likely address most of your immediate concerns. This is how stdio is used. You may want to run <code>rm -rf ~/.mcp-auth</code> to clear the auth state for testing.</li>
<li>Add it to Cursor / VS Code / your favorite IDE client (not as an <code>npx mcp-remote</code>, but as <code>&quot;url&quot;: &quot;http://localhost:&lt;PORT&gt;&quot;</code>). This will make sure your tool connects (and can be used). These give poor errors but will show you what coverage you have if your tool is exposed correctly.</li>
<li>MCP Inspector: <code>npx @modelcontextprotocol/inspector</code>. Why not the inspector first? It has the strictest controls and will raise errors on completely functional servers. It's great with an unprotected server, but when you use an external, real authorization server, you'll run into CORS errors, which can be painful to debug. Servers that work well with Cursor and Claude often still encounter issues when using the inspector. Use this last to make sure your MCP server is robust in the tail.</li>
<li>Claude.ai or ChatGPT.com: once the inspector passes, you have auth, and you've deployed to remote, you can start testing with Claude.ai (a key consumer interface). If you need to test your local with this for debugging, use ngrok. ChatGPT.com will require you to have <a href="https://platform.openai.com/docs/mcp">search and fetch tools</a>.</li>
</ul>
<h2>How do I add auth / API keys?</h2>
<ul>
<li>Use local servers while you can, and add auth as soon as you want to make it remote. Debugging the vibes is easier locally and unprotected.</li>
<li>Make the remote OAuth. Everything in the spec assumes that the MCP server is an OAuth-protected resource server. Enterprises, registries, and common clients all assume OAuth.</li>
<li>⚠️ Do not roll your own auth; you will regret it in two weeks when the spec changes again.</li>
<li>API keys are fine for devs and can be a nice DX. This is for <code>mcp.json</code> or coding agent stuff, not for production Claude integrations/connectors, where OAuth is a better UX for everyone. If you need persistence, see M2M / async below.</li>
<li>If you're building a new app, just use Authkit. Just look at our customer list or MCP Night, and you can see why. It just works. Follow the <a href="https://workos.com/docs/authkit/mcp">docs</a> as the source of truth for setup.</li>
<li>If you already have an existing product with user management/auth of a different kind, WorkOS offers <a href="https://workos.com/changelog/standalone-oauth-for-mcp">Standalone OAuth for MCP</a>, a simple OAuth proxy tool that handles MCP OAuth and allows you to keep your existing solutions.</li>
<li>Your server should verify the JWT against both the <code>iss</code> (your Authkit domain) and the <code>aud</code> (the environment's client ID, e.g., <code>client_123abc</code>, the thing they can copy on the API keys page) for added security.</li>
</ul>
<h3>But I want my MCP to run asynchronously in an agent workflow</h3>
<ul>
<li>Me too. There are lots of persistent agents I want to have MCP access without user/browser intervention. This is not what DCR and user-consent flows are designed for.</li>
<li>Instead use M2M auth with client credentials. This is much more secure than API keys for a bunch of reasons and is widely supported across use cases and enterprises. MCP client support may vary for now, but is under discussion in the spec. If you need to run API keys for now in your own workflows, create a custom API key verifier tied to an existing user-management system.</li>
</ul>
<h3>How does user identity / RBAC interact with MCP?</h3>
<ul>
<li>MCP is super cool in that you can show different tools to different folks depending on their user role. You can also deeply customize the functionality of the MCP in the same way you can with any web infrastructure.</li>
<li>When the user connects to MCP, they get a 401/<code>WWW-Authenticate</code> request. The step right after is <code>list_tools</code>. As a result, the output of <code>list_tools</code> is dynamic based on the user. Just wrap the tools with an <code>if (user.role === ...)</code> and you can make dynamic MCP servers.</li>
<li>You can also make tool descriptions (explaining what can be done by the user/Claude) dynamic to the roles.</li>
<li>Otherwise, normal RBAC on functionality in the code logic also applies here.</li>
<li>You can also (sort of) dynamically update the MCP server tools at runtime by sending a new <code>WWW-Authenticate</code> and changing the tools in the background (e.g., updating the user's role/permissions). This doesn't seem fully supported right now.</li>
</ul>
<h3>Agent identity?</h3>
<ul>
<li>This is an ill-defined concept, but at the very least you have a User-Agent or possibly client ID metadata one day. This could allow you to edit the functionality of the MCP server depending on the context where it is deployed.</li>
<li>You might want to consider workarounds in instances where MCP clients have strict requirements (like ChatGPT forcing you to have search and fetch tools).</li>
</ul>
<h2>What issues are going to come up?</h2>
<ul>
<li>CORS. Especially with auth. If Authkit gives you errors, set the Inspector localhost URL in Authentication &gt; Sessions &gt; CORS. You might also need to pass the CORS headers from your server. <code>import { metadataCorsOptionsRequestHandler } from &quot;mcp-handler&quot;</code> might also help in TypeScript. Just ask Claude.</li>
<li>I need to reset my auth state locally: run <code>rm -rf ~/.mcp-auth</code>.</li>
<li>Resource indicators. For your server to authenticate correctly, the auth sessions need to be bound to a resource (the URL of your MCP server that is being connected to). This could be <code>localhost:&lt;PORT&gt;</code> or your deployment URL. Sometimes people bind to the server root; sometimes they bind to <code>&lt;URL&gt;/&lt;transport&gt;</code> (e.g., <code>https://mcp.shop/mcp</code>). You should make your code definition for the deployment URL dynamic or you'll run into random issues. If you need an example of this, look at this <a href="https://github.com/workos/mcp.shop/pull/19">PR</a> when we first noticed the bug.</li>
<li>Refresh tokens. Making OAuth not suck and not log you out depends on these via the <code>offline_access</code> scope. This is a client-side problem as long as you use a good authorization server. Clients are slowly getting better at supporting this.</li>
<li>CI/CD or eval. These are both hard and important. It's evolving and I'll add more notes later. You want to add (1) standard auth evals, (2) spoof auth success with tool access checks, (3) vibes-based tool-use evals. This is an evolving science. We have a related E2E auth testing setup <a href="https://github.com/workos/next-authkit-example/pull/41">here</a>.</li>
<li>JWT templates. Authkit custom JWT templates do not currently apply to auth flows with DCR (Dynamic Client Registration). This is on the roadmap—reach out if you're interested.</li>
<li>Extra scopes/permissions. Again, we're not supporting these right now in favor of doing more robust user-management logic inside your application.</li>
</ul>
<h3>Really silly bugs</h3>
<p>At some point you might just have no idea what the source of the bug could be. This could be almost anything. Just in case it's useful, here are a series of random bugs that I've seen before.</p>
<ul>
<li>Cloudflare or another bot blocker disabling the <code>Claude</code> user agent, restricting MCP connections.</li>
<li>Your AWS VPC is misconfigured.</li>
<li>Your <code>/.well-known</code> endpoints aren't actually accessible.</li>
<li>There is a bug upstream in one of the SDKs (I often spin up a TS or Python demo with my auth server just to isolate whether auth, the resource server, or the underlying SDK is the source of the issue).</li>
</ul>
<h3>Refresh tokens (my server keeps logging me out)</h3>
<ul>
<li>The job of refresh tokens/<code>offline_access</code> is to allow short-lived auth tokens to be refreshed. Not all clients handle this well. Similarly, different instances of something like Claude Code might spin up different clients without sharing auth.</li>
<li>A quick (bad) hack around this is to increase the default expiry time of the auth token (you can do this in the Authkit dashboard).</li>
</ul>
<h2>I want to use MCP-UI / Elicitations / Resources / more (not just tools)</h2>
<ul>
<li>These are all incredibly cool extensions on MCP.</li>
<li>MCP-UI is perhaps the coolest thing in MCP—full stop. It is also incredibly hard to figure out how to do right, especially with respect to Remote DOM. If you want to build with it, you should join the Discord and ask for help.</li>
<li>Elicitations is an extremely powerful primitive, but is <a href="https://modelcontextprotocol.io/clients">completely unsupported by most clients</a>.</li>
<li>Resources are the more obvious primitive to use than tools in many cases, but have remarkably <a href="https://modelcontextprotocol.io/clients">low support across clients</a>. Use with care.</li>
<li>In general, the spec is moving toward a core-plus-extensions model. Be prepared for a world with a lack of feature consistency.</li>
</ul>
<h2>I want to build a client</h2>
<ul>
<li>This is tougher than it seems sometimes (even though it shouldn't be).</li>
<li>Use one of the off-the-shelf frameworks (mcp-use, LangChain, AI SDK) to minimize the pain.</li>
<li>MCP-UI on the client side seems particularly hard.</li>
<li>Auth on the client side isn't that hard to do as a first pass, but a lot of care needs to be taken in token handling and refreshing. Again, copy—don't build.</li>
</ul>
<h2>I want to build an agent / sub-agent / A2A</h2>
<ul>
<li>MCP is a really nice way to offload a context-processing-heavy task into a subprocess that returns minimal, useful context without polluting the context window. This is the “Context Protocol” vision of MCP and is also my working definition of a context sub-agent (e.g., an MCP call to a process that runs an AI workflow that then returns context).</li>
<li>If you're running an LLM workflow in your MCP server tool, be careful of timeouts. You might want to think about async tools.</li>
</ul>
<h2>How do I explain MCP to my boss/mom/arch-nemesis?</h2>
<ul>
<li>I just say it's the USB-C for AI tools.</li>
<li>It helps agents connect to other software tools in a standardized way, which facilitates better infrastructure, interoperability, and model post-training.</li>
<li>It's more than an API wrapper.</li>
<li>I'll have a post about this soon.</li>
</ul>
]]></description>
    </item>
    
  </channel>
</rss>