The need for this work surfaced while operating in a heavily regulated context like AI for Public Administration, where each assistant should only be given the minimum set of data and documents it needs to answer. So every new agentic assistant for a client required dedicated tools, mostly different flavors of RAG implementing whatever filters that client called for.

Alternatives and their downsides

What followed was multiplied development and maintenance and a growing vulnerability surface. The diametrically opposite alternative, granting fully agentic access to systems and data with nothing but a prompt as the boundary, doesn’t hold up in a context regulated on auditability and security. External MCP servers didn’t fully solve the problem either: on the very same resource (say, a Weaviate collection), different users of the system had to see different parts of the documentation, and not others. There were simply too many filters and access patterns to bake them into an MCP spec attached to the resource being consumed.

4–5d → ½d agent build time
~10 lines of YAML per tool
0 code to ship a new tool

Primitives and YAML composition

So I consolidated the fragmented tools into a single internal MCP server (STDIO) that separates the primitives, maintained centrally (semantic search, exact metadata filtering, metadata discovery, conversational memory, API calls), from their composition into actual tools, declared in a YAML file: about ten lines of configuration per tool, no code. The server consumes the configuration and produces tools already scoped to the boundaries of each individual agent.

Three families of tools coexist in the runtime catalog: static ones typed as Python classes in the repository (the legacy of the previous approach), those routed to external MCP servers, and those served by the internal MCP server. When the agent invokes a tool, the runtime looks it up in the catalog and knows where to route the call:

              agent (tool call, OpenAI format)
                            |
                            v
                  runtime tool catalog
                            |  lookup: which kind is it?
          +-----------------+-----------------+
          v                 v                 v
    static tools       external MCP       internal MCP
    (Python classes     servers           server (STDIO)
     in the repo)                             |
                                              v
                                    binding from the YAML:
                                    primitives + filters

To the agent the three families are indistinguishable: routing and boundaries live below the catalog.

Declarative layer: what the model sees

The model only sees each tool’s name, description, and arguments: the binding section, the one declaring which primitive to use and which filters to apply, is invisible to it. And that is where the value lies in a regulated context. A filter can take its value from an argument of the tool call, but also from a constant fixed in the configuration or from the runtime hosting the assistant (say, the authenticated user’s organization): in the latter two cases the boundary holds by construction, with no need to trust the model to pass the right parameter, because the model can neither see the filter nor tamper with it. The principle is to move every boundary known in advance into static, versionable artifacts, reducing the model’s autonomy where it isn’t needed.

Binding layer: how three tools map to one primitive

An example makes the point: three tools with different names, arguments and boundaries for three assistants, the same primitive underneath (names are made up, the structure is conceptually the correct one):

search_tender_docs   search_org_records     search_faq
(tender_id, query)         (query)            (query)
         |                    |                  |
         v binding            v binding          v binding
 filter: tender id       filter: org     filter: doc type
 value <- argument    value <- runtime    value <- "faq"
    (tender_id)         (user's org)        (constant)
         |                    |                  |
         +--------------------+------------------+
                             v
                primitive: semantic search
             (one centralized implementation)

Where external MCP servers use the protocol for interoperability, the point here is reuse: many tools in the agents’ eyes, a single implementation to maintain and review.

The effect on development

Building an agent for a new client went from 4-5 days to half a day, spent on understanding the domain rather than on implementation. Security review applies to a small set of centralized primitives, fixes propagate automatically to every tool that uses them, and the bottleneck shifts from developers to domain experts, with a back-office UI on the way for composing tools autonomously.

Independent of the orchestrator

Building this tool-configurability layer on the MCP protocol also keeps it independent of the orchestrator wrapping it, be it a plain agentic loop, a graph system like LangGraph, and so on. All the orchestration system needs is an MCP connector, a requirement that is increasingly standard given the traction the protocol proposed by Anthropic is gaining.

The full architecture and the alternatives we ruled out are in the article on Agenda Digitale (in Italian).