How I Built a 375-Tool Enterprise MCP Server for Odoo (Architecture Deep Dive)
The exact architecture patterns from a 17-session, 34-module Odoo Enterprise MCP build — including what broke and what held.
I spent 17 sessions turning a 159-tool monolithic MCP server into a full Odoo Enterprise control plane with 375 registered tools across 34 modules. This post is the architecture I wish I had before starting — the patterns that scale, the constraints that shaped every decision, and the specific things that broke and forced better designs. If you are building an MCP server that will grow past a handful of tools, or integrating any large SaaS system with an AI agent, the patterns here are directly copyable.
This is not a "what is MCP" post. It is a case study on enterprise MCP architecture — what changes when you have to expose a real business system with hundreds of endpoints, dozens of module boundaries, and a live production instance you cannot break. The system in question is Odoo 19 Enterprise running at purplecrib.odoo.com, and the client is my own agency, Purple Crib Studios.
What you will learn
- What an MCP server is (and why enterprise changes everything)
- From monolith to modular — the registry pattern
- Conditional module registration for gated features
- The capability matrix — map the boundary before you build
- Blast-radius confirms for send-tools
- The AI Layer — composition, not magic
- What broke and what the architecture held
- Final thoughts
1. What Is an MCP Server (and Why Enterprise Changes Everything)
An MCP server exposes tools, resources, and prompts to an AI assistant like Claude over a standardized JSON-based protocol. It lets a language model perform real actions in external systems — read a database, send an email, update a record — through a defined tool contract. The Model Context Protocol specification is maintained by Anthropic and is now an open standard.
Every MCP tutorial you read demonstrates the pattern with 3 to 5 tools. That is fine for a proof of concept. It is a disaster the moment you try to expose a real business system, because the same architecture that made 5 tools readable makes 300 tools unmaintainable. My baseline before this build was a single index.js containing 159 handlers. Adding a new tool meant scrolling. Debugging meant grep. Verifying a rename meant restarting the server and hoping.
Enterprise MCP has three characteristics that break tutorial patterns: (1) tool count grows into the hundreds, (2) tools cluster by business domain with distinct rules and constraints, and (3) the target system has its own boundaries — permissions, transactional integrity, side effects — that the MCP layer must respect. This post is about the architecture that survives all three.
2. From Monolith to Modular — The Registry Pattern
The registry pattern centralizes tool registration through a single register() function that stores tools in a Map keyed by name. Every tool module becomes a self-contained file that exports one function, registerAll(client), and calls register(name, schema, handler) for each of its tools. The entry file becomes a pure registrar with one import per module.
The single most valuable feature of a registry is the duplicate-name guard. If two modules try to register the same tool name — a copy-paste that survived a refactor, a rename that missed one call site — the guard throws at server startup. You find out in the first second, not the first time a user calls the wrong handler. This one line has caught more mistakes for me than any test suite.
The Phase 0 refactor of my server moved 159 tools out of one file into 10 domain modules under tools/ in a single working session. The rule that made it safe was "move, do not rewrite" — every handler body was copied verbatim, one module per commit, tool count verified after each move. That discipline is the difference between "one afternoon" and "three weeks of subtle bugs".
3. Conditional Module Registration — Tools That Appear on Demand
Every enterprise system has features you have not turned on yet. Odoo Enterprise has 60+ apps, and no real business runs all of them — you have Sales but not Manufacturing, HR but not IoT, Website but not POS. Your MCP server should ship code for the tools regardless, and load only what the target instance actually supports.
The mechanism is one field on each tool module and one check at startup. Modules declare their required Enterprise app; the server queries ir.module.module once at boot, caches the installed list, and skips tool modules whose requirement is missing. The skip is logged, not silent, so the operator always knows what would appear if they activated the missing app.
My server ships 442 tools of code and registers 375 today. The 67 gated tools cover 10 modules — Purchase, Inventory, POS, Rental, Manufacturing, Quality, Maintenance, IoT, Events, and Payroll — that map to Odoo apps not currently installed. The day I install any of them, restart the server, and the tools appear. Zero code changes.
4. The Capability Matrix — Map the Boundary Before You Build
Access-control-list grants do not equal server acceptance on managed SaaS. Odoo.com Cloud restricts writes to framework models even when the ACL says the operation is allowed. The undocumented, empirical boundary can only be discovered by trying — and doing that by hand across dozens of models wastes a week. So I built a developer.js module first and used it to generate a capability matrix that gated every subsequent module.
The core tool is probe_write_capability(model). For any given Odoo model, it runs fields_get, reads the ACL rows for the current user, and — for safe models — attempts a create-then-unlink of a probe record named __MCP_PROBE_<timestamp>__. The result is a per-model capability record: can I read, create, write, unlink, and — if any of them fail — what was the exact error?
The output feeds directly into the next module's spec. My studio.js uses the matrix to enforce a hard rule: every custom model and field name must start with x_ and every server action must use a whitelisted state, never state='code' (server-side Python is disabled on SaaS). Encoding the boundary in code once means I never rediscover it at 2am with a broken deploy.
5. Blast-Radius Confirms — Wiring Agents to Send-Tools Safely
An agent that can call your send_mailing tool can also call it wrong. If a language model, or a prompt injection, decides to broadcast an unfinished newsletter to your full subscriber list, the damage is done before you see the error. The mechanism that fixes this without removing the tool is what I call the blast-radius confirm.
Every tool that sends, publishes, or charges requires an explicit confirm: true parameter. Missing it does not silently succeed and does not silently fail — the tool returns an error object that echoes back the consequences of the call it just refused: recipient count, list names, subject line, estimated cost. The calling agent sees the blast radius, has a real answer to "should I do this?", and can decide whether to make the second call.
The same pattern gates create_sign_request, dunning_pipeline, confirm_payslip, and every other tool where the wrong second call has real cost. It preserves agent capability while making mistakes visible before they happen.
6. The AI Layer — Composition, Not Magic
An AI Layer is a composition module that orchestrates existing tools into higher-order workflows. The AI Layer in my server contains no LLM calls, no embeddings, no vector stores — the calling agent (Claude) is the intelligence. The layer's job is to formalize the multi-step sequences you would otherwise drive by hand, and expose each one as a single tool call with real safety guarantees.
My publish_blog_post_pipeline is a good example. Publishing a post is a six-step process (fetch blog id → create post → inject schema → run AI-readiness audit → refresh SEO if score below threshold → schedule or publish). Doing that as six manual tool calls is tedious and fragile. The pipeline runs the six steps as one call, and does two things that make it safe:
- Dry-run: passing
dry_run: truereturns the exact ordered manifest of what would happen with zero actual writes. - Resumable manifests: every real run persists its manifest in
ir.config_parameter. Bridge timeout at step 4? Resume with the manifest, skip steps 1–3.
The AI Layer holds 10 pipelines — weekly_business_snapshot, lead_intake_pipeline, dunning_pipeline, pre_change_snapshot, and others. None of them contain business logic that could not be assembled from the 340+ tools underneath. But each one closes a real time sink: my Monday review used to be a 20-minute merge of pipeline forecast, MRR, churn, aged receivables, and calendar; weekly_business_snapshot returns the same data as one structured JSON in about 4 seconds.
7. What Broke Along the Way (And What Held)
Every load-bearing rule in this architecture was written after something broke. That is the whole point of writing this — you can adopt the rules without paying to discover them. Here is the ledger:
Quick-win checklist — take these to your next MCP build
- ✅ Start with a registry and a duplicate-name guard — one afternoon, saves months
- ✅ One tool file per business domain; entry file becomes a pure registrar
- ✅ Declare requiredModule on every gated module; skip at boot with a logged reason
- ✅ Before writing custom-model tools, build a capability probe and generate a matrix
- ✅ Every send/publish/charge tool gets a confirm gate that echoes back the blast radius
- ✅ Uploads = one file per call, file_size returned for verification
- ✅ Every money aggregate groups by currency; never sum across currencies
- ✅ Wrap multi-step workflows in an AI Layer with dry-run and resumable manifests
Test what you learned
Final Thoughts
The interesting question is not "should I build an MCP server for my business system?" — you should, and the earlier the better. The interesting question is how you build it so it grows past 50 tools without collapsing. Every architectural decision in this post exists because I hit the wall on a decision I made earlier without thinking. You do not have to.
The three highest-leverage rules, if you take only three: registry pattern with a duplicate-name guard on day one, conditional module registration before you have anything to gate, and blast-radius confirms on every send tool before an agent ever touches production. Those three will save you more time than any other architectural decision.
The full 34-module, 375-tool platform now runs everything at Purple Crib Studios — from blog publishing pipelines to the weekly business snapshot to the WhatsApp Money subscription backend under construction. The system that took 17 focused sessions to build now saves that much time back every week. That is the deal enterprise MCP offers when you build it correctly, and the reason I do not think there is any part of my business I would still run manually a year from now.
Frequently Asked Questions
What is an MCP server?
An MCP (Model Context Protocol) server exposes tools, resources, and prompts to AI assistants like Claude over a standardized JSON-based protocol. It lets a language model perform real actions in external systems — read a database, send an email, update a record — through a defined tool contract.
Why build a modular MCP server instead of one big file?
A monolithic MCP server becomes unmaintainable past roughly 50 tools. A modular architecture splits tools by domain (CRM, accounting, project) into separate files that register through a central registry, so debugging, testing, and scaling stay tractable as tool count grows.
How many tools can an MCP server have?
There is no hard protocol limit. The Odoo Enterprise MCP server described in this post registers 375 tools at runtime with a 442-tool ceiling, organized across 34 domain modules — each tool a discrete, testable unit.
What is the registry pattern in an MCP server?
The registry pattern centralizes tool registration through a single register() function that stores tools in a Map keyed by name. It enables a duplicate-name guard that throws at server startup instead of runtime, catching copy-paste errors before they reach production.
How does conditional module registration work?
Each tool module optionally declares a requiredModule field. At server startup, one query to ir.module.module checks which Enterprise apps are installed, and modules whose requirement is not met skip registration with a logged SKIP line. Activating the app plus a server restart makes the tools appear automatically.
Can you use MCP with Odoo SaaS (odoo.com cloud)?
Yes, but the SaaS write boundary must be mapped empirically because ACL grants do not equal server acceptance. Odoo.com restricts framework-model writes and disables server actions with state='code'. A capability audit module produces the boundary map before Studio-style customization tools are built against it.
What is an AI Layer in an MCP server?
An AI Layer is a composition module that orchestrates existing tools into higher-order workflows — publishing a blog post, running a weekly business snapshot, or executing a dunning cycle — as single tool calls with dry-run previews and resumable manifests.
How long did it take to build the Odoo MCP server?
The full architecture was built across 17 sessions using Claude as the specification engine and Claude Code as the implementation agent. Each module followed a spec → execute → verify rhythm, with the platform reaching 375 registered tools at completion.
Sources & Further Reading
- Model Context Protocol — Official Specification (Anthropic)
- Odoo 19 ORM Reference — for the JSON-2 API details behind the client layer
- Introducing the Model Context Protocol — Anthropic's launch post
- Official MCP Server Examples on GitHub
- Odoo Deployment Documentation — for the SaaS vs self-hosted distinction
Founder of Purple Crib Studios, a Lagos-based Mediatech agency specializing in AI SEO, Google Business Profile optimization, WebMCP, and Odoo ERP integrations for clients in Nigeria, the UK, USA, UAE, and Canada. Currently building the future where every business runs on agent-native infrastructure.