<?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: Kishan Soni</title>
    <description>The latest articles on DEV Community by Kishan Soni (@kishanx08).</description>
    <link>https://dev.to/kishanx08</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%2F3738777%2Fb294ec3a-adce-45b9-8d9f-f53f5e03bb38.png</url>
      <title>DEV Community: Kishan Soni</title>
      <link>https://dev.to/kishanx08</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kishanx08"/>
    <language>en</language>
    <item>
      <title>I added an MCP server to my image host — here's everything that bit me</title>
      <dc:creator>Kishan Soni</dc:creator>
      <pubDate>Tue, 22 Sep 2026 16:42:25 +0000</pubDate>
      <link>https://dev.to/kishanx08/i-added-an-mcp-server-to-my-image-host-heres-everything-that-bit-me-4baf</link>
      <guid>https://dev.to/kishanx08/i-added-an-mcp-server-to-my-image-host-heres-everything-that-bit-me-4baf</guid>
      <description>&lt;p&gt;A while back I wrote about &lt;a href="https://dev.to/kishanx08/why-i-built-an-image-hostingx02-4a8d"&gt;why I built x02&lt;/a&gt; — I needed somewhere to host images for my own projects and reuse them across sites, and over time I added only what I needed.&lt;/p&gt;

&lt;p&gt;It's still that. But the biggest change this year is that you can now connect x02 to Claude, ChatGPT or Grok and just ask for your files:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Find the screenshot I uploaded last Tuesday and tell me what's in it."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's an MCP server. Building it taught me more about the protocol than the spec did, mostly by breaking. Here's what I got wrong, in case it saves you the same afternoons.&lt;/p&gt;




&lt;h2&gt;
  
  
  First I deleted the CLI
&lt;/h2&gt;

&lt;p&gt;Before any of this, I had a CLI. It logged you in with an API key. Except the endpoint it called — &lt;code&gt;GET /api/auth/user/:apiKey&lt;/code&gt; — didn't exist.&lt;/p&gt;

&lt;p&gt;My SPA catch-all returns &lt;code&gt;200&lt;/code&gt; with an HTML page for anything unmatched. So axios never threw. The CLI parsed the HTML, found no error, and accepted &lt;strong&gt;any string&lt;/strong&gt; as a valid key.&lt;/p&gt;

&lt;p&gt;It had shipped like that. Nobody noticed, because nobody used it.&lt;/p&gt;

&lt;p&gt;I deleted it instead of fixing it. The lesson I'd actually pass on: a catch-all that returns &lt;code&gt;200&lt;/code&gt; makes every missing endpoint look alive, and any client that treats "no exception" as "success" will believe it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The MCP tools call my own public API, not my database
&lt;/h2&gt;

&lt;p&gt;The obvious design is to have MCP tools talk to the data layer directly. I didn't.&lt;/p&gt;

&lt;p&gt;Every upload on x02 goes through quota checks, ban checks, NSFW scanning, filename allocation and audit logging — all of which live in the upload route, not the DAL. Calling the DAL from MCP would mean reimplementing all of it, and then watching the two copies drift apart the first time I changed one.&lt;/p&gt;

&lt;p&gt;So the MCP tools make HTTP calls to x02's own public API over loopback. Slightly silly on paper. Zero duplicated business logic, which is the only thing I actually cared about.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;destructiveHint&lt;/code&gt; defaults to &lt;strong&gt;true&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Tool annotations tell the client whether a tool reads, writes, or destroys. I set &lt;code&gt;destructiveHint: true&lt;/code&gt; on my two delete tools and left it off everywhere else, assuming absence meant "not destructive."&lt;/p&gt;

&lt;p&gt;It doesn't. The MCP spec defaults it to &lt;code&gt;true&lt;/code&gt;. My upload tool, my folder-create tool and six others were all advertising themselves as dangerous, and clients were prompting users harder than they needed to.&lt;/p&gt;

&lt;p&gt;Silence is not neutral. I ended up making the TypeScript type require an explicit value so it can't be skipped.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;openWorldHint&lt;/code&gt; is not "does this make a network call"
&lt;/h2&gt;

&lt;p&gt;Then I set &lt;code&gt;openWorldHint: true&lt;/code&gt; on everything, reasoning that every tool reaches a remote service.&lt;/p&gt;

&lt;p&gt;Wrong axis. The hint describes how &lt;em&gt;wide&lt;/em&gt; a tool's domain of interaction is — open-ended external entities versus a bounded space. All my tools operate on one x02 account, which is closed. Only "rehost this image from a URL" dereferences an address that could point anywhere.&lt;/p&gt;

&lt;p&gt;Fourteen of fifteen tools were overstating their reach. OpenAI's review process flags exactly this, and lists incorrect action labels as a leading cause of rejection.&lt;/p&gt;




&lt;h2&gt;
  
  
  An &lt;code&gt;outputSchema&lt;/code&gt; silently ate every image
&lt;/h2&gt;

&lt;p&gt;This is the one I'd most want to have known in advance.&lt;/p&gt;

&lt;p&gt;I have a &lt;code&gt;view_file&lt;/code&gt; tool that returns a downscaled thumbnail as an MCP image content block, so the assistant can actually look at your picture instead of guessing from the filename. I also declared an &lt;code&gt;outputSchema&lt;/code&gt; with the thumbnail's dimensions, because more structure seemed better.&lt;/p&gt;

&lt;p&gt;It worked perfectly in Claude. Then someone tried it in ChatGPT:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"the X02 tool returned only its thumbnail metadata rather than the actual image pixels"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The spec says a tool declaring an &lt;code&gt;outputSchema&lt;/code&gt; &lt;strong&gt;must&lt;/strong&gt; return &lt;code&gt;structuredContent&lt;/code&gt; — and clients that read structured output are free to ignore the unstructured content blocks entirely. Which is where the image lives.&lt;/p&gt;

