<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Madhav Arora</title>
  <subtitle>Notes on AI engineering, agents, and building things</subtitle>
  <link href="https://madhavarora.net/"/>
  <link rel="self" href="https://madhavarora.net/atom.xml"/>
  <id>https://madhavarora.net/</id>
  <updated>2025-05-05T00:00:00+00:00</updated>
  <author><name>Madhav Arora</name></author>
  <entry>
    <title>How to Get Started with Model Context Protocol</title>
    <link href="https://madhavarora.net/blog/model_context_protocol_get_started.html"/>
    <id>https://madhavarora.net/blog/model_context_protocol_get_started.html</id>
    <updated>2025-05-05T00:00:00+00:00</updated>
    <content type="html">&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;br /&gt;
  Spin up a local SQLite MCP server, point your favourite LLM at it, and start talking SQL—in under five minutes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;img-20250505173159.png&quot; alt=&quot;alt text&quot; /&gt;
Source: Generated by Dall-E 3&lt;/p&gt;

&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;Large language models shine when they can &lt;strong&gt;reason over real data&lt;/strong&gt;, but handing them unfettered access to your production database is... well, terrifying. Enter &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;: a lightweight, tool‑based contract that lets you expose &lt;em&gt;just the right capabilities&lt;/em&gt; (read‑only queries, schema discovery, feedback collection, etc.) in a way that models can understand and invoke safely.&lt;/p&gt;

&lt;p&gt;In this guide we’ll walk through a minimal proof‑of‑concept (POC) MCP server built with &lt;strong&gt;FastMCP&lt;/strong&gt; and SQLite. We’ll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Setting up the server and sample database&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Exploring the custom tools we defined (execute_query, describe_table…)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Connecting the server to Claude Desktop for interactive querying&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ideas for extending the POC into something production‑ready&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code is public on &lt;a href=&quot;https://github.com/madhavarora1988/mcp_sqlite_poc&quot;&gt;GitHub&lt;/a&gt; — clone it, follow along, and drop questions in the comments if anything feels off.&lt;/p&gt;

&lt;h2 id=&quot;what-is-the-model-context-protocol&quot;&gt;What is the Model Context Protocol?&lt;/h2&gt;

&lt;p&gt;On &lt;strong&gt;November 25, 2024&lt;/strong&gt; Anthropic open‑sourced MCP, positioning it as a universal bridge between AI assistants and the systems where real data lives—content repositories, business apps, dev environments, you name it. Instead of writing a bespoke connector for every new data source, you expose a small set of JSON‑described tools on an &lt;strong&gt;MCP server&lt;/strong&gt; and let any &lt;strong&gt;MCP client&lt;/strong&gt; (Claude Desktop, an agent framework, your own script) call them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the fuss?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open, two‑way standard&lt;/strong&gt; — secure, structured requests &lt;em&gt;and&lt;/em&gt; responses flow between models and data stores.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Server / client split&lt;/strong&gt; — run a tiny server next to your database (Postgres, Git, Slack, Google Drive…), point the model at it, and you’re done. No extra glue.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ecosystem out of the gate&lt;/strong&gt; — Anthropic shipped ready‑made servers for Google Drive, Slack, GitHub, Postgres, and Puppeteer; early adopters like Block and Apollo already have MCP in production.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;First‑class in Claude Desktop&lt;/strong&gt; — local servers auto‑register so you can chat with your data straight from the desktop app.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not just databases — remote APIs too.&lt;/strong&gt; MCP servers can wrap any REST, GraphQL, or gRPC service (Stripe, Jira, your own microservice), letting models hit cloud APIs exactly like local tables.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Community‑driven&lt;/strong&gt; — spec, SDKs, and reference implementations all live in public repos—pull requests welcome.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With that backdrop, let’s zoom in on our SQLite demo and see where MCP really shines.&lt;/p&gt;

&lt;h3 id=&quot;general-architecture&quot;&gt;General architecture&lt;/h3&gt;

