<?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: Jaco Botha</title>
    <description>The latest articles on DEV Community by Jaco Botha (@ex0b1t).</description>
    <link>https://dev.to/ex0b1t</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%2F393428%2F321608b1-de23-4be4-b681-d570bc1ff161.jpeg</url>
      <title>DEV Community: Jaco Botha</title>
      <link>https://dev.to/ex0b1t</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ex0b1t"/>
    <language>en</language>
    <item>
      <title>Your first MCP server in TypeScript with Routecraft</title>
      <dc:creator>Jaco Botha</dc:creator>
      <pubDate>Mon, 01 Jun 2026 15:45:00 +0000</pubDate>
      <link>https://dev.to/ex0b1t/your-first-mcp-server-in-typescript-with-routecraft-hec</link>
      <guid>https://dev.to/ex0b1t/your-first-mcp-server-in-typescript-with-routecraft-hec</guid>
      <description>&lt;p&gt;If you have heard of the Model Context Protocol and want a working server in front of Claude Desktop, Cursor, or your IDE's MCP client in about ten minutes, this post is for you. We will scaffold a TypeScript project, write a tool, run it locally, and connect an AI agent to call it. No auth, no Docker, no platform.&lt;/p&gt;

&lt;p&gt;If you would rather start with the framework's own tour, the &lt;a href="https://routecraft.dev/docs/introduction" rel="noopener noreferrer"&gt;Routecraft introduction&lt;/a&gt; covers the same ground in reference form. This post is the narrative version, optimised for "I want to see it work in one tab".&lt;/p&gt;

&lt;h2&gt;
  
  
  What MCP is, briefly
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; is an open spec from Anthropic for connecting AI agents to your tools, data, and prompts. An MCP-capable client like Claude Desktop or Cursor can connect to any MCP server and call its tools with validated JSON inputs.&lt;/p&gt;

&lt;p&gt;Two transports are supported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;stdio&lt;/strong&gt;: the agent spawns your server as a subprocess and communicates over stdin/stdout. Local only, no network, no auth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP&lt;/strong&gt;: your server runs as a network service. Authentication is required for anything sensitive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post sticks to stdio because that is the fastest path to a working setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Routecraft for this
&lt;/h2&gt;

&lt;p&gt;You can write an MCP server in raw TypeScript. The MCP SDK ships a low-level server abstraction, and Anthropic's docs walk through it. It works, and for one-off scripts it is fine. The pain shows up when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you want &lt;strong&gt;typed inputs&lt;/strong&gt; validated before your tool runs,&lt;/li&gt;
&lt;li&gt;you want a tool to be both an &lt;strong&gt;MCP tool now&lt;/strong&gt; and a &lt;strong&gt;cron job later&lt;/strong&gt; without rewriting it,&lt;/li&gt;
&lt;li&gt;you want the same code to &lt;strong&gt;log structured events&lt;/strong&gt;, retry on failure, and run a test suite without you bolting it on,&lt;/li&gt;
&lt;li&gt;you want to add &lt;strong&gt;auth, rate limiting, or observability&lt;/strong&gt; later without rewriting your tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://routecraft.dev/docs/introduction" rel="noopener noreferrer"&gt;Routecraft&lt;/a&gt; is a TypeScript framework for exactly this shape. You write &lt;strong&gt;capabilities&lt;/strong&gt;, which are small composable routes (&lt;code&gt;source -&amp;gt; operations -&amp;gt; destination&lt;/code&gt;), and the runtime handles MCP transport, validation, logging, telemetry, and the awkward bits. Your tool is twenty lines of TypeScript that you can read in one sitting.&lt;/p&gt;

&lt;p&gt;For comparison, a hand-rolled MCP tool that does input validation, structured logging, and error formatting is closer to eighty lines. Routecraft is doing real work for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we will build
&lt;/h2&gt;

&lt;p&gt;A tiny &lt;strong&gt;notebook&lt;/strong&gt; MCP server with two tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;notes_list&lt;/code&gt; returns a list of notes, optionally filtered by query.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;notes_create&lt;/code&gt; adds a new note to an in-memory store.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is the shape, not the notes. Once you understand how a capability becomes a tool, the same pattern works for "list orders in Stripe", "search a Postgres table", "send a Slack message", or anything else you can call from a function.&lt;/p&gt;

&lt;p&gt;End state:&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%2Fa502e47x2d8wptvam98r.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%2Fa502e47x2d8wptvam98r.png" alt="The MCP Inspector connected to the notebook server, showing the notes_list and notes_create tools" width="799" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bun&lt;/strong&gt; 1.1 or newer (&lt;a href="https://bun.sh" rel="noopener noreferrer"&gt;install instructions&lt;/a&gt;). Routecraft works on Node 22+ too, but Bun is faster for the dev loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An MCP client.&lt;/strong&gt; Claude Desktop, Cursor, or any other client that speaks stdio MCP. Free downloads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Five to ten minutes.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. No accounts, no API keys, no platform setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaffold the project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx create-routecraft notebook
&lt;span class="nb"&gt;cd &lt;/span&gt;notebook
bun &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This drops you in a clean project with a &lt;code&gt;craft.config.ts&lt;/code&gt; at the root and a &lt;code&gt;capabilities/&lt;/code&gt; directory. Open it in your editor.&lt;/p&gt;

