Filippo D'Errigo Ideas, notes & experiments
Source Code Live

Duckburg Smart City

.NET MCP Gemini Claude
Duckburg Smart City
Source code github.com/fderrigo/DuckburgSmartCity

A complete digital municipality, built end to end and published as open source. A portal in the Designers Italia style with its own CMS, SPID and CIE digital identity with real federation, and an assistant that answers citizens only from the municipality's certified content.

It runs on an imaginary municipality, the Municipality of Paperopoli (Duckburg). The municipality is imaginary, not the software.

Three parts, one municipality

  • The portal and its CMS. The Municipality's pages built on the Comuni model, and the back office where the editorial team writes them.
  • Digital identity. An OIDC Federation Relying Party that speaks the Italian SPID/CIE profile, and the citizen services portal that consumes its login.
  • ChattyDuck. The virtual assistant, and the chain that powers it: a corpus of certified content exposed through an open protocol.

ChattyDuck is the most visible part, but it is only one part.

The assistant, in one line

The system prompt defines only the model's behavior. The content lives solely in the corpus. If a piece of information is not in the source, the assistant doesn't make it up: it says it isn't in the sources.

The models, Gemini and Claude, cite the identifier and version of every passage they retrieve.

Content is born where it is written

The corpus is not a file: it is a service, fed by the people who actually write the content. The editorial team publishes an entry in the CMS and sees it appear in the assistant's answers, without anyone touching a file.

Between the CMS and the corpus there is a single rule, and it holds everything else up. A piece of data becomes a fact only if it was already structured in the CMS. The cost of a service, a deadline, an opening time: those are attributes, and they can be queried. Everything else stays prose, and prose is never squeezed to extract facts. A regular expression that guesses an amount inside a paragraph produces data that looks certified and isn't.

Every piece of content carries its own validity period. A deadline that has passed is not presented as current: it is reported as expired.

Plugging it into another CMS

The corpus doesn't know the Paperopoli CMS. It speaks the vocabulary of the Comuni model: content types, attributes, relationships, sections. The schema is published by the service itself.

For another CMS, you only rewrite the adapter, which is the smallest piece of the chain. The corpus validates what it receives and responds with a precise list of errors and warnings, so an adapter can be fixed until it is clean without ever touching the live corpus. And it doesn't have to be .NET: the corpus speaks HTTP and JSON.

Two models, two ways of connecting

This is the part worth looking at closely.

  • Gemini doesn't speak MCP natively. The portal acts as a bridge: it translates the server's tools into the format Gemini understands and makes the calls on its behalf.
  • Claude speaks MCP natively. It connects directly to the server's public endpoint, with no bridge. All it needs is the address.

Same corpus, two different paths. For Gemini I wrote the bridge. For Claude it wasn't needed. Where the open standard is truly supported, the integration work disappears.

Anyone can connect

The corpus is not locked inside my portal. Any MCP client can consume it by pointing to the server's public endpoint. On the assistant's page there is a ready-to-copy configuration for connecting your own chatbot.

The municipality exposes the source once. Then anyone can connect, with the assistant they prefer.

When the source isn't there

A small detail that says a lot about how the rest is designed.

If the corpus is not yet aligned, the tools don't return an empty list: they return an error that tells the model to warn the user. The model would read an empty list as "this information doesn't exist", and would tell the citizen that the Municipality doesn't handle it.

An empty list, in an answer that presents itself as certified, is a lie.

Digital identity: SPID and CIE

Nothing here is simulated except the counterparts.

The Relying Party implements the Italian OpenID Connect Federation profile in full: trust chain resolved at every login, signed and published entity configuration, automatic client registration, authorization request in a signed request object, PKCE, private_key_jwt authentication, encrypted UserInfo, refresh token and RP-initiated logout.

It is not a mock of the SPID login. It is the protocol.

On the other side of the federation are the Trust Anchor and the CIE OpenID Provider: they are the official AgID images, italia/spid-cie-oidc-django, pinned by digest so the environment is reproducible. So it is not my code telling me the flow is correct: it is the reference implementation that accepts it. The full cycle, CIE login, refresh and logout, is covered by an end-to-end test.

A detail I enjoy: the central bodies of the federation are not the Municipality, and they don't pretend to be. They belong to the imaginary State of Palmipedia, with their own brands and palettes. AIDP in place of AgID, BeccoID in place of a private SPID provider, IPZP in place of the Poligrafico. The fiction is declared, not hidden.

What it takes to move to the real providers: the hostnames, the keys, the trust mark issued by the federation authority, onboarding with AgID, and three points in the code to change, including the list of providers shown on the login page, which today deliberately doesn't show the buttons of the real providers. It is not "just configuration", and it's honest to say so. But it is a distance of compliance, not of architecture.

Citizen services

After logging in, the citizen enters their personal area and finds their digital identity data: first name, last name, tax code, and the panel that says which identity they used to log in and who manages it.

Then the list of applications, which is empty, and says so:

There is nothing here yet: this is the Municipality's first online service and digital procedures will arrive one at a time.

It is worth being precise, because this is the difference between a demo and a promise. What is built is the authentication chain, not the services. Online TARI and registry certificates are announced cards, not working ones. The services portal is a separate application from the information portal: the two talk to each other with a signed, short-lived SSO token.

Building the door before the rooms is deliberate. The door is the hard part, and anyone who has tried to build one knows why.

Compliance, measured

There is also a module that runs the official pa-website-validator-ng validator against the portal and shows its report. Compliance with the Comuni model of measure 1.4.1 is not a self-declaration at the bottom of the page: it is a score anyone can reproduce.

How to try it

The assistant is online. The test questions that demonstrate its grounding in the source are these:

  • "When is the first TARI installment due?" It answers April 30 and cites the passage.
  • "How much does school lunch cost?" It gives Paperopoli's fee range, citing the costs section.
  • "What events are there?" It lists the valid events, each with its own identifier.
  • "What day is organic waste collected in the Vesuvio neighborhood?" It answers that it isn't in the sources, because that neighborhood doesn't exist.
  • The same question asked to different clients gives the same answer, anchored to the same passages.

The SPID/CIE login can be tried locally: you bring up the federation with docker compose, three containers, and log in with the demo credentials.

What's real and what's fake

The municipality is simulated, and only the municipality. The streets, the neighborhoods, the deadlines, and the State of Palmipedia around it.

Everything else is real: the CMS, the corpus, the MCP protocol, the OIDC federation, the validator, the models and the way they respond.

Between Paperopoli and a real municipality, the content changes, which is the municipality's job and not an IT matter, and the adapter changes, if the CMS is different. The corpus, the server, the identity and the assistant stay the same.

The code is public, under the Apache 2.0 license: you can fork it and use it as the basis of a product, even a commercial one. Try it, open it up, put it to the test.

Share
Back to the Lab