<?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: Adrian Bratulescu</title>
    <description>The latest articles on DEV Community by Adrian Bratulescu (@irutehe).</description>
    <link>https://dev.to/irutehe</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%2F4056772%2Fe3d0b5d2-13f1-4f38-b2d1-8dcad75c4dd7.jpg</url>
      <title>DEV Community: Adrian Bratulescu</title>
      <link>https://dev.to/irutehe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/irutehe"/>
    <language>en</language>
    <item>
      <title>I wrote a tool that reads your OpenAPI spec and refuses to guess</title>
      <dc:creator>Adrian Bratulescu</dc:creator>
      <pubDate>Tue, 18 Aug 2026 08:48:45 +0000</pubDate>
      <link>https://dev.to/irutehe/i-wrote-a-tool-that-reads-your-openapi-spec-and-refuses-to-guess-2gk4</link>
      <guid>https://dev.to/irutehe/i-wrote-a-tool-that-reads-your-openapi-spec-and-refuses-to-guess-2gk4</guid>
      <description>&lt;p&gt;Everyone's first instinct, when they see "turn an OpenAPI document into tool definitions an AI agent can call," is to hand the spec to a model and ask nicely.&lt;/p&gt;

&lt;p&gt;The trouble with that is the failure rate you land on. Not 100%, so you can't ship it unread. Not 50%, so you can't dismiss it. Somewhere in between — which means you spend longer reviewing the output than you would have spent writing it, and the mistakes are precisely the ones that look correct.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;archstone init&lt;/code&gt; has no LLM on any path. Not as a purity thing — because the interesting parts of this problem are the parts a model is worst at.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem, precisely
&lt;/h2&gt;

&lt;p&gt;You have a REST API. You want AI agents — Claude, ChatGPT, whatever ships next quarter — to be able to call it. The modern answer is MCP, and the modern shortcut is "generate an MCP server from your OpenAPI spec."&lt;/p&gt;

&lt;p&gt;That shortcut has a hole in it. &lt;strong&gt;An OpenAPI document describes your HTTP surface. It does not describe your business.&lt;/strong&gt; It says &lt;code&gt;POST /v1/bookings&lt;/code&gt; returns &lt;code&gt;201&lt;/code&gt;. It does not say whether calling that charges someone's card.&lt;/p&gt;

&lt;p&gt;That difference is the whole job.&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%2Fraw.githubusercontent.com%2FArchstone-Romania%2Farchstone%2Fmain%2Fdocs%2Finit.gif" 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%2Fraw.githubusercontent.com%2FArchstone-Romania%2Farchstone%2Fmain%2Fdocs%2Finit.gif" alt="archstone init reading an OpenAPI document, asking what it cannot infer, and writing a manifest the compiler has already accepted" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  It's a loop, not a generator
&lt;/h2&gt;

&lt;p&gt;The first thing that makes this different from codegen: nothing is written to your directory until it has already compiled.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;init&lt;/code&gt; drafts the manifest in a temp directory, runs the &lt;em&gt;real&lt;/em&gt; pipeline over it — &lt;code&gt;load&lt;/code&gt; → &lt;code&gt;validateSemantics&lt;/code&gt; → &lt;code&gt;compile&lt;/code&gt; → &lt;code&gt;new Registry()&lt;/code&gt;, the same code path the actual compiler uses, not a lookalike — and only then commits the files.&lt;/p&gt;

&lt;p&gt;There are exactly two terminal states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a manifest that compiles was written, or&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nothing was written, and here is why&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No half-scaffold. If the draft doesn't compile, if two capabilities collide on a tool name, if the confirmed set comes back empty — zero files. You never clean up after it.&lt;/p&gt;

&lt;p&gt;That invariant is load-bearing enough that it has a bug story. During review, &lt;code&gt;commitFileSet({force: true})&lt;/code&gt; was found overlaying files onto the target with &lt;code&gt;cpSync&lt;/code&gt; &lt;em&gt;without deleting first&lt;/em&gt; — so a file left by a previous run could survive into the committed output having never been compiled. It wasn't in the temp directory that got validated. The second run would report &lt;code&gt;ok: true, failures: []&lt;/code&gt; while compiling the result actually failed with two semantic errors.&lt;/p&gt;

