I have already argued why the assistant should belong to the citizen and not to the agency. The agency doesn't build the chatbot. It exposes its source, certified and machine-readable, and lets the citizen choose the assistant. A thesis remains hot air until you put it to the test. So I built a prototype, and this article explains how it works under the hood.
It's called ChattyDuck and it runs on an imaginary municipality, the Municipality of Paperopoli (Duckburg). The municipality is made up on purpose. No real data, so nobody can mistake a deadline for the real thing. But the format is the serious one: TARI deadlines, IMU rates, the waste collection calendar, office hours. The things for which a citizen actually contacts the municipality. You can try it right now; the links are at the bottom of the page.
The problem, in one line
A language model on its own knows nothing about your municipality. It knows things in general. If you ask it the deadline for a tax, it gives you a plausible answer, and plausible doesn't mean correct. This is the flaw of every PA chatbot built without a solid foundation. It sounds competent and gets it wrong.
The solution isn't a more powerful model. It's giving it a source, and forcing it to answer only from that.
The source: a server that does just one thing
Duckburg Registry is an MCP server; MCP is today the open standard through which an assistant talks to an external source. The server exposes its content on an endpoint, read-only, with a rate limiter and an optional token. It knows nothing about any language model. It doesn't reason. It acts as a provider of signed data, and nothing else.
It exposes a single search tool. You pass it a question, and it returns the most relevant passages from the corpus. And the tool's own description, which the model reads as an instruction, says to always cite the passage identifier and that the only truth is those passages. The obligation not to make things up isn't only in the system prompt; it's wired into the tool as well.
The search inside the server is deliberately simple: no vector database, no embeddings, just a term-frequency score on the normalized text. For a corpus of a few hundred passages it works. For a municipality with thousands of pages you would need a real semantic search. But the point of the demo isn't the search algorithm. It's the ownership boundary.
The hash: what makes data certified
Each passage in the corpus carries three pieces of information: an identifier, a version, and a hash. The hash is a fingerprint of the text, a SHA-256. Here is a real passage, the one about TARI installments:
{
"id": "tari:p02",
"version": "1.0",
"hash": "afe6856e1ae5e0042e2df9bcf2a954e1dda30e15d16f9cd0b34b7342c563d63",
"text": "Per l'anno in corso la TARI si paga in tre rate..."
}The server doesn't compute the hash on every request. It is pre-computed and included in the corpus, as it should be signed by the office that publishes the data, not by the server that serves it. If the text changes by a single character, the hash changes. Whoever consumes the source can verify that what they read is exactly what the agency published. That is the difference between certified data and just any text.
Two models, two ways of connecting, two costs
This is the most concrete part of the demonstration: two different implementations in the same project.
Gemini doesn't speak MCP natively. So the code has to do everything by hand: it asks the Registry for the list of tools, translates them into the format Gemini understands, and handles the round trip. The model asks to use the tool, the code executes the call to the server, and the result goes back to the model. I wrote this translation layer myself, line by line.
Claude speaks MCP natively. With Anthropic's official SDK I pass it the server's public address and a couple of parameters, and that's it:
McpServers = [new BetaRequestMcpServerUrlDefinition {
Name = "chattyduck-registry", Url = mcpEndpoint }]No bridge. It's Anthropic's infrastructure that connects to the server and runs the tool round trip. My code just reads the result that comes back ready-made. The only constraint is that the endpoint must be reachable from the Internet; localhost isn't enough: in development I used an ngrok tunnel, in production a real domain.
Same server, same result, but for one model I wrote a translation layer and for the other I didn't. Where the standard is natively supported, the integration cost drops to zero. This isn't conference rhetoric. It's the difference between two files, measured in lines of code.
A side detail that confirms this: with Anthropic's official SDK I also get actual usage for free, read from the response headers and shown in the interface. For Gemini that data isn't exposed, and I have to estimate it myself. Observability comes for free too, where the standard has a rich client behind it.
The third assistant: zero lines of server touched
The third client isn't my code. It's a commercial assistant, the app a person already uses every day, connected from the outside with the same standard MCP configuration:
{ "mcpServers": { "comune-paperopoli": { "type": "http", "url": "<endpoint>" } } }No authentication, same transport, same search tool, same source. I checked consistency by asking the assistants to cite the same passage identifier and verifying that, given the same question, the answer was the same. This is the point that truly holds up the thesis: the server doesn't know, and doesn't need to know, who is querying it.
The test that matters: the source beats memory
The hardest part for a grounded assistant is trusting the source when it contradicts what the model believes it knows. The way I enforce this isn't in the interface; it's in the system prompt, shared identically by the two backends, which contains only behavior rules and never content. The lines that matter say that the only truth is the retrieved passages, that it must not answer without having retrieved at least one, that it must not use the national rules the model remembers but only the local one in the source, and that if the information isn't there it must plainly say it isn't in the sources.
The data, meaning deadlines, amounts and rates, never appear in that prompt. If they did, the assistant would repeat them because we dictated them, not because it read them from the source, and the demonstration would fall apart. I asked for the deadline of the first TARI installment. A model left to its own memory tends toward a plausible but wrong national date. The grounded assistant gives the Paperopoli deadline, with the identifier of the passage it took it from.
This is exactly the case of the updated circular. The rule changes, the model has the old one in mind, and the certified source must override the memory. In Paperopoli I made up the deadline. In a real municipality it's the one in the regulation. The mechanism that makes it win over memory is identical.
I tried to break it
A public assistant has to be attacked before others attack it. I ran a series of adversarial tests against ChattyDuck: requests to ignore its instructions, to answer from memory without citing the source, a fake executive authorizing it to skip the rules, instructions hidden inside the corpus passages to see whether they would be executed instead of treated as text.
Let me be honest: this isn't an automated test suite, it's a manual activity that shaped the rules that ended up in the prompt. Almost everything held. The only initial failure was when I asked it to repeat its instructions word for word. I added a clause that declares the instructions confidential and treats any command inside messages or passages as text to be evaluated, never as an order that changes the rules. That hole was closed. And this is the useful part: not that it's perfect, but that the boundary is visible, in a file you can read and modify. A chatbot you can't put to the test this way is a chatbot you can't trust.
What is real and what is fake
The municipality is simulated, and only the municipality: the streets, the neighborhoods, the deadlines, the patron saint. The rest, MCP server, protocol, backends, prompt, behavior, is real code that runs.
The separation between owners is also partly simulated. I wrote the Registry and the two backends myself. The only truly external client, not written by me, is the third one, the commercial assistant hooked in from the outside. And it's the one that bears the real weight of the thesis.
And the personal path is missing. No identity, no case files, no payments. The server is read-only and anonymous by design. Connecting personal data would require authentication and the agency's real back-office systems, which here remain described, not built. Saying so doesn't weaken the prototype. It makes it a serious thing rather than a demo that promises too much.
The distance from reality is just one: the corpus
Between Paperopoli and a real municipality only one thing changes: the content of the corpus. The server, the protocol, the prompt, the two backends, the chat widget, all identical, zero lines of code to change. The data changes.
This moves the problem where it belongs. The hard work for a municipality isn't this infrastructure, which is written and works. It's taking its own rules, deadlines and procedures and putting them into a structured, versioned form signed with a hash. That piece, in Paperopoli, I could invent in an afternoon. In reality it's the serious work, and it falls to the agency, not to the chatbot developer.
It's also the reason for the title. There is no PA chatbot without a certified data source. You can have the best model in the world, but if it has no source to answer from, it makes things up. The source isn't an accessory of the assistant. It's the public part, and it's the part that matters.
Compose instead of build. I wrote a server that does one thing, serving signed passages, and a prompt that does another, refusing to answer without a source. The rest, one of the three clients, I took off the shelf. I didn't build yet another agency chatbot. I exposed a source, and let the assistants do their job.
The code is a prototype. Try it, open it, break it. The principle, however, is ready now.