&lt;p&gt;Add the MCP and validation packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun add @routecraft/ai zod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@routecraft/ai&lt;/code&gt; provides the &lt;code&gt;mcp()&lt;/code&gt; source adapter that turns a capability into an MCP tool. &lt;code&gt;zod&lt;/code&gt; validates inputs before your tool ever runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write your first tool
&lt;/h2&gt;

&lt;p&gt;A small in-memory store first. Create &lt;code&gt;capabilities/notes/_lib/store.ts&lt;/code&gt;:&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;// capabilities/notes/_lib/store.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Note&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&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;title&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;body&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;createdAt&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Note&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&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;Note&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;query&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;notes&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;query&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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;n&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="nf"&gt;toLowerCase&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="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&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="nf"&gt;create&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&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;Note&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;notes&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;note&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;note&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;Now the &lt;code&gt;notes_list&lt;/code&gt; capability. Create &lt;code&gt;capabilities/notes/list-notes/route.ts&lt;/code&gt;:&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;// capabilities/notes/list-notes/route.ts&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;mcp&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;@routecraft/ai&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;craft&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;@routecraft/routecraft&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;store&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;../_lib/store&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;ListNotesInput&lt;/span&gt; &lt;span class="o"&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;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;query&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;optional&lt;/span&gt;&lt;span class="p"&gt;(),&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;ListNotesInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;ListNotesInput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;craft&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes_list&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="nf"&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;List notes, optionally filtered by a search query.&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="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ListNotesInput&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ListNotesInput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;input&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;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the entire tool. Let us read it line by line, because if you understand this you understand Routecraft:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;craft()&lt;/code&gt; starts a capability builder.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.id('notes_list')&lt;/code&gt; is the tool name the AI sees. Pick something descriptive.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.description()&lt;/code&gt; is what the AI reads to decide when to call this tool. Treat it as prompt engineering, not docs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.input({ body: ListNotesInput })&lt;/code&gt; is the Zod schema for the input. Routecraft validates against it before your code runs, so invalid calls are rejected with a structured error.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.from&amp;lt;ListNotesInput&amp;gt;(mcp())&lt;/code&gt; says "this capability's source is an MCP call". That is what turns the capability into an MCP tool. The generic flows the input type through the chain so the transform is fully typed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.transform((input) =&amp;gt; ...)&lt;/code&gt; is your business logic. The &lt;code&gt;input&lt;/code&gt; argument is the validated body, already typed. (The transform also receives the full exchange as a second argument, which we use later for auth.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole pattern: input schema in, transform out. Adapters on either end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the create tool
&lt;/h2&gt;

&lt;p&gt;Same shape, different verb. Create &lt;code&gt;capabilities/notes/create-note/route.ts&lt;/code&gt;:&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;// capabilities/notes/create-note/route.ts&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;mcp&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;@routecraft/ai&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;craft&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;@routecraft/routecraft&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;store&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;../_lib/store&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;CreateNoteInput&lt;/span&gt; &lt;span class="o"&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;object&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="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;min&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="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;body&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;min&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="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&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;CreateNoteInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;CreateNoteInput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;craft&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes_create&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="nf"&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;Create a new note with a title and body.&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="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateNoteInput&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CreateNoteInput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;input&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;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register both in &lt;code&gt;capabilities/index.ts&lt;/code&gt;:&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;// capabilities/index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;listNotes&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;./notes/list-notes/route&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;createNote&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;./notes/create-note/route&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;listNotes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createNote&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wire the MCP transport
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;craft.config.ts&lt;/code&gt; and replace it 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="c1"&gt;// craft.config.ts&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;mcpPlugin&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;@routecraft/ai&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;defineConfig&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;@routecraft/routecraft&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;const&lt;/span&gt; &lt;span class="nx"&gt;craftConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&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;notebook&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;mcpPlugin&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;notebook&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;0.1.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stdio&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;transport: 'stdio'&lt;/code&gt; tells Routecraft to speak MCP over stdin/stdout. That is the format MCP clients like Claude Desktop expect when they spawn a server as a subprocess. No ports, no networking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wire the routes into the entry point
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;craft run&lt;/code&gt; executes &lt;code&gt;index.ts&lt;/code&gt;, and a fresh project starts with an empty route list. Point it at the capabilities you just registered:&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;// index.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;craftConfig&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="s2"&gt;./craft.config.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;capabilities&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./capabilities/index.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the one piece of glue between the files you wrote and the runner: &lt;code&gt;index.ts&lt;/code&gt; re-exports the config from &lt;code&gt;craft.config.ts&lt;/code&gt; and the routes from &lt;code&gt;capabilities/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect it with the MCP Inspector
&lt;/h2&gt;