&lt;p&gt;Every individual function was correct. The defect lived entirely in the gap between &lt;em&gt;which file set was validated&lt;/em&gt; and &lt;em&gt;which file set reached disk&lt;/em&gt;. That's the class of bug that "just ask the model" cannot even be wrong about, because it never had the invariant in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one answer it never guesses
&lt;/h2&gt;

&lt;p&gt;CDL capabilities carry an &lt;code&gt;effect&lt;/code&gt;: &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, or &lt;code&gt;irreversible&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No OpenAPI document contains this. You might think method maps to it — &lt;code&gt;GET&lt;/code&gt; is read, &lt;code&gt;POST&lt;/code&gt; is write — and then you meet &lt;code&gt;POST /search&lt;/code&gt;, which every real API has, and the mapping is dead.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;effect&lt;/code&gt; is the difference between looking up a price and charging a card. An agent that treats one as the other is not a bug report, it's an incident. So &lt;code&gt;init&lt;/code&gt; refuses to infer it. A confirmed &lt;code&gt;effect&lt;/code&gt; exists only in a &lt;strong&gt;Decision Record&lt;/strong&gt; — an explicit artifact of a human answering — and the emitter takes the Decision Record, never an adapter's hint.&lt;/p&gt;

&lt;p&gt;Slower. Correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ambiguous is a refusal, never a guess
&lt;/h2&gt;

&lt;p&gt;The rule the inference code is built around, quoted from its own header:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AMBIGUOUS IS A REFUSAL, NEVER A GUESS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's where it bites. A response body like this:&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;"estimatedPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"warnings"&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;"dimensions rounded to nearest cm"&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;is &lt;em&gt;structurally identical&lt;/em&gt; to a paginated list wrapper. There is one array of objects in there. Pick the array and you get a collection of warnings, and you have silently dropped the price — the entire point of the call.&lt;/p&gt;

&lt;p&gt;That is not hypothetical. It happened, on a real document, to this tool. The price capability emitted a collection of &lt;em&gt;warnings&lt;/em&gt; and no price. It compiled. It passed &lt;code&gt;apply&lt;/code&gt;. It wrote files. It was found by running the tool against a real API, not by reading the rules.&lt;/p&gt;

&lt;p&gt;The fix wasn't a better heuristic. It was to stop picking: &lt;code&gt;init&lt;/code&gt; now enumerates the candidate response loci and asks which one is the payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode that would have reached your customers
&lt;/h2&gt;

&lt;p&gt;This is the subtle one, and it's why I think the whole "just generate it" approach is more dangerous than it looks.&lt;/p&gt;

&lt;p&gt;CDL fields can be &lt;code&gt;required&lt;/code&gt;. Get that wrong in the strict direction and the compiler yells at you — loud, immediate, free.&lt;/p&gt;

&lt;p&gt;Get it wrong in the &lt;em&gt;loose&lt;/em&gt; direction — mark something required that your backend sometimes returns as &lt;code&gt;null&lt;/code&gt; — and you ship a manifest that compiles, passes verification green, and then throws a contract violation the first time a real null comes back. Silent at build time. Loud in front of a customer.&lt;/p&gt;

&lt;p&gt;So the rule is narrow on purpose: &lt;code&gt;required: true&lt;/code&gt; only on &lt;strong&gt;positive evidence&lt;/strong&gt; of non-nullability — declared required, &lt;em&gt;and&lt;/em&gt; non-nullable, &lt;em&gt;and&lt;/em&gt; (if probed) present and non-null on every recorded item. Anything less is optional.&lt;/p&gt;

&lt;p&gt;The two ways of being wrong have completely different costs, so they don't get the same default.&lt;/p&gt;

&lt;h2&gt;
  
  
  It tells you what it didn't understand
