<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ali Ibrahim</title>
    <description>The latest articles on DEV Community by Ali Ibrahim (@ialijr).</description>
    <link>https://dev.to/ialijr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F301265%2F7f39b933-cc2c-4a37-9f20-87d4dc902b7c.png</url>
      <title>DEV Community: Ali Ibrahim</title>
      <link>https://dev.to/ialijr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ialijr"/>
    <language>en</language>
    <item>
      <title>5 Agent Skills I’d install before starting any new agent project in 2026</title>
      <dc:creator>Ali Ibrahim</dc:creator>
      <pubDate>Mon, 16 Mar 2026 14:44:21 +0000</pubDate>
      <link>https://dev.to/ialijr/5-agent-skills-id-install-before-starting-any-new-agent-project-in-2026-3mg1</link>
      <guid>https://dev.to/ialijr/5-agent-skills-id-install-before-starting-any-new-agent-project-in-2026-3mg1</guid>
      <description>&lt;p&gt;Your coding agent can write code, refactor functions, and debug errors. But can it design production-grade prompts? Build MCP servers that follow best practices? Evaluate whether your agent's outputs are actually good?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://agentskills.io" rel="noopener noreferrer"&gt;Agent Skills&lt;/a&gt; give your coding assistant specialized expertise on demand. They're folders containing a &lt;code&gt;SKILL.md&lt;/code&gt; file with instructions, workflows, and references that your agent loads only when relevant. No context bloat, no manual setup. For a deep dive into how skills work and how to build your own, see &lt;a href="https://blog.agentailor.com/blog/how-to-build-and-deploy-agent-skill-from-scratch" rel="noopener noreferrer"&gt;How to Build and Deploy an Agent Skill from Scratch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here are 5 skills that cover the full agent development lifecycle, from designing prompts to evaluating outputs. Every skill listed works across Claude Code, Cursor, VS Code Copilot, Codex, and Gemini CLI.&lt;/p&gt;

&lt;p&gt;To install any skill, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add &amp;lt;owner/repo&amp;gt; &lt;span class="nt"&gt;--skill&lt;/span&gt; &amp;lt;skill-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  1. prompt-engineer
&lt;/h2&gt;

&lt;p&gt;An expert prompt engineering skill that teaches your agent advanced techniques for designing effective LLM prompts. It covers system prompt architecture, few-shot example design, chain-of-thought patterns, output format specification, and context management. The skill identifies common pitfalls like imprecise language, missing format constraints, and prompt injection vulnerabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Prompt design is the highest-leverage activity in agent development. A well-crafted prompt can be the difference between a prototype and a production-ready agent. This skill turns your coding assistant into a prompt engineering partner that catches issues before they reach users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Any developer writing or refining prompts for LLM-powered applications and agents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add davila7/claude-code-templates &lt;span class="nt"&gt;--skill&lt;/span&gt; prompt-engineer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/davila7/claude-code-templates" rel="noopener noreferrer"&gt;davila7/claude-code-templates&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For Anthropic's approach to agent-specific prompting, see &lt;a href="https://blog.agentailor.com/blog/the-art-of-agent-prompting" rel="noopener noreferrer"&gt;The Art of Agent Prompting&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. skill-creator
&lt;/h2&gt;

&lt;p&gt;Anthropic's official skill for creating, modifying, and evaluating skills. Instead of starting from a blank &lt;code&gt;SKILL.md&lt;/code&gt;, this skill guides your agent through an iterative development cycle: define intent, draft the skill file, test with sample prompts, evaluate outputs, and refine. It adapts to different environments (Claude.ai, Claude Code, Cursor) and supports users across technical expertise levels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Building skills manually is educational but slow. This skill automates the creation process and includes built-in evaluation with variance analysis, helping you ship higher-quality skills faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers who want to create custom skills for their team, product, or domain without starting from scratch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add anthropics/skills &lt;span class="nt"&gt;--skill&lt;/span&gt; skill-creator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/anthropics/skills" rel="noopener noreferrer"&gt;anthropics/skills&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For the manual approach that teaches you every component, see &lt;a href="https://blog.agentailor.com/blog/how-to-build-and-deploy-agent-skill-from-scratch" rel="noopener noreferrer"&gt;How to Build and Deploy an Agent Skill from Scratch&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. mcp-builder
&lt;/h2&gt;

&lt;p&gt;Anthropic's official guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services. The skill covers the full development cycle in four phases: research and planning, implementation, review and testing, and evaluation creation. It supports both Python (FastMCP) and TypeScript implementations, and emphasizes concise tool descriptions and actionable error messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Skills give agents knowledge; MCP servers give agents capabilities. If you need your agent to interact with APIs, databases, or external services, this skill teaches it how to build MCP servers that follow best practices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers building custom MCP servers to extend their agents' capabilities.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add anthropics/skills &lt;span class="nt"&gt;--skill&lt;/span&gt; mcp-builder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/anthropics/skills" rel="noopener noreferrer"&gt;anthropics/skills&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For a quick hands-on start with MCP, see &lt;a href="https://blog.agentailor.com/blog/create-your-first-mcp-server-in-5-minutes" rel="noopener noreferrer"&gt;Create Your First MCP Server in 5 Minutes&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. agentic-eval
&lt;/h2&gt;

&lt;p&gt;Patterns and techniques for evaluating and improving AI agent outputs through iterative refinement. This skill teaches your agent to implement self-critique loops, evaluator-optimizer pipelines, rubric-based assessment, and LLM-as-judge evaluation systems. Rather than relying on single-shot generation, it introduces systematic approaches to measuring and improving output quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Building an agent is half the challenge. Knowing whether it works reliably is the other half. This skill teaches your coding assistant how to evaluate agent outputs systematically, a practice that separates prototype-quality agents from production-ready ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers testing, debugging, or improving agent output quality, especially when building evaluation pipelines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add github/awesome-copilot &lt;span class="nt"&gt;--skill&lt;/span&gt; agentic-eval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/github/awesome-copilot" rel="noopener noreferrer"&gt;github/awesome-copilot&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. openai-docs
&lt;/h2&gt;

&lt;p&gt;Provides up-to-date OpenAI developer documentation with citations, covering the Responses API, Agents SDK, Chat Completions, Codex, Realtime API, model capabilities, and more. The skill uses OpenAI's MCP server to search, fetch, and browse official documentation pages, prioritizing MCP tools over general web search for accuracy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; LLM training data goes stale. If you are building with OpenAI APIs, this skill ensures your agent references the latest documentation rather than outdated knowledge. Every answer comes with a citation to the official source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers building with OpenAI APIs who need accurate, current references without leaving their IDE.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add openai/skills &lt;span class="nt"&gt;--skill&lt;/span&gt; openai-docs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/openai/skills" rel="noopener noreferrer"&gt;openai/skills&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus: ai-sdk
&lt;/h2&gt;

&lt;p&gt;The top 5 skills above are broadly useful to any agent builder regardless of stack. This bonus is more specialized, but for its audience, it may be the most valuable skill on this list.&lt;/p&gt;