&lt;p&gt;Claude reads both. ChatGPT reads the structured object and drops the rest. So the image was being sent and thrown away, and every test I had still passed.&lt;/p&gt;

&lt;p&gt;Removing the &lt;code&gt;outputSchema&lt;/code&gt; fixed it. The dimensions moved into the caption text. If your tool's whole purpose is to return an image, audio or a file, don't declare an output schema — you're telling the client the JSON is the real answer.&lt;/p&gt;

&lt;p&gt;Test it in &lt;strong&gt;two&lt;/strong&gt; clients. One client is not a test.&lt;/p&gt;




&lt;h2&gt;
  
  
  You cannot send a file from a chat to a server
&lt;/h2&gt;

&lt;p&gt;There is no client-to-server file transfer primitive in MCP. There's &lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1306" rel="noopener noreferrer"&gt;an open proposal&lt;/a&gt; for one, unimplemented. No client exposes chat attachments as fetchable URLs either, and none should.&lt;/p&gt;

&lt;p&gt;So the only way to upload is base64 inside the tool call. A perfectly ordinary 150 KB screenshot becomes roughly 200,000 characters, and most clients simply refuse to emit that. "Upload this image" — the single most obvious thing to ask a file host — did not work.&lt;/p&gt;

&lt;p&gt;What I ended up with: the assistant calls &lt;code&gt;request_file_from_user&lt;/code&gt;, which returns a short link. You open it, paste or drop the file, and it uploads through the same endpoint the website uses — so quota, scanning and folders all apply, with nothing reimplemented.&lt;/p&gt;

&lt;p&gt;The part I like is the waiting. The assistant's tool call blocks on an in-memory &lt;code&gt;EventEmitter&lt;/code&gt; keyed by the drop code, with a database re-check on entry so a file that already landed returns instantly. It resolves the moment the upload finishes, and the assistant continues mid-answer. &lt;strong&gt;You never have to type "done."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's also a standing tray: drop files at &lt;code&gt;/drop&lt;/code&gt; beforehand, then ask, and they're already waiting.&lt;/p&gt;




&lt;h2&gt;
  
  
  ChatGPT gives you two tools unless you flip a flag
&lt;/h2&gt;

&lt;p&gt;Added as an ordinary connector, ChatGPT exposes exactly two tools from any MCP server: &lt;code&gt;search&lt;/code&gt; and &lt;code&gt;fetch&lt;/code&gt;. Nothing else. My server has neither.&lt;/p&gt;

&lt;p&gt;The connection succeeds. It looks completely fine. Then every request fails with &lt;em&gt;"isn't exposed to me in the current toolset"&lt;/em&gt; and ChatGPT quietly falls back to searching the web.&lt;/p&gt;

&lt;p&gt;The fix is Developer mode, under Settings → Apps &amp;amp; Connectors → Advanced. It's one level deeper than most guides suggest, which is why people report not finding it.&lt;/p&gt;

&lt;p&gt;Worth knowing before you spend an afternoon assuming your server is broken. I did.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it ended up
&lt;/h2&gt;

&lt;p&gt;Fifteen tools — browse, search, view images, upload, rehost from a URL, rename, move, folders, share links, delete. OAuth 2.1 with PKCE and dynamic client registration, so you sign in on x02.me and approve scopes instead of pasting an API key into someone else's software. An app that only asked to read your files can't delete them, and disconnecting one app doesn't touch anything else.&lt;/p&gt;

&lt;p&gt;It's live in Anthropic's connector directory. The ChatGPT listing is in review.&lt;/p&gt;

&lt;p&gt;If you want to try it, the whole configuration is one address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://up.x02.me/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://x02.me/blog/set-up-x02-mcp-in-your-ai-client" rel="noopener noreferrer"&gt;Setup guide for each app&lt;/a&gt; · &lt;a href="https://x02.me" rel="noopener noreferrer"&gt;x02.me&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As always, happy to answer questions, talk through any of the design decisions, and I appreciate the feedback.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>x02</category>
    </item>
    <item>
      <title>Why I built an image hosting(x02)</title>
      <dc:creator>Kishan Soni</dc:creator>
      <pubDate>Thu, 29 Jan 2026 05:22:09 +0000</pubDate>
      <link>https://dev.to/kishanx08/why-i-built-an-image-hostingx02-4a8d</link>
      <guid>https://dev.to/kishanx08/why-i-built-an-image-hostingx02-4a8d</guid>
      <description>&lt;p&gt;I needed a simple way to host images for my own projects and reuse them across different sites. The goal was to upload an image once and use a stable link without worrying about it changing later.&lt;/p&gt;

&lt;p&gt;One thing I specifically wanted was image URLs that felt identifiable, including the option to use my own name or namespace instead of relying on random-looking links.&lt;/p&gt;

&lt;p&gt;Over time, I added only what I needed: an upload dashboard, history management, optional watermarking, API access, and CDN-backed delivery. The result became x02, which continues to evolve based on practical needs.&lt;/p&gt;

&lt;p&gt;If this sounds useful, you can check it out here:&lt;br&gt;
&lt;a href="https://x02.me/" rel="noopener noreferrer"&gt;x02.me&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’m happy to answer questions, discuss design decisions, and appreciate any feedback. If you notice any issues or have suggestions, feel free to mention them in the comments.&lt;br&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%2Fjb1oyj53t0nzhc9j3v75.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%2Fjb1oyj53t0nzhc9j3v75.png" alt=" " width="435" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>x02</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