&lt;/h2&gt;

&lt;p&gt;The usual failure of a spec-reading tool is silent partial comprehension: it consumes your document, emits something plausible, and never mentions the 30% it skipped.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;init&lt;/code&gt; inverts the default. Each object type declares the keys it &lt;em&gt;reads&lt;/em&gt; and the keys it argues are &lt;em&gt;inert&lt;/em&gt; — and everything else, including keys from a future OpenAPI revision and keys a contributor forgot to declare, falls through into the report rather than being dropped.&lt;/p&gt;

&lt;p&gt;Anything it can't handle gets exactly one disposition: skip it, name it with a reason code, emit nothing for it. The codes are a closed, written-down list — &lt;code&gt;no-response-shape&lt;/code&gt;, &lt;code&gt;ambiguous-collection&lt;/code&gt;, &lt;code&gt;pagination-not-modeled&lt;/code&gt;, &lt;code&gt;nested-object-not-mapped&lt;/code&gt;, &lt;code&gt;field-path-not-expressible&lt;/code&gt;, and so on. A contributor adding support for a new construct has to either reuse a code or argue for a new one in the open, rather than quietly half-handling it.&lt;/p&gt;

&lt;p&gt;Every fact in the intermediate model also carries its derivation — &lt;code&gt;declared&lt;/code&gt; (from the spec), &lt;code&gt;observed&lt;/code&gt; (from a real response), or &lt;code&gt;absent&lt;/code&gt; — so a classification made from three observed items says so, instead of presenting itself as a measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optionally, it calls your backend once
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;--probe&lt;/code&gt;, &lt;code&gt;init&lt;/code&gt; will make one read-only request and record a real fixture, so &lt;code&gt;archstone verify&lt;/code&gt; has something true to replay later.&lt;/p&gt;

&lt;p&gt;The consent model is deliberately annoying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;off by default&lt;/li&gt;
&lt;li&gt;asked per capability, not once for the run&lt;/li&gt;
&lt;li&gt;never issued for a capability whose confirmed &lt;code&gt;effect&lt;/code&gt; isn't &lt;code&gt;read&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a non-&lt;code&gt;GET&lt;/code&gt;/&lt;code&gt;HEAD&lt;/code&gt; method needs a second, separate confirmation&lt;/li&gt;
&lt;li&gt;refused outright when there's no terminal to ask at&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the recorded fixture is written by the &lt;em&gt;same&lt;/em&gt; &lt;code&gt;recordContract()&lt;/code&gt; in the runtime, over the &lt;em&gt;same&lt;/em&gt; &lt;code&gt;invokeRest&lt;/code&gt; call, that &lt;code&gt;verifyTool&lt;/code&gt; uses. So the fixture &lt;code&gt;init&lt;/code&gt; writes is by construction the artifact &lt;code&gt;verify&lt;/code&gt; later replays — not a lookalike that drifts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; @archstone/cli
archstone init openapi.yaml &lt;span class="nt"&gt;--out&lt;/span&gt; manifest &lt;span class="nt"&gt;--company&lt;/span&gt; acme &lt;span class="nt"&gt;--domain&lt;/span&gt; catalog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a spec in the repo you can run it against without pointing it at anything of yours (&lt;code&gt;examples/demo/stays-openapi.yaml&lt;/code&gt;), and a live compiled capability you can add to Claude in about thirty seconds, no install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://demo.archstone.dev/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apache-2.0: &lt;strong&gt;&lt;a href="https://github.com/Archstone-Romania/archstone" rel="noopener noreferrer"&gt;https://github.com/Archstone-Romania/archstone&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;The broader argument — why any of this is a compiler and not just another MCP server — is &lt;a href="https://archstone.dev/why-a-compiler-not-the-fifth-mcp-server" rel="noopener noreferrer"&gt;here&lt;/a&gt;. This post is the part you can run.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>opensource</category>
      <category>architecture</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why I built a compiler, not the fifth MCP server</title>
      <dc:creator>Adrian Bratulescu</dc:creator>
      <pubDate>Fri, 31 Jul 2026 19:53:27 +0000</pubDate>
      <link>https://dev.to/irutehe/why-i-built-a-compiler-not-the-fifth-mcp-server-4gba</link>
      <guid>https://dev.to/irutehe/why-i-built-a-compiler-not-the-fifth-mcp-server-4gba</guid>
      <description>&lt;p&gt;Writing your first MCP server is not hard. It is a few hundred lines, and you can have one running before lunch. I know, because I wrote one.&lt;/p&gt;