&lt;p&gt;The fastest way to see your tools is the official &lt;a href="https://github.com/modelcontextprotocol/inspector" rel="noopener noreferrer"&gt;MCP Inspector&lt;/a&gt;. It spawns your server and gives you a browser UI to list and call tools, with no client setup. From the project root:&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 bunx @routecraft/cli &lt;span class="nt"&gt;--log-level&lt;/span&gt; silent run index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--log-level silent&lt;/code&gt; keeps stdout clean: a stdio MCP server uses stdout exclusively for protocol frames, so anything else printed there breaks the connection. The Inspector opens in your browser. Click &lt;strong&gt;Connect&lt;/strong&gt;, then &lt;strong&gt;List Tools&lt;/strong&gt;, and you should see &lt;code&gt;notes_list&lt;/code&gt; and &lt;code&gt;notes_create&lt;/code&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%2Fa502e47x2d8wptvam98r.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%2Fa502e47x2d8wptvam98r.png" alt="The MCP Inspector listing the notebook server's notes_list and notes_create tools" width="799" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;notes_create&lt;/code&gt;, fill in a title and body, and &lt;strong&gt;Run Tool&lt;/strong&gt;. Then run &lt;code&gt;notes_list&lt;/code&gt; and you will see the note you just created. That round-trip is your MCP server working end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use it in a real client
&lt;/h2&gt;

&lt;p&gt;Once it works in the Inspector, any MCP client can call the same command. In Claude Desktop or Cursor, add an &lt;code&gt;mcpServers&lt;/code&gt; entry that runs the server over stdio, pointing at the &lt;strong&gt;absolute path&lt;/strong&gt; to your project's &lt;code&gt;index.ts&lt;/code&gt;:&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;"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;"notebook"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bunx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"@routecraft/cli"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--log-level"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"silent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"/absolute/path/to/notebook/index.ts"&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;p&gt;Clients spawn the server with a minimal environment and do not expand &lt;code&gt;~&lt;/code&gt;, so use absolute paths. Fully restart the client (quit, not just close the window) and the tools appear. Ask it: &lt;em&gt;"Create a note titled Groceries with body milk, bread, eggs, then list all notes"&lt;/em&gt; and it will call &lt;code&gt;notes_create&lt;/code&gt; then &lt;code&gt;notes_list&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you got for the twenty lines
&lt;/h2&gt;

&lt;p&gt;This is the moment to call out what Routecraft did under the hood, because it is genuinely a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP protocol framing.&lt;/strong&gt; All the JSON-RPC handshake, capability discovery, tool listing, and error formatting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input validation.&lt;/strong&gt; Your Zod schema is enforced before &lt;code&gt;transform&lt;/code&gt; runs. Invalid inputs become structured MCP errors automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type safety.&lt;/strong&gt; Inside &lt;code&gt;transform&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt; is already typed as &lt;code&gt;{ query?: string }&lt;/code&gt;. No casting, no &lt;code&gt;as&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured logging.&lt;/strong&gt; Tool calls, inputs, outputs, durations, errors. All logged to stderr in a structured format you can pipe to a log aggregator later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful shutdown.&lt;/strong&gt; When the MCP client disconnects, Routecraft cleans up cleanly. No zombie processes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you wrote this in raw Node with the MCP SDK you would be writing each of those by hand. None of it is hard. All of it is annoying. Routecraft's pitch is "the boring parts are done so you write only the part that is yours".&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;A few natural next steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add a real data source.&lt;/strong&gt; Swap the in-memory &lt;code&gt;store&lt;/code&gt; for a SQLite database, a Postgres query, or an HTTP API. Routecraft has &lt;a href="https://routecraft.dev/docs/introduction/adapters" rel="noopener noreferrer"&gt;adapters&lt;/a&gt; for all three.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run it as a cron job too.&lt;/strong&gt; Change &lt;code&gt;.from(mcp())&lt;/code&gt; to &lt;code&gt;.from(cron('0 9 * * *'))&lt;/code&gt; and the same capability runs every morning at 9. No other code changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose capabilities.&lt;/strong&gt; &lt;code&gt;direct()&lt;/code&gt; lets one capability call another with type safety. Build a graph, test each node in isolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go to HTTP, with auth.&lt;/strong&gt; When you want this reachable from anywhere, not just your laptop, check out the &lt;a href="https://routecraft.dev/docs/advanced/expose-as-mcp#http-transport" rel="noopener noreferrer"&gt;HTTP transport&lt;/a&gt;. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://routecraft.dev/docs/introduction" rel="noopener noreferrer"&gt;Routecraft docs&lt;/a&gt; cover all of the above in more depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it without leaving your browser
&lt;/h2&gt;

&lt;p&gt;If you want to play with the framework before installing anything, open the &lt;a href="https://codespaces.new/routecraftjs/craft-playground" rel="noopener noreferrer"&gt;Routecraft playground in GitHub Codespaces&lt;/a&gt;. Full terminal, hammer-ready in about thirty seconds.&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;# Or scaffold a new project locally&lt;/span&gt;
bunx create-routecraft my-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>mcp</category>
      <category>routecraft</category>
      <category>typescript</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
