<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ mcp server - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ mcp server - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 17 Aug 2026 19:17:58 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/mcp-server/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Create a Marketing Landing Page Using shadcn/ui ]]>
                </title>
                <description>
                    <![CDATA[ Most marketing landing pages start with the same problem: you're staring at a blank screen and rebuilding sections you've already created countless times. A hero section, feature grid, testimonials, p ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-create-a-marketing-landing-page-using-shadcn-ui/</link>
                <guid isPermaLink="false">6a6c8095409fd0bcb0afd1c6</guid>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mcp server ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Design ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Vaibhav Gupta ]]>
                </dc:creator>
                <pubDate>Fri, 31 Jul 2026 11:01:41 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5fc16e412cae9c5b190b6cdd/82bb4c21-aebe-48c4-9a66-f013011223f4.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most marketing landing pages start with the same problem: you're staring at a blank screen and rebuilding sections you've already created countless times.</p>
<p>A hero section, feature grid, testimonials, pricing, FAQ, and footer are common building blocks, yet developers often spend hours recreating them for every new project.</p>
<p>In this guide, you'll learn how to build a modern marketing landing page using Next.js, shadcn/ui, Tailwind CSS, and reusable Shadcn blocks. Instead of building every section from scratch, you'll assemble a production-ready page, customize it to match your brand, and finish with a foundation that's ready for real-world projects.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-are-we-building">What Are We Building?</a></p>
</li>
<li><p><a href="#heading-project-setup-with-base-ui-using-the-shadcn-preset">Project Setup with Base UI Using the Shadcn Preset</a></p>
</li>
<li><p><a href="#heading-two-ways-to-build-your-marketing-landing-page">Two Ways to Build Your Marketing Landing Page</a></p>
</li>
<li><p><a href="#heading-option-1-build-using-the-cli">Option 1: Build Using the CLI</a></p>
</li>
<li><p><a href="#heading-option-2-build-using-the-mcp-server">Option 2: Build Using the MCP Server</a></p>
</li>
<li><p><a href="#heading-how-to-optimize-your-landing-page">How to Optimize Your Landing Page</a></p>
</li>
<li><p><a href="#heading-how-to-expand-your-marketing-website">How to Expand Your Marketing Website</a></p>
</li>
<li><p><a href="#heading-live-preview">Live Preview:</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>Before you start, make sure you have:</p>
<ul>
<li><p>Node.js 18 or higher installed</p>
</li>
<li><p>shadcn/ui initialized in your project (<code>npx shadcn@latest init</code>)</p>
</li>
<li><p>Basic knowledge of React and TypeScript</p>
</li>
</ul>
<p>If you haven't initialized shadcn/ui yet, run <code>npx shadcn@latest init</code> in your project root and follow the prompts before continuing.</p>
<h2 id="heading-what-are-we-building"><strong>What Are We Building?</strong></h2>
<p>In this tutorial, we'll build a modern marketing landing page using production-ready blocks from <a href="https://shadcnspace.com/">Shadcn Space</a>. Instead of designing and developing every section from scratch, we'll assemble a complete landing page using reusable shadcn/ui blocks and customize them to fit our brand and product.</p>
<p>You can use the same approach to create landing pages for SaaS products, AI tools, startups, agencies, developer tools, portfolios, and many other types of websites. Since every block is built with React, Tailwind CSS, and shadcn/ui, you have full control over the code and can easily modify the content, layout, and styling.</p>
<h3 id="heading-why-build-with-shadcn-space">Why Build with Shadcn Space?</h3>
<p>Creating a professional marketing website typically involves designing multiple sections that work together to tell your product's story and guide visitors to take action.</p>
<p>But instead of building every section from scratch, you can start with production-ready blocks that are easy to customize.</p>
<p>This lets you build marketing websites faster with reusable blocks (and you can also mix and match blocks to create unique page layouts). It also gives you full ownership of your clean React and Tailwind CSS code. And overall, you save development time without sacrificing flexibility.</p>
<h3 id="heading-sections-well-build">Sections We'll Build</h3>
<p>We'll build our marketing landing page using the following sections:</p>
<ul>
<li><p>Hero section with a compelling headline, call-to-action, and trusted-by logos.</p>
</li>
<li><p>Features section to highlight your product's key capabilities.</p>
</li>
<li><p>Product Showcase &amp; Benefits section to demonstrate your product and communicate its value.</p>
</li>
<li><p>Testimonials section to build credibility with customer feedback.</p>
</li>
<li><p>Pricing section to present your plans clearly.</p>
</li>
<li><p>FAQ section answers common questions and reduces friction.</p>
</li>
<li><p>Call-to-Action section to encourage visitors to get started.</p>
</li>
<li><p>Footer with navigation and important links.</p>
</li>
</ul>
<h3 id="heading-final-page-structure">Final Page Structure</h3>
<p>Our landing page will have this structure:</p>
<pre><code class="language-javascript">&lt;main&gt;
  {/* 1. Hero section + Trusted by / Logo cloud */}
  &lt;AgencyHeroSection /&gt;

  {/* 2. Features section */}
  &lt;Feature01 /&gt;

  {/* 3. Product showcase &amp; Benefits */}
  &lt;AboutAndStats01 /&gt;

  {/* 4. Testimonials */}
  &lt;Testimonials /&gt;

  {/* 5. Pricing section */}
  &lt;Pricing /&gt;

  {/* 6. FAQ section */}
  &lt;Faq /&gt;

  {/* 7. Call-to-action section */}
  &lt;CTA /&gt;

  {/* Footer */}
  &lt;Footer /&gt;
&lt;/main&gt;
</code></pre>
<p>Each section will be installed from the Shadcn Space registry and customized directly inside our project. By the end of this tutorial, you'll have a fully responsive marketing landing page built with Next.js, Tailwind CSS, and shadcn/ui that you can adapt for your own product or business.</p>
<h2 id="heading-project-setup-with-base-ui-using-the-shadcn-preset"><strong>Project Setup with Base UI Using the Shadcn Preset</strong></h2>
<p>Since Shadcn Space blocks are built using Base UI primitives, we'll create our project using the Base UI preset instead of the default Radix setup.</p>
<p>This ensures that our landing page uses the same foundation as the blocks we're going to install.</p>
<h3 id="heading-1-create-the-project-with-base-ui">1. Create the Project with Base UI</h3>
<p>Run the following command:</p>
<pre><code class="language-javascript">pnpm dlx shadcn@latest init --preset b0 --template next
</code></pre>
<p>This command does a few important things:</p>
<ul>
<li><p>Creates a Next.js project</p>
</li>
<li><p>Configures Tailwind CSS</p>
</li>
<li><p>Sets up Base UI as the component foundation</p>
</li>
<li><p>Uses the Nova style preset</p>
</li>
<li><p>Configures Lucide icons</p>
</li>
<li><p>Uses Inter font</p>
</li>
<li><p>Applies neutral theme tokens</p>
</li>
</ul>
<p>You now have a Base UI-powered Next.js project ready for building your landing page.</p>
<h3 id="heading-2-add-the-shadcn-space-registry">2. Add the Shadcn Space Registry</h3>
<p>Open your <code>components.json</code> and add the following registry configuration:</p>
<pre><code class="language-javascript">{
  "registries": {
    "@shadcn-space": {
      "url": "https://shadcnspace.com/r/{name}.json",
    }
  }
}
</code></pre>
<p>This tells the CLI where to fetch components and blocks from the registry.</p>
<p>For more information about how to use it in your project, <a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli">check out the docs</a>.</p>
<h2 id="heading-two-ways-to-build-your-marketing-landing-page"><strong>Two Ways to Build Your Marketing Landing Page</strong></h2>
<p>Now that your project is set up with shadcn/ui, it's time to start building the marketing landing page.</p>
<p>You can install production-ready blocks directly into your project in two different ways:</p>
<ul>
<li><p>Using the CLI</p>
</li>
<li><p>Using the MCP Server inside your AI-powered editor</p>
</li>
</ul>
<p>Both approaches install the actual React and Tailwind CSS source code into your project, giving you complete control over customization. The only difference is how you discover and install the blocks.</p>
<h2 id="heading-option-1-build-using-the-cli">Option 1: Build Using the CLI</h2>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/n6dvjVxy02U" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<p>The CLI is the fastest way to browse the registry and install individual blocks into your project. It gives you full control over which sections you want to use and how you customize them.</p>
<h3 id="heading-step-1-browse-marketing-blocks">Step 1: Browse Marketing Blocks</h3>
<p>Visit the block registry and explore the available marketing blocks.</p>
<p>Let's review the sections we'll use for this tutorial:</p>
<ul>
<li><p>Hero section with a call-to-action and trusted-by logos</p>
</li>
<li><p>Features section</p>
</li>
<li><p>Product showcase &amp; benefits section</p>
</li>
<li><p>Testimonials section</p>
</li>
<li><p>Pricing section</p>
</li>
<li><p>FAQ section</p>
</li>
<li><p>Call-to-action section</p>
</li>
<li><p>Footer</p>
</li>
</ul>
<p>Choose the blocks that best match the design and style of your website. Since every block is fully customizable, you can easily update the content, colors, spacing, and layout to match your brand.</p>
<p>In the following sections, we'll install each block and customize them.</p>
<h3 id="heading-step-2-install-selected-blocks">Step 2: Install Selected Blocks</h3>
<p>Once you find a block you like, install it using the CLI:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/{block-name}
</code></pre>
<p>Each command downloads the block, places it inside <code>components/shadcn-space/blocks</code>, and installs the required dependencies.</p>
<p>Now your folder might look like this:</p>
<pre><code class="language-javascript">components/
  shadcn-space/
    blocks/
      about-us-section-01/
      hero-01/
      features-01/
      pricing-01/
      testimonial-01/
      faq-01/
      cta-01/
      footer-01/
</code></pre>
<p><strong>Note:</strong> I've used the first block from each section in this tutorial. You can choose any other <a href="https://shadcnspace.com/blocks"><strong>shadcn block</strong></a> that suits best according to your needs.</p>
<h3 id="heading-step-3-add-a-hero-section">Step 3: Add a Hero Section</h3>
<p>Every great marketing landing page starts with a strong hero section. It's the first thing visitors see, so it should clearly communicate what your product does, who it's for, and encourage users to take action.</p>
<p>Instead of building the section from scratch, we'll install a production-ready Hero block and customize it to match our landing page.</p>
<h4 id="heading-1-install-the-hero-block">1. Install the Hero Block</h4>
<p>Run the following CLI command to add the Hero block to your project:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/hero-01
</code></pre>
<p>The CLI will automatically download the Hero block source code, add the component to your project, and install any required dependencies.</p>
<p>After the installation completes, you should see the following directory structure:</p>
<pre><code class="language-javascript">components/
  shadcn-space/
    blocks/
      hero-01/
        index.tsx
</code></pre>
<p>You can now open the component and customize the heading, description, call-to-action buttons, images, and other content to match your product and branding.</p>
<p><strong>2. Understand the Hero Block Structure</strong></p>
<p>Once the installation is complete, open the Hero block located at:</p>
<pre><code class="language-javascript">components/shadcn-space/blocks/hero-01/index.tsx
</code></pre>
<p>You'll see a component similar to the following:</p>
<pre><code class="language-javascript">import HeroSection from "@/components/shadcn-space/blocks/hero-01/hero";
import type { NavigationSection } from "@/components/shadcn-space/blocks/hero-01/header";
import Header from "@/components/shadcn-space/blocks/hero-01/header";
import BrandSlider, { BrandList } from "@/components/shadcn-space/blocks/hero-01/brand-slider";
import type { AvatarList } from "@/components/shadcn-space/blocks/hero-01/hero";

export default function AgencyHeroSection() {
  const avatarList: AvatarList[] = [...];
  const navigationData: NavigationSection[] = [...];
  const brandList: BrandList[] = [...];

  return (
    &lt;div className="relative"&gt;
      &lt;Header navigationData={navigationData} /&gt;
      &lt;main&gt;
        &lt;HeroSection avatarList={avatarList} /&gt;
        &lt;BrandSlider brandList={brandList} /&gt;
      &lt;/main&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p><strong>What Should You Notice?</strong></p>
<p>Before making any changes, take a moment to understand how the block is organized. Rather than being a single large component, it's composed of smaller, reusable components that work together.</p>
<p>In this example:</p>
<ul>
<li><p>The <code>Header</code> component renders the navigation and receives its menu items through the <code>navigationData</code> array.</p>
</li>
<li><p>The <code>HeroSection</code> component contains the main headline, description, call-to-action buttons, and social proof, while the <code>avatarList</code> provides the data displayed in the hero.</p>
</li>
<li><p>The <code>BrandSlider</code> component displays the company logos using the <code>brandList</code> array.</p>
</li>
</ul>
<p>This separation keeps the code modular and makes each part of the landing page easier to customize or replace independently.</p>
<p><strong>Why This Matters?</strong></p>
<p>Because the block is copied directly into your project, you're working with standard React components instead of a compiled package. Every file is fully editable, allowing you to understand how the section is built and modify it to suit your own requirements.</p>
<p>For example, you can update the navigation links, replace the placeholder content with your own branding, add additional sections, integrate custom functionality, or adjust the styling using Tailwind CSS classes. Since everything lives inside your codebase, you're free to restructure the component however you like without being locked into predefined APIs or abstractions.</p>
<h4 id="heading-3-render-the-hero-section">3. Render the Hero Section</h4>
<p>Now that the Hero block has been installed, it's time to display it on the page.</p>
<p>Import the component into your <code>app/page.tsx</code> file:</p>
<pre><code class="language-javascript">import AgencyHeroSection from "@/components/shadcn-space/blocks/hero-01";

export default function Page() {
  return (
    &lt;AgencyHeroSection /&gt;
  );
}
</code></pre>
<p>Save the file and start your development server if it isn't already running. When you open the application in your browser, you'll see the Hero section rendered as the first part of your marketing landing page.</p>
<p>With the Hero section in place, we've completed the first building block of our landing page. Next, we'll continue by adding the remaining sections to create a complete marketing website.</p>
<h3 id="heading-install-the-remaining-blocks">Install the Remaining Blocks</h3>
<p>Now that you've added and rendered the Hero section, let's install the remaining blocks required for our marketing landing page.</p>
<p>Run the following command to install all the remaining sections at once:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/feature-01 @shadcn-space/about-us-section-01 @shadcn-space/testimonial-01 @shadcn-space/pricing-01 @shadcn-space/faq-01 @shadcn-space/cta-01  @shadcn-space/footer-01
</code></pre>
<p><strong>Note:</strong> If you're using Windows Command Prompt or PowerShell, run the command on a single line instead of using <code>\</code> for line continuation.</p>
<p>After the installation is complete, update your <code>app/page.tsx</code> by importing the newly added blocks and rendering them in the following order:</p>
<pre><code class="language-javascript">import AgencyHeroSection from "@/components/shadcn-space/blocks/hero-01";
import AboutAndStats01 from "@/components/shadcn-space/blocks/about-us-01";
import Feature01 from "@/components/shadcn-space/blocks/feature-01";
import Pricing from "@/components/shadcn-space/blocks/pricing-01/pricing";
import Testimonials from "@/components/shadcn-space/blocks/testimonial-01/testimonial";
import Faq from "@/components/shadcn-space/blocks/faq-01/faq";
import CTA from "@/components/shadcn-space/blocks/cta-01/cta";
import Footer from "@/components/shadcn-space/blocks/footer-01/footer";


export const metadata = {
  title: "Acme Agency – Innovative Digital Solutions",
  description:
    "We craft immersive digital experiences for bold brands. Explore our services, pricing, and success stories.",
};


export default function Page() {
  return (
    &lt;main&gt;
      {/* 1. Hero section + Trusted by / logo cloud */}
      &lt;AgencyHeroSection /&gt;


      {/* 2. Features section */}
      &lt;Feature01 /&gt;


      {/* 3. Product showcase &amp; Benefits section (Using About/Stats as a placeholder for these) */}
      &lt;AboutAndStats01 /&gt;


      {/* 4. Testimonials */}
      &lt;Testimonials /&gt;


      {/* 5. Pricing section */}
      &lt;Pricing /&gt;


      {/* 6. FAQ section */}
      &lt;Faq /&gt;


      {/* 7. Call-to-action section */}
      &lt;CTA /&gt;


      {/* Footer */}
      &lt;Footer /&gt;
    &lt;/main&gt;
  );
}
</code></pre>
<p>That's it! Your complete marketing landing page is now assembled. You can start customizing the content, images, colors, and layout of each section to match your product and brand.</p>
<h3 id="heading-customize-the-marketing-landing-page"><strong>Customize the Marketing Landing Page</strong></h3>
<p>At this point, the overall structure of your marketing landing page is complete. The next step is to personalize each section so it reflects your product, brand, and messaging instead of the default placeholder content.</p>
<p>One of the biggest advantages of working with reusable React components is that every section can be customized independently. You can update the content, replace images, adjust layouts, and refine the styling without rebuilding the page from scratch.</p>
<p>The following examples show how the default blocks can be transformed into a polished marketing website.</p>
<p><strong>Customize the Hero Section</strong></p>
<p>The Hero section is the first thing visitors see, so it's the most important place to communicate your product's value. Replace the placeholder headline, supporting text, call-to-action buttons, and trusted brand logos with content that represents your own business.</p>
<p><strong>Before:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/f29da0e0-0da0-482e-bd29-35f3c3c696db.png" alt="Customize the Hero Section Before Using MCP" style="display:block;margin:0 auto" width="1919" height="850" loading="lazy">

<p><strong>After:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/1ec0dad0-0dee-448f-aed2-136e310bbc5b.png" alt="Customize the Hero Section After Using MCP" style="display:block;margin:0 auto" width="1906" height="865" loading="lazy">

<p>Notice how the customized version immediately establishes the product's identity through updated messaging, branding, imagery, and call-to-action buttons.</p>
<p><strong>Customize the Features Section</strong></p>
<p>The Features section should explain what your product offers and why it stands out. Replace the sample feature cards with capabilities that highlight your product's most valuable functionality.</p>
<p><strong>Before:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/b44dfc47-8603-4a1c-847e-b608b84c7723.png" alt="Customize the Features Section Before Using MCP" style="display:block;margin:0 auto" width="1474" height="866" loading="lazy">

<p><strong>After:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/dbc1d529-88ac-42f2-a5d1-0c0348165944.png" alt="Customize the Features Section After Using MCP" style="display:block;margin:0 auto" width="1525" height="936" loading="lazy">

<p>Updating the feature titles, descriptions, and icons makes the section more relevant to your audience while reinforcing your product's key selling points.</p>
<p><strong>Customize the Pricing Section</strong></p>
<p>Your pricing section should clearly communicate the plans you offer and help visitors choose the option that best fits their needs. Replace the default plans, pricing, feature lists, and button labels with information that matches your business model.</p>
<p><strong>Before:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/7e053524-b281-4ee4-9afe-f0ee1ad76d48.png" alt="Customize the Pricing Section Before Using MCP" style="display:block;margin:0 auto" width="1631" height="722" loading="lazy">

<p><strong>After:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/5ae201a8-46a9-4ca4-909c-7e1ecc5c4077.png" alt="Customize the Pricing Section After Using MCP" style="display:block;margin:0 auto" width="1631" height="817" loading="lazy">

<p>A customized pricing section builds trust by presenting accurate information while making it easier for potential customers to compare plans.</p>
<p><strong>Customize the FAQ Section</strong></p>
<p>The FAQ section is a great opportunity to answer common questions before visitors contact your team. Replace the placeholder questions with answers related to your product, pricing, integrations, support, or onboarding process.</p>
<p><strong>Before:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/4b161671-61f0-45c0-8266-3f39015958ec.png" alt="Customize the FAQ Section Before Using MCP" style="display:block;margin:0 auto" width="1650" height="883" loading="lazy">

<p><strong>After:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/8f68f5a8-ad09-4021-843a-58329d0a08ce.png" alt="Customize the FAQ Section After Using MCP" style="display:block;margin:0 auto" width="1638" height="785" loading="lazy">

<p>Tailoring the FAQ to your product helps reduce uncertainty, improves the user experience, and can answer many questions before a customer reaches out.</p>
<p><strong>The Result</strong></p>
<p>With just a few content updates, the default blocks evolve into a professional marketing landing page tailored to your brand. Since every section is built with reusable React components, you can continue refining the design, adjusting layouts, and adding new content as your product grows without changing the overall page structure.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/3759ae7a-d244-487d-8354-d9dd42570bfd.gif" alt="Full Preview of the Landing Page" style="display:block;margin:0 auto" width="1909" height="840" loading="lazy">

<h2 id="heading-option-2-build-using-the-mcp-server"><strong>Option 2: Build Using the MCP Server</strong></h2>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/mMlxAmJlbMI" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<p>If you prefer a faster workflow, you can use the MCP Server to generate your landing page directly inside your editor. Instead of manually browsing and installing individual blocks, you simply describe the page you want to build, and the MCP Server assembles an initial version for you.</p>
<p>The MCP Server works with supported editors like Antigravity, VS Code, Cursor, Windsurf, and other MCP-compatible editors.</p>
<h3 id="heading-step-1-install-the-mcp-server">Step 1: Install the MCP Server</h3>
<p><strong>Quick Installation</strong>:</p>
<p>The fastest way to get started. Choose your package manager and run the command corresponding to your client:</p>
<p><strong>For Claude Code</strong>:</p>
<pre><code class="language-javascript">claude mcp add shadcnspace-mcp -- npx -y shadcnspace-mcp@latest
</code></pre>
<p><strong>For Others</strong>:</p>
<pre><code class="language-javascript">npx shadcnspace-cli install &lt;client&gt;
</code></pre>
<p>Replace <code>&lt;client&gt;</code> with <strong>cursor, antigravity, vscode,</strong> or <strong>windsurf</strong>.</p>
<p><strong>Manual Installation For VS Code:</strong></p>
<pre><code class="language-javascript">{
  "servers": {
    "shadcnspace-mcp": {
      "command": "npx",
      "args": ["-y", "shadcnspace-mcp@latest"]
    }
  }
}
</code></pre>
<ul>
<li><p>Open .vscode/mcp.json.</p>
</li>
<li><p>Click Start next to the Shadcn Space MCP server</p>
</li>
</ul>
<p>For a detailed guide, follow the <a href="https://shadcnspace.com/docs/getting-started/mcp-server-docs"><strong>MCP Server documentation</strong></a> to install it for your preferred editor.</p>
<p>Once the installation is complete, restart your editor to enable the MCP connection.</p>
<h3 id="heading-step-2-open-the-ai-chat">Step 2: Open the AI Chat</h3>
<p>Open the AI chat panel inside your editor and describe the landing page you'd like to create.</p>
<p>For example:</p>
<pre><code class="language-javascript">Create a modern marketing landing page for an AI SaaS product.

Use Shadcn Space blocks and include:

- Hero section
- Features section
- Product showcase &amp; benefits
- Testimonials
- Pricing
- FAQ
- Call-to-action
- Footer

Use a clean, modern, and responsive design.
</code></pre>
<p>Feel free to replace the product description with your own and customize as you like.</p>
<h3 id="heading-step-3-generate-the-landing-page">Step 3: Generate the Landing Page</h3>
<p>After receiving your prompt, the MCP Server analyzes your requirements and selects the most suitable blocks for your landing page. It automatically assembles the page using a combination of reusable sections, giving you a working layout in just a few moments.</p>
<p>The generated sections are added directly to your project as standard React components. Since everything is real source code, you can customize every part of the page: update the content, replace images, modify the layout, adjust spacing, or restyle components using Tailwind CSS.</p>
<p>The MCP Server simply speeds up the initial setup, while giving you complete control over the final implementation.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/2247726d-12ab-47d8-bd7a-49b025ea0eb5.gif" alt="How to Generate the Landing Page" style="display:block;margin:0 auto" width="1909" height="913" loading="lazy">

<h2 id="heading-how-to-optimize-your-landing-page">How to Optimize Your Landing Page</h2>
<p>Building a visually appealing landing page is only the first step. Before publishing your website, it's worth spending some time optimizing it for performance, search engines, and user experience.</p>
<p>A fast, responsive landing page not only feels more polished but also helps improve engagement and conversion rates. Fortunately, Next.js provides several built-in features that make these optimizations straightforward.</p>
<h3 id="heading-optimize-images-with-nextjs">Optimize Images with Next.js</h3>
<p>Images are often the largest assets on a marketing website, so optimizing them can significantly improve loading performance. If you're using Next.js, prefer the built-in <code>next/image</code> component instead of the standard HTML image tag. It automatically serves appropriately sized images, supports modern image formats, and reduces layout shifts as the page loads.</p>
<p>Before adding screenshots or illustrations to your project, make sure they're compressed and sized appropriately. Well-optimized images create a smoother browsing experience across desktop and mobile devices while also contributing to better Core Web Vitals.</p>
<h3 id="heading-improve-seo-for-your-landing-page">Improve SEO for Your Landing Page</h3>
<p>A well-designed landing page is only effective if people can discover it. Search engine optimization starts with creating meaningful content that clearly communicates what your product offers. Choose a descriptive page title, write a concise meta description, and organize your content using logical headings.</p>
<p>Your primary keyword should appear naturally throughout the page without forcing it into every paragraph. Focus on writing for your audience first, then structure the content in a way that search engines can easily understand. Combining valuable content with a clear page hierarchy gives your landing page the best chance of ranking for relevant searches.</p>
<h3 id="heading-add-metadata-and-open-graph-images">Add Metadata and Open Graph Images</h3>
<p>When someone shares your landing page on social media or in a messaging application, the preview is generated from your page's metadata. Configuring Open Graph and Twitter metadata allows you to control the title, description, and preview image that appear when your website is shared.</p>
<p>A custom preview image that reflects your branding makes your links look more professional and can encourage more people to click through. Taking a few minutes to configure these settings helps create a more polished experience whenever your content is shared online.</p>
<h3 id="heading-optimize-performance">Optimize Performance</h3>
<p>Performance plays a major role in how visitors perceive your website. A page that loads quickly feels more responsive and encourages users to continue exploring your content. As you customize your landing page, keep unnecessary JavaScript to a minimum, optimize static assets, and avoid loading resources that aren't immediately needed.</p>
<p>Even small improvements, such as reducing image sizes or simplifying animations, can noticeably improve loading speed. Before deploying your project, test the page under different network conditions to ensure it performs well for all visitors.</p>
<h3 id="heading-improve-core-web-vitals">Improve Core Web Vitals</h3>
<p>Core Web Vitals are Google's metrics for measuring real-world user experience. They evaluate how quickly your main content appears, how responsive the page feels during interactions, and whether elements remain stable as the page loads. Monitoring these metrics throughout development helps identify potential issues before they affect users.</p>
<p>Tools such as Lighthouse and PageSpeed Insights provide detailed reports that can help you improve loading performance and responsiveness. A landing page with strong Core Web Vitals not only creates a better experience for visitors but can also contribute to improved search rankings.</p>
<hr>
<h2 id="heading-how-to-expand-your-marketing-website"><strong>How to Expand Your Marketing Website</strong></h2>
<p>A landing page is often just the beginning of a complete marketing website. As your product grows, you'll likely need additional pages that provide more information, improve navigation, and create a better experience for your visitors. Common additions include Blog, Blog Details, Pricing, FAQ, Changelog, Contact, Integration, Error, and About Us pages.</p>
<p>Building these pages with a consistent design system helps maintain a unified look and feel across your entire website while reducing development time. Reusing layouts and components also makes your project easier to maintain as it evolves.</p>
<p>If you're looking to expand your website beyond a single landing page, explore the collection of production-ready <a href="https://shadcnspace.com/pages"><strong>Shadcn website pages</strong></a>, built with React, Next.js, Tailwind CSS, and shadcn/ui.</p>
<h2 id="heading-live-preview"><strong>Live Preview:</strong></h2>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/a189f09d-550d-4c7f-b20a-477f5bc1bfa0.gif" alt="How to Expand Your Marketing Website" style="display:block;margin:0 auto" width="1909" height="913" loading="lazy">

<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>In this tutorial, we built a complete marketing landing page using Next.js, shadcn/ui, Tailwind CSS, and reusable Shadcn Space blocks. Starting from a fresh project, we assembled a production-ready page by combining sections such as the Hero, Features, Product Showcase, Testimonials, Pricing, FAQ, Call-to-Action, and Footer.</p>
<p>Because every block is added as standard React source code, you're free to customize the content, layout, styling, and functionality to match your own product and branding. Whether you're building a SaaS application, a startup website, an AI product, an agency site, or a developer tool, the same approach can be adapted to your requirements.</p>
<p>A marketing landing page is just the foundation of your online presence. As your product grows, you can continue expanding your website with additional marketing pages while maintaining a consistent design system and development workflow.</p>
<p>I hope this guide has helped you understand how to quickly build a modern, responsive marketing landing page using reusable components. Feel free to experiment with different block combinations, personalize the design, and create a website that best represents your product.</p>
<p><strong>Appreciation</strong>: I wrote this article with the help of Mihir Koshti (Sr. Full Stack Developer) – <a href="https://www.linkedin.com/in/mihir-koshti/">Connect on LinkedIn</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build Your Own MCP Server and Publish Your ChatGPT App with Supabase Auth and DigitalOcean ]]>
                </title>
                <description>
                    <![CDATA[ A new type of app is emerging with the development of LLMs and AI-native apps. It lives inside an AI chat (like ChatGPT) rather than being a fully native web or mobile app. In this tutorial, you'll le ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-your-own-mcp-server-and-publish-your-chatgpt-app/</link>
                <guid isPermaLink="false">6a4fc672a2e4b5543646e329</guid>
                
                    <category>
                        <![CDATA[ mcp server ]]>
                    </category>
                
                    <category>
                        <![CDATA[ llm ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ai agents ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Abdurrahman Rajab ]]>
                </dc:creator>
                <pubDate>Thu, 09 Jul 2026 16:04:02 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/f7fdd4f8-d0c0-44ee-aaf5-f3277522e32c.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A new type of app is emerging with the development of LLMs and AI-native apps. It lives inside an AI chat (like ChatGPT) rather than being a fully native web or mobile app.</p>
<p>In this tutorial, you'll learn how to build an MCP (Model Context Protocol) server from scratch, including a UI you can use as a ChatGPT app with authentication and a database.</p>
<p>You'll go through the process of building, testing, adding the ChatGPT app as a connector, and submitting it to publish to the app directory. This will let you build the app on three levels:</p>
<ul>
<li><p>Level one: you will build your basic MCP Server that returns textual data.</p>
</li>
<li><p>Level two: you will build a UI for your MCP Server to be used within an LLM UI.</p>
</li>
<li><p>Level three: you will add authentication and a database to your MCP Server.</p>
</li>
</ul>
<p>To fully understand this article, you'll need to have basic knowledge of:</p>
<ul>
<li><p>Web development</p>
</li>
<li><p>JavaScript</p>
</li>
<li><p>React and React Native</p>
</li>
<li><p>SQL and databases</p>
</li>
</ul>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-what-is-an-mcp-server">What is an MCP Server?</a></p>
<ul>
<li><a href="#heading-what-can-you-do-with-an-mcp-server">What Can You Do with an MCP Server?</a></li>
</ul>
</li>
<li><p><a href="#heading-level-1-how-to-build-your-own-mcp-server">Level 1: How to Build Your Own MCP Server</a></p>
<ul>
<li><p><a href="#heading-step-0-prepare-your-project">Step 0: Prepare your project</a></p>
</li>
<li><p><a href="#heading-step-1-create-a-nodejs-server">Step 1: Create a Node.js Server</a></p>
</li>
<li><p><a href="#heading-step-2-setting-up-mcp-server-sdk">Step 2: Setting Up MCP Server SDK</a></p>
</li>
<li><p><a href="#heading-step-3-add-mcp-server-tools-create-and-add-a-todo">Step 3: Add MCP Server Tools – Create and Add a Todo</a></p>
</li>
<li><p><a href="#heading-step-4-list-todos-from-mcp-server">Step 4: List Todos from MCP Server</a></p>
</li>
<li><p><a href="#heading-step-5-add-todo-complete-functions">Step 5: Add Todo Complete Functions</a></p>
</li>
<li><p><a href="#heading-step-6-connect-your-mcp-server-with-the-nodejs-server">Step 6: Connect Your MCP Server with the Node.js Server</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-test-your-mcp-server">How to Test Your MCP Server</a></p>
</li>
<li><p><a href="#heading-level-2-how-to-build-the-ui">Level 2: How to Build the UI</a></p>
<ul>
<li><p><a href="#heading-step-1-create-the-html-file-to-show-the-ui">Step 1: Create the HTML File to Show the UI</a></p>
</li>
<li><p><a href="#heading-step-2-add-a-javascript-module-to-handle-mcp-server-data">Step 2: Add a JavaScript Module to Handle MCP Server Data</a></p>
</li>
<li><p><a href="#heading-step-3-styling-your-ui">Step 3: Styling your UI</a></p>
</li>
<li><p><a href="#heading-step-4-add-the-ui-to-your-mcp-server">Step 4: Add the UI to your MCP Server</a></p>
</li>
<li><p><a href="#heading-step-5-update-your-mcp-server-to-handle-the-ui">Step 5: Update Your MCP Server to Handle the UI</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-test-your-chatgpt-app">How to Test Your ChatGPT App</a></p>
</li>
<li><p><a href="#heading-level-3-how-to-add-supabase-auth-and-database-to-the-mcp-server">Level 3: How to Add Supabase (Auth and Database) to the MCP Server</a></p>
<ul>
<li><p><a href="#heading-step-1-create-the-todos-table">Step 1: Create the Todos Table</a></p>
</li>
<li><p><a href="#heading-step-2-enabling-the-mcp-server-to-connect-with-supabase-auth">Step 2: Enabling the MCP Server to Connect with Supabase Auth</a></p>
</li>
<li><p><a href="#heading-step-3-create-a-proxy-server-for-the-mcp-server-to-handle-the-auth">Step 3: Create a Proxy Server for the MCP Server to Handle the Auth</a></p>
</li>
<li><p><a href="#heading-step-4-implementing-the-consent-and-login-page">Step 4: Implementing the Consent and Login Page</a></p>
</li>
<li><p><a href="#heading-step-5-testing-the-oauth-implementation-with-mcp-server-inspector">Step 5: Testing the OAuth Implementation with MCP Server Inspector</a></p>
</li>
<li><p><a href="#heading-step-6-adding-oauth-security-to-your-mcp-server-tools">Step 6: Adding OAuth Security to Your MCP Server Tools</a></p>
</li>
<li><p><a href="#heading-step-7-updating-the-mcp-server-function-to-handle-the-authentication">Step 7: Updating the MCP Server Function to Handle the Authentication</a></p>
</li>
<li><p><a href="#heading-step-8-testing-the-server-with-supabase">Step 8: Testing the Server with Supabase</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-deploy-your-mcp-server-to-digitalocean">How to Deploy Your MCP Server to DigitalOcean</a></p>
</li>
<li><p><a href="#heading-how-to-publish-your-chatgpt-app">How to Publish Your ChatGPT App</a></p>
</li>
<li><p><a href="#heading-what-to-do-next">What to Do Next</a></p>
</li>
<li><p><a href="#heading-acknowledgments">Acknowledgments</a></p>
</li>
<li><p><a href="#heading-references">References</a></p>
</li>
</ul>
<h2 id="heading-what-is-an-mcp-server">What is an MCP Server?</h2>
<p>A <a href="https://www.freecodecamp.org/news/how-the-model-context-protocol-works/">Model Context Protocol</a> (MCP) server is a program that exposes tools, resources, and prompts to an AI application through a standard protocol. An MCP server can provide read-only context, callable tools, or reusable prompt templates that help extend what an AI application can do.</p>
<p>A developer builds or configures the MCP server, and an MCP client inside a host application connects to it. The application can then allow the model to discover available capabilities and, when appropriate, invoke tools or fetch resources via the MCP protocol to help complete a task.</p>
<h3 id="heading-what-can-you-do-with-an-mcp-server">What Can You Do with an MCP Server?</h3>
<p>An MCP server lets an AI application work with information and systems outside the model itself. For example, it can help the model look up current information, save and retrieve user data, search documents, or trigger actions in another application.</p>
<p>In practice, one MCP server might connect to an online database, while another might work with files on your local machine. This makes it possible to build AI workflows that are more useful, practical, and connected to real tools.</p>
<h2 id="heading-level-1-how-to-build-your-own-mcp-server">Level 1: How to Build Your Own MCP Server</h2>
<p>In this tutorial, you'll learn how to build an MCP server using the default HTTP server from Node.js, Supabase for the database and authentication, and the official MCP server SDK. Then you'll deploy it to DigitalOcean and publish your app on ChatGPT.</p>
<p>That means you'll do two steps here:</p>
<ul>
<li><p>First step: connect your deployed MCP server to ChatGPT as an app/connector so it can be used within ChatGPT.</p>
</li>
<li><p>Second step: submit the app for review and, if approved, publish it to the ChatGPT app directory.</p>
</li>
</ul>
<p>The MCP server SDK isn't the only tool or framework for building your own MCP server. You can use other SDKs and tools for that if you prefer. But to simplify the first steps, here I've decided to use the more straightforward tools.</p>
<h3 id="heading-step-0-prepare-your-project">Step 0: Prepare your project</h3>
<p>You're going to write a full project here, so you should start by creating packages and initializing the project. To do this, follow these steps:</p>
<ul>
<li><p>Create a new folder with the project name. For this example, you can use <code>mcp_todo</code>.</p>
</li>
<li><p>Navigate to this new folder.</p>
</li>
<li><p>Open the terminal in this folder.</p>
</li>
<li><p>Initialize the npm project with <code>npm init --init-type=module -y</code> to create a JavaScript package file and add the packages to the project with ES6 support.</p>
</li>
<li><p>Initialize Git with <code>git init</code> in the project to enable version control and track changes.</p>
</li>
<li><p>Install related packages that you're going to use in your project:</p>
<ul>
<li><p>The packages are Supabase, the MCP SDK (which we'll cover in step 2), and the zod validation package for validating LLM inputs and data.</p>
<pre><code class="language-shell">npm install @modelcontextprotocol/sdk zod @supabase/supabase-js
</code></pre>
</li>
</ul>
</li>
<li><p>Create a <code>.gitignore</code> file and add the <code>node_modules</code> to it so that it won't be tracked by Git.</p>
</li>
<li><p>Add the current state of the project to your Git tracker by writing the following:</p>
<ul>
<li><p><code>git add .</code></p>
</li>
<li><p><code>git commit -m "init project"</code></p>
</li>
</ul>
</li>
</ul>
<p>With this, you've created a new project for yourself that you can use as a starting point for managing and following the project.</p>
<h3 id="heading-step-1-create-a-nodejs-server">Step 1: Create a Node.js Server</h3>
<p>To start the project, you'll need to create a simple Node.js server, which you can do by creating a new file named <code>server.js</code> and writing the following code:</p>
<pre><code class="language-javascript">import { createServer } from "node:http";

const port = Number(process.env.PORT ?? 8787);

const httpServer = createServer(async (req, res) =&gt; {

    console.log(`${req.method} ${req.url}`);

    if (!req.url) {

        res.writeHead(400).end("Missing URL");

        return;

    }

    const url = new URL(req.url, `http://${req.headers.host ?? "localhost"}`);

    res.writeHead(404).end("Not Found");

});

httpServer.listen(port, () =&gt; {
    console.log(`Todo MCP server listening on http://localhost:${port}, press Ctrl+C to stop`);
});
</code></pre>
<p>This is a simple Node server that you'll use as the base for building your MCP Server.</p>
<p>To build your MCP server, you'll need to set it up using the MCP Server SDK. After that, you'll need to define two things: the tools you'll show the LLM and the UI and resources the LLM will use to render.</p>
<p>To define the tools and UI concepts, you'll use the MCP Server SDK.</p>
<h3 id="heading-step-2-setting-up-mcp-server-sdk">Step 2: Setting Up MCP Server SDK</h3>
<p>To set up and start the MCP server, you need to have the following:</p>
<ul>
<li><p>Tools: The functions exposed by MCP Server to an LLM, enabling the LLM to interact with the server and external systems. Like calling an API, performing a computation, or querying a database.</p>
</li>
<li><p>Resources (optional): Data the MCP Server shares with an LLM. For example, a file, database schema, or an HTML UI to use inside the LLM Chat UI as an embedded frame.</p>
</li>
</ul>
<p>You can start the server by adding this line of code at the top of the server.js file:</p>
<pre><code class="language-javascript">import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

function createTodoServer() {
    const server = new McpServer({ name: "todo-app", version: "0.1.0" });
    return server;
}
</code></pre>
<p>Then add a tool and resources using the following function signature:</p>
<pre><code class="language-javascript">server.registerTool(
    "NAME",
    {},
    async (args, meta) =&gt; { }
);
</code></pre>
<p>You can think about the tool registrar as your endpoint to the MCP Server. The LLM will check it and, based on the name and metadata, start processing the data using the arguments and results you have in this tool.</p>
<p>Today, you're going to build three simple tools:</p>
<ul>
<li><p>Add todo</p>
</li>
<li><p>Update todo</p>
</li>
<li><p>List todos</p>
</li>
</ul>
<p>They all look a bit similar, but you'll see how to write them all to understand the concepts in the next sections.</p>
<h3 id="heading-step-3-add-mcp-server-tools-create-and-add-a-todo">Step 3: Add MCP Server Tools – Create and Add a Todo</h3>
<p>To start with, when adding todos, you'll need a simple in-memory array to manipulate. You can create the array outside the create server function to access it throughout the server.</p>
<pre><code class="language-javascript">let todos = [];// outside the createTodoServer function block
let nextId = 1; // outside the createTodoServer function block (this is a mock id for your todos)
</code></pre>
<p>After the array, you'll need to have two more supporting functions: first, the validator for the tools, which specifies the expected input types from the LLM.</p>
<p>At the top of the file, you should import the zod library:</p>
<pre><code class="language-javascript">import { z } from "zod";
</code></pre>
<p>Then you can write the helper function to validate it and tell the LLM what to expect from them:</p>
<pre><code class="language-javascript">const addTodoInputSchema = {
    title: z.string().min(1),
}; // outside the createTodoServer function block
</code></pre>
<p>Next, you'll need the return function, which you can use with other functions to have a unified return function for the tools</p>
<pre><code class="language-javascript">const replyWithTodos = (message) =&gt; ({
    content: message ? [{ type: 'text', text: message }] : [],
    structuredContent: { tasks: todos },
}); //outside the createTodoServer function block
</code></pre>
<p>Then you can register the add todo function in the server, inside the createTodoServer function block, before <code>return server</code>:</p>
<pre><code class="language-javascript">server.registerTool(
    'add_todo',
    {
        title: 'Add todo',
        description: 'Creates a todo item with the given title.',
        inputSchema: addTodoInputSchema,
        _meta: {
            'openai/toolInvocation/invoking': 'Adding todo',
            'openai/toolInvocation/invoked': 'Added todo',
        },
    },
    async (args) =&gt; {
        const title = args?.title?.trim?.() ?? '';
        if (!title) return replyWithTodos('Missing title.');
        const todo = { id: `todo-${nextId++}`, title, completed: false };
        todos = [...todos, todo];
        return replyWithTodos(`${todo.title}`);
    },
); // inside the createTodoServer function block
</code></pre>
<p>In the above code, you've added the tool name and used a simple approach to add the todos to the in-memory array you already identified. The trick here is to validate the data before adding it and create the related object for it.</p>
<p>In the metadata, you've added the title, description, inputSchema, and _meta for OpenAI to use while rendering this. You'll get a rendering, add a todo when the AI adds it, and have the latest version of the added todo when it’s finished.</p>
<p>At the same time, you've added the input schema so the LLM knows what to provide when invoking your server, and you've added a reply helper function to handle your todos. It’s a simple function that shows the todos in a structured way for LLMs to understand.</p>
<h3 id="heading-step-4-list-todos-from-mcp-server">Step 4: List Todos from MCP Server</h3>
<p>To list the todos, you can use a simple list function to show the todos without any changes. In the code below, you use the same concept for naming, metadata, and description context as you provided before. You're also using the previous helper function to return the todos that you have in memory. You should write this code inside the createTodoServer function block.</p>
<pre><code class="language-javascript">server.registerTool(
  'list_todos',
  {
    title: 'List todos',
    description: 'Lists all todo items.',
    _meta: {
      'openai/toolInvocation/invoking': 'Listing todos',
      'openai/toolInvocation/invoked': 'Listed todos',
    },
  },
  async () =&gt; {
    return replyWithTodos();
  },
);
</code></pre>
<h3 id="heading-step-5-add-todo-complete-functions">Step 5: Add Todo Complete Functions</h3>
<p>To complete and edit todos, you can create a new tool with that name that takes the todo ID and returns the updated todos. To do this, you need to add the helper function for validating the request outside the createTodoServer:</p>
<pre><code class="language-javascript">const completeTodoInputSchema = {
    id: z.string().min(1),
};
</code></pre>
<p>Then inside the createTodoServer function, you can add the following:</p>
<pre><code class="language-javascript">server.registerTool(
    'complete_todo',

    {
        title: 'Complete todo',
        description: 'Marks a todo as done by id.',
        inputSchema: completeTodoInputSchema,
        _meta: {
            'openai/toolInvocation/invoking': 'Completing todo',
            'openai/toolInvocation/invoked': 'Completed todo',
        },
    },

    async (args) =&gt; {
        const id = args?.id;
        if (!id) return replyWithTodos('Missing todo id.');
        const todo = todos.find((task) =&gt; task.id === id);
        if (!todo) {
            return replyWithTodos(`Todo ${id} was not found.`);
        }
        todos = todos.map((task) =&gt;
            task.id === id ? { ...task, completed: true } : task,
        );
        return replyWithTodos(`Completed "${todo.title}".`);
    },
);
</code></pre>
<p>In this tool, you used the same function definition as for list todos, while adding extra guards to check whether the LLM has returned the ID and whether that ID is correct. You should always manually check the data you have before processing it, since LLMs can hallucinate and aren't required to validate their inputs.</p>
<p>Now you can commit your code to Git and track it in the tree:</p>
<ul>
<li><p><code>git add .</code></p>
</li>
<li><p><code>git commit -m "feat: add MCP todo server"</code></p>
</li>
</ul>
<h3 id="heading-step-6-connect-your-mcp-server-with-the-nodejs-server">Step 6: Connect Your MCP Server with the Node.js Server</h3>
<p>Since you have written the main functions for the MCP server, you need to connect your MCP server to the Node.js HTTP server.</p>
<p>To do that, you need to write the streamable function and the related code. You will use this code on top of the server code from step 1 as a replacement, since it includes more functions to handle the MCP server.</p>
<p>First, import the StreamableHTTPServerTransport function:</p>
<pre><code class="language-javascript">import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
</code></pre>
<p>Then you can copy the next code and replace it with the server code, which has the server's structure, to use in your project.</p>
<pre><code class="language-javascript">const port = Number(process.env.PORT ?? 8787);
const MCP_PATH = '/mcp';

const httpServer = createServer(async (req, res) =&gt; {
    if (!req.url) {
        res.writeHead(400).end('Missing URL');
        return;
    }

    const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);

    // handle the options call for the endpoint
    if (req.method === 'OPTIONS' &amp;&amp; url.pathname === MCP_PATH) {
        res.writeHead(204, {
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
            'Access-Control-Allow-Headers': 'content-type, mcp-session-id',
            'Access-Control-Expose-Headers': 'Mcp-Session-Id',
        });
        res.end();
        return;
    }

    // handles normal get method for the main link
    if (req.method === 'GET' &amp;&amp; url.pathname === '/') {
        res.writeHead(200, { 'content-type': 'text/plain' }).end('Todo MCP server');
        return;
    }
    // here you are handling your MCP calls with streamable HTTP
    const MCP_METHODS = new Set(['POST', 'GET', 'DELETE']);
    if (url.pathname === MCP_PATH &amp;&amp; req.method &amp;&amp; MCP_METHODS.has(req.method)) {
        res.setHeader('Access-Control-Allow-Origin', '*');
        res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
        const server = createTodoServer();
        const transport = new StreamableHTTPServerTransport({
            sessionIdGenerator: undefined, // stateless mode
            enableJsonResponse: true,
        });
        res.on('close', () =&gt; {
            transport.close();
            server.close();
        });
        try {
            await server.connect(transport);
            await transport.handleRequest(req, res);
        } catch (error) {
            console.error('Error handling MCP request:', error);
            if (!res.headersSent) {
                res.writeHead(500).end('Internal server error');
            }
        }
        return;
    }
    res.writeHead(404).end('Not Found');
});

httpServer.listen(port, () =&gt; {
    console.log(
        `Todo MCP server listening on http://localhost:${port}${MCP_PATH}`,
    );
});
</code></pre>
<p>In this code, you're running the main HTTP server to handle the requests. The server exposes a /mcp endpoint for MCP clients and connects each request to a stateless MCP server using Streamable HTTP.</p>
<p>Now you can commit your code to Git and track it in the tree:</p>
<ul>
<li><p><code>git add .</code></p>
</li>
<li><p><code>git commit -m "feat: add MCP server functions"</code></p>
</li>
</ul>
<h2 id="heading-how-to-test-your-mcp-server">How to Test Your MCP Server</h2>
<p>Now you can test the basic structure of your MCP server by running the following code:</p>
<pre><code class="language-shell">node server.js
</code></pre>
<p>By using this command, you'll run the server you created in the previous steps. It will make it active and listen to changes at <code>http://localhost:8787/mcp</code>. After running <a href="http://server.js">server.js</a>, you need to open the inspector, a tool that helps you see the MCP server registration and the endpoints and tools you need to use and run in a secure environment.</p>
<pre><code class="language-shell">npx @modelcontextprotocol/inspector@latest --server-url http://localhost:8787/mcp --transport http
</code></pre>
<p>When you run the previous command, you can see that you have a connection to your MCP, and you need to run it and use it through the inspector UI. Using the inspector UI will help you test your MCP server without connecting it to any external services and test the inputs and outputs locally.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/ebe3380f-e95e-48dd-a3e5-9236ec72e8f1.png" alt="Showing MCP Server Inspector Too" style="display:block;margin:0 auto" width="1920" height="1080" loading="lazy">

<p>To test your tools, connect to the server first, and then you can see and explore them.</p>
<p>After writing this code, you may wonder: what UI could I show the user through an LLM? If you run your project right now, you'll only get text results as LLM chat answers. But if you build a UI, you can improve your LLM's experience. In the next section, that's what we'll tackle.</p>
<h2 id="heading-level-2-how-to-build-the-ui">Level 2: How to Build the UI</h2>
<p>With the previous code, you built a simple MCP server that adds todos to a todo list and marks them as complete from the app. Now you're going to explore the registerResource tool, which registers a UI resource of your design so ChatGPT can use it.</p>
<p>Resources are the LLM-specific data provided by your MCP Server. You can share your UI with the LLM so it can use it to display additional data and widgets in the chat.</p>
<p>To share the UI, you need to have an HTML file that relies on your MCP server data and uses the MCP server. So for that, you'll create a new HTML file.</p>
<h3 id="heading-step-1-create-the-html-file-to-show-the-ui">Step 1: Create the HTML File to Show the UI</h3>
<p>The TodoHTML you provided earlier should be an HTML file that can communicate with the Server and the ChatGPT UI. The UI will look like the following image:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/40db34a6-d29c-4304-9dd9-895252fb071b.png" alt="UI Style Inside ChatGPT" style="display:block;margin:0 auto" width="908" height="702" loading="lazy">

<p>To build such a UI you saw previously, you need to create a <code>public/todo-widget.html</code> file and write the following structured code:</p>
<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="utf-8" /&gt;
    &lt;title&gt;Todo list&lt;/title&gt;
    &lt;style&gt;&lt;/style&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;main&gt;
    &lt;/main&gt;
    &lt;script type="module"&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Then inside <code>&lt;main&gt;</code> tag, you should add the following:</p>
<pre><code class="language-html">      &lt;h2&gt;Todo list&lt;/h2&gt;
      &lt;form id="add-form" autocomplete="off"&gt;
        &lt;input id="todo-input" name="title" placeholder="Add a task" /&gt;
        &lt;button type="submit"&gt;Add&lt;/button&gt;
      &lt;/form&gt;
      &lt;ul id="todo-list"&gt;&lt;/ul&gt;
</code></pre>
<p>You can see it’s just simple HTML tags that allow you to have the header, form with an input, and an unordered list with <code>id = todo-list</code>. But the tricky part is the JavaScript module you're going to add to it.</p>
<h3 id="heading-step-2-add-a-javascript-module-to-handle-mcp-server-data">Step 2: Add a JavaScript Module to Handle MCP Server Data.</h3>
<p>To add the JavaScript module and code, you'll write all the code below inside the <code>&lt;script type="module"&gt;&lt;/script&gt;</code> tag.</p>
<p>First, you need to identify the elements by selecting the HTML tag IDs you provided to them in the HTML code:</p>
<pre><code class="language-javascript">const listEl = document.querySelector("#todo-list");
const formEl = document.querySelector("#add-form");
const inputEl = document.querySelector("#todo-input");
</code></pre>
<p>Then you can use these elements to extract data from the ChatGPT response using some special <code>windows.openai</code> code. This will allow you to receive results and responses from ChatGPT while using your MCP server.</p>
<p>For this case, you'll use the following:</p>
<ul>
<li><p><code>window.openai.callTool</code></p>
</li>
<li><p><code>window.openai?.toolOutput</code></p>
</li>
</ul>
<p><code>callTool</code> calls the tools from your MCP server by name, and <code>toolOutput</code> is the result of the tools you get from your MCP.</p>
<p>To create the first todos and show them, you can use the <code>toolOutput</code> and get the output from there to use in your UI. Here's a code example:</p>
<pre><code class="language-javascript">let tasks = [...(window.openai?.toolOutput?.tasks ?? [])];
</code></pre>
<p>You can then loop through all tasks to add them to the list element:</p>
<pre><code class="language-javascript">const render = () =&gt; {
    listEl.innerHTML = '';

    tasks.forEach((task) =&gt; {
        const li = document.createElement('li');
        li.dataset.id = task.id;
        li.dataset.completed = String(Boolean(task.completed));
        const label = document.createElement('label');
        label.style.display = 'flex';
        label.style.alignItems = 'center';
        label.style.gap = '10px';
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.checked = Boolean(task.completed);
        const span = document.createElement('span');
        span.textContent = task.title;
        label.appendChild(checkbox);
        label.appendChild(span);
        li.appendChild(label);
        listEl.appendChild(li);
    });
};
</code></pre>
<p>You can call this function to loop through the tasks from the OpenAI result and print them on the screen.</p>
<p>You can add the update function to update tasks to be completed with the following code:</p>
<pre><code class="language-javascript">const updateFromResponse = (response) =&gt; {
    if (response?.structuredContent?.tasks) {
        tasks = response.structuredContent.tasks;
        render();
    }
};
</code></pre>
<p>In the code above, you received a new response from the AI and an update form via the function. This function will get the todos list from the LLM and re-render the HTML to show the todos:</p>
<pre><code class="language-javascript">const handleSetGlobals = (event) =&gt; {
    const globals = event.detail?.globals;
    if (!globals?.toolOutput?.tasks) return;
    tasks = globals.toolOutput.tasks;
    render();
};
</code></pre>
<p>In the next code block, you'll handle the form response in the updateFormResponse function and set event listeners to update the code when changes are detected:</p>
<pre><code class="language-javascript">
window.addEventListener("openai:set_globals", handleSetGlobals, {
    passive: true,
});

const mutateTasksLocally = (name, payload) =&gt; {
    if (name === "add_todo") {
        tasks = [
            ...tasks,
            { id: crypto.randomUUID(), title: payload.title, completed: false },
        ];
    }

    if (name === "complete_todo") {
        tasks = tasks.map((task) =&gt;
            task.id === payload.id ? { ...task, completed: true } : task
        );
    }

    if (name === "set_completed") {
        tasks = tasks.map((task) =&gt;
            task.id === payload.id
                ? { ...task, completed: payload.completed }
                : task
        );
    }
    render();
};

const callTodoTool = async (name, payload) =&gt; {
    if (window.openai?.callTool) {
        const response = await window.openai.callTool(name, payload);
        updateFromResponse(response);
        return;
    }
    mutateTasksLocally(name, payload);
};

formEl.addEventListener("submit", async (event) =&gt; {
    event.preventDefault();
    const title = inputEl.value.trim();
    if (!title) return;
    await callTodoTool("add_todo", { title });
    inputEl.value = "";
});

listEl.addEventListener("change", async (event) =&gt; {
    const checkbox = event.target;
    if (!checkbox.matches('input[type="checkbox"]')) return;
    const id = checkbox.closest("li")?.dataset.id;
    if (!id) return;
    if (!checkbox.checked) {
        if (window.openai?.callTool) {
            checkbox.checked = true;
            return;
        }
        mutateTasksLocally("set_completed", { id, completed: false });
        return;
    }
    await callTodoTool("complete_todo", { id });
});

render();
</code></pre>
<h3 id="heading-step-3-styling-your-ui">Step 3: Styling your UI</h3>
<p>Since you've created the HTML tags and JavaScript code for your UI, you can improve the look of it by styling it the way you like with CSS. For that, you can use the following code and add it inside the <code>style</code> tag in the HTML file.</p>
<pre><code class="language-css"> :root {
        color: #0b0b0f;
        font-family:
          "Inter",
          system-ui,
          -apple-system,
          sans-serif;
      }

      html,
      body {
        width: 100%;
        min-height: 100%;
        box-sizing: border-box;
      }

      body {
        margin: 0;
        padding: 16px;
        background: #f6f8fb;
      }

      main {
        width: 100%;
        max-width: 360px;
        min-height: 260px;
        margin: 0 auto;
        background: #fff;
        border-radius: 16px;
        padding: 20px;
        box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08);
      }

      h2 {
        margin: 0 0 16px;
        font-size: 1.25rem;
      }

      form {
        display: flex;
        gap: 8px;
        margin-bottom: 16px;
      }

      form input {
        flex: 1;
        padding: 10px 12px;
        border-radius: 10px;
        border: 1px solid #cad3e0;
        font-size: 0.95rem;
      }

      form button {
        border: none;
        border-radius: 10px;
        background: #111bf5;
        color: white;
        font-weight: 600;
        padding: 0 16px;
        cursor: pointer;
      }

      input[type="checkbox"] {
        accent-color: #111bf5;
      }

      ul {
        list-style: none;
        padding: 0;
        margin: 0;
        display: flex;
        flex-direction: column;
        gap: 8px;
      }

      li {
        background: #f2f4fb;
        border-radius: 12px;
        padding: 10px 14px;
        display: flex;
        align-items: center;
        gap: 10px;
      }

      li span {
        flex: 1;
      }

      li[data-completed="true"] span {
        text-decoration: line-through;
        color: #6c768a;
      }
</code></pre>
<p>Now you can commit your code to Git and track it in the tree:</p>
<ul>
<li><p><code>git add .</code></p>
</li>
<li><p><code>git commit -m "feat: add MCP server UI"</code></p>
</li>
</ul>
<h3 id="heading-step-4-add-the-ui-to-your-mcp-server">Step 4: Add the UI to your MCP Server:</h3>
<p>Writing the HTML file isn't enough to add resources to your project. You also need to upload the HTML and resources to the MCP server and configure the server to use them using the tools you provided.</p>
<p>To make your MCP server aware of the UI and HTML, you need to add extra functions to the MCP server and some _meta keys to the server tools.</p>
<p>Here's the signature of the resources function that the MCP server will use. This signature tells the LLM what type of file to read and which resources to use when it returns the output template. You'll add this code to your <a href="http://server.js">server.js</a> and your MCP server, then create your own HTML file that includes the design and UI.</p>
<pre><code class="language-javascript">registerResource(name: string, uriOrTemplate: string, config: ResourceMetadata, readCallback: ReadResourceCallback): RegisteredResource;
</code></pre>
<p>To use the signature function, you can use the following simple code at the top of your file, which will read the HTML file you created:</p>
<pre><code class="language-javascript">import { readFileSync } from "node:fs";

const todoHtml = readFileSync("public/todo-widget.html", "utf8");
</code></pre>
<p>And this resources registration code in the <code>createTodoServer</code> function, which will tell the LLM the type of HTML to use and where to find it.</p>
<pre><code class="language-javascript">server.registerResource(
    "todo-widget",
    "ui://widget/todo.html",
    {},
    async () =&gt; ({
        contents: [
            {
                uri: "ui://widget/todo.html",
                mimeType: "text/html+skybridge",
                text: todoHtml,
                _meta: { "openai/widgetPrefersBorder": true },
            },
        ],
    })
);
</code></pre>
<p>In the above code, you've added the following parameters:</p>
<ul>
<li><p>The name of the resource</p>
</li>
<li><p>The sources of the resource or the template as a string</p>
</li>
</ul>
<p>You kept the config empty to simplify the example</p>
<p>You only used the contents of the callback to show the information about the resources with the following details:</p>
<ul>
<li><p>mimeType: the type of the file you provided. You added Skybridge, which is the OpenAI protocol that renders the HTML inside an iframe in the ChatGPT UI.</p>
</li>
<li><p>URI: a specific name of your widget</p>
</li>
<li><p>Text: Which is your HTML file</p>
</li>
<li><p>_meta: specific details for ChatGPT</p>
</li>
</ul>
<h3 id="heading-step-5-update-your-mcp-server-to-handle-the-ui">Step 5: Update Your MCP Server to Handle the UI</h3>
<p>Now that you've written the HTML pages to show a simple UI for your data and added the HTML as a resource to your MCP server, you'll add the following code to the _meta section in the MCP server tools so it can handle and render the HTML output when needed. Without this, the LLM will only return the output without returning the UI:</p>
<pre><code class="language-javascript">_meta: {
            "openai/outputTemplate": "ui://widget/todo.html",
            "openai/toolInvocation/invoking": "Listing todos",
            "openai/toolInvocation/invoked": "Listed todos",
        },
</code></pre>
<p>So the _meta tag in your tools functions will look like the following:</p>
<pre><code class="language-javascript">    server.registerTool(
        'list_todos',
        {
            title: 'List todos',
            description: 'Lists all todo items.',
            _meta: {
                "openai/outputTemplate": "ui://widget/todo.html",
                'openai/toolInvocation/invoking': 'Listing todos',
                'openai/toolInvocation/invoked': 'Listed todos',
            },
        },
        async () =&gt; {
            return replyWithTodos();
        },
    );
</code></pre>
<p>Now you can commit your code to Git and track it in the tree:</p>
<ul>
<li><p><code>git add .</code></p>
</li>
<li><p><code>git commit -m "feat: add _meta outputTemplate tag to MCP server tools"</code></p>
</li>
</ul>
<h2 id="heading-how-to-test-your-chatgpt-app">How to Test Your ChatGPT App</h2>
<p>After adding the UI to your MCP server, you can run and test the project on ChatGPT by doing the following:</p>
<p>First, run your server normally with:</p>
<pre><code class="language-shell">node server.js
</code></pre>
<p>Then run your server through ngrok to enable online access, since you need OpenAI servers to be able to access your local machine:</p>
<pre><code class="language-shell">ngrok http 8787
</code></pre>
<p>Note: You need to have an ngrok account and log in to it via the CLI.</p>
<p>To add your resources to ChatGPT, you need to enable dev mode and add it as a connector:</p>
<ul>
<li><p>Click on your profile in the ChatGPT UI</p>
</li>
<li><p>Click on Apps</p>
</li>
<li><p>Click on Advanced settings to create your own app</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/cd78be7e-05e0-435e-86a6-9613b08f4e53.png" alt="Showing how to add a connector from ChatGPT Interface" style="display:block;margin:0 auto" width="1326" height="798" loading="lazy">

<p>Then you can add your server to ChatGPT and test it thoroughly.</p>
<p>You'll need to write the following data in this input:</p>
<ul>
<li><p>App name</p>
</li>
<li><p>Descripiton</p>
</li>
<li><p>Connection: as a server URL with your ngrok link from the terminal, with the <code>mcp</code> slash</p>
</li>
<li><p>No authentication, since we haven't implemented it yet</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/005ab434-a487-469a-9500-b4e64175e870.png" alt="the app input data for OpenAI" style="display:block;margin:0 auto" width="488" height="733" loading="lazy">

<p>After adding the app, you can use it in the conversation by calling it with the app name by writing <code>@app_name</code></p>
<p>Here are the examples from ChatGPT:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/0b36e741-e7c9-4d18-aa63-0fd419a68896.png" alt="Example of using the app inside ChatGPT" style="display:block;margin:0 auto" width="844" height="596" loading="lazy">

<p>Here is the example of completing a step:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/b19c338d-4b1b-4786-b514-40f0d586fbe5.png" alt="Example of completing a task inside the app in ChatGPT" style="display:block;margin:0 auto" width="846" height="788" loading="lazy">

<p>In the next section, you'll add authentication and a database to your project to move it to the next level.</p>
<h2 id="heading-level-3-how-to-add-supabase-auth-and-database-to-the-mcp-server">Level 3: How to Add Supabase (Auth and Database) to the MCP Server</h2>
<p>To add authentication and a backend, you'll need a backend/SQL server and an authentication server. The easiest current way is to use a service that can provide that. For this, you'll use Supabase.</p>
<p>To start, you'll create a new Supabase project for your backend. The project will include a simple table for the todos you have created in your MCP server and use it as the backend. Then you'll implement authentication.</p>
<h3 id="heading-step-1-create-the-todos-table">Step 1: Create the Todos Table</h3>
<p>To create the table, navigate through your project on Supabase and use the SQL editor to write the following code to add the todos:</p>
<pre><code class="language-sql">-- Enable pgcrypto for gen_random_uuid
CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- Create todos table
CREATE TABLE IF NOT EXISTS public.todos (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE,
  title text NOT NULL,
  completed boolean NOT NULL DEFAULT false,
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now()
);

-- Function to keep updated_at current
CREATE OR REPLACE FUNCTION public.set_updated_at()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
  NEW.updated_at = now();
  RETURN NEW;
END;
$$;

-- Attach trigger
DROP TRIGGER IF EXISTS set_updated_at_trigger ON public.todos;

CREATE TRIGGER set_updated_at_trigger
BEFORE UPDATE ON public.todos
FOR EACH ROW
EXECUTE FUNCTION public.set_updated_at();
</code></pre>
<p>At the end, set the table to row-level security. This allows related users to see their data:</p>
<pre><code class="language-sql">-- Enable Row Level Security
ALTER TABLE public.todos ENABLE ROW LEVEL SECURITY;

-- Users can read only their own todos
CREATE POLICY "Users can view their own todos"
ON public.todos
FOR SELECT
TO authenticated
USING (user_id = (SELECT auth.uid()));

-- Users can insert only their own todos
CREATE POLICY "Users can insert their own todos"
ON public.todos
FOR INSERT
TO authenticated
WITH CHECK ((user_id IS NOT NULL) 
AND (user_id = (SELECT auth.uid())));

-- Users can update only their own todos
CREATE POLICY "Users can update their own todos"
ON public.todos
FOR UPDATE
TO authenticated 
USING (user_id = (SELECT auth.uid())) 
WITH CHECK (user_id = (SELECT auth.uid()));

-- Users can delete only their own todos
CREATE POLICY "Users can delete their own todos"
ON public.todos
FOR DELETE
USING (auth.uid() = user_id);

-- Index for faster user-specific queries
CREATE INDEX IF NOT EXISTS idx_todos_user_id
ON public.todos(user_id);
</code></pre>
<p>Since your database is now ready, you can integrate authentication with your server. First, you need to authenticate the server, get the token, and use it on the server. Then you can test the app again.</p>
<p>To authenticate, you need to implement the following endpoints on your server (and add your own information in place of the example info):</p>
<ul>
<li><p>GET: <a href="https://your-mcp.example.com/.well-known/oauth-protected-resource">https://your-mcp.example.com/.well-known/oauth-protected-resource</a></p>
</li>
<li><p>OAuth 2.0 metadata: <a href="https://auth.yourcompany.com/.well-known/oauth-authorization-server">https://auth.yourcompany.com/.well-known/oauth-authorization-server</a></p>
</li>
<li><p>OpenID Connect metadata: <a href="https://auth.yourcompany.com/.well-known/openid-configuration">https://auth.yourcompany.com/.well-known/openid-configuration</a></p>
</li>
</ul>
<p>The OAuth-protected resource communicates with the server about how to use and register the tools, how to run them, and what to call them. The other two endpoints share the related metadata from the server</p>
<p>You'll need to implement those endpoints on your server and use them as a proxy to fetch data from Supabase, since it will be your main auth server.</p>
<h3 id="heading-step-2-enabling-the-mcp-server-to-connect-with-supabase-auth">Step 2: Enabling the MCP Server to Connect with Supabase Auth</h3>
<p>For this, you need to do the following:</p>
<ul>
<li><p>Enable the OAuth server at Supabase and enable the dynamic registration of tools</p>
</li>
<li><p>Implement a page for login to use for OAuth permission</p>
</li>
</ul>
<p>To enable the OAuth server on your Supabase, you need to go to <a href="https://supabase.com/dashboard/project/_/auth/oauth-server">https://supabase.com/dashboard/project/_/auth/oauth-server</a>, then follow the next steps:</p>
<ul>
<li><p>Toggle Enable OAuth server</p>
</li>
<li><p>Allow dynamic apps</p>
</li>
<li><p>Create your consent page: the page that LLM tools will show users when they need to grant access to the data.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/136c2185-2d74-45f6-b981-2af946e1b330.png" alt="Showing how to enable dynamic apps from Supabase" style="display:block;margin:0 auto" width="1266" height="674" loading="lazy">

<p>To use the consent page and see it in action, you'll need to implement the OAuth server in your MCP server first. This is what you'll do in the next section.</p>
<h3 id="heading-step-3-create-a-proxy-server-for-the-mcp-server-to-handle-the-auth">Step 3: Create a Proxy Server for the MCP Server to Handle the Auth.</h3>
<p>After enabling the OAuth server in Supabase, you can start implementing the OAuth code on the MCP server. To do that, you need a proxy code on your MCP server and to create a logging endpoint to use it. The proxy server will allow your MCP server to use Supabase's OAuth server.</p>
<p>You'll continue by adding the next code to the MCP server you've created earlier. At the top of your code, after the imports in the <code>server.js</code> file, you should define the following variables:</p>
<pre><code class="language-javascript">const SUPABASE_URL = "https://YOURPORJECT.supabase.co";
const MCP_SERVER_URL = "http://localhost:8787/mcp";
const SUPABASE_AUTH_URL = `${SUPABASE_URL}/auth/v1`;
</code></pre>
<p>Note: Don't forget to enter your own project URL for the Supabase URL. You can find it in the Supabase UI by clicking Connect at the top of the page.</p>
<p>Inside the createServer function and after the <code>if (req.method === 'OPTIONS')</code> condition, add the following proxy code to link your Supabase project:</p>
<pre><code class="language-javascript">const OIDC_DISCOVERY_URL = `${SUPABASE_AUTH_URL}/.well-known/openid-configuration`;

if (req.method === "GET" &amp;&amp; url.pathname === "/.well-known/openid-configuration") {
    const response = await fetch(OIDC_DISCOVERY_URL);
    const data = await response.json();
    res.writeHead(200, {
        "content-type": "application/json",
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "GET, OPTIONS",
    });
    res.end(JSON.stringify(data));
    return;
}
</code></pre>
<p>Then you can add this code for the OAuth authorities server:</p>
<pre><code class="language-javascript">const OAUTH_DISCOVERY_URL = `${SUPABASE_URL}/.well-known/oauth-authorization-server/auth/v1`;

if (req.method === "GET" &amp;&amp; url.pathname === "/.well-known/oauth-authorization-server") {
    const response = await fetch(OAUTH_DISCOVERY_URL);
    const data = await response.json();
    res.writeHead(200, {
        "content-type": "application/json",
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "GET, OPTIONS",
    });
    res.end(JSON.stringify(data));
    return;
}
</code></pre>
<p>Then add this code for the well-known server:</p>
<pre><code class="language-javascript">// OPTIONS /.well-known/oauth-protected-resource/mcp
// GET /.well-known/oauth-protected-resource/mcp
if (req.method === "GET" &amp;&amp; (url.pathname === "/.well-known/oauth-protected-resource/mcp" || url.pathname === "/.well-known/oauth-protected-resource")) {
    const metadata = {
        resource: MCP_SERVER_URL,
        authorization_servers: [SUPABASE_AUTH_URL],
        // Use standard OIDC scopes. Custom resource scopes are enforced server-side, not by Supabase.
        scopes_supported: ["openid", "profile", "email", "phone"],
    };
    res.writeHead(200, {
        "content-type": "application/json",
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "GET, OPTIONS",
        "Access-Control-Allow-Headers": "content-type, MCP-Protocol-Version, mcp-protocol-version, authorization",
    });
    res.end(JSON.stringify(metadata));
    return;
}
</code></pre>
<p>By adding the previous code snippets, you've implemented a proxy server that fetches data from Supabase and relays it to the MCP protocol as if it were your own server.</p>
<p>Now you can commit your code to Git and track it in the tree:</p>
<ul>
<li><p><code>git add .</code></p>
</li>
<li><p><code>git commit -m "feat: add supabase proxy server"</code></p>
</li>
</ul>
<p>Since you've implemented your proxy server, you can use authentication and authorization from your MCP server to retrieve data in your tools.</p>
<h3 id="heading-step-4-implementing-the-consent-and-login-page">Step 4: Implementing the Consent and Login Page</h3>
<p>On the OAuth server, you might have noticed a consent page. The goal of this page is to inform the user that they are authorizing the LLM to connect to a database or an external resource. In the next section, you will implement this page by making two steps:</p>
<ul>
<li><p>First, create a login page that lets users log in to the app.</p>
</li>
<li><p>Second, you will create a consent page that allows the logged-in user to communicate with the LLM</p>
</li>
</ul>
<p>You'll start by creating a new Next.js server, which gives you more flexibility when working with pages.</p>
<p>You can create your NextJS app with the command:</p>
<pre><code class="language-shell">npx create-next-app@latest mcp_consent --yes
</code></pre>
<p>Navigate to the mcp_consent folder and add Supabase:</p>
<pre><code class="language-shell">npm install @supabase/ssr
</code></pre>
<p>Add the current state of the project to your Git tracker by writing the following:</p>
<ul>
<li><p><code>git add .</code></p>
</li>
<li><p><code>git commit -m "init nextjs project"</code></p>
</li>
</ul>
<p>Add <code>.env</code> file from your Supabase, which will include the following code:</p>
<pre><code class="language-plaintext">NEXT_PUBLIC_SUPABASE_URL=YOUR_URL
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=YOUR_KEY
</code></pre>
<p>Now you can create a login page in the next path:</p>
<p><code>app/login/page.tsx</code></p>
<p>The login page:</p>
<pre><code class="language-javascript">"use client";
import { useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { createBrowserClient } from "@supabase/ssr/dist/module/createBrowserClient";


export default function LoginPage() {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState&lt;string | null&gt;(null);
    const router = useRouter();
    const searchParams = useSearchParams();
    const supabase = createBrowserClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
    );

    const handleLogin = async (e: React.FormEvent) =&gt; {
        e.preventDefault();
        setLoading(true);
        setError(null);


        try {
            const { error } = await supabase.auth.signInWithPassword({
                email,
                password,
            });


            if (error) {
                setError(error.message);
            } else {
                const redirectTo = searchParams.get("redirect") || "/";
                router.push(redirectTo);
                router.refresh();
            }
        } catch (err) {
            setError("An unexpected error occurred");
        } finally {
            setLoading(false);
        }
    };


    const handleSignUp = async (e: React.FormEvent) =&gt; {
        e.preventDefault();
        setLoading(true);
        setError(null);


        try {
            const { error } = await supabase.auth.signUp({
                email,
                password,
            });


            if (error) {
                setError(error.message);
            } else {
                setError(null);
                alert("Sign up successful! Please check your email to confirm your account.");
            }
        } catch (err) {
            setError("An unexpected error occurred");
        } finally {
            setLoading(false);
        }
    };


    return (
        &lt;div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"&gt;
            &lt;div className="max-w-md w-full bg-white rounded-lg shadow-md p-8"&gt;
                &lt;h2 className="text-center text-3xl font-extrabold text-gray-900 mb-8"&gt;
                    Authentication
                &lt;/h2&gt;


                {error &amp;&amp; (
                    &lt;div className="mb-4 p-4 bg-red-50 border border-red-200 text-red-700 rounded"&gt;
                        {error}
                    &lt;/div&gt;
                )}


                &lt;form onSubmit={handleLogin} className="space-y-6"&gt;
                    &lt;div&gt;
                        &lt;label
                            htmlFor="email"
                            className="block text-sm font-medium text-gray-700"
                        &gt;
                            Email address
                        &lt;/label&gt;
                        &lt;input
                            id="email"
                            type="email"
                            required
                            value={email}
                            onChange={(e) =&gt; setEmail(e.target.value)}
                            className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-black"
                            placeholder="you@example.com"
                        /&gt;
                    &lt;/div&gt;


                    &lt;div&gt;
                        &lt;label
                            htmlFor="password"
                            className="block text-sm font-medium text-gray-700"
                        &gt;
                            Password
                        &lt;/label&gt;
                        &lt;input
                            id="password"
                            type="password"
                            required
                            value={password}
                            onChange={(e) =&gt; setPassword(e.target.value)}
                            className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-black"
                            placeholder="••••••••"
                        /&gt;
                    &lt;/div&gt;


                    &lt;div className="flex gap-3"&gt;
                        &lt;button
                            type="submit"
                            disabled={loading}
                            className="flex-1 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
                        &gt;
                            {loading ? "Loading..." : "Login"}
                        &lt;/button&gt;
                        &lt;button
                            type="button"
                            onClick={handleSignUp}
                            disabled={loading}
                            className="flex-1 py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
                        &gt;
                            {loading ? "Loading..." : "Sign Up"}
                        &lt;/button&gt;
                    &lt;/div&gt;
                &lt;/form&gt;


                &lt;div className="mt-6"&gt;
                    &lt;p className="text-center text-sm text-gray-600"&gt;
                        Password reset or other options available upon request.
                    &lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    );
}
</code></pre>
<p>The OAuth decision page:</p>
<pre><code class="language-javascript">// app/api/oauth/decision/route.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
    const formData = await request.formData()
    const decision = formData.get('decision')
    const authorizationId = formData.get('authorization_id') as string
    if (!authorizationId) {
        return NextResponse.json({ error: 'Missing authorization_id' }, { status: 400 })
    }
    const supabase = createServerClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
        {
            cookies: {
                getAll: async () =&gt; (await cookies()).getAll(),
                setAll: async (cookiesToSet) =&gt; {
                    const cookieStore = await cookies()
                    cookiesToSet.forEach(({ name, value, options }) =&gt; cookieStore.set(name, value, options))
                },
            },
        }
    )
    if (decision === 'approve') {
        const { data, error } = await supabase.auth.oauth.approveAuthorization(authorizationId)
        if (error) {
            return NextResponse.json({ error: error.message }, { status: 400 })
        }
        // Redirect back to the client with authorization code
        return NextResponse.redirect(data.redirect_url)
    } else {
        const { data, error } = await supabase.auth.oauth.denyAuthorization(authorizationId)
        if (error) {
            return NextResponse.json({ error: error.message }, { status: 400 })
        }
        // Redirect back to the client with error
        return NextResponse.redirect(data.redirect_url)
    }
}
</code></pre>
<p>The OAuth Consent page:</p>
<pre><code class="language-typescript">// app/oauth/consent/page.tsx
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'

export default async function ConsentPage({
    searchParams,
}: {
    searchParams: { authorization_id?: string }
}) {
    const authorizationId = (await searchParams).authorization_id

    if (!authorizationId) {
        return &lt;div&gt;Error: Missing authorization_id&lt;/div&gt;
    }

    const supabase = createServerClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
        {
            cookies: {
                getAll: async () =&gt; (await cookies()).getAll(),
                setAll: async (cookiesToSet) =&gt; {
                    try {
                        const cookieStore = await cookies()
                        cookiesToSet.forEach(({ name, value, options }) =&gt;
                            cookieStore.set(name, value, options)
                        )
                    } catch (error) {
                        // In Server Components, cookie writes can fail during render.
                        // Route Handlers/Server Actions should handle persistence.
                        console.warn('Skipping cookie write in Server Component render context', error)
                    }
                },
            },
        }
    )

    // Check if user is authenticated
    const {
        data: { user },
    } = await supabase.auth.getUser()

    if (!user) {
        // Redirect to login, preserving authorization_id
        redirect(`/login?redirect=/oauth/consent?authorization_id=${authorizationId}`)
    }

    // Get authorization details using the authorization_id
    const { data: authDetails, error } =
        await supabase.auth.oauth.getAuthorizationDetails(authorizationId)
    console.log("Auth Details: ", authDetails)
    if (error || !authDetails) {
        return &lt;div&gt;Error: {error?.message || 'Invalid authorization request'}&lt;/div&gt;
    }
    if ("redirect_url" in authDetails &amp;&amp; authDetails.redirect_url &amp;&amp; typeof authDetails.redirect_url === "string") {
        const redirectUrl = authDetails.redirect_url;
        console.log("Redirect URL:", redirectUrl);
        return redirect(redirectUrl);
    }
    if (!("client" in authDetails)) {
        return &lt;div&gt;Error: Invalid authorization details format&lt;/div&gt;
    }
    return (
        &lt;div className="relative min-h-screen w-full overflow-hidden flex items-center justify-center p-4"&gt;
            {/* Animated gradient background */}
            &lt;div className="fixed inset-0 -z-10"&gt;
                &lt;div className="absolute inset-0 bg-gradient-to-br from-slate-900 via-slate-900 to-slate-800" /&gt;
                &lt;div className="absolute top-0 right-0 w-96 h-96 bg-blue-500/10 rounded-full blur-3xl" /&gt;
                &lt;div className="absolute bottom-0 left-0 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl" /&gt;
            &lt;/div&gt;

            {/* Main Card Container */}
            &lt;div className="w-full max-w-md animate-fade-in-up"&gt;
                {/* Gradient border effect */}
                &lt;div className="relative"&gt;
                    &lt;div className="absolute inset-0 bg-gradient-to-r from-blue-500 via-purple-500 to-cyan-500 rounded-2xl blur opacity-75 group-hover:opacity-100 transition duration-1000" /&gt;

                    {/* Content Card */}
                    &lt;div className="relative bg-slate-900/80 backdrop-blur-xl rounded-2xl p-8 border border-slate-700/50 shadow-2xl"&gt;
                        {/* Header Section */}
                        &lt;div className="text-center mb-8"&gt;
                            &lt;div className="inline-block mb-4"&gt;
                                &lt;div className="w-16 h-16 rounded-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center shadow-lg"&gt;
                                    &lt;svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"&gt;
                                        &lt;path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /&gt;
                                    &lt;/svg&gt;
                                &lt;/div&gt;
                            &lt;/div&gt;
                            &lt;h1 className="text-3xl font-bold text-white mb-2"&gt;Authorization Required&lt;/h1&gt;
                            &lt;p className="text-slate-400 text-sm"&gt;Review and authorize access to your account&lt;/p&gt;
                        &lt;/div&gt;

                        {/* Client Information */}
                        &lt;div className="space-y-4 mb-8 bg-slate-800/50 rounded-lg p-4 border border-slate-700/30"&gt;
                            &lt;div className="flex items-start space-x-3"&gt;
                                &lt;div className="w-2 h-2 rounded-full bg-cyan-400 mt-2 flex-shrink-0" /&gt;
                                &lt;div className="flex-1"&gt;
                                    &lt;p className="text-xs text-slate-500 uppercase tracking-widest"&gt;Application&lt;/p&gt;
                                    &lt;p className="text-lg font-semibold text-white"&gt;{authDetails.client.name}&lt;/p&gt;
                                &lt;/div&gt;
                            &lt;/div&gt;

                            &lt;div className="flex items-start space-x-3"&gt;
                                &lt;div className="w-2 h-2 rounded-full bg-purple-400 mt-2 flex-shrink-0" /&gt;
                                &lt;div className="flex-1 min-w-0"&gt;
                                    &lt;p className="text-xs text-slate-500 uppercase tracking-widest"&gt;Redirect URI&lt;/p&gt;
                                    &lt;p className="text-xs text-slate-300 break-all font-mono mt-1"&gt;{authDetails.redirect_uri}&lt;/p&gt;
                                &lt;/div&gt;
                            &lt;/div&gt;
                        &lt;/div&gt;

                        {/* Permissions Section */}
                        {authDetails.scope &amp;&amp; authDetails.scope.length &gt; 0 &amp;&amp; (
                            &lt;div className="mb-8"&gt;
                                &lt;p className="text-xs text-slate-500 uppercase tracking-widest mb-3 font-semibold"&gt;Requested Permissions&lt;/p&gt;
                                &lt;div className="space-y-2"&gt;
                                    {authDetails.scope.split(" ").map((scope, index) =&gt; (
                                        &lt;div key={index} className="flex items-center space-x-2 text-sm text-slate-300 bg-slate-800/30 rounded-lg p-3 border border-slate-700/20"&gt;
                                            &lt;svg className="w-4 h-4 text-blue-400 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"&gt;
                                                &lt;path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /&gt;
                                            &lt;/svg&gt;
                                            &lt;span&gt;{scope}&lt;/span&gt;
                                        &lt;/div&gt;
                                    ))}
                                &lt;/div&gt;
                            &lt;/div&gt;
                        )}

                        {/* Action Buttons */}
                        &lt;form action="/api/oauth/decision" method="POST" className="space-y-3"&gt;
                            &lt;input type="hidden" name="authorization_id" value={authorizationId} /&gt;

                            &lt;button
                                type="submit"
                                name="decision"
                                value="approve"
                                className="w-full py-3 px-4 rounded-lg font-semibold text-white bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 shadow-lg hover:shadow-blue-500/50 transform hover:scale-105 transition-all duration-300 ease-out active:scale-95"
                            &gt;
                                Authorize Access
                            &lt;/button&gt;

                            &lt;button
                                type="submit"
                                name="decision"
                                value="deny"
                                className="w-full py-3 px-4 rounded-lg font-semibold text-slate-300 border-2 border-slate-600 hover:border-slate-500 hover:text-white hover:bg-slate-800/50 transition-all duration-300 ease-out active:scale-95"
                            &gt;
                                Cancel
                            &lt;/button&gt;
                        &lt;/form&gt;

                        {/* Security Info */}
                        &lt;p className="text-center text-xs text-slate-500 mt-6 flex items-center justify-center space-x-1"&gt;
                            &lt;svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"&gt;
                                &lt;path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" /&gt;
                            &lt;/svg&gt;
                            &lt;span&gt;Your data is protected with industry-standard encryption&lt;/span&gt;
                        &lt;/p&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>Now you can add the current state of the project to your Git tracker by writing the following:</p>
<ul>
<li><p><code>git add .</code></p>
</li>
<li><p><code>git commit -m "feat: add consent page"</code></p>
</li>
</ul>
<h3 id="heading-step-5-testing-the-oauth-implementation-with-mcp-server-inspector">Step 5: Testing the OAuth Implementation with MCP Server Inspector</h3>
<p>Since you've implemented the consent page, now you can test it and check the authorization in the MCP Server Inspector. This step will help you see how OAuth works and how to test it with the inspector.</p>
<p>First, create a new user in Supabase for login and authentication.</p>
<ul>
<li><p>Go to: <a href="https://supabase.com/dashboard/project/_/auth/users">https://supabase.com/dashboard/project/_/auth/users</a><br>(Auth -&gt; users from the UI)</p>
</li>
<li><p>Click Add User -&gt; Create a new user, then add the new user email and password.</p>
</li>
</ul>
<p>After creating the user, you can run the projects by typing the following in different terminals:</p>
<p>Run your MCP server:</p>
<pre><code class="language-plaintext">node server.js
</code></pre>
<p>Open your inspector:</p>
<pre><code class="language-plaintext">npx @modelcontextprotocol/inspector@latest --server-url http://localhost:8787/mcp --transport http
</code></pre>
<p>Run the Next.js project to get access to the consent page:</p>
<pre><code class="language-plaintext">cd mcp_consent
npm run dev
</code></pre>
<p>When you run the inspector, you can connect to your MCP Server on the left and navigate to Auth on the top tab. This will show you the authentication flow to test and run.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/f06db9f1-bdfa-4757-8177-21651c291666.png" alt="OAuth Flow inside the MCP Server Inspector" style="display:block;margin:0 auto" width="1510" height="680" loading="lazy">

<p>When you click Connect, go to Auth to check your options. The guided OAuth flow will show you a step-by-step guide to how the MCP Server obtains OAuth authorization and will help you debug your code if issues arise. The Check OAuth Flow button lets you connect directly and see the latest result immediately.</p>
<p>For the sake of speed, you can just click on the "Check OAuth Flow". This will redirect you to the login page:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/3a7c5090-41d2-489f-b46b-5554ebcfc149.png" alt="Login page screenshot" style="display:block;margin:0 auto" width="561" height="487" loading="lazy">

<p>After you log in, you'll get redirected again to the consent page so that you can give consent to the LLM to access your data:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/c9caa39b-4750-43ef-9c77-603898914e90.png" alt="Consent page screenshot" style="display:block;margin:0 auto" width="550" height="904" loading="lazy">

<p>Then you'll be redirected again to the MCP Server and you can check the results of the OAuth flow:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/e43e2096-901e-4669-b965-0019810704b4.png" alt="Correct MCP Server OAuth flow" style="display:block;margin:0 auto" width="256" height="611" loading="lazy">

<p>In the next step, you'll harden your MCP Server functions to take your app to the next level by using OAuth for MCP Server.</p>
<h3 id="heading-step-6-adding-oauth-security-to-your-mcp-server-tools">Step 6: Adding OAuth Security to Your MCP Server Tools</h3>
<p>Congrats on implementing your OAuth flow and getting it to work! Now you'll add this flow to your MCP Server tools so it runs only when the user is authenticated.</p>
<p>Before updating the tools, you'll write a few helper functions to assist you during the process. First, you'll write a verification token to process every request. Then you'll update the list of MCP server tools to use the verification function instead of implementing it for each function by itself.</p>
<p>Inside your <code>server.js</code> file, you'll implement a function that verifies the token with Supabase. First, import the Supabase client to use it:</p>
<pre><code class="language-javascript">import { SupabaseClient } from "@supabase/supabase-js";
</code></pre>
<p>Then add the Supabase publishable key to use it in the client at the top of the server:</p>
<pre><code class="language-javascript">const SUPABASE_PUBLISHABLE_KEY = "YOUR_KEY";
</code></pre>
<p>And update the reply todos list to get an argument of todos, instead of the in-memory array.</p>
<pre><code class="language-javascript">const replyWithTodos = (message, todos) =&gt; ({
    content: message ? [{ type: 'text', text: message }] : [],
    structuredContent: { tasks: todos },
}); //outside the createTodoServer function block
</code></pre>
<p>Then you'll need to create a helper function to verify the user tokens:</p>
<pre><code class="language-javascript">const verifyToken = async (token) =&gt; {
    if (!token || !token.startsWith("Bearer ")) {
        return { isValid: false, error: "Missing or invalid Authorization header" };
    }
    // Verify token with Supabase
    try {
        // use supabase client to verify token
        const supabase = new SupabaseClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
            global: {
                headers: {
                    Authorization: token,
                },
            },
        });
        const { data: user, error } = await supabase.auth.getUser();
        if (error || !user) {
            return { isValid: false, error: "Token verification failed" + (error?.message || "") };
        }
        console.log("Token verified for user:", user);
        return { isValid: true, token, user, supabase };
    } catch (error) {
        console.error("Token verification failed:", error);
        return { isValid: false, error: "Token verification failed" + (error?.message || "") };
    }
};
</code></pre>
<p>In this function, you get the token as a string, check Supabase, and return an error if the token isn't provided. If it's correct, you return the token, user data, and Supabase client.</p>
<p>After this, you need to have a helper function to adhere to MCP Server specs:</p>
<pre><code class="language-javascript">/**
 * Build WWW-Authenticate header for 401/403 responses
 * Per RFC 9728 OAuth 2.1 Protected Resource Metadata specification
 */
function buildWwwAuthenticateHeader(error, errorDescription) {
    const resourceMetadataUrl = `${MCP_SERVER_URL}/.well-known/oauth-protected-resource`

    let header = `Bearer resource_metadata="${resourceMetadataUrl}"`

    if (error) {
        header += `, error="${error}"`
    }

    if (errorDescription) {
        header += `, error_description="${errorDescription}"`
    }

    return header
}

function returnAuthErrorResponse(resOrMessage, error = "unauthorized", errorDescription = "Missing or invalid authorization token.") {
    const wwwAuthenticate = buildWwwAuthenticateHeader(error, errorDescription);

    if (resOrMessage &amp;&amp; typeof resOrMessage.writeHead === "function") {
        resOrMessage.writeHead(401, {
            "content-type": "application/json",
            "Access-Control-Allow-Origin": "*",
            "WWW-Authenticate": wwwAuthenticate,
        });
        resOrMessage.end(JSON.stringify({ error, error_description: errorDescription }));
        return;
    }

    const message = typeof resOrMessage === "string" &amp;&amp; resOrMessage.length &gt; 0
        ? resOrMessage
        : errorDescription;

    return {
        content: [{ type: "text", text: message }],
        isError: true,
        statusCode: 401,
        _meta: {
            "mcp/www_authenticate": wwwAuthenticate,
        },
    };
}


function returnErrorResponse(message) {
    return {
        content: [
            {
                type: "text",
                text: message
            }
        ],
        isError: true
    };
}
</code></pre>
<p>In these helper functions, you create unique functions for errors and the OAuth error return function. At the same time, you define the OAuth-protected resources discovery specs.</p>
<p>Add the current state of the project to your Git tracker by writing the following:</p>
<ul>
<li><p><code>git add .</code></p>
</li>
<li><p><code>git commit -m "feat: add helper functions"</code></p>
</li>
</ul>
<p>Now you can apply them to your MCP Server tools, making them easier to read.</p>
<h3 id="heading-step-7-updating-the-mcp-server-function-to-handle-the-authentication">Step 7: Updating the MCP Server Function to Handle the Authentication</h3>
<p>After you've built the proxy to handle authentication requests, you need to update the MCP server functions and metadata to indicate whether the tool can be used with or without authentication.</p>
<p>Here you'll add two main things:</p>
<ul>
<li><p>The security schema.</p>
</li>
<li><p>The logic for the function to handle.</p>
</li>
</ul>
<pre><code class="language-javascript">   server.registerTool(
        "list_todos",
        {
            title: "List todos",
            description: "Lists all todo items.",
            _meta: {
                "openai/outputTemplate": "ui://widget/todo.html",
                "openai/toolInvocation/invoking": "Listing todos",
                "openai/toolInvocation/invoked": "Listed todos",
            },
            securitySchemes: [
                { type: "oauth2", scopes: ["todos.read"] }
            ],
            "annotations": {
                "readOnlyHint": true,
                "openWorldHint": false,
                "destructiveHint": false,
            }
        },
        async (meta) =&gt; {
            const authHeader = meta.requestInfo.headers?.authorization;
            const authResult = await verifyToken(authHeader);
            if (!authResult?.isValid) {
                return returnAuthErrorResponse(authResult?.error);
            }
            const { data, error } = await authResult.supabase
                .from("todos")
                .select("*")
                .eq("user_id", authResult.user.user.id)
                .order("created_at", { ascending: false });
            if (error) {
                console.error("Error listing todos:", error);
                return returnErrorResponse(error.message);
            }
            return replyWithTodos(null, data ?? []);
        }
    )
</code></pre>
<p>In this code, you've done the following:</p>
<ul>
<li><p>Updated the metadata to have a security schema that tells the MCP server to request authentication when invoking these tools.</p>
</li>
<li><p>Added an annotation, which helps the LLM model know how this function will perform. The annotations declare three types of changes that the tool can make:</p>
<ul>
<li><p>Read Only Hint: tells the LLM whether the tool is read-only and only shows data</p>
</li>
<li><p>Open World Hint: tells the LLM whether the tool can access external data, websites, or the internet.</p>
</li>
<li><p>Destructive Hint: tells the LLM if this is a destructive function, like deleting data permanently for the user</p>
</li>
</ul>
</li>
<li><p>Then, in the function itself, you retrieved the metadata from the callback and verified the token using the Supabase helper function. After that, you used the basic Supabase functions to retrieve the data and any errors that might occur.</p>
</li>
</ul>
<p>You can get the authorization from the metadata in the callback function itself.</p>
<p>In the previous function, you used the Supabase client to access the todos table, select all columns where the user_id condition is met, and order them by creation time. If the Supabase client returns an error, you return an error. Here's the code snippet that relates to Supabase:</p>
<pre><code class="language-javascript">const { data, error } = await authResult.supabase
  .from("todos")
  .select("*")
  .eq("user_id", authResult.user.user.id)
  .order("created_at", { ascending: false });
if (error) {
  console.error("Error listing todos:", error);
  return returnErrorResponse(error.message);
}
</code></pre>
<p>For the other function, you have the same logic applied, yet instead of select, you'll use either <code>insert</code> or <code>update</code>.</p>
<p>For the other functions, you can follow the same principles and use the same code. The only change is that first you get the data, then the metadata in the callback function:</p>
<pre><code class="language-javascript">    server.registerTool(
        "add_todo",
        {
            title: "Add todo",
            description: "Creates a todo item with the given title.",
            inputSchema: addTodoInputSchema,
            _meta: {
                "openai/outputTemplate": "ui://widget/todo.html",
                "openai/toolInvocation/invoking": "Adding todo",
                "openai/toolInvocation/invoked": "Added todo",
            },
            securitySchemes: [
                { type: "oauth2", scopes: ["todos.write"] }
            ],
            "annotations": {
                "readOnlyHint": false,
                "openWorldHint": false,
                "destructiveHint": true,
            }
        },
        async (args, meta) =&gt; {
            const authorizationHeader = meta.requestInfo.headers?.authorization;
            console.log("Authorization header:", authorizationHeader);
            const authResult = await verifyToken(authorizationHeader);
            console.log("Auth result:", authResult);
            if (!authResult?.isValid) {
                return returnAuthErrorResponse(authResult?.error);
            }
            const title = args?.title?.trim?.() ?? "";
            if (!title) return returnErrorResponse("Missing title.");
            let { data, error } = await authResult.supabase
                .from("todos")
                .insert({ title, user_id: authResult.user.user.id })
                .select("*");
            if (error) {
                console.error("Error adding todo:", error);
                return returnErrorResponse(error.message);
            }
            return replyWithTodos(`"${title}"`, data);
        }
    );
</code></pre>
<p>Here's the updated function code:</p>
<pre><code class="language-javascript">    server.registerTool(
        "complete_todo",
        {
            title: "Complete todo",
            description: "Marks a todo as done by id.",
            inputSchema: completeTodoInputSchema,
            _meta: {
                "openai/outputTemplate": "ui://widget/todo.html",
                "openai/toolInvocation/invoking": "Completing todo",
                "openai/toolInvocation/invoked": "Completed todo",
            },
            securitySchemes: [
                { type: "oauth2", scopes: ["todos.write"] }
            ],
            "annotations": {
                "readOnlyHint": false,
                "openWorldHint": false,
                "destructiveHint": true,
            }
        },
        async (args, meta) =&gt; {
            const authorizationHeader = meta.requestInfo.headers?.authorization;
            const authResult = await verifyToken(authorizationHeader);
            if (!authResult?.isValid) {
                return returnAuthErrorResponse(authResult?.error);
            }
            const id = args?.id;
            if (!id) return replyWithTodos("Missing todo id.");
            const { data, error } = await authResult.supabase
                .from("todos")
                .update({ completed: true })
                .eq("id", id)
                .eq("user_id", authResult.user.user.id)
                .select("*");
            if (error) {
                console.error("Error completing todo:", error);
                return returnErrorResponse(error.message);
            }
            if (!data || data.length === 0) {
                return replyWithTodos(`Todo ${id} was not found.`);
            }
            return replyWithTodos(`Completed "${data[0].title}".`, data);
        }
    );
</code></pre>
<p>By applying this, you already have a fully functioning MCP server connected to your Supabase, and you can rely on it to run.</p>
<p>Add the current state of the project to your Git tracker by writing the following:</p>
<ul>
<li><p><code>git add .</code></p>
</li>
<li><p><code>git commit -m "feat: update tools to use database"</code></p>
</li>
</ul>
<h3 id="heading-step-8-testing-the-server-with-supabase">Step 8: Testing the Server with Supabase:</h3>
<p>To do this, you can follow the same steps as in "Testing the OAuth implementation with MCP Server Inspector." But as an extra point, keep an eye on your database table in the Supabase UI, where you can see the added and updated todos. Then you can check the tools, test your todos, and even use ngrok to test them in the ChatGPT UI.</p>
<h2 id="heading-how-to-deploy-your-mcp-server-to-digitalocean">How to Deploy your MCP Server to DigitalOcean</h2>
<p>Since you have your MCP server running and working well, you can now deploy it to DigitalOcean using their App service.</p>
<p>First, upload your code to GitHub and commit it with the following command:</p>
<pre><code class="language-shell">gh repo create todo_mcp_server --private --source=. --remote=upstream

git push
</code></pre>
<p>This command creates a new repo on GitHub, sets your stream to GitHub, and pushes the current branches to GitHub.</p>
<p>Then log in to your DigitalOcean account and go to Apps (<a href="https://cloud.digitalocean.com/apps">https://cloud.digitalocean.com/apps)</a>.</p>
<p>Click on Create app:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/b306ee0f-ccd1-4d3c-b928-cadb0c78d760.png" alt="b306ee0f-ccd1-4d3c-b928-cadb0c78d760" style="display:block;margin:0 auto" width="720" height="303" loading="lazy">

<p>Choose the source as GitHub:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/e6a3c368-d5e4-4529-8e98-22023ca285df.png" alt="Digital Ocean showing how to get a repo from GitHub" style="display:block;margin:0 auto" width="915" height="827" loading="lazy">

<p>Then you need to select your repository and your branch. Write the source directories as: <code>/</code> and <code>mcp_consent</code>. You're doing this because you'll be running two apps: the MCP Server and the consent and login page frontend.</p>
<p>Next, enable auto-deploy if you want the app to update whenever you push your code to GitHub:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/a16462f9-41fb-4412-885e-f91eb6b4958a.png" alt="a16462f9-41fb-4412-885e-f91eb6b4958a" style="display:block;margin:0 auto" width="843" height="801" loading="lazy">

<p>Since we've created two source directories, you'll have two apps and will have to manage them separately.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/71bc8d47-2098-41ca-b168-aba6a3a5e3a9.png" alt="71bc8d47-2098-41ca-b168-aba6a3a5e3a9" style="display:block;margin:0 auto" width="625" height="843" loading="lazy">

<p>You'll use the MCP server in the first app and the frontend for the second app. For that reason, you'll update the network to have the server under the <code>/server</code> route:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/2b58cf42-6ed2-4c91-9395-f3114060d2a9.png" alt="2b58cf42-6ed2-4c91-9395-f3114060d2a9" style="display:block;margin:0 auto" width="630" height="602" loading="lazy">

<p>And you'll downsize the CPU to minimize the cost for this demo:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/d7a57963-7b4d-415b-bd8a-e02ea14b044e.png" alt="d7a57963-7b4d-415b-bd8a-e02ea14b044e" style="display:block;margin:0 auto" width="609" height="636" loading="lazy">

<p>You can update the size later based on your needs for the app.</p>
<p>As for the last step here, you'll update the run command to <code>node server.js</code> to ensure the app is running correctly.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/8a8c0357-bd33-445a-a25a-cb90a3682bdd.png" alt="8a8c0357-bd33-445a-a25a-cb90a3682bdd" style="display:block;margin:0 auto" width="623" height="608" loading="lazy">

<p>For the frontend project, you'll have to click on the second app, update the inputs as well, and add the environment variables:</p>
<p>First, you can downsize the app:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/f82e0305-d3a2-4fc7-bc14-08d6c34a7174.png" alt="f82e0305-d3a2-4fc7-bc14-08d6c34a7174" style="display:block;margin:0 auto" width="618" height="660" loading="lazy">

<p>Update the build and run commands for the Next js server:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/1341f752-85e3-4e94-b816-b3c1413a7b73.png" alt="1341f752-85e3-4e94-b816-b3c1413a7b73" style="display:block;margin:0 auto" width="616" height="615" loading="lazy">

<p>Then you can check the route of this web app and set it as the main one:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/2f4e4e0f-20bf-46c8-ba1e-80ccb9c11167.png" alt="2f4e4e0f-20bf-46c8-ba1e-80ccb9c11167" style="display:block;margin:0 auto" width="626" height="503" loading="lazy">

<p>At the end, you need to add the <code>.env</code> variables from your <code>.env</code> file to the project. You can copy and paste them directly from your <code>mcp_const/.env</code> file to the project.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/4dd9ac38-ed69-4c0d-b5da-eb2dfef41be8.png" alt="4dd9ac38-ed69-4c0d-b5da-eb2dfef41be8" style="display:block;margin:0 auto" width="615" height="623" loading="lazy">

<p>After setting them up, you can create and run the app, which will generate a public URL from DigitalOcean that you can use in the ChatGPT UI again to test it and run the project.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66e02a4159bd66d90e61a22c/ef889fa2-bdd5-47ed-84e7-816cb0c71a30.png" alt="Showing how to copy the link from Digital Ocean" style="display:block;margin:0 auto" width="1258" height="714" loading="lazy">

<p>Before adding the project to test it in ChatGPT, you need to update the consent page URL in Supabase <a href="https://supabase.com/dashboard/project/_/auth/url-configuration">from here</a>.</p>
<p>Instead of having<code>localhost:300</code>, you can add the link from your DigitalOcean account.</p>
<p>At this point, you can test the server with your DigitalOcean by using the following links:</p>
<ul>
<li><p>YOUR_DIGITAL_OCEAN.com/server/mcp</p>
</li>
<li><p>YOUR_DIGITAL_OCEAN.com/login</p>
</li>
<li><p>YOUR_DIGITAL_OCEAN.com/oauth/consent</p>
</li>
</ul>
<h2 id="heading-how-to-publish-your-chatgpt-app">How to Publish Your ChatGPT App</h2>
<p>After running your app, you need to host it. You can simply upload it to GitHub and host it on DigitalOcean as a JavaScript app. You can get the URL from DigitalOcean, then go to your OpenAI <a href="https://platform.openai.com/apps-manage">dashboard</a>, verify yourself as a company or a solo developer, and upload the file there.</p>
<p>To publish your app to ChatGPT, you'll need to provide the following information:</p>
<ul>
<li><p><strong>App Info:</strong> the basic information about your app, including the logo, description, a video demo, website, support, privacy policy, and terms of service URLs (plus a few more details about monetizing your app if you have done that).</p>
</li>
<li><p><strong>MCP Server:</strong> the links to your MCP server, the tools you have, and how you'll use them, plus a verification token for your URL that you'll need to add to your project as a path.</p>
</li>
<li><p><strong>Testing:</strong> you'll need to provide at least 5 test cases for your MCP server so OpenAI can test its functionality. They require you to have coverage over all the major use cases that you intend to support and include all information required to successfully run the test case.</p>
<p>In the tests you share:</p>
<ul>
<li><p>Scenario: where you describe the use case to test (for example, “Research flights”, “Create a slideshow”, “Find a hiking trail”).</p>
</li>
<li><p>User prompt: The exact prompt or interaction you should conduct to begin the test.</p>
</li>
<li><p>Tool triggered: Which tools should be called? You have already implemented them.</p>
</li>
<li><p>Expected output: The output or experience you should expect to receive back from the MCP server.</p>
</li>
<li><p>Then you share the negative cases with the same examples.</p>
</li>
</ul>
</li>
<li><p><strong>Screenshots:</strong>&nbsp;App screenshots for the directory. You can use this public&nbsp;<a href="https://www.figma.com/design/SIiC9BoS6Jkr2oz9JoGFlt/-Public--ChatGPT-Apps---Screenshots?node-id=0-1&amp;p=f&amp;t=npK72eKLrTmXAiZ0-0">Figma</a>&nbsp;to help you with your design. Here, you should upload 1–4 screenshots of your app widget UI in PNG or JPG format, each with a width of 706px and a height of 400–860px (at least one must be 2× retina quality). The first three screenshots are publicly visible in install views across all screen sizes and locales. Ensure the images show only your widget UI – no ChatGPT interface, user prompts, model responses, or embedded text.</p>
</li>
<li><p><strong>Global:</strong> shows the text and localization for your app. You can also select specific countries to publish to.</p>
</li>
<li><p><strong>Submit:</strong> This requires you to write the release notes and run a few compliance checks on your app.</p>
</li>
</ul>
<p>After uploading and updating your project, you can wait for OpenAI to review your project and get the results from them.</p>
<h2 id="heading-what-to-do-next">What to Do Next</h2>
<p>You now have the basic knowledge you need to explore MCP servers and ChatGPT apps. You can dive deeper by reading the documentation and checking the related tools and platforms for building apps like&nbsp;<a href="https://github.com/alpic-ai/skybridge">Skybridge</a>.</p>
<p>If you liked this tutorial, you can follow me on <a href="https://twitter.com/a0m0rajab">Twitter</a> or <a href="https://www.youtube.com/@hadithtech/live">YouTube</a> and run the full project demo script on <a href="https://github.com/a0m0rajab/OpenAi_MCP_Supabase">GitHub</a>.</p>
<h2 id="heading-acknowledgments">Acknowledgments:</h2>
<p>Thanks to <a href="https://www.linkedin.com/in/ahmedmukbilsaleh/">Ahmed Saleh</a> for supporting me with the ChatGPT Apps concept, Abbey from freeCodeCamp for her patience during the editorial process, and the Supabase and OpenAI teams for their awesome work and documentation!</p>
<h2 id="heading-references">References:</h2>
<p>This blog would not have been written without the hard work of the OpenAI, Supabase, and DigitalOcean teams that they put into the following documentation:</p>
<ul>
<li><p><a href="https://docs.digitalocean.com/products/app-platform/how-to/deploy-from-monorepo/">How to Deploy from Monorepos (DigitalOcean)</a></p>
</li>
<li><p><a href="https://developers.openai.com/apps-sdk">OpenAI Apps SDK Documentation</a></p>
</li>
<li><p><a href="https://supabase.com/docs/guides/auth/oauth-server/getting-started?queryGroups=oauth-setup&amp;oauth-setup=programmatically">Supabase OAuth 2.1 Server Documentation</a></p>
</li>
<li><p><a href="https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization">Model Context Protocol Authorization</a></p>
</li>
<li><p><a href="https://github.com/Rodriguespn/mcp-auth-edge">Supabase MCP Demo by Pedro Rodrigues</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an Agentic Terminal Workflow with GitHub Copilot CLI and MCP Servers ]]>
                </title>
                <description>
                    <![CDATA[ Most developers live in their terminal. You run commands, debug pipelines, manage infrastructure, and navigate codebases, all from a shell prompt. But despite how central the terminal is to developer  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-an-agentic-terminal-workflow-with-github-copilot-cli-and-mcp-servers/</link>
                <guid isPermaLink="false">69f212526e0124c05e1857b5</guid>
                
                    <category>
                        <![CDATA[ Developer Tools ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub ]]>
                    </category>
                
                    <category>
                        <![CDATA[ terminal ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mcp ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mcp server ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Caleb Mintoumba ]]>
                </dc:creator>
                <pubDate>Wed, 29 Apr 2026 14:14:42 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/3e4e3d7e-6cbf-4742-a63b-f9a2579f2318.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most developers live in their terminal. You run commands, debug pipelines, manage infrastructure, and navigate codebases, all from a shell prompt.</p>
<p>But despite how central the terminal is to developer workflows, AI assistance there has remained shallow: autocomplete a command here, explain an error there.</p>
<p>That changes when you combine GitHub Copilot CLI with MCP (Model Context Protocol) servers. Instead of an AI that reacts to isolated prompts, you get a terminal that understands your project context, queries live data sources, and chains tool calls autonomously – what the industry is starting to call an agentic workflow.</p>
<p>In this tutorial, you'll learn exactly how to wire these two systems together, step by step. By the end, your terminal will be able to do things like understand your Git history before suggesting a fix, query your running Docker containers before writing a compose patch, or pull live API schemas before generating a request.</p>
<h3 id="heading-table-of-contents">Table of Contents</h3>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-is-github-copilot-cli">What is GitHub Copilot CLI?</a></p>
</li>
<li><p><a href="#heading-what-is-the-model-context-protocol">What is the Model Context Protocol?</a></p>
</li>
<li><p><a href="#heading-how-mcp-servers-work-in-a-terminal-context">How MCP Servers Work in a Terminal Context</a></p>
</li>
<li><p><a href="#heading-step-1-install-and-configure-github-copilot-cli">Step 1 – Install and Configure GitHub Copilot CLI</a></p>
</li>
<li><p><a href="#heading-step-2-set-up-your-first-mcp-server">Step 2 – Set Up Your First MCP Server</a></p>
</li>
<li><p><a href="#heading-step-3-wire-copilot-cli-to-your-mcp-server">Step 3 – Wire Copilot CLI to Your MCP Server</a></p>
</li>
<li><p><a href="#heading-step-4-build-a-real-agentic-workflow">Step 4 – Build a Real Agentic Workflow</a></p>
</li>
<li><p><a href="#heading-step-5-extend-with-multiple-mcp-servers">Step 5 – Extend with Multiple MCP Servers</a></p>
</li>
<li><p><a href="#heading-debugging-common-issues">Debugging Common Issues</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>Before you start, make sure you have the following:</p>
<ul>
<li><p><strong>Node.js</strong> v18 or later (<code>node --version</code>)</p>
</li>
<li><p><strong>npm</strong> v9 or later</p>
</li>
<li><p>A GitHub account with Copilot enabled. The free tier (available to all GitHub users) is sufficient to follow this tutorial. Pro, Business, and Enterprise plans unlock higher usage limits but aren't required.</p>
</li>
<li><p><strong>GitHub CLI</strong> (<code>gh</code>) installed. We'll use it to authenticate.</p>
</li>
<li><p>Basic familiarity with the terminal and JSON configuration files</p>
</li>
<li><p>(Optional) <strong>Docker</strong> installed if you want to follow the Docker MCP example in Step 5</p>
</li>
</ul>
<p>You don't need prior experience with MCP or agentic AI systems, as this guide builds that understanding from the ground up.</p>
<h2 id="heading-what-is-github-copilot-cli">What is GitHub Copilot CLI?</h2>
<p>GitHub Copilot CLI is the terminal-native interface to GitHub's Copilot AI. Unlike the IDE plugin (which assists with code completion), Copilot CLI is designed specifically for shell workflows. It exposes three main commands:</p>
<ul>
<li><p><code>gh copilot suggest</code> proposes a shell command based on a natural language description</p>
</li>
<li><p><code>gh copilot explain</code> explains what a given command does</p>
</li>
<li><p><code>gh copilot alias</code> generates shell aliases for Copilot subcommands</p>
</li>
</ul>
<p>Here's a quick example of <code>suggest</code> in action:</p>
<pre><code class="language-shell">gh copilot suggest "find all files modified in the last 24 hours and larger than 1MB"
</code></pre>
<p>Copilot will return something like:</p>
<pre><code class="language-shell">find . -mtime -1 -size +1M
</code></pre>
<p>It will also ask if you want to copy it, run it directly, or revise the request. This interactive loop is already useful – but by itself, Copilot CLI has no awareness of your project context. It doesn't know your repo structure, your running services, or your deployment environment. That's where MCP comes in.</p>
<h2 id="heading-what-is-the-model-context-protocol">What is the Model Context Protocol?</h2>
<p>The <strong>Model Context Protocol (MCP)</strong> is an open standard introduced by Anthropic in late 2024. Its goal is straightforward: give AI models a standardized way to connect to external tools, data sources, and services.</p>
<p>Think of MCP as a universal adapter layer between an AI model and the real world. Without MCP, each AI integration is custom-built: one plugin for GitHub, another for Postgres, another for Slack, all with incompatible interfaces. MCP defines a single protocol that any tool can implement, and any compatible AI client can consume.</p>
<p>An MCP server exposes <strong>tools</strong> (functions the AI can call), <strong>resources</strong> (data the AI can read), and <strong>prompts</strong> (reusable instruction templates). The AI client in our case, a Copilot-powered terminal discovers these capabilities at runtime and uses them autonomously to complete a task.</p>
<p>A few notable MCP servers that are already production-ready:</p>
<table>
<thead>
<tr>
<th>MCP Server</th>
<th>What it exposes</th>
</tr>
</thead>
<tbody><tr>
<td>@modelcontextprotocol/server-filesystem</td>
<td>Read/write access to local files</td>
</tr>
<tr>
<td>@modelcontextprotocol/server-git</td>
<td>Git log, diff, blame, branch operations</td>
</tr>
<tr>
<td>@modelcontextprotocol/server-github</td>
<td>GitHub Issues, PRs, repos via API</td>
</tr>
<tr>
<td>@modelcontextprotocol/server-postgres</td>
<td>Live query execution on a Postgres DB</td>
</tr>
<tr>
<td>@modelcontextprotocol/server-docker</td>
<td>Container inspection, logs, stats</td>
</tr>
</tbody></table>
<p>The full registry lives at <code>github.com/modelcontextprotocol/servers</code>.</p>
<h2 id="heading-how-mcp-servers-work-in-a-terminal-context">How MCP Servers Work in a Terminal Context</h2>
<p>Before we get hands-on, it's worth understanding the communication model.</p>
<p>MCP servers run as local processes. They communicate with the AI client over <strong>stdio</strong> (standard input/output) or over an <strong>HTTP/SSE transport</strong>. The client sends JSON-RPC messages to the server, and the server responds with structured data.</p>
<p>Here's the simplified flow:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/e1844dc5-a869-4201-ad8a-fd1cb305f646.png" alt="An architectural flowchart illustrating the Model Context Protocol (MCP) workflow. The process starts with a user typing a natural language prompt, passes through the Copilot CLI (the MCP client), communicates via JSON-RPC over stdio with an MCP Server (e.g., server-git), executes real tools like git log, returns a structured result to Copilot, which finally synthesizes a context-aware response for the user." style="display:block;margin:0 auto" width="1408" height="768" loading="lazy">

<p>The key word here is <strong>grounded</strong>. Without MCP, Copilot responds based purely on its training data and your prompt. With MCP, it can call <code>git log --oneline -20</code> before answering your question about recent regressions and its answer is based on <em>your actual code history</em>, not a generalized assumption.</p>
<h3 id="heading-step-1-install-and-configure-github-copilot-cli">Step 1 – Install and Configure GitHub Copilot CLI</h3>
<p>If you haven't already, install the GitHub CLI:</p>
<pre><code class="language-shell"># macOS
brew install gh

# Ubuntu/Debian
sudo apt install gh

# Windows (via winget)
winget install --id GitHub.cli
</code></pre>
<p>Then authenticate:</p>
<pre><code class="language-shell">gh auth login
</code></pre>
<p>Follow the interactive prompts. Select <strong>GitHub.com</strong>, then <strong>HTTPS</strong>, and authenticate via browser when prompted.</p>
<p>Now install the Copilot CLI extension:</p>
<pre><code class="language-shell">gh extension install github/gh-copilot
</code></pre>
<p>Verify the installation:</p>
<pre><code class="language-shell">gh copilot --version
</code></pre>
<p>You should see output like <code>gh-copilot version 1.x.x</code>.</p>
<p><strong>Optional but recommended: set up shell aliases.</strong> This makes the workflow much faster. For <code>bash</code> or <code>zsh</code>:</p>
<pre><code class="language-shell"># Add to your ~/.bashrc or ~/.zshrc
eval "$(gh copilot alias -- bash)"   # for bash
eval "$(gh copilot alias -- zsh)"    # for zsh
</code></pre>
<p>After reloading your shell (<code>source ~/.bashrc</code>), you can use <code>ghcs</code> as shorthand for <code>gh copilot suggest</code> and <code>ghce</code> for <code>gh copilot explain</code>.</p>
<h3 id="heading-step-2-set-up-your-first-mcp-server">Step 2 – Set Up Your First MCP Server</h3>
<p>We'll start with <code>server-git</code>. It's the most immediately useful for a development workflow and has zero external dependencies.</p>
<p>Install it globally via npm:</p>
<pre><code class="language-shell">npm install -g @modelcontextprotocol/server-git
</code></pre>
<p>Test that it runs:</p>
<pre><code class="language-shell">mcp-server-git --version
</code></pre>
<p>This server exposes the following tools to any compatible MCP client:</p>
<ul>
<li><p><code>git_log</code> retrieve commit history with filters</p>
</li>
<li><p><code>git_diff</code> diff between branches or commits</p>
</li>
<li><p><code>git_status</code> current working tree status</p>
</li>
<li><p><code>git_show</code> inspect a specific commit</p>
</li>
<li><p><code>git_blame</code> annotate file lines with commit info</p>
</li>
<li><p><code>git_branch</code> list or switch branches</p>
</li>
</ul>
<p>Now create a configuration file. MCP clients look for a file called <code>mcp.json</code> to discover available servers. Create it in your project root or in a global config directory:</p>
<pre><code class="language-shell">mkdir -p ~/.config/mcp
touch ~/.config/mcp/mcp.json
</code></pre>
<p>Add the following content:</p>
<pre><code class="language-markdown">{
  "mcpServers": {
    "git": {
      "command": "mcp-server-git",
      "args": ["--repository", "."],
      "transport": "stdio"
    }
  }
}
</code></pre>
<p>A few notes on this config:</p>
<ul>
<li><p><code>command</code> is the binary to run. Make sure it's on your <code>$PATH</code>.</p>
</li>
<li><p><code>args</code> passes <code>--repository .</code> so the server scopes itself to the current working directory.</p>
</li>
<li><p><code>transport: "stdio"</code> means communication happens over standard input/output the simplest and most stable option for local servers.</p>
</li>
</ul>
<h3 id="heading-step-3-wire-copilot-cli-to-your-mcp-server">Step 3 – Wire Copilot CLI to Your MCP Server</h3>
<p>This is where the two systems connect. GitHub Copilot CLI supports MCP via its <code>--mcp-config</code> flag (available from version 1.3+). You point it at your <code>mcp.json</code>, and Copilot will automatically initialize the declared servers before processing your prompt.</p>
<p>Here's the basic invocation:</p>
<pre><code class="language-shell">gh copilot suggest --mcp-config ~/.config/mcp/mcp.json "why did the build break in the last commit?"
</code></pre>
<p>When you run this inside a Git repository, Copilot CLI will:</p>
<ol>
<li><p>Start the <code>mcp-server-git</code> process</p>
</li>
<li><p>Call <code>git_log</code> to retrieve recent commits</p>
</li>
<li><p>Call <code>git_diff</code> on the most recent commit</p>
</li>
<li><p>Synthesize an answer based on the actual diff output</p>
</li>
</ol>
<p>Try it yourself on a repo with a recent failing commit. The difference in response quality compared to a plain <code>gh copilot suggest</code> is immediately obvious.</p>
<p><strong>Tip: avoid retyping the flag every time.</strong> Add a shell function to your <code>.bashrc</code>/<code>.zshrc</code>:</p>
<pre><code class="language-shell">function aterm() {
  gh copilot suggest --mcp-config ~/.config/mcp/mcp.json "$@"
}
</code></pre>
<p>Now you just type:</p>
<pre><code class="language-shell">aterm "what changed between main and feature/auth?"
</code></pre>
<p>And you're running a fully context-aware, MCP-powered query from a single short command. This function name <code>aterm</code> for <em>agentic terminal</em> is what we'll use throughout the rest of this tutorial.</p>
<h3 id="heading-step-4-build-a-real-agentic-workflow">Step 4 – Build a Real Agentic Workflow</h3>
<p>Let's move beyond individual queries and build a workflow that chains multiple tool calls to complete a real developer task: <strong>diagnosing a regression</strong>.</p>
<p>Imagine you pushed a feature branch and your CI pipeline failed. You don't know exactly which change caused it. Here's how your agentic terminal handles it:</p>
<h4 id="heading-query-1-understand-what-changed">Query 1: understand what changed</h4>
<pre><code class="language-shell">aterm "summarize all commits on feature/auth that aren't on main yet"
</code></pre>
<p>Copilot calls <code>git_log</code> with branch filters, then returns a structured summary of commits unique to your branch. No copy-pasting SHAs manually.</p>
<h4 id="heading-query-2-isolate-the-diff">Query 2: isolate the diff</h4>
<pre><code class="language-shell">aterm "show me everything that changed in the auth middleware between main and feature/auth"
</code></pre>
<p>This triggers <code>git_diff</code> scoped to the path containing your middleware. Copilot returns the diff with an explanation of what each change does.</p>
<h4 id="heading-query-3-find-the-likely-culprit">Query 3: find the likely culprit</h4>
<pre><code class="language-shell">aterm "which of those changes could cause a JWT validation failure?"
</code></pre>
<p>At this point, Copilot has the diff in its context window from the previous tool calls. It reasons over the actual code changes not generic knowledge about JWT and pinpoints the likely issue.</p>
<h4 id="heading-query-4-generate-the-fix">Query 4: generate the fix</h4>
<pre><code class="language-shell">aterm "write the corrected version of that validation function"
</code></pre>
<p>Copilot generates a targeted fix based on the specific code it retrieved via MCP. You get a patch you can directly apply, not a generic code template.</p>
<p>This four-step sequence – understand, isolate, reason, fix – is a complete agentic loop. Each step is grounded in live repository data retrieved through MCP tools. The AI is not hallucinating context. Instead, it's reading your actual codebase.</p>
<h3 id="heading-step-5-extend-with-multiple-mcp-servers">Step 5 – Extend with Multiple MCP Servers</h3>
<p>One MCP server is useful. Multiple MCP servers working together is where the workflow becomes genuinely powerful. Let's add two more: <code>server-filesystem</code> and <code>server-docker</code>.</p>
<p>Install the additional servers:</p>
<pre><code class="language-shell">npm install -g @modelcontextprotocol/server-filesystem
npm install -g @modelcontextprotocol/server-docker
</code></pre>
<p>Update your <code>mcp.json</code>:</p>
<pre><code class="language-markdown">{
  "mcpServers": {
    "git": {
      "command": "mcp-server-git",
      "args": ["--repository", "."],
      "transport": "stdio"
    },
    "filesystem": {
      "command": "mcp-server-filesystem",
      "args": ["--root", "."],
      "transport": "stdio"
    },
    "docker": {
      "command": "mcp-server-docker",
      "transport": "stdio"
    }
  }
}
</code></pre>
<p>With all three servers active, your terminal can now answer cross-domain questions:</p>
<pre><code class="language-shell">aterm "my Express app container keeps restarting, check the logs and compare with what the healthcheck in my Dockerfile expects"
</code></pre>
<p>To answer this, Copilot will:</p>
<ol>
<li><p>Call <code>docker_logs</code> (server-docker) to pull the container's recent stderr output</p>
</li>
<li><p>Call <code>read_file</code> (server-filesystem) to read your <code>Dockerfile</code></p>
</li>
<li><p>Parse the <code>HEALTHCHECK</code> instruction</p>
</li>
<li><p>Cross-reference the log errors with the health endpoint path</p>
</li>
<li><p>Return a diagnosis explaining the mismatch and suggest the fix</p>
</li>
</ol>
<p>This is an <strong>agentic workflow</strong>: the model autonomously decides which tools to call, in what order, and synthesizes the results into a coherent answer. You didn't tell it to read the Dockerfile. It inferred that was necessary based on your question.</p>
<p><strong>A note on security:</strong> When running <code>server-filesystem</code>, always scope it to a specific directory using <code>--root</code>. Never point it at <code>/</code> or your home directory. Similarly, <code>server-docker</code> has access to your Docker socket run it only in trusted environments.</p>
<h2 id="heading-debugging-common-issues">Debugging Common Issues</h2>
<p><code>mcp-server-git: command not found</code></p>
<p>The npm global bin directory isn't on your <code>$PATH</code>. Fix:</p>
<pre><code class="language-shell">export PATH="\(PATH:\)(npm bin -g)"
# or for newer npm versions:
export PATH="\(PATH:\)(npm prefix -g)/bin"
</code></pre>
<p>Add this line to your <code>.bashrc</code>/<code>.zshrc</code> to persist it.</p>
<h4 id="heading-copilot-cli-doesnt-seem-to-be-using-mcp-tools">Copilot CLI doesn't seem to be using MCP tools</h4>
<p>Check your Copilot CLI version:</p>
<pre><code class="language-shell">gh copilot --version
</code></pre>
<p>MCP support requires version 1.3 or later. Update with:</p>
<pre><code class="language-shell">gh extension upgrade copilot
</code></pre>
<p>Also verify your <code>mcp.json</code> is valid JSON a trailing comma or missing bracket will silently prevent server initialization.</p>
<h4 id="heading-mcp-server-starts-but-returns-no-data">MCP server starts but returns no data</h4>
<p>Run the server manually to check for errors:</p>
<pre><code class="language-shell">mcp-server-git --repository .
</code></pre>
<p>If it exits immediately, check that you're running the command inside a valid Git repository. For <code>server-docker</code>, make sure the Docker daemon is running and your user has access to the Docker socket:</p>
<pre><code class="language-shell">sudo usermod -aG docker $USER
# Then log out and back in
</code></pre>
<h4 id="heading-responses-are-slow-with-multiple-servers">Responses are slow with multiple servers</h4>
<p>Each MCP server is a separate subprocess. Spawning several at once adds startup latency, especially on slower machines. Two optimizations:</p>
<ol>
<li><p>Only declare the servers you actually need for a given project in your <code>mcp.json</code></p>
</li>
<li><p>Use project-specific config files instead of one global config:</p>
</li>
</ol>
<pre><code class="language-shell"># project A (backend)
aterm --mcp-config ./mcp-backend.json "..."

# project B (infra)
aterm --mcp-config ./mcp-infra.json "..."
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've just built an agentic terminal workflow from scratch. Here's a quick recap of what you did:</p>
<ul>
<li><p>Installed and configured GitHub Copilot CLI with shell aliases for fast access</p>
</li>
<li><p>Set up MCP servers (<code>server-git</code>, <code>server-filesystem</code>, <code>server-docker</code>) and wired them through a <code>mcp.json</code> config</p>
</li>
<li><p>Created a shell function (<code>aterm</code>) that transparently passes your MCP config to every Copilot query</p>
</li>
<li><p>Built a multi-step agentic loop for diagnosing regressions using live Git data</p>
</li>
<li><p>Extended the setup with cross-domain tool orchestration across Git, filesystem, and Docker</p>
</li>
</ul>
<p>The architecture you've built here is not a demo – it's a production-ready pattern. You can extend it with any MCP-compatible server: <code>server-postgres</code> for database-aware queries, <code>server-github</code> for issue and PR context, or custom MCP servers you write yourself for your internal APIs.</p>
<p>The terminal has always been the most powerful surface in a developer's environment. With Copilot CLI and MCP, it's finally becoming an intelligent one.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an MCP Server with Python, Docker, and Claude Code ]]>
                </title>
                <description>
                    <![CDATA[ Every MCP tutorial I've found so far has followed the same basic script: build a server, point Claude Desktop at it, screenshot the chat window, done. This is fine if you want a demo. But it's not fin ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-an-mcp-server-with-python-docker-and-claude-code/</link>
                <guid isPermaLink="false">69b09018abc0d95001a8f07f</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ML ]]>
                    </category>
                
                    <category>
                        <![CDATA[ claude.ai ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mcp ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mcp server ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Balajee Asish Brahmandam ]]>
                </dc:creator>
                <pubDate>Tue, 10 Mar 2026 21:41:44 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/02826050-87fa-42cb-8167-73bca4b42616.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Every MCP tutorial I've found so far has followed the same basic script: build a server, point Claude Desktop at it, screenshot the chat window, done.</p>
<p>This is fine if you want a demo. But it's not fine if you want something you can ship, defend in an interview, or hand to another developer without a README that starts with "first, install this Electron app."</p>
<p>So I built an MCP server in Python, containerized it with Docker, and wired it into Claude Code – all from the terminal, no GUI required.</p>
<p>This article walks through the full loop in one afternoon: what MCP actually is, why it matters now that OpenAI and Google have adopted it, the real security problems nobody puts in their tutorial (complete with CVEs), and every command you need to go from an empty directory to a working tool.</p>
<p>If you're between jobs and need a portfolio project that shows you understand how AI tooling actually works under the hood, this is the one.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="https://claude.ai/chat/1a92e709-4c86-4c9a-8fa3-b1533b9d21a5#what-you-will-build">What You Will Build</a></p>
</li>
<li><p><a href="https://claude.ai/chat/1a92e709-4c86-4c9a-8fa3-b1533b9d21a5#prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="https://claude.ai/chat/1a92e709-4c86-4c9a-8fa3-b1533b9d21a5#what-is-mcp-and-why-should-you-care">What is MCP (and Why Should You Care)?</a></p>
</li>
<li><p><a href="https://claude.ai/chat/1a92e709-4c86-4c9a-8fa3-b1533b9d21a5#why-claude-code-instead-of-claude-desktop">Why Claude Code Instead of Claude Desktop?</a></p>
</li>
<li><p><a href="https://claude.ai/chat/1a92e709-4c86-4c9a-8fa3-b1533b9d21a5#step-1-build-the-mcp-server">Step 1: Build the MCP Server</a></p>
</li>
<li><p><a href="https://claude.ai/chat/1a92e709-4c86-4c9a-8fa3-b1533b9d21a5#step-2-test-it-locally">Step 2: Test It Locally</a></p>
</li>
<li><p><a href="https://claude.ai/chat/1a92e709-4c86-4c9a-8fa3-b1533b9d21a5#step-3-dockerize-it">Step 3: Dockerize It</a></p>
</li>
<li><p><a href="https://claude.ai/chat/1a92e709-4c86-4c9a-8fa3-b1533b9d21a5#step-4-wire-it-into-claude-code">Step 4: Wire It Into Claude Code</a></p>
</li>
<li><p><a href="https://claude.ai/chat/1a92e709-4c86-4c9a-8fa3-b1533b9d21a5#step-5-use-it">Step 5: Use It</a></p>
</li>
<li><p><a href="https://claude.ai/chat/1a92e709-4c86-4c9a-8fa3-b1533b9d21a5#security-what-the-other-tutorials-leave-out">Security: What the Other Tutorials Leave Out</a></p>
</li>
<li><p><a href="https://claude.ai/chat/1a92e709-4c86-4c9a-8fa3-b1533b9d21a5#what-to-do-next">What to Do Next</a></p>
</li>
<li><p><a href="https://claude.ai/chat/1a92e709-4c86-4c9a-8fa3-b1533b9d21a5#wrapping-up">Wrapping Up</a></p>
</li>
</ul>
<h2 id="heading-what-you-will-build">What You Will Build</h2>
<p>By the end of this tutorial, you will have:</p>
<ul>
<li><p>A Python MCP server that exposes custom tools to any MCP-compatible AI client</p>
</li>
<li><p>A Docker container that packages the server for reproducible deployment</p>
</li>
<li><p>A working connection between that container and Claude Code in your terminal</p>
</li>
<li><p>An understanding of the security risks involved and how to mitigate the worst of them</p>
</li>
</ul>
<p>The server we are building is a <strong>project scaffolder</strong>. You give it a project name and a language, and it generates a starter directory structure with the right files. It's simple enough to build in an afternoon, but useful enough to actually put on your résumé.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You will need the following installed on your machine:</p>
<ul>
<li><p><strong>Python 3.10+</strong> (check with <code>python3 --version</code>)</p>
</li>
<li><p><strong>Docker</strong> (check with <code>docker --version</code>)</p>
</li>
<li><p><strong>Claude Code</strong> with an active Claude Pro, Max, or API plan (check with <code>claude --version</code>)</p>
</li>
<li><p><strong>Node.js 20+</strong> (required by Claude Code – check with <code>node --version</code>)</p>
</li>
<li><p>A terminal you are comfortable in</p>
</li>
</ul>
<p>If you don't have Claude Code installed yet, follow the <a href="https://code.claude.com/docs/en/getting-started">official installation instructions</a>. The npm installation method is deprecated, so make sure you use the native binary installer instead.</p>
<h2 id="heading-what-is-mcp-and-why-should-you-care">What is MCP (and Why Should You Care)?</h2>
<p>The Model Context Protocol (MCP) is an open standard that lets AI models connect to external tools and data sources. Anthropic released it in November 2024, and within a year it became the default way to extend what an LLM can do. OpenAI adopted it in March 2025. Google DeepMind followed in April. The protocol now has over 97 million monthly SDK downloads and more than 10,000 active servers.</p>
<p>The easiest way to think about MCP is as a USB-C port for AI. Before MCP, every AI provider had its own way of calling tools. OpenAI had function calling. Google had their own format. If you wanted your tool to work with multiple models, you had to implement it multiple times. MCP gives you one interface that works everywhere.</p>
<p>Here is how the pieces fit together:</p>
<ul>
<li><p>An <strong>MCP server</strong> exposes tools, resources, and prompts. It is your code.</p>
</li>
<li><p>An <strong>MCP client</strong> (like Claude Code, Claude Desktop, or Cursor) discovers those tools and calls them on behalf of the LLM.</p>
</li>
<li><p>The <strong>transport</strong> is how they communicate. For local servers, that's usually stdio (standard input/output). For remote servers, it's HTTP.</p>
</li>
</ul>
<p>When you type a message in Claude Code and it decides to use one of your tools, here is what happens: Claude Code sends a JSON-RPC 2.0 message to your server over stdin, your server executes the tool and writes the result to stdout, and Claude Code reads it back. The LLM never talks to your server directly. The client is always in the middle.</p>
<p>If you want the deeper architecture breakdown, freeCodeCamp already has a <a href="https://www.freecodecamp.org/news/how-does-an-mcp-work-under-the-hood/">solid explainer on how MCP works under the hood</a>. Here, I will focus on building.</p>
<h2 id="heading-why-claude-code-instead-of-claude-desktop">Why Claude Code Instead of Claude Desktop?</h2>
<p>Most MCP tutorials use Claude Desktop as the client. That works, but Claude Code has a few advantages for developers:</p>
<ol>
<li><p><strong>It lives in your terminal.</strong> No GUI to configure. No JSON files to hand-edit in hidden config directories. You add an MCP server with one command and you are done.</p>
</li>
<li><p><strong>It's already where you code.</strong> If you're writing the server, testing it, and connecting it, doing all of that in the same terminal session cuts the context switching.</p>
</li>
<li><p><strong>It works on headless machines.</strong> If you're SSHing into a dev box or running in CI, Claude Desktop isn't an option. Claude Code is.</p>
</li>
<li><p><strong>It's also an MCP server itself.</strong> Claude Code can expose its own tools (file reading, writing, shell commands) to other MCP clients via <code>claude mcp serve</code>. That's a neat trick we won't use today, but it's worth knowing about.</p>
</li>
</ol>
<p>The relevant commands:</p>
<pre><code class="language-bash"># Add an MCP server
claude mcp add &lt;name&gt; -- &lt;command&gt;

# List configured servers
claude mcp list

# Remove a server
claude mcp remove &lt;name&gt;

# Check MCP status inside Claude Code
/mcp
</code></pre>
<h2 id="heading-step-1-build-the-mcp-server">Step 1: Build the MCP Server</h2>
<p>We're using <a href="https://github.com/jlowin/fastmcp">FastMCP</a>, a Python framework that handles all the protocol plumbing so you can focus on your tools. Create a new project directory and set it up:</p>
<pre><code class="language-bash">mkdir mcp-scaffolder &amp;&amp; cd mcp-scaffolder
python3 -m venv .venv
source .venv/bin/activate
pip install "mcp[cli]&gt;=1.25,&lt;2"
</code></pre>
<p>Why pin the version? The MCP Python SDK v2.0 is in development and will change the transport layer significantly. Pinning to &gt;=1.25,&lt;2 keeps your server working until you're ready to migrate.</p>
<p>Now create <code>server.py</code>:</p>
<pre><code class="language-python"># server.py
from mcp.server.fastmcp import FastMCP
import os
import json

mcp = FastMCP("project-scaffolder")

# Templates for different languages
TEMPLATES = {
    "python": {
        "files": {
            "main.py": '"""Entry point."""\n\n\ndef main():\n    print("Hello, world!")\n\n\nif __name__ == "__main__":\n    main()\n',
            "requirements.txt": "",
            "README.md": "# {name}\n\nA Python project.\n\n## Setup\n\n```bash\npip install -r requirements.txt\npython main.py\n```\n",
            ".gitignore": "__pycache__/\n*.pyc\n.venv/\n",
        },
        "dirs": ["tests"],
    },
    "node": {
        "files": {
            "index.js": 'console.log("Hello, world!");\n',
            "package.json": '{{\n  "name": "{name}",\n  "version": "1.0.0",\n  "main": "index.js"\n}}\n',
            "README.md": "# {name}\n\nA Node.js project.\n\n## Setup\n\n```bash\nnpm install\nnode index.js\n```\n",
            ".gitignore": "node_modules/\n",
        },
        "dirs": [],
    },
    "go": {
        "files": {
            "main.go": 'package main\n\nimport "fmt"\n\nfunc main() {{\n\tfmt.Println("Hello, world!")\n}}\n',
            "go.mod": "module {name}\n\ngo 1.21\n",
            "README.md": "# {name}\n\nA Go project.\n\n## Setup\n\n```bash\ngo run main.go\n```\n",
            ".gitignore": "bin/\n",
        },
        "dirs": ["cmd", "internal"],
    },
}


@mcp.tool()
def scaffold_project(name: str, language: str) -&gt; str:
    """Create a new project directory structure.

    Args:
        name: The project name (used as the directory name)
        language: The programming language - one of: python, node, go
    """
    language = language.lower().strip()

    if language not in TEMPLATES:
        return json.dumps({
            "error": f"Unsupported language: {language}",
            "supported": list(TEMPLATES.keys()),
        })

    template = TEMPLATES[language]
    base_path = os.path.join(os.getcwd(), name)

    if os.path.exists(base_path):
        return json.dumps({
            "error": f"Directory already exists: {name}",
        })

    # Create the project directory
    os.makedirs(base_path, exist_ok=True)

    # Create subdirectories
    for dir_name in template["dirs"]:
        os.makedirs(os.path.join(base_path, dir_name), exist_ok=True)

    # Create files
    created_files = []
    for filename, content in template["files"].items():
        filepath = os.path.join(base_path, filename)
        formatted_content = content.replace("{name}", name)
        with open(filepath, "w") as f:
            f.write(formatted_content)
        created_files.append(filename)

    return json.dumps({
        "status": "created",
        "path": base_path,
        "language": language,
        "files": created_files,
        "directories": template["dirs"],
    })


@mcp.tool()
def list_templates() -&gt; str:
    """List all available project templates and their contents."""
    result = {}
    for lang, template in TEMPLATES.items():
        result[lang] = {
            "files": list(template["files"].keys()),
            "directories": template["dirs"],
        }
    return json.dumps(result, indent=2)


if __name__ == "__main__":
    mcp.run(transport="stdio")
</code></pre>
<p>A few things to notice about this code:</p>
<p>Tools return strings. MCP tools communicate through text. I'm returning JSON strings so the LLM can parse the results reliably. You could return plain text, but structured data gives the model more to work with.</p>
<p>The <code>@mcp.tool()</code> decorator does the heavy lifting. FastMCP reads your function signature and docstring to generate the JSON schema that tells the LLM what this tool does, what arguments it takes, and what types they are. Good docstrings aren't optional here – they're how the LLM decides whether to call your tool.</p>
<p><code>transport="stdio"</code> is the key line. This tells FastMCP to communicate over standard input/output, which is what Claude Code expects for local servers.</p>
<h2 id="heading-step-2-test-it-locally">Step 2: Test It Locally</h2>
<p>Before we Dockerize anything, make sure the server actually works:</p>
<pre><code class="language-bash"># Quick smoke test - the server should start without errors
python server.py
</code></pre>
<p>You should see... nothing. That is correct. An MCP server over stdio just sits there waiting for JSON-RPC messages on stdin. Press <code>Ctrl+C</code> to stop it.</p>
<p>For a proper test, use the MCP Inspector (Anthropic's debugging tool):</p>
<pre><code class="language-bash"># Install and run the inspector
npx @modelcontextprotocol/inspector python server.py
</code></pre>
<p>This opens a web interface where you can see your tools, call them manually, and inspect the JSON-RPC messages going back and forth. Verify that both <code>scaffold_project</code> and <code>list_templates</code> show up and return sensible results.</p>
<p><strong>Here's a debugging tip that will save you time:</strong> If your MCP server logs anything to stdout, it will corrupt the JSON-RPC stream and the client will disconnect. Use stderr for all logging: <code>print("debug info", file=sys.stderr)</code>. This is the single most common source of "my server connects but then immediately fails" bugs. The New Stack called stdio transport "incredibly fragile" for exactly this reason.</p>
<h2 id="heading-step-3-dockerize-it">Step 3: Dockerize It</h2>
<p>Create a <code>Dockerfile</code> in your project root:</p>
<pre><code class="language-dockerfile">FROM python:3.12-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy server code
COPY server.py .

# MCP servers over stdio need unbuffered output
ENV PYTHONUNBUFFERED=1

# The server reads from stdin and writes to stdout
CMD ["python", "server.py"]
</code></pre>
<p>Create <code>requirements.txt</code>:</p>
<pre><code class="language-plaintext">mcp[cli]&gt;=1.25,&lt;2
</code></pre>
<p>Build and verify:</p>
<pre><code class="language-bash">docker build -t mcp-scaffolder .

# Quick test - should start without errors
docker run -i mcp-scaffolder
</code></pre>
<p>Again, you'll see nothing because the server is waiting for input. <code>Ctrl+C</code> to stop.</p>
<p>Two things matter in this Dockerfile:</p>
<ol>
<li><p><code>PYTHONUNBUFFERED=1</code> <strong>is critical.</strong> Without it, Python buffers stdout, and the MCP client may hang waiting for responses that are sitting in a buffer. This is one of those bugs that works fine in local testing and breaks in Docker.</p>
</li>
<li><p><code>docker run -i</code> <strong>(interactive mode) is required.</strong> The <code>-i</code> flag keeps stdin open so the MCP client can send messages to the container. Without it, the server gets an immediate EOF and exits.</p>
</li>
</ol>
<h2 id="heading-step-4-wire-it-into-claude-code">Step 4: Wire It Into Claude Code</h2>
<p>Now connect your Docker container to Claude Code:</p>
<pre><code class="language-bash">claude mcp add scaffolder -- docker run -i --rm mcp-scaffolder
</code></pre>
<p>That's the whole command. Let me break it down:</p>
<ul>
<li><p><code>claude mcp add</code> registers a new MCP server</p>
</li>
<li><p><code>scaffolder</code> is the name you will reference it by</p>
</li>
<li><p>Everything after <code>--</code> is the command Claude Code runs to start the server</p>
</li>
<li><p><code>docker run -i --rm mcp-scaffolder</code> starts the container with interactive stdin and removes it when done</p>
</li>
</ul>
<p>Verify that it registered:</p>
<pre><code class="language-bash">claude mcp list
</code></pre>
<p>You should see <code>scaffolder</code> in the output with a <code>stdio</code> transport type.</p>
<p>Now launch Claude Code and check the connection:</p>
<pre><code class="language-bash">claude
</code></pre>
<p>Once inside Claude Code, type <code>/mcp</code> to see the status of your MCP servers. You should see <code>scaffolder</code> listed as connected with two tools available.</p>
<h2 id="heading-step-5-use-it">Step 5: Use It</h2>
<p>Still inside Claude Code, try it out:</p>
<pre><code class="language-plaintext">Create a new Python project called "weather-api"
</code></pre>
<p>Claude Code should discover your <code>scaffold_project</code> tool, call it with <code>name="weather-api"</code> and <code>language="python"</code>, and report back what it created. Check your filesystem and you should see the full project structure.</p>
<p>Try a few more:</p>
<pre><code class="language-plaintext">What project templates are available?
</code></pre>
<pre><code class="language-plaintext">Scaffold a Go project called "url-shortener"
</code></pre>
<p>If Claude Code doesn't pick up your tools, run <code>/mcp</code> to check the connection status. If it shows as disconnected, the most common causes are that the Docker image failed to build, stdout is being polluted (check for stray print statements), or the Docker daemon is not running.</p>
<h2 id="heading-security-what-the-other-tutorials-leave-out">Security: What the Other Tutorials Leave Out</h2>
<p>This is the section most MCP tutorials skip. They should not. MCP has had real security incidents, not theoretical ones, and understanding them makes you a better developer.</p>
<h3 id="heading-the-prompt-injection-problem">The Prompt Injection Problem</h3>
<p>MCP servers execute code on your machine based on what an LLM decides to do. If an attacker can influence what the LLM sees, they can influence what your server does. This is called prompt injection, and it is the number one unsolved security problem in the MCP ecosystem.</p>
<p>In May 2025, researchers at Invariant Labs demonstrated this against the official GitHub MCP server. They created a malicious GitHub issue that, when read by an AI agent, hijacked the agent into leaking private repository data (including salary information) into a public pull request. The root cause was an overly broad Personal Access Token combined with untrusted content landing in the LLM's context window.</p>
<p>This was not a contrived lab demo. It used the official GitHub MCP server, the kind of thing people install from the MCP server directory without a second thought.</p>
<h3 id="heading-real-cves-not-theory">Real CVEs, Not Theory</h3>
<p>The ecosystem has accumulated real vulnerability reports:</p>
<ul>
<li><p><strong>CVE-2025-6514:</strong> A critical command-injection bug in <code>mcp-remote</code>, a popular OAuth proxy that 437,000+ environments used. An attacker could execute arbitrary OS commands through crafted OAuth redirect URIs.</p>
</li>
<li><p><strong>CVE-2025-6515:</strong> Session hijacking in <code>oatpp-mcp</code> through predictable session IDs, letting attackers inject prompts into other users' sessions.</p>
</li>
<li><p><strong>MCP Inspector RCE:</strong> Anthropic's own debugging tool allowed unauthenticated remote code execution. Inspecting a malicious server meant giving the attacker a shell on your machine.</p>
</li>
</ul>
<p>An Equixly security assessment found command injection in 43% of tested MCP server implementations. Nearly a third were vulnerable to server-side request forgery.</p>
<h3 id="heading-what-you-should-actually-do">What You Should Actually Do</h3>
<p>For the server we built today, here is what matters:</p>
<h4 id="heading-limit-file-system-access">Limit file system access</h4>
<p>Our Docker container doesn't mount your home directory. That's intentional. If you need the server to write files to your host, mount only the specific directory you need: <code>docker run -i --rm -v $(pwd)/projects:/app/projects mcp-scaffolder</code>. Never mount <code>/</code> or <code>~</code>.</p>
<h4 id="heading-validate-all-inputs">Validate all inputs</h4>
<p>Our <code>scaffold_project</code> tool checks that the language is in a known list and that the directory does not already exist. But think about what happens if someone passes <code>name="../../etc/passwd"</code> as the project name. Path traversal is the kind of thing you need to catch. Add this to the tool:</p>
<pre><code class="language-python"># Add this validation at the top of scaffold_project
if ".." in name or "/" in name or "\\" in name:
    return json.dumps({"error": "Invalid project name"})
</code></pre>
<h4 id="heading-use-least-privilege-tokens">Use least-privilege tokens</h4>
<p>If your MCP server connects to an API, give it the minimum permissions it needs. The GitHub MCP incident happened because the PAT had access to every private repo. A read-only token scoped to one repo would have contained the blast radius.</p>
<h4 id="heading-do-not-install-mcp-servers-from-untrusted-sources">Do not install MCP servers from untrusted sources</h4>
<p>A malicious npm package posing as a "Postmark MCP Server" was caught silently BCC'ing all emails to an attacker's address. Treat MCP server packages with the same caution you would give any code that runs on your machine with your permissions.</p>
<h2 id="heading-what-to-do-next">What to Do Next</h2>
<p>You have a working MCP server in a Docker container, connected to Claude Code. Here is how to make it portfolio-ready:</p>
<ol>
<li><p><strong>Add more tools:</strong> The scaffolder is a starting point. Add a tool that reads a project's dependency file and lists outdated packages. Add one that generates a Dockerfile for an existing project. Each tool is a function with a decorator – the pattern is the same every time.</p>
</li>
<li><p><strong>Add tests:</strong> Write pytest tests that call your tool functions directly and verify the output. MCP tools are just Python functions. Test them like Python functions.</p>
</li>
<li><p><strong>Push the Docker image:</strong> Tag it and push to Docker Hub or GitHub Container Registry. Then your <code>claude mcp add</code> command becomes <code>claude mcp add scaffolder -- docker run -i --rm yourusername/mcp-scaffolder:latest</code> and anyone can use it.</p>
</li>
<li><p><strong>Write a README that explains the security model:</strong> What permissions does your server need? What file system access? What happens if inputs are malicious? Answering these questions in your README signals that you think about security, which is exactly what hiring managers are looking for right now.</p>
</li>
</ol>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>We built a Python MCP server with FastMCP, containerized it with Docker, and connected it to Claude Code. The whole thing fits in about 100 lines of Python, a six-line Dockerfile, and one <code>claude mcp add</code> command.</p>
<p>The MCP ecosystem is real and growing fast. The protocol has the backing of Anthropic, OpenAI, and Google. It's now governed by the Linux Foundation. But it's also young, and the security story is still being written. Build with it, but build with your eyes open.</p>
<p>If you want to go deeper, here are the resources I found most useful:</p>
<ul>
<li><p><a href="https://modelcontextprotocol.io/specification/2025-11-25">MCP specification</a>: the actual protocol docs</p>
</li>
<li><p><a href="https://code.claude.com/docs/en/mcp">Claude Code MCP documentation</a>: how Claude Code implements MCP</p>
</li>
<li><p><a href="https://github.com/jlowin/fastmcp">FastMCP GitHub</a>: the Python framework we used</p>
</li>
<li><p><a href="https://authzed.com/blog/timeline-mcp-breaches">AuthZed's timeline of MCP security incidents</a>: required reading if you are building MCP servers for production</p>
</li>
<li><p><a href="https://simonwillison.net/2025/Apr/9/mcp-prompt-injection/">Simon Willison on MCP prompt injection</a>: the clearest explanation of why this is hard to solve</p>
</li>
</ul>
<p>The complete source code for this tutorial is on <a href="https://github.com/balajeeasish/ai-workshop/tree/main/mcp-server">GitHub</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build MCP Servers for Your Internal Data ]]>
                </title>
                <description>
                    <![CDATA[ The Model Context Protocol (MCP) is changing how AI applications connect to external tools and data. While some tutorials stop at "connect to GitHub" or "read a file," the real power of MCP is unlocki ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-mcp-servers-for-your-internal-data/</link>
                <guid isPermaLink="false">69a849e9e55311e40f03f00e</guid>
                
                    <category>
                        <![CDATA[ mcp ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mcp server ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Mayur Vekariya ]]>
                </dc:creator>
                <pubDate>Wed, 04 Mar 2026 15:04:09 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5fc16e412cae9c5b190b6cdd/2e428238-cbc3-4892-97df-c1dd854c74c3.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The Model Context Protocol (MCP) is changing how AI applications connect to external tools and data. While some tutorials stop at "connect to GitHub" or "read a file," the real power of MCP is unlocking your <em>internal</em> data—databases, internal APIs, knowledge bases, and proprietary systems—for AI assistants in a structured, secure way.</p>
<p>In this guide, I'll walk you through building production-grade MCP servers that expose your organization's internal data to AI models. We'll go beyond simple examples and cover authentication, multi-tenancy, streaming, and deployment patterns you'll actually need.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#what-is-mcp-and-why-does-it-matter-for-internal-data">What is MCP and Why Does It Matter for Internal Data?</a></p>
</li>
<li><p><a href="#architecture-overview">Architecture Overview</a></p>
</li>
<li><p><a href="#setting-up-the-project">Setting Up the Project</a></p>
</li>
<li><p><a href="#building-the-mcp-server">Building the MCP Server</a></p>
<ul>
<li><p><a href="#step-1-server-skeleton">Step 1: Server Skeleton</a></p>
</li>
<li><p><a href="#step-2-connecting-to-internal-data">Step 2: Connecting to Internal Data</a></p>
</li>
<li><p><a href="#step-3-defining-tools">Step 3: Defining Tools</a></p>
</li>
<li><p><a href="#tool-design-principles">Tool Design Principles</a></p>
</li>
<li><p><a href="#step-4-exposing-resources">Step 4: Exposing Resources</a></p>
</li>
<li><p><a href="#step-5-transport-and-startup">Step 5: Transport and Startup</a></p>
</li>
</ul>
</li>
<li><p><a href="#adding-authentication">Adding Authentication</a></p>
<ul>
<li><p><a href="#bearer-token-authentication">Bearer Token Authentication</a></p>
</li>
<li><p><a href="#oauth-20-for-mcp">OAuth 2.0 for MCP</a></p>
</li>
</ul>
</li>
<li><p><a href="#scoping-data-access-per-user">Scoping Data Access Per User</a></p>
</li>
<li><p><a href="#connecting-to-internal-apis">Connecting to Internal APIs</a></p>
</li>
<li><p><a href="#building-a-rag-tool-for-internal-documents">Building a RAG Tool for Internal Documents</a></p>
</li>
<li><p><a href="#production-deployment">Production Deployment</a></p>
<ul>
<li><p><a href="#dockerizing-the-mcp-server">Dockerizing the MCP Server</a></p>
</li>
<li><p><a href="#health-checks-and-monitoring">Health Checks and Monitoring</a></p>
</li>
<li><p><a href="#logging-and-audit-trail">Logging and Audit Trail</a></p>
</li>
</ul>
</li>
<li><p><a href="#connecting-your-mcp-server-to-ai-clients">Connecting Your MCP Server to AI Clients</a></p>
<ul>
<li><p><a href="#claude-desktop">Claude Desktop</a></p>
</li>
<li><p><a href="#custom-application-using-the-mcp-client-sdk">Custom Application (using the MCP Client SDK)</a></p>
</li>
</ul>
</li>
<li><p><a href="#common-pitfalls">Common Pitfalls</a></p>
</li>
<li><p><a href="#wrapping-up">Wrapping Up</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This is an advanced guide. You should be comfortable with:</p>
<ul>
<li><p>TypeScript / Node.js</p>
</li>
<li><p>REST APIs and server-side development</p>
</li>
<li><p>Basic understanding of LLMs and tool calling</p>
</li>
<li><p>Familiarity with protocols like JSON-RPC</p>
</li>
</ul>
<h2 id="heading-what-is-mcp-and-why-does-it-matter-for-internal-data">What is MCP, and Why Does It Matter for Internal Data?</h2>
<p>MCP is an open protocol (created by Anthropic) that standardizes how AI assistants discover and invoke external tools. Think of it as a USB-C port for AI — one standard interface that lets any AI model connect to any data source.</p>
<p>Before MCP, connecting an AI assistant to your internal database meant:</p>
<ul>
<li><p>Writing custom tool definitions for each LLM provider</p>
</li>
<li><p>Hardcoding data access logic into your AI application</p>
</li>
<li><p>Rebuilding everything when you switched models or added new data sources</p>
</li>
</ul>
<p>MCP separates the <em>data layer</em> from the <em>AI layer</em>. Your MCP server exposes tools and resources. Any MCP-compatible client—Claude, ChatGPT, your custom app—can use them without modification.</p>
<p>For internal data, this is significant because:</p>
<ul>
<li><p><strong>Your CRM, ERP, ticketing system, and wiki all become AI-accessible</strong> through one protocol</p>
</li>
<li><p><strong>Access control stays in your MCP server</strong>, not scattered across AI application code</p>
</li>
<li><p><strong>New AI models or clients automatically get access</strong> without rewiring integrations</p>
</li>
<li><p><strong>Tool definitions live close to the data</strong>, making them easier to maintain and version</p>
</li>
</ul>
<h2 id="heading-architecture-overview">Architecture Overview</h2>
<p>Here's what we're building:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6763484155ac493748dfb95b/3b8f4b25-aa15-4cf1-971d-94f2f7ee9e70.png" alt="MCP server architecture connecting an AI client to internal data sources — PostgreSQL, Internal API, and File Store — via JSON-RPC over HTTP/SSE/stdio." style="display:block;margin:0 auto" width="906" height="397" loading="lazy">

<p>The MCP server sits between your AI client and your internal systems. It handles:</p>
<ul>
<li><p><strong>Tool discovery</strong>: Tells the AI what operations are available</p>
</li>
<li><p><strong>Parameter validation</strong>: Ensures the AI sends correct inputs</p>
</li>
<li><p><strong>Data access</strong>: Queries your internal systems</p>
</li>
<li><p><strong>Response formatting</strong>: Returns structured data the AI can reason about</p>
</li>
<li><p><strong>Authentication</strong>: Verifies who's making the request</p>
</li>
</ul>
<h2 id="heading-setting-up-the-project">Setting Up the Project</h2>
<p>Let's build an MCP server that exposes an internal employee directory and project management system.</p>
<pre><code class="language-shell">mkdir internal-data-mcp &amp;&amp; cd internal-data-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod express pg
npm install -D typescript @types/node @types/express @types/pg tsx
</code></pre>
<p>These commands scaffold the project. <code>npm install</code> pulls in the runtime dependencies: the official MCP SDK, Zod for schema validation, Express for the HTTP server, and <code>pg</code> for PostgreSQL. The <code>-D</code> flag installs TypeScript and its type definitions as dev-only dependencies — they're needed to compile the code but don't ship to production. <code>tsx</code> lets you run TypeScript directly during development without a separate compile step.</p>
<p>Now, create your <code>tsconfig.json</code>:</p>
<pre><code class="language-json">{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true
  },
  "include": ["src/**/*"]
}
</code></pre>
<p>This TypeScript config targets ES2022, which supports modern JavaScript features like top-level <code>await</code>. <code>"module": "Node16"</code> and <code>"moduleResolution": "Node16"</code> are required when using the MCP SDK's <code>.js</code> import extensions. <code>"strict": true</code> enables all of TypeScript's strictness checks, which helps catch bugs in tool handlers before they reach production. The <code>outDir</code>/<code>rootDir</code> pair tells the compiler to take source files from <code>src/</code> and emit compiled JavaScript into <code>dist/</code>.</p>
<h2 id="heading-building-the-mcp-server">Building the MCP Server</h2>
<h3 id="heading-step-1-server-skeleton">Step 1: Server Skeleton</h3>
<p>Create <code>src/server.ts</code>:</p>
<pre><code class="language-typescript">import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer(
  { name: "internal-data", version: "1.0.0" },
  { capabilities: { tools: {}, resources: {} } }
);
</code></pre>
<p>The <code>McpServer</code> class from the official SDK handles the JSON-RPC protocol, transport negotiation, and lifecycle management. We declare support for both <code>tools</code> (actions the AI can take) and <code>resources</code> (data the AI can read).</p>
<h3 id="heading-step-2-connecting-to-internal-data">Step 2: Connecting to Internal Data</h3>
<p>Let's say you have a PostgreSQL database with employee and project data. Create a data access layer:</p>
<pre><code class="language-typescript">// src/db.ts
import pg from "pg";

const pool = new pg.Pool({
  connectionString: process.env.INTERNAL_DB_URL,
  max: 10,
  idleTimeoutMillis: 30000,
});

export interface Employee {
  id: string;
  name: string;
  email: string;
  department: string;
  role: string;
  manager_id: string | null;
  start_date: string;
}

export interface Project {
  id: string;
  name: string;
  status: "active" | "completed" | "on_hold";
  lead_id: string;
  department: string;
  deadline: string | null;
}

export async function searchEmployees(
  query: string,
  department?: string
): Promise&lt;Employee[]&gt; {
  const conditions = ["(name ILIKE \(1 OR email ILIKE \)1 OR role ILIKE $1)"];
  const params: string[] = [`%${query}%`];

  if (department) {
    conditions.push(`department = $${params.length + 1}`);
    params.push(department);
  }

  const result = await pool.query&lt;Employee&gt;(
    `SELECT id, name, email, department, role, manager_id, start_date
     FROM employees
     WHERE ${conditions.join(" AND ")}
     ORDER BY name
     LIMIT 25`,
    params
  );

  return result.rows;
}

export async function getProjectsByStatus(
  status: string
): Promise&lt;Project[]&gt; {
  const result = await pool.query&lt;Project&gt;(
    `SELECT id, name, status, lead_id, department, deadline
     FROM projects
     WHERE status = $1
     ORDER BY deadline ASC NULLS LAST`,
    [status]
  );

  return result.rows;
}

export async function getProjectMembers(
  projectId: string
): Promise&lt;Employee[]&gt; {
  const result = await pool.query&lt;Employee&gt;(
    `SELECT e.id, e.name, e.email, e.department, e.role,
            e.manager_id, e.start_date
     FROM employees e
     JOIN project_members pm ON pm.employee_id = e.id
     WHERE pm.project_id = $1
     ORDER BY e.name`,
    [projectId]
  );

  return result.rows;
}
</code></pre>
<p>Notice this is plain SQL with parameterized queries. Your MCP server's data access layer should use whatever your team already uses — Prisma, Drizzle, Knex, raw SQL. MCP doesn't dictate your data access patterns.</p>
<h3 id="heading-step-3-defining-tools">Step 3: Defining Tools</h3>
<p>Now expose this data through MCP tools. This is where the design matters most. Good tool definitions directly impact how well the AI uses your data.</p>
<pre><code class="language-typescript">// src/tools.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import {
  searchEmployees,
  getProjectsByStatus,
  getProjectMembers,
} from "./db.js";

export function registerTools(server: McpServer) {
  // Tool 1: Search the employee directory
  server.tool(
    "search_employees",
    `Search the internal employee directory by name, email, or role.
     Returns matching employees with their department and reporting structure.
     Use this when the user asks about people, teams, or org structure.`,
    {
      query: z
        .string()
        .describe("Search term: employee name, email, or role title"),
      department: z
        .string()
        .optional()
        .describe(
          "Filter by department name (e.g., 'Engineering', 'Marketing')"
        ),
    },
    async ({ query, department }) =&gt; {
      const employees = await searchEmployees(query, department);

      if (employees.length === 0) {
        return {
          content: [
            {
              type: "text",
              text: `No employees found matching "\({query}"\){department ? ` in ${department}` : ""}.`,
            },
          ],
        };
      }

      const formatted = employees
        .map(
          (e) =&gt;
            `- **\({e.name}** (\){e.email})\n  Role: \({e.role} | Dept: \){e.department} | Since: ${e.start_date}`
        )
        .join("\n");

      return {
        content: [
          {
            type: "text",
            text: `Found \({employees.length} employee(s):\n\n\){formatted}`,
          },
        ],
      };
    }
  );

  // Tool 2: List projects by status
  server.tool(
    "list_projects",
    `List internal projects filtered by status.
     Returns project name, lead, department, and deadline.
     Use this when the user asks about ongoing work, project status, or deadlines.`,
    {
      status: z
        .enum(["active", "completed", "on_hold"])
        .describe("Project status to filter by"),
    },
    async ({ status }) =&gt; {
      const projects = await getProjectsByStatus(status);

      if (projects.length === 0) {
        return {
          content: [
            {
              type: "text",
              text: `No ${status} projects found.`,
            },
          ],
        };
      }

      const formatted = projects
        .map(
          (p) =&gt;
            `- **\({p.name}** [\){p.status}]\n  Lead: \({p.lead_id} | Dept: \){p.department} | Deadline: ${p.deadline ?? "None"}`
        )
        .join("\n");

      return {
        content: [
          {
            type: "text",
            text: `\({projects.length} \){status} project(s):\n\n${formatted}`,
          },
        ],
      };
    }
  );

  // Tool 3: Get team members for a project
  server.tool(
    "get_project_team",
    `Get all team members assigned to a specific project.
     Returns employee details for each member.
     Use this when the user asks who is working on a project.`,
    {
      project_id: z
        .string()
        .uuid()
        .describe("The UUID of the project to look up"),
    },
    async ({ project_id }) =&gt; {
      const members = await getProjectMembers(project_id);

      if (members.length === 0) {
        return {
          content: [
            {
              type: "text",
              text: "No team members found for this project.",
            },
          ],
        };
      }

      const formatted = members
        .map((m) =&gt; `- \({m.name} (\){m.role}, ${m.department})`)
        .join("\n");

      return {
        content: [
          {
            type: "text",
            text: `Project team (\({members.length} members):\n\n\){formatted}`,
          },
        ],
      };
    }
  );
}
</code></pre>
<p><code>server.tool()</code> registers each tool with four arguments: the tool name, a plain-English description the AI reads to decide when to call it, a Zod schema defining the parameters, and the async handler that runs when the tool is invoked. The handler receives validated, typed parameters — Zod rejects malformed inputs before your handler ever runs. Each handler returns a <code>content</code> array; the <code>type: "text"</code> block is the most common format and tells the AI client to treat the response as readable text. Returning an empty result (zero matches) is handled explicitly so the AI gets a useful message rather than an empty array it might misinterpret.</p>
<h3 id="heading-tool-design-principles">Tool Design Principles</h3>
<p>Three things make the difference between tools an AI uses well and tools it struggles with:</p>
<p><strong>1. Descriptive names and descriptions.</strong> The AI decides which tool to call based entirely on the description. Be specific about <em>when</em> to use the tool, not just <em>what</em> it does. Compare:</p>
<pre><code class="language-plaintext">// Vague — the AI won't know when to pick this
"Search employees"

// Specific — the AI knows exactly when this tool is relevant
"Search the internal employee directory by name, email, or role.
 Use this when the user asks about people, teams, or org structure."
</code></pre>
<p><strong>2. Typed parameters with descriptions.</strong> Use Zod's <code>.describe()</code> on every parameter. The AI needs to understand what each field expects:</p>
<pre><code class="language-typescript">// The AI has to guess what format "query" expects
{ query: z.string() }

// The AI knows exactly what to pass
{ query: z.string().describe("Search term: employee name, email, or role title") }
</code></pre>
<p><strong>3. Structured return values.</strong> Return data in a format the AI can reason about. Use markdown tables or structured lists rather than raw JSON dumps. The AI processes structured text better than deeply nested objects.</p>
<h3 id="heading-step-4-exposing-resources">Step 4: Exposing Resources</h3>
<p>Resources are read-only data the AI can pull into its context. Unlike tools (which the AI invokes during reasoning), resources are typically loaded upfront to provide background knowledge.</p>
<pre><code class="language-typescript">// src/resources.ts
import {
  McpServer,
  ResourceTemplate,
} from "@modelcontextprotocol/sdk/server/mcp.js";

export function registerResources(server: McpServer) {
  // Static resource: org chart overview
  server.resource(
    "org-structure",
    "internal://org-structure",
    {
      description:
        "Overview of the organization structure including departments and leadership",
      mimeType: "text/markdown",
    },
    async (uri) =&gt; ({
      contents: [
        {
          uri: uri.href,
          mimeType: "text/markdown",
          text: await generateOrgOverview(),
        },
      ],
    })
  );

  // Dynamic resource template: department details
  server.resource(
    "department-info",
    new ResourceTemplate("internal://departments/{name}", {
      list: undefined,
    }),
    {
      description: "Detailed information about a specific department",
      mimeType: "text/markdown",
    },
    async (uri, variables) =&gt; ({
      contents: [
        {
          uri: uri.href,
          mimeType: "text/markdown",
          text: await getDepartmentDetails(
            variables.name as string
          ),
        },
      ],
    })
  );
}
</code></pre>
<p><code>server.resource()</code> registers two kinds of resources here. The first uses a fixed URI (<code>internal://org-structure</code>) — this is a static resource the AI can request by name. The second uses a <code>ResourceTemplate</code>, which defines a URI pattern with a <code>{name}</code> placeholder; the AI can request <code>internal://departments/Engineering</code> and the <code>variables.name</code> parameter will be populated with <code>"Engineering"</code> at runtime. Both resources return a <code>contents</code> array with <code>mimeType: "text/markdown"</code> — this tells the client how to render the response. Resources differ from tools in that they're meant to be read as background context, not invoked as actions.</p>
<p>Resources are useful for data that provides context rather than answering a specific question — company policies, API documentation, database schemas, configuration references.</p>
<h3 id="heading-step-5-transport-and-startup">Step 5: Transport and Startup</h3>
<p>MCP supports multiple transports. For internal data servers, you'll typically use one of two:</p>
<p><strong>Streamable HTTP</strong> — the recommended transport for remote servers (replaces the older SSE transport):</p>
<pre><code class="language-typescript">// src/index.ts
import express from "express";
import { randomUUID } from "node:crypto";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { registerTools } from "./tools.js";
import { registerResources } from "./resources.js";

const app = express();
app.use(express.json());

const server = new McpServer(
  { name: "internal-data", version: "1.0.0" },
  { capabilities: { tools: {}, resources: {} } }
);

registerTools(server);
registerResources(server);

// Store transports by session ID
const transports = new Map&lt;string, StreamableHTTPServerTransport&gt;();

// Handle all MCP requests on a single endpoint
app.all("/mcp", async (req, res) =&gt; {
  // Check for existing session
  const sessionId = req.headers["mcp-session-id"] as string | undefined;

  if (sessionId &amp;&amp; transports.has(sessionId)) {
    // Existing session — route to its transport
    const transport = transports.get(sessionId)!;
    await transport.handleRequest(req, res);
    return;
  }

  if (sessionId &amp;&amp; !transports.has(sessionId)) {
    // Unknown session ID
    res.status(404).json({ error: "Session not found" });
    return;
  }

  // New session — create transport and connect
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: () =&gt; randomUUID(),
    onsessioninitialized: (id) =&gt; {
      transports.set(id, transport);
    },
  });

  transport.onclose = () =&gt; {
    if (transport.sessionId) {
      transports.delete(transport.sessionId);
    }
  };

  await server.connect(transport);
  await transport.handleRequest(req, res);
});

app.listen(3100, () =&gt; {
  console.log("MCP server running on http://localhost:3100/mcp");
});
</code></pre>
<p>This sets up a single <code>/mcp</code> endpoint that handles all MCP communication. When a new client connects (no <code>mcp-session-id</code> header), a <code>StreamableHTTPServerTransport</code> is created and stored in the <code>transports</code> Map keyed by a generated UUID. On subsequent requests, the session ID from the header is used to look up the existing transport and route the request to it — this is how the server maintains stateful sessions with multiple clients simultaneously. <code>transport.onclose</code> cleans up the Map entry when a session ends, preventing memory leaks. The <code>StdioServerTransport</code> alternative (shown below) skips all of this: it reads from stdin and writes to stdout, which is how Claude Desktop spawns local servers as child processes.</p>
<p><strong>Stdio</strong> — for local development or when the MCP client spawns the server as a child process:</p>
<pre><code class="language-typescript">// src/stdio.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerTools } from "./tools.js";
import { registerResources } from "./resources.js";

const server = new McpServer(
  { name: "internal-data", version: "1.0.0" },
  { capabilities: { tools: {}, resources: {} } }
);

registerTools(server);
registerResources(server);

const transport = new StdioServerTransport();
await server.connect(transport);
</code></pre>
<p>For internal data in a production setting, HTTP/SSE is almost always what you want. Stdio is convenient for development and when the client and server run on the same machine.</p>
<h2 id="heading-adding-authentication">Adding Authentication</h2>
<p>Internal data servers need authentication. You don't want every AI client on the network querying your employee database unauthenticated.</p>
<h3 id="heading-bearer-token-authentication">Bearer Token Authentication</h3>
<p>The simplest approach is to validate a token on every request:</p>
<pre><code class="language-typescript">// src/auth-middleware.ts
import { Request, Response, NextFunction } from "express";

interface AuthenticatedRequest extends Request {
  userId?: string;
  orgId?: string;
}

export function authMiddleware(
  req: AuthenticatedRequest,
  res: Response,
  next: NextFunction
) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Missing authorization header" });
  }

  const token = authHeader.slice(7);

  try {
    // Validate against your internal auth system
    const claims = validateInternalToken(token);
    req.userId = claims.sub;
    req.orgId = claims.org;
    next();
  } catch {
    return res.status(403).json({ error: "Invalid token" });
  }
}

function validateInternalToken(token: string) {
  // Replace with your actual token validation:
  // - JWT verification against your auth service
  // - API key lookup in your database
  // - Session token validation against Redis
  // This is a placeholder
  return { sub: "user-123", org: "org-456" };
}
</code></pre>
<p>The middleware checks every request for an <code>Authorization: Bearer &lt;token&gt;</code> header before it reaches the MCP handler. <code>validateInternalToken</code> is a placeholder — replace it with your real validation logic: JWT verification using a library like <code>jsonwebtoken</code>, an API key lookup in your database, or a session token check against Redis. The validated claims are attached to the request object (<code>req.userId</code>, <code>req.orgId</code>) so downstream tool handlers can use them for access scoping. The <code>app.use("/mcp", authMiddleware)</code> line ensures no request reaches the MCP endpoint without passing this check first.</p>
<p>Add it to your Express app:</p>
<pre><code class="language-typescript">app.use("/mcp", authMiddleware);
</code></pre>
<h3 id="heading-oauth-20-for-mcp">OAuth 2.0 for MCP</h3>
<p>For clients that support MCP's built-in OAuth flow (like Claude Desktop), you can implement the full OAuth handshake. The MCP SDK provides the <code>OAuthServerProvider</code> interface with these required methods:</p>
<pre><code class="language-typescript">import type { OAuthServerProvider } from "@modelcontextprotocol/sdk/server/auth/provider.js";
import type {
  AuthorizationParams,
  OAuthClientInformationFull,
  OAuthRegisteredClientsStore,
  OAuthTokens,
  AuthInfo,
} from "@modelcontextprotocol/sdk/server/auth/types.js";

class InternalOAuthProvider implements OAuthServerProvider {
  // Store for registered OAuth clients
  get clientsStore(): OAuthRegisteredClientsStore {
    return this._clientsStore;
  }

  private _clientsStore: OAuthRegisteredClientsStore = {
    async getClient(clientId: string) {
      // Look up the registered client in your database
      return db.getOAuthClient(clientId);
    },
    async registerClient(clientMetadata) {
      // Register a new dynamic client
      return db.createOAuthClient(clientMetadata);
    },
  };

  // Redirect the user to your internal SSO for authorization
  async authorize(
    client: OAuthClientInformationFull,
    params: AuthorizationParams,
    res: Response
  ): Promise&lt;void&gt; {
    const authUrl = new URL(
      "https://sso.internal.company.com/authorize"
    );
    authUrl.searchParams.set("client_id", client.client_id);
    authUrl.searchParams.set("redirect_uri", params.redirectUri);
    authUrl.searchParams.set("state", params.state ?? "");
    authUrl.searchParams.set(
      "code_challenge",
      params.codeChallenge
    );
    // The method writes to the response directly
    res.redirect(authUrl.toString());
  }

  // Return the PKCE challenge for a given authorization code
  async challengeForAuthorizationCode(
    _client: OAuthClientInformationFull,
    authorizationCode: string
  ): Promise&lt;string&gt; {
    const session = await db.getSessionByCode(authorizationCode);
    return session.codeChallenge;
  }

  // Exchange authorization code for access + refresh tokens
  async exchangeAuthorizationCode(
    client: OAuthClientInformationFull,
    authorizationCode: string,
    _codeVerifier?: string,
    _redirectUri?: string
  ): Promise&lt;OAuthTokens&gt; {
    const response = await fetch(
      "https://sso.internal.company.com/token",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: new URLSearchParams({
          grant_type: "authorization_code",
          code: authorizationCode,
          client_id: client.client_id,
        }),
      }
    );

    return response.json() as Promise&lt;OAuthTokens&gt;;
  }

  // Refresh expired tokens
  async exchangeRefreshToken(
    client: OAuthClientInformationFull,
    refreshToken: string
  ): Promise&lt;OAuthTokens&gt; {
    const response = await fetch(
      "https://sso.internal.company.com/token",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: new URLSearchParams({
          grant_type: "refresh_token",
          refresh_token: refreshToken,
          client_id: client.client_id,
        }),
      }
    );

    return response.json() as Promise&lt;OAuthTokens&gt;;
  }

  // Validate an access token on every request
  async verifyAccessToken(token: string): Promise&lt;AuthInfo&gt; {
    const response = await fetch(
      "https://sso.internal.company.com/introspect",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: new URLSearchParams({ token }),
      }
    );

    const data = await response.json();
    if (!data.active) throw new Error("Token inactive");

    return {
      token,
      clientId: data.client_id,
      scopes: data.scope?.split(" ") ?? [],
      expiresAt: data.exp,
    };
  }
}
</code></pre>
<p><code>InternalOAuthProvider</code> implements the <code>OAuthServerProvider</code> interface, which the MCP SDK calls at each stage of the OAuth flow. <code>clientsStore</code> handles dynamic client registration — MCP clients like Claude Desktop register themselves the first time they connect. <code>authorize()</code> redirects the user to your internal SSO; it writes directly to the Express response. <code>challengeForAuthorizationCode()</code> returns the PKCE code challenge stored when the authorization session began — this is how the token exchange is verified without transmitting secrets. <code>exchangeAuthorizationCode()</code> and <code>exchangeRefreshToken()</code> make server-to-server calls to your SSO's token endpoint, keeping credentials out of the browser. <code>verifyAccessToken()</code> is called on every incoming MCP request using the token introspection endpoint to confirm the token is still active and extract the user's scopes.</p>
<h2 id="heading-scoping-data-access-per-user">Scoping Data Access Per User</h2>
<p>This is the most important part of an internal data MCP server: <strong>the AI should only access data the requesting user is authorized to see.</strong></p>
<p>Don't skip this. Without user-scoped access, you're building a data exfiltration tool with an AI wrapper.</p>
<pre><code class="language-typescript">// src/scoped-tools.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

export function registerScopedTools(
  server: McpServer,
  getUserContext: () =&gt; { userId: string; orgId: string; role: string }
) {
  server.tool(
    "search_employees",
    "Search the employee directory. Results are filtered based on your access level.",
    {
      query: z.string().describe("Name, email, or role to search for"),
    },
    async ({ query }) =&gt; {
      const ctx = getUserContext();

      // Enforce access boundaries
      let departmentFilter: string | undefined;

      if (ctx.role === "manager") {
        // Managers see their department only
        departmentFilter = await getUserDepartment(ctx.userId);
      } else if (ctx.role === "employee") {
        // Regular employees see limited fields
        departmentFilter = await getUserDepartment(ctx.userId);
      }
      // Admins and HR see everything — no filter

      const employees = await searchEmployees(query, departmentFilter);

      // Redact sensitive fields based on role
      const results = employees.map((e) =&gt; ({
        name: e.name,
        email: e.email,
        department: e.department,
        role: e.role,
        // Only HR and admins see start date and manager info
        ...(["admin", "hr"].includes(ctx.role)
          ? { start_date: e.start_date, manager_id: e.manager_id }
          : {}),
      }));

      return {
        content: [
          {
            type: "text",
            text: formatEmployeeList(results),
          },
        ],
      };
    }
  );
}
</code></pre>
<p>The pattern here:</p>
<ol>
<li><p><strong>Extract user context</strong> from the authenticated session</p>
</li>
<li><p><strong>Filter queries</strong> at the database level (not after fetching everything)</p>
</li>
<li><p><strong>Redact fields</strong> the user shouldn't see</p>
</li>
<li><p><strong>Log access</strong> for audit trails</p>
</li>
</ol>
<h2 id="heading-connecting-to-internal-apis">Connecting to Internal APIs</h2>
<p>Not all internal data lives in databases. You often need to wrap existing internal APIs:</p>
<pre><code class="language-typescript">server.tool(
  "get_ticket_details",
  `Look up a support ticket from the internal ticketing system.
   Returns ticket status, assignee, priority, and recent updates.`,
  {
    ticket_id: z
      .string()
      .regex(/^TK-\d+$/)
      .describe("Ticket ID in format TK-12345"),
  },
  async ({ ticket_id }) =&gt; {
    const ctx = getUserContext();

    const response = await fetch(
      `\({process.env.TICKETING_API_URL}/api/v2/tickets/\){ticket_id}`,
      {
        headers: {
          Authorization: `Bearer ${process.env.TICKETING_SERVICE_TOKEN}`,
          "X-On-Behalf-Of": ctx.userId,
        },
      }
    );

    if (response.status === 404) {
      return {
        content: [
          { type: "text", text: `Ticket ${ticket_id} not found.` },
        ],
      };
    }

    if (response.status === 403) {
      return {
        content: [
          {
            type: "text",
            text: `You don't have access to ticket ${ticket_id}.`,
          },
        ],
      };
    }

    const ticket = await response.json();

    return {
      content: [
        {
          type: "text",
          text: [
            `**\({ticket.id}: \){ticket.title}**`,
            `Status: \({ticket.status} | Priority: \){ticket.priority}`,
            `Assignee: ${ticket.assignee?.name ?? "Unassigned"}`,
            `Created: ${ticket.created_at}`,
            "",
            `**Latest Update:**`,
            ticket.updates?.[0]?.body ?? "No updates yet.",
          ].join("\n"),
        },
      ],
    };
  }
);
</code></pre>
<p>Key points when wrapping internal APIs:</p>
<ul>
<li><p><strong>Use service tokens</strong> for server-to-server auth, but pass user identity via headers like <code>X-On-Behalf-Of</code></p>
</li>
<li><p><strong>Handle HTTP errors explicitly</strong> — return user-friendly messages, not raw error objects</p>
</li>
<li><p><strong>Validate input formats</strong> — the regex on <code>ticket_id</code> prevents injection and guides the AI on expected format</p>
</li>
<li><p><strong>Don't leak internal implementation details</strong> in error messages</p>
</li>
</ul>
<h2 id="heading-building-a-rag-tool-for-internal-documents">Building a RAG Tool for Internal Documents</h2>
<p>One of the highest-value use cases: letting the AI search your internal knowledge base. Here's a tool that performs vector search against an internal document store:</p>
<pre><code class="language-typescript">server.tool(
  "search_internal_docs",
  `Search the internal knowledge base for relevant documents.
   Covers engineering docs, runbooks, architecture decisions, and policies.
   Use this when the user asks about internal processes, systems, or decisions.`,
  {
    query: z
      .string()
      .describe("Natural language search query"),
    category: z
      .enum(["engineering", "policy", "runbook", "architecture", "all"])
      .default("all")
      .describe("Document category to search within"),
    limit: z
      .number()
      .min(1)
      .max(10)
      .default(5)
      .describe("Maximum number of results"),
  },
  async ({ query, category, limit }) =&gt; {
    // Generate embedding for the search query
    const embedding = await generateEmbedding(query);

    // Vector similarity search against your document store
    const results = await pool.query(
      `SELECT
         d.id,
         d.title,
         d.category,
         d.content_chunk,
         d.source_url,
         d.updated_at,
         1 - (d.embedding &lt;=&gt; $1::vector) AS similarity
       FROM document_chunks d
       WHERE (\(2 = 'all' OR d.category = \)2)
         AND 1 - (d.embedding &lt;=&gt; $1::vector) &gt; 0.7
       ORDER BY d.embedding &lt;=&gt; $1::vector
       LIMIT $3`,
      [JSON.stringify(embedding), category, limit]
    );

    if (results.rows.length === 0) {
      return {
        content: [
          {
            type: "text",
            text: `No relevant documents found for "${query}".`,
          },
        ],
      };
    }

    const formatted = results.rows
      .map(
        (doc, i) =&gt;
          `### \({i + 1}. \){doc.title}\n` +
          `Category: \({doc.category} | Updated: \){doc.updated_at} | Relevance: ${(doc.similarity * 100).toFixed(0)}%\n\n` +
          `${doc.content_chunk}\n\n` +
          `Source: ${doc.source_url}`
      )
      .join("\n\n---\n\n");

    return {
      content: [
        {
          type: "text",
          text: `Found \({results.rows.length} relevant document(s):\n\n\){formatted}`,
        },
      ],
    };
  }
);
</code></pre>
<p>This tool combines two operations: embedding generation and vector similarity search. <code>generateEmbedding(query)</code> calls an embedding model (such as OpenAI's <code>text-embedding-3-small</code> or a self-hosted model) to convert the user's query into a numeric vector. The SQL query then uses pgvector's <code>&lt;=&gt;</code> operator to compute cosine distance between the query vector and stored document chunk embeddings — lower distance means higher similarity. The <code>1 - (embedding &lt;=&gt; $1) &gt; 0.7</code> condition filters out results below 70% similarity, so the AI doesn't receive loosely related noise. Results are ordered by ascending distance (most similar first) and capped by the <code>limit</code> parameter. The formatted output includes a relevance percentage so the AI can communicate confidence levels to the user.</p>
<h2 id="heading-production-deployment">Production Deployment</h2>
<h3 id="heading-dockerizing-the-mcp-server">Dockerizing the MCP Server</h3>
<pre><code class="language-dockerfile">FROM node:22-slim AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-slim AS runtime

WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

ENV NODE_ENV=production
EXPOSE 3100

HEALTHCHECK --interval=30s --timeout=5s \
  CMD curl -f http://localhost:3100/health || exit 1

CMD ["node", "dist/index.js"]
</code></pre>
<p>The Dockerfile uses a two-stage build. The <code>builder</code> stage installs all dependencies (including devDependencies) and compiles TypeScript to JavaScript in <code>dist/</code>. The <code>runtime</code> stage starts fresh from a clean Node image and copies only the compiled output and <code>node_modules</code> — devDependencies like TypeScript are excluded, keeping the final image small. The <code>HEALTHCHECK</code> instruction tells Docker (and orchestrators like Kubernetes) to poll <code>/health</code> every 30 seconds; if the endpoint fails, the container is marked unhealthy and can be automatically restarted or removed from the load balancer rotation.</p>
<h3 id="heading-health-checks-and-monitoring">Health Checks and Monitoring</h3>
<p>Add a health endpoint that verifies your dependencies:</p>
<pre><code class="language-typescript">app.get("/health", async (_req, res) =&gt; {
  const checks = {
    database: false,
    ticketingApi: false,
  };

  try {
    await pool.query("SELECT 1");
    checks.database = true;
  } catch {}

  try {
    const resp = await fetch(
      `${process.env.TICKETING_API_URL}/health`
    );
    checks.ticketingApi = resp.ok;
  } catch {}

  const healthy = Object.values(checks).every(Boolean);
  res.status(healthy ? 200 : 503).json({
    status: healthy ? "healthy" : "degraded",
    checks,
    uptime: process.uptime(),
  });
});
</code></pre>
<p>The <code>/health</code> endpoint runs two dependency checks in parallel: a lightweight <code>SELECT 1</code> query to confirm the database connection is live, and an HTTP ping to the ticketing API. Both results are collected into a <code>checks</code> object. If any check fails, the endpoint returns HTTP 503 (Service Unavailable) — this is the signal load balancers and container orchestrators use to stop routing traffic to an unhealthy instance. <code>process.uptime()</code> is included as a diagnostic field so you can quickly tell whether a degraded instance just started or has been running for hours.</p>
<h3 id="heading-logging-and-audit-trail">Logging and Audit Trail</h3>
<p>Every tool invocation against internal data should be logged:</p>
<pre><code class="language-typescript">function createAuditLogger() {
  return {
    logToolCall(params: {
      userId: string;
      tool: string;
      input: Record&lt;string, unknown&gt;;
      resultSize: number;
      durationMs: number;
    }) {
      // Ship to your logging infrastructure
      // (Datadog, ELK, CloudWatch, etc.)
      console.log(
        JSON.stringify({
          event: "mcp_tool_call",
          timestamp: new Date().toISOString(),
          ...params,
        })
      );
    },
  };
}
</code></pre>
<p><code>createAuditLogger</code> returns a logger object rather than a class instance, which makes it easy to swap the underlying transport (stdout, a logging SDK, etc.) without changing the call sites. The <code>audited</code> wrapper function is a higher-order function: it takes a tool handler and returns a new function with the same signature, but with timing and logging added around the original call. The <code>try/catch</code> ensures a log entry is written even when the handler throws — you want failed calls in your audit trail, not just successful ones. Shipping these logs to a centralized store (Datadog, CloudWatch, ELK) lets you answer questions like "what data did this user's AI session access last Tuesday?" — which is often required for compliance in organizations handling sensitive internal data.</p>
<p>Wrap your tool handlers to automatically log every call:</p>
<pre><code class="language-typescript">function audited&lt;T extends Record&lt;string, unknown&gt;&gt;(
  handler: (params: T) =&gt; Promise&lt;ToolResult&gt;,
  toolName: string,
  audit: ReturnType&lt;typeof createAuditLogger&gt;
) {
  return async (params: T): Promise&lt;ToolResult&gt; =&gt; {
    const start = Date.now();
    const ctx = getUserContext();

    try {
      const result = await handler(params);
      audit.logToolCall({
        userId: ctx.userId,
        tool: toolName,
        input: params,
        resultSize: JSON.stringify(result).length,
        durationMs: Date.now() - start,
      });
      return result;
    } catch (error) {
      audit.logToolCall({
        userId: ctx.userId,
        tool: toolName,
        input: params,
        resultSize: 0,
        durationMs: Date.now() - start,
      });
      throw error;
    }
  };
}
</code></pre>
<h2 id="heading-connecting-your-mcp-server-to-ai-clients">Connecting Your MCP Server to AI Clients</h2>
<h3 id="heading-claude-desktop">Claude Desktop</h3>
<p>Add to your <code>claude_desktop_config.json</code>:</p>
<pre><code class="language-json">{
  "mcpServers": {
    "internal-data": {
      "url": "http://localhost:3100/mcp",
      "headers": {
        "Authorization": "Bearer your-internal-token"
      }
    }
  }
}
</code></pre>
<h3 id="heading-custom-application-using-the-mcp-client-sdk">Custom Application (using the MCP Client SDK)</h3>
<pre><code class="language-typescript">import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const transport = new StreamableHTTPClientTransport(
  new URL("http://localhost:3100/mcp"),
  {
    requestInit: {
      headers: {
        Authorization: `Bearer ${userToken}`,
      },
    },
  }
);

const client = new Client(
  { name: "my-ai-app", version: "1.0.0" }
);

await client.connect(transport);

// Discover available tools
const { tools } = await client.listTools();
console.log("Available tools:", tools.map((t) =&gt; t.name));

// Call a tool
const result = await client.callTool({
  name: "search_employees",
  arguments: { query: "engineering manager" },
});

console.log(result.content);
</code></pre>
<p><code>StreamableHTTPClientTransport</code> manages the HTTP connection to your MCP server, including attaching the <code>Authorization</code> header to every request. <code>client.connect(transport)</code> performs the MCP initialization handshake — the client announces its capabilities and the server responds with the list of available tools and resources. <code>client.listTools()</code> returns the full tool catalog, which you can use to dynamically build a UI or pass directly to an LLM's tool-calling API. <code>client.callTool()</code> sends a JSON-RPC request to invoke a specific tool by name and returns the <code>content</code> array from the handler — the same format the AI model receives. In a production application, you'd pass this <code>content</code> back to the model as a tool result in the conversation history.</p>
<h2 id="heading-common-pitfalls">Common Pitfalls</h2>
<p><strong>1. Returning too much data.</strong> LLMs have context limits. If your database query returns 500 rows, don't send them all. Paginate, summarize, or limit results. 25 items is a reasonable default.</p>
<p><strong>2. Tool descriptions that are too generic.</strong> If you have <code>search_employees</code> and <code>search_contractors</code>, the AI needs to know the difference. Don't rely on the tool name alone — the description is what the model reads.</p>
<p><strong>3. Missing error handling.</strong> When a database query fails, return a structured error message, not a stack trace. The AI needs to tell the user something useful, and raw errors leak implementation details.</p>
<p><strong>4. No rate limiting.</strong> AI tool calls can happen in loops. If the model calls your tool 50 times in one conversation, you need circuit breakers:</p>
<pre><code class="language-typescript">const rateLimiter = new Map&lt;string, number[]&gt;();

function checkRateLimit(userId: string, limit = 30, windowMs = 60000) {
  const now = Date.now();
  const calls = rateLimiter.get(userId) ?? [];
  const recent = calls.filter((t) =&gt; now - t &lt; windowMs);

  if (recent.length &gt;= limit) {
    throw new Error(
      `Rate limit exceeded. Max ${limit} calls per minute.`
    );
  }

  recent.push(now);
  rateLimiter.set(userId, recent);
}
</code></pre>
<p><strong>5. Not testing with actual AI models.</strong> Your tools might look correct in unit tests but confuse the model. Test the full loop: AI model receives tool definitions, decides to call a tool, gets the result, and reasons about it. Adjust descriptions based on how the model actually behaves.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>Building MCP servers for internal data is about three things:</p>
<ol>
<li><p><strong>Good tool design</strong> — clear descriptions, typed parameters, structured responses</p>
</li>
<li><p><strong>Proper access control</strong> — authenticate users, scope data access, log everything</p>
</li>
<li><p><strong>Production readiness</strong> — health checks, rate limiting, error handling, monitoring</p>
</li>
</ol>
<p>The protocol itself is straightforward. The hard work is designing the right abstractions over your internal systems so the AI can use them effectively without leaking data or overwhelming the context window.</p>
<p>Start with one or two high-value tools (employee lookup, document search), test them with real users, and expand from there. The best internal MCP servers grow organically based on what people actually ask the AI.</p>
<p>The full source code from this guide is available on <a href="https://github.com/mayur9210/build-mcp-server-template">GitHub</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use the Model Context Protocol (MCP) with Flutter and Dart ]]>
                </title>
                <description>
                    <![CDATA[ Software development is moving fast toward AI-assisted workflows and smarter tooling. Whether it’s your IDE completing code, an AI assistant analyzing your project, or automated testing pipelines, all these tools need a standardized way to communicat... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-the-model-context-protocol-mcp-with-flutter-and-dart/</link>
                <guid isPermaLink="false">68fbd79c4ea129b1fef69825</guid>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mcp server ]]>
                    </category>
                
                    <category>
                        <![CDATA[ flutter-aware ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Atuoha Anthony ]]>
                </dc:creator>
                <pubDate>Fri, 24 Oct 2025 19:46:36 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761335181944/18a2fe98-2d77-490c-8b80-5f254c3f9c99.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Software development is moving fast toward AI-assisted workflows and smarter tooling. Whether it’s your IDE completing code, an AI assistant analyzing your project, or automated testing pipelines, all these tools need a standardized way to communicate. That’s where the <strong>Model Context Protocol (MCP)</strong> comes in.</p>
<p>If you’ve been hearing about MCP and wondering what it means for you as a Dart or Flutter developer, this guide is for you. It explains what MCP is and how it connects with Dart through the official <code>dart_mcp</code> package. You’ll also learn how you can start building or integrating MCP-based tools yourself, so AI can actually understand and act on your Flutter/Dart project, not just answer questions about pasted code.</p>
<p>By the end of this guide, you’ll understand:</p>
<ul>
<li><p>What the Model Context Protocol (MCP) is and why it matters.</p>
</li>
<li><p>How MCP powers AI and development tools to communicate in a structured, consistent way.</p>
</li>
<li><p>How Dart integrates MCP through the <code>dart_mcp</code> package and server tools.</p>
</li>
<li><p>Practical examples of how to build an MCP server and client in Dart.</p>
</li>
<li><p>How to get started, including prerequisites and learning resources.</p>
</li>
</ul>
<h3 id="heading-table-of-contents">Table of Contents:</h3>
<ol>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-mcp-model-context-protocol">What is MCP (Model Context Protocol)?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-mcp-matters-for-dart-and-flutter-developers">Why MCP Matters for Dart and Flutter Developers</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-mcp-in-the-dart-ecosystem">MCP in the Dart Ecosystem</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-what-is-the-dartmcp-package">What Is the dart_mcp Package?</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-real-world-use-caseshttpspubdevpackagesdartmcputmsourcechatgptcom">Real-world use cases</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-mcp-actually-works">How MCP actually works</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-getting-started-step-by-step">Getting started, step-by-step</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-prerequisites">1) Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-start-the-dart-amp-flutter-mcp-server-locally">2) Start the Dart &amp; Flutter MCP server locally</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-configure-an-mcp-client">3) Configure an MCP client</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-try-an-easy-request-from-your-ide-assistant">4) Try an easy request from your IDE assistant</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-5-build-a-custom-capability-optional">5) Build a custom capability (optional)</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-hands-on-example-fix-a-layout-overflow">Hands-on example</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-step-by-step-explanation">Step-by-step explanation</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-a-simple-mcp-server-in-dart">How to Build a Simple MCP Server in Dart</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-add-dependency">Step 1: Add Dependency</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-create-the-server">Step 2: Create the Server</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-example-creating-an-mcp-client">Example: Creating an MCP Client</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-mcp-vs-custom-http-servers">MCP vs. Custom HTTP Servers</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-best-practices-safety-and-permissions">Best Practices, Safety, and Permissions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-getting-started-as-a-beginner">Getting Started as a Beginner</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-moving-beyond-beginner-level">Moving Beyond Beginner Level</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-references">References</a></p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along with this guide, you should have the following:</p>
<ol>
<li><p>Dart SDK 3.9 or later/Flutter 3.35 beta or later installed on your machine.</p>
</li>
<li><p>Basic understanding of async programming in Dart (using <code>async</code> / <code>await</code>).</p>
</li>
<li><p>Familiarity with standard I/O streams (<code>stdin</code>, <code>stdout</code>) since the MCP client communicates through them.</p>
</li>
<li><p>Access to an MCP-compatible server (for example, Dart MCP Server or a custom implementation).</p>
</li>
<li><p>Pub dependencies properly set up with <code>dart_mcp</code> added to your <code>pubspec.yaml</code>.</p>
</li>
</ol>
<h2 id="heading-what-is-mcp-model-context-protocol">What is MCP (Model Context Protocol)?</h2>
<p>MCP (Model Context Protocol) is a standard that lets AI models (agents) communicate with developer tools, editors, and projects in a structured, permissioned way. Instead of asking an AI to reason about code you paste into chat, MCP lets the AI call specific capabilities (tools) your project exposes, for example, “run analyzer”, “get file contents”, “run tests”, or “search pub.dev”. This turns the AI into a contextual collaborator that can inspect, run, and even modify your codebase in a controlled fashion.</p>
<p>In simpler terms, it’s a protocol that helps <strong>AI agents and tools talk to each other</strong>. It removes the need for one-off integrations and standardizes how capabilities like “run this tool,” “fetch this file,” or “get these logs” are described and used.</p>
<h2 id="heading-why-mcp-matters-for-dart-and-flutter-developers">Why MCP Matters for Dart and Flutter Developers</h2>
<p>For developers building in Dart and Flutter, MCP opens up new possibilities:</p>
<ol>
<li><p>You can build your own AI-driven tools (for example, analyzers, file processors, or code review assistants) that integrate with editors and assistants through MCP.</p>
</li>
<li><p>You can extend your development workflow, letting AI assistants interact directly with your local Dart projects, run commands, analyze files, or trigger Flutter builds.</p>
</li>
<li><p>You can automate tasks within your local dev environment (like linting, dependency analysis, or report generation) without needing a dedicated API server.</p>
</li>
</ol>
<p>It’s not just about AI, it’s also about standardized automation in your toolchain.</p>
<h2 id="heading-mcp-in-the-dart-ecosystem">MCP in the Dart Ecosystem</h2>
<p>The Dart team has started embracing MCP directly through an official experimental package called <code>dart_mcp</code>. This package gives Dart developers the tools to create both MCP servers and MCP clients, enabling two-way communication between your Dart tools and AI assistants or IDEs.</p>
<h3 id="heading-what-is-the-dartmcp-package">What Is the <code>dart_mcp</code> Package?</h3>
<p>The <a target="_blank" href="https://pub.dev/packages/dart_mcp"><code>dart_mcp</code></a> package provides APIs to implement MCP servers and clients using Dart. It’s published by <a target="_blank" href="https://pub.dev/publishers/labs.dart.dev/packages"><code>labs.dart.dev</code></a>, which means it’s an official Dart Labs experiment, actively evolving and backed by the Dart team.</p>
<p><strong>Key features:</strong></p>
<ol>
<li><p>Build MCP servers that expose tools and capabilities.</p>
</li>
<li><p>Build MCP clients that connect to those servers.</p>
</li>
<li><p>Support for STDIO transport, allowing local, low-latency communication.</p>
</li>
<li><p>Support for Prompts, Resources, and Tools capabilities.</p>
</li>
<li><p>Protocol-ali<a target="_blank" href="https://pub.dev/packages/dart_mcp?utm_source=chatgpt.com">g</a>ned structure for initialization, schema validation, and request/response handling.</p>
</li>
</ol>
<p><strong>Limitations (as of version 0.3.3):</strong></p>
<ol>
<li><p>HTTP and streamable transports are still experimental.</p>
</li>
<li><p>Authorization and batching aren’t fully supported yet.</p>
</li>
<li><p>The package API may change as it matures.</p>
</li>
</ol>
<h2 id="heading-real-world-use-cases">Real-World Use Cases</h2>
<p>These are some of the situations where MCP moves from “cool” to genuinely useful:</p>
<ol>
<li><p><strong>Fix a runtime UI bug.</strong> The AI inspects runtime logs and the widget tree, then suggests and applies a fix for a <code>RenderFlex</code> overflow.</p>
</li>
<li><p><strong>Add a package and scaffold usage.</strong> You can ask the assistant to add charting, and it searches <code>pub.dev</code>, updates <code>pubspec.yaml</code><a target="_blank" href="https://pub.dev/packages/dart_mcp?utm_source=chatgpt.com">,</a> runs <code>dart pub get</code>, and generates the basic widget usage.</p>
</li>
<li><p><strong>Automated code review.</strong> On each PR, an AI agent runs <code>dart analyze</code> and <code>flutter test</code>, and comments with suggested improvements.</p>
</li>
<li><p><strong>Learning and mentorship.</strong> The tool can inspect a learner’s project and then suggest idiomatic Flutter patterns and add unit tests.</p>
</li>
<li><p><strong>Custom dev tools.</strong> It can build internal tools: for example, “list all routes and generate a navigation test”, exposed as a capability and callable by the assistant.</p>
</li>
</ol>
<h2 id="heading-how-mcp-actually-works">How MCP Actually Works</h2>
<p>Before we dive into the flow, it’s important to understand what’s happening under the hood. MCP defines how an AI assistant communicates securely with your local environment. It enables structured, permission-based interactions between your IDE, AI assistant, and development tools, without giving the model unrestricted access.</p>
<p>Let’s look at an example:</p>
<pre><code class="lang-dart">AI Assistant (LLM)  ⇄  MCP Client (<span class="hljs-keyword">in</span> IDE/agent)  ⇄  Dart/Flutter MCP Server (dart mcp-server)  ⇄  Tools &amp; Codebase
</code></pre>
<ul>
<li><p>The MCP server runs inside your environment and exposes tools (capabilities).</p>
</li>
<li><p>The MCP client (for example, Gemini CLI, GitHub Copilot, Firebase Studio, Cursor) communicates with the server.</p>
</li>
<li><p>The AI issues structured tool calls. The server executes and returns structured results.</p>
</li>
</ul>
<p>You stay in control, and tools are explicit and permissioned (instead of having ephemeral “give everything to the model” access).</p>
<h2 id="heading-getting-started-step-by-step">Getting Started, Step by Step</h2>
<p>Follow the below instructions to go from zero to a working MCP-enabled project.</p>
<h3 id="heading-1-prerequisites">1) Prerequisites</h3>
<p>First, you’ll need to install Dart SDK 3.9+ and Flutter (if you want to experiment with Flutter runtime introspection). The Dart MCP server requires Dart 3.9 or later.</p>
<p>You can use VS Code, IntelliJ, or another editor. Many clients/plugins will integrate with MCP.</p>
<h3 id="heading-2-start-the-dart-amp-flutter-mcp-server-locally">2) Start the Dart &amp; Flutter MCP server locally</h3>
<p>You can run the Dart MCP server with the following command:</p>
<pre><code class="lang-bash">dart mcp-server
</code></pre>
<p>This command launches the MCP server, the component that client tools (like IDEs or AI assistants) connect to in order to communicate with your local environment.</p>
<h3 id="heading-3-configure-an-mcp-client">3) Configure an MCP client</h3>
<p>You can configure clients like Gemini CLI, Firebase Studio, GitHub Copilot and Cursor to talk to your server. Here’s an example for the Gemini CLI (add to <code>~/.gemini/settings.json</code> or project <code>.gemini/settings.json</code>):</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"mcpServers"</span>: {
    <span class="hljs-attr">"dart"</span>: {
      <span class="hljs-attr">"command"</span>: <span class="hljs-string">"dart"</span>,
      <span class="hljs-attr">"args"</span>: [<span class="hljs-string">"mcp-server"</span>]
    }
  }
}
</code></pre>
<p>This tells the client to start the <code>dart mcp-server</code> process and use it as a tool provider.</p>
<h3 id="heading-4-try-an-easy-request-from-your-ide-assistant">4) Try an easy request from your IDE assistant</h3>
<p>Open your project in VS Code (with an AI assistant enabled). Ask something practical, like:</p>
<blockquote>
<p>“Find untested functions and create a test file skeleton for them.”</p>
</blockquote>
<p>The assistant will use MCP tools to inspect code, run analysis, and can generate test scaffolding for you to review.</p>
<h3 id="heading-5-build-a-custom-capability-optional">5) Build a custom capability (optional)</h3>
<p>One of the most powerful aspects of MCP is that you can extend it with your own capabilities. For example, you might want to expose a script that lists all Flutter routes, checks for deprecated APIs, or runs internal code quality checks, all from within your IDE or AI assistant.  </p>
<p>In the example below, a simple Dart MCP server registers a custom tool called <code>list_routes</code>. When the client calls this tool, the server runs a function that scans your project for route definitions and returns them as structured data. This lets your AI assistant interact directly with your codebase in a safe, controlled way.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:dart_mcp_server/dart_mcp_server.dart'</span>;

<span class="hljs-keyword">void</span> main() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> server = McpServer();

  <span class="hljs-comment">// Define a custom capability</span>
  server.registerTool(
    <span class="hljs-string">'list_routes'</span>,
    (context, params) <span class="hljs-keyword">async</span> {
      <span class="hljs-comment">// Example logic: extract all route names in your project</span>
      <span class="hljs-keyword">final</span> routes = <span class="hljs-keyword">await</span> extractRoutesFromProject();
      <span class="hljs-keyword">return</span> {<span class="hljs-string">'routes'</span>: routes};
    },
  );

  <span class="hljs-keyword">await</span> server.start();
}

Future&lt;<span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">String</span>&gt;&gt; extractRoutesFromProject() <span class="hljs-keyword">async</span> {
  <span class="hljs-comment">// Your logic here — e.g., scanning lib/ for route definitions</span>
  <span class="hljs-keyword">return</span> [<span class="hljs-string">'/'</span>, <span class="hljs-string">'/login'</span>, <span class="hljs-string">'/dashboard'</span>];
}
</code></pre>
<p>Once registered, your MCP client (for example, Gemini, Cursor, or Copilot) can call this tool just like any built-in capability, enabling the AI assistant to understand your app’s routes or detect outdated APIs.</p>
<p>Beyond custom scripts, you can tailor MCP to your team’s needs by integrating internal linters, CI scripts, or design system checkers. You can also connect it to internal APIs such as analytics or configuration servers, or create domain-specific commands that reflect how your team builds, tests, and deploys projects. This makes MCP not just a protocol but a flexible foundation you can shape around your workflow.</p>
<h2 id="heading-hands-on-example">Hands-On Example</h2>
<p>Before we look at the code, let’s clarify what it means to <strong>expose an MCP capability in Dart</strong>. In the MCP world, a <strong>capability</strong> is simply a tool or function that an AI assistant can call, for example, to analyze code, read a file, or run a build. <strong>Exposing</strong> a capability means making that tool accessible through a well-defined interface (usually over HTTP or another structured protocol) so the AI or MCP client can request it, execute it, and receive structured results in return.</p>
<p>In this example, you’ll see how to simulate that idea using a small Dart script. Instead of using the full MCP stack, we’ll create a simple local HTTP server that exposes two basic capabilities: <code>analyze</code>, which runs <code>dart analyze</code> on your project, and <code>getFileContent</code>, which reads and returns the contents of a given file.</p>
<p>This shows the same underlying pattern MCP uses: structured requests come in, your server performs an action, and structured responses go back out.</p>
<p>Create a file <code>simple_mcp_server.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// simple_mcp_server.dart</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:convert'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;

Future&lt;<span class="hljs-keyword">void</span>&gt; main() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> server = <span class="hljs-keyword">await</span> HttpServer.bind(InternetAddress.loopbackIPv4, <span class="hljs-number">8081</span>);
  <span class="hljs-built_in">print</span>(<span class="hljs-string">'Simple MCP-like server listening at http://localhost:8081'</span>);

  <span class="hljs-keyword">await</span> <span class="hljs-keyword">for</span> (<span class="hljs-keyword">final</span> request <span class="hljs-keyword">in</span> server) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> body = <span class="hljs-keyword">await</span> utf8.decoder.bind(request).join();
      <span class="hljs-keyword">final</span> data = jsonDecode(body) <span class="hljs-keyword">as</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt;;

      <span class="hljs-keyword">final</span> command = data[<span class="hljs-string">'command'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">String?</span> ?? <span class="hljs-string">''</span>;
      <span class="hljs-keyword">if</span> (command == <span class="hljs-string">'analyze'</span>) {
        <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> Process.run(<span class="hljs-string">'dart'</span>, [<span class="hljs-string">'analyze'</span>]);
        request.response
          ..statusCode = <span class="hljs-number">200</span>
          ..headers.contentType = ContentType.json
          ..write(jsonEncode({<span class="hljs-string">'output'</span>: result.stdout.toString(), <span class="hljs-string">'exitCode'</span>: result.exitCode}));
      } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (command == <span class="hljs-string">'getFileContent'</span>) {
        <span class="hljs-keyword">final</span> path = data[<span class="hljs-string">'args'</span>]?[<span class="hljs-string">'path'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">String?</span>;
        <span class="hljs-keyword">if</span> (path == <span class="hljs-keyword">null</span>) {
          request.response
            ..statusCode = <span class="hljs-number">400</span>
            ..write(jsonEncode({<span class="hljs-string">'error'</span>: <span class="hljs-string">'Missing path'</span>}));
        } <span class="hljs-keyword">else</span> {
          <span class="hljs-keyword">final</span> file = File(path);
          <span class="hljs-keyword">if</span> (!<span class="hljs-keyword">await</span> file.exists()) {
            request.response
              ..statusCode = <span class="hljs-number">404</span>
              ..write(jsonEncode({<span class="hljs-string">'error'</span>: <span class="hljs-string">'File not found'</span>}));
          } <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">final</span> content = <span class="hljs-keyword">await</span> file.readAsString();
            request.response
              ..statusCode = <span class="hljs-number">200</span>
              ..headers.contentType = ContentType.json
              ..write(jsonEncode({<span class="hljs-string">'content'</span>: content}));
          }
        }
      } <span class="hljs-keyword">else</span> {
        request.response
          ..statusCode = <span class="hljs-number">400</span>
          ..write(jsonEncode({<span class="hljs-string">'error'</span>: <span class="hljs-string">'Unknown command'</span>}));
      }
    } <span class="hljs-keyword">catch</span> (e, st) {
      request.response
        ..statusCode = <span class="hljs-number">500</span>
        ..write(jsonEncode({<span class="hljs-string">'error'</span>: e.toString(), <span class="hljs-string">'stack'</span>: st.toString()}));
    } <span class="hljs-keyword">finally</span> {
      <span class="hljs-keyword">await</span> request.response.close();
    }
  }
}
</code></pre>
<p>This Dart script creates a simple local HTTP server that listens for JSON commands on port 8081. It accepts specific commands such as <code>"analyze"</code> and <code>"getFileContent"</code>, executes corresponding actions on your machine, and returns a JSON response.</p>
<p>This is a simplified demonstration of how an MCP (Model Context Protocol) server handles requests and executes tools or actions.</p>
<p>Let’s go through it piece by piece so you understand the code really well.</p>
<h4 id="heading-1-imports">1. Imports</h4>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:convert'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;
</code></pre>
<ul>
<li><p><code>dart:io</code> provides access to file system, processes, and networking features (used here to start the HTTP server and interact with the system).</p>
</li>
<li><p><code>dart:convert</code> allows encoding and decoding of JSON data (used to parse the incoming request body and send structured JSON responses).</p>
</li>
</ul>
<h4 id="heading-2-starting-the-server">2. Starting the server</h4>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> server = <span class="hljs-keyword">await</span> HttpServer.bind(InternetAddress.loopbackIPv4, <span class="hljs-number">8081</span>);
<span class="hljs-built_in">print</span>(<span class="hljs-string">'Simple MCP-like server listening at http://localhost:8081'</span>);
</code></pre>
<p><code>HttpServer.bind</code> starts an HTTP server on the local machine (<code>127.0.0.1</code>) and port <code>8081</code>. The server will only be accessible from your own computer, not the internet, and the message confirms the server is running and listening for incoming requests.</p>
<h4 id="heading-3-handling-requests">3. Handling requests</h4>
<pre><code class="lang-dart"><span class="hljs-keyword">await</span> <span class="hljs-keyword">for</span> (<span class="hljs-keyword">final</span> request <span class="hljs-keyword">in</span> server) {
</code></pre>
<p>This continuously listens for incoming HTTP requests. Each request triggers a new iteration of the loop, allowing multiple requests over time.</p>
<h4 id="heading-4-reading-the-request-body">4. Reading the request body</h4>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> body = <span class="hljs-keyword">await</span> utf8.decoder.bind(request).join();
<span class="hljs-keyword">final</span> data = jsonDecode(body) <span class="hljs-keyword">as</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt;;
</code></pre>
<p>This reads the full request body (assuming it’s UTF-8 encoded text) and converts the JSON string into a Dart map (<code>data</code>) so it can be accessed programmatically.</p>
<p>Example expected input:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"command"</span>: <span class="hljs-string">"analyze"</span>
}
</code></pre>
<h4 id="heading-5-parsing-and-routing-the-command">5. Parsing and routing the command</h4>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> command = data[<span class="hljs-string">'command'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">String?</span> ?? <span class="hljs-string">''</span>;
</code></pre>
<p>This extracts the <code>command</code> key from the request body. If it’s missing or null, it defaults to an empty string.</p>
<p>The server uses this value to determine what action to perform.</p>
<h4 id="heading-6-handling-the-analyze-command">6. Handling the "analyze" command</h4>
<pre><code class="lang-dart"><span class="hljs-keyword">if</span> (command == <span class="hljs-string">'analyze'</span>) {
  <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> Process.run(<span class="hljs-string">'dart'</span>, [<span class="hljs-string">'analyze'</span>]);
  request.response
    ..statusCode = <span class="hljs-number">200</span>
    ..headers.contentType = ContentType.json
    ..write(jsonEncode({<span class="hljs-string">'output'</span>: result.stdout.toString(), <span class="hljs-string">'exitCode'</span>: result.exitCode}));
}
</code></pre>
<p>If the command is <code>"analyze"</code>, the script runs the terminal command <code>dart analyze</code> using <code>Process.run()</code>. This checks your Dart project for errors, warnings, or lints. The output of that command (<code>stdout</code>) and its exit code are sent back as JSON in the HTTP response.</p>
<p>Expected response example:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"output"</span>: <span class="hljs-string">"Analyzing project...\nNo issues found!"</span>,
  <span class="hljs-attr">"exitCode"</span>: <span class="hljs-number">0</span>
}
</code></pre>
<h4 id="heading-7-handling-the-getfilecontent-command">7. Handling the "getFileContent" command</h4>
<pre><code class="lang-dart"><span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (command == <span class="hljs-string">'getFileContent'</span>) {
  <span class="hljs-keyword">final</span> path = data[<span class="hljs-string">'args'</span>]?[<span class="hljs-string">'path'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">String?</span>;
</code></pre>
<p>This command expects an <code>"args"</code> object containing a <code>"path"</code> key.</p>
<p>Example request:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"command"</span>: <span class="hljs-string">"getFileContent"</span>,
  <span class="hljs-attr">"args"</span>: { <span class="hljs-attr">"path"</span>: <span class="hljs-string">"lib/main.dart"</span> }
}
</code></pre>
<p>The rest of the block:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">if</span> (path == <span class="hljs-keyword">null</span>) {
  request.response
    ..statusCode = <span class="hljs-number">400</span>
    ..write(jsonEncode({<span class="hljs-string">'error'</span>: <span class="hljs-string">'Missing path'</span>}));
} <span class="hljs-keyword">else</span> {
  <span class="hljs-keyword">final</span> file = File(path);
  <span class="hljs-keyword">if</span> (!<span class="hljs-keyword">await</span> file.exists()) {
    request.response
      ..statusCode = <span class="hljs-number">404</span>
      ..write(jsonEncode({<span class="hljs-string">'error'</span>: <span class="hljs-string">'File not found'</span>}));
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">final</span> content = <span class="hljs-keyword">await</span> file.readAsString();
    request.response
      ..statusCode = <span class="hljs-number">200</span>
      ..headers.contentType = ContentType.json
      ..write(jsonEncode({<span class="hljs-string">'content'</span>: content}));
  }
}
</code></pre>
<p>If no path is provided, it returns an HTTP 400 error. If the file doesn’t exist, it returns a 404. And if the file exists, it reads its content and sends it back in JSON format.</p>
<p>Example response:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"content"</span>: <span class="hljs-string">"void main() { print('Hello World'); }"</span>
}
</code></pre>
<h4 id="heading-8-handling-unknown-commands">8. Handling unknown commands</h4>
<pre><code class="lang-dart"><span class="hljs-keyword">else</span> {
  request.response
    ..statusCode = <span class="hljs-number">400</span>
    ..write(jsonEncode({<span class="hljs-string">'error'</span>: <span class="hljs-string">'Unknown command'</span>}));
}
</code></pre>
<p>If the <code>command</code> field does not match any known options, the server returns an error.</p>
<h4 id="heading-9-error-handling">9. Error handling</h4>
<pre><code class="lang-dart">} <span class="hljs-keyword">catch</span> (e, st) {
  request.response
    ..statusCode = <span class="hljs-number">500</span>
    ..write(jsonEncode({<span class="hljs-string">'error'</span>: e.toString(), <span class="hljs-string">'stack'</span>: st.toString()}));
}
</code></pre>
<p>If any unhandled exception occurs (such as invalid JSON or runtime errors), the server catches it. It responds with a 500 status and includes both the error message and stack trace for debugging.</p>
<h4 id="heading-10-closing-the-response">10. Closing the response</h4>
<pre><code class="lang-dart"><span class="hljs-keyword">finally</span> {
  <span class="hljs-keyword">await</span> request.response.close();
}
</code></pre>
<p>Ensures that the response is properly closed after every request to prevent resource leaks.</p>
<h4 id="heading-summary">Summary</h4>
<p>This is a local HTTP server that mimics a very basic MCP workflow. It accepts commands over HTTP, performs system or file operations, and returns structured JSON results. It demonstrates how an AI assistant could interact with a Dart environment programmatically (for example, by analyzing code or reading files) through a safe, structured protocol.</p>
<p><strong>How to run it:</strong></p>
<ol>
<li><p>Save the file in the root of a Dart project.</p>
</li>
<li><p>Run <code>dart run simple_mcp_server.dart</code>.</p>
</li>
<li><p>In another terminal, test the <code>analyze</code> command:</p>
</li>
</ol>
<pre><code class="lang-bash">curl -X POST http://localhost:8081 -H <span class="hljs-string">"Content-Type: application/json"</span> -d <span class="hljs-string">'{"command":"analyze"}'</span>
</code></pre>
<p>You’ll get back the analyzer output as JSON. In a real MCP workflow, the AI client would make similarly structured calls, except those calls are managed by an MCP client and follow the MCP spec for tools/resources/roots. The official Dart MCP server provides many more built-in tools and a full implementation that integrates with supported clients.</p>
<h2 id="heading-how-to-build-a-simple-mcp-server-in-dart">How to Build a Simple MCP Server in Dart</h2>
<p>In the previous section, we built a simplified, conceptual version of an MCP-like server using plain Dart and HTTP. That example helped illustrate the basic idea: receiving structured requests, executing specific actions, and returning structured results.</p>
<p>Now, let’s take that concept further and see how to build a proper MCP server using the official <code>dart_mcp</code> package. This version follows the real MCP specification and can interact with actual MCP clients, giving you a foundation for extending, testing, or customizing how your development tools communicate with the AI assistant.</p>
<h3 id="heading-step-1-add-dependency">Step 1: Add Dependency</h3>
<p>Add this to your <code>pubspec.yaml</code>:</p>
<pre><code class="lang-dart">dependencies:
  dart_mcp: ^<span class="hljs-number">0.3</span><span class="hljs-number">.3</span>
</code></pre>
<h3 id="heading-step-2-create-the-server">Step 2: Create the Server</h3>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:dart_mcp/server.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyServer</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">MCPServer</span> <span class="hljs-title">with</span> <span class="hljs-title">ToolsSupport</span>, <span class="hljs-title">ResourcesSupport</span> </span>{
  MyServer()
      : <span class="hljs-keyword">super</span>(Implementation(
          name: <span class="hljs-string">'my-dart-mcp-server'</span>,
          version: <span class="hljs-string">'1.0.0'</span>,
        ));

  <span class="hljs-meta">@override</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; initialize() <span class="hljs-keyword">async</span> {
    <span class="hljs-comment">// Register a simple tool</span>
    registerTool(
      <span class="hljs-string">'analyzeCode'</span>,
      description: <span class="hljs-string">'Analyze Dart code using the analyzer.'</span>,
      inputSchema: {
        <span class="hljs-string">'type'</span>: <span class="hljs-string">'object'</span>,
        <span class="hljs-string">'properties'</span>: {
          <span class="hljs-string">'path'</span>: {<span class="hljs-string">'type'</span>: <span class="hljs-string">'string'</span>}
        },
        <span class="hljs-string">'required'</span>: [<span class="hljs-string">'path'</span>]
      },
      callback: (args, extra) <span class="hljs-keyword">async</span> {
        <span class="hljs-keyword">final</span> path = args[<span class="hljs-string">'path'</span>];
        <span class="hljs-comment">// You can call `dart analyze` here or integrate analyzer APIs</span>
        <span class="hljs-keyword">return</span> {<span class="hljs-string">'message'</span>: <span class="hljs-string">'Analyzed project at <span class="hljs-subst">$path</span> successfully.'</span>};
      },
    );

    <span class="hljs-keyword">await</span> <span class="hljs-keyword">super</span>.initialize();
  }
}

<span class="hljs-keyword">void</span> main() {
  <span class="hljs-keyword">final</span> server = MyServer();
  server.connect(StdioServerTransport());
}
</code></pre>
<p>This Dart code defines a basic MCP server using the official <code>dart_mcp</code> package.</p>
<p>It’s a minimal working example that demonstrates how to create a custom MCP server, register a command (“tool”) that AI assistants or clients can call, and expose it over a local connection (using standard input/output, <code>stdio</code>).</p>
<p>Let’s break it down line by line.</p>
<p><strong>1. Import the package</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:dart_mcp/server.dart'</span>;
</code></pre>
<p>This imports the server-side APIs from the <code>dart_mcp</code> package. These APIs allow you to create and configure an MCP server, register tools (commands) and resources, and handle incoming requests from MCP clients (for example, editors or AI assistants).</p>
<p><strong>2. Create a server class</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyServer</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">MCPServer</span> <span class="hljs-title">with</span> <span class="hljs-title">ToolsSupport</span>, <span class="hljs-title">ResourcesSupport</span> </span>{
</code></pre>
<p>This defines a custom class named <code>MyServer</code> that extends <code>MCPServer</code>.</p>
<p><code>MCPServer</code> is the base class that manages communication, initialization, and capability discovery. The <code>with</code> keywords mix in additional capabilities. <code>ToolsSupport</code> allows you to register tools, callable commands that perform actions. <code>ResourcesSupport</code> allows you to register resources, accessible data like project files or datasets.</p>
<p>So this server supports both tools (commands) and resources (data).</p>
<p><strong>3. The constructor</strong></p>
<pre><code class="lang-dart">MyServer()
    : <span class="hljs-keyword">super</span>(Implementation(
        name: <span class="hljs-string">'my-dart-mcp-server'</span>,
        version: <span class="hljs-string">'1.0.0'</span>,
      ));
</code></pre>
<p>Here, the constructor passes information about the implementation to the parent <code>MCPServer</code> class.</p>
<p><code>Implementation</code> is a metadata object that describes the server, including:</p>
<ul>
<li><p><code>name</code>, a unique name for your server, and</p>
</li>
<li><p><code>version</code>, the version number.</p>
</li>
</ul>
<p>This metadata helps clients identify which MCP server they’re communicating with.</p>
<p><strong>4. Overriding the initialization method</strong></p>
<pre><code class="lang-dart"><span class="hljs-meta">@override</span>
Future&lt;<span class="hljs-keyword">void</span>&gt; initialize() <span class="hljs-keyword">async</span> {
</code></pre>
<p>This method runs when the server starts up. It’s where you register tools and resources before the server begins listening for commands.</p>
<p><strong>5. Registering a tool</strong></p>
<pre><code class="lang-dart">registerTool(
  <span class="hljs-string">'analyzeCode'</span>,
  description: <span class="hljs-string">'Analyze Dart code using the analyzer.'</span>,
  inputSchema: {
    <span class="hljs-string">'type'</span>: <span class="hljs-string">'object'</span>,
    <span class="hljs-string">'properties'</span>: {
      <span class="hljs-string">'path'</span>: {<span class="hljs-string">'type'</span>: <span class="hljs-string">'string'</span>}
    },
    <span class="hljs-string">'required'</span>: [<span class="hljs-string">'path'</span>]
  },
  callback: (args, extra) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> path = args[<span class="hljs-string">'path'</span>];
    <span class="hljs-comment">// You can call `dart analyze` here or integrate analyzer APIs</span>
    <span class="hljs-keyword">return</span> {<span class="hljs-string">'message'</span>: <span class="hljs-string">'Analyzed project at <span class="hljs-subst">$path</span> successfully.'</span>};
  },
);
</code></pre>
<p>This section defines a tool called <code>"analyzeCode"</code>.</p>
<p>Let’s explain each part:</p>
<ul>
<li><p><code>'analyzeCode'</code>: the tool’s name (how a client identifies it).</p>
</li>
<li><p><code>description</code>: a short explanation of what the tool does.</p>
</li>
<li><p><code>inputSchema</code>: a JSON schema that defines what input this tool expects. It expects an object with one property <code>"path"</code>, which must be a string.</p>
</li>
<li><p><code>callback</code>: a function that runs when the client calls this tool.</p>
</li>
<li><p><code>args</code> contains the client’s input, and you can use <code>args['path']</code> to access the provided path. In a real implementation, you could call <code>dart analyze</code> or use the Dart analyzer APIs to check the code at that path. The callback returns a response (in this case, a success message).</p>
</li>
</ul>
<p>This tool can later be invoked by a connected MCP client, for example:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"command"</span>: <span class="hljs-string">"analyzeCode"</span>,
  <span class="hljs-attr">"args"</span>: { <span class="hljs-attr">"path"</span>: <span class="hljs-string">"lib/"</span> }
}
</code></pre>
<p>And the server would respond:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"message"</span>: <span class="hljs-string">"Analyzed project at lib/ successfully."</span>
}
</code></pre>
<p><strong>6. Call the parent initializer</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">await</span> <span class="hljs-keyword">super</span>.initialize();
</code></pre>
<p>This ensures that the base class (<code>MCPServer</code>) performs its own initialization logic after your custom setup (for example, registering built-in tools or preparing internal structures).</p>
<p><strong>7. Main entry point</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> main() {
  <span class="hljs-keyword">final</span> server = MyServer();
  server.connect(StdioServerTransport());
}
</code></pre>
<p>This is the entry point of the application. It creates an instance of your <code>MyServer</code> class. Then it connects using <code>StdioServerTransport()</code> which allows the server to communicate via standard input/output (stdio), the same mechanism used by local AI assistants and command-line tools.</p>
<p>In practice, this means the server doesn’t need to run an HTTP server. It can talk directly to other local tools that use MCP, such as IDE extensions or AI assistants that launch it.</p>
<p>This kind of MCP server could be connected to an IDE like VS Code or JetBrains via MCP to run Dart analysis automatically. It would let an AI assistant access your local Dart project, analyze files, and return insights, serving as a bridge between your Flutter project and external automation tools.</p>
<p>It’s a simple example that creates a command (<code>analyzeCode</code>) that an MCP client (like an AI assistant) can call. The client will send input through the MCP protocol, and your Dart server responds accordingly.</p>
<h2 id="heading-connecting-to-your-mcp-server-with-a-client">Connecting to Your MCP Server with a Client</h2>
<p>Now that you’ve built a simple MCP server, the next logical step is to see how a client interacts with it. The client is the other half of the equation: it connects to your server, initializes communication, and calls the tools (capabilities) you’ve registered.</p>
<p>The example below shows how to create a basic Dart-based MCP client that talks to the server you built earlier and invokes one of its tools.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:dart_mcp/client.dart'</span>;

<span class="hljs-keyword">void</span> main() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> client = <span class="hljs-keyword">await</span> MCPClient.connectStdioServer(stdin, stdout);
  <span class="hljs-keyword">final</span> initResult = <span class="hljs-keyword">await</span> client.initialize(
    Implementation(name: <span class="hljs-string">'my-client'</span>, version: <span class="hljs-string">'1.0.0'</span>),
  );

  <span class="hljs-built_in">print</span>(<span class="hljs-string">'Connected to server: <span class="hljs-subst">${initResult.serverCapabilities.tools}</span>'</span>);
  <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> client.callTool(<span class="hljs-string">'analyzeCode'</span>, {<span class="hljs-string">'path'</span>: <span class="hljs-string">'lib/'</span>});
  <span class="hljs-built_in">print</span>(result);
}
</code></pre>
<p>This code creates a simple MCP client that connects to an MCP server (like the one you built earlier) and calls one of its registered tools.</p>
<p>Here’s what it does, step by step:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:dart_mcp/client.dart'</span>;
</code></pre>
<p>This imports the client-side API from the <code>dart_mcp</code> package. This lets you connect to an MCP server and call its tools.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> client = <span class="hljs-keyword">await</span> MCPClient.connectStdioServer(stdin, stdout);
</code></pre>
<p>This creates a new MCP client and connects to a server using standard input/output (stdio). Again, this is how local tools or AI assistants communicate with MCP servers running on your system.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> initResult = <span class="hljs-keyword">await</span> client.initialize(
  Implementation(name: <span class="hljs-string">'my-client'</span>, version: <span class="hljs-string">'1.0.0'</span>),
);
</code></pre>
<p>This sends an initialization request to the server. The <code>Implementation</code> object identifies this client by name and version. The server responds with its available capabilities (like registered tools and resources).</p>
<pre><code class="lang-dart"><span class="hljs-built_in">print</span>(<span class="hljs-string">'Connected to server: <span class="hljs-subst">${initResult.serverCapabilities.tools}</span>'</span>);
</code></pre>
<p>This prints the list of tools that the server has registered, for example, <code>["analyzeCode"]</code>.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> client.callTool(<span class="hljs-string">'analyzeCode'</span>, {<span class="hljs-string">'path'</span>: <span class="hljs-string">'lib/'</span>});
</code></pre>
<p>This calls the server’s <code>analyzeCode</code> tool, passing <code>{'path': 'lib/'}</code> as the input. The server runs the callback registered for that tool and returns a response.</p>
<pre><code class="lang-dart"><span class="hljs-built_in">print</span>(result);
</code></pre>
<p>And this prints the result returned by the server (for example: <code>{"message": "Analyzed project at lib/ successfully."}</code>).</p>
<p><strong>Summary:</strong><br>This client connects to an MCP server over stdio, initializes communication, lists available tools, calls one (<code>analyzeCode</code>), and prints the response. It’s the client-side counterpart of the earlier server example. Together, they demonstrate how two Dart programs can communicate using the MCP protocol.</p>
<p>This connects to the MCP server through stdin/stdout, initializes communication, and calls the <code>analyzeCode</code> tool.</p>
<h2 id="heading-mcp-vs-custom-http-servers">MCP vs. Custom HTTP Servers</h2>
<p>If you’ve ever built a simple Dart HTTP server that handles commands like “analyze” or “getFileContent,” you’ve already done something similar to MCP. The difference is that MCP provides a formal structure and protocol that standardizes how such interactions occur.</p>
<p>Instead of manual JSON parsing and ad-hoc commands, the MCP layer handles:</p>
<ul>
<li><p>Tool registration and discovery</p>
</li>
<li><p>Schema-based validation</p>
</li>
<li><p>Standardized request and response types</p>
</li>
<li><p>Built-in initialization and capabilities negotiation</p>
</li>
</ul>
<p>So while a custom HTTP approach works for quick experiments, <code>dart_mcp</code> lets you build compliant, future-proof MCP tools that integrate cleanly with editors and assistants.</p>
<h2 id="heading-best-practices-safety-and-permissions">Best Practices, Safety, and Permissions</h2>
<ul>
<li><p><strong>Start read-only.</strong> Give the assistant tools that read project state first (analyze, read file, run tests). Only enable write actions (edit files, git commit) after you trust the automation.</p>
</li>
<li><p><strong>Review every change.</strong> Even if the AI can apply fixes, treat it as a co-author: inspect diffs and run your tests.</p>
</li>
<li><p><strong>Limit scopes.</strong> Don’t expose secrets or keys to the MCP server. Use environment separation (dev vs. CI) and explicit capability gating.</p>
</li>
<li><p><strong>Audit logs.</strong> Keep logs for MCP calls and changes made by agents so you can trace who did what and when.</p>
</li>
<li><p><strong>Set up team rules.</strong> Define team policies for automated edits, for example, “AI can apply formatting and minor lint fixes, but not major architectural changes without human approval.”</p>
</li>
</ul>
<h2 id="heading-getting-started-as-a-beginner">Getting Started as a Beginner</h2>
<p>If you’re new to this space, here’s a simple roadmap:</p>
<ol>
<li><p><strong>Read about MCP basics:</strong> Visit the <a target="_blank" href="https://dart.dev/tools/mcp-server">official Dart MCP</a> page to understand what it is and where it fits in your workflow.</p>
</li>
<li><p><strong>Install and explore</strong> <code>dart_mcp</code>: Try running one of the examples on <a target="_blank" href="https://pub.dev/packages/dart_mcp/example">pub.dev</a>. Experiment with building your own simple tool.</p>
</li>
<li><p><strong>Connect it with your AI assistant or IDE:</strong> Tools like Gemini or VS Code MCP plugins allow you to register your local Dart MCP server in the <code>mcpServers</code> configuration file.</p>
</li>
<li><p><strong>Expand your tools:</strong> Build more complex commands like “run tests,” “format code,” “fetch dependencies,” or “generate reports.”</p>
</li>
<li><p><strong>Contribute or follow the Dart Labs repo:</strong> MCP support in Dart is evolving rapidly. Keeping up with updates helps you stay ahead.</p>
</li>
</ol>
<h2 id="heading-moving-beyond-beginner-level">Moving Beyond Beginner Level</h2>
<p>Once you understand the basics:</p>
<ul>
<li><p>Start integrating your MCP tools into Flutter development pipelines.</p>
</li>
<li><p>Build an AI-powered assistant that interacts directly with your Flutter project files.</p>
</li>
<li><p>Explore the MCP GitHub discussions and OpenAI’s model context spec for deeper insights.</p>
</li>
<li><p>You can even contribute your own MCP-based package to the community.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The Model Context Protocol is shaping the next generation of developer tools, enabling smarter, AI-driven, and more integrated workflows. As a Dart or Flutter developer, learning MCP now positions you ahead of the curve.</p>
<p>By leveraging the <code>dart_mcp</code> package, you can start building compliant, extensible, and automated tools today, transforming how your development environment interacts with code, analysis, and AI.</p>
<h2 id="heading-references">References</h2>
<ul>
<li><p>"<a target="_blank" href="https://dart.dev/tools/mcp-server">Dart and Flutter MCP Server” (official docs)</a></p>
</li>
<li><p><a target="_blank" href="https://pub.dev/packages/dart_mcp"><code>dart_mcp</code> package</a> on pub.dev</p>
</li>
<li><p>Medium article “<a target="_blank" href="https://medium.com/flutter/supercharge-your-dart-flutter-development-experience-with-the-dart-mcp-server-2edcc8107b49">Supercharge Your Dart &amp; Flutter Development Experience with the Dart and Flutter MCP Server</a>”</p>
</li>
<li><p><a target="_blank" href="https://github.com/its-dart/dart-mcp-server">GitHub repository</a> for a Dart MCP server implementation</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a To-Do List MCP Server Using TypeScript – with Auth, Database, and Billing ]]>
                </title>
                <description>
                    <![CDATA[ In this tutorial, you’ll build a To-Do list MCP server using TypeScript. You’ll learn how to implement authentication, persistence, and billing, to make the server robust and functional for real users. By the end, you’ll have a working MCP server tha... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-to-do-list-mcp-server-using-typescript/</link>
                <guid isPermaLink="false">68f93792b7f64a597dc407f9</guid>
                
                    <category>
                        <![CDATA[ mcp ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mcp server ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Shola Jegede ]]>
                </dc:creator>
                <pubDate>Wed, 22 Oct 2025 19:59:14 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761162036666/77972b3f-9dc8-404f-b40d-fb70ee73e2a5.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this tutorial, you’ll build a To-Do list MCP server using TypeScript. You’ll learn how to implement authentication, persistence, and billing, to make the server robust and functional for real users.</p>
<p>By the end, you’ll have a working MCP server that:</p>
<ul>
<li><p>Authenticates users with Kinde.</p>
</li>
<li><p>Stores to-do data in a Neon Postgres database.</p>
</li>
<li><p>Enforces billing limits and supports upgrades.</p>
</li>
<li><p>Exposes all these features as MCP tools inside Cursor.</p>
</li>
</ul>
<p>This article will walk you through each step, helping you understand design decisions that you can adapt for your own projects.</p>
<h2 id="heading-what-youll-learn">What You’ll Learn</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-why-go-beyond-basic-mcp-servers">Why Go Beyond Basic MCP Servers</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-youll-build">What You’ll Build</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-database-setup-with-neon-postgresql">Database Setup with Neon PostgreSQL</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-connect-your-neon-database">1. Connect your Neon Database</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-create-your-db-file">2. Create your DB File</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-step-by-step-breakdown-of-setup-dbts">3. Step-by-Step Breakdown of setup-db.ts</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-full-setup-dbts-file">4. Full setup-db.ts File</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-authentication-with-kinde">Authentication with Kinde</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-create-a-kinde-application">1. Create a Kinde Application</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-configure-kinde-settings">2. Configure Kinde Settings</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-environment-variables">3. Environment Variables</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-create-the-kinde-auth-server">4. Create the Kinde Auth Server</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-5-complete-authentication-flow">5. Complete Authentication Flow</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-6-why-this-matters">6. Why This Matters</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-7-key-connections">7. Key Connections</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-8-full-kinde-auth-serverts-file">8. Full kinde-auth-server.ts File</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-mcp-server-implementation-with-billing-system-integration">MCP Server Implementation (with Billing System Integration)</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-create-your-file">1. Create Your File</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-project-setup-and-imports">2. Project Setup and Imports</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-database-connection-and-configuration">3. Database Connection and Configuration</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-authentication-system">4. Authentication System</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-5-core-helper-functions">5. Core Helper Functions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-6-core-server-implementation">6. Core Server Implementation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-7-register-tools">7. Register Tools</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-8-tool-handlers">8. Tool Handlers</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-9-full-serverts-file">9. Full server.ts File</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-10-data-flow-amp-integration">10. Data Flow &amp; Integration</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-11-error-handling-amp-security">11. Error Handling &amp; Security</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-12-testing-amp-deployment">12. Testing &amp; Deployment</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-testing-the-complete-system">Testing the Complete System</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-start-the-services">1. Start the Services</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-configure-cursor-mcp">2. Configure Cursor MCP</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-test-the-complete-flow">3. Test the Complete Flow</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-troubleshooting">Troubleshooting</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-mcp-server-not-detected">1. MCP Server Not Detected</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-database-connection-issues">2. Database Connection Issues</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-kinde-authentication-problems">3. Kinde Authentication Problems</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-token-errors">4. Token Errors</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-final-mcp-server-architecture">Final MCP Server Architecture</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-next-steps">Next Steps</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-resources">Resources</a></p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-why-go-beyond-basic-mcp-servers">Why Go Beyond Basic MCP Servers?</h2>
<p>If you read this <a target="_blank" href="https://www.freecodecamp.org/news/how-to-build-a-custom-mcp-server-with-typescript-a-handbook-for-developers">freeCodeCamp MCP handbook</a>, you learned how to set up a simple MCP server in TypeScript. That’s useful for learning the protocol, but it doesn’t reflect what you need in production.</p>
<p>A real application requires:</p>
<ul>
<li><p><strong>Authentication</strong> so each user has their own data and permissions.</p>
</li>
<li><p><strong>Persistence</strong> so data is stored in a reliable database.</p>
</li>
<li><p><strong>Billing</strong> so you can enforce limits and monetize usage.</p>
</li>
</ul>
<p>Without these, an MCP server is just a demo.</p>
<h2 id="heading-what-youll-build">What You’ll Build</h2>
<p>In this tutorial, you’ll build a to-do MCP server with TypeScript that includes the essentials of a production-ready backend:</p>
<ul>
<li><p><strong>Authentication</strong> with Kinde</p>
</li>
<li><p><strong>Database persistence</strong> with Neon Postgres</p>
</li>
<li><p><strong>Billing enforcement</strong> with a free tier and upgrade path</p>
</li>
<li><p><strong>MCP tool exposure</strong> so all of this works seamlessly</p>
</li>
</ul>
<p>By the end, you’ll have an MCP server that feels more like the backend of a SaaS app and a template you can extend for your own ideas.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before we start, you'll need:</p>
<p><strong>Accounts &amp; Services (all free to use):</strong></p>
<ul>
<li><p><a target="_blank" href="https://kinde.com">Kinde Account</a> <strong>→</strong> for authentication and billing</p>
</li>
<li><p><a target="_blank" href="https://neon.com">Neon Account</a> <strong>→</strong> for PostgreSQL database</p>
</li>
<li><p>Node.js (v18+) (<a target="_blank" href="https://nodejs.org/en/download">download</a>)</p>
</li>
<li><p>Cursor IDE <strong>→</strong> for MCP integration and tool testing (<a target="_blank" href="https://cursor.com/download">download</a>)</p>
</li>
</ul>
<p><strong>Development Tools:</strong></p>
<ul>
<li><p>Terminal/Command line access</p>
</li>
<li><p>Git (optional, for version control)</p>
</li>
</ul>
<h2 id="heading-project-setup">Project Setup</h2>
<p>First, create a new folder:</p>
<pre><code class="lang-powershell">mkdir todo<span class="hljs-literal">-mcp</span><span class="hljs-literal">-server</span>
<span class="hljs-built_in">cd</span> todo<span class="hljs-literal">-mcp</span><span class="hljs-literal">-server</span>
</code></pre>
<p>Then initialize a Node.js project:</p>
<pre><code class="lang-powershell">npm init <span class="hljs-literal">-y</span>
</code></pre>
<p>Next, install the dependencies your server will need:</p>
<pre><code class="lang-powershell">npm install @modelcontextprotocol/sdk @neondatabase/serverless @kinde<span class="hljs-literal">-oss</span>/kinde<span class="hljs-literal">-typescript</span><span class="hljs-literal">-sdk</span> express jsonwebtoken jwks<span class="hljs-literal">-client</span> express<span class="hljs-literal">-session</span>
</code></pre>
<p>The <code>@modelcontextprotocol/sdk</code> package gives us everything we need to build and expose MCP servers and tools. We’re using <code>@neondatabase/serverless</code> to connect to a Neon Postgres database, and <code>@kinde-oss/kinde-typescript-sdk</code> handles authentication and billing through Kinde.</p>
<p>We’ll also install <code>express</code>, which makes it easy to define routes and handle middleware. To verify user tokens from Kinde, we’ll use <code>jsonwebtoken</code> together with <code>jwks-client</code>. And finally, <code>express-session</code> will take care of managing session state so users can stay logged in across requests.</p>
<p>Next, set up TypeScript and a few type definitions for development:</p>
<pre><code class="lang-powershell">npm install <span class="hljs-literal">-D</span> typescript @types/node @types/express @types/express<span class="hljs-literal">-session</span> tsx
</code></pre>
<p>The <code>typescript</code> package enables TypeScript in your project so you can write strongly typed code. The <code>@types/*</code> packages provide type definitions for Node.js, Express, and the session middleware, giving you better autocomplete and error checking in your editor.</p>
<p>Finally, <code>tsx</code> makes it super easy to run TypeScript files directly without the need to pre-compile them before running your app.</p>
<p>Then create a <code>.env</code> file in your project root and paste these variables:</p>
<pre><code class="lang-json"># Database
DATABASE_URL=postgresql:<span class="hljs-comment">//user:pass@host:port/db</span>

# Kinde Authentication
KINDE_ISSUER_URL=https:<span class="hljs-comment">//your-domain.kinde.com</span>
KINDE_CLIENT_ID=your_client_id
KINDE_CLIENT_SECRET=your_client_secret

# Security
JWT_SECRET=your_secret_key

# Environment
NODE_ENV=development
</code></pre>
<p>This stores all the credentials that you’ll be using for this project.</p>
<p>Next, create a <code>tsconfig.json</code> in the project root to tell the TypeScript compiler how to handle your code:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"target"</span>: <span class="hljs-string">"ES2022"</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"ESNext"</span>,
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"node"</span>,
    <span class="hljs-attr">"strict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"esModuleInterop"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"skipLibCheck"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"forceConsistentCasingInFileNames"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"outDir"</span>: <span class="hljs-string">"./dist"</span>,
    <span class="hljs-attr">"rootDir"</span>: <span class="hljs-string">"./src"</span>
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"src/**/*"</span>],
  <span class="hljs-attr">"exclude"</span>: [<span class="hljs-string">"node_modules"</span>, <span class="hljs-string">"dist"</span>]
}
</code></pre>
<p>Finally, update your <code>package.json</code> scripts</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"tsc"</span>,
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"tsx src/server.ts"</span>,
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"node dist/server.js"</span>,
    <span class="hljs-attr">"auth-server"</span>: <span class="hljs-string">"tsx src/kinde-auth-server.ts"</span>,
    <span class="hljs-attr">"setup-db"</span>: <span class="hljs-string">"tsx src/setup-db.ts"</span>
  }
}
</code></pre>
<h2 id="heading-database-setup-with-neon-postgresql">Database Setup with Neon PostgreSQL</h2>
<p>To power your to-do MCP server, you’ll use Neon, a serverless PostgreSQL platform. This gives us a fully managed, scalable database without worrying about infrastructure.</p>
<h3 id="heading-1-connect-your-neon-database">1. Connect your Neon Database</h3>
<ul>
<li><p>Sign up or log in to your <a target="_blank" href="https://console.neon.tech">Neon account console</a>.</p>
</li>
<li><p>Create a new project.</p>
</li>
<li><p>Copy the connection string, you’ll need it in your <code>.env</code> file.</p>
</li>
</ul>
<h3 id="heading-2-create-your-db-file">2. Create your DB File</h3>
<p>Inside your project, create a new file in your <code>src/</code> folder and name it <code>setup-db.ts</code>. This file will create the tables, indexes, and schema your app relies on.</p>
<p>Database Architecture Overview:</p>
<pre><code class="lang-markdown">┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   setup-db.ts   │───▶│  Neon Database  │───▶│   PostgreSQL    │
│   (Schema)      │    │   (Serverless)  │    │   (Tables)      │
└─────────────────┘    └─────────────────┘    └─────────────────┘
</code></pre>
<h3 id="heading-3-step-by-step-breakdown-of-setup-dbts">3. Step-by-Step Breakdown of <code>setup-db.ts</code></h3>
<p><strong>Step 1: Imports and Database Connection</strong></p>
<p>Start by importing the packages you’ll need and setting up your database connection:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { neon } <span class="hljs-keyword">from</span> <span class="hljs-string">'@neondatabase/serverless'</span>;
<span class="hljs-keyword">import</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">'dotenv'</span>;

dotenv.config();

<span class="hljs-keyword">const</span> sql = neon(process.env.DATABASE_URL!);
</code></pre>
<p>The <code>dotenv</code> package loads your environment variables from a <code>.env</code> file so you don’t have to hardcode secrets in your code. The <code>neon</code> function connects your app to your Neon Postgres database, and the <code>sql</code> variable gives you a clean, type-safe way to run queries.</p>
<p>At this point, your app has everything it needs to talk to the database.</p>
<p><strong>Step 2: Main Setup Function</strong></p>
<p>Now let’s create a function to handle the database setup process:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setupDatabase</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Setting up database schema...'</span>);
  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// Database operations here</span>
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error setting up database:'</span>, error);
    process.exit(<span class="hljs-number">1</span>);
  }
}
</code></pre>
<p>This function keeps all your schema creation logic in one place, making it easy to manage. It also catches and logs any errors instead of failing silently, so you’ll immediately know if something goes wrong. The console messages give you real-time feedback as the setup runs which is super helpful when you’re debugging or deploying.</p>
<p><strong>Step 3: To-Dos Table</strong></p>
<p>Next, create a table to store all user to-dos:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">await</span> sql<span class="hljs-string">`
  CREATE TABLE IF NOT EXISTS todos (
    id SERIAL PRIMARY KEY,
    user_id TEXT NOT NULL,
    title TEXT NOT NULL,
    description TEXT,
    completed BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  )
`</span>;
</code></pre>
<p>This table holds every user’s tasks. The <code>user_id</code> column links each to-do to the user who created it, while the <code>completed</code> field tracks whether a task is done or still pending. The automatic <code>created_at</code> and <code>updated_at</code> timestamps make it easy to sort tasks or track their history over time.</p>
<p><strong>Step 4: Users Table</strong></p>
<p>Now, let’s define a table to manage user accounts and billing details:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">await</span> sql<span class="hljs-string">`
  CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    user_id TEXT UNIQUE NOT NULL,
    name TEXT,
    email TEXT,
    subscription_status TEXT DEFAULT 'free' CHECK (subscription_status IN ('free', 'active', 'cancelled')),
    plan TEXT DEFAULT 'free',
    free_todos_used INTEGER DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  )
`</span>;
</code></pre>
<p>This table stores each user’s basic information along with their subscription details. The <code>user_id</code> value comes directly from Kinde during authentication. The <code>subscription_status</code> and <code>free_todos_used</code> columns help you enforce billing tiers and limit how many free tasks a user can create before needing to upgrade.</p>
<p><strong>Step 5: Performance Indexes</strong></p>
<p>Next, let’s add a few indexes to make common database operations faster:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">await</span> sql<span class="hljs-string">`
  CREATE INDEX IF NOT EXISTS idx_todos_user_id ON todos(user_id)
`</span>;

<span class="hljs-keyword">await</span> sql<span class="hljs-string">`
  CREATE INDEX IF NOT EXISTS idx_todos_created_at ON todos(created_at)
`</span>;

<span class="hljs-keyword">await</span> sql<span class="hljs-string">`
  CREATE INDEX IF NOT EXISTS idx_users_user_id ON users(user_id)
`</span>;
</code></pre>
<p>These indexes help speed up lookups and sorting. The first one lets the database quickly find to-dos that belong to a specific user. The second makes it faster to sort tasks by their creation date. And the last one allows fast lookups of users based on their Kinde <code>user_id</code>.</p>
<p><strong>Step 6: Success Logging</strong></p>
<p>After everything runs, it’s helpful to log a clear summary of what was created:</p>
<pre><code class="lang-typescript"><span class="hljs-built_in">console</span>.log(<span class="hljs-string">'✅ Database schema created successfully!'</span>);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'📋 Tables created:'</span>);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'  - todos (id, user_id, title, description, completed, created_at, updated_at)'</span>);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'  - users (id, user_id, subscription_status, plan, free_todos_used, created_at, updated_at)'</span>);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'🔍 Indexes created for optimal performance'</span>);
</code></pre>
<p>These logs give you immediate feedback once the setup completes. They show exactly which tables and indexes were created, making it easy to confirm that your database schema is ready to go and everything ran as expected.</p>
<p><strong>Step 7: Error Handling</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">try</span> {
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'❌ Error setting up database:'</span>, error);
  process.exit(<span class="hljs-number">1</span>);
}
</code></pre>
<p>This handles any database setup errors gracefully.</p>
<p><strong>Step 8:</strong> Update your <code>.env</code> file in your project root with your Neon database connection string:</p>
<pre><code class="lang-json">DATABASE_URL=postgresql:<span class="hljs-comment">//username:password@host/database?sslmode=require</span>
</code></pre>
<p><strong>Step 9: Function Execution</strong></p>
<p>At the bottom of <code>setup-db.ts</code>, run the function:</p>
<pre><code class="lang-typescript">setupDatabase();
</code></pre>
<p>This immediately executes the database setup when the script runs.</p>
<p>Now, run this command in your CLI:</p>
<pre><code class="lang-powershell">npm run setup<span class="hljs-literal">-db</span>
</code></pre>
<p>Expected output:</p>
<pre><code class="lang-powershell">🚀 Setting up database schema...
✅ Database schema created successfully!
📋 Tables created:
  - todos (id, user_id, title, description, completed, created_at, updated_at)
  - users (id, user_id, subscription_status, plan, free_todos_used, created_at, updated_at)
🔍 Indexes created <span class="hljs-keyword">for</span> optimal performance
</code></pre>
<h3 id="heading-4-full-setup-dbts-file">4. Full <code>setup-db.ts</code> File</h3>
<p>You can view the complete implementation of the <code>setup-db.ts</code> file in the <a target="_blank" href="https://github.com/sholajegede/todo_mcp_server/blob/main/src/setup-db.ts">GitHub repo</a> and copy it directly into your project.</p>
<h2 id="heading-authentication-with-kinde">Authentication with Kinde</h2>
<p>To secure your MCP server, you’ll use <a target="_blank" href="https://kinde.com">Kinde</a>, an authentication provider that makes it easy to handle logins, user sessions, and tokens.</p>
<p>You’ll also connect Kinde to your Neon database so new users are automatically created when they log in.</p>
<h3 id="heading-1-create-a-kinde-application">1. Create a Kinde Application</h3>
<ul>
<li><p>Go to the <a target="_blank" href="https://app.kinde.com/admin">Kinde Dashboard</a>.</p>
</li>
<li><p>Create a new application.</p>
</li>
<li><p>Note down these values (you’ll use them shortly):</p>
<ul>
<li><p><strong>Domain</strong>: <a target="_blank" href="https://your-domain.kinde.com"><code>https://your-domain.kinde.com</code></a></p>
</li>
<li><p><strong>Client ID</strong>: <code>your-client-id</code></p>
</li>
<li><p><strong>Client Secret</strong>: <code>your-client-secret</code></p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-2-configure-kinde-settings">2. Configure Kinde Settings</h3>
<p>In your Kinde dashboard, save these URLs as your:</p>
<ul>
<li><p>Redirect URL → <a target="_blank" href="http://localhost:3000/callback"><code>http://localhost:3000/callback</code></a></p>
</li>
<li><p>Logout URL → <a target="_blank" href="http://localhost:3000"><code>http://localhost:3000</code></a></p>
</li>
</ul>
<h3 id="heading-3-environment-variables">3. Environment Variables</h3>
<p>Update the <code>.env</code> file in your project root with the credentials from your Kinde Dashboard:</p>
<pre><code class="lang-json">KINDE_ISSUER_URL=https:<span class="hljs-comment">//your-domain.kinde.com</span>
KINDE_CLIENT_ID=your_client_id
KINDE_CLIENT_SECRET=your_client_secret
</code></pre>
<h3 id="heading-4-create-the-kinde-auth-server">4. Create the Kinde Auth Server</h3>
<p>You’ll build an Express server (<code>src/kinde-auth-server.ts</code>) to handle authentication. This server will manage OAuth login and logout with Kinde, store user sessions, and automatically create or update users in your Neon database whenever they sign in.</p>
<p><strong>4.1: Dependencies and Setup</strong></p>
<p>Start by importing the packages you’ll need:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> session <span class="hljs-keyword">from</span> <span class="hljs-string">'express-session'</span>;
<span class="hljs-keyword">import</span> { createKindeServerClient, GrantType, SessionManager } <span class="hljs-keyword">from</span> <span class="hljs-string">'@kinde-oss/kinde-typescript-sdk'</span>;
<span class="hljs-keyword">import</span> jwt <span class="hljs-keyword">from</span> <span class="hljs-string">'jsonwebtoken'</span>;
<span class="hljs-keyword">import</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">'dotenv'</span>;
<span class="hljs-keyword">import</span> { neon } <span class="hljs-keyword">from</span> <span class="hljs-string">'@neondatabase/serverless'</span>;
</code></pre>
<p>The <code>express</code> import powers the web server that will handle authentication routes. <code>express-session</code> manages user sessions so you can persist login state between requests. The <code>@kinde-oss/kinde-typescript-sdk</code> package is the official Kinde SDK, which handles OAuth flows and user authentication.</p>
<p>You’ll use <code>jsonwebtoken</code> to decode and verify user tokens, while <code>dotenv</code> loads environment variables from your <code>.env</code> file. Finally, <code>@neondatabase/serverless</code> connects your server to the Neon Postgres database where user data will be stored.</p>
<p><strong>4.2: Connect to Your Database</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> sql = neon(process.env.DATABASE_URL!);
</code></pre>
<p>This initializes a type-safe SQL client using your <code>DATABASE_URL</code>.</p>
<p><strong>4.3: Extend login Session</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">declare</span> <span class="hljs-keyword">module</span> 'express-session' {
  <span class="hljs-keyword">interface</span> SessionData {
    accessToken?: <span class="hljs-built_in">string</span>;
    idToken?: <span class="hljs-built_in">string</span>;
    userInfo?: <span class="hljs-built_in">any</span>;
    userName?: <span class="hljs-built_in">string</span>;
    userEmail?: <span class="hljs-built_in">string</span>;
  }
}
</code></pre>
<p>This extends <code>express-session</code> types so you can store Kinde tokens and user info directly in the session.</p>
<p><strong>4.4: Configure Sessions</strong></p>
<p>Now, let’s configure session management so users can stay logged in across requests:</p>
<pre><code class="lang-typescript">app.use(session({
  secret: process.env.JWT_SECRET || <span class="hljs-string">'your_jwt_secret_key'</span>,
  resave: <span class="hljs-literal">true</span>,
  saveUninitialized: <span class="hljs-literal">true</span>,
  cookie: { 
    secure: <span class="hljs-literal">false</span>,
    maxAge: <span class="hljs-number">7</span> * <span class="hljs-number">24</span> * <span class="hljs-number">60</span> * <span class="hljs-number">60</span> * <span class="hljs-number">1000</span>, <span class="hljs-comment">// 7 days</span>
    httpOnly: <span class="hljs-literal">true</span>,
    sameSite: <span class="hljs-string">'lax'</span>
  }
}));
</code></pre>
<p>The <code>secret</code> value is used to sign and verify session cookies, ensuring that sessions can’t be tampered with. The cookie settings keep users logged in for up to 7 days and make sure session tokens persist even after a browser refresh.</p>
<p>Setting <code>httpOnly</code> helps protect against cross-site scripting (XSS) attacks, while <code>sameSite: 'lax'</code> allows users to log in across different origins without breaking authentication.</p>
<p><strong>4.5: Create a Session Manager Factory</strong></p>
<p>Next, you’ll define a small helper function to manage session data for each request:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> createSessionManager = (req: <span class="hljs-built_in">any</span>): <span class="hljs-function"><span class="hljs-params">SessionManager</span> =&gt;</span> ({
  getSessionItem: <span class="hljs-keyword">async</span> (key: <span class="hljs-built_in">string</span>) =&gt; req.session?.[key],
  setSessionItem: <span class="hljs-keyword">async</span> (key: <span class="hljs-built_in">string</span>, value: <span class="hljs-built_in">any</span>) =&gt; {
    <span class="hljs-keyword">if</span> (!req.session) req.session = {};
    req.session[key] = value;
  },
  removeSessionItem: <span class="hljs-keyword">async</span> (key: <span class="hljs-built_in">string</span>) =&gt; {
    <span class="hljs-keyword">if</span> (req.session) <span class="hljs-keyword">delete</span> req.session[key];
  },
  destroySession: <span class="hljs-keyword">async</span> () =&gt; {
    req.session = {};
  }
});
</code></pre>
<p>This function creates a session manager that’s tied to each request. It provides a consistent way to store, retrieve, and clear session data which is exactly what the Kinde SDK needs to keep track of tokens and user info during authentication.</p>
<p><strong>4.6: Create the Kinde Client</strong></p>
<p>Next, set up the Kinde client that will handle the OAuth login and logout flow:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> kindeClient = createKindeServerClient(GrantType.AUTHORIZATION_CODE, {
  authDomain: process.env.KINDE_ISSUER_URL!,
  clientId: process.env.KINDE_CLIENT_ID!,
  clientSecret: process.env.KINDE_CLIENT_SECRET!,
  redirectURL: <span class="hljs-string">'&lt;http://localhost:3000/callback&gt;'</span>,
  logoutRedirectURL: <span class="hljs-string">'&lt;http://localhost:3000&gt;'</span>,
});
</code></pre>
<p>This connects your app to Kinde using the <strong>Authorization Code</strong> grant type which is the most secure option for server-side applications like this one.</p>
<p>The <code>redirectURL</code> and <code>logoutRedirectURL</code> define where users should be sent after logging in or out.</p>
<p><strong>4.7: Create the Home Page Route (</strong><code>GET /</code><strong>)</strong></p>
<p>Then you’ll define a basic route for your home page that checks whether a user is logged in:</p>
<pre><code class="lang-typescript">app.get(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> token = req.session?.accessToken;
  <span class="hljs-keyword">const</span> userInfo = req.session?.userInfo;

  <span class="hljs-keyword">if</span> (token) {
    <span class="hljs-comment">// Show logged-in state with tokens</span>
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Show login button</span>
  }
});
</code></pre>
<p>This route reads the session to determine if a user is authenticated. If a token exists, you can display their account details or dashboard. Otherwise, you’ll show a login button that directs them to Kinde’s sign-in page.</p>
<p><strong>4.8: Create the Login Route (</strong><code>GET /login</code><strong>)</strong></p>
<p>Now, add a route that starts the OAuth login process:</p>
<pre><code class="lang-typescript">app.get(<span class="hljs-string">'/login'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> sessionManager = createSessionManager(req);
    <span class="hljs-keyword">const</span> loginUrl = <span class="hljs-keyword">await</span> kindeClient.login(sessionManager);
    res.redirect(loginUrl.toString());
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Login error:'</span>, error);
    res.status(<span class="hljs-number">500</span>).send(<span class="hljs-string">'Login failed'</span>);
  }
});
</code></pre>
<p>When users visit this route, your app uses the Kinde client to generate a secure login URL and redirects them to Kinde’s hosted login page. Once they log in, Kinde will send them back to your callback route with the necessary tokens.</p>
<p><strong>4.9: OAuth Callback Route (</strong><code>GET /callback</code><strong>)</strong></p>
<p>This is where Kinde redirects users back after login. First, you’ll get the authorization <code>code</code> from Kinde for token exchange:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> fullUrl = <span class="hljs-string">`http://<span class="hljs-subst">${req.headers.host}</span><span class="hljs-subst">${req.url}</span>`</span>;
<span class="hljs-keyword">const</span> url = <span class="hljs-keyword">new</span> URL(fullUrl);
<span class="hljs-keyword">const</span> code = url.searchParams.get(<span class="hljs-string">'code'</span>);
</code></pre>
<p>Next, you’ll exchange this code for tokens that will be needed for API calls:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> tokenResponse = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`<span class="hljs-subst">${process.env.KINDE_ISSUER_URL}</span>/oauth2/token`</span>, {
  method: <span class="hljs-string">'POST'</span>,
  headers: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/x-www-form-urlencoded'</span> },
  body: <span class="hljs-keyword">new</span> URLSearchParams({
    grant_type: <span class="hljs-string">'authorization_code'</span>,
    client_id: process.env.KINDE_CLIENT_ID!,
    client_secret: process.env.KINDE_CLIENT_SECRET!,
    code: code,
    redirect_uri: <span class="hljs-string">'http://localhost:3000/callback'</span>,
  }),
});
</code></pre>
<p>Then, you’ll store these tokens in the session for future requests so users don't need to login every time:</p>
<pre><code class="lang-typescript">req.session.accessToken = tokenData.access_token;
req.session.idToken = tokenData.id_token;
req.session.userInfo = tokenData;
</code></pre>
<p>The next thing you’ll do is to extract the user’s information from JWT and then store this in your database</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> user = <span class="hljs-built_in">JSON</span>.parse(Buffer.from(idToken.split(<span class="hljs-string">'.'</span>)[<span class="hljs-number">1</span>], <span class="hljs-string">'base64'</span>).toString());
req.session.userName = user.given_name || user.name || <span class="hljs-string">'User'</span>;
req.session.userEmail = user.email || <span class="hljs-string">'user@example.com'</span>;
</code></pre>
<p>Finally, add this code that will automatically create or update the user in the database so your MCP server can track to-dos and billing:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> existingUser = <span class="hljs-keyword">await</span> sql<span class="hljs-string">`SELECT * FROM users WHERE user_id = <span class="hljs-subst">${userId}</span>`</span>;

<span class="hljs-keyword">if</span> (existingUser.length === <span class="hljs-number">0</span>) {
  <span class="hljs-keyword">await</span> sql<span class="hljs-string">`
    INSERT INTO users (user_id, name, email, subscription_status, plan, free_todos_used)
    VALUES (<span class="hljs-subst">${userId}</span>, <span class="hljs-subst">${userName}</span>, <span class="hljs-subst">${userEmail}</span>, 'free', 'free', 0)
  `</span>;
} <span class="hljs-keyword">else</span> {
  <span class="hljs-keyword">await</span> sql<span class="hljs-string">`
    UPDATE users 
    SET name = <span class="hljs-subst">${userName}</span>, email = <span class="hljs-subst">${userEmail}</span>
    WHERE user_id = <span class="hljs-subst">${userId}</span>
  `</span>;
}
</code></pre>
<p><strong>4.10: Create the Logout Route (</strong><code>GET /logout</code><strong>)</strong></p>
<pre><code class="lang-typescript">app.get(<span class="hljs-string">'/logout'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    req.session.destroy(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (err) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Session destroy error:'</span>, err);
      }
      res.redirect(<span class="hljs-string">'/'</span>);
    });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Logout error:'</span>, error);
    res.status(<span class="hljs-number">500</span>).send(<span class="hljs-string">'Logout failed'</span>);
  }
});
</code></pre>
<p>This route simply destroys the user’s session, removing all stored tokens and user information. Once the session is cleared, it redirects the user back to the home page, effectively logging them out of the app.</p>
<h3 id="heading-5-complete-authentication-flow">5. Complete Authentication Flow</h3>
<pre><code class="lang-markdown"><span class="hljs-bullet">1.</span> User visits / → sees login button.
<span class="hljs-bullet">2.</span> Clicks login → goes to Kinde.
<span class="hljs-bullet">3.</span> Logs in → redirected back to /callback.
<span class="hljs-bullet">4.</span> Callback exchanges code for tokens.
<span class="hljs-bullet">5.</span> Tokens stored in session.
<span class="hljs-bullet">6.</span> User is created/updated in database.
<span class="hljs-bullet">7.</span> User can now access the MCP server.
</code></pre>
<h3 id="heading-6-why-this-matters">6. Why This Matters</h3>
<p>By placing Kinde in front of your MCP server, you get a secure and seamless authentication layer without the extra trouble of handling passwords or tokens manually. Your users can log in safely, and their sessions persist across page refreshes without the need to log in again each time they revisit your app.</p>
<p>Every new user who signs in is automatically added to your Neon database, making it easy to track accounts and usage. This setup also lays the groundwork for more advanced features later on, like enforcing billing limits or managing user-specific to-dos.</p>
<h3 id="heading-7-key-connections">7. Key Connections</h3>
<ul>
<li><p><strong>Session ↔ Database</strong>: Sync user data</p>
</li>
<li><p><strong>Kinde ↔ Session</strong>: Tokens flow from Kinde to session storage</p>
</li>
<li><p><strong>Session ↔ MCP</strong>: Tokens passed into the server for access control</p>
</li>
<li><p><strong>Database ↔ MCP</strong>: User billing + to-dos read from Neon</p>
</li>
</ul>
<h3 id="heading-8-full-kinde-auth-serverts-file">8. Full <code>kinde-auth-server.ts</code> File</h3>
<p>You can view the complete implementation of the <code>kinde-auth-server.ts</code> file in the <a target="_blank" href="https://github.com/sholajegede/todo_mcp_server/blob/main/src/kinde-auth-server.ts">GitHub repo</a> and copy it directly into your project.</p>
<h2 id="heading-mcp-server-implementation-with-billing-system-integration">MCP Server Implementation (with Billing System Integration)</h2>
<p>Now it’s time to create the main file for your MCP server. This file acts as the entry point, wiring up your database, authentication, tool handlers, and overall flow into a single server.</p>
<h3 id="heading-1-create-your-file">1. Create Your File</h3>
<p>Inside your project, create a new file:</p>
<pre><code class="lang-bash">src/server.ts
</code></pre>
<p>This file will contain the full implementation of your MCP server.</p>
<h3 id="heading-2-project-setup-and-imports">2. Project Setup and Imports</h3>
<p>At the top of the file, import the dependencies you’ll need:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Server } <span class="hljs-keyword">from</span> <span class="hljs-string">'@modelcontextprotocol/sdk/server/index.js'</span>;
<span class="hljs-keyword">import</span> { StdioServerTransport } <span class="hljs-keyword">from</span> <span class="hljs-string">'@modelcontextprotocol/sdk/server/stdio.js'</span>;
<span class="hljs-keyword">import</span> { CallToolRequestSchema, ListToolsRequestSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'@modelcontextprotocol/sdk/types.js'</span>;
<span class="hljs-keyword">import</span> { neon } <span class="hljs-keyword">from</span> <span class="hljs-string">'@neondatabase/serverless'</span>;
<span class="hljs-keyword">import</span> jwt <span class="hljs-keyword">from</span> <span class="hljs-string">'jsonwebtoken'</span>;
<span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">'fs'</span>;
<span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">'path'</span>;
</code></pre>
<p>Each import in your MCP server has a specific purpose. The <code>Server</code> class is the foundation that powers your entire implementation. It listens for requests, manages responses, and keeps track of all registered tools. The <code>StdioServerTransport</code> handles communication between your MCP server and other tools through standard input and output, which is exactly how Cursor connects behind the scenes.</p>
<p>The <code>CallToolRequestSchema</code> and <code>ListToolsRequestSchema</code> act as validators, ensuring every incoming request follows the correct structure before it’s processed. This reduces errors and keeps communication between your tools and the MCP client consistent.</p>
<p><code>neon</code> connects your server to the Neon PostgreSQL database, providing a clean way to manage persistent data like users and to-dos. The <code>jsonwebtoken</code> library decodes and verifies tokens from Kinde, letting you identify and authenticate users securely.</p>
<p><code>fs</code> is used to read and write authentication tokens locally, which means users don’t need to log in every time. Finally, <code>path</code> helps manage file paths cleanly across different systems, keeping everything organized and portable.</p>
<p>Together, these imports form the backbone of your server’s logic, handling authentication, database access, and reliable communication with Cursor.</p>
<h3 id="heading-3-database-connection-and-configuration">3. Database Connection and Configuration</h3>
<p>Next, configure your database connection and token storage:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> sql = neon(process.env.DATABASE_URL!);
<span class="hljs-keyword">const</span> TOKEN_FILE = <span class="hljs-string">'.auth-token'</span>;
</code></pre>
<p>The <code>sql</code> constant creates a live connection to your Neon PostgreSQL database using the <code>DATABASE_URL</code> environment variable. Think of it as the bridge that lets your MCP server talk to your database with every query, insert, and update running through this connection.</p>
<p>It’s what allows your server to persist user data, to-dos, and billing information reliably without having to manage complex configurations manually.</p>
<p>The <code>TOKEN_FILE</code> constant, on the other hand, acts as a lightweight local storage system for authentication tokens. Whenever a user logs in, their token is saved here so they don’t have to reauthenticate every time they restart the server.</p>
<p>It’s a simple but effective way to maintain session continuity, especially during local development or testing.</p>
<h3 id="heading-4-authentication-system">4. Authentication System</h3>
<p>To manage tokens, you’ll add three helper functions:</p>
<p><strong>4.1. Get Stored Token</strong></p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getStoredToken</span>(<span class="hljs-params"></span>): <span class="hljs-title">string</span> | <span class="hljs-title">null</span> </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">if</span> (fs.existsSync(TOKEN_FILE)) {
      <span class="hljs-keyword">return</span> fs.readFileSync(TOKEN_FILE, <span class="hljs-string">'utf8'</span>).trim();
    }
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error reading token file:'</span>, error);
  }
  <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
}
</code></pre>
<p>Retrieves a saved JWT token from the local file system.</p>
<p><strong>4.2. Store Token</strong></p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">storeToken</span>(<span class="hljs-params">token: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">void</span> </span>{
  <span class="hljs-keyword">try</span> {
    fs.writeFileSync(TOKEN_FILE, token);
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error storing token:'</span>, error);
  }
}
</code></pre>
<p>Stores a new JWT token locally so authentication persists across server restarts.</p>
<p><strong>4.3. Decode JWT</strong></p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">decodeJWT</span>(<span class="hljs-params">token: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">any</span> </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">return</span> jwt.decode(token);
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error decoding JWT:'</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }
}
</code></pre>
<p>Decodes JWTs to extract user info such as ID, email, and subscription status.</p>
<h3 id="heading-5-core-helper-functions">5. Core Helper Functions</h3>
<p><strong>5.1. Check Billing Status</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getKindeBillingStatus</span>(<span class="hljs-params">userId: <span class="hljs-built_in">string</span>, accessToken: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Promise</span>&lt;</span>{ plan: <span class="hljs-built_in">string</span>; features: <span class="hljs-built_in">any</span>; canCreate: <span class="hljs-built_in">boolean</span>; reason?: <span class="hljs-built_in">string</span> }&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> decoded = jwt.decode(accessToken) <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>;
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'🔍 JWT Token data for user:'</span>, userId, <span class="hljs-string">'Decoded:'</span>, decoded);

    <span class="hljs-keyword">const</span> subscription = <span class="hljs-keyword">await</span> sql<span class="hljs-string">`
      SELECT * FROM users 
      WHERE user_id = <span class="hljs-subst">${userId}</span>
    `</span>;

    <span class="hljs-keyword">if</span> (subscription.length === <span class="hljs-number">0</span>) {
      <span class="hljs-keyword">await</span> sql<span class="hljs-string">`
        INSERT INTO users (user_id, name, email, subscription_status, plan, free_todos_used)
        VALUES (<span class="hljs-subst">${userId}</span>, <span class="hljs-subst">${decoded.given_name || decoded.name || <span class="hljs-string">'User'</span>}</span>, <span class="hljs-subst">${decoded.email || <span class="hljs-string">'user@example.com'</span>}</span>, 'free', 'free', 0)
      `</span>;
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'👤 New user created:'</span>, decoded.given_name || decoded.name, decoded.email);
    }

    <span class="hljs-keyword">const</span> freeTodosUsed = subscription.length &gt; <span class="hljs-number">0</span> ? subscription[<span class="hljs-number">0</span>].free_todos_used : <span class="hljs-number">0</span>;

    <span class="hljs-keyword">if</span> (freeTodosUsed &lt; <span class="hljs-number">1</span>) {
      <span class="hljs-keyword">return</span> {
        plan: <span class="hljs-string">'free'</span>,
        features: { maxTodos: <span class="hljs-number">1</span>, used: freeTodosUsed },
        canCreate: <span class="hljs-literal">true</span>,
        reason: <span class="hljs-string">`Free tier - <span class="hljs-subst">${<span class="hljs-number">1</span> - freeTodosUsed}</span> todo remaining`</span>
      };
    }

    <span class="hljs-keyword">return</span> {
      plan: <span class="hljs-string">'free'</span>,
      features: { maxTodos: <span class="hljs-number">1</span>, used: freeTodosUsed },
      canCreate: <span class="hljs-literal">false</span>,
      reason: <span class="hljs-string">'You have used your free todo. Please upgrade your plan at &lt;https://learnflowai.kinde.com/portal&gt; to create more todos.'</span>
    };
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error checking Kinde billing:'</span>, error);
    <span class="hljs-keyword">return</span> {
      plan: <span class="hljs-string">'free'</span>,
      features: { maxTodos: <span class="hljs-number">1</span> },
      canCreate: <span class="hljs-literal">false</span>,
      reason: <span class="hljs-string">'Error checking billing status'</span>
    };
  }
}
</code></pre>
<p>This checks a user's billing status and enforces a 1-todo free tier. It also auto-creates a user in the database if it doesn’t exist.</p>
<p><strong>5.2. Check To-Do Permission</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">canCreateTodo</span>(<span class="hljs-params">userId: <span class="hljs-built_in">string</span>, accessToken: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">boolean</span>&gt; </span>{
  <span class="hljs-keyword">const</span> billingStatus = <span class="hljs-keyword">await</span> getKindeBillingStatus(userId, accessToken);
  <span class="hljs-keyword">return</span> billingStatus.canCreate;
}
</code></pre>
<p>This is a simple wrapper that returns a boolean for permission checks.</p>
<h3 id="heading-6-core-server-implementation">6. Core Server Implementation</h3>
<p>Initialize your MCP server:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> server = <span class="hljs-keyword">new</span> Server(
  {
    name: <span class="hljs-string">'todo-mcp-server'</span>,
    version: <span class="hljs-string">'1.0.0'</span>,
  },
  {
    capabilities: {
      tools: {},
    },
  }
);
</code></pre>
<p>This declares a new MCP server with the capability to register tools.</p>
<h3 id="heading-7-register-tools">7. Register Tools</h3>
<p>Every tool that your Cursor IDE can use needs to be listed so it knows what’s available. In this part of the server setup, you’re registering all the tools that your MCP server will expose. Think of it like giving Cursor a menu of what your backend can do.</p>
<p>Here’s how that looks in code:</p>
<pre><code class="lang-typescript">server.setRequestHandler(ListToolsRequestSchema, <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">return</span> {
    tools: [
      {
        name: <span class="hljs-string">'login'</span>,
        description: <span class="hljs-string">'Get authentication URL for Kinde login'</span>,
        inputSchema: {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'object'</span>,
          properties: {},
        },
      },
      <span class="hljs-comment">// ... more tools</span>
    ],
  };
});
</code></pre>
<p>Tools you’ll create include:</p>
<ul>
<li><p>Authentication → <code>login</code>, <code>save_token</code>, <code>logout</code></p>
</li>
<li><p>To-Do Management → <code>list_todos</code>, <code>create_todo</code>, <code>update_todo</code>, <code>delete_todo</code></p>
</li>
<li><p>Billing → <code>refresh_billing_status</code></p>
</li>
</ul>
<h3 id="heading-8-tool-handlers">8. Tool Handlers</h3>
<p>Each tool in your MCP server has its own handler. The handler checks if the user is authenticated, talks to the database to perform the request, and returns a clean, structured response that Cursor can display.</p>
<p>This keeps the server organized, secure, and easy to extend later.</p>
<p><strong>8.1. Login Tool</strong></p>
<p>The <code>login</code> tool is responsible for starting the authentication flow with Kinde. When users call it, the server returns a short message explaining how to sign in and store their token.</p>
<p>Once logged in, it lists a few commands that they user can try.</p>
<p>Here’s the handler:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">case</span> <span class="hljs-string">'login'</span>: {
  <span class="hljs-keyword">return</span> {
    content: [
      {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
        text: <span class="hljs-string">`🔐 **Authentication Required**

To use this MCP server, you need to authenticate with Kinde:

1. **Open your browser** and go to: &lt;http://localhost:3000&gt;
2. **Click "Login with Kinde"** to authenticate
3. **Copy your ID Token** from the page
4. **Use the save_token tool** to store it

**Note:** Make sure the Kinde auth server is running:
\\`</span>\\<span class="hljs-string">`\\`</span>bash
npm run auth-server
\\<span class="hljs-string">`\\`</span>\\<span class="hljs-string">`

After authentication, you can use commands like:
- \\`</span>list todos\\<span class="hljs-string">` - List your todos
- \\`</span>create todo\\<span class="hljs-string">` - Create a new todo
- \\`</span>refresh billing status\\<span class="hljs-string">` - Check your plan status`</span>,
      },
    ],
  };
}
</code></pre>
<p><strong>8.2. Save Token Tool</strong></p>
<p>The <code>save_token</code> tool handles storing the user’s authentication token locally so they don’t need to re-authenticate each time they use the MCP server.</p>
<p>Here’s the handler:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">case</span> <span class="hljs-string">'save_token'</span>: {
  <span class="hljs-keyword">const</span> { token } = args <span class="hljs-keyword">as</span> { token: <span class="hljs-built_in">string</span> };
  storeToken(token);
  <span class="hljs-keyword">return</span> {
    content: [
      {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
        text: <span class="hljs-string">'✅ Token saved successfully! You can now use commands like "list todos" and "create todo" without providing the token each time.'</span>,
      },
    ],
  };
}
</code></pre>
<p>When a user runs this command and passes in their token, the server saves it to a local file using the <code>storeToken()</code> function. From then on, every other command (like <code>list todos</code>, <code>create todo</code>, or <code>refresh billing status</code>) can automatically authenticate using that stored token.</p>
<p>This small step makes the development flow smoother and keeps authentication persistent across sessions.</p>
<p><strong>8.3. List To-Dos Tool</strong></p>
<p>The <code>list_todos</code> tool retrieves all to-dos that belong to the currently authenticated user. It first checks for a stored authentication token and decodes it to identify the user. If the token is missing or invalid, it asks the user to log in again.</p>
<p>Here’s the handler:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">case</span> <span class="hljs-string">'list_todos'</span>: {
  <span class="hljs-keyword">const</span> token = getStoredToken();
  <span class="hljs-keyword">if</span> (!token) {
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">'❌ No authentication token found. Please:\\n1. Type "login" to get the authentication URL\\n2. Complete login at &lt;http://localhost:3000&gt;\\n3. Copy your token and use "save_token" to store it\\n4. Then try "list todos" again'</span>,
        },
      ],
    ];
  }

  <span class="hljs-keyword">const</span> decoded = decodeJWT(token);
  <span class="hljs-keyword">if</span> (!decoded || !decoded.sub) {
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">'❌ Invalid token. Please login again using the "login" command.'</span>,
        },
      ],
    ];
  }

  <span class="hljs-keyword">const</span> todos = <span class="hljs-keyword">await</span> sql<span class="hljs-string">`
    SELECT * FROM todos 
    WHERE user_id = <span class="hljs-subst">${decoded.sub}</span> 
    ORDER BY created_at DESC
  `</span>;

  <span class="hljs-keyword">if</span> (todos.length === <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">'📝 No todos found. Create your first todo using "create todo"!'</span>,
        },
      ],
    ];
  }

  <span class="hljs-keyword">const</span> todosList = todos.map(<span class="hljs-function">(<span class="hljs-params">todo: <span class="hljs-built_in">any</span></span>) =&gt;</span> 
    <span class="hljs-string">`**<span class="hljs-subst">${todo.id}</span>.** <span class="hljs-subst">${todo.title}</span><span class="hljs-subst">${todo.description ? <span class="hljs-string">` - <span class="hljs-subst">${todo.description}</span>`</span> : <span class="hljs-string">''</span>}</span> <span class="hljs-subst">${todo.completed ? <span class="hljs-string">'✅'</span> : <span class="hljs-string">'⏳'</span>}</span>`</span>
  ).join(<span class="hljs-string">'\\n'</span>);

  <span class="hljs-keyword">return</span> {
    content: [
      {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
        text: <span class="hljs-string">`📝 **Your Todos (<span class="hljs-subst">${todos.length}</span>):**\\n\\n<span class="hljs-subst">${todosList}</span>`</span>,
      },
    ],
  };
}
</code></pre>
<p>Results from this tool are formatted for easy reading in the MCP client, showing each task’s title, description, and completion status. If there are no to-dos yet, it simply prompts the user to create one.</p>
<p><strong>8.4. Create To-Do Tool</strong></p>
<p>The <code>create_todo</code> tool lets authenticated users add new to-dos to their list. It starts by verifying that a valid token exists, ensuring only logged-in users can create tasks. If the token is present, it checks billing limits otherwise, it instructs the user to log in again.</p>
<p>Here’s the handler:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">case</span> <span class="hljs-string">'create_todo'</span>: {
  <span class="hljs-keyword">const</span> token = getStoredToken();
  <span class="hljs-keyword">if</span> (!token) {
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">'❌ No authentication token found. Please login first.'</span>,
        },
      ],
    ];
  }

  <span class="hljs-keyword">const</span> decoded = decodeJWT(token);
  <span class="hljs-keyword">if</span> (!decoded || !decoded.sub) {
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">'❌ Invalid token. Please login again.'</span>,
        },
      ],
    ];
  }

  <span class="hljs-keyword">const</span> { title, description, completed } = args <span class="hljs-keyword">as</span> { 
    title: <span class="hljs-built_in">string</span>; 
    description?: <span class="hljs-built_in">string</span>; 
    completed?: <span class="hljs-built_in">boolean</span>; 
  };

  <span class="hljs-comment">// Check billing status before creating todo</span>
  <span class="hljs-keyword">const</span> canCreate = <span class="hljs-keyword">await</span> canCreateTodo(decoded.sub, token);
  <span class="hljs-keyword">if</span> (!canCreate) {
    <span class="hljs-keyword">const</span> billingStatus = <span class="hljs-keyword">await</span> getKindeBillingStatus(decoded.sub, token);
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">`🚫 **Cannot create todo**

<span class="hljs-subst">${billingStatus.reason}</span>

**Upgrade your plan:** &lt;https://learnflowai.kinde.com/portal`</span>&gt;,
        },
      ],
    ];
  }

  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> sql<span class="hljs-string">`
    INSERT INTO todos (user_id, title, description, completed)
    VALUES (<span class="hljs-subst">${decoded.sub}</span>, <span class="hljs-subst">${title}</span>, <span class="hljs-subst">${description || <span class="hljs-literal">null</span>}</span>, <span class="hljs-subst">${completed || <span class="hljs-literal">false</span>}</span>)
    RETURNING *
  `</span>;

  <span class="hljs-comment">// Update free todos used count</span>
  <span class="hljs-keyword">await</span> sql<span class="hljs-string">`
    UPDATE users 
    SET free_todos_used = free_todos_used + 1 
    WHERE user_id = <span class="hljs-subst">${decoded.sub}</span>
  `</span>;

  <span class="hljs-keyword">return</span> {
    content: [
      {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
        text: <span class="hljs-built_in">JSON</span>.stringify({
          success: <span class="hljs-literal">true</span>,
          todoId: result[<span class="hljs-number">0</span>].id,
          message: <span class="hljs-string">'Todo created successfully'</span>,
          title: result[<span class="hljs-number">0</span>].title,
          description: result[<span class="hljs-number">0</span>].description,
          completed: result[<span class="hljs-number">0</span>].completed
        }, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>),
      },
    ],
  };
}
</code></pre>
<p>If the user has hit their limit after calling this tool, it returns a clear message explaining why and provides a link to upgrade their plan.</p>
<p><strong>8.5. Update To-Do Tool</strong></p>
<p>The <code>update_todo</code> tool allows an authenticated user to modify an existing to-do’s title, description, or completion status. It first checks for a valid token and decodes it to identify the user. If authentication fails, the tool instructs the user to log in again.</p>
<p>Here’s the handler:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">case</span> <span class="hljs-string">'update_todo'</span>: {
  <span class="hljs-keyword">const</span> token = getStoredToken();
  <span class="hljs-keyword">if</span> (!token) {
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">'❌ No authentication token found. Please login first.'</span>,
        },
      ],
    ];
  }

  <span class="hljs-keyword">const</span> decoded = decodeJWT(token);
  <span class="hljs-keyword">if</span> (!decoded || !decoded.sub) {
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">'❌ Invalid token. Please login again.'</span>,
        },
      ],
    ];
  }

  <span class="hljs-keyword">const</span> { todoId, title, description, completed } = args <span class="hljs-keyword">as</span> { 
    todoId: <span class="hljs-built_in">number</span>; 
    title?: <span class="hljs-built_in">string</span>; 
    description?: <span class="hljs-built_in">string</span>; 
    completed?: <span class="hljs-built_in">boolean</span>; 
  };

  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> sql<span class="hljs-string">`
    UPDATE todos 
    SET 
      title = COALESCE(<span class="hljs-subst">${title || <span class="hljs-literal">null</span>}</span>, title),
      description = COALESCE(<span class="hljs-subst">${description || <span class="hljs-literal">null</span>}</span>, description),
      completed = COALESCE(<span class="hljs-subst">${completed !== <span class="hljs-literal">undefined</span> ? completed : <span class="hljs-literal">null</span>}</span>, completed),
      updated_at = CURRENT_TIMESTAMP
    WHERE id = <span class="hljs-subst">${todoId}</span> AND user_id = <span class="hljs-subst">${decoded.sub}</span>
    RETURNING *
  `</span>;

  <span class="hljs-keyword">if</span> (result.length === <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">'❌ Todo not found or you do not have permission to update it.'</span>,
        },
      ],
    ];
  }

  <span class="hljs-keyword">return</span> {
    content: [
      {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
        text: <span class="hljs-built_in">JSON</span>.stringify({
          success: <span class="hljs-literal">true</span>,
          message: <span class="hljs-string">'Todo updated successfully'</span>,
          todo: result[<span class="hljs-number">0</span>]
        }, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>),
      },
    ],
  };
}
</code></pre>
<p>Once the token is verified, the server updates the specified to-do in the database, using <code>COALESCE</code> to leave any fields unchanged if no new value is provided. The <code>updated_at</code> timestamp is refreshed automatically.</p>
<p>If the to-do doesn’t exist or the user doesn’t have permission to modify it, the tool returns an error message. Otherwise, it responds with the updated to-do in a clean JSON format:</p>
<p><strong>8.6. Delete To-Do Tool</strong></p>
<p>The <code>delete_todo</code> tool allows an authenticated user to remove a specific to-do. It first checks for a valid token and decodes it to identify the user. If the token is missing or invalid, the tool instructs the user to log in again.</p>
<p>Here’s the handler:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">case</span> <span class="hljs-string">'delete_todo'</span>: {
  <span class="hljs-keyword">const</span> token = getStoredToken();
  <span class="hljs-keyword">if</span> (!token) {
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">'❌ No authentication token found. Please login first.'</span>,
        },
      ],
    ];
  }

  <span class="hljs-keyword">const</span> decoded = decodeJWT(token);
  <span class="hljs-keyword">if</span> (!decoded || !decoded.sub) {
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">'❌ Invalid token. Please login again.'</span>,
        },
      ],
    ];
  }

  <span class="hljs-keyword">const</span> { todoId } = args <span class="hljs-keyword">as</span> { todoId: <span class="hljs-built_in">number</span> };

  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> sql<span class="hljs-string">`
    DELETE FROM todos 
    WHERE id = <span class="hljs-subst">${todoId}</span> AND user_id = <span class="hljs-subst">${decoded.sub}</span>
    RETURNING *
  `</span>;

  <span class="hljs-keyword">if</span> (result.length === <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">'❌ Todo not found or you do not have permission to delete it.'</span>,
        },
      ],
    ];
  }

  <span class="hljs-keyword">return</span> {
    content: [
      {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
        text: <span class="hljs-built_in">JSON</span>.stringify({
          success: <span class="hljs-literal">true</span>,
          message: <span class="hljs-string">'Todo deleted successfully'</span>,
          deletedTodo: result[<span class="hljs-number">0</span>]
        }, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>),
      },
    ],
  };
}
</code></pre>
<p>This ensures users can only delete their own tasks, keeps the response structured for easy consumption in Cursor, and provides immediate confirmation of the deletion.</p>
<p><strong>8.7. Refresh Billing Status Tool</strong></p>
<p>The <code>refresh_billing_status</code> tool allows an authenticated user to force a fresh check of their billing status from Kinde. It first verifies that a valid token exists and decodes it to identify the user. If the token is missing or invalid, the tool instructs the user to log in again.</p>
<p>Here’s the handler:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">case</span> <span class="hljs-string">'refresh_billing_status'</span>: {
  <span class="hljs-keyword">const</span> token = getStoredToken();
  <span class="hljs-keyword">if</span> (!token) {
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">'❌ No authentication token found. Please login first.'</span>,
        },
      ],
    ];
  }

  <span class="hljs-keyword">const</span> decoded = decodeJWT(token);
  <span class="hljs-keyword">if</span> (!decoded || !decoded.sub) {
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">'❌ Invalid token. Please login again.'</span>,
        },
      ],
    ];
  }

  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'🔄 Force refreshing billing status for user:'</span>, decoded.sub);
  <span class="hljs-keyword">const</span> billingStatus = <span class="hljs-keyword">await</span> getKindeBillingStatus(decoded.sub, token);

  <span class="hljs-keyword">return</span> {
    content: [
      {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
        text: <span class="hljs-built_in">JSON</span>.stringify({
          success: <span class="hljs-literal">true</span>,
          message: <span class="hljs-string">'Billing status refreshed successfully!'</span>,
          kindeBilling: {
            plan: billingStatus.plan,
            features: billingStatus.features,
            canCreate: billingStatus.canCreate,
            reason: billingStatus.reason,
            upgradeUrl: <span class="hljs-string">'&lt;https://learnflowai.kinde.com/portal&gt;'</span>,
            selfServicePortal: <span class="hljs-string">'&lt;https://learnflowai.kinde.com/portal&gt;'</span>,
            lastChecked: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString()
          }
        }, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>),
      },
    ],
  };
}
</code></pre>
<p>This tool ensures that users always have an up-to-date view of their subscription status and usage limits, providing all necessary information to decide whether they need to upgrade their plan.</p>
<p><strong>8.8. Logout Tool</strong></p>
<p>The <code>logout</code> tool lets a user end their session by clearing the locally stored authentication token. When called, it checks if the token file exists and deletes it. If successful, the tool confirms that the user has been logged out.</p>
<p>Here’s the handler:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">case</span> <span class="hljs-string">'logout'</span>: {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">if</span> (fs.existsSync(TOKEN_FILE)) {
      fs.unlinkSync(TOKEN_FILE);
    }
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">'✅ Logged out successfully. Authentication token cleared.'</span>,
        },
      ],
    ];
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> {
      content: [
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'text'</span>,
          text: <span class="hljs-string">'⚠️ Logout completed, but there was an issue clearing the token file.'</span>,
        },
      ],
    };
  }
}
</code></pre>
<p>This ensures that the session ends cleanly, preventing further use of the MCP server until the user logs in again. It also provides immediate feedback so the user knows the logout process succeeded or if there were minor issues.</p>
<h3 id="heading-9-full-serverts-file">9. Full <code>server.ts</code> File</h3>
<p>You can view the complete implementation of the <code>server.ts</code> file in the <a target="_blank" href="https://github.com/sholajegede/todo_mcp_server/blob/main/src/server.ts">GitHub repo</a> and copy it directly into your project.</p>
<h3 id="heading-10-data-flow-amp-integration">10. Data Flow &amp; Integration</h3>
<p>Now that you’ve wired up authentication, database persistence, and billing checks, let’s step back and look at how everything fits together.</p>
<p>Authentication Flow:</p>
<ul>
<li><p>User clicks <strong>Login with Kinde</strong>.</p>
</li>
<li><p>They are redirected to the <strong>Kinde Auth URL.</strong></p>
</li>
<li><p>Kinde issues a <strong>JWT token</strong> after successful login.</p>
</li>
<li><p>The user copies this token and runs <em>“save_token: &lt;your-jwt-token&gt;”</em> in the Cursor Chat to store it.</p>
</li>
<li><p>The token is stored in the user’s session.</p>
</li>
<li><p>All future requests include the token for validation.</p>
</li>
</ul>
<p>To-do Flow:</p>
<ul>
<li><p>User sends a request (for example, to <em>“create todo”</em> to create a new todo).</p>
</li>
<li><p>The server checks the session for a valid token.</p>
</li>
<li><p>The server verifies the user’s billing plan and usage limits.</p>
</li>
<li><p>If valid, the request hits the <strong>Neon Postgres database</strong>.</p>
</li>
<li><p>Usage counters are updated and a success response is returned.</p>
</li>
</ul>
<p>Complete Data Flow:</p>
<pre><code class="lang-markdown">User Input 
   → MCP Server 
<span class="hljs-code">      → Authentication Check
         → Billing Check
            → Database Operation
               → Response</span>
</code></pre>
<p>Diagram:</p>
<pre><code class="lang-mermaid">flowchart LR
    A[User Input] --&gt; B[MCP Server]
    B --&gt; C{Authenticated?}
    C -- No --&gt; D[Reject Request]
    C -- Yes --&gt; E{Billing OK?}
    E -- No --&gt; F[Reject Request]
    E -- Yes --&gt; G[Database Operation]
    G --&gt; H[Update Usage + Return Response]
</code></pre>
<p>This overview shows how the different components work together. Each feature you’ve added (authentication, billing, and persistence) acts as a checkpoint in the request flow.</p>
<h3 id="heading-11-error-handling-amp-security">11. Error Handling &amp; Security</h3>
<p>Authentication Security:</p>
<ul>
<li><p>JWT Validation: Every request validates JWT token</p>
</li>
<li><p>User Isolation: Users can only access their own to-dos</p>
</li>
<li><p>Token Storage: Tokens are stored locally, not in your database</p>
</li>
</ul>
<p>Database Security:</p>
<ul>
<li><p>SQL Injection Prevention: Your MCP server uses parameterized queries</p>
</li>
<li><p>User Scoping: All queries are filtered by <code>user_id</code></p>
</li>
<li><p>Permission Checks: Every operation validates user ownership</p>
</li>
</ul>
<p>Error Handling:</p>
<ul>
<li><code>try/catch</code> returns safe error messages</li>
</ul>
<h3 id="heading-12-testing-amp-deployment">12. Testing &amp; Deployment</h3>
<p>Finally, start the MCP server:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> transport = <span class="hljs-keyword">new</span> StdioServerTransport();
  <span class="hljs-keyword">await</span> server.connect(transport);
  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Todo MCP server running on stdio'</span>);
}

main().catch(<span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Fatal error in main():'</span>, error);
  process.exit(<span class="hljs-number">1</span>);
});
</code></pre>
<p>Test it by running:</p>
<pre><code class="lang-powershell">npm run dev
</code></pre>
<p>In Cursor, you can now try commands like:</p>
<pre><code class="lang-markdown">login
save<span class="hljs-emphasis">_token: <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">your-jwt-token</span>&gt;</span></span>
list todos
create todo
refresh billing status
logout</span>
</code></pre>
<h2 id="heading-testing-the-complete-system"><strong>Testing the Complete System</strong></h2>
<h3 id="heading-1-start-the-services">1. Start the Services</h3>
<pre><code class="lang-powershell"><span class="hljs-comment"># Terminal 1: Start MCP Server</span>
npm run dev

<span class="hljs-comment"># Terminal 2: Start Kinde Auth Server</span>
npm run auth<span class="hljs-literal">-server</span>
</code></pre>
<h3 id="heading-2-configure-cursor-mcp">2. Configure Cursor MCP</h3>
<p>In your Cursor project:</p>
<ul>
<li><p>Go to Settings → Tools &amp; MCP → New MCP Server</p>
</li>
<li><p>Edit the <code>~/.cursor/mcp.json</code> and paste this code below</p>
</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"mcpServers"</span>: {
    <span class="hljs-attr">"todo-mcp-server"</span>: {
      <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node"</span>,
      <span class="hljs-attr">"args"</span>: [<span class="hljs-string">"dist/server.js"</span>],
      <span class="hljs-attr">"cwd"</span>: <span class="hljs-string">"/path/to/your/todo-mcp-server"</span>,
      <span class="hljs-attr">"env"</span>: {
        <span class="hljs-attr">"DATABASE_URL"</span>: <span class="hljs-string">"your-neon-connection-string"</span>,
        <span class="hljs-attr">"KINDE_ISSUER_URL"</span>: <span class="hljs-string">"&lt;https://your-domain.kinde.com&gt;"</span>,
        <span class="hljs-attr">"KINDE_CLIENT_ID"</span>: <span class="hljs-string">"your-client-id"</span>,
        <span class="hljs-attr">"KINDE_CLIENT_SECRET"</span>: <span class="hljs-string">"your-client-secret"</span>,
        <span class="hljs-attr">"JWT_SECRET"</span>: <span class="hljs-string">"your-jwt-secret-key"</span>,
        <span class="hljs-attr">"NODE_ENV"</span>: <span class="hljs-string">"development"</span>
      }
    }
  }
}
</code></pre>
<h3 id="heading-3-test-the-complete-flow">3. Test the Complete Flow</h3>
<p>Open your Cursor chat window and test MCP commands:</p>
<ul>
<li><p><em>login</em> → Get authentication URL</p>
</li>
<li><p><em>save_token</em> → Save your token gotten from Kinde</p>
</li>
<li><p><em>list to-dos</em> → List to-dos</p>
</li>
<li><p><em>create to-do</em> - Create a to-do</p>
</li>
<li><p><em>refresh billing status</em> - Check billing</p>
</li>
</ul>
<h2 id="heading-troubleshooting">Troubleshooting</h2>
<p>Even with everything set up correctly, you might run into issues. Here are some common problems and how to fix them.</p>
<h3 id="heading-1-mcp-server-not-detected">1. MCP Server Not Detected</h3>
<p>If Cursor can’t see your server:</p>
<ul>
<li><p>Double-check the syntax of your <code>~/.cursor/mcp.json</code> file.</p>
</li>
<li><p>Make sure all file paths in <code>mcp.json</code> are <strong>absolute paths</strong> (not relative).</p>
</li>
<li><p>Restart Cursor after making changes to the config file.</p>
</li>
</ul>
<h3 id="heading-2-database-connection-issues">2. Database Connection Issues</h3>
<p>If your Neon database won’t connect:</p>
<ul>
<li><p>Confirm your <code>DATABASE_URL</code> environment variable is correctly formatted.</p>
</li>
<li><p>Log into the <a target="_blank" href="https://console.neon.tech">Neon dashboard</a> and make sure your database is active and not paused.</p>
</li>
<li><p>If you’re using SSL, verify that the SSL mode matches Neon’s connection settings.</p>
</li>
</ul>
<h3 id="heading-3-kinde-authentication-problems">3. Kinde Authentication Problems</h3>
<p>If login isn’t working as expected:</p>
<ul>
<li><p>In your <a target="_blank" href="https://app.kinde.com/admin">Kinde dashboard</a>, make sure the redirect URLs are set correctly (for example, <a target="_blank" href="http://localhost:3000"><code>http://localhost:3000</code></a>).</p>
</li>
<li><p>Double-check that your client ID and client secret are correct.</p>
</li>
<li><p>Ensure your auth server is running locally on port <code>3000</code> before attempting login.</p>
</li>
</ul>
<h3 id="heading-4-token-errors">4. Token Errors</h3>
<p>If you’re getting token-related errors:</p>
<ul>
<li><p>Confirm the token you’re saving is in JWT format (three dot-separated parts).</p>
</li>
<li><p>Make sure the token hasn’t expired.</p>
</li>
<li><p>Use the ID token provided by Kinde, not the access token.</p>
</li>
</ul>
<p>Following these steps should resolve most issues you’ll run into when setting up your MCP server with Cursor, Neon, and Kinde.</p>
<h2 id="heading-final-mcp-server-architecture">Final MCP Server Architecture</h2>
<pre><code class="lang-markdown">┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Cursor IDE    │    │   MCP Server     │    │  Kinde Auth     │
│                 │◄──►│                  │◄──►│   Server        │
│ - MCP Tools     │    │ - Todo CRUD      │    │ - OAuth Flow    │
│ - Chat Interface│    │ - Billing Check  │    │ - Token Storage │
└─────────────────┘    └──────────────────┘    └─────────────────┘
<span class="hljs-code">                                │
                                ▼
                       ┌─────────────────┐
                       │ Neon PostgreSQL │
                       │                 │
                       │ - Users Table   │
                       │ - Todos Table   │
                       │ - Billing Data  │
                       └─────────────────┘</span>
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You’ve just built a fully functional MCP server with:</p>
<ul>
<li><p><strong>Authentication</strong> → secure logins with Kinde</p>
</li>
<li><p><strong>Data persistence</strong> → to-dos stored in Neon</p>
</li>
<li><p><strong>Billing enforcement</strong> → usage limits + upgrade path</p>
</li>
<li><p><strong>Tool exposure</strong> → MCP tools accessible in Cursor</p>
</li>
</ul>
<p>This foundation is flexible enough to power more advanced apps while keeping the core flow simple and secure.</p>
<h3 id="heading-next-steps">Next Steps</h3>
<p>Here are some ideas to extend what you’ve built:</p>
<ul>
<li><p><strong>Role-based access control (RBAC):</strong> create admin vs normal user permissions (see my <a target="_blank" href="https://dev.to/sholajegede/part-1-master-authentication-and-role-based-access-control-rbac-with-kinde-and-convex-in-a-h3c">two-part RBAC guide</a> for reference).</p>
</li>
<li><p><strong>Billing tiers:</strong> offer free, pro, and enterprise plans with different limits.</p>
</li>
<li><p><strong>Features:</strong> add search, tags, or sharing to to-dos.</p>
</li>
<li><p><strong>Deployment:</strong> run the service on a cloud platform with HTTPS and a production-grade database.</p>
</li>
</ul>
<h3 id="heading-resources">Resources</h3>
<p>You can find the complete source code for this tutorial in <a target="_blank" href="https://github.com/sholajegede/todo_mcp_server">this GitHub repository</a>. If it helped you in any way, consider giving it a star (⭐) to show your support!</p>
<p>Also, if you found this tutorial valuable, feel free to share it with others who might benefit from it. I’d really appreciate your thoughts, you can mention me on X <a target="_blank" href="https://x.com/wani_shola">@wani_shola</a> or <a target="_blank" href="https://www.linkedin.com/in/sholajegede">connect with me on LinkedIn</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Fix the Python ENOENT Error When Setting Up MCP Servers – A Complete Guide ]]>
                </title>
                <description>
                    <![CDATA[ Getting the "spawn python ENOENT" error while setting up an MCP (Model Context Protocol) server on macOS can be frustrating. But don't worry – in this tutorial, I'll guide you through fixing it by rebuilding your Python virtual environment. By the en... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-fix-the-python-enoent-error-when-setting-up-mcp-servers-a-complete-guide/</link>
                <guid isPermaLink="false">68963890790ac4491c15b00a</guid>
                
                    <category>
                        <![CDATA[ mcp server ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Blockchain ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Developer ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Beginner Developers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ macOS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Idris Olubisi ]]>
                </dc:creator>
                <pubDate>Fri, 08 Aug 2025 17:49:04 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754675334533/6a05e45a-9703-49c0-b427-6c4960c01d86.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Getting the "spawn python ENOENT" error while setting up an MCP (Model Context Protocol) server on macOS can be frustrating. But don't worry – in this tutorial, I'll guide you through fixing it by rebuilding your Python virtual environment.</p>
<p>By the end, you'll have a fully functional MCP server integrated with Claude Desktop in about 10 minutes. This solution applies to any MCP setup facing this standard error after Python upgrades.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-what-causes-the-enoent-error">What Causes the ENOENT Error?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-diagnose-your-broken-virtual-environment">How to Diagnose Your Broken Virtual Environment</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-completely-rebuild-your-virtual-environment">How to Completely Rebuild Your Virtual Environment</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-install-mcp-server-dependencies">How to Install MCP Server Dependencies</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-locate-your-server-files">How to Locate Your Server Files</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-test-your-server-setup">How to Test Your Server Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-configure-claude-desktop">How to Configure Claude Desktop</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-restart-claude-desktop-and-test-integration">How to Restart Claude Desktop and Test Integration</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-mcp-server-capabilities">Understanding MCP Server Capabilities</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-alternative-installation-methods">Alternative Installation Methods</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-method-1-direct-package-installation">Method 1: Direct Package Installation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-method-2-using-uv-package-manager">Method 2: Using UV Package Manager</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-prevent-future-enoent-errors">How to Prevent Future ENOENT Errors</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-troubleshooting-common-issues">Troubleshooting Common Issues</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-what-causes-the-enoent-error">What Causes the ENOENT Error?</h2>
<p>The ENOENT (Error NO ENTry) error means your system can’t locate the Python executable at the specified path. This occurs when the file is missing or inaccessible.</p>
<p>On macOS, this typically happens when:</p>
<ul>
<li><p>You've upgraded Python through Homebrew</p>
</li>
<li><p>The <code>brew cleanup</code> command removed old Python versions</p>
</li>
<li><p>Your virtual environment's symlinks now point to non-existent files</p>
</li>
</ul>
<p>What makes this particularly challenging is that your virtual environment folder still exists – it looks fine from the outside, but the Python executable inside is completely broken.</p>
<p>When MCP servers try to spawn Python processes using these broken paths, you get the dreaded ENOENT error. This affects any Python-based MCP server, whether you're building custom tools, connecting to APIs, or working with file systems.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow this tutorial, you'll need:</p>
<ul>
<li><p>macOS with <a target="_blank" href="https://brew.sh/">Homebrew</a> installed</p>
</li>
<li><p>Python 3.10 or higher</p>
</li>
<li><p>An MCP server repository cloned locally</p>
</li>
<li><p><a target="_blank" href="https://claude.ai/download">Claude Desktop</a> installed</p>
</li>
<li><p>Basic familiarity with terminal commands and Python virtual environments</p>
</li>
</ul>
<p>If you haven't cloned an MCP server repository yet, you can start with any open-source MCP server. For this tutorial, I'll use generic examples that work with any MCP setup:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/your-username/your-mcp-server.git
<span class="hljs-built_in">cd</span> your-mcp-server
</code></pre>
<h2 id="heading-how-to-diagnose-your-broken-virtual-environment">How to Diagnose Your Broken Virtual Environment</h2>
<p>First, you need to confirm that your virtual environment is actually the problem. Open your terminal and navigate to your MCP directory:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> /path/to/your/mcp-server
</code></pre>
<p>Now check if your Python executable exists:</p>
<pre><code class="lang-bash">ls -la venv/bin/python*
</code></pre>
<p>If you see broken symlinks or get "No such file or directory" errors, you've found your problem. You might see output like:</p>
<pre><code class="lang-bash">lrwxr-xr-x  1 username  staff  16 Jan  1 12:00 python -&gt; /usr/<span class="hljs-built_in">local</span>/bin/python3.11
lrwxr-xr-x  1 username  staff  16 Jan  1 12:00 python3 -&gt; /usr/<span class="hljs-built_in">local</span>/bin/python3.11
</code></pre>
<p>But when you try to run these Python executables:</p>
<pre><code class="lang-bash">./venv/bin/python --version
</code></pre>
<p>You'll get an error because the target files no longer exist. This confirms your virtual environment is broken and needs rebuilding.</p>
<h2 id="heading-how-to-completely-rebuild-your-virtual-environment">How to Completely Rebuild Your Virtual Environment</h2>
<p>The most reliable solution is to rebuild your virtual environment from scratch. This ensures all paths and dependencies are correctly configured for your current Python installation.</p>
<p>Here's your step-by-step rebuild process:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Make sure you're in the MCP server directory</span>
<span class="hljs-built_in">cd</span> /path/to/your/mcp-server

<span class="hljs-comment"># Remove the corrupted virtual environment</span>
rm -rf venv

<span class="hljs-comment"># Create a fresh virtual environment</span>
python3 -m venv venv

<span class="hljs-comment"># Activate the new environment</span>
<span class="hljs-built_in">source</span> venv/bin/activate
</code></pre>
<p>You should now see <code>(venv)</code> in your terminal prompt, indicating the virtual environment is active. This prefix confirms you're working within the isolated Python environment.</p>
<h2 id="heading-how-to-install-mcp-server-dependencies">How to Install MCP Server Dependencies</h2>
<p>With your fresh virtual environment active, install the MCP server and its dependencies. The exact installation command depends on your specific MCP server, but typically follows one of these patterns:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># For package-based installation</span>
pip install -e .

<span class="hljs-comment"># Or for requirements file</span>
pip install -r requirements.txt

<span class="hljs-comment"># Or for specific MCP frameworks</span>
pip install fastmcp
</code></pre>
<p>Common MCP server dependencies include:</p>
<ul>
<li><p>FastMCP for the server framework</p>
</li>
<li><p>JSON-RPC libraries for communication protocols</p>
</li>
<li><p>HTTP clients for API integrations</p>
</li>
<li><p>File system utilities for local operations</p>
</li>
</ul>
<p>The installation process displays all packages as they install. Don't worry if you see deprecation warnings – they're normal and won't affect functionality.</p>
<h2 id="heading-how-to-locate-your-server-files">How to Locate Your Server Files</h2>
<p>After installation, identify where your main server file lives. Run this command to find all server.py files:</p>
<pre><code class="lang-bash">find . -name <span class="hljs-string">"server.py"</span> -<span class="hljs-built_in">type</span> f
</code></pre>
<p>You may see results like:</p>
<ul>
<li><p><code>./server.py</code> (in the root directory)</p>
</li>
<li><p><code>./src/server.py</code> (in a source directory)</p>
</li>
<li><p><code>./mcp_server/server.py</code> (in a package directory)</p>
</li>
</ul>
<p>Check your current directory structure:</p>
<pre><code class="lang-bash">ls -la
</code></pre>
<p>Look for the main server entry point. Most MCP servers follow standard Python project structures with either a root-level server file or one nested in a package directory.</p>
<h2 id="heading-how-to-test-your-server-setup">How to Test Your Server Setup</h2>
<p>Now you’ll want to test your server to ensure it's working correctly. Start with the main server file you identified:</p>
<pre><code class="lang-bash">python server.py
</code></pre>
<p>If this is the correct server and everything is configured correctly, you'll see output similar to:</p>
<pre><code class="lang-typescript">╭─ MCP Server ───────────────────────────────────────────────────────────────╮
│ 🖥️  Server name: Example-MCP                                              │
│ 📦 Transport: STDIO                                                        │
│ 🤝 Protocol: <span class="hljs-built_in">JSON</span>-RPC                                                      │
╰────────────────────────────────────────────────────────────────────────────╯
[INFO] Starting MCP server <span class="hljs-keyword">with</span> transport <span class="hljs-string">'stdio'</span>
[INFO] Server ready <span class="hljs-keyword">for</span> connections
</code></pre>
<p>This output confirms your MCP server is working correctly. The server uses standard input/output (STDIO) for communication, which is perfect for Claude Desktop integration. You can stop the server with <code>Ctrl+C</code>.</p>
<h2 id="heading-how-to-configure-claude-desktop">How to Configure Claude Desktop</h2>
<p>Now that your server runs properly, configure Claude Desktop to connect to it. The configuration file location depends on your operating system:</p>
<p><strong>For macOS:</strong></p>
<pre><code class="lang-bash">~/Library/Application Support/Claude/claude_desktop_config.json
</code></pre>
<p><strong>For Windows:</strong></p>
<pre><code class="lang-bash">%APPDATA%\Claude\claude_desktop_config.json
</code></pre>
<p><strong>For Linux:</strong></p>
<pre><code class="lang-bash">~/.config/Claude/claude_desktop_config.json
</code></pre>
<p>Create or edit this file with your exact paths. Your configuration should look like this:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"mcpServers"</span>: {
    <span class="hljs-attr">"example-mcp"</span>: {
      <span class="hljs-attr">"command"</span>: <span class="hljs-string">"/Users/yourusername/path/to/mcp-server/venv/bin/python"</span>,
      <span class="hljs-attr">"args"</span>: [<span class="hljs-string">"/Users/yourusername/path/to/mcp-server/server.py"</span>],
      <span class="hljs-attr">"cwd"</span>: <span class="hljs-string">"/Users/yourusername/path/to/mcp-server"</span>
    }
  }
}
</code></pre>
<p>Replace <code>/Users/yourusername/path/to/mcp-server/</code> with your actual path. You can get your precise path by running <code>pwd</code> in your MCP server directory.</p>
<p>The configuration tells Claude Desktop:</p>
<ul>
<li><p>Which Python interpreter to use (from your virtual environment)</p>
</li>
<li><p>Where to find the server script</p>
</li>
<li><p>Which directory to run the server from</p>
</li>
</ul>
<h2 id="heading-how-to-restart-claude-desktop-and-test-integration">How to Restart Claude Desktop and Test Integration</h2>
<p>After saving your configuration file, altogether quit Claude Desktop (not just close the window). On macOS, use <code>Cmd+Q</code> or right-click the dock icon and select Quit. Then restart Claude Desktop.</p>
<p>Once Claude Desktop is running again, test your MCP integration. You can verify the connection by:</p>
<ol>
<li><p>Looking for your MCP server name in Claude's interface</p>
</li>
<li><p>Testing basic MCP functionality with prompts like:</p>
<ul>
<li><p>"What MCP tools are available?"</p>
</li>
<li><p>"Can you check the MCP server status?"</p>
</li>
<li><p>"Show me the available MCP commands"</p>
</li>
</ul>
</li>
</ol>
<p>If everything is working correctly, Claude will respond using the MCP server tools, confirming successful integration.</p>
<h2 id="heading-understanding-mcp-server-capabilities">Understanding MCP Server Capabilities</h2>
<p>MCP servers extend Claude's capabilities by providing structured access to external tools and data sources. Common MCP server implementations include:</p>
<ol>
<li><p>File system operations: MCP servers can provide controlled access to local files, allowing Claude to read, analyze, and process documents while maintaining security boundaries.</p>
</li>
<li><p>API integrations: Connect Claude to external services through MCP servers that handle authentication, rate limiting, and data formatting for various APIs.</p>
</li>
<li><p>Database connections: Query databases safely through MCP servers that manage connections, handle credentials securely, and format results for Claude's consumption.</p>
</li>
<li><p>Custom tools: Build specialized tools for your workflow, from code analysis to data processing, all accessible through the standardized MCP interface.</p>
</li>
</ol>
<p>The beauty of MCP is its flexibility – you can create servers for virtually any tool or service you need Claude to interact with.</p>
<h2 id="heading-alternative-installation-methods">Alternative Installation Methods</h2>
<p>If you want more streamlined approaches for future setups, here are two excellent alternatives:</p>
<h3 id="heading-method-1-direct-package-installation">Method 1: Direct Package Installation</h3>
<p>For MCP servers available as packages, you can install directly:</p>
<pre><code class="lang-bash">pip install mcp-server-package
</code></pre>
<p>Then use this simpler configuration:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"mcpServers"</span>: {
    <span class="hljs-attr">"example-mcp"</span>: {
      <span class="hljs-attr">"command"</span>: <span class="hljs-string">"mcp-server-command"</span>
    }
  }
}
</code></pre>
<p>This method works when the MCP server provides a command-line entry point through its setup configuration.</p>
<h3 id="heading-method-2-using-uv-package-manager">Method 2: Using UV Package Manager</h3>
<p>UV provides more robust dependency management – perfect if you're tired of Python version conflicts:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install UV</span>
curl -LsSf https://astral.sh/uv/install.sh | sh

<span class="hljs-comment"># Use UV in your configuration</span>
{
  <span class="hljs-string">"mcpServers"</span>: {
    <span class="hljs-string">"example-mcp"</span>: {
      <span class="hljs-string">"command"</span>: <span class="hljs-string">"uv"</span>,
      <span class="hljs-string">"args"</span>: [
        <span class="hljs-string">"run"</span>,
        <span class="hljs-string">"--with"</span>, <span class="hljs-string">"fastmcp"</span>,
        <span class="hljs-string">"python"</span>,
        <span class="hljs-string">"/path/to/mcp-server/server.py"</span>
      ],
      <span class="hljs-string">"cwd"</span>: <span class="hljs-string">"/path/to/mcp-server"</span>
    }
  }
}
</code></pre>
<p>UV automatically manages Python versions and dependencies, reducing the likelihood of environment-related errors.</p>
<h2 id="heading-how-to-prevent-future-enoent-errors">How to Prevent Future ENOENT Errors</h2>
<p>To avoid this issue in the future, follow these best practices:</p>
<h3 id="heading-1-use-virtual-environment-copies-instead-of-symlinks">1. Use Virtual Environment Copies Instead of Symlinks</h3>
<p>When creating virtual environments, use the <code>--copies</code> flag:</p>
<pre><code class="lang-bash">python3 -m venv venv --copies
</code></pre>
<p>This creates actual copies of files instead of symlinks, making your environment more resilient to Python upgrades.</p>
<h3 id="heading-2-pin-your-homebrew-python-version">2. Pin Your Homebrew Python Version</h3>
<p>Prevent automatic Python upgrades that break environments:</p>
<pre><code class="lang-bash">brew pin python@3.11
</code></pre>
<p>Remember to unpin when you're ready to upgrade intentionally.</p>
<h3 id="heading-3-create-a-health-check-script">3. Create a Health Check Script</h3>
<p>Save this script as <code>health_check.sh</code> in your MCP server directory:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>
<span class="hljs-comment"># health_check.sh</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Checking Python virtual environment..."</span>
<span class="hljs-built_in">source</span> venv/bin/activate

python -c <span class="hljs-string">"import sys; print(f'Python: {sys.executable}')"</span>
python -c <span class="hljs-string">"print('✓ Python is working')"</span>

<span class="hljs-comment"># Check for common MCP dependencies</span>
python -c <span class="hljs-string">"import json; print('✓ JSON module available')"</span>
python -c <span class="hljs-string">"import asyncio; print('✓ Asyncio available')"</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">"Health check complete!"</span>
</code></pre>
<p>Make it executable and run it periodically:</p>
<pre><code class="lang-bash">chmod +x health_check.sh
./health_check.sh
</code></pre>
<h3 id="heading-4-document-your-python-version">4. Document Your Python Version</h3>
<p>Create a <code>.python-version</code> file in your project:</p>
<pre><code class="lang-bash">python --version &gt; .python-version
</code></pre>
<p>This helps you remember which Python version the project was built with.</p>
<h2 id="heading-troubleshooting-common-issues">Troubleshooting Common Issues</h2>
<p>Even with the fix applied, you might encounter these challenges:</p>
<h3 id="heading-import-errors">Import Errors</h3>
<p>If you see import-related errors, ensure all dependencies are installed:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">source</span> venv/bin/activate
pip list  <span class="hljs-comment"># Check installed packages</span>
pip install -r requirements.txt  <span class="hljs-comment"># Reinstall if needed</span>
</code></pre>
<h3 id="heading-permission-denied-errors">Permission Denied Errors</h3>
<p>Make sure your server file is executable:</p>
<pre><code class="lang-bash">chmod +x server.py
</code></pre>
<h3 id="heading-claude-desktop-not-finding-the-server">Claude Desktop Not Finding the Server</h3>
<p>Double-check your configuration paths are absolute, not relative:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Good - absolute path</span>
<span class="hljs-string">"/Users/username/projects/mcp-server/server.py"</span>

<span class="hljs-comment"># Bad - relative path</span>
<span class="hljs-string">"./server.py"</span>
</code></pre>
<h3 id="heading-server-starts-but-claude-cant-connect">Server Starts, But Claude Can't Connect</h3>
<p>Verify that the transport method matches between your server and the configuration. Most MCP servers use STDIO, but some might use HTTP or WebSocket transports.</p>
<h3 id="heading-multiple-python-installations">Multiple Python Installations</h3>
<p>If you have multiple Python versions, be explicit about which one to use:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Check available Python versions</span>
ls -la /usr/<span class="hljs-built_in">local</span>/bin/python*

<span class="hljs-comment"># Use a specific version</span>
/usr/<span class="hljs-built_in">local</span>/bin/python3.11 -m venv venv
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've successfully fixed the "spawn python ENOENT" error by rebuilding your Python virtual environment and properly configuring your MCP server for Claude Desktop. You've also learned how to prevent future mistakes and troubleshoot common issues.</p>
<p>With your MCP server running smoothly, you can now:</p>
<ul>
<li><p>Build custom tools that extend Claude's capabilities</p>
</li>
<li><p>Create integrations with your favorite services</p>
</li>
<li><p>Develop specialized workflows for your specific needs</p>
</li>
<li><p>Share your MCP servers with the community</p>
</li>
</ul>
<p>The <a target="_blank" href="https://www.anthropic.com/news/model-context-protocol">MCP</a> ecosystem is growing rapidly, with new servers and tools being developed constantly. Whether you're building file system tools, API integrations, or custom utilities, you now have the foundation to create and maintain robust MCP servers.</p>
<p>Happy building, and enjoy your error-free development journey! For more tutorials, follow my work on <a target="_blank" href="https://github.com/Olanetsoft">GitHub</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