&lt;p&gt;The problem is the fifth one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the work
&lt;/h2&gt;

&lt;p&gt;Here is what actually happens when a company decides its capabilities should be reachable by AI agents.&lt;/p&gt;

&lt;p&gt;You write an MCP server. It works. Then ChatGPT wants the same capability shaped slightly differently. Then Gemini. Then whatever ships next quarter, because something always ships next quarter. Each one is a separate integration project with its own SDK, its own idea of what a tool definition looks like, and its own release cadence.&lt;/p&gt;

&lt;p&gt;And underneath all of them, your own API keeps changing — a field gets renamed, a response becomes nullable, an endpoint moves. Now you have four hand-written integrations that are each independently wrong, and no build step that will tell you.&lt;/p&gt;

&lt;p&gt;The first server is an afternoon. The fifth is a team.&lt;/p&gt;

&lt;p&gt;So the question I got stuck on was not &lt;em&gt;how do I write an MCP server&lt;/em&gt;. It was: &lt;strong&gt;what is the artifact that survives when the protocol changes?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One definition, many emitters
&lt;/h2&gt;

&lt;p&gt;That question has a boring, well-understood answer, and it is the same answer compilers have been giving since the 1970s: put an intermediate representation in the middle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;capabilities.yaml  →  semantic model  →  IR  →  emitter  →  MCP tools
                                          ↘  emitter  →  SDK / REST / GraphQL / …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You describe a capability once, in business terms — what it does, what it needs, what it returns, and whether calling it is safe to retry. No HTTP, no JSON Schema, no SDK. That lowers to a target-agnostic IR, and emitters consume the IR.&lt;/p&gt;

&lt;p&gt;The whole &lt;code&gt;tourism.search&lt;/code&gt; capability in the demo is twelve lines:&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="na"&gt;capability&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tourism.search&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;Find places to stay matching a traveler's intent.&lt;/span&gt;
  &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;location&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;dates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;       &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;date-range&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;travelers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;party&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;budget&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;money&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;false&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;stays&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;collection&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Stay&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;booking-engine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change the protocol and you regenerate. Change the backend and the definition does not move at all — only the binding does, which is a separate file that never appears in the business description.&lt;/p&gt;

&lt;h2&gt;
  
  
  The evidence that the IR is the product, not MCP
&lt;/h2&gt;

&lt;p&gt;This is the part I would want to see if someone else were making the argument, so here it is.&lt;/p&gt;

&lt;p&gt;The system that most depends on Archstone in production &lt;strong&gt;does not speak MCP at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://artvinci.ro" rel="noopener noreferrer"&gt;ArtVinci&lt;/a&gt; is a custom-framing workshop. Its site runs an assistant that answers customer questions — real catalog, real prices computed live by its own backend. It does not run an MCP server process. It loads the compiled IR directly:&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;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;archstone&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;framing.estimate-frame-price&lt;/span&gt;&lt;span class="dl"&gt;"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same IR, same validation, same fail-closed response mapping as the MCP path — just no separate process to deploy. MCP is one emitter. If it had been the product, that deployment would have been impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that is actually hard
&lt;/h2&gt;

&lt;p&gt;Generating a tool definition is the easy half. The half that decides whether an agent gives your customer a wrong answer is the response mapping.&lt;/p&gt;

