<?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>Tue, 19 May 2026 04:42:55 +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 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="600" height="400" 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>
