<?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom"> <generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator> <link href="https://blog.jaysinh.dev/tags/learning/feed.xml" rel="self" type="application/atom+xml"/> <link href="https://blog.jaysinh.dev/" rel="alternate" type="text/html"/> <updated>2026-09-20T05:58:41+00:00</updated> <id>https://blog.jaysinh.dev/</id> <title type="html">Jaysinh’s own heed</title> <subtitle>Software Engineer at Amazon, building large-scale distributed systems in the transportation and finance domains. I write about distributed systems, engineering leadership, and generative AI — passing on whatever I learn. </subtitle> <entry> <title type="html">I wrote the code and let an LLM review it</title> <link href="https://blog.jaysinh.dev/2026/09/20/i-wrote-the-mcp-server-and-let-an-llm-review-it.html" rel="alternate" type="text/html" title="I wrote the code and let an LLM review it"/> <published>2026-09-20T05:03:12+00:00</published> <updated>2026-09-20T05:03:12+00:00</updated> <id>https://blog.jaysinh.dev/2026/09/20/i-wrote-the-mcp-server-and-let-an-llm-review-it</id> <content type="html" xml:base="https://blog.jaysinh.dev/2026/09/20/i-wrote-the-mcp-server-and-let-an-llm-review-it.html">&lt;p&gt;&lt;img src=&quot;https://blog.jaysinh.dev/assets/images/transcript_mcp/llm_as_guru.jpg&quot; alt=&quot;A guru at sunset, seated under a tree, whose head is a glowing circuit-board brain labelled LLM, placing a hand in blessing on a kneeling student; beside them a stack of books titled Knowledge, Reasoning, Perspective, Clarity and Better Thinking&quot; /&gt;&lt;/p&gt; &lt;p&gt;I learned the fundamentals of MCP architecture with an LLM. Instead of passive reading, I implemented a pet project in which the LLM was responsible for preparing a project plan, reviewing my code, answering my questions, tracking project progress but not writing a single line of code. In this post, I will describe how I leveraged an LLM for this exercise.&lt;/p&gt; &lt;h2 id=&quot;why-should-i-still-write-code-by-hand&quot;&gt;Why should I still write code by hand?&lt;/h2&gt; &lt;p&gt;Learning can’t be poured into my head. I understand better when I do things by hand. During my undergraduate studies, I soon realized that reading computer science theory or a concept of a programming language from a book did not help much, but I was getting a better understanding when I was implementing it. Rather than considering programming as a subject, I was pushing hard to implement the exercises. Now vibe coding is popular tech slang, but in my time “Get your hands dirty with code” was a popular phrase. The Recurse Center (formerly Hacker School) has always championed learning by building. In a recent analysis of AI’s role in learning programming, they highlighted the danger of letting LLMs do things for you:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;“You can no sooner learn a hard skill like programming by passively consuming LLM output than you can by merely listening to a teacher talk. As Holt put it: ‘We learn to do something by doing it. There is no other way.’” — Recurse Center, &lt;a href=&quot;https://www.recurse.com/blog/191-developing-our-position-on-ai&quot;&gt;Developing our position on AI&lt;/a&gt;&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;I am not against using LLMs for generating code. I should leverage LLMs to ship fast but when it comes to learning, slow and steady wins the race. When I write code without using an LLM, I slow down which gives time to my brain to digest things better. I can take a pause to think, question or retrospect and then come back. Slowing down helps me to form a deeper understanding. I can identify flaws in my assumptions and discover underlying truths. I am not the only one who is insisting on slowing down while learning. Please read Harvard’s &lt;a href=&quot;https://www.gse.harvard.edu/ideas/ed-magazine/19/08/no-need-speed&quot;&gt;No Need for Speed&lt;/a&gt; and &lt;a href=&quot;https://schoolofthought.substack.com/p/slow-down-you-learn-too-fast&quot;&gt;Slow down! You learn too fast&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&quot;creating-a-learning-plan&quot;&gt;Creating a learning plan&lt;/h2&gt; &lt;p&gt;For the first time, I had a learning plan which was divided into small actionable steps. Each step had an estimated time to complete. Additionally, my learning plan also had stretch goals. Each step was isolated, and it also had an expected quantifiable outcome. All steps were sequentially arranged as if milestones of a long term project.&lt;/p&gt; &lt;style&gt; .llm-block &gt; summary { cursor: pointer; } .llm-block pre { white-space: pre-wrap; word-break: break-word; } &lt;/style&gt; &lt;details class=&quot;llm-block&quot; open=&quot;&quot;&gt; &lt;summary&gt;&lt;strong&gt;Prompt&lt;/strong&gt; — sent to Claude Opus 5 (max effort)&lt;/summary&gt; &lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Role: You are a principle software engineer at Amazon. You are having 10+ years of experience. Background: You are my mentor. I am your mentee. You are guiding me on my next project. Ask: &lt;span class=&quot;p&quot;&gt; *&lt;/span&gt; I want to write an MCP server which will be responsible for downloading transcript of a youtube video. &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; I will connect this mcp server with Claude and I will share a link of a youtube video and tell Claude to give me summary of the video. Claude should invoke my MCP server to fetch transcript of the video and perform analysis. &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; The mcp server should be independent of Claude. It should be configurable with any other LLM. &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; The mcm server will run locally on my workstation. As a docker container service or demon etc. &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; I know Java and Python. We can decide any language to implement this. &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; I have found this https://github.com/jdepoix/youtube-transcript-api package which helps me to download the transcript via api and it is written in Python.&lt;span class=&quot;sb&quot;&gt; &lt;/span&gt;Rules: &lt;span class=&quot;p&quot;&gt; *&lt;/span&gt; Understand the requirements. Ask if there are any followup questions until all answers to complete this task is received. &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; You are not responsible for writing this task. You are responsible for preparing a plan to achieve this in step by step manner. I will implement this to practice and sharpen my programming skills. You will act as instructor or tutor. You will verify my solutions. &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; Include tentative time it can take for each step execution and also include over how much time would it would take to complete. &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; Each step should have its reference like blog post or any reference material, I will consume them, and then I will write code accordingly. &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt; &lt;/div&gt; &lt;/details&gt; &lt;details class=&quot;llm-block&quot; open=&quot;&quot;&gt; &lt;summary&gt;&lt;strong&gt;Follow-up questions&lt;/strong&gt;&lt;/summary&gt; &lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;1.&lt;/span&gt; Which language do you want to implement the server in? Python (Recommended) &lt;span class=&quot;p&quot;&gt;2.&lt;/span&gt; How should Claude (and other LLMs) connect to the server? Streamable HTTP (Recommended) &lt;span class=&quot;p&quot;&gt;3.&lt;/span&gt; How familiar are you with the MCP protocol itself? New to MCP &lt;span class=&quot;p&quot;&gt;4.&lt;/span&gt; Beyond raw transcript text, which capabilities should the plan include? Metadata &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt; &lt;/div&gt; &lt;/details&gt; &lt;p&gt;&lt;sup&gt;Claude Opus 5 is a bit verbose. I am only pasting the core plan.&lt;/sup&gt;&lt;/p&gt; &lt;details class=&quot;llm-block&quot;&gt; &lt;summary&gt;&lt;strong&gt;Generated execution plan&lt;/strong&gt; — 9 steps + stretch goals (click to expand)&lt;/summary&gt; &lt;p&gt;&lt;strong&gt;Learning Plan: YouTube Transcript MCP Server&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Prerequisites / Environment&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Python 3.11+ (3.12 recommended).&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; for env + dependency management (fast, now the MCP-docs default). pip+venv is fine too.&lt;/li&gt; &lt;li&gt;Docker Desktop (for the containerization step).&lt;/li&gt; &lt;li&gt;Node.js (only to run the MCP Inspector via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx&lt;/code&gt; — our primary testing tool).&lt;/li&gt; &lt;li&gt;Claude Desktop installed (for the final end-to-end step).&lt;/li&gt; &lt;/ul&gt; &lt;hr /&gt; &lt;p&gt;&lt;strong&gt;Step 0 — Project scaffold &amp;amp; tooling&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;⏱ Estimate: 1–2 hrs&lt;/strong&gt; (more if you’re new to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt;/Docker install)&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; A clean Python project you can run and test, before any MCP or YouTube code exists.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Concepts to internalize:&lt;/strong&gt; virtual environments, dependency pinning, project layout (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/&lt;/code&gt; layout vs flat), why we isolate deps.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Build:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Create a new git repo (separate from this blog repo).&lt;/li&gt; &lt;li&gt;Initialize with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv init&lt;/code&gt;; add deps: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mcp&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;youtube-transcript-api&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;httpx&lt;/code&gt;.&lt;/li&gt; &lt;li&gt;Layout: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/yt_transcript_mcp/&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server.py&lt;/code&gt;, plus a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tests/&lt;/code&gt; dir and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyproject.toml&lt;/code&gt;.&lt;/li&gt; &lt;li&gt;Add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;README.md&lt;/code&gt; stub and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitignore&lt;/code&gt;.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;uv guide: https://docs.astral.sh/uv/ (read “Working on projects”).&lt;/li&gt; &lt;li&gt;Python packaging / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; layout: https://packaging.python.org/en/latest/tutorials/packaging-projects/&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;How I’ll verify:&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv run python -c &quot;import mcp, youtube_transcript_api&quot;&lt;/code&gt; succeeds; repo layout is clean; deps are pinned in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyproject.toml&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv.lock&lt;/code&gt;.&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;strong&gt;Step 1 — MCP fundamentals (reading only, no code)&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;⏱ Estimate: 1.5–2 hrs&lt;/strong&gt; (pure reading + note-taking)&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; Be able to explain, in your own words: what MCP is, the difference between &lt;strong&gt;tools / resources / prompts&lt;/strong&gt;, the client–server–host model, and what “transport” means (stdio vs Streamable HTTP).&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Concepts to internalize:&lt;/strong&gt; MCP is a &lt;em&gt;protocol&lt;/em&gt; (JSON-RPC based) that standardizes how an LLM app discovers and calls external capabilities. A &lt;strong&gt;tool&lt;/strong&gt; is a function the model can choose to call. The &lt;strong&gt;host&lt;/strong&gt; (e.g. Claude Desktop) runs a &lt;strong&gt;client&lt;/strong&gt; that talks to your &lt;strong&gt;server&lt;/strong&gt;.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Intro: https://modelcontextprotocol.io/introduction&lt;/li&gt; &lt;li&gt;Core architecture: https://modelcontextprotocol.io/docs/concepts/architecture&lt;/li&gt; &lt;li&gt;Tools concept: https://modelcontextprotocol.io/docs/concepts/tools&lt;/li&gt; &lt;li&gt;Transports (find “Streamable HTTP”): https://modelcontextprotocol.io/docs/concepts/transports &lt;em&gt;(Note: MCP docs URLs shift occasionally — if a link 404s, search from the docs home.)&lt;/em&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;How I’ll verify:&lt;/strong&gt; You give me a 4–5 sentence explanation of tool-vs-resource and why we chose HTTP transport for this project. (No code — this is the conceptual foundation everything rests on.)&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;strong&gt;Step 2 — “Hello world” MCP server over Streamable HTTP + test in Inspector&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;⏱ Estimate: 2–3 hrs&lt;/strong&gt; (first contact with FastMCP + Inspector is where the time goes)&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; A minimal FastMCP server exposing one trivial tool (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;add(a, b)&lt;/code&gt;), served over Streamable HTTP, and successfully called from the MCP Inspector. This proves your transport, tooling, and mental model &lt;em&gt;before&lt;/em&gt; YouTube enters the picture.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Concepts to internalize:&lt;/strong&gt; how FastMCP turns a decorated Python function into a tool; how it infers the input schema from &lt;strong&gt;type hints + docstring&lt;/strong&gt; (so docstrings are not optional — the LLM reads them to decide when/how to call your tool); how to run over &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;streamable-http&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Build:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server.py&lt;/code&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;from mcp.server.fastmcp import FastMCP&lt;/code&gt;; create &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mcp = FastMCP(&quot;yt-transcript&quot;)&lt;/code&gt;; decorate a function with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@mcp.tool()&lt;/code&gt;; run with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mcp.run(transport=&quot;streamable-http&quot;)&lt;/code&gt;.&lt;/li&gt; &lt;li&gt;Note the default endpoint path (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/mcp&lt;/code&gt;) and port; make host &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.0.0.0&lt;/code&gt; so Docker can expose it later.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Python SDK README (quickstart + “Streamable HTTP” section): https://github.com/modelcontextprotocol/python-sdk&lt;/li&gt; &lt;li&gt;Server quickstart: https://modelcontextprotocol.io/quickstart/server&lt;/li&gt; &lt;li&gt;MCP Inspector: https://github.com/modelcontextprotocol/inspector (run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx @modelcontextprotocol/inspector&lt;/code&gt;, connect via &lt;strong&gt;Streamable HTTP&lt;/strong&gt; to your URL).&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;How I’ll verify:&lt;/strong&gt; You connect Inspector to the running server, list tools, invoke &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;add&lt;/code&gt;, and get the right answer back. Screenshot or paste the Inspector result.&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;strong&gt;Step 3 — URL → video ID parser (pure function + unit tests)&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;⏱ Estimate: 1.5–2 hrs&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; A robust, well-tested function &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extract_video_id(url) -&amp;gt; str&lt;/code&gt; handling all common YouTube URL shapes: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;watch?v=&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;youtu.be/&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/shorts/&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/embed/&lt;/code&gt;, extra query params, and invalid input.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Concepts to internalize:&lt;/strong&gt; parse, don’t regex-guess — use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;urllib.parse&lt;/code&gt;; fail loudly on bad input with a clear exception; &lt;strong&gt;test-first&lt;/strong&gt; design (this is the perfect place to practice TDD because the function is pure and the cases are enumerable).&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Build:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extract_video_id&lt;/code&gt; in a small &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;youtube.py&lt;/code&gt; module (keep it out of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server.py&lt;/code&gt; — separation of concerns).&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tests/test_url.py&lt;/code&gt; covering each URL shape + at least two invalid cases.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;urllib.parse&lt;/code&gt;: https://docs.python.org/3/library/urllib.parse.html&lt;/li&gt; &lt;li&gt;pytest getting started: https://docs.pytest.org/en/stable/getting-started.html&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;How I’ll verify:&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv run pytest&lt;/code&gt; is green; I’ll throw 2–3 adversarial URLs at your function and check it handles them (or rejects them cleanly).&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;strong&gt;Step 4 — Fetch the transcript (library function + error handling)&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;⏱ Estimate: 2–3 hrs&lt;/strong&gt; (the version-drift gotcha + exception handling eat time)&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; A plain function &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetch_transcript(video_id) -&amp;gt; str&lt;/code&gt; (not yet an MCP tool) that returns the joined transcript text, with real error handling.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Concepts to internalize:&lt;/strong&gt; reading a third-party library’s API from its README/source; &lt;strong&gt;the library’s API changed across versions&lt;/strong&gt; — older code uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;YouTubeTranscriptApi.get_transcript(...)&lt;/code&gt; while newer versions instantiate and call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.fetch(...)&lt;/code&gt;. &lt;em&gt;Check the version you installed and use the matching API&lt;/em&gt; (great real-world lesson in not trusting stale tutorials). Handle the real failure modes explicitly: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TranscriptsDisabled&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NoTranscriptFound&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VideoUnavailable&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Build:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetch_transcript&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;youtube.py&lt;/code&gt;; join the returned segments into one string.&lt;/li&gt; &lt;li&gt;Map library exceptions to your own clear, user-facing error messages (the LLM will surface these).&lt;/li&gt; &lt;li&gt;Awareness note: YouTube blocks &lt;em&gt;datacenter&lt;/em&gt; IPs. Running locally on your workstation (residential IP) is fine; if you ever host this in the cloud you may get blocked — worth a comment.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;The library README (API usage + exception list): https://github.com/jdepoix/youtube-transcript-api&lt;/li&gt; &lt;li&gt;Read the installed source for the exact current method names (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.venv/.../youtube_transcript_api&lt;/code&gt;).&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;How I’ll verify:&lt;/strong&gt; Call it from a scratch script on a real public video (with captions) → get text; on a video with captions disabled → get your clean error, not a raw stack trace.&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;strong&gt;Step 5 — Fetch metadata (title / author / thumbnail)&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;⏱ Estimate: 1.5–2 hrs&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; A function &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetch_metadata(video_id) -&amp;gt; dict&lt;/code&gt; returning title, author, and thumbnail — &lt;strong&gt;no API key required.&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Concepts to internalize:&lt;/strong&gt; picking the &lt;em&gt;lightest&lt;/em&gt; tool for the job. YouTube’s &lt;strong&gt;oEmbed&lt;/strong&gt; endpoint (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://www.youtube.com/oembed?url=&amp;lt;watch-url&amp;gt;&amp;amp;format=json&lt;/code&gt;) returns title, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;author_name&lt;/code&gt;, and thumbnail with no key and no auth — ideal for v1. Trade-off to understand: oEmbed does &lt;strong&gt;not&lt;/strong&gt; include duration. If you later want duration, that needs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yt-dlp&lt;/code&gt; (heavier dep) or the YouTube Data API (needs a key) — note it as a documented v2 option, don’t build it now.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Build:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetch_metadata&lt;/code&gt; using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;httpx&lt;/code&gt; to call oEmbed; handle non-200 / missing video gracefully.&lt;/li&gt; &lt;li&gt;Decide your metadata dict shape (keep it small and explicit).&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;oEmbed spec: https://oembed.com/&lt;/li&gt; &lt;li&gt;httpx quickstart: https://www.python-httpx.org/quickstart/&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;How I’ll verify:&lt;/strong&gt; Returns correct title/author for a known video; degrades gracefully (empty/partial metadata, not a crash) when oEmbed fails.&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;strong&gt;Step 6 — Compose the real MCP tool&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;⏱ Estimate: 1.5–2 hrs&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; Replace the hello-world &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;add&lt;/code&gt; tool with the real one: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_video_transcript(url: str) -&amp;gt; dict&lt;/code&gt; that ties together Steps 3–5 and returns a structured result &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{ transcript, metadata }&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Concepts to internalize:&lt;/strong&gt; designing a tool’s &lt;em&gt;contract&lt;/em&gt; for an LLM consumer — a precise docstring (this is the tool’s “prompt”), typed inputs, and a predictable structured output; keeping &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server.py&lt;/code&gt; thin (it orchestrates; the logic lives in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;youtube.py&lt;/code&gt;); returning errors in a form the model can relay to the user.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Build:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@mcp.tool()&lt;/code&gt; wrapper calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extract_video_id → fetch_transcript → fetch_metadata&lt;/code&gt;.&lt;/li&gt; &lt;li&gt;Write the docstring deliberately: say what it does, what a valid &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;url&lt;/code&gt; looks like, what it returns.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Tool design / structured output in the Python SDK README (tools section): https://github.com/modelcontextprotocol/python-sdk&lt;/li&gt; &lt;li&gt;Tools concept (recap): https://modelcontextprotocol.io/docs/concepts/tools&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;How I’ll verify:&lt;/strong&gt; In MCP Inspector, call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_video_transcript&lt;/code&gt; with a real URL → transcript + metadata come back; call with a bad URL and a captions-disabled video → clean, informative errors.&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;strong&gt;Step 7 — Containerize as a local service (Docker)&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;⏱ Estimate: 2–3 hrs&lt;/strong&gt; (add ~1 hr if Docker networking is new to you)&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; The server runs as a Docker container/daemon on your workstation, listening on a port, restartable, independent of any client.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Concepts to internalize:&lt;/strong&gt; slim base images (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python:3.12-slim&lt;/code&gt;), layer caching (copy dep files before source), binding to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.0.0.0&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EXPOSE&lt;/code&gt; + port mapping, running detached (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-d&lt;/code&gt;) and with a restart policy so it behaves like a daemon; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker compose&lt;/code&gt; for a one-command start.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Build:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; (install deps, copy source, expose the MCP port, run the server).&lt;/li&gt; &lt;li&gt;Optional &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt; with a restart policy.&lt;/li&gt; &lt;li&gt;Run detached; confirm the container survives and the port is reachable.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Docker Python guide: https://docs.docker.com/language/python/&lt;/li&gt; &lt;li&gt;Official Python image notes: https://hub.docker.com/_/python&lt;/li&gt; &lt;li&gt;Dockerfile best practices: https://docs.docker.com/build/building/best-practices/&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;How I’ll verify:&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker run -d -p &amp;lt;port&amp;gt;:&amp;lt;port&amp;gt; ...&lt;/code&gt;, then point MCP Inspector at the containerized URL and successfully call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_video_transcript&lt;/code&gt;. Container restarts cleanly.&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;strong&gt;Step 8 — Connect to Claude Desktop + prove LLM-agnosticism&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;⏱ Estimate: 1–2 hrs&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; End-to-end: paste a YouTube link in Claude, ask for a summary, watch Claude call &lt;em&gt;your&lt;/em&gt; containerized server, fetch the transcript, and summarize.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Concepts to internalize:&lt;/strong&gt; Claude Desktop connects to a remote/HTTP MCP server as a &lt;strong&gt;custom connector&lt;/strong&gt; (Settings → Connectors → Add custom connector → your local URL). Understand &lt;em&gt;why&lt;/em&gt; this is LLM-agnostic: nothing in your server knows about Claude — any MCP-capable client (other hosts, the OpenAI Agents SDK’s MCP support, etc.) can use the same URL. That’s the payoff of the HTTP- transport decision.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Build:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Register the running container’s URL as a custom connector in Claude Desktop.&lt;/li&gt; &lt;li&gt;Test prompt: &lt;em&gt;“Summarize this video: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;link&amp;gt;&lt;/code&gt;”&lt;/em&gt; → confirm Claude invokes the tool.&lt;/li&gt; &lt;li&gt;(Optional flex) point one other MCP client at the same URL to prove independence.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Connecting a server (user side): https://modelcontextprotocol.io/quickstart/user&lt;/li&gt; &lt;li&gt;Anthropic help on custom connectors / remote MCP: https://support.anthropic.com/en/articles/11175166-about-custom-connectors-remote-mcp &lt;em&gt;(If the connector UI differs on your version, MCP Inspector already proved the server works; the remaining issue is purely client-side wiring.)&lt;/em&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;strong&gt;How I’ll verify:&lt;/strong&gt; You share the Claude transcript showing the tool call + a summary derived from the real transcript. That’s “done.”&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;strong&gt;Stretch goals (v2 — only after v1 works end-to-end)&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;Each is a clean, isolated next lesson:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;strong&gt;Caching&lt;/strong&gt; — file or SQLite cache keyed by video ID, to avoid refetching and survive rate limits.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Language selection&lt;/strong&gt; — expose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list_transcripts()&lt;/code&gt;, let the caller pick a language / fall back to auto-generated captions.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Timestamps + chunking&lt;/strong&gt; — return timestamped segments; handle very long transcripts so summaries can cite moments.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Duration metadata&lt;/strong&gt; — add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yt-dlp&lt;/code&gt; for full metadata including duration.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Hardening&lt;/strong&gt; — structured logging, config via env vars, a proper test suite in CI, health check.&lt;/li&gt; &lt;/ol&gt; &lt;/details&gt; &lt;h2 id=&quot;executing-plan&quot;&gt;Executing plan&lt;/h2&gt; &lt;p&gt;I had not written production grade Python code in the last 7 years. Tools like &lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;uv&lt;/a&gt; and &lt;a href=&quot;https://gofastmcp.com/getting-started/welcome&quot;&gt;Fast-mcp&lt;/a&gt; were not invented when I was writing Python code. Without LLMs, I would have struggled to identify the right scope. By not using LLMs for writing code, I was going back to the library documentation to make choices. And I was also getting instant feedback on my choices. This helped me to slow down and as a result I remembered the syntax and examined my decisions. That would not have been possible if an LLM had written that code. After I completed my actions, I asked the LLM to verify the outcome against the expected outcome of a step. If I was going in a wrong direction, then I prompted the LLM to only give me high-level hints rather than fixing errors on my behalf.&lt;/p&gt; &lt;p&gt;The LLM estimated ~18 hours. I spent about 20+ hours, spread across ~10 sessions of 2+ hours on weekends and holidays. I took more time than planned, but I don’t want to blame the LLM for this. I am suffering from a perfectionist syndrome :/. In the middle of execution, I extended the scope of the project by making it more generic and extensible. I tried to apply design patterns, testing best practices which were never in execution plan. The LLM highlighted that I had done more than the plan and also acknowledged the importance of my decisions.&lt;/p&gt; &lt;p&gt;I executed this plan in-consistently. Because of the LLM, I could go back to the conversation and reload context in my brain from a point where I had left off. It was extremely helpful to sustain my motivation for long term execution.&lt;/p&gt; &lt;p&gt;I requested the LLM to evaluate my code at each step. The LLM reviewed my code, highlighted where I had diverged and summarized what was done and what remained.&lt;/p&gt; &lt;details class=&quot;llm-block&quot; open=&quot;&quot;&gt; &lt;summary&gt;&lt;strong&gt;Prompt&lt;/strong&gt; — requesting LLM to evaluate my work&lt;/summary&gt; &lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;I have completed step 0. Can you please verify that I have completed steps? Please try to help me if I have completed all expected steps. I have invoked &apos;uv lock&apos; at the end. I was going through https://packaging.python.org/en/latest/tutorials/packaging-projects/ and it says configuring &apos;uv&apos; as a build system. Can you please help me by sharing if I need to perform any updates to pyproject.toml file any build system specific configurations at step 0? Also, I am skipping updating the readme.md file right now. I will update at the last. &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt; &lt;/div&gt; &lt;/details&gt; &lt;details class=&quot;llm-block&quot;&gt; &lt;summary&gt;&lt;strong&gt;Feedback&lt;/strong&gt; - initial evaluation&lt;/summary&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Found it and reviewed it. Nice work — the core of Step 0 is solid. Here&apos;s my verification, then the answer to your build-system question (which is a genuinely good thing to have noticed). ✅ Verification checklist Item Status Notes Separate git repo ✅ git init done, outside the blog repo Three deps added ✅ httpx, mcp, youtube-transcript-api all in pyproject.toml uv.lock present ✅ you ran uv lock — good Import sanity check ✅ import mcp, youtube_transcript_api, httpx → ok src/ package ⚠️ empty src/ and tests/ dirs exist but have no files — no yt_transcript_mcp/, no server.py, no __init__.py .gitignore ⚠️ missing no root .gitignore (uv hides its own venv via .venv/.gitignore, but you still want a project one for __pycache__/, .pytest_cache/, etc.) main.py at root ℹ️ leftover uv init dropped a default main.py — we&apos;ll delete it once the package exists README ⏭️ skipped fine, as you said — do it last So: 80% there. Two real gaps (empty src/ package, missing .gitignore) and one leftover file. Let&apos;s fix those and answer your build-system question together, because they&apos;re connected. 🎓 Your question: do I need [build-system] config? Excellent instinct to ask. The short answer: it depends on whether your project is an application or a package — and because we chose a src/ layout, we want it to be a package. Here&apos;s the mental model: The packaging.python.org tutorial you read is about building distributable artifacts (wheels/sdists) to publish to PyPI. You are not publishing to PyPI. So you might think &quot;skip it.&quot; But there&apos;s a subtlety: App style (what uv init gave you by default): a flat main.py you run directly. No [build-system], and your project is not installed into the venv — so import yt_transcript_mcp would fail from a src/ layout. Package style (what we actually want): with a [build-system], uv sync installs your own code into the venv in editable mode. Then import yt_transcript_mcp works from anywhere — crucially, from your tests/ in Step 3 and when you run the server in Docker in Step 7. Since our plan uses src/yt_transcript_mcp/ and we&apos;ll import it from tests, yes — you should add a build-system. This is the correct call, not premature complexity. What to add to pyproject.toml: That&apos;s it — hatchling is the lightweight default build backend uv uses. (You could also have gotten all this automatically by running uv init --package instead of plain uv init. Good to know for next time.) 📖 Reference: uv — Project structure &amp;amp; --package/--lib and Build systems in uv. 🔎 One more heads-up: Python 3.14 You pinned requires-python = &quot;&amp;gt;=3.14&quot; and .python-version = 3.14. Imports pass, so it works — but 3.14 is bleeding-edge. The risk shows up later: some dependencies (or the python:3.14-slim Docker image in Step 7) may lag on prebuilt wheels, causing slow or failing builds. Not a blocker now. If you want the smoothest ride, pinning to 3.12 is the safe default. Your call — just know why I&apos;m flagging it. 📝 To close out Step 0, please: Add the [build-system] block above to pyproject.toml. Create the package: src/yt_mcp/__init__.py (empty) and src/yt_mcp/server.py (empty for now). Add tests/__init__.py (empty) so pytest discovers it cleanly later. Delete the leftover root main.py. Add a root .gitignore (at minimum: .venv/, __pycache__/, .pytest_cache/, *.pyc). Run uv sync — this should now install yt-mcp itself, then confirm: Then show me: your updated pyproject.toml, the output of that import check, and — you skipped these earlier 😄 — your answers to the two conceptual questions: Why a src/ layout instead of server.py in the repo root? What&apos;s the difference between a dep in pyproject.toml vs one pinned in uv.lock? Knock those out and Step 0 is a clean pass. 🚀 &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt; &lt;/div&gt; &lt;/details&gt; &lt;p&gt;&lt;br /&gt;&lt;/p&gt; &lt;p&gt;After performing remaining steps, I asked the LLM to re-verify with the prompt below.&lt;/p&gt; &lt;details class=&quot;llm-block&quot; open=&quot;&quot;&gt; &lt;summary&gt;&lt;strong&gt;Prompt&lt;/strong&gt;&lt;/summary&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Can you re-check? I have performed required changes &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt; &lt;/div&gt; &lt;/details&gt; &lt;details class=&quot;llm-block&quot;&gt; &lt;summary&gt;&lt;strong&gt;Feedback&lt;/strong&gt; — acknowledging that step-0 is completed&lt;/summary&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;That&apos;s a clean pass. ✅ [build-system] is now its own table, uv sync is happy, and import yt_mcp works — the project is correct and portable now. Step 0: COMPLETE. 🎉 Your scaffold is real: isolated env, pinned deps, proper src/ package that installs editable, tests dir ready, .gitignore in place. [ Truncated ] 🎯 Next: Step 1 — MCP fundamentals (reading only, ~1.5–2 hrs) [ Truncated ] [Intro / What is MCP](https://modelcontextprotocol.io/introduction) [Core architecture — host ↔ client ↔ server](https://modelcontextprotocol.io/docs/concepts/architecture) [Tools](https://modelcontextprotocol.io/docs/concepts/tools) [Transports — focus on Streamable HTTP (our choice)](https://modelcontextprotocol.io/docs/concepts/transports) (If a link 404s — MCP docs move occasionally — search from the docs home page.) As you read, hold these questions in mind (I&apos;ll quiz you, no code): [ Truncated ] &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt; &lt;/div&gt; &lt;/details&gt; &lt;p&gt;&lt;br /&gt; At some point, the LLM not only evaluated my progress but also acknowledged that I had done what was beyond the scope.&lt;/p&gt; &lt;details class=&quot;llm-block&quot;&gt; &lt;summary&gt;&lt;strong&gt;Prompt&lt;/strong&gt;&lt;/summary&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; I have updated the code and added requested exception. It should work fine now. Please validate and confirm if we are done with step-4 &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt; &lt;/div&gt; &lt;/details&gt; &lt;details class=&quot;llm-block&quot; open=&quot;&quot;&gt; &lt;summary&gt;&lt;strong&gt;Feedback&lt;/strong&gt;&lt;/summary&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Step 4 is complete. What you built beyond the original spec: a domain entity separate from the library&apos;s types, an ABC so the transcript source is swappable, a table-driven exception translation layer, retriable/non-retriable classification, and validation at the mapping boundary with DataError deliberately excluded from translation. That last one is the piece most people get wrong — your own bugs no longer masquerade as YouTube&apos;s. Two housekeeping items before Step 5: delete the stray 2 file at the repo root, and commit — Steps 3 and 4 are both still uncommitted. &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt; &lt;/div&gt; &lt;/details&gt; &lt;h2 id=&quot;outcome--transcript-mcp-server&quot;&gt;Outcome — Transcript MCP server&lt;/h2&gt; &lt;p&gt;Please find the Transcript MCP server code here: &lt;a href=&quot;https://github.com/ultimatecoder/transcript_mcp&quot;&gt;Github&lt;/a&gt;&lt;/p&gt; &lt;h3 id=&quot;architecture&quot;&gt;Architecture&lt;/h3&gt; &lt;p&gt;Nothing in the server knows which LLM is calling it. Any MCP client speaks to the same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost:8000/mcp&lt;/code&gt; endpoint over Streamable HTTP, and the server talks to YouTube through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;youtube-transcript-api&lt;/code&gt;. That is what the HTTP transport decision bought me.&lt;/p&gt; &lt;p&gt;&lt;img src=&quot;https://blog.jaysinh.dev/assets/images/transcript_mcp/architecture.png&quot; alt=&quot;Architecture diagram: Claude and Codex both call a single Transcript MCP server over Streamable HTTP at localhost:8000/mcp; the server exposes one tool, get_transcript(url), and fetches captions from YouTube through youtube-transcript-api, returning the transcript as JSON&quot; /&gt;&lt;/p&gt; &lt;p&gt;&lt;em&gt;Figure 1 — One MCP server, two interchangeable clients. Blue arrows are requests, green are responses.&lt;/em&gt;&lt;/p&gt; &lt;h3 id=&quot;providers-and-extractors&quot;&gt;Providers and extractors&lt;/h3&gt; &lt;p&gt;The tool itself is thin. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TranscriptProviderManager&lt;/code&gt; picks a provider, and the provider delegates URL parsing to a chain of extractors. Adding a second transcript source, or a new YouTube URL shape, means adding a class rather than editing an existing one.&lt;/p&gt; &lt;p&gt;&lt;img src=&quot;https://blog.jaysinh.dev/assets/images/transcript_mcp/request-flow.svg&quot; alt=&quot;Class diagram: the server module holds a FastMCP instance exposing get_transcript; a factory builds TranscriptProviderManager, which delegates to the TranscriptProvider abstract base class, implemented by YtTranscriptApiBasedProvider; URL parsing is delegated to YtURLAttributeExtractorCoordinator over an YtURLAttributeExtractor ABC with standard-URL and short-URL implementations&quot; /&gt;&lt;/p&gt; &lt;p&gt;&lt;em&gt;Figure 2 — Both extension points are abstract base classes: the transcript source and the URL extractor.&lt;/em&gt;&lt;/p&gt; &lt;h3 id=&quot;domain-model&quot;&gt;Domain model&lt;/h3&gt; &lt;p&gt;The library’s types never leave the boundary. Mappers convert &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FetchedTranscript&lt;/code&gt; into my own frozen dataclasses, so a change in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;youtube-transcript-api&lt;/code&gt; cannot ripple through the rest of the server.&lt;/p&gt; &lt;p&gt;&lt;img src=&quot;https://blog.jaysinh.dev/assets/images/transcript_mcp/domain-model.svg&quot; alt=&quot;Class diagram in three columns: external entities FetchedTranscript and FetchedTranscriptSnippet on the left, a column of mapper classes in the middle, and frozen dataclasses Transcript, TranscriptMetadata and TranscriptLineItem on the right&quot; /&gt;&lt;/p&gt; &lt;p&gt;&lt;em&gt;Figure 3 — External entities enter on the left, mappers translate, domain entities leave on the right.&lt;/em&gt;&lt;/p&gt; &lt;h3 id=&quot;error-model&quot;&gt;Error model&lt;/h3&gt; &lt;p&gt;Every library exception is translated into one of my own, classified as retriable or not. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DataError&lt;/code&gt; is deliberately excluded from that translation, so my own bugs surface as my own bugs instead of masquerading as a YouTube failure.&lt;/p&gt; &lt;p&gt;&lt;img src=&quot;https://blog.jaysinh.dev/assets/images/transcript_mcp/error-model.svg&quot; alt=&quot;Diagram mapping youtube_transcript_api exceptions such as VideoUnavailable, TranscriptsDisabled and NoTranscriptFound onto the server&apos;s own error types, each classified under RetriableError or NonRetriableError, with DataError marked as defined but not raised&quot; /&gt;&lt;/p&gt; &lt;p&gt;&lt;em&gt;Figure 4 — Where each error starts, and what the caller actually receives.&lt;/em&gt;&lt;/p&gt; &lt;h2 id=&quot;takeaways&quot;&gt;Takeaways&lt;/h2&gt; &lt;p&gt;The LLM never wrote a line of this server, and that was the point. It planned, reviewed, answered my questions and tracked progress. I typed. Seven years after my last production Python, I have a working MCP server running in a container, and I can still explain every decision in it without opening the code. The cost was real. The plan estimated ~18 hours and I spent about 20+ hours, spread across ~10 sessions that I kept skipping. I will do this again for the next unfamiliar technology. Writing code by hand is slower, and the slowness is the mechanism, not the price.&lt;/p&gt;</content> <author> <name>Jaysinh Shukla</name> </author> <category term="learning"/> <category term="MCP"/> <category term="AI"/> <summary type="html"></summary> </entry> </feed>