&lt;p&gt;A binding says where each field of a declared resource comes from in the provider's payload, and which of them are required. When a required field is missing, the call fails closed — a structured error, never a silent pass-through of whatever the backend happened to return. A missing optional field degrades instead. Those two words, &lt;code&gt;DEGRADED&lt;/code&gt; and &lt;code&gt;VIOLATION&lt;/code&gt;, are the difference between an agent saying "I don't have that" and an agent inventing a price.&lt;/p&gt;

&lt;p&gt;And because a provider will eventually change its payload without telling you, each binding carries a recorded fixture and a fingerprint. &lt;code&gt;archstone verify&lt;/code&gt; replays the recorded request against the live backend on demand and reports a health status per binding, so contract drift surfaces as a red line in your CI rather than as a customer complaint.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But I could just paste my OpenAPI spec into Claude"
&lt;/h2&gt;

&lt;p&gt;Yes. You could, it would take ninety seconds, and it would cost you nothing. This was the sharpest objection anyone raised while I was building the onboarding command, and I think it is correct as far as it goes.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;archstone init&lt;/code&gt; is deliberately not a code generator. It is a loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It reads your OpenAPI document and proposes candidates.&lt;/li&gt;
&lt;li&gt;It asks you the questions no document can answer — chiefly whether a call is &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt; or &lt;code&gt;irreversible&lt;/code&gt;, which is the difference between "look up a price" and "charge a card."&lt;/li&gt;
&lt;li&gt;It runs &lt;strong&gt;the real compiler&lt;/strong&gt;. If the manifest it drafted does not compile, it writes nothing at all. There is no "mostly works, fix the errors yourself" mode.&lt;/li&gt;
&lt;li&gt;Optionally, it calls &lt;strong&gt;your real backend&lt;/strong&gt;, once, read-only, and records a genuine fixture — then replays it through the shipped mapper to check that its own mapping actually holds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A language model can write plausible YAML. It cannot run your compiler or call your API. That is the entire difference, and it is why every unit of effort went into the loop rather than into nicer field names.&lt;/p&gt;

&lt;h2&gt;
  
  
  An example of it being wrong, because that is more useful
&lt;/h2&gt;

&lt;p&gt;The first time I pointed &lt;code&gt;init&lt;/code&gt; at a real spec, it produced a price-estimation capability whose entire output was a list of advisory warnings. No price.&lt;/p&gt;

&lt;p&gt;The rule was working exactly as designed: find the array of objects in the response, make it the resource. In that response the only array of objects was &lt;code&gt;warnings&lt;/code&gt;. The payload — the price, the currency, the geometry — sat in scalar fields beside it and got dropped.&lt;/p&gt;

&lt;p&gt;It compiled. It passed validation. It wrote files. An agent calling that tool would have received a list of warnings and no price, and nothing anywhere would have flagged it.&lt;/p&gt;

&lt;p&gt;The fix was not a cleverer heuristic. Distinguishing "the array is the payload and the scalars are pagination" from "the array is diagnostics and the scalars are the payload" cannot be done from the document — the two shapes are structurally identical. So &lt;code&gt;init&lt;/code&gt; now enumerates the candidates and asks, once, with the field names shown. One candidate, no question.&lt;/p&gt;

&lt;p&gt;That is the general posture, and it is the only thing keeping a tool like this honest:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An ambiguity is a question or a refusal, never a guess.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What it costs you
&lt;/h2&gt;

&lt;p&gt;You write YAML describing your business. That is the trade. If your capabilities are genuinely one endpoint and you never expect a second protocol, hand-writing an MCP server is the right call and you should do that instead.&lt;/p&gt;

&lt;p&gt;The compiler earns its keep at the point where you have several capabilities, more than one consumer, and a backend that changes — which is to say, at the point where the hand-written version stops being an afternoon.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Archstone is Apache-2.0: &lt;a href="https://github.com/Archstone-Romania/archstone" rel="noopener noreferrer"&gt;github.com/Archstone-Romania/archstone&lt;/a&gt;. A capability compiled by it is live — paste &lt;code&gt;https://demo.archstone.dev/mcp&lt;/code&gt; into Claude's custom connectors and ask about a trip.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>showdev</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
