<?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: Serik Shaikamalov</title>
    <description>The latest articles on DEV Community by Serik Shaikamalov (@serikshaikamalov).</description>
    <link>https://dev.to/serikshaikamalov</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F138011%2Fb999f455-20b8-4bf0-99c2-aad43557b3a2.jpeg</url>
      <title>DEV Community: Serik Shaikamalov</title>
      <link>https://dev.to/serikshaikamalov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/serikshaikamalov"/>
    <language>en</language>
    <item>
      <title>I turned my Hono API into an MCP server by changing one import</title>
      <dc:creator>Serik Shaikamalov</dc:creator>
      <pubDate>Mon, 19 Oct 2026 16:37:46 +0000</pubDate>
      <link>https://dev.to/serikshaikamalov/i-turned-my-hono-api-into-an-mcp-server-by-changing-one-import-3nkf</link>
      <guid>https://dev.to/serikshaikamalov/i-turned-my-hono-api-into-an-mcp-server-by-changing-one-import-3nkf</guid>
      <description>&lt;p&gt;I have a travel-agency backend on &lt;a href="https://hono.dev" rel="noopener noreferrer"&gt;Hono&lt;/a&gt;: tours, bookings, payments. I wanted AI agents to use it too, so that a manager could tell Claude &lt;em&gt;"find March tours under $2000 and book the first one for Ali Aliyev"&lt;/em&gt; and Claude would call the right endpoints itself.&lt;/p&gt;

&lt;p&gt;The standard way to do that is &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;MCP&lt;/a&gt;: a server tells the agent which tools exist, what they do and which parameters they take, as JSON Schema.&lt;/p&gt;

&lt;p&gt;The usual approach is to write that server next to your API and describe every endpoint a second time:&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;search_tours&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;Search tours&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;month&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="na"&gt;maxPrice&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="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="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;args&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="cm"&gt;/* call the same logic as GET /tours again */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But my routes already have those schemas:&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;app&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;/tours&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;zValidator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;'&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;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;month&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="nx"&gt;coerce&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="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;12&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;Departure month&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;maxPrice&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="nx"&gt;coerce&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="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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&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;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;searchTours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Method, path, parameters, types, even descriptions: everything an agent needs is already there. So I built &lt;a href="https://github.com/serikshaikamalov/hono-mcp-bridge" rel="noopener noreferrer"&gt;hono-mcp-bridge&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- import { zValidator } from '@hono/zod-validator'
&lt;/span&gt;&lt;span class="gi"&gt;+ import { zValidator, mountMcp } from 'hono-mcp-bridge'
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ mountMcp(app)   // POST /mcp: every route is now a tool
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A drop-in &lt;code&gt;zValidator&lt;/code&gt;.&lt;/strong&gt; It calls the real &lt;code&gt;@hono/zod-validator&lt;/code&gt; and attaches the schema to the returned middleware with a symbol. Same signature, same types, so &lt;code&gt;c.req.valid('query')&lt;/code&gt; is still fully typed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walk &lt;code&gt;app.routes&lt;/code&gt;.&lt;/strong&gt; Hono keeps every handler as &lt;code&gt;{ method, path, handler }&lt;/code&gt;. Group by route, read the schemas back, convert them with Zod 4's &lt;code&gt;z.toJSONSchema&lt;/code&gt;. &lt;code&gt;GET /tours/:id&lt;/code&gt; becomes &lt;code&gt;get_tours_by_id&lt;/code&gt; with input &lt;code&gt;{ params, query, body }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tool call is a real request.&lt;/strong&gt; Arguments become a &lt;code&gt;Request&lt;/code&gt; passed to &lt;code&gt;app.fetch()&lt;/code&gt;. Validation, auth middleware (the &lt;code&gt;Authorization&lt;/code&gt; and &lt;code&gt;Cookie&lt;/code&gt; headers are forwarded), logging and rate limits all apply. If the agent sends bad input, it gets your zod 400 back as &lt;code&gt;isError: true&lt;/code&gt;, reads it and retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transport.&lt;/strong&gt; About 150 lines of dependency-free JSON-RPC implementing MCP's Streamable HTTP in stateless JSON mode. The test suite connects with the official &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt; client to check compatibility.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Live
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http tours http://localhost:3000/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Find March tours under $2000 and book the first one for Ali Aliyev, +77001234567&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;→ get_tours      {"query": {"month": 3, "maxPrice": 2000}}
← {"tours":[{"id":1,"title":"Umrah, 10 days","price":1890}]}
→ post_bookings  {"body": {"tourId": 1, "fullName": "Ali Aliyev", "phone": "+77001234567"}}
← {"bookingId":"20aa50cd-...", ...}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Security
&lt;/h2&gt;

&lt;p&gt;Agents act with the credentials of whoever connected them. Use &lt;code&gt;include&lt;/code&gt;, &lt;code&gt;exclude&lt;/code&gt; or &lt;code&gt;methods: ['GET']&lt;/code&gt; to limit what is exposed, hide single routes with &lt;code&gt;describeTool({ hidden: true })&lt;/code&gt;, and set &lt;code&gt;allowedOrigins&lt;/code&gt; when serving on localhost. Your API still decides who can do what.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compared to
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hono-mcp-server&lt;/code&gt;: wrap each route in &lt;code&gt;registerTool()&lt;/code&gt; with its own schema.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hono-gql-mcp&lt;/code&gt;: one GraphQL tool derived from &lt;code&gt;@hono/zod-openapi&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@hono/mcp&lt;/code&gt;: transport only.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fastapi-mcp&lt;/code&gt; does the same job for Python and FastAPI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations and roadmap
&lt;/h2&gt;

&lt;p&gt;No SSE or server-initiated messages yet, zod v4 only. Next up: &lt;code&gt;@hono/zod-openapi&lt;/code&gt;, Valibot and ArkType via Standard Schema, and &lt;code&gt;outputSchema&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i hono-mcp-bridge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/serikshaikamalov/hono-mcp-bridge" rel="noopener noreferrer"&gt;https://github.com/serikshaikamalov/hono-mcp-bridge&lt;/a&gt;. Issues and PRs welcome. I'd love to hear how you handle permissions when exposing your API to agents.&lt;/p&gt;

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