&lt;p&gt;The ai-sdk skill answers questions about the Vercel AI SDK and helps build AI-powered features. It covers core functions like &lt;code&gt;generateText&lt;/code&gt;, &lt;code&gt;streamText&lt;/code&gt;, &lt;code&gt;ToolLoopAgent&lt;/code&gt;, &lt;code&gt;embed&lt;/code&gt;, and tool calling. The skill checks local &lt;code&gt;node_modules/ai/docs/&lt;/code&gt; first, then falls back to ai-sdk.dev for the latest information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; The AI SDK is the &lt;a href="https://blog.agentailor.com/blog/top-typescript-ai-agent-frameworks-2026" rel="noopener noreferrer"&gt;most downloaded TypeScript AI framework&lt;/a&gt; with 2.8M weekly npm downloads. If you're building AI features in a Next.js or React application, this skill makes your coding assistant an AI SDK expert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; React and Next.js developers integrating AI features using the Vercel AI SDK.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add vercel/ai &lt;span class="nt"&gt;--skill&lt;/span&gt; ai-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/vercel/ai" rel="noopener noreferrer"&gt;vercel/ai&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Created By&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;prompt-engineer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Writing effective LLM prompts&lt;/td&gt;
&lt;td&gt;Community (davila7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;skill-creator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Creating and iterating on skills&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;mcp-builder&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Building MCP servers&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;agentic-eval&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Evaluating agent outputs&lt;/td&gt;
&lt;td&gt;GitHub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;openai-docs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OpenAI API documentation&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;ai-sdk&lt;/strong&gt; (Bonus)&lt;/td&gt;
&lt;td&gt;Vercel AI SDK development&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;These 5 skills cover the full agent development lifecycle:&lt;/strong&gt; design prompts, package expertise, build tools, evaluate quality, and reference documentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All skills are cross-platform.&lt;/strong&gt; They work across Claude Code, Cursor, VS Code Copilot, Codex, and Gemini CLI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combine skills for compound effect.&lt;/strong&gt; Use prompt-engineer to design your agent's prompts, skill-creator to package your expertise, and agentic-eval to verify quality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vet skills before installing.&lt;/strong&gt; Prefer skills published by known organizations like Anthropic, GitHub, or OpenAI. For community skills, check their detail page on &lt;a href="https://skills.sh" rel="noopener noreferrer"&gt;skills.sh&lt;/a&gt;: every listed skill displays a Security Audit report so you know what you're installing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The skills ecosystem is growing fast.&lt;/strong&gt; Browse &lt;a href="https://skills.sh" rel="noopener noreferrer"&gt;skills.sh&lt;/a&gt; regularly to discover new skills as they are published.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What to Read Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/how-to-build-and-deploy-agent-skill-from-scratch?utm_source=blog&amp;amp;utm_medium=read_next&amp;amp;utm_campaign=top_agent_skills_2026" rel="noopener noreferrer"&gt;How to Build and Deploy an Agent Skill from Scratch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/the-art-of-agent-prompting?utm_source=blog&amp;amp;utm_medium=read_next&amp;amp;utm_campaign=top_agent_skills_2026" rel="noopener noreferrer"&gt;The Art of Agent Prompting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/create-your-first-mcp-server-in-5-minutes?utm_source=blog&amp;amp;utm_medium=read_next&amp;amp;utm_campaign=top_agent_skills_2026" rel="noopener noreferrer"&gt;Create Your First MCP Server in 5 Minutes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Enjoying content like this? Sign up for &lt;a href="https://www.linkedin.com/newsletters/agent-briefings-7391777936955310080/" rel="noopener noreferrer"&gt;Agent Briefings&lt;/a&gt;, where I share insights and news on building and scaling AI agents.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://agentskills.io" rel="noopener noreferrer"&gt;Agent Skills Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anthropics/skills" rel="noopener noreferrer"&gt;Anthropic Skills Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://skills.sh" rel="noopener noreferrer"&gt;Skills.sh Registry&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Securing MCP Servers: A Practical Guide with Keycloak (using create-mcp-server)</title>
      <dc:creator>Ali Ibrahim</dc:creator>
      <pubDate>Tue, 03 Mar 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/ialijr/securing-mcp-servers-a-practical-guide-with-keycloak-using-create-mcp-server-518n</link>
      <guid>https://dev.to/ialijr/securing-mcp-servers-a-practical-guide-with-keycloak-using-create-mcp-server-518n</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;MCP servers are powerful. They let AI agents interact with databases, APIs, file systems, and virtually anything you can imagine. But there's a catch: most tutorials show you how to build MCP servers without authentication.&lt;/p&gt;

&lt;p&gt;That's fine for local development. It's a problem for production.&lt;/p&gt;

&lt;p&gt;An unsecured MCP server is an open door. Anyone who discovers your endpoint can invoke your tools, access your resources, and potentially wreak havoc on your systems. As MCP adoption grows and servers move from localhost to cloud deployments, security isn't optional anymore.&lt;/p&gt;

&lt;p&gt;The good news? The &lt;a href="https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization" rel="noopener noreferrer"&gt;MCP Authorization specification&lt;/a&gt; provides a standard way to secure MCP servers using OAuth 2.1. And with the right tools, implementing it is straightforward.&lt;/p&gt;

&lt;p&gt;In this guide, you'll learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How MCP authorization works (without the jargon)&lt;/li&gt;
&lt;li&gt;How to scaffold a secure MCP server with &lt;code&gt;create-mcp-server&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;How to set up Keycloak as your OIDC provider&lt;/li&gt;
&lt;li&gt;How to test your authenticated server with VS Code, Cursor and a terminal client&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's lock down your MCP server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding MCP Authorization
&lt;/h2&gt;

&lt;p&gt;If you're new to MCP, here's the short version: the &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; is an open standard that lets AI assistants discover and use external tools. Think of it as a universal adapter between AI models and the real world. For a deeper dive, check out our article on &lt;a href="https://blog.agentailor.com/docker-mcp-catalog-and-toolkit?utm_source=devto&amp;amp;utm_medium=blog_post&amp;amp;utm_campaign=mcp_oauth" rel="noopener noreferrer"&gt;running MCP servers with Docker&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Security Matters
&lt;/h3&gt;

&lt;p&gt;When you deploy an MCP server, you're exposing capabilities to the network. Those capabilities might include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reading and writing to databases&lt;/li&gt;
&lt;li&gt;Sending emails or notifications&lt;/li&gt;
&lt;li&gt;Accessing internal APIs&lt;/li&gt;
&lt;li&gt;Managing cloud resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without authentication, anyone can call these tools. That's why the MCP specification includes authorization as a core feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  OAuth: The Hotel Key Card
&lt;/h3&gt;

&lt;p&gt;OAuth might sound intimidating, but the concept is simple. Think of it like a hotel key card system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You check in at the front desk (authentication)&lt;/li&gt;
&lt;li&gt;You receive a key card (access token)&lt;/li&gt;
&lt;li&gt;You use the key card to access your room (authorized requests)&lt;/li&gt;
&lt;li&gt;The door checks if your card is valid (token validation)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's OAuth in a nutshell. The MCP client gets a token from an authorization server, then includes that token with every request. The MCP server validates the token before granting access.&lt;/p&gt;

&lt;p&gt;The MCP specification requires OAuth 2.1 with PKCE (Proof Key for Code Exchange), which adds an extra security layer to prevent token interception. You don't need to understand the cryptographic details, just know that it's a modern, secure approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic Client Registration (DCR)
&lt;/h3&gt;

&lt;p&gt;Here's something important that often gets overlooked: &lt;strong&gt;Dynamic Client Registration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In traditional OAuth setups, you manually register each client application with your authorization server. You create a client, get a client ID and secret, and configure them in your app. This works fine when you have a handful of known clients.&lt;/p&gt;

&lt;p&gt;But MCP is different. The whole point is that &lt;em&gt;any&lt;/em&gt; MCP-compatible client should be able to connect to your server. Claude Desktop, VS Code, Cursor, custom agents, there could be dozens of different clients trying to connect.&lt;/p&gt;

&lt;p&gt;DCR solves this. It allows clients to register themselves automatically with the authorization server. No manual setup required. The client says "hey, I'd like to connect," and the server says "here are your credentials."&lt;/p&gt;

&lt;p&gt;This is critical for the MCP ecosystem to scale. And it's one of the main reasons we're using Keycloak: &lt;strong&gt;Keycloak fully supports Dynamic Client Registration&lt;/strong&gt;. Not all OIDC providers do. If you're evaluating alternatives like Auth0, Azure AD, or Okta, check their DCR support carefully.&lt;/p&gt;

&lt;p&gt;For the complete technical specification, see the &lt;a href="https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization" rel="noopener noreferrer"&gt;MCP Authorization documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing create-mcp-server
&lt;/h2&gt;

&lt;p&gt;Building an MCP server from scratch with OAuth is tedious. You need to set up Express, configure middleware, handle token validation, manage sessions, and wire up SSE for real-time updates. That's hours of boilerplate before you write a single tool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/agentailor/create-mcp-server" rel="noopener noreferrer"&gt;&lt;code&gt;create-mcp-server&lt;/code&gt;&lt;/a&gt; eliminates that friction. It's a CLI tool that scaffolds production-ready MCP servers in seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @agentailor/create-mcp-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI walks you through a few questions and generates a complete project with TypeScript, Express.js, and optionally OAuth authentication baked in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two Templates
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Stateless&lt;/th&gt;
&lt;th&gt;Stateful&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Session management&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSE support&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth option&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoints&lt;/td&gt;
&lt;td&gt;POST /mcp&lt;/td&gt;
&lt;td&gt;POST, GET, DELETE /mcp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Stateless&lt;/strong&gt;: Each request creates a new transport instance. Simple, but no session persistence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stateful&lt;/strong&gt;: Sessions are maintained across requests. Supports Server-Sent Events for real-time updates. This is what you need for OAuth.&lt;/p&gt;

&lt;p&gt;For this guide, we'll use the &lt;strong&gt;Stateful template with OAuth enabled&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaffolding Your Secure MCP Server
&lt;/h2&gt;

&lt;p&gt;Let's create our server. Run the CLI and answer the prompts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @agentailor/create-mcp-server@0.2.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you are using v0.3.0 or later, you'll have a new prompt for selecting a framework. To follow this article, you should select the official SDK.&lt;/p&gt;

&lt;p&gt;When prompted:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enter (y)&lt;/strong&gt; for npx to download the package&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project name&lt;/strong&gt;: &lt;code&gt;my-secure-mcp-server&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Template&lt;/strong&gt;: &lt;code&gt;Stateful&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable OAuth&lt;/strong&gt;: &lt;code&gt;Yes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Package manager&lt;/strong&gt;: Your preference (npm, pnpm, or yarn)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The CLI generates this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-secure-mcp-server/
├── src/
│   ├── server.ts     # MCP server (tools, prompts, resources)
│   ├── index.ts      # Express app and transport setup
│   └── auth.ts       # OAuth middleware
├── package.json
├── tsconfig.json
├── .gitignore
├── .env.example
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Files Explained
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/server.ts&lt;/code&gt;&lt;/strong&gt;: This is where you define your MCP tools, prompts, and resources. It's the "business logic" of your server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/index.ts&lt;/code&gt;&lt;/strong&gt;: The Express application. It sets up routes, applies middleware, and manages the HTTP transport.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/auth.ts&lt;/code&gt;&lt;/strong&gt;: The OAuth middleware. This is provider-agnostic, it works with any OIDC-compliant authorization server. You configure the provider through environment variables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.env.example&lt;/code&gt;&lt;/strong&gt;: Template for required environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3000

&lt;span class="c"&gt;# OAuth Configuration&lt;/span&gt;
&lt;span class="c"&gt;# Issuer URL - your OAuth provider's base URL&lt;/span&gt;
&lt;span class="c"&gt;# Examples:&lt;/span&gt;
&lt;span class="c"&gt;#   Auth0: https://your-tenant.auth0.com&lt;/span&gt;
&lt;span class="c"&gt;#   Keycloak: http://localhost:8080/realms/your-realm&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_ISSUER_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://your-oauth-provider.com

&lt;span class="c"&gt;# Audience - the API identifier (optional, but recommended)&lt;/span&gt;
&lt;span class="c"&gt;# This should match the "aud" claim in your JWT tokens&lt;/span&gt;
&lt;span class="nv"&gt;OAUTH_AUDIENCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://your-mcp-server.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install dependencies and you're ready to configure Keycloak:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;my-secure-mcp-server
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting Up Keycloak
&lt;/h2&gt;

&lt;p&gt;We're using &lt;a href="https://www.keycloak.org/" rel="noopener noreferrer"&gt;Keycloak&lt;/a&gt; as our OIDC provider. Here's why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open-source&lt;/strong&gt;: No vendor lock-in, full transparency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OIDC-compliant&lt;/strong&gt;: Works with any OAuth 2.1 / OpenID Connect client&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supports DCR&lt;/strong&gt;: Dynamic Client Registration out of the box&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted&lt;/strong&gt;: Complete control over your auth infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Battle-tested&lt;/strong&gt;: Used by enterprises worldwide&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Running Keycloak with Docker
&lt;/h3&gt;

&lt;p&gt;From your terminal, run the following command to start the Keycloak container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:8080:8080 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;KC_BOOTSTRAP_ADMIN_USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;KC_BOOTSTRAP_ADMIN_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin quay.io/keycloak/keycloak start-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait a minute for startup, then access the admin console at &lt;a href="http://localhost:8080" rel="noopener noreferrer"&gt;http://localhost:8080&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Keycloak
&lt;/h3&gt;

&lt;p&gt;Log in with &lt;code&gt;admin&lt;/code&gt; / &lt;code&gt;admin&lt;/code&gt;, then follow these steps:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq9e7gkcskj2szirmgu6f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq9e7gkcskj2szirmgu6f.png" alt="Login Screen" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Create a Realm
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Click on "Manage realms" in the top-left sidebar&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create realm&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Name it &lt;code&gt;mcp-realm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9fxevzvittmztovti0qo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9fxevzvittmztovti0qo.png" alt="Realm Creation" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Create a Client
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Clients&lt;/strong&gt; → &lt;strong&gt;Create client&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client ID&lt;/strong&gt;: &lt;code&gt;mcp-server-client&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client authentication&lt;/strong&gt;: Enable (this makes it confidential)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leave redirect URIs empty&lt;/strong&gt; (we'll update later if needed)&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9n6gd9etmvyr94iw1kw6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9n6gd9etmvyr94iw1kw6.png" alt="Client Creation" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment Variables
&lt;/h3&gt;

&lt;p&gt;Update/create your &lt;code&gt;.env&lt;/code&gt; file with the Keycloak values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3000

&lt;span class="nv"&gt;OAUTH_ISSUER_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8080/realms/mcp-realm
&lt;span class="nv"&gt;OAUTH_AUDIENCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="c"&gt;#leave empty for Keycloak&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. Create a Test User
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Users&lt;/strong&gt; → &lt;strong&gt;Add user&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Username&lt;/strong&gt;: &lt;code&gt;testuser&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Email verified&lt;/strong&gt; to ON&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Credentials&lt;/strong&gt; tab → &lt;strong&gt;Set password&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enter a password and disable "Temporary"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3kuu1pndicfrh9m7hf4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3kuu1pndicfrh9m7hf4.png" alt="User Creation" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting MCP Server to Keycloak
&lt;/h2&gt;

&lt;p&gt;With Keycloak running and your &lt;code&gt;.env&lt;/code&gt; configured, start your MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see output like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[auth] Validating OAuth configuration for issuer: http://localhost:8080/realms/mcp-realm
[auth] Successfully fetched OIDC discovery document
[auth] Authorization endpoint: http://localhost:8080/realms/mcp-realm/protocol/openid-connect/auth
[auth] Token endpoint: http://localhost:8080/realms/mcp-realm/protocol/openid-connect/token
[auth] JWKS URI: http://localhost:8080/realms/mcp-realm/protocol/openid-connect/certs
[auth] JWKS endpoint is accessible
[auth] OAuth configuration validated successfully
MCP Stateful HTTP Server listening on port 3000
OAuth metadata available at http://localhost:3000/.well-known/oauth-protected-resource
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The auth middleware automatically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Intercepts incoming requests&lt;/li&gt;
&lt;li&gt;Extracts the Bearer token from the Authorization header&lt;/li&gt;
&lt;li&gt;Validates the token against Keycloak's JWKS endpoint&lt;/li&gt;
&lt;li&gt;Rejects requests with invalid or missing tokens&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your server is now protected. Unauthenticated requests will receive a 401 Unauthorized response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Your Secure MCP Server
&lt;/h2&gt;

&lt;p&gt;Let's verify everything works. We'll test with three methods: VS Code integration, Cursor integration, and a terminal client.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Redirect URIs for VS Code and Cursor
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In Keycloak, go to &lt;strong&gt;Clients&lt;/strong&gt; → &lt;strong&gt;mcp-server-client&lt;/strong&gt; → &lt;strong&gt;Settings&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Valid Redirect URIs&lt;/strong&gt;, add the following URIs:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cursor://anysphere.cursor-mcp/oauth/callback
https://vscode.dev/redirect/*
http://127.0.0.1:33418/* # this may vary based on your setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv686djyu4py44dj01hcm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv686djyu4py44dj01hcm.png" alt="Redirect URIs" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: If you have an error of "Invalid redirect URI", double-check the URIs match exactly or add missing ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  VS Code Integration
&lt;/h3&gt;

&lt;p&gt;VS Code supports MCP servers with OAuth authentication. Create a &lt;code&gt;.vscode/mcp.json&lt;/code&gt; file in your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"my-secure-mcp-server"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:3000/mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On top of the server name you'll see "Start":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;VS Code will try to connect to your MCP server&lt;/li&gt;
&lt;li&gt;If it detects OAuth, it initiates the authorization flow&lt;/li&gt;
&lt;li&gt;If DCR is supported, it registers the client dynamically, otherwise it will prompt you to enter client details&lt;/li&gt;
&lt;li&gt;You log in with your Keycloak credentials&lt;/li&gt;
&lt;li&gt;VS Code receives the token and connects to your server&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your MCP tools are now available through VS Code's Copilot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cursor Integration
&lt;/h3&gt;

&lt;p&gt;Cursor also supports MCP with OAuth. In Cursor, add a new MCP server:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Go to Cursor settings → Tools &amp;amp; MCP → New MCP Server&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enter the following details:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"oauth-server"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:3000/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"auth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"CLIENT_ID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-server-client"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"CLIENT_SECRET"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-client-secret | EMPTY it's optional&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"scopes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp:tools"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;After saving, go back to Tools &amp;amp; MCP and click on "Connect" next to your new server.&lt;/li&gt;
&lt;li&gt;Cursor will open a browser window for you to log in via Keycloak.&lt;/li&gt;
&lt;li&gt;After logging in, Cursor will receive the access token and connect to your MCP server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F66mlgher214m5nzomm7q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F66mlgher214m5nzomm7q.png" alt="MCP Tools" width="800" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Terminal Client Testing
&lt;/h3&gt;

&lt;p&gt;The terminal client uses Dynamic Client Registration (DCR) to connect to your MCP server. This requires additional Keycloak configuration that wasn't needed for VS Code or Cursor (which use predefined clients).&lt;/p&gt;

&lt;h4&gt;
  
  
  Enabling Dynamic Client Registration in Keycloak
&lt;/h4&gt;

&lt;p&gt;For DCR to work, Keycloak needs to trust the host where your client is running:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In Keycloak, go to &lt;strong&gt;Clients&lt;/strong&gt; → &lt;strong&gt;Client registration&lt;/strong&gt; → &lt;strong&gt;Trusted Hosts&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Disable the &lt;strong&gt;Client URIs Must Match&lt;/strong&gt; setting&lt;/li&gt;
&lt;li&gt;Add your testing host's IP address to the trusted hosts list&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To find your host IP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux/macOS&lt;/strong&gt;: Run &lt;code&gt;ifconfig&lt;/code&gt; in your terminal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: Run &lt;code&gt;ipconfig&lt;/code&gt; in Command Prompt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're unsure which IP to add, check the Keycloak logs for a line like &lt;code&gt;Failed to verify remote host : 192.168.x.x&lt;/code&gt;. That's the IP you need to whitelist.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj46pfrnsef1bhsiky2gg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj46pfrnsef1bhsiky2gg.png" alt="DCR Trusted Hosts Configuration" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Creating the mcp:tools Scope
&lt;/h4&gt;

&lt;p&gt;The terminal client requires a custom scope to access MCP tools. Without this, authentication will succeed but tool access will fail.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In Keycloak, go to &lt;strong&gt;Client scopes&lt;/strong&gt; → &lt;strong&gt;Create client scope&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt;: &lt;code&gt;mcp:tools&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type&lt;/strong&gt;: Set to &lt;strong&gt;Default&lt;/strong&gt; (so it's automatically included)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Include in token scope&lt;/strong&gt;: Enable this toggle (required for token validation)&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9wq9s6fel1t0fo52e31a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9wq9s6fel1t0fo52e31a.png" alt="Create mcp:tools Scope" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Running the Terminal Client
&lt;/h4&gt;

&lt;p&gt;Now you can test with the &lt;a href="https://github.com/IBJunior/mcp-oauth-client" rel="noopener noreferrer"&gt;mcp-oauth-client&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/IBJunior/mcp-oauth-client
&lt;span class="nb"&gt;cd &lt;/span&gt;mcp-oauth-client
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure the client with your server and Keycloak details, then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use DCR to register itself with Keycloak automatically&lt;/li&gt;
&lt;li&gt;Perform the OAuth flow&lt;/li&gt;
&lt;li&gt;Obtain an access token (if the browser doesn't open automatically, copy-paste the URL from the console)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fskf50x1ibtk0mhyk5tet.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fskf50x1ibtk0mhyk5tet.png" alt="Simple MCP Client" width="800" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect to your MCP server&lt;/li&gt;
&lt;li&gt;List available tools&lt;/li&gt;
&lt;li&gt;Allow you to invoke tools interactively&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is useful for debugging and verifying your setup works end-to-end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq03lu94vmwr2vlz7pvwk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq03lu94vmwr2vlz7pvwk.png" alt=" " width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Custom Tools
&lt;/h2&gt;

&lt;p&gt;The scaffolded server comes with example tools. Let's add a custom one to see how easy it is.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;src/server.ts&lt;/code&gt; and add a new tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;greet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Greets a user in their preferred language.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The user's name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Greeting language&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;greetings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Hello, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;! Welcome to the secure MCP server.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;es&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`¡Hola, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;! Bienvenido al servidor MCP seguro.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bonjour, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;! Bienvenue sur le serveur MCP sécurisé.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;greetings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Restart your server and the new tool is available, &lt;strong&gt;automatically protected by OAuth&lt;/strong&gt;. No additional authentication code required per tool. The middleware handles everything at the request level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using MCP Inspector
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/legacy/tools/inspector" rel="noopener noreferrer"&gt;MCP Inspector&lt;/a&gt; is a debugging UI for MCP servers. It lets you explore available tools, test invocations, and inspect responses.&lt;/p&gt;

&lt;p&gt;The scaffolded project includes an inspect script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run inspect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important caveat&lt;/strong&gt;: For authenticated servers, you'll need to manually setup the oauth flow in MCP Inspector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;Let's summarize what we've covered:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MCP Authorization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Industry standard for securing MCP servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OAuth 2.1 + PKCE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Modern, secure token-based authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dynamic Client Registration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Allows any MCP client to connect without manual setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Keycloak&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open-source, OIDC-compliant, supports DCR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;create-mcp-server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast scaffolding with authentication built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OIDC abstraction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Swap providers via environment variables&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key insight: security doesn't have to be complicated. With the right tools and a clear understanding of the concepts, you can go from zero to a production-ready, authenticated MCP server in minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You've built a production-ready MCP server with OAuth authentication. Your tools are protected, tokens are validated, and you're following the MCP Authorization specification.&lt;/p&gt;

&lt;p&gt;But here's the best part: &lt;strong&gt;Keycloak is just one option&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;create-mcp-server&lt;/code&gt; uses a provider-agnostic OIDC implementation, you can swap Keycloak for any compliant provider. Just update your environment variables and you're done. That's the power of standards-based authentication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on production deployments&lt;/strong&gt;: The Keycloak configuration in this guide is designed for demonstration purposes. A production setup requires additional hardening: HTTPS everywhere, stricter redirect URI validation, token lifetime tuning, proper realm and client policies, and more. For production-grade configuration, refer to the &lt;a href="https://www.keycloak.org/" rel="noopener noreferrer"&gt;official Keycloak documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Next Steps
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Star &lt;a href="https://github.com/agentailor/create-mcp-server" rel="noopener noreferrer"&gt;create-mcp-server&lt;/a&gt; on GitHub&lt;/li&gt;
&lt;li&gt;Contribute to &lt;a href="https://github.com/agentailor/create-mcp-server" rel="noopener noreferrer"&gt;create-mcp-server&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Explore the &lt;a href="https://github.com/IBJunior/mcp-oauth-client" rel="noopener noreferrer"&gt;mcp-oauth-client&lt;/a&gt; for testing&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Enjoying content like this? Sign up for &lt;a href="https://www.linkedin.com/newsletters/agent-briefings-7391777936955310080/" rel="noopener noreferrer"&gt;Agent Briefings&lt;/a&gt;, where I share insights and news on building and scaling MCP Servers and AI agents.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization" rel="noopener noreferrer"&gt;MCP Authorization Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/docs/tutorials/security/authorization" rel="noopener noreferrer"&gt;Understanding Authorization in MCP (Tutorial)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/agentailor/create-mcp-server" rel="noopener noreferrer"&gt;create-mcp-server GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/IBJunior/mcp-oauth-client" rel="noopener noreferrer"&gt;mcp-oauth-client&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.keycloak.org/documentation" rel="noopener noreferrer"&gt;Keycloak Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Related Articles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/docker-mcp-catalog-and-toolkit?utm_source=devto&amp;amp;utm_medium=blog_post&amp;amp;utm_campaign=mcp_oauth" rel="noopener noreferrer"&gt;Run Any MCP Server Locally with Docker's MCP Catalog and Toolkit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/publishing-mcp-server-to-official-registry?utm_source=&amp;lt;devto&amp;gt;&amp;amp;utm_medium=blog_post&amp;amp;utm_campaign=mcp_oauth" rel="noopener noreferrer"&gt;How to Publish Your MCP Server to the Official Registry&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>javascript</category>
      <category>security</category>
    </item>
    <item>
      <title>create-mcp-server v0.6.0 is out, now with stdio transport support</title>
      <dc:creator>Ali Ibrahim</dc:creator>
      <pubDate>Thu, 26 Feb 2026 20:22:59 +0000</pubDate>
      <link>https://dev.to/ialijr/create-mcp-server-v060-is-out-now-with-stdio-transport-support-27ch</link>
      <guid>https://dev.to/ialijr/create-mcp-server-v060-is-out-now-with-stdio-transport-support-27ch</guid>
      <description>&lt;p&gt;Until now, the CLI only scaffolded Streamable HTTP servers (remote, cloud-deployable). But a lot of people build local MCP servers for Claude Desktop and other local clients — and stdio is the right transport for that.&lt;/p&gt;

&lt;p&gt;So I added it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx @agentailor/create-mcp-server --name=my-server --stdio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No HTTP server, no port, no Dockerfile. Just a clean stdio server ready to connect to any local MCP client.&lt;/p&gt;

&lt;p&gt;You can also combine it with FastMCP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx @agentailor/create-mcp-server --name=my-server --stdio --framework=fastmcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;What the CLI now supports:&lt;br&gt;
→ HTTP (Streamable) or stdio transport&lt;br&gt;
→ Official MCP SDK or FastMCP&lt;br&gt;
→ Stateless or stateful server modes&lt;br&gt;
→ Optional OAuth (SDK + HTTP)&lt;br&gt;
→ npm, pnpm, or yarn&lt;/p&gt;




&lt;p&gt;📚 Learning Resources&lt;/p&gt;

&lt;p&gt;If you're getting started with MCP servers, here are the guides I've written:&lt;/p&gt;

&lt;p&gt;→ Build your first MCP server in 5 minutes&lt;br&gt;
&lt;a href="https://blog.agentailor.com/posts/create-your-first-mcp-server-in-5-minutes" rel="noopener noreferrer"&gt;https://blog.agentailor.com/posts/create-your-first-mcp-server-in-5-minutes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;→ Secure your MCP server with OAuth (Keycloak)&lt;br&gt;
&lt;a href="https://blog.agentailor.com/posts/oauth-for-mcp-servers-practical-guide-keycloak" rel="noopener noreferrer"&gt;https://blog.agentailor.com/posts/oauth-for-mcp-servers-practical-guide-keycloak&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;→ Getting started with FastMCP&lt;br&gt;
&lt;a href="https://blog.agentailor.com/posts/getting-started-with-fastmcp" rel="noopener noreferrer"&gt;https://blog.agentailor.com/posts/getting-started-with-fastmcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;→ OAuth for MCP Clients (Next.js + LangGraph.js)&lt;br&gt;
&lt;a href="https://blog.agentailor.com/posts/mcp-client-oauth-nextjs-langgraph" rel="noopener noreferrer"&gt;https://blog.agentailor.com/posts/mcp-client-oauth-nextjs-langgraph&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Star the repo if this is useful 🙏&lt;br&gt;
&lt;a href="https://github.com/agentailor/create-mcp-server" rel="noopener noreferrer"&gt;https://github.com/agentailor/create-mcp-server&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Deploy Your MCP Server to Google Cloud Run (For Free)</title>
      <dc:creator>Ali Ibrahim</dc:creator>
      <pubDate>Mon, 23 Feb 2026 18:47:58 +0000</pubDate>
      <link>https://dev.to/ialijr/deploy-your-mcp-server-to-google-cloud-run-for-free-5d7k</link>
      <guid>https://dev.to/ialijr/deploy-your-mcp-server-to-google-cloud-run-for-free-5d7k</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;You've built an MCP server. It works on localhost. Your AI assistant can call tools, fetch data, and do useful things, as long as everything runs on your machine.&lt;/p&gt;

&lt;p&gt;But what happens when you want to share it with your team? Or connect to it from a different device? Or just keep it running without your laptop open?&lt;/p&gt;

&lt;p&gt;You need to deploy it.&lt;/p&gt;

&lt;p&gt;This guide walks you through deploying a &lt;strong&gt;Streamable HTTP&lt;/strong&gt; MCP server to Google Cloud Run, from scaffolding to a live URL in minutes. Streamable HTTP is the transport designed for remote deployments: it works over standard HTTPS, plays nicely with load balancers, and doesn't require the client to run your server as a subprocess.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If you're working with a &lt;strong&gt;stdio&lt;/strong&gt; MCP server, the deployment path is different. We'll cover that briefly at the end of this article.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you've already built an MCP server using our &lt;a href="https://blog.agentailor.com/blog/create-your-first-mcp-server-in-5-minutes?utm_source=devto&amp;amp;utm_medium=blog_post&amp;amp;utm_campaign=deploy_mcp_cloud_run" rel="noopener noreferrer"&gt;first MCP server guide&lt;/a&gt;, you can deploy that project directly. Otherwise, we'll scaffold a fresh one below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll learn:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to scaffold a deployment-ready MCP server&lt;/li&gt;
&lt;li&gt;How to set up the Google Cloud CLI&lt;/li&gt;
&lt;li&gt;How to deploy to Cloud Run with a single command&lt;/li&gt;
&lt;li&gt;How to test your live server with MCP Inspector&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 or later&lt;/li&gt;
&lt;li&gt;A Google Cloud account (&lt;a href="https://cloud.google.com/free" rel="noopener noreferrer"&gt;free to create&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Basic terminal familiarity&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Google Cloud Run?
&lt;/h2&gt;

&lt;p&gt;Cloud Run can deploy from a container image, a Dockerfile, or even raw source code (using &lt;a href="https://cloud.google.com/docs/buildpacks/overview" rel="noopener noreferrer"&gt;buildpacks&lt;/a&gt;). The &lt;code&gt;create-mcp-server&lt;/code&gt; scaffold includes a Dockerfile, so that's the path we'll use. If you're bringing your own server, any of these options work.&lt;/p&gt;

&lt;p&gt;Here's why Cloud Run is a great fit for MCP servers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generous free tier&lt;/strong&gt; — 2 million requests/month, 180,000 vCPU-seconds, and 360,000 GiB-seconds of memory. More than enough for development, testing, and light production use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build from source&lt;/strong&gt; — No need to install Docker locally. Cloud Run uses Cloud Build to build your Dockerfile remotely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS by default&lt;/strong&gt; — Every deployed service gets an HTTPS URL automatically. MCP clients expect HTTPS for remote servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale to zero&lt;/strong&gt; — When no one is calling your server, it scales down to zero instances. You pay nothing when idle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No code changes&lt;/strong&gt; — Streamable HTTP servers work as-is on Cloud Run. The &lt;code&gt;POST /mcp&lt;/code&gt; endpoint maps directly to Cloud Run's request-based model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short: you get a free, secure, production-ready deployment with almost zero configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaffold Your MCP Server
&lt;/h2&gt;

&lt;p&gt;If you already have an MCP server project with a Dockerfile, skip ahead to Set Up the gcloud CLI.&lt;/p&gt;

&lt;p&gt;Otherwise, let's scaffold a fresh one. Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# For more options, see https://github.com/agentailor/create-mcp-server&lt;/span&gt;
npx @agentailor/create-mcp-server &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-mcp-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a &lt;strong&gt;stateless&lt;/strong&gt; MCP server using the Official TypeScript SDK. No interactive prompts, no choices needed. One command, one project.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We're using a stateless server here for simplicity, but you can deploy a stateful server to Cloud Run the same way. The deployment steps are identical.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The generated project structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-mcp-server/
├── src/
│   ├── server.ts     # MCP server (tools, prompts, resources)
│   └── index.ts      # Express app and HTTP transport
├── Dockerfile        # Production-ready Docker build
├── package.json
├── tsconfig.json
├── .env.example
├── .gitignore
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install dependencies and verify it works locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;my-mcp-server
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MCP Stateless HTTP Server listening on port 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your server is running at &lt;code&gt;http://localhost:3000/mcp&lt;/code&gt;. Stop it with &lt;code&gt;Ctrl+C&lt;/code&gt;. We're ready to deploy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Want to understand the scaffolded code in detail? See our &lt;a href="https://blog.agentailor.com/blog/create-your-first-mcp-server-in-5-minutes?utm_source=devto&amp;amp;utm_medium=blog_post&amp;amp;utm_campaign=deploy_mcp_cloud_run" rel="noopener noreferrer"&gt;Create Your First MCP Server&lt;/a&gt; guide.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Dockerfile
&lt;/h2&gt;

&lt;p&gt;The scaffold includes a production-ready Dockerfile. Here's what it does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Multi-stage build for production&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Copy package files&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;

&lt;span class="c"&gt;# Install all dependencies (including dev)&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci

&lt;span class="c"&gt;# Copy source code&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# Build the application&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="c"&gt;# Production stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Copy package files&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;

&lt;span class="c"&gt;# Install production dependencies only&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev

&lt;span class="c"&gt;# Copy built application from builder stage&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/dist ./dist&lt;/span&gt;

&lt;span class="c"&gt;# Expose the port the app runs on&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;

&lt;span class="c"&gt;# Start the application&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/index.js"]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a multi-stage build: the first stage compiles TypeScript, the second stage copies only the compiled output and dependencies. This keeps the final image small.&lt;/p&gt;

&lt;p&gt;The important part: it exposes &lt;strong&gt;port 3000&lt;/strong&gt;, which matches the &lt;code&gt;--port 3000&lt;/code&gt; flag we'll use when deploying. Cloud Build will use this Dockerfile automatically. You don't need Docker installed on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set Up the gcloud CLI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install gcloud CLI
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Windows&lt;/strong&gt; (via winget):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;winget &lt;span class="nb"&gt;install &lt;/span&gt;Google.CloudSDK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or &lt;a href="https://cloud.google.com/sdk/docs/install#windows" rel="noopener noreferrer"&gt;download the installer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS&lt;/strong&gt; (via Homebrew):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; google-cloud-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Linux&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-x86_64.tar.gz
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xf&lt;/span&gt; google-cloud-cli-linux-x86_64.tar.gz
./google-cloud-sdk/install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing, restart your terminal and authenticate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud init
gcloud auth login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enable Billing
&lt;/h3&gt;

&lt;p&gt;Cloud Run requires a billing account, even for the free tier. You won't be charged if you stay within the free limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Via the console&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://console.cloud.google.com/billing" rel="noopener noreferrer"&gt;Google Cloud Billing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Link a billing account&lt;/strong&gt; (or &lt;strong&gt;Create account&lt;/strong&gt; if you don't have one)&lt;/li&gt;
&lt;li&gt;Add a payment method&lt;/li&gt;
&lt;li&gt;Select your project and link it to the billing account&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Option 2: Via CLI&lt;/strong&gt; (if you already have a billing account):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud billing accounts list
gcloud billing projects &lt;span class="nb"&gt;link &lt;/span&gt;YOUR_PROJECT_ID &lt;span class="nt"&gt;--billing-account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_BILLING_ACCOUNT_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploy to Cloud Run
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create a New Project
&lt;/h3&gt;

&lt;p&gt;We recommend creating a &lt;strong&gt;dedicated project&lt;/strong&gt; for this tutorial. This keeps your demo isolated from existing resources, so cleanup commands won't accidentally affect your other projects or container images.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud projects create my-mcp-project &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"My MCP Server"&lt;/span&gt;
gcloud config &lt;span class="nb"&gt;set &lt;/span&gt;project my-mcp-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Project IDs must be globally unique. If &lt;code&gt;my-mcp-project&lt;/code&gt; is taken, choose something else.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Enable Required APIs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud services &lt;span class="nb"&gt;enable &lt;/span&gt;run.googleapis.com artifactregistry.googleapis.com cloudbuild.googleapis.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This enables three services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Run&lt;/strong&gt;: hosts your container&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifact Registry&lt;/strong&gt;: stores your container image&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Build&lt;/strong&gt;: builds your Dockerfile remotely&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Deploy from Source
&lt;/h3&gt;

&lt;p&gt;Make sure you're in your project directory (&lt;code&gt;my-mcp-server/&lt;/code&gt;), then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud run deploy my-mcp-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-central1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allow-unauthenticated&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what each flag does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--source .&lt;/code&gt;: sends your source code to Cloud Build, which builds the container using your Dockerfile&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--region us-central1&lt;/code&gt;: deploys to a US region (or choose another region closer to you)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--allow-unauthenticated&lt;/code&gt;: makes your &lt;code&gt;/mcp&lt;/code&gt; endpoint publicly accessible&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--port 3000&lt;/code&gt;: tells Cloud Run which port your container listens on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first deploy takes a couple of minutes as Cloud Build pulls the base image and builds your container. Subsequent deploys are faster thanks to layer caching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get Your Service URL
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud run services describe my-mcp-server &lt;span class="nt"&gt;--region&lt;/span&gt; us-central1 &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'value(status.url)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This outputs something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://my-mcp-server-abc123xyz.us-central1.run.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your MCP endpoint is now live at &lt;code&gt;https://&amp;lt;service-url&amp;gt;/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Troubleshooting: Invalid Host Error
&lt;/h3&gt;

&lt;p&gt;If you get an error like &lt;code&gt;"Invalid Host: my-mcp-server-abc123xyz.us-central1.run.app"&lt;/code&gt;, it means the server is validating the &lt;code&gt;Host&lt;/code&gt; header and rejecting the Cloud Run domain.&lt;/p&gt;

&lt;p&gt;Fix it by setting the &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt; environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud run services update my-mcp-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-central1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set-env-vars&lt;/span&gt; &lt;span class="nv"&gt;ALLOWED_HOSTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-mcp-server-abc123xyz.us-central1.run.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the domain with your actual service URL (without &lt;code&gt;https://&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Your Deployed Server
&lt;/h2&gt;

&lt;p&gt;Let's verify everything works. Open a terminal and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @modelcontextprotocol/inspector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the inspector opens in your browser:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Change the transport type to &lt;strong&gt;Streamable HTTP&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enter your Cloud Run URL: &lt;code&gt;https://&amp;lt;service-url&amp;gt;/mcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Connect&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You should see your server's tools listed. Click on any tool, fill in the parameters, and run it.&lt;/p&gt;

&lt;p&gt;That's it. Your MCP server is live on the internet, accessible from any MCP-compatible client, anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;If you want to remove the deployed resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Delete the Cloud Run service&lt;/span&gt;
gcloud run services delete my-mcp-server &lt;span class="nt"&gt;--region&lt;/span&gt; us-central1

&lt;span class="c"&gt;# Delete the container image from Artifact Registry&lt;/span&gt;
gcloud artifacts docker images list us-central1-docker.pkg.dev/my-mcp-project &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'value(IMAGE)'&lt;/span&gt; | xargs &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; gcloud artifacts docker images delete &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="nt"&gt;--quiet&lt;/span&gt;

&lt;span class="c"&gt;# Optionally delete the entire project (removes everything)&lt;/span&gt;
gcloud projects delete my-mcp-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Cloud Build stores your container image in Artifact Registry, which charges for storage after the first 500MB free. Deleting the project removes everything, but if you're keeping the project, clean up the images separately.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're staying within the free tier, there's no urgency to clean up. But it's good practice to remove resources you no longer need.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Now that your server is deployed, here are some ideas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add authentication&lt;/strong&gt;: secure your deployed server with OAuth using our &lt;a href="https://blog.agentailor.com/blog/oauth-for-mcp-servers-practical-guide-keycloak?utm_source=devto&amp;amp;utm_medium=blog_post&amp;amp;utm_campaign=deploy_mcp_cloud_run" rel="noopener noreferrer"&gt;OAuth for MCP Servers guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build real tools&lt;/strong&gt;: replace the placeholder tools with your own by following our &lt;a href="https://blog.agentailor.com/blog/create-your-first-mcp-server-in-5-minutes?utm_source=devto&amp;amp;utm_medium=blog_post&amp;amp;utm_campaign=deploy_mcp_cloud_run" rel="noopener noreferrer"&gt;first MCP server guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try FastMCP&lt;/strong&gt;: build with a different framework using our &lt;a href="https://blog.agentailor.com/blog/getting-started-with-fastmcp?utm_source=devto&amp;amp;utm_medium=blog_post&amp;amp;utm_campaign=deploy_mcp_cloud_run" rel="noopener noreferrer"&gt;FastMCP guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connect your IDE&lt;/strong&gt;: point your VS Code or Cursor MCP configuration at your Cloud Run URL instead of localhost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set up a custom domain&lt;/strong&gt;: configure a &lt;a href="https://cloud.google.com/run/docs/mapping-custom-domains" rel="noopener noreferrer"&gt;custom domain in Cloud Run&lt;/a&gt; for a cleaner URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About Stdio Servers?
&lt;/h2&gt;

&lt;p&gt;Everything in this guide applies to &lt;strong&gt;Streamable HTTP&lt;/strong&gt; servers, the transport designed for cloud deployment. But not all MCP servers use HTTP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stdio servers&lt;/strong&gt; communicate via stdin/stdout and run as local subprocesses. They can't be deployed as web services. Instead, they're &lt;em&gt;distributed&lt;/em&gt; so clients can run them locally: via &lt;strong&gt;npm&lt;/strong&gt; (&lt;code&gt;npx your-server&lt;/code&gt;), &lt;strong&gt;PyPI&lt;/strong&gt; (&lt;code&gt;uvx your-server&lt;/code&gt;), or &lt;strong&gt;Docker Hub&lt;/strong&gt; (pull and run via Docker).&lt;/p&gt;

&lt;p&gt;We'll cover stdio distribution in detail in a future article. Stay tuned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You just went from a scaffolded project to a live, publicly accessible MCP server, in minutes, for free. No Docker installed locally, no infrastructure to manage, just a single &lt;code&gt;gcloud run deploy --source .&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;With Cloud Run, your MCP server is always available, scales automatically, and costs nothing while idle. That's a pretty good deal for getting your AI tools off localhost and into the real world.&lt;/p&gt;

&lt;p&gt;If you found &lt;code&gt;create-mcp-server&lt;/code&gt; useful, consider giving it a star on &lt;a href="https://github.com/agentailor/create-mcp-server" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and sharing it with others who are building MCP servers. It helps the project grow and helps more developers get started quickly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Enjoying content like this? Sign up for &lt;a href="https://www.linkedin.com/newsletters/agent-briefings-7391777936955310080/" rel="noopener noreferrer"&gt;Agent Briefings&lt;/a&gt;, where I share insights and news on building and scaling MCP Servers and AI agents.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/agentailor/create-mcp-server" rel="noopener noreferrer"&gt;create-mcp-server (GitHub)&lt;/a&gt;: the CLI tool used in this guide&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/run/docs" rel="noopener noreferrer"&gt;Google Cloud Run Documentation&lt;/a&gt;: official Cloud Run docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/free" rel="noopener noreferrer"&gt;Google Cloud Free Tier&lt;/a&gt;: free tier details and limits&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/modelcontextprotocol/inspector" rel="noopener noreferrer"&gt;MCP Inspector (GitHub)&lt;/a&gt;: testing and debugging tool for MCP servers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP Documentation&lt;/a&gt;: official protocol specification&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/sdk/gcloud/reference" rel="noopener noreferrer"&gt;gcloud CLI Reference&lt;/a&gt;: CLI documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Related Articles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/create-your-first-mcp-server-in-5-minutes?utm_source=devto&amp;amp;utm_medium=blog_post&amp;amp;utm_campaign=deploy_mcp_cloud_run" rel="noopener noreferrer"&gt;Create Your First MCP Server in 5 Minutes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/oauth-for-mcp-servers-practical-guide-keycloak?utm_source=devto&amp;amp;utm_medium=blog_post&amp;amp;utm_campaign=deploy_mcp_cloud_run" rel="noopener noreferrer"&gt;Securing MCP Servers with OAuth and Keycloak&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/getting-started-with-fastmcp?utm_source=devto&amp;amp;utm_medium=blog_post&amp;amp;utm_campaign=deploy_mcp_cloud_run" rel="noopener noreferrer"&gt;Getting Started with FastMCP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>cloud</category>
      <category>programming</category>
    </item>
    <item>
      <title>Lessons from OpenClaw's Architecture for Agent Builders</title>
      <dc:creator>Ali Ibrahim</dc:creator>
      <pubDate>Thu, 19 Feb 2026 10:51:26 +0000</pubDate>
      <link>https://dev.to/ialijr/lessons-from-openclaws-architecture-for-agent-builders-1j93</link>
      <guid>https://dev.to/ialijr/lessons-from-openclaws-architecture-for-agent-builders-1j93</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;OpenClaw has over 200,000 GitHub stars. It is one of the fastest-growing open-source projects in history. Lex Fridman &lt;a href="https://www.youtube.com/watch?v=YFjfBk8HI5o" rel="noopener noreferrer"&gt;discussed it on his podcast&lt;/a&gt;. Andrej Karpathy called one of its side projects "the most incredible sci-fi takeoff-adjacent thing" he has seen recently.&lt;/p&gt;

&lt;p&gt;Most of the coverage has been surface-level: "look, an AI that controls your computer." But if you are building agents, the interesting question is not &lt;em&gt;what&lt;/em&gt; OpenClaw does — it is &lt;em&gt;how&lt;/em&gt; the architecture enables it.&lt;/p&gt;

&lt;p&gt;OpenClaw solves a problem most agent frameworks ignore: running a persistent, multi-channel AI agent on your own hardware that does not break under real-world usage. The engineering decisions behind that are worth studying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll learn:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The 4-layer gateway architecture and why a single process matters&lt;/li&gt;
&lt;li&gt;The Lane Queue system, the core reliability pattern most agents lack&lt;/li&gt;
&lt;li&gt;Skills-as-markdown: why prompt engineering beat code for extensibility&lt;/li&gt;
&lt;li&gt;Human-readable memory you can open in a text editor&lt;/li&gt;
&lt;li&gt;Security lessons from CVEs and supply chain attacks&lt;/li&gt;
&lt;li&gt;10 concrete patterns to adopt in your own agent systems&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why OpenClaw Matters for Builders
&lt;/h2&gt;

&lt;p&gt;OpenClaw was created by Peter Steinberger, an Austrian software engineer known for building developer tools in the iOS/macOS ecosystem. The project started in November 2025 as a personal WhatsApp relay script called Clawdbot. After a trademark dispute with Anthropic, it became Moltbot, then settled on OpenClaw three days later.&lt;/p&gt;

&lt;p&gt;Three factors drove the viral growth:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Local-first in the age of cloud lock-in.&lt;/strong&gt; Your data, your hardware, no vendor dependency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It actually works across platforms.&lt;/strong&gt; WhatsApp, Telegram, Discord, iMessage, Slack, Signal, and more from a single agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-modifying skills.&lt;/strong&gt; The agent can write and deploy its own new capabilities mid-conversation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But the insight that matters most for builders: OpenClaw is not a framework. It is a &lt;strong&gt;gateway&lt;/strong&gt; — a single runtime that sits between your AI model and the outside world. That architectural choice shaped every other decision in the project.&lt;/p&gt;

&lt;p&gt;Let's walk through the architecture layer by layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 4-Layer Architecture
&lt;/h2&gt;

&lt;p&gt;OpenClaw's architecture breaks down into four distinct layers, each with a clear responsibility:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjqyt94gl8tu59j6m9p9z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjqyt94gl8tu59j6m9p9z.png" alt="OpenClaw 4-Layer Architecture" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;
  &lt;em&gt;OpenClaw's 4-layer architecture: Gateway, Integration, Execution, and Intelligence.&lt;/em&gt;
&lt;/center&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Layer&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Responsibility&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Key Pattern&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gateway&lt;/td&gt;
&lt;td&gt;Connection management, routing, auth&lt;/td&gt;
&lt;td&gt;Single-process multiplexing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution&lt;/td&gt;
&lt;td&gt;Task ordering, concurrency control&lt;/td&gt;
&lt;td&gt;Per-session serial queues (Lane Queue)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration&lt;/td&gt;
&lt;td&gt;Platform normalization&lt;/td&gt;
&lt;td&gt;Channel adapters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intelligence&lt;/td&gt;
&lt;td&gt;Agent behavior, knowledge, proactivity&lt;/td&gt;
&lt;td&gt;Skills + Memory + Heartbeat&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What Runs the Agent Itself?
&lt;/h3&gt;

&lt;p&gt;A notable architectural decision: OpenClaw does not implement its own agent runtime. The core agent loop — tool calling, context management, LLM interaction — is handled by the &lt;a href="https://github.com/mariozechner/pi-agent" rel="noopener noreferrer"&gt;Pi agent framework&lt;/a&gt; (&lt;code&gt;@mariozechner/pi-agent-core&lt;/code&gt;, &lt;code&gt;pi-ai&lt;/code&gt;, &lt;code&gt;pi-coding-agent&lt;/code&gt;). OpenClaw builds the gateway, orchestration, and integration layers on top of Pi.&lt;/p&gt;

&lt;p&gt;This separation is telling. It reinforces the project's core thesis: the hard problem in personal AI agents is not the agent loop itself, but everything around it. Channel normalization, session management, memory persistence, skill extensibility, and security are where the complexity lives. Pi handles the "think and act" cycle. OpenClaw handles the "connect, queue, remember, and extend" layers.&lt;/p&gt;

&lt;p&gt;OpenClaw also implements the &lt;a href="https://agentclientprotocol.com/" rel="noopener noreferrer"&gt;Agent Client Protocol (ACP)&lt;/a&gt; via &lt;code&gt;@agentclientprotocol/sdk&lt;/code&gt;, a standardized protocol for agent-to-editor communication. This bridges the Gateway to tools like code editors, mapping ACP sessions to Gateway session keys and translating between protocol-native commands (&lt;code&gt;prompt&lt;/code&gt; → &lt;code&gt;chat.send&lt;/code&gt;, &lt;code&gt;cancel&lt;/code&gt; → &lt;code&gt;chat.abort&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Gateway Layer
&lt;/h3&gt;

&lt;p&gt;Everything routes through a single Node.js process — the Gateway. It runs locally on port &lt;code&gt;18789&lt;/code&gt; and handles WebSocket control messages, HTTP APIs (OpenAI-compatible), and a browser-based Control UI from a single multiplexed port.&lt;/p&gt;

&lt;p&gt;This is a deliberate trade-off. A single process means no inter-process communication overhead, simple deployment (one &lt;code&gt;npm i -g openclaw&lt;/code&gt; command), and straightforward debugging. It also means no horizontal scaling, but OpenClaw targets personal and small-team use, where operational simplicity matters more than throughput.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6oe0c2m67dsl2l3p7cun.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6oe0c2m67dsl2l3p7cun.png" alt="Connection Life Cycle" width="800" height="555"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Gateway enforces authentication by default. Non-loopback binding without a token is refused. The WebSocket protocol follows a strict handshake: the client sends a &lt;code&gt;connect&lt;/code&gt; frame, and the Gateway responds with a &lt;code&gt;hello-ok&lt;/code&gt; snapshot containing presence, health, state, uptime, and rate limits.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This single-process design is a conscious trade-off. If you need horizontal scaling across many users, you need a different architecture. OpenClaw optimizes for the "personal AI assistant" use case where one Gateway serves one person (or a small team).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Intelligence Layer
&lt;/h3&gt;

&lt;p&gt;The top layer is where agent behavior lives: skills, memory, the heartbeat daemon, and multi-agent routing. We will cover each of these in dedicated sections below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lane Queue: OpenClaw's Core Innovation
&lt;/h2&gt;

&lt;p&gt;If you take one pattern from this article, make it this one.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;Most agent systems let multiple requests execute concurrently against the same session state. A user sends three messages in quick succession. The agent starts processing all three in parallel. Now you have three tool calls potentially writing to the same file, three API requests with contradictory assumptions, and an interleaved log that is impossible to debug.&lt;/p&gt;

&lt;p&gt;Race conditions in agent systems are not edge cases — they are the default failure mode when you accept concurrent input without explicit ordering.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: Default Serial, Explicit Parallel
&lt;/h3&gt;

&lt;p&gt;OpenClaw's Lane Queue enforces a simple rule: &lt;strong&gt;every session gets its own queue, and tasks within a queue execute one at a time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpnbwc1j4ua6x3aq8gsz3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpnbwc1j4ua6x3aq8gsz3.png" alt="Lane Queue Message Flow" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;
  &lt;em&gt;
    With the Lane Queue, messages execute serially per session, eliminating race conditions by
    design.
  &lt;/em&gt;
&lt;/center&gt;

&lt;p&gt;Here is the conceptual model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SessionKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="c1"&gt;// workspace:channel:userId&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LaneQueue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;queues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SessionKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SessionKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// No other task running, execute immediately&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Otherwise, this task waits its turn&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SessionKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
    &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// Serial: wait for completion&lt;/span&gt;
      &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session keys are structured.&lt;/strong&gt; &lt;code&gt;workspace:channel:userId&lt;/code&gt;, not just a user ID. This prevents cross-context data leaks between the same user in different channels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallelism is opt-in.&lt;/strong&gt; Additional lanes (e.g., &lt;code&gt;cron&lt;/code&gt;, &lt;code&gt;subagent&lt;/code&gt;) allow background jobs to run without blocking the main session queue. But the default is serial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backpressure is built in.&lt;/strong&gt; If the agent is overwhelmed, the queue grows. You can implement timeout or overflow strategies at the queue level, not scattered across individual handlers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why This Matters for Your Agents
&lt;/h3&gt;

&lt;p&gt;Even if you are not building a multi-channel gateway, the per-session serial queue pattern prevents an entire class of bugs. If your agent can receive concurrent input — webhooks, streaming UI, multiple users — you need something like this.&lt;/p&gt;

&lt;p&gt;The Lane Queue also makes debugging straightforward. Every action for a given session happened in order. There is no "which thread was this?" question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Channel Abstraction: One Agent, Ten Platforms
&lt;/h2&gt;

&lt;p&gt;OpenClaw supports over a dozen messaging platforms. Core channels are implemented in &lt;a href="https://github.com/openclaw/openclaw" rel="noopener noreferrer"&gt;&lt;code&gt;src/&lt;/code&gt;&lt;/a&gt; (WhatsApp via Baileys, Telegram via grammY, Discord via @buape/carbon, Slack via @slack/bolt, iMessage, Signal), while extension channels live in the &lt;a href="https://github.com/openclaw/openclaw" rel="noopener noreferrer"&gt;&lt;code&gt;extensions/&lt;/code&gt;&lt;/a&gt; directory as standalone packages (Matrix via @vector-im/matrix-bot-sdk, Google Chat, Microsoft Teams, LINE, Feishu/Lark, and more).&lt;/p&gt;

&lt;p&gt;Each of these platforms has a wildly different message format, media handling, authentication model, and rate limiting strategy. OpenClaw normalizes all of this through channel adapter interfaces defined in its &lt;code&gt;plugin-sdk&lt;/code&gt; (including &lt;code&gt;ChannelMessagingAdapter&lt;/code&gt;, &lt;code&gt;ChannelGatewayAdapter&lt;/code&gt;, and &lt;code&gt;ChannelAuthAdapter&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Conceptually, the pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified illustration of the adapter pattern (not actual OpenClaw code)&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ChannelAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UnifiedMessage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UnifiedMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;UnifiedMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;text&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;media&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;MediaAttachment&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;replyTo&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key design decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Adapters are stateless.&lt;/strong&gt; Connection state lives in the Gateway, not in individual adapters. This means you can restart an adapter without losing session context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media is normalized.&lt;/strong&gt; Images, audio, and documents all get the same treatment regardless of source platform. The agent does not need to know whether a photo came from WhatsApp or Telegram.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform-specific features use a metadata bag.&lt;/strong&gt; Reactions, threads, typing indicators, and read receipts flow through &lt;code&gt;metadata&lt;/code&gt;. The core agent logic never touches platform-specific fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fault isolation.&lt;/strong&gt; Each adapter starts independently. If the WhatsApp connection fails, Telegram keeps running. One failing channel does not take down the Gateway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Takeaway for builders:&lt;/strong&gt; If your agent integrates with even two platforms, build a normalization layer early. The unified message format is the contract between your integration layer and your intelligence layer. Without it, platform-specific logic leaks into your agent's core and becomes impossible to untangle later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills: Prompt Engineering as the Extension Mechanism
&lt;/h2&gt;

&lt;p&gt;OpenClaw's capabilities are modular plugins called &lt;strong&gt;skills&lt;/strong&gt;, but they are not what you might expect. Skills are not TypeScript modules or Python packages. They are folders containing a &lt;code&gt;SKILL.md&lt;/code&gt; file, a markdown document with YAML frontmatter.&lt;/p&gt;

&lt;p&gt;This is the same format we covered in our &lt;a href="https://blog.agentailor.com/blog/how-to-build-and-deploy-agent-skill-from-scratch" rel="noopener noreferrer"&gt;previous article on building agent skills&lt;/a&gt;. OpenClaw was one of the first large projects to adopt this pattern at scale.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6bp5ms4rtnwc2vxg4aq1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6bp5ms4rtnwc2vxg4aq1.png" alt="Skills Lifecycle" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;
  &lt;em&gt;The skills lifecycle: discovery, activation, execution, and self-authoring.&lt;/em&gt;
&lt;/center&gt;

&lt;h3&gt;
  
  
  How Skills Work
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;On startup, the agent reads skill names and descriptions, roughly 97 characters per skill. This is the progressive disclosure pattern: keep initial context lean.&lt;/li&gt;
&lt;li&gt;When a user request matches a skill's description, the full skill content is injected into the agent's context as markdown.&lt;/li&gt;
&lt;li&gt;Skills can reference local files (scripts, templates, reference data).&lt;/li&gt;
&lt;li&gt;Skills are hot-reloadable. Edit the file, and the agent picks it up on the next turn (configurable debounce of 250ms).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Self-Writing Agent
&lt;/h3&gt;

&lt;p&gt;The feature that captured the most attention: the agent can create and edit its own &lt;code&gt;SKILL.md&lt;/code&gt; files. It observes patterns in how the user asks for things, identifies repetitive workflows, and writes a skill to handle them better next time.&lt;/p&gt;

&lt;p&gt;The agent stores self-authored skills in the per-agent workspace (&lt;code&gt;&amp;lt;workspace&amp;gt;/skills/&lt;/code&gt;) or the shared &lt;code&gt;~/.openclaw/skills/&lt;/code&gt; directory. These skills persist across sessions and survive restarts.&lt;/p&gt;

&lt;h3&gt;
  
  
  ClawHub Marketplace
&lt;/h3&gt;

&lt;p&gt;OpenClaw has a community skill registry called ClawHub, where users share and discover skills via CLI commands. The agent can even auto-search for and install skills at runtime based on user intent.&lt;/p&gt;

&lt;p&gt;This extensibility is powerful, and also where the security story gets interesting (more on that later).&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Markdown Over Code?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Approach&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Skills (Markdown)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Plugins (Code)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Extension language&lt;/td&gt;
&lt;td&gt;Natural language + YAML&lt;/td&gt;
&lt;td&gt;TypeScript / Python&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who can author&lt;/td&gt;
&lt;td&gt;Anyone (including the agent)&lt;/td&gt;
&lt;td&gt;Developers only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security surface&lt;/td&gt;
&lt;td&gt;Low (injected as context)&lt;/td&gt;
&lt;td&gt;High (arbitrary code execution)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hot reload&lt;/td&gt;
&lt;td&gt;Trivial (re-read the file)&lt;/td&gt;
&lt;td&gt;Requires restart or dynamic import&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debuggability&lt;/td&gt;
&lt;td&gt;Read the file, read the prompt&lt;/td&gt;
&lt;td&gt;Stack traces, runtime errors&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The markdown-first approach has a key advantage: the barrier to creating skills is effectively zero. Users who cannot write code can still teach their agent new workflows. And the agent itself can participate in the skill ecosystem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; For a hands-on guide to building skills in this format, see our &lt;a href="https://blog.agentailor.com/blog/how-to-build-and-deploy-agent-skill-from-scratch" rel="noopener noreferrer"&gt;How to Build and Deploy an Agent Skill from Scratch&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Memory Architecture: Files You Can Read
&lt;/h2&gt;

&lt;p&gt;Most agent memory lives in a vector database that humans cannot inspect, edit, or debug. When the agent remembers something wrong, you have no practical way to fix it.&lt;/p&gt;

&lt;p&gt;OpenClaw takes a different approach. Memory is stored as flat files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Markdown files&lt;/strong&gt; for long-form notes and context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YAML files&lt;/strong&gt; for structured data (user preferences, configurations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSONL files&lt;/strong&gt; for conversation history (one line per message, append-only)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything lives under &lt;code&gt;~/.openclaw/&lt;/code&gt; in a directory structure you can browse in your file manager.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hybrid Search
&lt;/h3&gt;

&lt;p&gt;Retrieval uses two complementary search strategies, both running locally in SQLite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vector similarity search&lt;/strong&gt; via &lt;code&gt;sqlite-vec&lt;/code&gt;, which finds semantically related content even when the wording differs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyword search&lt;/strong&gt; via FTS5 for precise matches on exact technical terms, names, and identifiers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hybrid search consistently outperforms either strategy alone. Vector search introduces semantic noise on precise queries. Keyword search misses paraphrased content. Combining them gives you the best of both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smart Sync
&lt;/h3&gt;

&lt;p&gt;When the agent writes to a memory file, a file monitor automatically triggers an index update for both vector embeddings and the full-text index. New "experiences" are immediately available for the next prompt. No manual reindexing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Flat Files Matter
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You can &lt;code&gt;git diff&lt;/code&gt; your agent's memory. Version control for agent state.&lt;/li&gt;
&lt;li&gt;You can edit memory in VS Code. Wrong fact? Fix it directly.&lt;/li&gt;
&lt;li&gt;You can back up memory with standard file system tools. No database export needed.&lt;/li&gt;
&lt;li&gt;You can review what your agent "knows" without building a custom admin UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Semantic Snapshots for Browser Automation
&lt;/h3&gt;

&lt;p&gt;When OpenClaw automates a browser, it does not rely on screenshots. Instead, it parses the &lt;strong&gt;accessibility tree&lt;/strong&gt;, a structured text representation of the page content. This "Semantic Snapshot" approach is cheaper in tokens, faster to process, and more accurate for LLM reasoning than pixel data.&lt;/p&gt;

&lt;p&gt;Accessibility trees give the model structured information about buttons, links, form fields, and content hierarchy. A screenshot gives it pixels. For most agent tasks, the structured data wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Heartbeat: Proactive Agents
&lt;/h2&gt;

&lt;p&gt;Most agents sit idle until a user sends a message. OpenClaw has a different model.&lt;/p&gt;

&lt;p&gt;The Gateway runs as a background daemon with a configurable &lt;strong&gt;heartbeat interval&lt;/strong&gt; (30 minutes by default). On each tick, the agent reads a &lt;code&gt;HEARTBEAT.md&lt;/code&gt; checklist in the workspace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; Check for new emails and summarize anything urgent
&lt;span class="p"&gt;-&lt;/span&gt; Review today's calendar for upcoming meetings
&lt;span class="p"&gt;-&lt;/span&gt; Run daily expense summary if it's after 6 PM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent processes each item and can send proactive messages to the user via any connected channel. If nothing requires attention, it responds with &lt;code&gt;HEARTBEAT_OK&lt;/code&gt; and goes back to sleep.&lt;/p&gt;

&lt;p&gt;This is a simple but powerful pattern: a cron job for your AI agent, configured in plain text. No scheduling framework, no database of recurring tasks. Just a markdown file the user can edit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway for builders:&lt;/strong&gt; If your agent should be proactive, implement a heartbeat. A markdown checklist is all you need to start. It is far simpler than building a full scheduling system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nodes: The Companion Device Model
&lt;/h2&gt;

&lt;p&gt;OpenClaw "nodes" are native apps on iOS, macOS, and Android that connect to the central Gateway via WebSocket. They act as peripherals: the phone node can take photos and read notifications, while the macOS node can interact with desktop apps and record the screen.&lt;/p&gt;

&lt;p&gt;Nodes register through a pairing protocol (&lt;code&gt;node.pair.request&lt;/code&gt; → &lt;code&gt;node.pair.approve&lt;/code&gt;) and expose capabilities through a standardized &lt;code&gt;node.invoke&lt;/code&gt; interface. The model never communicates directly with nodes. It talks to the Gateway, which forwards calls to the appropriate device.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Capability&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;macOS&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;iOS&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Android&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Headless&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WebView (canvas)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Camera&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shell commands&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMS sending&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screen recording&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Location&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Takeaway for builders:&lt;/strong&gt; If your agent needs device-specific capabilities, a lightweight WebSocket peripheral model is cleaner than trying to run everything on the server. The key is keeping nodes dumb: they execute commands, they do not run agent logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security: The Cautionary Tale
&lt;/h2&gt;

&lt;p&gt;OpenClaw's architecture is innovative. Its security track record is a cautionary tale. These lessons are essential for anyone building agent systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-25253: One-Click Remote Code Execution
&lt;/h3&gt;

&lt;p&gt;The most critical vulnerability, patched in v2026.1.29:&lt;/p&gt;

&lt;p&gt;The Gateway's Control UI trusted the &lt;code&gt;gatewayUrl&lt;/code&gt; from the query string without validation and auto-connected on load, sending the stored authentication token in the WebSocket payload. A crafted link could redirect this token to an attacker-controlled server.&lt;/p&gt;

&lt;p&gt;The root cause was deeper: the WebSocket server did not validate the &lt;code&gt;Origin&lt;/code&gt; header. Any website could connect to a running OpenClaw instance. With the token, an attacker could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect to the victim's local Gateway&lt;/li&gt;
&lt;li&gt;Modify configuration (disable sandbox, weaken tool policies)&lt;/li&gt;
&lt;li&gt;Invoke privileged actions using &lt;code&gt;operator.admin&lt;/code&gt; and &lt;code&gt;operator.approvals&lt;/code&gt; scopes&lt;/li&gt;
&lt;li&gt;Run arbitrary commands on the host machine — full remote code execution&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Lesson for builders:&lt;/strong&gt; If your agent exposes any network interface — HTTP, WebSocket, gRPC — origin validation and authentication are not optional. A local-first agent is only safe if the network boundary is actually enforced. In the single-process Gateway model, one WebSocket vulnerability compromises everything because there is no isolation boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  ClawHub Supply Chain Attacks
&lt;/h3&gt;

&lt;p&gt;ClawHub, OpenClaw's skill marketplace, became a major attack vector:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security audits found that &lt;strong&gt;12-20% of uploaded skills contained malicious instructions&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A campaign called &lt;strong&gt;ClawHavoc&lt;/strong&gt; distributed macOS malware through skills with professional documentation and names like &lt;code&gt;solana-wallet-tracker&lt;/code&gt; and &lt;code&gt;youtube-summarize-pro&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The #1 ranked community skill silently exfiltrated data and used direct prompt injection to bypass safety guidelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The attack vector is subtle: skills are markdown injected into agent context. A malicious skill can instruct the agent to exfiltrate data, modify other skills, or execute harmful commands. This is &lt;strong&gt;prompt injection via the extension ecosystem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;ClawHub's publishing requirement was minimal: a 1-week-old GitHub account. No code review, no content scanning, no sandboxing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson for builders:&lt;/strong&gt; If your agent loads third-party skills or prompts, treat them as untrusted input. "It is just markdown" does not mean "it is safe." Sandboxing, review processes, and automated content scanning are necessary for any public skill registry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plaintext Credential Storage
&lt;/h3&gt;

&lt;p&gt;Connected account credentials (WhatsApp sessions, API keys for Anthropic/OpenAI, Telegram bot tokens, Discord OAuth tokens) are stored as plaintext files under &lt;code&gt;~/.openclaw/&lt;/code&gt;. Known malware families are already building capabilities to harvest these file structures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson for builders:&lt;/strong&gt; Use your platform's secret storage (macOS Keychain, Windows Credential Manager, Linux secret-service). Never store credentials in plaintext, even for local-first applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Vulnerability&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Root Cause&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Lesson&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-25253 (RCE)&lt;/td&gt;
&lt;td&gt;Missing WebSocket origin validation&lt;/td&gt;
&lt;td&gt;Always authenticate network interfaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClawHub malicious skills&lt;/td&gt;
&lt;td&gt;No skill content scanning&lt;/td&gt;
&lt;td&gt;Treat third-party prompts as untrusted input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plaintext credentials&lt;/td&gt;
&lt;td&gt;No OS secret store integration&lt;/td&gt;
&lt;td&gt;Use platform-native credential storage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; These issues do not diminish OpenClaw's architectural innovations. They highlight that security for AI agents requires the same rigor as any networked application, and that agent-specific attack vectors (prompt injection via skills) require new defensive patterns.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What to Take Away: A Builder's Checklist
&lt;/h2&gt;

&lt;p&gt;Here are the concrete patterns from OpenClaw's architecture worth adopting in your own agent systems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Per-session serial queues.&lt;/strong&gt; Default to serial execution within a session. Opt into parallelism only when provably safe. This prevents an entire class of race condition bugs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Structured session keys.&lt;/strong&gt; Scope isolation with &lt;code&gt;workspace:channel:userId&lt;/code&gt;, not just a user ID. This prevents cross-context data leaks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Channel adapter pattern.&lt;/strong&gt; If you integrate with more than one platform, normalize messages before they reach your agent logic. Do this early.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Skills as markdown.&lt;/strong&gt; For extensibility, markdown-first beats code-first. Lower friction, agent-authorable, hot-reloadable, debuggable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Progressive skill disclosure.&lt;/strong&gt; Load skill names and descriptions upfront (low tokens). Load full skill content only when activated. Keep your base context lean.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Human-readable memory.&lt;/strong&gt; Store agent state in formats you can inspect and edit: Markdown, YAML, JSONL. The debugging advantage is worth the trade-off versus opaque vector stores.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hybrid search.&lt;/strong&gt; Combine vector similarity with keyword search. Use SQLite (&lt;code&gt;sqlite-vec&lt;/code&gt; + FTS5) if you want to stay local with no external dependencies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accessibility trees over screenshots.&lt;/strong&gt; For browser automation, parse the accessibility tree. It is cheaper, faster, and more accurate for LLM reasoning.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Heartbeat pattern.&lt;/strong&gt; For proactive agents, a simple cron + checklist file is enough. You do not need a complex scheduling system.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Authenticate everything.&lt;/strong&gt; WebSocket, HTTP, local sockets. If it accepts connections, it needs origin validation and authentication. Local-first does not mean security-optional.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;OpenClaw is not a framework you adopt — it is an architecture you study.&lt;/p&gt;

&lt;p&gt;The project's core insight is that a personal AI agent is fundamentally a &lt;strong&gt;gateway problem&lt;/strong&gt;, not a model problem. Getting the runtime right, queuing, channel normalization, memory, extensibility — matters more than which LLM you use.&lt;/p&gt;

&lt;p&gt;The Lane Queue and Skills system are the two patterns with the broadest applicability. If you take nothing else from this article, implement per-session serial execution and consider markdown-based extensibility for your agents.&lt;/p&gt;

&lt;p&gt;The security story is equally important. Agent systems have a larger attack surface than traditional applications because they accept natural language input from multiple untrusted sources, including their own extension ecosystems. Build with that in mind.&lt;/p&gt;

&lt;p&gt;Build agents that are reliable before they are clever. OpenClaw got that part right.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Enjoying content like this? Sign up for &lt;a href="https://www.linkedin.com/newsletters/agent-briefings-7391777936955310080/" rel="noopener noreferrer"&gt;Agent Briefings&lt;/a&gt;, where I share insights and news on building and scaling AI agents.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/openclaw/openclaw" rel="noopener noreferrer"&gt;OpenClaw GitHub Repository&lt;/a&gt;: Source code and documentation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.openclaw.ai/" rel="noopener noreferrer"&gt;OpenClaw Official Documentation&lt;/a&gt;: Architecture guides and API reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://openclaw.ai/" rel="noopener noreferrer"&gt;OpenClaw Official Website&lt;/a&gt;: Project overview and getting started&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-25253" rel="noopener noreferrer"&gt;CVE-2026-25253 Details&lt;/a&gt;: Full vulnerability disclosure&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://agentskills.io" rel="noopener noreferrer"&gt;Agent Skills Specification&lt;/a&gt;: The skills format OpenClaw uses at scale&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Related Articles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://blog.agentailor.com/blog/how-to-build-and-deploy-agent-skill-from-scratch" rel="noopener noreferrer"&gt;How to Build and Deploy an Agent Skill from Scratch&lt;/a&gt;: Build your own skills using the format OpenClaw adopted&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.agentailor.com/blog/message-history-summarization-strategies" rel="noopener noreferrer"&gt;Don't Let Your AI Agent Forget: Smarter Strategies for Summarizing Message History&lt;/a&gt;: Memory management patterns that complement OpenClaw's approach&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.agentailor.com/blog/writing-tools-for-ai-agents" rel="noopener noreferrer"&gt;Writing Effective Tools for AI Agents: Production Lessons from Anthropic&lt;/a&gt;: Tool design principles for the capabilities layer&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Build and Deploy an Agent Skill from Scratch</title>
      <dc:creator>Ali Ibrahim</dc:creator>
      <pubDate>Mon, 16 Feb 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/ialijr/how-to-build-and-deploy-an-agent-skill-from-scratch-53po</link>
      <guid>https://dev.to/ialijr/how-to-build-and-deploy-an-agent-skill-from-scratch-53po</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;AI agents are increasingly capable, but they often lack the specialized knowledge needed for real work. Your agent might write code, but does it know your team's deployment process? It can analyze data, but does it understand your company's reporting standards?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent Skills&lt;/strong&gt; solve this by packaging domain expertise, workflows, and context into portable folders that agents can discover and use on demand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll build:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this guide, you'll create a skill that teaches AI agents how to generate beautiful financial reports with charts and insights—building on Cameron AI, the personal finance assistant from our previous articles on &lt;a href="https://blog.agentailor.com/blog/the-art-of-agent-prompting" rel="noopener noreferrer"&gt;agent prompting&lt;/a&gt; and &lt;a href="https://blog.agentailor.com/blog/writing-tools-for-ai-agents" rel="noopener noreferrer"&gt;tool design&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is an overview of the report your agent will generate with this skill:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2q5xuq6sc7tcf5qtu5t2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2q5xuq6sc7tcf5qtu5t2.png" alt="Cameron Expense Report" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;
  &lt;em&gt;Example report generated by the Cameron Expense Reporter skill.&lt;/em&gt;
&lt;/center&gt;

&lt;h2&gt;
  
  
  What Are Agent Skills?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://agentskills.io" rel="noopener noreferrer"&gt;Agent Skills&lt;/a&gt; are folders containing instructions, scripts, and resources that AI tools can discover and use. Think of them as training documents for your AI assistant, except they load automatically when relevant.&lt;/p&gt;

&lt;p&gt;The key insight is &lt;strong&gt;progressive disclosure&lt;/strong&gt;. When your agent starts, it only loads the name and description of each skill—roughly 100 tokens per skill. This means you can have dozens of skills available without bloating your context window. Your agent stays fast and focused, pulling in detailed guidance only when needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skills vs MCP: Complementary, Not Competing
&lt;/h3&gt;

&lt;p&gt;If you've worked with MCP servers, you might wonder how skills fit in. Here's the distinction:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Agent Skills&lt;/th&gt;
&lt;th&gt;MCP Servers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Teach workflows and knowledge&lt;/td&gt;
&lt;td&gt;Provide tools and capabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single SKILL.md file&lt;/td&gt;
&lt;td&gt;Server code (TypeScript, Python)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"How to write agent prompts"&lt;/td&gt;
&lt;td&gt;"Fetch URL", "Query database"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Skills provide the &lt;em&gt;knowledge&lt;/em&gt;; MCP provides the &lt;em&gt;capabilities&lt;/em&gt;. A skill might instruct an agent to "fetch the API documentation and analyze it", while an MCP server provides the actual &lt;code&gt;fetch&lt;/code&gt; tool.&lt;/p&gt;

&lt;p&gt;For a deeper dive, see the &lt;a href="https://agentskills.io" rel="noopener noreferrer"&gt;official Agent Skills specification&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding SKILL.md Format
&lt;/h2&gt;

&lt;p&gt;Every skill is a folder containing a &lt;code&gt;SKILL.md&lt;/code&gt; file. This file has two parts: YAML frontmatter and markdown instructions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy8lmhudbeozbpwfbgaee.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy8lmhudbeozbpwfbgaee.png" alt="Agent Skill format" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;
  &lt;em&gt;Image credit: Anthropic&lt;/em&gt;
&lt;/center&gt;

&lt;h3&gt;
  
  
  Required Fields
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Max Length&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;64 chars&lt;/td&gt;
&lt;td&gt;Lowercase letters, numbers, and hyphens only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1024 chars&lt;/td&gt;
&lt;td&gt;When and why to use this skill&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;description&lt;/code&gt; is crucial, it's what the AI uses to decide when to activate your skill. Include specific keywords that match how users naturally ask for help.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The specification supports additional optional fields (&lt;code&gt;license&lt;/code&gt;, &lt;code&gt;compatibility&lt;/code&gt;, &lt;code&gt;allowed-tools&lt;/code&gt;, etc.). See the &lt;a href="https://agentskills.io" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt; for the complete list.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Building the Cameron Expense Reporter Skill
&lt;/h2&gt;

&lt;p&gt;Let's build a skill for Cameron AI, a personal finance assistant that helps users manage budgets and track expenses.&lt;/p&gt;

&lt;p&gt;For simplicity, we'll suppose that Cameron has file system access via MCP and can execute JavaScript in a sandboxed environment—the infrastructure needed to discover and run skills.&lt;/p&gt;

&lt;p&gt;The skill we're building will teach Cameron how to transform raw expense data into professional visualizations with Chart.js.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context:&lt;/strong&gt; If you're new to Cameron AI, check out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://blog.agentailor.com/blog/the-art-of-agent-prompting" rel="noopener noreferrer"&gt;The Art of Agent Prompting&lt;/a&gt;: How Cameron's prompts are designed&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.agentailor.com/blog/writing-tools-for-ai-agents" rel="noopener noreferrer"&gt;Writing Tools for AI Agents&lt;/a&gt;: How Cameron's &lt;code&gt;cameron_get_expenses&lt;/code&gt; tool works&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What you'll build:&lt;/strong&gt; A complete skill with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;6-step visualization workflow (understand request → gather data → format → generate chart → add insights → compose report)&lt;/li&gt;
&lt;li&gt;Helper scripts for Chart.js configuration and data formatting&lt;/li&gt;
&lt;li&gt;Reference guides for chart type selection&lt;/li&gt;
&lt;li&gt;HTML template for professional reports&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full implementation:&lt;/strong&gt; The complete skill is available on &lt;a href="https://github.com/IBJunior/cameron-expense-reporter" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. We'll show function signatures and key patterns here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 1: Create the Skill Directory
&lt;/h3&gt;

&lt;p&gt;Create a folder structure for your skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; my-skills/cameron-expense-reporter
&lt;span class="nb"&gt;cd &lt;/span&gt;my-skills/cameron-expense-reporter

&lt;span class="c"&gt;# Create subdirectories for supporting files&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; scripts references assets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This skill will use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;scripts/&lt;/strong&gt;: Reusable JavaScript utilities for charts and data formatting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;references/&lt;/strong&gt;: Decision guides and complete examples&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;assets/&lt;/strong&gt;: HTML templates for professional output&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Write the Frontmatter
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;SKILL.md&lt;/code&gt; with frontmatter that clearly describes when to activate this skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cameron-expense-reporter&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate financial reports with charts using Chart.js. Use when users ask to visualize spending, show trends, create expense charts, analyze spending patterns, compare categories, track budget progress, or generate financial reports. Handles chart type selection (bar/line/pie), data formatting (currency, dates, aggregation), Chart.js configuration, and insight generation. Works with expense data from tools like cameron_get_expenses or similar financial data sources.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key decisions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;name:&lt;/strong&gt; Scoped to Cameron's domain with clear purpose (we can also just use expense-reporter)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;description:&lt;/strong&gt; Includes natural trigger phrases users would say ("visualize spending", "show trends", "expense charts")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The description is critical, it determines when the AI loads this skill. Include synonyms and common phrasings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Define the Workflow
&lt;/h3&gt;

&lt;p&gt;After frontmatter, add the skill title and a 6-step workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Cameron Expense Reporter&lt;/span&gt;

Generate beautiful, insightful financial reports with charts and written analysis.

&lt;span class="gu"&gt;## Workflow&lt;/span&gt;

&lt;span class="gu"&gt;### Step 1: Understand the Request&lt;/span&gt;

Identify the user's intent to select the appropriate visualization:
&lt;span class="p"&gt;
-&lt;/span&gt; Category comparison → Bar chart
&lt;span class="p"&gt;-&lt;/span&gt; Time-series trends → Line chart
&lt;span class="p"&gt;-&lt;/span&gt; Distribution/proportions → Pie chart
&lt;span class="p"&gt;-&lt;/span&gt; Budget tracking → Line chart with budget reference line

&lt;span class="gu"&gt;### Step 2: Gather Expense Data&lt;/span&gt;

Use the &lt;span class="sb"&gt;`cameron_get_expenses`&lt;/span&gt; tool to retrieve relevant data...

&lt;span class="gu"&gt;### Step 3: Prepare and Format Data&lt;/span&gt;

Use helper scripts for data transformation...

&lt;span class="gu"&gt;### Step 4: Generate Chart Configuration&lt;/span&gt;

Use helper scripts to create Chart.js configurations...

&lt;span class="gu"&gt;### Step 5: Generate Insights&lt;/span&gt;

Provide written analysis alongside the chart...

&lt;span class="gu"&gt;### Step 6: Compose the Report&lt;/span&gt;

Generate HTML, markdown, or inline visualization...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why 6 steps:&lt;/strong&gt; Each represents a distinct decision point in the visualization workflow. This guides the AI through: intent recognition → data retrieval → transformation → rendering → analysis → output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Add Supporting Scripts
&lt;/h3&gt;

&lt;p&gt;For complex logic, extract reusable functions into scripts. Create two JavaScript utilities:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;scripts/format_financial_data.js&lt;/strong&gt; — Data transformation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Aggregate expenses by category
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;aggregateByCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expense&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;expense&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Other&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;expense&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Prepare expense data for visualization
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;prepareChartData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aggregationType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sortByValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;aggregated&lt;/span&gt;

  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aggregationType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;aggregated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;aggregateByCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;month&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;aggregated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;aggregateByMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;week&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;aggregated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;aggregateByWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sortByValue&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;aggregationType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aggregated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sortByAmount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aggregated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;toChartFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aggregated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Also includes: aggregateByMonth, aggregateByWeek, formatCurrency,&lt;/span&gt;
&lt;span class="c1"&gt;// calculatePercentageChange, formatDateRange, toChartFormat, sortByAmount&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;scripts/generate_chart_config.js&lt;/strong&gt; — Chart.js configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Generate a bar chart configuration for categorical spending data
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateBarChart&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;datasets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Spending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(59, 130, 246, 0.8)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;borderColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(59, 130, 246, 1)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;borderWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;responsive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;maintainAspectRatio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bold&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;tooltip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;scales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;beginAtZero&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;ticks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Also includes: generateLineChart, generatePieChart&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why scripts:&lt;/strong&gt; Instead of inlining complex logic in SKILL.md, extract to reusable functions the AI can read and execute.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full implementations:&lt;/strong&gt; See the &lt;a href="https://github.com/IBJunior/cameron-expense-reporter" rel="noopener noreferrer"&gt;complete skill on GitHub&lt;/a&gt; for all utility functions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 5: Add Reference Guides
&lt;/h3&gt;

&lt;p&gt;For decision logic, use reference documents the AI loads on-demand:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;references/chart-types-guide.md&lt;/strong&gt; — Chart selection decision tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Decision Tree&lt;/span&gt;

&lt;span class="gu"&gt;### Categorical Comparison ("Which category did I spend most on?")&lt;/span&gt;

&lt;span class="gs"&gt;**Use: Bar Chart**&lt;/span&gt;

Best for:
&lt;span class="p"&gt;
-&lt;/span&gt; Comparing spending across categories
&lt;span class="p"&gt;-&lt;/span&gt; Showing top spending categories

Example queries:
&lt;span class="p"&gt;
-&lt;/span&gt; "What are my biggest expenses?"
&lt;span class="p"&gt;-&lt;/span&gt; "Show spending by category"

&lt;span class="gu"&gt;### Time-Series Trends ("How is my spending changing?")&lt;/span&gt;

&lt;span class="gs"&gt;**Use: Line Chart**&lt;/span&gt;

Best for:
&lt;span class="p"&gt;
-&lt;/span&gt; Showing spending trends over weeks/months
&lt;span class="p"&gt;-&lt;/span&gt; Comparing to budget over time

Example queries:
&lt;span class="p"&gt;
-&lt;/span&gt; "Show my spending trends over the year"
&lt;span class="p"&gt;-&lt;/span&gt; "How has my spending changed?"

&lt;span class="gu"&gt;### Distribution/Proportions ("Where does my money go?")&lt;/span&gt;

&lt;span class="gs"&gt;**Use: Pie Chart**&lt;/span&gt;

Best for:
&lt;span class="p"&gt;
-&lt;/span&gt; Showing percentage breakdown
&lt;span class="p"&gt;-&lt;/span&gt; Understanding spending distribution

Example queries:
&lt;span class="p"&gt;
-&lt;/span&gt; "Where does my money go?"
&lt;span class="p"&gt;-&lt;/span&gt; "What percentage do I spend on dining?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why references:&lt;/strong&gt; Separates decision-making guidance from the main workflow. The SKILL.md references this guide: &lt;code&gt;"For chart selection, see [references/chart-types-guide.md]"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The skill also includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;references/chartjs-examples.md&lt;/strong&gt;: Complete HTML examples for each chart type&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;assets/report-template.html&lt;/strong&gt;: Professional HTML template with placeholders&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 6: Reference Supporting Files in SKILL.md
&lt;/h3&gt;

&lt;p&gt;In your workflow, tell the AI when to use these resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### Step 1: Understand the Request&lt;/span&gt;

Identify the user's intent to select the appropriate visualization.

For detailed chart selection guidance, see &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;references/chart-types-guide.md&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;references/chart-types-guide.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;.

&lt;span class="gu"&gt;### Step 2: Gather Expense Data&lt;/span&gt;

Use the &lt;span class="sb"&gt;`cameron_get_expenses`&lt;/span&gt; tool (or equivalent) to retrieve relevant data:

&lt;span class="gu"&gt;### Step 3: Prepare and Format Data&lt;/span&gt;

&lt;span class="gs"&gt;**Load the formatting utilities:**&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Read and use scripts/format_financial_data.js --&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;### Step 4: Generate Chart Configuration&lt;/span&gt;

&lt;span class="gs"&gt;**Load the chart generator:**&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Read and use scripts/generate_chart_config.js --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Progressive disclosure:&lt;/strong&gt; Reference files are loaded &lt;strong&gt;on-demand&lt;/strong&gt; by the AI when needed. You don't inline everything in SKILL.md.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Add Best Practices to SKILL.md
&lt;/h3&gt;

&lt;p&gt;Include guidance on edge cases and performance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Best Practices&lt;/span&gt;

&lt;span class="gs"&gt;**Data preparation:**&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Always validate date ranges before querying
&lt;span class="p"&gt;-&lt;/span&gt; Handle empty results gracefully
&lt;span class="p"&gt;-&lt;/span&gt; Round currency to 2 decimal places for display

&lt;span class="gs"&gt;**Chart configuration:**&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Use consistent color scheme (blue primary, red for warnings)
&lt;span class="p"&gt;-&lt;/span&gt; Set responsive: true for all charts
&lt;span class="p"&gt;-&lt;/span&gt; Format currency in tooltips and axis ticks

&lt;span class="gs"&gt;**Insight generation:**&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Lead with the most important finding
&lt;span class="p"&gt;-&lt;/span&gt; Use concrete numbers, not vague language ("Dining increased 15%" not "You spent more on dining")
&lt;span class="p"&gt;-&lt;/span&gt; Provide context (percentages, comparisons)

&lt;span class="gs"&gt;**Performance:**&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; For large datasets (&amp;gt;1000 expenses), aggregate before visualizing
&lt;span class="p"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`response_format: 'concise'`&lt;/span&gt; when fetching chart data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 8: Validate the Complete Skill
&lt;/h3&gt;

&lt;p&gt;Verify your skill has all components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cameron-expense-reporter/
├── SKILL.md                          ✓ Main instructions with 6-step workflow
├── scripts/
│   ├── generate_chart_config.js      ✓ Bar, line, pie chart generators
│   └── format_financial_data.js      ✓ Aggregation and formatting utilities
├── references/
│   ├── chart-types-guide.md          ✓ Chart selection decision tree
│   └── chartjs-examples.md           ✓ Complete working examples
└── assets/
    └── report-template.html          ✓ HTML template for reports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Validation checklist:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] SKILL.md has frontmatter with clear trigger phrases&lt;/li&gt;
&lt;li&gt;[ ] Workflow is step-by-step and actionable&lt;/li&gt;
&lt;li&gt;[ ] Supporting scripts have clear function signatures&lt;/li&gt;
&lt;li&gt;[ ] References are linked from main SKILL.md with relative paths&lt;/li&gt;
&lt;li&gt;[ ] All examples use consistent patterns (Chart.js v4, Tailwind colors)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Get the complete skill:&lt;/strong&gt; &lt;a href="https://github.com/IBJunior/cameron-expense-reporter" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Testing Your Skill
&lt;/h2&gt;

&lt;p&gt;Now let's install and test the skill.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install in Claude
&lt;/h3&gt;

&lt;p&gt;The quickest way to test is with &lt;a href="https://claude.ai" rel="noopener noreferrer"&gt;Claude.ai&lt;/a&gt; or Claude Desktop:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you are using the final Github repo skill, make sure to remove the expenses.csv file from the root of the skill folder before zipping and uploading to Claude.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Zip your skill folder: &lt;code&gt;zip -r cameron-expense-reporter.zip cameron-expense-reporter/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings &amp;gt; Capabilities&lt;/strong&gt; and ensure "Code execution and file creation" is enabled&lt;/li&gt;
&lt;li&gt;Scroll to the &lt;strong&gt;Skills&lt;/strong&gt; section and click &lt;strong&gt;Upload skill&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Upload your ZIP file&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy0pnbnr4jx43w21lz2t6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy0pnbnr4jx43w21lz2t6.png" alt="Claude ai skills setting" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;
  &lt;em&gt;Upload your skill in Claude's settings.&lt;/em&gt;
&lt;/center&gt;

&lt;p&gt;For details, see &lt;a href="https://support.claude.com/en/articles/12512180-using-skills-in-claude" rel="noopener noreferrer"&gt;Using Skills in Claude&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Other tools:&lt;/strong&gt; You're not limited to Claude, any skills-compatible agent works. For example, in Cursor, place your skill folder in &lt;code&gt;.cursor/skills/&lt;/code&gt;. You can also use &lt;a href="https://skills.sh" rel="noopener noreferrer"&gt;skills.sh&lt;/a&gt; to install skills across tools with a single command: &lt;code&gt;npx skills add &amp;lt;owner/repo&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Test with Sample Data
&lt;/h3&gt;

&lt;p&gt;Download the &lt;a href="https://github.com/IBJunior/cameron-expense-reporter/blob/master/cameron_expenses.csv" rel="noopener noreferrer"&gt;sample expenses CSV&lt;/a&gt; and try this prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Here are my expenses [attach the CSV file]. Can you create a report using the cameron-expense-reporter skill?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expected behavior:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7boo14p0kbtn5q9bvxog.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7boo14p0kbtn5q9bvxog.png" alt="Expenses report 1" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;
  &lt;em&gt;Claude generates a report with a bar chart comparing spending.&lt;/em&gt;
&lt;/center&gt;

&lt;ol&gt;
&lt;li&gt;Claude activates the cameron-expense-reporter skill&lt;/li&gt;
&lt;li&gt;Reads the CSV data&lt;/li&gt;
&lt;li&gt;Selects an appropriate chart type (bar chart for category comparison)&lt;/li&gt;
&lt;li&gt;Generates a Chart.js visualization with formatted data&lt;/li&gt;
&lt;li&gt;Provides written insights about spending patterns&lt;/li&gt;
&lt;li&gt;Outputs a complete HTML report&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxn2linv41tn1mcg716kh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxn2linv41tn1mcg716kh.png" alt="Expenses report 2" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;
  &lt;em&gt;Claude generates a Pie chart showing spending distribution.&lt;/em&gt;
&lt;/center&gt;

&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;p&gt;Now that you've built your first skill, here are some tips for creating effective skills:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it focused.&lt;/strong&gt; A skill should do one thing well. If your &lt;code&gt;SKILL.md&lt;/code&gt; exceeds 500 lines, consider splitting into multiple skills or using supporting files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Description keywords matter.&lt;/strong&gt; The &lt;code&gt;description&lt;/code&gt; field determines when the AI activates your skill. Include synonyms and common phrasings for your use case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use supporting files strategically.&lt;/strong&gt; Complex skills benefit from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;scripts/&lt;/strong&gt;: Reusable code the AI can execute (JavaScript, Python, shell scripts)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;references/&lt;/strong&gt;: Decision trees, documentation, examples (loaded on-demand)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;assets/&lt;/strong&gt;: Templates, images, static files (used in output, not loaded into context)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Show function signatures, not full implementations.&lt;/strong&gt; In SKILL.md, include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Good: Clear signature with purpose&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;aggregateByCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Avoid: Inline implementation&lt;/span&gt;
&lt;span class="c1"&gt;// Keep actual logic in scripts/ directory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Balance brevity with completeness.&lt;/strong&gt; The cameron-expense-reporter SKILL.md is ~290 lines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~100 lines: Workflow steps&lt;/li&gt;
&lt;li&gt;~100 lines: Best practices and common patterns&lt;/li&gt;
&lt;li&gt;~90 lines: Quick references and troubleshooting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Test with real scenarios.&lt;/strong&gt; Run your skill against actual tasks you face. Adjust the description and instructions based on what works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the Skill Creator
&lt;/h2&gt;

&lt;p&gt;We built the skill manually to understand each component, but you don't have to. Anthropic provides a &lt;a href="https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md" rel="noopener noreferrer"&gt;skill-creator skill&lt;/a&gt; that can generate or iterate on skills for you, just describe what you want and let the AI handle the structure and formatting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond the Tutorial
&lt;/h2&gt;

&lt;p&gt;The skill we built here is for learning purposes. Before shipping a skill to production or sharing it with other users, there are additional concerns to consider,especially if you're building custom agents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluation:&lt;/strong&gt; Test your skill across diverse prompts to see how reliably the agent activates it, follows the workflow, and produces correct output. A single successful test isn't enough, edge cases and ambiguous requests will reveal gaps in your instructions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context management:&lt;/strong&gt; Monitor how your agent's context grows when a skill is active. Loading full instructions plus reference files plus scripts can consume significant tokens. If your agent uses multiple skills in one session, context pressure becomes a real concern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure (custom agents):&lt;/strong&gt; Coding tools like Cursor and Claude handle skill discovery and code execution out of the box. For custom agents, you may need to evaluate whether your MCP file server and sandbox are robust enough—particularly around file access permissions, execution timeouts, and error handling when scripts fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You've just created a sophisticated skill that transforms Cameron AI into a financial visualization expert. The &lt;code&gt;SKILL.md&lt;/code&gt; file, along with its supporting scripts, reference guides, and templates—works across Cursor, Claude Code, and any custom agent with the right infrastructure.&lt;/p&gt;

&lt;p&gt;This is the power of open standards: write once, use everywhere, whether in coding assistants or custom agents you build.&lt;/p&gt;

&lt;p&gt;The skill you built demonstrates key patterns for production-ready agent skills:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Progressive disclosure:&lt;/strong&gt; Only load detailed references when needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusable scripts:&lt;/strong&gt; Extract complex logic into executable functions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision trees:&lt;/strong&gt; Reference guides for consistent choices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Professional output:&lt;/strong&gt; Templates for polished deliverables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What workflow does your agent repeat most often? That's your next skill.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Enjoying content like this? Sign up for &lt;a href="https://www.linkedin.com/newsletters/agent-briefings-7391777936955310080/" rel="noopener noreferrer"&gt;Agent Briefings&lt;/a&gt;, where I share insights and news on building and scaling AI agents.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://agentskills.io" rel="noopener noreferrer"&gt;Agent Skills Specification&lt;/a&gt; — Official open standard&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/skills" rel="noopener noreferrer"&gt;Example Skills&lt;/a&gt; — Official skill examples&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://skills.sh" rel="noopener noreferrer"&gt;Vercel Skills&lt;/a&gt; — Registry of popular skills&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Related Articles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://blog.agentailor.com/blog/the-art-of-agent-prompting" rel="noopener noreferrer"&gt;The Art of Agent Prompting: Anthropic's Playbook&lt;/a&gt; — Prompt design patterns behind Cameron AI&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.agentailor.com/blog/writing-tools-for-ai-agents" rel="noopener noreferrer"&gt;Writing Tools for AI Agents&lt;/a&gt; — How to design effective tools like &lt;code&gt;cameron_get_expenses&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.agentailor.com/blog/create-your-first-mcp-server-in-5-minutes" rel="noopener noreferrer"&gt;Create Your First MCP Server in 5 Minutes&lt;/a&gt; — Add tool capabilities to complement your skills&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>productivity</category>
      <category>agents</category>
    </item>
    <item>
      <title>Create Your First MCP Server in 5 Minutes with create-mcp-server</title>
      <dc:creator>Ali Ibrahim</dc:creator>
      <pubDate>Sun, 11 Jan 2026 18:41:31 +0000</pubDate>
      <link>https://dev.to/ialijr/create-your-first-mcp-server-in-5-minutes-with-create-mcp-server-fch</link>
      <guid>https://dev.to/ialijr/create-your-first-mcp-server-in-5-minutes-with-create-mcp-server-fch</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;You've seen AI assistants do impressive things: answer questions, write code, explain concepts. But have you noticed their limitation? They can't actually &lt;em&gt;do&lt;/em&gt; anything in the real world. They can't fetch live data, access your APIs, or interact with external systems.&lt;/p&gt;

&lt;p&gt;That's where MCP comes in.&lt;/p&gt;

&lt;p&gt;In this guide, you'll build a &lt;strong&gt;Fetch MCP Server&lt;/strong&gt;, a tool that gives AI assistants (LLMs) the ability to fetch web pages and read their content. By the end, you'll have a production-ready server running locally, ready to connect to VS Code, Cursor, or any MCP-compatible client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll learn:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What MCP is and why it matters&lt;/li&gt;
&lt;li&gt;How to scaffold an MCP server in seconds with &lt;code&gt;create-mcp-server&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;How to implement a real-world tool (fetching URLs)&lt;/li&gt;
&lt;li&gt;How to test your server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 or later&lt;/li&gt;
&lt;li&gt;Basic TypeScript familiarity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's build something useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is MCP?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;MCP (Model Context Protocol)&lt;/strong&gt; is an open standard that lets AI Models (LLMs) discover and use external tools. Think of it as &lt;strong&gt;USB for AI&lt;/strong&gt;: a universal connector that lets AI models plug into any tool or service you create.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;AI Models (LLMs) are powerful but isolated. They're trained on static data and can't access:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time information from the web&lt;/li&gt;
&lt;li&gt;Your company's internal APIs&lt;/li&gt;
&lt;li&gt;Databases, file systems, or external services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every time you need the AI to interact with the outside world, you're stuck copying and pasting data manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution
&lt;/h3&gt;

&lt;p&gt;MCP provides a standardized way for AI Models (LLMs) to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discover&lt;/strong&gt; what tools are available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understand&lt;/strong&gt; how to use them (parameters, descriptions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute&lt;/strong&gt; them safely with user oversight&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Three Core Concepts
&lt;/h3&gt;

&lt;p&gt;MCP servers expose three types of capabilities:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Actions the AI can perform&lt;/td&gt;
&lt;td&gt;Fetch a URL, send an email, query a database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Data the AI can read&lt;/td&gt;
&lt;td&gt;Files, database records, API responses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prompts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pre-defined templates for common tasks&lt;/td&gt;
&lt;td&gt;"Summarize this webpage"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For this tutorial, we'll focus on &lt;strong&gt;Tools&lt;/strong&gt;, specifically a &lt;code&gt;fetch&lt;/code&gt; tool that retrieves web content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Industry Adoption
&lt;/h3&gt;

&lt;p&gt;MCP isn't just a side project. Originally created by Anthropic, the protocol has been donated to the &lt;a href="https://aaif.io/" rel="noopener noreferrer"&gt;Agentic AI Foundation&lt;/a&gt; — a collaborative effort founded by Anthropic, Google, OpenAI, and others. With support from major players including Docker and Cloudflare, MCP is becoming the standard way AI Models interact with external systems.&lt;/p&gt;

&lt;p&gt;For the complete specification, see the &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;official MCP documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Transports: How Clients Talk to Servers
&lt;/h2&gt;

&lt;p&gt;Before we build anything, let's understand how MCP clients and servers communicate. MCP supports two transport mechanisms:&lt;/p&gt;

&lt;h3&gt;
  
  
  Stdio (Standard Input/Output)
&lt;/h3&gt;

&lt;p&gt;The original transport method. The MCP server runs as a subprocess, and the client communicates through stdin/stdout pipes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnd85u1fojvhfl1k5ijww.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnd85u1fojvhfl1k5ijww.png" alt="MCP STDIO" width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Simple to implement, no network configuration needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: Server must run locally, can't be shared across machines or deployed remotely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streamable HTTP
&lt;/h3&gt;

&lt;p&gt;The modern transport method. The MCP server runs as an HTTP service, and clients connect over the network.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffkatizsbbufsq0y129tn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffkatizsbbufsq0y129tn.png" alt="MCP Streamable HTTP" width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Deploy anywhere, share across teams, scale horizontally, add authentication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: Slightly more complex setup, requires network configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which Should You Use?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Recommended Transport&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local development, personal tools&lt;/td&gt;
&lt;td&gt;Stdio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team sharing, production deployment&lt;/td&gt;
&lt;td&gt;Streamable HTTP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adding OAuth/authentication&lt;/td&gt;
&lt;td&gt;Streamable HTTP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Running in Docker/cloud&lt;/td&gt;
&lt;td&gt;Streamable HTTP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;create-mcp-server&lt;/code&gt; generates Streamable HTTP servers.&lt;/strong&gt; This is the right choice for production-ready servers that can be deployed, shared, and secured.&lt;/p&gt;

&lt;p&gt;The good news: if you understand Streamable HTTP, stdio is even simpler. The MCP SDK handles both, and your tool implementations stay exactly the same. Only the transport layer changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing create-mcp-server
&lt;/h2&gt;

&lt;p&gt;Building an MCP server from scratch means setting up Express, configuring transports, handling sessions, and wiring up the MCP protocol. That's a lot of boilerplate before you write your first tool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/agentailor/create-mcp-server" rel="noopener noreferrer"&gt;&lt;code&gt;create-mcp-server&lt;/code&gt;&lt;/a&gt; eliminates that friction. It's a CLI tool that scaffolds production-ready MCP servers in seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @agentailor/create-mcp-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI asks a few questions and generates a complete TypeScript project with everything configured.&lt;/p&gt;

&lt;h3&gt;
  
  
  Template Options
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Stateless&lt;/th&gt;
&lt;th&gt;Stateful&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Session management&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSE support&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth option&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoints&lt;/td&gt;
&lt;td&gt;POST /mcp&lt;/td&gt;
&lt;td&gt;POST, GET, DELETE /mcp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Stateless&lt;/strong&gt;: Each request is independent. Simple, but no session persistence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stateful&lt;/strong&gt;: Sessions are maintained across requests. Supports Server-Sent Events for streaming. This is what most production servers need.&lt;/p&gt;

&lt;p&gt;For this tutorial, we'll use the &lt;strong&gt;Stateful template&lt;/strong&gt; (without OAuth).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If you're following this tutorial in the future and the CLI prompts you to choose a framework, select &lt;strong&gt;Official SDK&lt;/strong&gt; to match this guide.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Setting Up Your Project
&lt;/h2&gt;

&lt;p&gt;Let's scaffold your MCP server. Run the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @agentailor/create-mcp-server@0.2.1 &lt;span class="c"&gt;# specify this version for consistency&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When prompted, enter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Project name&lt;/strong&gt;: &lt;code&gt;fetch-mcp-server&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Package manager&lt;/strong&gt;: &lt;code&gt;npm&lt;/code&gt; (or your preference)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Template type&lt;/strong&gt;: &lt;code&gt;Stateful&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable OAuth authentication?&lt;/strong&gt;: &lt;code&gt;No&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Initialize git repository?&lt;/strong&gt;: Your choice&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The CLI generates this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch-mcp-server/
├── src/
│   ├── server.ts     # MCP server (tools, prompts, resources)
│   └── index.ts      # Express app and HTTP transport
├── package.json
├── tsconfig.json
├── .env.example
├── .gitignore
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate to the project and install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;fetch-mcp-server
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your server is ready to run, but it only has placeholder tools. Let's look at the generated code before adding our own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Scaffolded Code
&lt;/h2&gt;

&lt;p&gt;The CLI generates two key files. Let's understand what each does.&lt;/p&gt;

&lt;h3&gt;
  
  
  src/index.ts — The HTTP Transport
&lt;/h3&gt;

&lt;p&gt;This file handles all the networking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomUUID&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createMcpExpressApp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@modelcontextprotocol/sdk/server/express.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StreamableHTTPServerTransport&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@modelcontextprotocol/sdk/server/streamableHttp.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isInitializeRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@modelcontextprotocol/sdk/types.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./server.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMcpExpressApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Map to store transports by session ID&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;StreamableHTTPServerTransport&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="c1"&gt;// ... Express routes for POST, GET, DELETE /mcp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creates an Express app with MCP middleware&lt;/li&gt;
&lt;li&gt;Manages sessions using unique IDs&lt;/li&gt;
&lt;li&gt;Handles three endpoints:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /mcp&lt;/code&gt;: Main endpoint for tool calls and messages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /mcp&lt;/code&gt;: Server-Sent Events for streaming&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DELETE /mcp&lt;/code&gt;: Session cleanup&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You don't need to modify this file.&lt;/strong&gt; The transport layer is complete, just focus on &lt;code&gt;server.ts&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  src/server.ts — Your MCP Server
&lt;/h3&gt;

&lt;p&gt;This is where the magic happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;GetPromptResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ReadResourceResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@modelcontextprotocol/sdk/types.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;McpServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@modelcontextprotocol/sdk/server/mcp.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;McpServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-mcp-server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Register tools, prompts, and resources here&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scaffolded version includes example implementations. We'll replace them with our &lt;code&gt;fetch&lt;/code&gt; tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Fetch Tool
&lt;/h2&gt;

&lt;p&gt;Now for the core of our server: a tool that fetches URLs and converts HTML to readable markdown.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install the HTML-to-Markdown Library
&lt;/h3&gt;

&lt;p&gt;We need one additional dependency to convert HTML to markdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;node-html-markdown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Implement the Fetch Tool
&lt;/h3&gt;

&lt;p&gt;Replace the contents of &lt;code&gt;src/server.ts&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GetPromptResult&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@modelcontextprotocol/sdk/types.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;McpServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@modelcontextprotocol/sdk/server/mcp.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NodeHtmlMarkdown&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node-html-markdown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// User agent for HTTP requests&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_USER_AGENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FetchMCPServer/1.0 (+https://github.com/agentailor/fetch-mcp-server)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;McpServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch-mcp-server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Register the fetch tool&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Fetches a URL from the internet and extracts its contents as markdown.

Use this tool when you need to:
- Read documentation or articles from the web
- Get current information from websites
- Access publicly available web content

The HTML content is automatically converted to clean markdown for easier reading.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The URL to fetch. Must be a valid HTTP or HTTPS URL.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;max_length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Maximum number of characters to return. Defaults to 5000.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;start_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Start content from this character index. Use for pagination when content is truncated.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;If true, returns raw HTML instead of converting to markdown.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;max_length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start_index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Fetch the URL&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User-Agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_USER_AGENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;follow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
              &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Failed to fetch &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="na"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;// Check if the content is HTML&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
          &lt;span class="nx"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
          &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;!doctype&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
          &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isHtml&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Convert HTML to markdown&lt;/span&gt;
          &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NodeHtmlMarkdown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isHtml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Content-Type: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Handle pagination&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start_index&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;originalLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
              &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`No more content available. Total length: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;originalLength&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; characters, requested start: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;start_index&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Extract the requested portion&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;truncatedContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start_index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;max_length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;originalLength&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start_index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;truncatedContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Contents of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:\n\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;truncatedContent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

        &lt;span class="c1"&gt;// Add pagination hint if content was truncated&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;start_index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;truncatedContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
          &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`\n\n---\n[Content truncated. &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; characters remaining. Use start_index=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nextIndex&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; to continue.]`&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errorMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Error fetching &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Register a prompt for fetching URLs&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Fetch a URL and extract its contents as markdown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;argsSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;URL to fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GetPromptResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Please fetch and summarize the content from: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Code Breakdown
&lt;/h3&gt;

&lt;p&gt;Let's understand what we built:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool Registration&lt;/strong&gt;: The &lt;code&gt;server.registerTool()&lt;/code&gt; method defines our &lt;code&gt;fetch&lt;/code&gt; tool with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A descriptive name and description (helps AI understand when to use it)&lt;/li&gt;
&lt;li&gt;An input schema using Zod for validation&lt;/li&gt;
&lt;li&gt;An async handler function that does the actual work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Input Parameters&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;The URL to fetch (required)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max_length&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;5000&lt;/td&gt;
&lt;td&gt;Max characters to return&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;start_index&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Start position for pagination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;raw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;Skip HTML-to-markdown conversion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;HTML Conversion&lt;/strong&gt;: We use &lt;code&gt;node-html-markdown&lt;/code&gt; to convert HTML to clean markdown. This makes web content much easier for AI to process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pagination&lt;/strong&gt;: Long content is truncated with a helpful message showing how to fetch the next chunk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error Handling&lt;/strong&gt;: Network errors and HTTP failures return user-friendly error messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Your Server
&lt;/h2&gt;

&lt;p&gt;Start your server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MCP Stateful HTTP Server listening on port 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your server is now running at &lt;code&gt;http://localhost:3000/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using MCP Inspector
&lt;/h3&gt;

&lt;p&gt;The project includes a testing tool. In a new terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run inspect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the inspector opens in your browser:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Change the transport from &lt;strong&gt;STDIO&lt;/strong&gt; to &lt;strong&gt;HTTP&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Connect&lt;/strong&gt; in the left sidebar&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshltb07u3v6h94t6r0kx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshltb07u3v6h94t6r0kx.png" alt="MCP Inspector Home" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Testing the Prompt
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Prompts&lt;/strong&gt; → &lt;strong&gt;List Prompts&lt;/strong&gt; (you'll see the &lt;code&gt;fetch&lt;/code&gt; prompt we created)&lt;/li&gt;
&lt;li&gt;Click on the &lt;strong&gt;fetch&lt;/strong&gt; prompt&lt;/li&gt;
&lt;li&gt;Paste a URL in the input field&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7fl3lq5bxzsc1ywbphe7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7fl3lq5bxzsc1ywbphe7.png" alt="MCP Inspector Fetch Prompt" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Testing the Tool
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Tools&lt;/strong&gt; → &lt;strong&gt;List Tools&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;fetch&lt;/strong&gt; tool&lt;/li&gt;
&lt;li&gt;You'll see a form with all the parameters we defined (&lt;code&gt;url&lt;/code&gt;, &lt;code&gt;max_length&lt;/code&gt;, &lt;code&gt;start_index&lt;/code&gt;, &lt;code&gt;raw&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Fill in the &lt;strong&gt;url&lt;/strong&gt; (required) and keep the defaults for other fields&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Run Tool&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result will display the fetched content converted to markdown:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnjjyba0pbdjntfevw5xw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnjjyba0pbdjntfevw5xw.png" alt="MCP Inspector Tool Result" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip&lt;/strong&gt;: Try fetching &lt;code&gt;https://modelcontextprotocol.io/docs/getting-started/intro&lt;/code&gt; to see how the tool converts documentation pages to clean markdown.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Connecting to VS Code or Cursor
&lt;/h3&gt;

&lt;p&gt;To use your server with VS Code or Cursor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your server URL&lt;/strong&gt;: &lt;code&gt;http://localhost:3000/mcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VS Code setup&lt;/strong&gt;: See the &lt;a href="https://code.visualstudio.com/docs/copilot/chat/mcp-servers" rel="noopener noreferrer"&gt;official VS Code MCP documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cursor setup&lt;/strong&gt;: See the &lt;a href="https://cursor.com/docs/context/mcp" rel="noopener noreferrer"&gt;official Cursor MCP documentation&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once connected, your AI assistant can use the &lt;code&gt;fetch&lt;/code&gt; tool to read web pages directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;You've built a working MCP server. Here are some ideas to extend it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add caching&lt;/strong&gt;: Store fetched pages to avoid repeated requests for the same URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support more content types&lt;/strong&gt;: Handle PDFs, images, or JSON APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add rate limiting&lt;/strong&gt;: Prevent abuse when deploying publicly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secure your server&lt;/strong&gt;: Add OAuth authentication using our &lt;a href="https://blog.agentailor.com/blog/oauth-for-mcp-servers-practical-guide-keycloak?utm_source=agentailor_blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=first_mcp_server" rel="noopener noreferrer"&gt;OAuth for MCP Servers guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Publish to the registry&lt;/strong&gt;: Share your server with the community by following our &lt;a href="https://blog.agentailor.com/blog/publishing-mcp-server-to-official-registry?utm_source=agentailor_blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=first_mcp_server" rel="noopener noreferrer"&gt;publishing guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy with Docker&lt;/strong&gt;: See our guide on &lt;a href="https://blog.agentailor.com/blog/docker-mcp-catalog-and-toolkit?utm_source=agentailor_blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=first_mcp_server" rel="noopener noreferrer"&gt;running MCP servers with Docker&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You've just built something that major AI platforms are adopting as a standard. In under 5 minutes, you went from zero to a production-ready MCP server that gives AI assistants the ability to fetch and read web content.&lt;/p&gt;

&lt;p&gt;The complete code is available on GitHub: &lt;a href="https://github.com/agentailor/fetch-mcp-server" rel="noopener noreferrer"&gt;agentailor/fetch-mcp-server&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clone it, extend it, deploy it. The MCP ecosystem is growing fast, and now you're part of it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Enjoying content like this? Sign up for &lt;a href="https://www.linkedin.com/newsletters/agent-briefings-7391777936955310080/" rel="noopener noreferrer"&gt;Agent Briefings&lt;/a&gt;, where I share insights and news on building and scaling MCP Servers and AI agents.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/agentailor/fetch-mcp-server" rel="noopener noreferrer"&gt;Fetch MCP Server (GitHub)&lt;/a&gt; — Complete code from this tutorial&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/agentailor/create-mcp-server" rel="noopener noreferrer"&gt;create-mcp-server (GitHub)&lt;/a&gt; — The CLI tool used in this guide&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP Documentation&lt;/a&gt; — Official protocol specification&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://code.visualstudio.com/docs/copilot/chat/mcp-servers" rel="noopener noreferrer"&gt;VS Code MCP Servers&lt;/a&gt; — VS Code integration guide&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cursor.com/docs/context/mcp" rel="noopener noreferrer"&gt;Cursor MCP Documentation&lt;/a&gt; — Cursor integration guide&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Related Articles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/oauth-for-mcp-servers-practical-guide-keycloak?utm_source=agentailor_blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=first_mcp_server" rel="noopener noreferrer"&gt;Securing MCP Servers with OAuth and Keycloak&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/docker-mcp-catalog-and-toolkit?utm_source=agentailor_blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=first_mcp_server" rel="noopener noreferrer"&gt;Run Any MCP Server with Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/publishing-mcp-server-to-official-registry?utm_source=agentailor_blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=first_mcp_server" rel="noopener noreferrer"&gt;How to Publish Your MCP Server to the Official Registry&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Top 5 TypeScript AI Agent Frameworks You Should Know in 2026</title>
      <dc:creator>Ali Ibrahim</dc:creator>
      <pubDate>Thu, 01 Jan 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/ialijr/top-5-typescript-ai-agent-frameworks-you-should-know-in-2026-139c</link>
      <guid>https://dev.to/ialijr/top-5-typescript-ai-agent-frameworks-you-should-know-in-2026-139c</guid>
      <description>&lt;p&gt;In GitHub's 2025 language report, TypeScript officially surpassed Python. The race was fierce, but TypeScript won. While Python still dominates the AI agent ecosystem, JavaScript and TypeScript are the fastest-growing alternatives—and for good reason. Full-stack developers can now build AI agents without switching stacks.&lt;/p&gt;

&lt;p&gt;If you want the full picture across all languages, check out &lt;a href="https://dev.to/blog/top-ai-agent-frameworks-github-2026"&gt;Top 10 Most Starred AI Agent Frameworks on GitHub (2026)&lt;/a&gt;. This article focuses on the TypeScript ecosystem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short on time&lt;/strong&gt;? Get the key takeaways of this article in under 5 seconds with &lt;strong&gt;DocuMentor AI&lt;/strong&gt;, my Chrome extension. No account required. &lt;a href="https://documentorai.io/extension" rel="noopener noreferrer"&gt;Try it Here&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. Vercel AI SDK ⭐ 20,400 | 📦 2.8M downloads/week
&lt;/h2&gt;

&lt;p&gt;From the creators of Next.js, the Vercel AI SDK is the most downloaded TypeScript AI framework by a massive margin. It provides streaming-first primitives for building AI-powered user interfaces, with built-in support for React Server Components and edge runtimes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; React and Next.js developers who want to add AI features to their web applications with minimal friction.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/vercel/ai" rel="noopener noreferrer"&gt;vercel/ai&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Mastra ⭐ 19,024 | 📦 151K downloads/week
&lt;/h2&gt;

&lt;p&gt;Mastra is a TypeScript-native AI agent framework built for production. It includes assistants, RAG pipelines, and built-in observability out of the box. Unlike ports from Python, Mastra was designed from the ground up for the TypeScript ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Teams building production-grade agent systems who need full observability and don't want to piece together multiple libraries.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/mastra-ai/mastra" rel="noopener noreferrer"&gt;mastra-ai/mastra&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. LangChain.js ⭐ 16,600 | 📦 795K downloads/week
&lt;/h2&gt;

&lt;p&gt;The JavaScript port of the most popular Python AI framework. LangChain.js brings the same modular architecture—chains, agents, tools, and memory—to the JS ecosystem. If your team already uses LangChain in Python, the API will feel familiar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Teams already invested in the LangChain ecosystem, or developers who want access to LangChain's extensive documentation and community.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/langchain-ai/langchainjs" rel="noopener noreferrer"&gt;langchain-ai/langchainjs&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. VoltAgent ⭐ 4,478 | 📦 151K downloads/week
&lt;/h2&gt;

&lt;p&gt;VoltAgent is an open-source AI agent framework with LLM observability built into its core. It's a rising star in the ecosystem, offering debugging and tracing capabilities that are often afterthoughts in other frameworks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers who prioritize debugging, tracing, and understanding what their agents are actually doing under the hood.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/VoltAgent/voltagent" rel="noopener noreferrer"&gt;VoltAgent/voltagent&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. LangGraph.js ⭐ 2,388 | 📦 529K downloads/week
&lt;/h2&gt;

&lt;p&gt;LangGraph.js brings graph-based orchestration to AI agents. Instead of linear chains, you define your agent logic as nodes and edges—making it easier to build complex, multi-step workflows with branching, loops, and conditional logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Complex agent workflows that need fine-grained control over execution flow, especially multi-agent systems.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/langchain-ai/langgraphjs" rel="noopener noreferrer"&gt;langchain-ai/langgraphjs&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Special Mentions
&lt;/h2&gt;

&lt;p&gt;These frameworks are brand new but backed by major players. Watch this space.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/google/adk-js" rel="noopener noreferrer"&gt;Google ADK TypeScript&lt;/a&gt;&lt;/strong&gt; ⭐ 581 | 📦 5K downloads/week — Google's agent development toolkit, released December 2025. Code-first approach with flexibility and control.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/openai/openai-agents-js" rel="noopener noreferrer"&gt;OpenAI Agents SDK TypeScript&lt;/a&gt;&lt;/strong&gt; ⭐ 2,100 | 📦 128K downloads/week — Official OpenAI framework for multi-agent workflows and voice agents. Lightweight but powerful.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Insights
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vercel AI SDK dominates downloads&lt;/strong&gt; — 2.8M weekly vs. the next best at 795K. The Next.js ecosystem effect is real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LangGraph.js punches above its weight&lt;/strong&gt; — Only 2.3K stars but 529K weekly downloads. Developers are using it, even if they're not starring it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stars ≠ adoption&lt;/strong&gt; — Mastra has more stars than LangChain.js but fewer downloads. Community buzz doesn't always match production usage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The big players just arrived&lt;/strong&gt; — Google ADK and OpenAI SDK are brand new. Momentum over the next 6 months will tell the real story.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What to Read Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/top-ai-agent-frameworks-github-2026?utm_source=devto&amp;amp;utm_medium=read_next&amp;amp;utm_campaign=top_ts_agents_2026" rel="noopener noreferrer"&gt;Top 10 Most Starred AI Agent Frameworks on GitHub (2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/langgraph-vs-llamaindex-javascript?utm_source=devto&amp;amp;utm_medium=read_next&amp;amp;utm_campaign=top_ts_agents_2026" rel="noopener noreferrer"&gt;LangGraph vs LlamaIndex: Who Makes AI Agents Easier in JavaScript?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/openai-agent-typescript-sdk?utm_source=devto&amp;amp;utm_medium=read_next&amp;amp;utm_campaign=top_ts_agents_2026" rel="noopener noreferrer"&gt;Getting Started with OpenAI's Agents SDK for TypeScript&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Enjoying content like this? Sign up for &lt;a href="https://www.linkedin.com/newsletters/agent-briefings-7391777936955310080/" rel="noopener noreferrer"&gt;Agent Briefings&lt;/a&gt;, where I share insights and news on building and scaling AI agents.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Star counts and npm downloads as of January 2026. Numbers change—check the repos and npm for the latest.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>ai</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Top 10 Most Starred AI Agent Frameworks on GitHub (2026)</title>
      <dc:creator>Ali Ibrahim</dc:creator>
      <pubDate>Wed, 31 Dec 2025 21:40:27 +0000</pubDate>
      <link>https://dev.to/ialijr/top-10-most-starred-ai-agent-frameworks-on-github-2026-3d4o</link>
      <guid>https://dev.to/ialijr/top-10-most-starred-ai-agent-frameworks-on-github-2026-3d4o</guid>
      <description>&lt;p&gt;AI agents are reshaping how we build software. GitHub stars are a strong indicator of developer trust and community adoption. Here are the top 10 most starred AI agent frameworks heading into 2026.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short on time&lt;/strong&gt;? Get the key takeaways of this article in under 5 seconds with &lt;strong&gt;DocuMentor AI&lt;/strong&gt;, my Chrome extension. No account required. &lt;a href="https://documentorai.io/extension" rel="noopener noreferrer"&gt;Try it Here&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. LangChain ⭐ 122,850
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/langchain-ai/langchain" rel="noopener noreferrer"&gt;langchain-ai/langchain&lt;/a&gt;&lt;/strong&gt; | Python | MIT&lt;/p&gt;

&lt;p&gt;The most popular framework for building LLM-powered applications, with extensive tooling for chains, agents, and retrieval.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. MetaGPT ⭐ 61,919
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/FoundationAgents/MetaGPT" rel="noopener noreferrer"&gt;FoundationAgents/MetaGPT&lt;/a&gt;&lt;/strong&gt; | Python | MIT&lt;/p&gt;

&lt;p&gt;A multi-agent framework that simulates a software company, with agents taking on roles like product manager, architect, and engineer.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. AutoGen ⭐ 52,927
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/microsoft/autogen" rel="noopener noreferrer"&gt;microsoft/autogen&lt;/a&gt;&lt;/strong&gt; | Python | CC-BY-4.0&lt;/p&gt;

&lt;p&gt;Microsoft's framework for building multi-agent conversational systems with customizable agent behaviors.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. LlamaIndex ⭐ 46,100
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/run-llama/llama_index" rel="noopener noreferrer"&gt;run-llama/llama_index&lt;/a&gt;&lt;/strong&gt; | Python | MIT&lt;/p&gt;

&lt;p&gt;The leading framework for connecting LLMs to your data, with powerful indexing and retrieval capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. CrewAI ⭐ 41,871
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/crewAIInc/crewAI" rel="noopener noreferrer"&gt;crewAIInc/crewAI&lt;/a&gt;&lt;/strong&gt; | Python | MIT&lt;/p&gt;

&lt;p&gt;Framework for orchestrating role-playing autonomous agents that collaborate to accomplish complex tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Agno ⭐ 36,414
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/agno-agi/agno" rel="noopener noreferrer"&gt;agno-agi/agno&lt;/a&gt;&lt;/strong&gt; | Python | Apache-2.0&lt;/p&gt;

&lt;p&gt;A multi-agent framework with a runtime and control plane for managing agent deployments at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Haystack ⭐ 23,741
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/deepset-ai/haystack" rel="noopener noreferrer"&gt;deepset-ai/haystack&lt;/a&gt;&lt;/strong&gt; | Python | Apache-2.0&lt;/p&gt;

&lt;p&gt;Production-ready AI orchestration framework focused on building customizable LLM applications and RAG pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Vercel AI SDK ⭐ 20,400
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/vercel/ai" rel="noopener noreferrer"&gt;vercel/ai&lt;/a&gt;&lt;/strong&gt; | TypeScript | Apache-2.0&lt;/p&gt;

&lt;p&gt;The AI toolkit for TypeScript from the creators of Next.js, designed for building AI-powered web applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Open-AutoGLM ⭐ 19,800
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/zai-org/Open-AutoGLM" rel="noopener noreferrer"&gt;zai-org/Open-AutoGLM&lt;/a&gt;&lt;/strong&gt; | Python | Apache-2.0&lt;/p&gt;

&lt;p&gt;An open-source phone agent model and framework for building mobile device automation agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Mastra ⭐ 19,021
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/mastra-ai/mastra" rel="noopener noreferrer"&gt;mastra-ai/mastra&lt;/a&gt;&lt;/strong&gt; | TypeScript | Apache-2.0&lt;/p&gt;

&lt;p&gt;A TypeScript-native AI agent framework with built-in workflows, tools, and integrations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Special Mentions
&lt;/h2&gt;

&lt;p&gt;Stars don't tell the whole story. These frameworks have fewer stars but are gaining momentum with high download counts and modern architectures.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/openai/openai-agents-python" rel="noopener noreferrer"&gt;OpenAI Agents SDK&lt;/a&gt;&lt;/strong&gt; ⭐ 18,022 — Official OpenAI framework for multi-agent workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/google/adk-python" rel="noopener noreferrer"&gt;Google ADK Python&lt;/a&gt;&lt;/strong&gt; ⭐ 16,800 — Google's new agent development toolkit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/pydantic/pydantic-ai" rel="noopener noreferrer"&gt;Pydantic AI&lt;/a&gt;&lt;/strong&gt; ⭐ 14,000 — The Pythonic way to build AI agents&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python dominates&lt;/strong&gt;: 8 out of 10 top frameworks are Python-based&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LangChain leads by a massive margin&lt;/strong&gt; with 2x the stars of the runner-up&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript options&lt;/strong&gt;: Vercel AI SDK and Mastra for JS/TS developers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the special mentions&lt;/strong&gt;: Momentum matters more than legacy stars&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What to Read Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/agents-vs-workflows?utm_source=devto&amp;amp;utm_medium=read_next&amp;amp;utm_campaign=top_ai_agents_2026" rel="noopener noreferrer"&gt;The Future of AI Building: Workflows, Agents, and Everything In Between&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/langgraph-vs-llamaindex-javascript?utm_source=devto&amp;amp;utm_medium=read_next&amp;amp;utm_campaign=top_ai_agents_2026" rel="noopener noreferrer"&gt;LangGraph vs LlamaIndex: Who Makes AI Agents Easier in JavaScript?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/openai-agent-typescript-sdk?utm_source=devto&amp;amp;utm_medium=read_next&amp;amp;utm_campaign=top_ai_agents_2026" rel="noopener noreferrer"&gt;Getting Started with OpenAI's Agents SDK for TypeScript&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Enjoying content like this? Sign up for &lt;a href="https://www.linkedin.com/newsletters/agent-briefings-7391777936955310080/" rel="noopener noreferrer"&gt;Agent Briefings&lt;/a&gt;, where I share insights and news on building and scaling AI agents.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Star counts as of January 2026. Numbers change—check the repos for the latest.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Engineering Context for Local and Cloud AI: Personas, Content Intelligence, and Zero-Prompt UX</title>
      <dc:creator>Ali Ibrahim</dc:creator>
      <pubDate>Sat, 20 Dec 2025 14:55:45 +0000</pubDate>
      <link>https://dev.to/ialijr/engineering-context-for-local-and-cloud-ai-personas-content-intelligence-and-zero-prompt-ux-11h2</link>
      <guid>https://dev.to/ialijr/engineering-context-for-local-and-cloud-ai-personas-content-intelligence-and-zero-prompt-ux-11h2</guid>
      <description>&lt;p&gt;In &lt;a href="https://blog.agentailor.com/blog/chrome-builtin-ai-cloud-hybrid-system" rel="noopener noreferrer"&gt;the previous article&lt;/a&gt;, we covered how &lt;a href="https://documentorai.io?utm_source=blog&amp;amp;utm_medium=referral&amp;amp;utm_campaign=engineering_context" rel="noopener noreferrer"&gt;DocuMentor AI's&lt;/a&gt; hybrid architecture seamlessly adapts between Chrome's local Gemini Nano and cloud AI. We built a system that automatically routes tasks based on capabilities and performance constraints.&lt;/p&gt;

&lt;p&gt;But having a robust execution layer is only half the battle. The other half? &lt;strong&gt;Engineering the context that goes into those models.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where most AI tools fall short. They give you a powerful model and a blank text box, then expect you to figure out what to ask. It's like handing someone a professional camera and saying "take a good photo"—technically possible, but the burden is entirely on the user.&lt;/p&gt;

&lt;p&gt;DocuMentor takes a different approach: &lt;strong&gt;zero-prompt UX through intelligent context engineering&lt;/strong&gt;. Users never write prompts. They click a feature (Quick Scan, Deep Analysis, Cheat Sheet), and the extension handles the rest—assembling the right persona elements, extracting the right page sections, and shaping everything into a request the AI can't misinterpret.&lt;/p&gt;

&lt;p&gt;This article breaks down how that works: the philosophy behind zero-prompt design, the persona system that personalizes every response, and the content intelligence layer that knows exactly what to send to the AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Zero-Prompt Philosophy
&lt;/h2&gt;

&lt;p&gt;Most technical documentation tools and AI-powered browsers present the same UX pattern: a blank chat input with a placeholder like "Ask me anything about this page."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbtrb5u348x6b3yboa8ic.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbtrb5u348x6b3yboa8ic.png" alt="Ask Me anything" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This seems user-friendly on the surface, but if you don't know what you want to ask, it creates three problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cognitive load&lt;/strong&gt; - Users have to think about how to phrase their question&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intent ambiguity&lt;/strong&gt; - Small wording changes lead to wildly different answers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic responses&lt;/strong&gt; - Without context about the user, AI gives one-size-fits-all answers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I built DocuMentor to solve my own problem: I spend hours every week scanning documentation, blog posts, and API references trying to extract what I need quickly. Sometimes I want a TL;DR to decide if it's worth reading. Other times I need a cheat sheet for future reference. And sometimes I just want to know "should I care about this?"&lt;/p&gt;

&lt;p&gt;These are &lt;strong&gt;specific, recurring needs&lt;/strong&gt;. Why should I have to articulate them from scratch every time?&lt;/p&gt;

&lt;p&gt;The solution: &lt;strong&gt;feature-first design&lt;/strong&gt;. Instead of a blank chat box, DocuMentor exposes four purpose-built features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quick Scan&lt;/strong&gt; - Instant insights: TL;DR, "should I read this?", related resources, page architecture&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep Analysis&lt;/strong&gt; - The deep dive: comprehensive overview, code patterns, video recommendations, learning resources with reasoning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheat Sheet&lt;/strong&gt; - Future reference: condensed, actionable summary optimized for quick lookup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AskMe&lt;/strong&gt; - Targeted chat: select text or images and ask specific questions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each feature represents a pre-crafted intent. Users don't have to think about how to ask, they just pick the outcome they want. The extension takes care of the rest: crafting the prompt, selecting the right page sections, and applying the user's persona.&lt;/p&gt;

&lt;p&gt;This isn't just about convenience. It's about &lt;strong&gt;eliminating ambiguity&lt;/strong&gt;. When a user clicks "Quick Scan," there's zero room for misinterpretation. The AI knows exactly what format to return, what level of detail to provide, and what the user cares about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persona-Driven Personalization
&lt;/h2&gt;

&lt;p&gt;After building the initial feature set, I realized something critical: &lt;strong&gt;none of these features should return generic answers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A "Should I read this?" recommendation means nothing without knowing who's asking. A senior AI engineer doesn't need to read an intro to neural networks. A junior frontend developer does need to read advanced React hooks patterns. Same feature, same page, completely different answers.&lt;/p&gt;

&lt;p&gt;That's when I introduced the &lt;strong&gt;persona system&lt;/strong&gt;—a user profile that shapes every AI response:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's in a persona:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Role&lt;/strong&gt; - AI/ML Engineer, Frontend Developer, Backend Engineer, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seniority&lt;/strong&gt; - Beginner, Intermediate, Senior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills&lt;/strong&gt; - Programming languages, frameworks, concepts each with a proficiency level (Beginner, Intermediate, Advanced)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning Goals&lt;/strong&gt; - What the user wants to master right now (e.g., "Master LangGraph for production AI agents")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning Preferences&lt;/strong&gt; - Text, video, or mixed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl70i2i1bt76n576ohrsl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl70i2i1bt76n576ohrsl.png" alt="Persona components diagram" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure: The five components of a DocuMentor persona.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The challenge wasn't just collecting this information—it was knowing &lt;strong&gt;which elements matter for which features&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Learning preferences&lt;/strong&gt; are irrelevant for cheat sheets (the user already decided they want text)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills and goals&lt;/strong&gt; are critical for "Should I read this?" recommendations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seniority&lt;/strong&gt; affects explanation depth across all features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role&lt;/strong&gt; determines which code patterns to highlight in Deep Analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a mapping problem: each feature needs a &lt;strong&gt;subset of the persona&lt;/strong&gt;, not the whole thing. Sending irrelevant persona data just adds noise and wastes tokens (especially on local AI with tight context limits).&lt;/p&gt;

&lt;p&gt;Here's where it gets interesting. Let's say you're a &lt;strong&gt;Junior Frontend Developer&lt;/strong&gt; learning React, and you land on an article about advanced state management patterns. DocuMentor's "Should I read this?" feature might say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Yes, read this.&lt;/strong&gt; This covers useReducer and Context API patterns that will level up your React skills. It assumes familiarity with useState, which you have. The examples are practical and match your learning goal: mastering React for production apps.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now imagine a &lt;strong&gt;Senior Backend Engineer&lt;/strong&gt; who knows React but isn't focused on frontend work hits the same page:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Skip this.&lt;/strong&gt; You already understand these patterns from your React experience. This won't advance your current goal (mastering distributed systems). If you need a refresher later, the cheat sheet feature has you covered.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Same page. Same feature. Completely different recommendations, because the persona told the AI who's asking and why they care.&lt;/p&gt;

&lt;p&gt;This isn't just personalization for its own sake. It's about &lt;strong&gt;respecting the user's time&lt;/strong&gt;. Generic AI tools waste your time by making you read irrelevant content or miss important insights. Persona-driven AI acts like a knowledgeable colleague who knows your background and priorities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content Intelligence: Strategic Page Decomposition
&lt;/h2&gt;

&lt;p&gt;Early on, I made the naive mistake most AI developers make: I fed the entire page HTML to the model, assuming it could "figure it out."&lt;/p&gt;

&lt;p&gt;This failed spectacularly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context overflow&lt;/strong&gt; - Raw HTML easily exceeds Chrome AI's ~4K token limit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Noise drowning signal&lt;/strong&gt; - Ads, navigation, footers, and JavaScript compete with actual content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hallucinations&lt;/strong&gt; - Small models like Gemini Nano get confused by irrelevant information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first fix was obvious: &lt;strong&gt;content extraction&lt;/strong&gt;. I used Mozilla's Readability library (with a custom fallback for pages where Readability fails) to extract clean, readable text from the page.&lt;/p&gt;

&lt;p&gt;But that still wasn't enough. Even clean content presented a new problem: &lt;strong&gt;not every feature needs the same information&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Summaries and cheat sheets&lt;/strong&gt; need the full article content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video recommendations&lt;/strong&gt; only need a summary of the page (not 10K words of content)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Learn Resources" suggestions&lt;/strong&gt; need page links and navigation context, not the article body&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sending everything to every feature wastes tokens, increases latency, and reduces relevance. The solution: &lt;strong&gt;strategic page decomposition&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;DocuMentor breaks every page into purpose-driven sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Main content&lt;/strong&gt; - The core article text (extracted via Readability)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table of contents&lt;/strong&gt; - Page structure and hierarchy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page links&lt;/strong&gt; - URLs embedded in the content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code blocks&lt;/strong&gt; - Extracted separately for pattern analysis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breadcrumbs &amp;amp; navigation&lt;/strong&gt; - Contextual metadata about where this page fits in the documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo88ru790wrnu361mtd5d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo88ru790wrnu361mtd5d.png" alt="Page Decomposition" width="800" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each feature gets &lt;strong&gt;exactly what it needs&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Content Sections Used&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Summary&lt;/td&gt;
&lt;td&gt;Main content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cheat Sheet&lt;/td&gt;
&lt;td&gt;Main content + code blocks + page links&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video Recommendations&lt;/td&gt;
&lt;td&gt;Summary only (not full content)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learn Resources&lt;/td&gt;
&lt;td&gt;Summary + page links + breadcrumbs + navigation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code Patterns (Deep Analysis)&lt;/td&gt;
&lt;td&gt;Code blocks + surrounding context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Figure: Page sections are strategically routed to different features based on what information is actually relevant.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's look at a concrete example: &lt;strong&gt;video recommendations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The naive approach would send the full article content and ask the AI to find relevant YouTube videos. For a 10K-word documentation page, this would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Burn through most of Chrome AI's token budget on a single feature&lt;/li&gt;
&lt;li&gt;Slow down the response (the model has to process 10K words before even calling the YouTube API)&lt;/li&gt;
&lt;li&gt;Risk quota errors on devices with lower VRAM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, DocuMentor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generates a &lt;strong&gt;summary&lt;/strong&gt; of the page (200-300 words)&lt;/li&gt;
&lt;li&gt;Sends the summary + user persona to the AI&lt;/li&gt;
&lt;li&gt;AI generates an optimal YouTube search query based on the topic and user's learning goals&lt;/li&gt;
&lt;li&gt;Calls the YouTube Data API (handled by the extension, not the AI)&lt;/li&gt;
&lt;li&gt;AI ranks the top 10 results based on relevance to the user's goals and the page summary&lt;/li&gt;
&lt;li&gt;Returns the top 3 videos with compelling, personalized descriptions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach is &lt;strong&gt;10x faster&lt;/strong&gt; and uses &lt;strong&gt;1/10th the tokens&lt;/strong&gt; compared to sending full content. And because the AI only sees relevant information (summary + persona), the recommendations are more accurate.&lt;/p&gt;

&lt;p&gt;This pattern repeats across every feature: &lt;strong&gt;content intelligence isn't about giving the AI more information—it's about giving it the right information.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adaptive Prompting Across Providers
&lt;/h2&gt;

&lt;p&gt;One final layer of context engineering: &lt;strong&gt;how you shape the request matters as much as what you send&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;As covered in the previous article, DocuMentor runs on two AI providers: Chrome's local Gemini Nano and a cloud backend (Gemini 2.0 Flash). They have radically different capabilities.&lt;/p&gt;

&lt;p&gt;For the same feature, the prompts are adapted:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemini Nano (local)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple, directive instructions&lt;/li&gt;
&lt;li&gt;One reasoning task per prompt&lt;/li&gt;
&lt;li&gt;Defensive output parsing (it sometimes returns malformed JSON)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cloud models&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Richer, multi-step instructions&lt;/li&gt;
&lt;li&gt;Tool-calling support&lt;/li&gt;
&lt;li&gt;Reliable structured output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The persona and content sections stay the same, but the way they're framed changes based on the model's reasoning capacity.&lt;/p&gt;

&lt;p&gt;For example, video recommendations on local AI use &lt;strong&gt;sequential decomposition&lt;/strong&gt; (covered in the previous article): generate search query → call API → rank results → format output. Each step is a separate AI call.&lt;/p&gt;

&lt;p&gt;On cloud AI, the same feature runs as a &lt;strong&gt;single tool-augmented call&lt;/strong&gt;: the model generates the query, calls the YouTube tool, ranks results, and formats the output—all in one request.&lt;/p&gt;

&lt;p&gt;Users never see this complexity. They click "Video Recommendations," and the system automatically routes to the appropriate provider and prompt strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This is just the first version of DocuMentor's context engineering system. Two areas I'm exploring for future iterations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. User-customizable feature prompts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let users add personalized instructions to individual features. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"In summaries, always include a brief definition of core concepts"&lt;/li&gt;
&lt;li&gt;"For video recommendations, prioritize short tutorials under 15 minutes"&lt;/li&gt;
&lt;li&gt;"When suggesting resources, focus on official documentation over blog posts"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This would let users fine-tune the experience without overthinking every request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Dynamic personas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Right now, personas are static. But a fullstack developer might want to view a page as a &lt;strong&gt;frontend engineer&lt;/strong&gt; one day and a &lt;strong&gt;backend engineer&lt;/strong&gt; the next, depending on context.&lt;/p&gt;

&lt;p&gt;Future versions could let users switch personas per page or even infer persona adjustments based on the content type (e.g., automatically apply a "security-focused" lens when reading about authentication).&lt;/p&gt;

&lt;p&gt;The goal remains the same: &lt;strong&gt;personalization without overthinking&lt;/strong&gt;. AI should adapt to you, not the other way around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building effective AI features isn't just about picking the right model or writing clever prompts. It's about &lt;strong&gt;engineering the context&lt;/strong&gt; that goes into those prompts: knowing your user (persona), knowing what information matters (content intelligence), and eliminating ambiguity (zero-prompt UX).&lt;/p&gt;

&lt;p&gt;DocuMentor's context engineering system rests on three pillars:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero-prompt UX&lt;/strong&gt; - Features replace chat boxes, eliminating user guesswork&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persona-driven personalization&lt;/strong&gt; - Every response adapts to role, skills, goals, and preferences&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content intelligence&lt;/strong&gt; - Strategic decomposition ensures features get exactly what they need&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result: an AI tool that feels less like a chatbot and more like a knowledgeable colleague who understands what you're trying to accomplish.&lt;/p&gt;

&lt;p&gt;If you want to see this in action, &lt;a href="https://documentorai.io/extension?utm_source=blog&amp;amp;utm_medium=referral&amp;amp;utm_campaign=engineering_context" rel="noopener noreferrer"&gt;try DocuMentor AI&lt;/a&gt; on a technical article or documentation page. And if you find it useful, the best way to support this work is to &lt;strong&gt;leave a review and share it with someone who might benefit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I'd also love to hear from you: &lt;strong&gt;What other aspects of building DocuMentor would you like to hear about?&lt;/strong&gt; Drop a comment or reach out—your feedback shapes what I write next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related Articles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/chrome-builtin-ai-cloud-hybrid-system?utm_source=devto" rel="noopener noreferrer"&gt;Engineering a Hybrid AI System with Chrome's Built-in AI and the Cloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/fullstack-ai-agent-app-with-langgraphjs-and-nextjs?utm_source=devto" rel="noopener noreferrer"&gt;Building a Fullstack AI Agent with LangGraph.js and Next.js&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>ai</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Engineering Context for Local and Cloud AI: Personas, Content Intelligence, and Zero-Prompt UX</title>
      <dc:creator>Ali Ibrahim</dc:creator>
      <pubDate>Sat, 20 Dec 2025 14:47:30 +0000</pubDate>
      <link>https://dev.to/ialijr/engineering-context-for-local-and-cloud-ai-personas-content-intelligence-and-zero-prompt-ux-10dn</link>
      <guid>https://dev.to/ialijr/engineering-context-for-local-and-cloud-ai-personas-content-intelligence-and-zero-prompt-ux-10dn</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/blog/chrome-builtin-ai-cloud-hybrid-system"&gt;the previous article&lt;/a&gt;, we covered how &lt;a href="https://documentorai.io?utm_source=blog&amp;amp;utm_medium=referral&amp;amp;utm_campaign=engineering_context" rel="noopener noreferrer"&gt;DocuMentor AI's&lt;/a&gt; hybrid architecture seamlessly adapts between Chrome's local Gemini Nano and cloud AI. We built a system that automatically routes tasks based on capabilities and performance constraints.&lt;/p&gt;

&lt;p&gt;But having a robust execution layer is only half the battle. The other half? &lt;strong&gt;Engineering the context that goes into those models.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where most AI tools fall short. They give you a powerful model and a blank text box, then expect you to figure out what to ask. It's like handing someone a professional camera and saying "take a good photo"—technically possible, but the burden is entirely on the user.&lt;/p&gt;

&lt;p&gt;DocuMentor takes a different approach: &lt;strong&gt;zero-prompt UX through intelligent context engineering&lt;/strong&gt;. Users never write prompts. They click a feature (Quick Scan, Deep Analysis, Cheat Sheet), and the extension handles the rest—assembling the right persona elements, extracting the right page sections, and shaping everything into a request the AI can't misinterpret.&lt;/p&gt;

&lt;p&gt;This article breaks down how that works: the philosophy behind zero-prompt design, the persona system that personalizes every response, and the content intelligence layer that knows exactly what to send to the AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Zero-Prompt Philosophy
&lt;/h2&gt;

&lt;p&gt;Most technical documentation tools and AI-powered browsers present the same UX pattern: a blank chat input with a placeholder like "Ask me anything about this page."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbtrb5u348x6b3yboa8ic.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbtrb5u348x6b3yboa8ic.png" alt="Ask Me anything" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This seems user-friendly on the surface, but if you don't know what you want to ask, it creates three problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cognitive load&lt;/strong&gt; - Users have to think about how to phrase their question&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intent ambiguity&lt;/strong&gt; - Small wording changes lead to wildly different answers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic responses&lt;/strong&gt; - Without context about the user, AI gives one-size-fits-all answers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I built DocuMentor to solve my own problem: I spend hours every week scanning documentation, blog posts, and API references trying to extract what I need quickly. Sometimes I want a TL;DR to decide if it's worth reading. Other times I need a cheat sheet for future reference. And sometimes I just want to know "should I care about this?"&lt;/p&gt;

&lt;p&gt;These are &lt;strong&gt;specific, recurring needs&lt;/strong&gt;. Why should I have to articulate them from scratch every time?&lt;/p&gt;

&lt;p&gt;The solution: &lt;strong&gt;feature-first design&lt;/strong&gt;. Instead of a blank chat box, DocuMentor exposes four purpose-built features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quick Scan&lt;/strong&gt; - Instant insights: TL;DR, "should I read this?", related resources, page architecture&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep Analysis&lt;/strong&gt; - The deep dive: comprehensive overview, code patterns, video recommendations, learning resources with reasoning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheat Sheet&lt;/strong&gt; - Future reference: condensed, actionable summary optimized for quick lookup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AskMe&lt;/strong&gt; - Targeted chat: select text or images and ask specific questions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each feature represents a pre-crafted intent. Users don't have to think about how to ask, they just pick the outcome they want. The extension takes care of the rest: crafting the prompt, selecting the right page sections, and applying the user's persona.&lt;/p&gt;

&lt;p&gt;This isn't just about convenience. It's about &lt;strong&gt;eliminating ambiguity&lt;/strong&gt;. When a user clicks "Quick Scan," there's zero room for misinterpretation. The AI knows exactly what format to return, what level of detail to provide, and what the user cares about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persona-Driven Personalization
&lt;/h2&gt;

&lt;p&gt;After building the initial feature set, I realized something critical: &lt;strong&gt;none of these features should return generic answers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A "Should I read this?" recommendation means nothing without knowing who's asking. A senior AI engineer doesn't need to read an intro to neural networks. A junior frontend developer does need to read advanced React hooks patterns. Same feature, same page, completely different answers.&lt;/p&gt;

&lt;p&gt;That's when I introduced the &lt;strong&gt;persona system&lt;/strong&gt;—a user profile that shapes every AI response:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's in a persona:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Role&lt;/strong&gt; - AI/ML Engineer, Frontend Developer, Backend Engineer, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seniority&lt;/strong&gt; - Beginner, Intermediate, Senior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills&lt;/strong&gt; - Programming languages, frameworks, concepts each with a proficiency level (Beginner, Intermediate, Advanced)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning Goals&lt;/strong&gt; - What the user wants to master right now (e.g., "Master LangGraph for production AI agents")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning Preferences&lt;/strong&gt; - Text, video, or mixed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl70i2i1bt76n576ohrsl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl70i2i1bt76n576ohrsl.png" alt="Persona components diagram" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure: The five components of a DocuMentor persona.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The challenge wasn't just collecting this information—it was knowing &lt;strong&gt;which elements matter for which features&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Learning preferences&lt;/strong&gt; are irrelevant for cheat sheets (the user already decided they want text)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills and goals&lt;/strong&gt; are critical for "Should I read this?" recommendations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seniority&lt;/strong&gt; affects explanation depth across all features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role&lt;/strong&gt; determines which code patterns to highlight in Deep Analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a mapping problem: each feature needs a &lt;strong&gt;subset of the persona&lt;/strong&gt;, not the whole thing. Sending irrelevant persona data just adds noise and wastes tokens (especially on local AI with tight context limits).&lt;/p&gt;

&lt;p&gt;Here's where it gets interesting. Let's say you're a &lt;strong&gt;Junior Frontend Developer&lt;/strong&gt; learning React, and you land on an article about advanced state management patterns. DocuMentor's "Should I read this?" feature might say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Yes, read this.&lt;/strong&gt; This covers useReducer and Context API patterns that will level up your React skills. It assumes familiarity with useState, which you have. The examples are practical and match your learning goal: mastering React for production apps.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now imagine a &lt;strong&gt;Senior Backend Engineer&lt;/strong&gt; who knows React but isn't focused on frontend work hits the same page:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Skip this.&lt;/strong&gt; You already understand these patterns from your React experience. This won't advance your current goal (mastering distributed systems). If you need a refresher later, the cheat sheet feature has you covered.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Same page. Same feature. Completely different recommendations, because the persona told the AI who's asking and why they care.&lt;/p&gt;

&lt;p&gt;This isn't just personalization for its own sake. It's about &lt;strong&gt;respecting the user's time&lt;/strong&gt;. Generic AI tools waste your time by making you read irrelevant content or miss important insights. Persona-driven AI acts like a knowledgeable colleague who knows your background and priorities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content Intelligence: Strategic Page Decomposition
&lt;/h2&gt;

&lt;p&gt;Early on, I made the naive mistake most AI developers make: I fed the entire page HTML to the model, assuming it could "figure it out."&lt;/p&gt;

&lt;p&gt;This failed spectacularly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context overflow&lt;/strong&gt; - Raw HTML easily exceeds Chrome AI's ~4K token limit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Noise drowning signal&lt;/strong&gt; - Ads, navigation, footers, and JavaScript compete with actual content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hallucinations&lt;/strong&gt; - Small models like Gemini Nano get confused by irrelevant information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first fix was obvious: &lt;strong&gt;content extraction&lt;/strong&gt;. I used Mozilla's Readability library (with a custom fallback for pages where Readability fails) to extract clean, readable text from the page.&lt;/p&gt;

&lt;p&gt;But that still wasn't enough. Even clean content presented a new problem: &lt;strong&gt;not every feature needs the same information&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Summaries and cheat sheets&lt;/strong&gt; need the full article content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video recommendations&lt;/strong&gt; only need a summary of the page (not 10K words of content)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Learn Resources" suggestions&lt;/strong&gt; need page links and navigation context, not the article body&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sending everything to every feature wastes tokens, increases latency, and reduces relevance. The solution: &lt;strong&gt;strategic page decomposition&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;DocuMentor breaks every page into purpose-driven sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Main content&lt;/strong&gt; - The core article text (extracted via Readability)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table of contents&lt;/strong&gt; - Page structure and hierarchy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page links&lt;/strong&gt; - URLs embedded in the content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code blocks&lt;/strong&gt; - Extracted separately for pattern analysis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breadcrumbs &amp;amp; navigation&lt;/strong&gt; - Contextual metadata about where this page fits in the documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each feature gets &lt;strong&gt;exactly what it needs&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Content Sections Used&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Summary&lt;/td&gt;
&lt;td&gt;Main content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cheat Sheet&lt;/td&gt;
&lt;td&gt;Main content + code blocks + page links&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video Recommendations&lt;/td&gt;
&lt;td&gt;Summary only (not full content)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learn Resources&lt;/td&gt;
&lt;td&gt;Summary + page links + breadcrumbs + navigation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code Patterns (Deep Analysis)&lt;/td&gt;
&lt;td&gt;Code blocks + surrounding context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Figure: Page sections are strategically routed to different features based on what information is actually relevant.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's look at a concrete example: &lt;strong&gt;video recommendations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The naive approach would send the full article content and ask the AI to find relevant YouTube videos. For a 10K-word documentation page, this would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Burn through most of Chrome AI's token budget on a single feature&lt;/li&gt;
&lt;li&gt;Slow down the response (the model has to process 10K words before even calling the YouTube API)&lt;/li&gt;
&lt;li&gt;Risk quota errors on devices with lower VRAM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, DocuMentor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generates a &lt;strong&gt;summary&lt;/strong&gt; of the page (200-300 words)&lt;/li&gt;
&lt;li&gt;Sends the summary + user persona to the AI&lt;/li&gt;
&lt;li&gt;AI generates an optimal YouTube search query based on the topic and user's learning goals&lt;/li&gt;
&lt;li&gt;Calls the YouTube Data API (handled by the extension, not the AI)&lt;/li&gt;
&lt;li&gt;AI ranks the top 10 results based on relevance to the user's goals and the page summary&lt;/li&gt;
&lt;li&gt;Returns the top 3 videos with compelling, personalized descriptions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach is &lt;strong&gt;10x faster&lt;/strong&gt; and uses &lt;strong&gt;1/10th the tokens&lt;/strong&gt; compared to sending full content. And because the AI only sees relevant information (summary + persona), the recommendations are more accurate.&lt;/p&gt;

&lt;p&gt;This pattern repeats across every feature: &lt;strong&gt;content intelligence isn't about giving the AI more information—it's about giving it the right information.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adaptive Prompting Across Providers
&lt;/h2&gt;

&lt;p&gt;One final layer of context engineering: &lt;strong&gt;how you shape the request matters as much as what you send&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;As covered in the previous article, DocuMentor runs on two AI providers: Chrome's local Gemini Nano and a cloud backend (Gemini 2.0 Flash). They have radically different capabilities.&lt;/p&gt;

&lt;p&gt;For the same feature, the prompts are adapted:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemini Nano (local)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple, directive instructions&lt;/li&gt;
&lt;li&gt;One reasoning task per prompt&lt;/li&gt;
&lt;li&gt;Defensive output parsing (it sometimes returns malformed JSON)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cloud models&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Richer, multi-step instructions&lt;/li&gt;
&lt;li&gt;Tool-calling support&lt;/li&gt;
&lt;li&gt;Reliable structured output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The persona and content sections stay the same, but the way they're framed changes based on the model's reasoning capacity.&lt;/p&gt;

&lt;p&gt;For example, video recommendations on local AI use &lt;strong&gt;sequential decomposition&lt;/strong&gt; (covered in the previous article): generate search query → call API → rank results → format output. Each step is a separate AI call.&lt;/p&gt;

&lt;p&gt;On cloud AI, the same feature runs as a &lt;strong&gt;single tool-augmented call&lt;/strong&gt;: the model generates the query, calls the YouTube tool, ranks results, and formats the output—all in one request.&lt;/p&gt;

&lt;p&gt;Users never see this complexity. They click "Video Recommendations," and the system automatically routes to the appropriate provider and prompt strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This is just the first version of DocuMentor's context engineering system. Two areas I'm exploring for future iterations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. User-customizable feature prompts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let users add personalized instructions to individual features. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"In summaries, always include a brief definition of core concepts"&lt;/li&gt;
&lt;li&gt;"For video recommendations, prioritize short tutorials under 15 minutes"&lt;/li&gt;
&lt;li&gt;"When suggesting resources, focus on official documentation over blog posts"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This would let users fine-tune the experience without overthinking every request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Dynamic personas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Right now, personas are static. But a fullstack developer might want to view a page as a &lt;strong&gt;frontend engineer&lt;/strong&gt; one day and a &lt;strong&gt;backend engineer&lt;/strong&gt; the next, depending on context.&lt;/p&gt;

&lt;p&gt;Future versions could let users switch personas per page or even infer persona adjustments based on the content type (e.g., automatically apply a "security-focused" lens when reading about authentication).&lt;/p&gt;

&lt;p&gt;The goal remains the same: &lt;strong&gt;personalization without overthinking&lt;/strong&gt;. AI should adapt to you, not the other way around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building effective AI features isn't just about picking the right model or writing clever prompts. It's about &lt;strong&gt;engineering the context&lt;/strong&gt; that goes into those prompts: knowing your user (persona), knowing what information matters (content intelligence), and eliminating ambiguity (zero-prompt UX).&lt;/p&gt;

&lt;p&gt;DocuMentor's context engineering system rests on three pillars:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero-prompt UX&lt;/strong&gt; - Features replace chat boxes, eliminating user guesswork&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persona-driven personalization&lt;/strong&gt; - Every response adapts to role, skills, goals, and preferences&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content intelligence&lt;/strong&gt; - Strategic decomposition ensures features get exactly what they need&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result: an AI tool that feels less like a chatbot and more like a knowledgeable colleague who understands what you're trying to accomplish.&lt;/p&gt;

&lt;p&gt;If you want to see this in action, &lt;a href="https://documentorai.io/extension?utm_source=blog&amp;amp;utm_medium=referral&amp;amp;utm_campaign=engineering_context" rel="noopener noreferrer"&gt;try DocuMentor AI&lt;/a&gt; on a technical article or documentation page. And if you find it useful, the best way to support this work is to &lt;strong&gt;leave a review and share it with someone who might benefit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I'd also love to hear from you: &lt;strong&gt;What other aspects of building DocuMentor would you like to hear about?&lt;/strong&gt; Drop a comment or reach out—your feedback shapes what I write next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related Articles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/chrome-builtin-ai-cloud-hybrid-system?utm_source=devto" rel="noopener noreferrer"&gt;Engineering a Hybrid AI System with Chrome's Built-in AI and the Cloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.agentailor.com/blog/fullstack-ai-agent-app-with-langgraphjs-and-nextjs?utm_source=devto" rel="noopener noreferrer"&gt;Building a Fullstack AI Agent with LangGraph.js and Next.js&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
    <item>
      <title>I was tired of copy-pasting to ChatGPT, so I built a Chrome extension</title>
      <dc:creator>Ali Ibrahim</dc:creator>
      <pubDate>Mon, 15 Dec 2025 19:12:38 +0000</pubDate>
      <link>https://dev.to/ialijr/i-was-tired-of-copy-pasting-to-chatgpt-so-i-built-a-chrome-extension-m3f</link>
      <guid>https://dev.to/ialijr/i-was-tired-of-copy-pasting-to-chatgpt-so-i-built-a-chrome-extension-m3f</guid>
      <description>&lt;p&gt;Every article I write requires hours of research. I open 10, 20, sometimes 30 browser tabs. Technical docs. Blog posts. GitHub READMEs. API references. Research papers.&lt;/p&gt;

&lt;p&gt;Most of them? Not worth the 15 minutes I spend reading them.&lt;/p&gt;

&lt;p&gt;But I don't know that until I'm already 10 minutes in.&lt;/p&gt;

&lt;p&gt;So I developed a workflow: copy the article, open ChatGPT, paste, type "summarize this and give me the key points," wait for the response, read the summary, decide if it's worth a full read.&lt;/p&gt;

&lt;p&gt;Then repeat. For every. Single. Article.&lt;/p&gt;

&lt;h2&gt;
  
  
  The breaking point
&lt;/h2&gt;

&lt;p&gt;After the 50th time opening a new ChatGPT tab, pasting a wall of text, and typing the same prompt, I thought: "There has to be a better way."&lt;/p&gt;

&lt;p&gt;I wasn't learning faster. I was spending more time &lt;em&gt;managing my learning workflow&lt;/em&gt; than actually learning.&lt;/p&gt;

&lt;p&gt;The problem wasn't ChatGPT. The problem was the context switching. Every time I wanted to understand something:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Highlight text on the page&lt;/li&gt;
&lt;li&gt;Copy it&lt;/li&gt;
&lt;li&gt;Open a new tab&lt;/li&gt;
&lt;li&gt;Paste into ChatGPT&lt;/li&gt;
&lt;li&gt;Write a prompt&lt;/li&gt;
&lt;li&gt;Wait for the response&lt;/li&gt;
&lt;li&gt;Switch back to the original tab&lt;/li&gt;
&lt;li&gt;Try to remember where I was&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My flow was broken. My focus was scattered. And I was tired of writing the same prompts over and over.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://documentorai.io/" rel="noopener noreferrer"&gt;DocuMentor AI&lt;/a&gt;: a Chrome extension that does all of this &lt;em&gt;on the page I'm already reading&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;No copy-paste. No tab switching. No prompt engineering.&lt;/p&gt;

&lt;p&gt;Just click the extension icon, and it analyzes the page instantly using Chrome's built-in AI (Gemini Nano). Everything happens locally in your browser—no uploading content to cloud services by default.&lt;/p&gt;

&lt;p&gt;Here's what it does:&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick Scan – "Is this worth reading?"
&lt;/h3&gt;

&lt;p&gt;Click the extension. Get an instant answer to "Should I read this?" in about 5 seconds.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Summary of the main ideas&lt;/li&gt;
&lt;li&gt;Difficulty level (beginner, intermediate, advanced)&lt;/li&gt;
&lt;li&gt;Prerequisites you should know first&lt;/li&gt;
&lt;li&gt;Estimated reading time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All matched to your skill level and learning goals.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc79r9407l6l0usg07z0h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc79r9407l6l0usg07z0h.png" alt="Quick Scan" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No more bookmarking articles you'll never open. No more reading 10 minutes into something before realizing it's not relevant.&lt;/p&gt;

&lt;p&gt;You decide &lt;em&gt;before&lt;/em&gt; you invest the time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deep Analysis – Extract what actually matters
&lt;/h3&gt;

&lt;p&gt;When you find something worth reading, run a Deep Analysis.&lt;/p&gt;

&lt;p&gt;It extracts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Core concepts and design decisions&lt;/li&gt;
&lt;li&gt;Trade-offs and limitations&lt;/li&gt;
&lt;li&gt;Code patterns, anti-patterns, and best practices from examples&lt;/li&gt;
&lt;li&gt;Related topics you might want to review first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjbk885u92ihq8ooa69sq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjbk885u92ihq8ooa69sq.jpg" alt="Deep Analysis" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You're not just reading anymore. You're building a mental model of &lt;em&gt;why&lt;/em&gt; things work the way they do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cheat Sheet Generator – Save the good stuff
&lt;/h3&gt;

&lt;p&gt;You know that feeling when you google the same Git command for the fourth time this month?&lt;/p&gt;

&lt;p&gt;That stops here.&lt;/p&gt;

&lt;p&gt;One click generates a clean Markdown cheat sheet from the current page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Key concepts and commands&lt;/li&gt;
&lt;li&gt;Code snippets with syntax highlighting&lt;/li&gt;
&lt;li&gt;"Gotchas" and edge cases in one place&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Download it. Save it to Obsidian, Notion, your notes app, a Git repo—wherever you keep your personal knowledge base.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqt6zyw6bupcs6lbopdi0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqt6zyw6bupcs6lbopdi0.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You own the reference. No cloud lock-in. No subscription required to access your own notes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ask Me – No more copy-paste
&lt;/h3&gt;

&lt;p&gt;Select any text or code snippet on the page. Click "Ask Me." Get an explanation grounded in the content you're reading—without leaving the tab.&lt;/p&gt;

&lt;p&gt;Ask things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Explain this like I'm a junior developer"&lt;/li&gt;
&lt;li&gt;"What does this code actually do?"&lt;/li&gt;
&lt;li&gt;"When would I use this pattern?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answers reference the actual documentation in front of you, not generic web results.&lt;/p&gt;

&lt;p&gt;Your questions and the page content are processed locally. Nothing leaves your browser by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smart Recommendations – What to learn next
&lt;/h3&gt;

&lt;p&gt;After you finish reading, &lt;a href="https://documentorai.io/" rel="noopener noreferrer"&gt;DocuMentor AI&lt;/a&gt; suggests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What to learn next based on the current topic&lt;/li&gt;
&lt;li&gt;Follow-up docs and related concepts worth exploring&lt;/li&gt;
&lt;li&gt;Videos and alternative resources when you'd rather watch than read&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All personalized to your skill level and role (set once in your User Persona, applied everywhere).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why local-first matters
&lt;/h2&gt;

&lt;p&gt;Here's the thing: I care about privacy. I don't want to upload every technical article I read to a random cloud service just to get a summary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://documentorai.io/" rel="noopener noreferrer"&gt;DocuMentor AI&lt;/a&gt; runs on &lt;strong&gt;Chrome's built-in Gemini Nano model&lt;/strong&gt; (requires Chrome 138+). That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI processing happens &lt;strong&gt;locally in your browser&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Page content &lt;strong&gt;never leaves your device&lt;/strong&gt; by default&lt;/li&gt;
&lt;li&gt;No tracking, no data uploads, no "we'll use your data to train our models"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can optionally enable cloud sync for cross-device features, but it's off by default.&lt;/p&gt;

&lt;p&gt;Your research workflow? Your reading history? Your learning profile? All stored on your device. You control the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned building this
&lt;/h2&gt;

&lt;p&gt;The biggest surprise? &lt;strong&gt;Developers hate writing prompts as much as I do.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everyone I showed this to said the same thing: "Wait, it just &lt;em&gt;knows&lt;/em&gt; what I want without me having to ask?"&lt;/p&gt;

&lt;p&gt;Turns out, most people learning from technical docs want the same things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this worth reading?&lt;/li&gt;
&lt;li&gt;What are the key concepts?&lt;/li&gt;
&lt;li&gt;Can I save this for later?&lt;/li&gt;
&lt;li&gt;What should I learn next?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By building those workflows directly into the extension, I removed the friction of thinking about &lt;em&gt;how&lt;/em&gt; to ask. You just click and get what you need.&lt;/p&gt;

&lt;p&gt;The other thing I learned: &lt;strong&gt;local AI is fast enough.&lt;/strong&gt; Chrome's Gemini Nano can analyze a full documentation page in seconds. No API latency. No rate limits. No "you've exceeded your quota" errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you're tired of the copy-paste-to-ChatGPT workflow, give &lt;a href="https://documentorai.io/" rel="noopener noreferrer"&gt;DocuMentor AI&lt;/a&gt; a shot.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://chrome.google.com/webstore/detail/kmpcjfcpapdmgfdgdnknpkbgcdmmfhjm" rel="noopener noreferrer"&gt;&lt;strong&gt;[Chrome Web Store link]&lt;/strong&gt;&lt;/a&gt; (Requires Chrome 138+ for local AI)&lt;/p&gt;

&lt;p&gt;I'd love to hear your feedback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What doc would you scan first?&lt;/li&gt;
&lt;li&gt;What feature would make this more useful for you?&lt;/li&gt;
&lt;li&gt;What's missing?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop a comment or reach out. I'm actively working on this and want to build something that actually fits how developers learn.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Built with:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chrome's built-in AI (Gemini Nano)&lt;/li&gt;
&lt;li&gt;Next.js for the marketing site&lt;/li&gt;
&lt;li&gt;TypeScript everywhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Currently:&lt;/strong&gt; Local AI free forever and 100% free Cloud AI during early access.&lt;/p&gt;

&lt;p&gt;Stop fighting with prompts. Let the extension read the docs for you.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
      <category>extensions</category>
    </item>
  </channel>
</rss>