&lt;p&gt;MCP follows a simple &lt;strong&gt;client ↔ server&lt;/strong&gt; pattern that scales from your laptop to the cloud:
&lt;img src=&quot;img-20250505141259.png&quot; alt=&quot;alt text&quot; /&gt;
Source: &lt;a href=&quot;https://modelcontextprotocol.io/introduction&quot;&gt;https://modelcontextprotocol.io/introduction&lt;/a&gt;&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
  &lt;th&gt;Component&lt;/th&gt;
  &lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
  &lt;td&gt;&lt;strong&gt;MCP Host&lt;/strong&gt;&lt;/td&gt;
  &lt;td&gt;The UI or agent (Claude Desktop, your IDE) that wants context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;&lt;strong&gt;MCP Client&lt;/strong&gt;&lt;/td&gt;
  &lt;td&gt;Maintains a 1‑to‑1 connection with a server, handling auth &amp;amp; retries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;&lt;strong&gt;MCP Server&lt;/strong&gt;&lt;/td&gt;
  &lt;td&gt;Lightweight process that exposes domain‑specific tools via JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;&lt;strong&gt;Local Data Source&lt;/strong&gt;&lt;/td&gt;
  &lt;td&gt;Files, DBs, or services the server can access on your machine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;&lt;strong&gt;Remote Service&lt;/strong&gt;&lt;/td&gt;
  &lt;td&gt;External API the server can proxy (e.g., Stripe, Jira)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;A single host can juggle multiple servers at once—imagine Claude Desktop chatting with your local SQLite DB, a Git repo, and a SaaS ticketing API simultaneously.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;What we’ll build in this article:&lt;/strong&gt; we’re going to implement the &lt;strong&gt;MCP Server&lt;/strong&gt; block—highlighted above—using Python, FastMCP, and SQLite. By the end you’ll have a fully‑functioning server that exposes database tools the host (Claude Desktop) can call in real time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;in-a-hurry&quot;&gt;In a Hurry?&lt;/h2&gt;

&lt;p&gt;If you just want to run the demo locally, the quick‑start is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# 1. Clone the repo
git clone https://github.com/madhavarora1988/mcp_sqlite_poc.git
cd mcp_sqlite_poc

# 2. Install dependencies
pip install -r requirements.txt

# 3. (Optional) generate a fresh SQLite DB with sample tables &amp;amp; rows
python generate_sample_db.py

# 4. Fire up the MCP server (read‑only by default)
python server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;By default the server listens on &lt;strong&gt;stdio&lt;/strong&gt;, which is exactly what Claude Desktop expects when it auto‑detects local MCP servers. If you’d rather expose an HTTP port, simply change the &lt;code&gt;transport&lt;/code&gt; parameter in &lt;code&gt;app.run_async()&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;understanding-the-poc&quot;&gt;Understanding the POC&lt;/h2&gt;

&lt;h3 id=&quot;project-layout&quot;&gt;Project layout&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;.
├── generate_sample_db.py  # creates sample.db with metrics, users, orders…
├── requirements.txt       # FastMCP, aiosqlite, pydantic…
├── server.py              # the SQLite MCP server
└── README.md              # extended docs &amp;amp; Env vars
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;generate_sample_db.py&lt;/strong&gt; bootstraps a realistic e‑commerce schema with products, orders, and an &lt;code&gt;order_analytics&lt;/code&gt; view so you have something to query straight away.&lt;br /&gt;
&lt;strong&gt;server.py&lt;/strong&gt; exposes six MCP tools and handles validation, logging, and optional read‑only mode.&lt;/p&gt;

