[{"data":1,"prerenderedAt":705},["ShallowReactive",2],{"navigation":3,"\u002Fblog\u002Fmcp-ext-apps-claude-desktop-developer-mode":212,"\u002Fblog\u002Fmcp-ext-apps-claude-desktop-developer-mode-surround":701},[4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208],{"title":5,"path":6,"stem":7},"You do not have time to not have tests","\u002Fblog\u002Fyou-do-not-have-time-to-not-have-tests","2.blog\u002F20211217.you-do-not-have-time-to-not-have-tests",{"title":9,"path":10,"stem":11},"Migrate Vue 2 with Vuetify and Jest to Vite and Vitest","\u002Fblog\u002Fmigrate-vue-2-with-vuetify-and-jest-to-vite-and-vitest","2.blog\u002F20220109.migrate-vue-2-with-vuetify-and-jest-to-vite-and-vitest",{"title":13,"path":14,"stem":15},"I am a Dark Matter Developer","\u002Fblog\u002Fi-am-a-dark-matter-developer","2.blog\u002F20220626.i-am-a-dark-matter-developer",{"title":17,"path":18,"stem":19},"Why using Conventional commits is useful","\u002Fblog\u002Fusing-conventional-commits","2.blog\u002F20240623.using-conventional-commits",{"title":21,"path":22,"stem":23},"Why you should make a toolbox repository","\u002Fblog\u002Fwhy-you-should-make-a-toolbox-repository","2.blog\u002F20240630.Why-you-should-make-a-toolbox-repository",{"title":25,"path":26,"stem":27},"Apache Airflow Part 1 - Why and Goals for a near Serverless ELT","\u002Fblog\u002Fapache-airflow-part-1-why-and-goals","2.blog\u002F20240710.apache-airflow-part-1-why-and-goals",{"title":29,"path":30,"stem":31},"Oh My Zsh on your server","\u002Fblog\u002Foh-my-zsh-on-your-server","2.blog\u002F20240711.oh-my-zsh-on-your-server",{"title":33,"path":34,"stem":35},"Fire tablet and YouTube Kids","\u002Fblog\u002Ffire-tablet-and-youtube-kids","2.blog\u002F20240714.fire-tablet-and-youtube-kids",{"title":37,"path":38,"stem":39},"Using Ollama and Continue as a GitHub Copilot Alternative","\u002Fblog\u002Fusing-ollama-and-continue-as-github-copilot-alternative","2.blog\u002F20240723.using-ollama-and-continue-as-github-copilot-alternative",{"title":41,"path":42,"stem":43},"Debugging Local Packages Made Easy with pnpm","\u002Fblog\u002Fdebugging-local-packages-with-pnpm-link","2.blog\u002F20250422.debugging local-packages-with-pnpm-link",{"title":45,"path":46,"stem":47},"Two Weeks with Cloudflare AI and Tools","\u002Fblog\u002Ftwo-weeks-with-cloudflare-ai-and-tools","2.blog\u002F20250509.two-weeks-with-cloudflare-aI-and-tools",{"title":49,"path":50,"stem":51},"Adding Prompts to VS Code - How I Learned to Stop Worrying and Love AI Context","\u002Fblog\u002Fadding-prompts-to-vscode","2.blog\u002F20250528.adding-prompts-to-vscode",{"title":53,"path":54,"stem":55},"My Best Practices","\u002Fblog\u002Fmy-best-practicies","2.blog\u002F20250607.my-best-practicies",{"title":57,"path":58,"stem":59},"Creating my own CLI Tool - Towles Tool","\u002Fblog\u002Ftowles-tool","2.blog\u002F20250607.towles-tool",{"title":61,"path":62,"stem":63},"Software Development Best Practices & ITIL","\u002Fblog\u002Fsoftware-engineering-and-itil-best-practices","2.blog\u002F20250612.software-engineering-and-itil-best-practices",{"title":65,"path":66,"stem":67},"Voice to Text","\u002Fblog\u002Fvoice-to-text","2.blog\u002F20250622.voice-to-text",{"title":69,"path":70,"stem":71},"Setting Up ComfyUI - A Better Alternative to Fooocus","\u002Fblog\u002Fcomfy-ui-setup","2.blog\u002F20250628.comfy-ui-setup",{"title":73,"path":74,"stem":75},"Voice to System","\u002Fblog\u002Fvoice-to-system","2.blog\u002F20250705.voice-to-system",{"title":77,"path":78,"stem":79},"Tips for Claude Code","\u002Fblog\u002Ftips-for-claude-code","2.blog\u002F20250713.tips-for-claude-code",{"title":81,"path":82,"stem":83},"Review That AI Code: Why I Read Every Line Generated Code","\u002Fblog\u002Freview-that-ai-code","2.blog\u002F20250720.review-that-ai-code",{"title":85,"path":86,"stem":87},"My Context Engineering Journey: From Dev Scripts to AI Collaboration","\u002Fblog\u002F20250803-1.my-context-engineering-journey","2.blog\u002F20250803-1.my-context-engineering-journey",{"title":89,"path":90,"stem":91},"Context Engineering at Scale: Enterprise Lessons and the Future of Development","\u002Fblog\u002F20250803-2.context-engineering-at-scale","2.blog\u002F20250803-2.context-engineering-at-scale",{"title":93,"path":94,"stem":95},"Check That Your Tools and Linters Do Not Burn Tokens","\u002Fblog\u002Fcheck-that-your-tools-and-linters-do-not-burn-tokens","2.blog\u002F20250806.check-that-your-tools-and-linters-do-not-burn-tokens",{"title":97,"path":98,"stem":99},"Markdown + AI: The Communication Protocol That Changes Everything","\u002Fblog\u002Fmarkdown-plus-ai-the-communication-protocol-that-changes-everything","2.blog\u002F20250814.markdown-plus-ai-the-communication-protocol-that-changes-everything",{"title":101,"path":102,"stem":103},"Finally: Type-Safe AI in Production (And Why I'm Here For It)","\u002Fblog\u002Ffinally-type-safe-ai-in-production-and-why-im-here-for-it","2.blog\u002F20250819.finally-type-safe-ai-in-production-and-why-im-here-for-it",{"title":105,"path":106,"stem":107},"Dotfiles: Masterpiece or Late Stage Picasso?","\u002Fblog\u002Fdotfiles-masterpiece-or-late-stage-picasso","2.blog\u002F20250822.dotfiles-masterpiece-or-late-stage-picasso",{"title":109,"path":110,"stem":111},"Beyond API Wrappers: Building State-Driven MCP Servers for Long-Horizon Agent Orchestration","\u002Fblog\u002Fbeyond-api-wrappers-mcp-servers","2.blog\u002F20250907.beyond-api-wrappers-mcp-servers",{"title":113,"path":114,"stem":115},"Why Vertical Integration Wins: A Software Engineer's Case for Owning Your Stack","\u002Fblog\u002Fwhy-i-bought-tesla-model-3-vertical-integration","2.blog\u002F20250928.why-i-bought-tesla-model-3-vertical-integration",{"title":117,"path":118,"stem":119},"The Min-Maxer's Trifecta: Building Tools for the Game You Actually Play","\u002Fblog\u002Fmin-maxer-trifecta","2.blog\u002F20251004.min-maxer-trifecta",{"title":121,"path":122,"stem":123},"Read The Source: Learning by Cutting Out The Middleman and RTFM","\u002Fblog\u002Fread-the-source","2.blog\u002F20251010.read-the-source",{"title":125,"path":126,"stem":127},"The Exponential Shift: Why AI Progress Feels Different Now","\u002Fblog\u002Fthe-exponential-shift","2.blog\u002F20251015.the-exponential-shift",{"title":129,"path":130,"stem":131},"Plan Mode for Your Problems, Edit Mode for Claude's","\u002Fblog\u002Fplan-mode-problems-edit-mode-solutions","2.blog\u002F20251019.plan-mode-problems-edit-mode-solutions",{"title":133,"path":134,"stem":135},"AWS Aurora DSQL Looked Perfect Until I Needed the Connection String","\u002Fblog\u002Faws-aurora-dsql-postgres-serverless-authentication","2.blog\u002F20251028.aws-aurora-dsql-postgres-serverless-authentication",{"title":137,"path":138,"stem":139},"Switchback: Browser History for Your Thoughts","\u002Fblog\u002Fswitchback-second-order-reasoning","2.blog\u002F20251205.switchback-second-order-reasoning",{"title":141,"path":142,"stem":143},"AI Pairing: Notes to Self","\u002Fblog\u002Fai-pairing-notes-to-self","2.blog\u002F20251216.ai-pairing-notes-to-self",{"title":145,"path":146,"stem":147},"I've Been Sleeping on Zellij","\u002Fblog\u002Fsleeping-on-zellij","2.blog\u002F20251229.sleeping-on-zellij",{"title":149,"path":150,"stem":151},"Implementing a Ralph Wiggum Loop: The Secret is Session Markers","\u002Fblog\u002Fimplementing-ralph-wiggum-loop-for-autonomous-ai-coding","2.blog\u002F20260114.implementing-ralph-wiggum-loop-for-autonomous-ai-coding",{"title":153,"path":154,"stem":155},"Goodhart's Law Ate My Context Window","\u002Fblog\u002Fgoodharts-law-ate-my-context-window","2.blog\u002F20260119.goodharts-law-ate-my-context-window",{"title":157,"path":158,"stem":159},"Claude Code's Hidden Multi-Agent System Is Real","\u002Fblog\u002Fclaude-code-hidden-multi-agent-system","2.blog\u002F20260124.claude-code-hidden-multi-agent-system",{"title":161,"path":162,"stem":163},"Free Printable Math Sheets for Kids — Number Chart, Skip Counting, Multiplication, and More","\u002Fblog\u002Ffree-printable-number-chart-and-coin-sheets","2.blog\u002F20260214.free-printable-number-chart-and-coin-sheets",{"title":165,"path":166,"stem":167},"We Are Near the End of the Exponential","\u002Fblog\u002Fnear-the-end-of-the-exponential","2.blog\u002F20260214.near-the-end-of-the-exponential",{"title":169,"path":170,"stem":171},"Free Printable Language Arts Sheets for Kids — Sight Words, Parts of Speech, Homophones, and More","\u002Fblog\u002Ffree-printable-sight-words-and-grammar-sheets","2.blog\u002F20260215.free-printable-sight-words-and-grammar-sheets",{"title":173,"path":174,"stem":175},"Interactive Code Execution with Artifacts","\u002Fblog\u002Finteractive-code-execution-with-artifacts","2.blog\u002F20260215.interactive-code-execution-with-artifacts",{"title":177,"path":178,"stem":179},"Free Printable Telling Time Worksheet for Kids — Clock Reference & Practice Sheet","\u002Fblog\u002Ffree-printable-telling-time-worksheet","2.blog\u002F20260216.free-printable-telling-time-worksheet",{"title":181,"path":182,"stem":183},"Claude Code Skills: Teaching AI Your Playbook","\u002Fblog\u002Fclaude-code-skills-guide","2.blog\u002F20260221.claude-code-skills-guide",{"title":185,"path":186,"stem":187},"Building a Multi-Agent Loan Approval System with Human-in-the-Loop","\u002Fblog\u002Fmulti-agent-loan-approval-human-in-the-loop","2.blog\u002F20260225.multi-agent-loan-approval-human-in-the-loop",{"title":189,"path":190,"stem":191},"The Inception of AI Infrastructure: Bottlenecks All the Way Down","\u002Fblog\u002Fbiggest-bottleneck-scaling-ai-compute","2.blog\u002F20260313.biggest-bottleneck-scaling-ai-compute",{"title":193,"path":194,"stem":195},"What I Tell Teams About Claude Code","\u002Fblog\u002Fwhat-i-tell-teams-about-claude-code","2.blog\u002F20260314.what-i-tell-teams-about-claude-code",{"title":197,"path":198,"stem":199},"The Hardest Part of AI Isn't the AI","\u002Fblog\u002Fthe-hardest-part-of-ai-isnt-the-ai","2.blog\u002F20260327.the-hardest-part-of-ai-isnt-the-ai",{"title":201,"path":202,"stem":203},"Claude Code Hooks: The Capability I Left on the Table","\u002Fblog\u002Fclaude-code-hooks-capability-left-on-the-table","2.blog\u002F20260401.claude-code-hooks-capability-left-on-the-table",{"title":205,"path":206,"stem":207},"Claude in a Box: Trying Computer-Use on My Laptop","\u002Fblog\u002Fclaude-computer-use-in-a-box","2.blog\u002F20260418.claude-computer-use-in-a-box",{"title":209,"path":210,"stem":211},"MCP Apps: We Spent $100B on AI to Bring Back the Iframe","\u002Fblog\u002Fmcp-ext-apps-claude-desktop-developer-mode","2.blog\u002F20260505.mcp-ext-apps-claude-desktop-developer-mode",{"id":213,"title":209,"authors":214,"badge":220,"body":222,"date":690,"description":691,"extension":692,"image":693,"meta":696,"navigation":697,"path":210,"seo":698,"status":699,"stem":211,"__hash__":700},"posts\u002F2.blog\u002F20260505.mcp-ext-apps-claude-desktop-developer-mode.md",[215],{"name":216,"to":217,"avatar":218},"Chris Towles","https:\u002F\u002Ftwitter.com\u002FChris_Towles",{"src":219},"\u002Fimages\u002Fctowles-profile-512x512.png",{"label":221},"AI Tools",{"type":223,"value":224,"toc":675},"minimark",[225,229,232,235,238,243,246,249,252,255,263,277,284,297,300,303,321,324,327,331,338,341,348,351,357,376,379,385,392,400,406,409,413,416,434,447,461,479,505,518,535,539,546,553,562,573,578,581,603,606,613,616,625,629,646,653,657,660,663,666,669,672],[226,227,228],"p",{},"In 2026, Karpathy is teaching the world how transformers work on YouTube. Boris is shipping Claude Code that ships software end-to-end. The model on my laptop can write a working compiler in the time it takes me to read this paragraph. We have collectively poured something like a hundred billion dollars into training runs, GPU farms, and the most powerful coding tools ever assembled.",[226,230,231],{},"The frontier of AI just shipped its frontend story.",[226,233,234],{},"It's an iframe.",[226,236,237],{},"I'm not making this up.",[239,240,242],"h2",{"id":241},"the-apps-die-the-frameworks-survive","The apps die. The frameworks survive.",[226,244,245],{},"Every developer I know has a graveyard. Not of bad apps — useful ones, that solved real problems. The internal tool built in Cordova that nobody could rebuild once the toolchain rotted. The Xamarin app shelved when Microsoft renamed Xamarin for the third time. The Angular 1 dashboard the team started porting to Angular 2 and never finished. The Flutter side project that needed one dependency upgrade nobody ever had time for.",[226,247,248],{},"The components inside those apps were always the same. Auth. Storage. A list view, a detail view, a form with five fields and a submit button. We re-implemented them in each stack's idioms — provider here, store there, hook next, signal after — and every re-implementation fused the UI to the stack so tightly that porting later wasn't porting. It was writing the whole app again.",[226,250,251],{},"So we didn't port. We abandoned. The framework kept shipping. The team picked the next one. That's been the deal for twenty years.",[226,253,254],{},"Then the chat happened.",[239,256,258,262],{"id":257},"attention-iframe-is-all-you-need",[259,260,261],"del",{},"Attention"," Iframe Is All You Need",[226,264,265],{},[266,267,268,269,276],"em",{},"With apologies to ",[270,271,275],"a",{"href":272,"rel":273},"https:\u002F\u002Farxiv.org\u002Fabs\u002F1706.03762",[274],"nofollow","Vaswani et al",".",[226,278,279,280,283],{},"What I missed about MCP is that it has two audiences. The model needs structured context to reason — that's the half everyone has been building. The other half is the human staring at the chat, who needs to actually ",[266,281,282],{},"understand"," what just happened.",[226,285,286,287,292,293,296],{},"There's a ",[270,288,291],{"href":289,"rel":290},"https:\u002F\u002Fx.com\u002Fkarpathy\u002Fstatus\u002F2049907410303865030",[274],"quote Karpathy keeps citing"," that fits here: ",[266,294,295],{},"you can outsource your thinking but you cannot outsource your understanding."," A paragraph from the model carries fine into the model's next turn — that's the model's reasoning, and the model is the audience for that. It is not enough for the person who has to look at the answer and decide what to do with it. The chat is where understanding has to happen, and a wall of text is not where understanding happens.",[226,298,299],{},"That's what the iframe is for. The structured payload still goes back to the model through the AppBridge so the agent can keep talking. The UI bundle is for the human — the chart they read, the SQL they verify, the chips they click instead of retyping the question. MCP became a UI layer because the chat is where humans do the thinking they can't hand off.",[226,301,302],{},"The chat host is doing in 2026 what the browser did in 1996. It already has the user. It already has identity. It already has the model. What it needed was a sandboxed render frame, a clean message-passing protocol, and a security boundary you could actually reason about. Once it had those, every \"platform\" you used to ship to became a single render target.",[226,304,305,306,311,312,316,317,320],{},"So the ",[270,307,310],{"href":308,"rel":309},"https:\u002F\u002Fgithub.com\u002Fmodelcontextprotocol\u002Fmodelcontextprotocol\u002Fblob\u002Fmain\u002Fspecification\u002F2026-01-26\u002Fapps.mdx",[274],"SEP-1865"," spec is exactly that. Your tool tags itself with a ",[313,314,315],"code",{},"ui:\u002F\u002F"," resource URI. The host pulls down the bundle, drops it into a sandboxed iframe, and routes ",[313,318,319],{},"postMessage"," traffic between the iframe and the model. Your code is HTML. The host is the runtime. Auth, storage, model access — delegated through the spec.",[226,322,323],{},"One endpoint. Every chat client renders it the same way.",[226,325,326],{},"I built it to make sure I wasn't hallucinating.",[239,328,330],{"id":329},"the-screenshots-in-order","The screenshots, in order",[226,332,333,334,337],{},"An MCP server over the FAA Aircraft Registry, live at ",[313,335,336],{},"chris.towles.dev\u002Fmcp\u002Faviation",". Same endpoint, two host surfaces, same render.",[226,339,340],{},"Adding it to Claude Desktop is no different from any other MCP server — Settings → Connectors, custom URL:",[226,342,343],{},[344,345],"img",{"alt":346,"src":347},"Claude Desktop Connectors settings showing chris.towles.dev - aviation listed as a CUSTOM connector pointing at https:\u002F\u002Fchris.towles.dev\u002Fmcp\u002Faviation","\u002Fimages\u002Fblog\u002F20260505-mcp-ext-apps-connectors.png",[226,349,350],{},"Open the connector and the host shows me something I didn't write a line of code for:",[226,352,353],{},[344,354],{"alt":355,"src":356},"chris.towles.dev - aviation connector page showing Interactive tools (ask_aviation, 1) separated from Other tools (list_questions, schema, 2), each toggled to Always allow","\u002Fimages\u002Fblog\u002F20260505-mcp-ext-apps-permissions.png",[226,358,359,362,363,367,368,371,372,375],{},[313,360,361],{},"ask_aviation"," is filed under ",[364,365,366],"strong",{},"Interactive tools",". The other two are under ",[364,369,370],{},"Other tools",". The host is reading the ",[313,373,374],{},"_meta.ui.resourceUri"," off my registration and labeling tools that ship a UI bundle differently from tools that return JSON. The user gets a different consent prompt for each. I didn't ask for that. The SDK tags it, the host honors it, and the plumbing reaches all the way down.",[226,377,378],{},"Then the answer:",[226,380,381],{},[344,382],{"alt":383,"src":384},"Claude Desktop rendering the aviation MCP tool result as an interactive card with a Boeing fleet chart","\u002Fimages\u002Fblog\u002F20260505-mcp-ext-apps-chat-result.png",[226,386,387,388,391],{},"That card is an iframe my server shipped. The chart paints from a structured option object the model emitted. The \"show SQL\" toggle, the loading state, the follow-up chips — all in my bundle. The chips aren't decoration: clicking one fires ",[313,389,390],{},"ui\u002Fmessage"," (a spec method), the host adds it to the conversation as the next user turn, and the round trip happens again without the user touching the keyboard.",[226,393,394,395,399],{},"The same bundle renders in ",[270,396,398],{"href":397},"\u002Fchat","my blog's own chat",". Different MCP client, same iframe, same behavior:",[226,401,402],{},[344,403],{"alt":404,"src":405},"The same MCP iframe rendering inside Chris Towles's blog chat -- a Top 20 operators by model diversity bar chart over the FAA Aircraft Registry, with follow-up chips, served by the same \u002Fmcp\u002Faviation server","\u002Fimages\u002Fblog\u002F20260505-mcp-ext-apps-blog-chat.png",[226,407,408],{},"That's the bidirectional UI contract working — one server, two MCP clients, identical render. Claude Desktop didn't get a privileged path. The blog's chat acts as its own MCP client and gets the same iframe out the back end.",[239,410,412],{"id":411},"the-parts-i-had-to-actually-figure-out","The parts I had to actually figure out",[226,414,415],{},"A handful of things from building this that are worth knowing before you try.",[226,417,418,421,422,425,426,429,430,433],{},[364,419,420],{},"The sandbox has to live on a different origin."," SEP-1865 is explicit about this — the host iframe and your bundle MUST come from separate origins, or the security boundary doesn't exist. I tried serving the proxy at ",[313,423,424],{},"\u002Fsandbox\u002F..."," on the blog's own origin first, because it was the cheap path. It's also the broken path. I ended up spinning up ",[313,427,428],{},"sandbox.towles.dev"," as a separate Cloudflare Pages site whose only job is ",[313,431,432],{},"document.write"," of the inner iframe with the right CSP headers. Without that subdomain, neither host renders the bundle.",[226,435,436,439,440,442,443,446],{},[364,437,438],{},"MCP tool calls block the model."," This is the single biggest constraint shaping the rest of the design. If ",[313,441,361],{}," takes seven seconds — DuckDB cold start, LLM emitting SQL plus the ECharts option object, query execution — the chat sits frozen for seven seconds. So the tool returns instantly with ",[313,444,445],{},"{ pending: true, queryUrl }",". The iframe, once mounted, POSTs that URL on its own and reads progress + the final structured payload over SSE. The model gets the result back through the AppBridge once the iframe has it. From the chat's point of view the tool finished in milliseconds and the surface filled in afterward.",[226,448,449,452,453,456,457,460],{},[364,450,451],{},"DuckDB on GCS authenticates via HMAC, not the service account."," DuckDB's ",[313,454,455],{},"gs:\u002F\u002F"," path goes through the S3-compat API, which only accepts HMAC keys. ADC doesn't work — there's no Google-OAuth path through that codebase. Terraform mints an HMAC key for the Cloud Run service account, stores it in Secret Manager, and injects it as an env var. Until I read the DuckDB httpfs source, this looked like a meaningless ",[313,458,459],{},"SignatureDoesNotMatch"," and a wasted afternoon.",[226,462,463,466,467,470,471,474,475,478],{},[364,464,465],{},"Cold-start latency is real."," DuckDB httpfs takes two to five seconds to spin up a fresh GCS connection on a cold container. Stack that on top of an actual query and you've blown past anyone's patience for a chat answer. The fix is a Nitro startup hook that runs ",[313,468,469],{},"INSTALL httpfs"," and a tiny ",[313,472,473],{},"read_parquet"," against a one-row file as soon as the container boots. By the time a request arrives, httpfs is warm. Cloud Run's ",[313,476,477],{},"min_instances=1"," keeps at least one container alive, so most users never see the cold path at all.",[226,480,481,484,485,488,489,492,493,496,497,500,501,504],{},[364,482,483],{},"SQL safety is layered, not clever."," The model writes the SQL. So the connection is read-only, extension auto-load is disabled, the local filesystem is blocked, an AST allowlist parses every query and rejects anything that isn't a single ",[313,486,487],{},"SELECT","\u002F",[313,490,491],{},"WITH"," against allowlisted relations, every query gets a ",[313,494,495],{},"LIMIT 10000"," injected if it doesn't have one, and there's an ",[313,498,499],{},"AbortController"," + DuckDB ",[313,502,503],{},"interrupt()"," timeout. None of those layers is trusted on its own. You can break one. You can't break all of them in a way that hits production data, because there is no production data — the whole bucket is read-only public-domain Parquet.",[226,506,507,510,511,513,514,517],{},[364,508,509],{},"Replay doesn't refetch."," When a chat reloads, the tool result isn't reissued from the server. The iframe re-renders from the static ",[313,512,315],{}," bundle (cached by the browser) and the ",[313,515,516],{},"structuredContent"," is replayed from what got saved alongside the message in Postgres. No tool call fires on chat reopen. The bundle is ~400KB gzipped, immutable per deploy; the browser pulls it once per session.",[226,519,520,521,524,525,528,529,528,532,534],{},"The same ",[313,522,523],{},"\u002Fmcp\u002Faviation"," endpoint serves Claude Desktop and the blog's own chat. Different MCP clients, same ",[313,526,527],{},"tools\u002Fcall",", same ",[313,530,531],{},"CallToolResult",[313,533,315],{}," resource. That's the part I most wanted to validate. It works.",[239,536,538],{"id":537},"i-built-it-on-a-lakehouse-not-a-database","I built it on a lakehouse, not a database",[226,540,541,542,545],{},"Most MCP demos read from a transactional database. That's the right shape for a notes app or a calendar tool. It is not how anyone in an enterprise actually answers analytical questions. Aviation is an analytics workload — \"which operators have the oldest 737 fleets\" is a ",[313,543,544],{},"GROUP BY"," over millions of rows, joined across two datasets, with predicate pushdown so you don't scan everything every time. Postgres can do it. It just isn't the right shape.",[226,547,548,549,552],{},"So the demo is built on the same pattern as a real enterprise data stack, scaled down. Parquet files in GCS instead of an Iceberg catalog. DuckDB embedded in the MCP server process instead of Snowflake or BigQuery. ",[313,550,551],{},"httpfs"," predicate pushdown instead of a query planner shipping work across a cluster. The shape is identical to a production lakehouse. Only the size is different.",[226,554,555,556,561],{},"OpenAI ",[270,557,560],{"href":558,"rel":559},"https:\u002F\u002Fopenai.com\u002Findex\u002Finside-our-in-house-data-agent\u002F",[274],"wrote about their in-house data agent"," in January — a custom internal tool built around their own data, permissions, and workflows, designed to take employees from question to insight in minutes instead of days. Same architectural shape as my aviation demo, much bigger numbers. The interesting analytical workload looks identical in both places: take a natural-language question, generate SQL, execute against the warehouse, render the answer. The MCP layer doesn't care whether the engine is DuckDB or Trino. The iframe doesn't care. The host doesn't care.",[226,563,564,565,567,568,567,570,572],{},"What changes when you scale up is exactly what you'd expect — query engine is bigger, data is partitioned by domain, auth wires into enterprise SSO, audit lands in your existing observability. The contract (",[313,566,527],{},", ",[313,569,531],{},[313,571,315],{}," resource) stays.",[574,575,577],"h3",{"id":576},"what-the-demo-is-actually-querying","What the demo is actually querying",[226,579,580],{},"Three US-public-domain aviation datasets, joined and stored as Parquet:",[582,583,584,591,597],"ul",{},[585,586,587,590],"li",{},[364,588,589],{},"FAA Aircraft Registry"," — ~300k active US N-numbers with manufacturer, model, year manufactured, operator, home-base airport. The fleet-composition side of the question.",[585,592,593,596],{},[364,594,595],{},"BTS T-100 Market"," — twelve months of US flight segment data, ~15M rows after Parquet compression. Carrier, origin, destination, distance, delays. The \"what did each fleet actually do\" side.",[585,598,599,602],{},[364,600,601],{},"OpenFlights"," — global airport, airline, and route reference data. The dimensional layer that makes airport codes and carrier IDs human-readable.",[226,604,605],{},"A small curated lookup table bridges BTS carrier codes to FAA operator names, because the two systems don't agree on identifiers and the interesting queries depend on the join working. Known-unmatched carriers stay as nulls; the schema doc enumerates them.",[226,607,608,609,612],{},"The whole bucket is public-domain or ODbL with attribution preserved in a co-hosted ",[313,610,611],{},"LICENSE.txt",". None of it is sensitive. None of it requires negotiating a redistribution agreement. It's the right shape of data for a public demo — real enough to mean something to anyone who works in aerospace, free enough that I can leave it running indefinitely behind the project's $10 GCP spend cap.",[226,614,615],{},"If you're at a company with a lakehouse and you've been wondering what to actually build with MCP, this is the pattern. Don't wrap your CRUD APIs. Wrap the warehouse your analysts already use. Ship a chart-shaped answer back into the chat where the question got asked.",[226,617,618,619,624],{},"All of the above is still a ",[270,620,623],{"href":621,"rel":622},"https:\u002F\u002Fgithub.com\u002Fmodelcontextprotocol\u002Fmodelcontextprotocol\u002Fdiscussions\u002F2639",[274],"moving target"," — the spec keeps shipping, and most of these workarounds get smaller every release.",[239,626,628],{"id":627},"try-it","Try it",[582,630,631,636],{},[585,632,633,635],{},[270,634,397],{"href":397}," on this site — pick an aviation starter question or ask \"which operators have the oldest 737 fleets?\" The iframe is the same bundle Claude Desktop loads.",[585,637,638,641,642,645],{},[270,639,640],{"href":640},"\u002Faviation"," — copy-pasteable Claude Desktop config in two flavors (native HTTP and ",[313,643,644],{},"mcp-remote"," wrapped). About 90 seconds end-to-end.",[226,647,648,649,652],{},"Endpoint: ",[313,650,651],{},"https:\u002F\u002Fchris.towles.dev\u002Fmcp\u002Faviation",". Public. Throttled at the edge. No auth.",[239,654,656],{"id":655},"the-boring-answer-was-the-right-answer","The boring answer was the right answer",[226,658,659],{},"The funny thing about building this is how anticlimactic the payoff is. We are living through the most exotic compute moment in human history. The sequence-to-sequence trick that started this thing cost more than every cross-platform framework I've named in this post, combined. Phones run 70B models. Claude can refactor a Rails monolith between meetings.",[226,661,662],{},"And the answer to \"how do I ship one UI to every chat\" turned out to be: drop an HTML file at an MCP endpoint, let the host iframe it, postMessage when you need to talk back. That's it. There isn't a layer below.",[226,664,665],{},"I keep waiting for it to be more complicated than that. It isn't.",[226,667,668],{},"I gave up on Xamarin. I gave up on Flutter. I gave up on getting the web to feel right on phones. Sun spent billions on Java applets. Adobe spent a decade on Flash. Facebook bet React Native. Google bet Flutter. Microsoft bet Xamarin twice. None of them shipped the universal client. The chat did, by accident, while we were busy training the model that lives inside it.",[226,670,671],{},"For new work I'm writing one HTML bundle and pointing every host I care about at it.",[226,673,674],{},"HTML and JavaScript are going to outlive my kids.",{"title":676,"searchDepth":677,"depth":677,"links":678},"",2,[679,680,682,683,684,688,689],{"id":241,"depth":677,"text":242},{"id":257,"depth":677,"text":681},"Attention Iframe Is All You Need",{"id":329,"depth":677,"text":330},{"id":411,"depth":677,"text":412},{"id":537,"depth":677,"text":538,"children":685},[686],{"id":576,"depth":687,"text":577},3,{"id":627,"depth":677,"text":628},{"id":655,"depth":677,"text":656},"2026-05-05","Karpathy is teaching the world how transformers work. Boris is shipping Claude Code. The frontier of AI just gave us its frontend story, and it's an iframe inside a chat. I built an MCP server to make sure I wasn't hallucinating.","md",{"src":694,"alt":695},"\u002Fimages\u002Fblog\u002F20260505-mcp-ext-apps-claude-desktop-developer-mode.png","Over-the-shoulder view of a developer at a modern home-office desk in the evening, focused on a large monitor showing a chat interface with a horizontal bar chart and follow-up question chips rendered inline in the conversation, warm desk-lamp lighting balanced against the cool blue glow of the screen, coffee cup and mechanical keyboard on the desk",{},true,{"title":209,"description":691},"published","jAda-gt3xnzYaCBDThj9l8jSHGrT7C_wos6y3pYrFDM",[702,704],{"title":205,"path":206,"stem":207,"description":703,"status":699,"children":-1},"Most of the apps I automate have a CLI or an API. Claude's computer-use is for the ones that don't. I spun up Anthropic's Docker demo to see what it can do.",null,1778041110333]