cd /news/ai-agents/the-agent-protocols-stopped-at-the-c… · home topics ai-agents article
[ARTICLE · art-130127] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

The Agent Protocols Stopped at the Client — Let's Talk About the Server Half

A developer has released Franca, an open-source Java 21 library published to Maven Central that implements the server side of agent wire protocols, terminating OpenAI Responses and Anthropic Messages dialects inside a developer's own service. The library, hosted via Spring Boot 3, uses pluggable adapters and drivers to let applications own the server half of the agent stack rather than relying on vendor-hosted implementations or client-side prompt bundles.

by read8 min views3 publishedSep 15, 2026

Every few weeks another "skills" announcement lands. Prompt bundles, client-side toolkits, SDK-level orchestration patterns. I read them the way you probably do — genuinely interested, quietly uneasy. The industry's channel for developer agency in the agent stack has ended up almost entirely on the client side. Which is strange, if you think about where capabilities actually have to run: against your data, your environment, your governance. Server-side.

This post is about a gap that sits precisely there, and an open-source Java library I shipped to Maven Central to fill it.

OpenAI Responses and Anthropic Messages are becoming the wire protocols for agents. Look at what they specify on the client's half and it's genuinely impressive: conversation shape, tool declarations, reasoning knobs, structured events, streaming semantics. A client SDK that speaks one of these can talk to anything that speaks it back.

Now look for the server's half. Which function actually answers a declared tool type? Where does it run? Should there be a retrieval pass before the answer? What can the server inject, observe, or veto?

The protocol is silent on all of it. And silence has a consequence: whoever implements the server owns your application layer.

Today the server half lives in one of three places:

/v1/responses too — but as a raw text-in/text-out endpoint. They say the words of the dialect without the agent semantics behind them. So the division of labor we all claim to want — model capability belongs to model vendors, application capability belongs to application developers, the wire protocol is the border — only got implemented on one side. On the other side, your options are: rent the vendor's server half, route through a translator, or accept a very smart autocomplete.

The industry's answer to the remaining developer agency is skills. Client-side prompt bundles. That's what you get when the server belongs to someone else.

Franca is my attempt at the missing piece: a Java 21 library that terminates both dialects inside your own service, and turns the server half of the protocol into an ordinary development surface.

The bottom boundary is any inference backend — currently an OpenAI-compatible chat driver, more via pluggable drivers. The top boundary is your /v1/responses and /v1/messages endpoints. The middle is yours.

Dependencies (Spring Boot 3 host):

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.github.franca-protocol</groupId>
      <artifactId>franca-bom</artifactId>
      <version>0.18.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>io.github.franca-protocol</groupId>
    <artifactId>franca-spring-boot</artifactId>
  </dependency>
  <dependency>
    <groupId>io.github.franca-protocol</groupId>
    <artifactId>franca-spring-tools-local</artifactId>
  </dependency>
</dependencies>

Configuration is three blocks — adapters decide which dialects you terminate, drivers decide how to speak to backends, routes map aliases to targets:

franca:
  adapters:
    - name: responses
      type: io.franca.adapter.responses.ResponsesAdapter
  drivers:
    - name: openai-chat
      type: io.franca.driver.chat.OpenAiChatDriver
  routes:
    - alias: gpt-5.6
      target:
        driver: openai-chat
        address: https://your-openai-compatible-backend
        model: your-model
        metadata:
          api-key: ${BACKEND_API_KEY}

With that, your service is a standard OpenAI-compatible server. Clients point base_url at you and change nothing else.

Both boundaries are pluggable, deliberately. The two dialect adapters and the chat driver are what ship today, but neither boundary is sealed: a new dialect is a new adapter implementation, a backend that isn't plain OpenAI-compatible chat is a new driver — ordinary SPI implementations, not forks. That matters because backends churn faster than protocols; you should be able to follow the backend without touching your capability code.

The hosted-tools asymmetry is my favorite part. The client declares a capability; the server decides what serves it.

A server-side function is one bean:

@Component
public class HashToolHandler implements HostedToolHandler {

    @Override
    public String toolType() {
        return "hash";
    }

    @Override
    public Mono<String> execute(String callId, String arguments) {
        // arguments: the JSON the model produced for this call.
        // Return a JSON string; it is fed back to the model for the next turn.
    }
}

The client declares only the type — no function body, no schema:

curl -X POST http://localhost:8080/v1/responses \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.6",
    "tools": [{"type": "hash"}],
    "input": "Compute the SHA-256 digest of 'hello franca'."
  }'