&lt;h3 id=&quot;key-safety-features&quot;&gt;Key safety features&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Read‑only toggle&lt;/strong&gt; – controlled via the &lt;code&gt;READ_ONLY&lt;/code&gt; env var (defaults to &lt;code&gt;true&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;validate_query()&lt;/code&gt;&lt;/strong&gt; – rejects any SQL that starts with or contains risky verbs (&lt;code&gt;DROP&lt;/code&gt;, &lt;code&gt;PRAGMA&lt;/code&gt;, &lt;code&gt;ATTACH&lt;/code&gt;, etc.) before it touches the database.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Param‑sanitised inserts&lt;/strong&gt; – when &lt;code&gt;READ_ONLY&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;, sample‑data helpers still use parameterised statements to avoid injection.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;available-tools&quot;&gt;Available tools&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
  &lt;th&gt;Tool&lt;/th&gt;
  &lt;th&gt;Purpose&lt;/th&gt;
  &lt;th&gt;Typical Payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
  &lt;td&gt;&lt;code&gt;execute_query&lt;/code&gt;|Run a SELECT (or safe write) statement&lt;/td&gt;
  &lt;td&gt;&lt;code&gt;{&quot;query&quot;: &quot;SELECT * FROM products LIMIT 3;&quot;}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;&lt;code&gt;list_tables&lt;/code&gt;|See what objects are available&lt;/td&gt;
  &lt;td&gt;&lt;code&gt;{}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;&lt;code&gt;describe_table&lt;/code&gt;|Get column names and types&lt;/td&gt;
  &lt;td&gt;&lt;code&gt;{&quot;table_name&quot;: &quot;orders&quot;}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;&lt;code&gt;count_rows&lt;/code&gt;|Quick row count&lt;/td&gt;
  &lt;td&gt;&lt;code&gt;{&quot;table_name&quot;: &quot;order_items&quot;}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;&lt;code&gt;insert_sample_data&lt;/code&gt;|Populate demo rows (write‑mode only)&lt;/td&gt;
  &lt;td&gt;&lt;code&gt;{&quot;table_name&quot;: &quot;customers&quot;, &quot;count&quot;: 10}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;&lt;code&gt;add_feedback&lt;/code&gt;|Collect user feedback&lt;/td&gt;
  &lt;td&gt;&lt;code&gt;{&quot;user&quot;: &quot;Alice&quot;, &quot;email&quot;: &quot;...&quot;, &quot;feedback&quot;: &quot;Great!&quot;}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Feel free to extend this list with your own domain‑specific tools—just decorate an async function with &lt;code&gt;@app.tool(&quot;my_tool&quot;)&lt;/code&gt; and FastMCP will handle the schema for you.&lt;/p&gt;

&lt;h2 id=&quot;testing-in-claude-desktop&quot;&gt;Testing in Claude Desktop&lt;/h2&gt;

&lt;p&gt;Claude Desktop automatically scans its configuration file for MCP servers and makes them available in any chat. On macOS you’ll find (or create) that file here:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;~/Library/Application Support/Claude/claude_desktop_config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Append (or merge) the following block so Claude knows how to start our SQLite server:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
  &quot;mcpServers&quot;: {
    &quot;sqlite&quot;: {
      &quot;command&quot;: &quot;&amp;lt;absolute uv path&amp;gt;&quot;,
      &quot;args&quot;: [
        &quot;--directory&quot;,
        &quot;&amp;lt;Path to workspace&amp;gt;&quot;,
        &quot;run&quot;,
        &quot;server.py&quot;
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;absolute uv path&amp;gt;&lt;/code&gt;&lt;/strong&gt; – full path to the &lt;strong&gt;uv&lt;/strong&gt; executable (e.g. &lt;code&gt;/usr/local/bin/uv&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;Path to workspace&amp;gt;&lt;/code&gt;&lt;/strong&gt; – folder that contains &lt;code&gt;server.py&lt;/code&gt;, &lt;code&gt;generate_sample_db.py&lt;/code&gt;, and &lt;code&gt;sample.db&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save the file—if the path didn’t exist, macOS will create the &lt;code&gt;Claude&lt;/code&gt; directory chain automatically—then &lt;strong&gt;restart Claude Desktop&lt;/strong&gt;, and you should see &lt;strong&gt;sqlite&lt;/strong&gt; listed in the left‑hand &lt;em&gt;Context&lt;/em&gt; panel. You’ll also notice a small &lt;em&gt;hammer icon&lt;/em&gt; with a count (e.g. &lt;strong&gt;6&lt;/strong&gt;) that reads &lt;strong&gt;“MCP tools available”&lt;/strong&gt; when you hover—proof that the server registered correctly (see Screenshot 1 below).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;img-20250505141228.png&quot; alt=&quot;alt text&quot; /&gt;
Source: Image by Author&lt;/p&gt;

&lt;p&gt;Click that icon and Claude pops up a modal detailing every tool your server exposed (Screenshot 2). From here you can double‑check descriptions—or skip ahead to the &lt;strong&gt;Example interactive query&lt;/strong&gt; section below to watch a full conversation in action.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;img-20250505141156.png&quot; alt=&quot;alt text&quot; /&gt;
Available Tools(Source: Image by Author)&lt;/p&gt;

&lt;h3 id=&quot;example-interactive-query&quot;&gt;Example interactive query&lt;/h3&gt;

&lt;p&gt;Once the tools show up you can chat naturally, and Claude will chain the right calls for you. In the screenshots below I asked three follow‑up questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&quot;Can you check how many orders were placed?&quot;&lt;/strong&gt; – Claude inspected the schema with &lt;code&gt;list_tables&lt;/code&gt;, used &lt;code&gt;count_rows&lt;/code&gt;, and reported &lt;strong&gt;7 orders&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;img-20250427195246.png&quot; alt=&quot;alt text&quot; /&gt;
Source:Image by Author&lt;/p&gt;

&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&quot;Yes, who placed them?&quot;&lt;/strong&gt; – Claude peeked at table schemas (&lt;code&gt;describe_table&lt;/code&gt;), executed a join via &lt;code&gt;execute_query&lt;/code&gt;, and returned a customer‑by‑customer breakdown. 
&lt;img src=&quot;img-20250427195300.png&quot; alt=&quot;alt text&quot; /&gt;
Source:Image by Author&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&quot;User John says &quot;This product is awesome&quot; — please update the associated table.&quot;&lt;/strong&gt; – Claude validated the schema again and invoked &lt;code&gt;add_feedback&lt;/code&gt; to insert the comment into the &lt;strong&gt;feedback&lt;/strong&gt; table, confirming success with the returned ID.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;img-20250427195557.png&quot; alt=&quot;alt text&quot; /&gt;
Source: Image by Author&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;💡 Notice how the assistant narrates each tool invocation, so you can audit exactly what was run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Feel free to riff on this flow—ask for the most expensive order, daily sales totals, add new rows (with read‑only off), or fetch inactive users and watch the model stitch together the correct SQL on the fly.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;putting-it-all-together&quot;&gt;Putting It All Together&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Spin up the server&lt;/strong&gt; – local, container, or cloud function.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add it to your model config&lt;/strong&gt; – Claude Desktop, Anthropic SDK, LangChain, etc.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Let the model explore the schema&lt;/strong&gt; – start with &lt;code&gt;list_tables()&lt;/code&gt; and &lt;code&gt;describe_table()&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Iterate&lt;/strong&gt; – add domain‑specific tools (e.g., &lt;code&gt;forecast_sales&lt;/code&gt;, &lt;code&gt;flag_anomaly&lt;/code&gt;) as your use‑case grows.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lock it down&lt;/strong&gt; – once happy, switch the DB to read‑only or point the server at a replica.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;suggestions&quot;&gt;Suggestions&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep it small.&lt;/strong&gt; Only expose tables the model genuinely needs (views are great for this).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Log aggressively.&lt;/strong&gt; The demo prints every query with user/time stamps—pipe that into Loki or CloudWatch in prod.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fail loudly.&lt;/strong&gt; If a malformed query sneaks past validation, raise a clear error so the model can retry with a better prompt.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Document tool semantics.&lt;/strong&gt; Use the &lt;code&gt;description&lt;/code&gt; field in &lt;code&gt;@app.tool&lt;/code&gt; to teach the model what each action does.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Model Context Protocol turns the vague idea of “let the LLM hit the database” into a concrete, enforceable contract. With less than 200 lines of Python we’ve wired up authentication‑free SQLite access, safe query validation, and a friendly tool catalogue the model can browse. Give the POC a whirl, customise it for your own datasets, and let me know in the comments what you build!&lt;/p&gt;
</content>
  </entry>
</feed>