What actually answers type: "hash" is entirely your call: that local bean, any MCP endpoint (namespaced, with model-visible aliases), or a fleet of cloud-vendor-hosted functions — Alibaba Cloud Function Compute, AWS Lambda, and friends. The model never learns where the code runs. The orchestration in between — call, execute, feed back, loop — is collapsed by the server into one continuous Responses event stream.

One SPI detail worth flagging, because it's where per-request context enters. The minimal form above is all a stateless tool needs, but the interface carries more when you want it: a default overload hands your handler the request's metadata field (session ids, trace ids, billing tags — whatever the caller sent), and a streaming form lets a tool emit progress events while it runs. The metadata channel is the interesting one for this argument: it's exactly how per-user context reaches server-side tool execution — from the request to your handler, inside your process, never through a third party.

Notice what the client did not do. It installed nothing. No environment to set up, no dependencies, no credentials granted — even in the case where the tool is calling services that already belong to you. That's the part of the skills model I could never reconcile: installing a skill means installing its world — dependencies, environment, authorization — on the client, per client, even when the capabilities live on your side of the wall. A hosted tool keeps the capability where your data, your credentials, and your permissions already live; the client's entire footprint is one declared type. In that mode you can add a platform function without changing a line of code — existing clients just start declaring it.

For private deployments this is the only shape that works: internal data sources can't be shipped to a vendor's hosted web_search, and you don't want credentials distributed to every client that might call you.

And note what bring-your-own-tools via the platform actually costs: to let the platform's runtime call your MCP endpoint you either hand over your credentials or open your network to it — and the first time your tool needs per-user authorization, your users' auth tokens are passing through a third party. The privacy question isn't only where the data lives. It's who holds the keys while the model works.

The declaration syntax stays on the wire untouched. The implementation right comes back to the server. For deployments that are heavy server-side and deliberately thin client-side, that's the whole game.

The one place I've deliberately stuck my neck out.

In the protocol, reasoning is a knob for how deeply the model thinks. Franca reinterprets it as a slot for server-side thinking strategies (a ThinkingPhase SPI). Request reasoning.effort: "research", and the server runs a retrieval pass through hosted tools first — streamed back as reasoning events so the client watches it look things up — then hands the findings to the answering pass. The original request context stays byte-identical: retrieval never pollutes your conversation history, which is the part of deep-research implementations that quietly rots.

There's a second motivation that took me longer to articulate. Planned, fixed-shape chains — the LangChain-style workflows everyone keeps rebuilding as client-side orchestration — are a genuinely strong need that is architecturally homeless. A chain isn't conversation and isn't a tool; it's how the server should think through the task. The thinking region is where it belongs: a phase can perceive (call hosted tools, gather signals) and guide the answering pass, and none of it lands in the visible context.

And because a phase is an ordinary artifact, the ecosystem story inverts. Skills distribute prompt bundles to the client; thinking phases distribute behavior to the server. Write one once, publish it, and another deployment enables it by importing the artifact and filling in a key in the reasoning parameter.

Reinterpreting reasoning.effort as a server behavior switch is not something the spec says. There is a "controversial decisions" doc in the repository defending it, and I'd genuinely like pushback on it.

The boring-but-necessary sibling: backends spell "thinking" differently (reasoning-effort, thinking-disabled, enable-thinking, …). Franca normalizes this per route via thinking-style and effort-value-map, so heterogeneous backends get one consistent semantic.

Because gateways answer a different question. A gateway answers "many upstreams, one entry point, unified billing" — traffic in the front door, out the back. Franca answers "where does the protocol terminate, and who gets to extend it":

A gateway is a new member of your architecture. A library is a part of your service. They solve access and capability respectively, and there is room for both — but only one of them hands you back the server half of the protocol.

The last option to close out is doing it yourself — the wire formats are public, after all. The honest tally of "yourself" is: extensible platform functions, two dialects' event streams, the tool call/execute/feed-back loop, protocol compatibility and reasoning normalization across heterogeneous backends, and the plumbing that keeps all of it streaming. None of it is your product. It's the server half of the protocol, rebuilt from scratch — which is the work this library packages, once.

io.github.franca-protocol:franca-bom:0.18.0 franca-example ships with both dialects, MCP tools, and the retrieval phase enabled Model capability belongs to the vendors. Application capability should belong to you. The protocol was supposed to be the border — it just stopped halfway. Franca is a attempt to finish drawing it.

If you think the server-side gap looks different from where you sit, or that skills and client-side agency are enough — I'd like to hear the argument.

── more in #ai-agents 4 stories · sorted by recency
── more on @franca 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/the-agent-protocols-…] indexed:0 read:8min 2026-09-15 ·