Decisions specific to this repository. Each carries its rejected alternatives, because the rejected option is where the reasoning lives — and because an unmarked loser read as authoritative is how two decisions end up disagreeing in a corpus for months.
Cross-project engineering decisions (DEC-NNN) live in CINO-Platform-Engineering and are referenced here by number, never restated.
Date: 2026-10-03 · Status: proposed, pending review
Supersedes: surfaces/mcp/ROADMAP.md, section "Why the foundation moved to last"
(2026-09-25).
The roadmap set an explicit order: five servers to a usable internal standard first, the shared foundation last, built from what those five actually needed. Its reasoning was good and should be read before this is accepted. Foundation-first means extracting from three servers, two of which were the only ones with a complete surface, and guessing at the needs of the two unbuilt ones — mail and the tenant-audit server — which is precisely what CSA's core principle forbids. It even named the cost it was accepting: "some duplication will happen, and we are choosing it."
Since then, four servers are built and published rather than three, and the duplication the roadmap
predicted can be measured instead of forecast. See EVIDENCE.md.
Extraction begins now, driven by measured duplication, with a hard eligibility bar: a module
enters csa-mcp only when it is already duplicated at ≥95%, or when it has no implementation in any
server at all.
That admits exactly four modules today, about 300 lines. Everything at 1–60% similarity stays where it is.
The roadmap's principle is not being set aside; it is being satisfied by different means. It deferred extraction because the evidence did not exist yet. The evidence now exists for a narrow slice, and only that slice moves.
Three specific supports:
- The ≥95% tier is the copy the roadmap authorised. It required a deliberate copy to carry a
comment naming its source and to be tracked for retirement.
_markdown.pydoes exactly that — "Ported from csa-zendesk's_markdown.py(same problem, same fix)" — and diffing the two copies shows 19 changed lines, all in the docstring, with byte-identical transform code. This is a tracked copy reaching its scheduled retirement, not a shortcut. - The two new modules have no incumbent.
server/discoverand a shared status vocabulary cannot be extracted from implementations that do not exist. Nothing has to be reconciled to adopt them, which is the test. - The measurement constrains the library more tightly than the deferral did. "Extract later from five" has no size limit. "≥95% or no incumbent" admits 300 lines and makes every future addition argue against a number.
- Keep the roadmap unchanged and extract nothing until all five servers are built. Not wrong,
and it was right when written — but it now defers a 96%-identical module whose own comment says it
is waiting to be retired, and it leaves
server/discoverto be written four times because the rule that forbids designing also forbids the thing with no incumbent. Rejected as unwarranted here, not as mistaken. - Treat this as mere execution of the existing roadmap, with no decision recorded. Defensible — retiring an authorised, tracked copy is arguably what the roadmap planned for. Rejected because the roadmap's text says "foundation last" and this starts it early; a reader comparing the two would find a contradiction with nothing marking which won. That failure has already cost this organisation five months once.
- Extract the OAuth trio too (58–60%). Rejected: that range is the most dangerous, not the most promising. Adopting either copy silently discards the other's behaviour. Reconcile as a decision first, extract second.
- Build a server base class covering
server.py/cli.py/_tools/_base.py. Rejected on the measurement — 10–15% similarity is a structural resemblance, not shared behaviour. See ADR-004.
surfaces/mcp/ROADMAP.md gains a pointer to this ADR. csa-mcp can be created. The servers
convert one at a time.
Date: 2026-10-03 · Status: proposed, pending review
Four published servers in four repositories, built sequentially, each learning from the last. They diverged in ways nobody chose: linting floors split two-and-two, a major-version gap on a library mandated fleet-wide, and a user-facing extra named differently in one of four.
A single repository, csa-mcp-servers-python, as a uv
workspace with one uv.lock. Published
package names and versions stay independent and unchanged.
One lockfile makes the divergent state unrepresentable rather than merely discouraged. The problem was never that four repositories could drift; it is that drift was invisible, because comparing them meant cloning them.
Co-location is also what makes extraction honest — four copies side by side turn "what is actually
shared?" into one command, which is how EVIDENCE.md exists at all.
And the migration is cheaper than feared, measured rather than assumed: nothing installs these from Git. All 44 references in the installer repositories are PyPI names, so a repository move is invisible to anyone installing a server.
- Separate repositories plus a shared library on PyPI. The conventional answer, and it fails the
specific problem: it fixes code duplication while leaving configuration drift untouched, which
is where the measured divergence actually is. A shared library does not unify
rufffloors. - Monorepo for the two Google servers only. They share the most (the entire 58–60% OAuth tier is theirs). Rejected because the drift being fixed is fleet-wide, and a half-migration means maintaining two CI conventions and two release conventions indefinitely.
- Start with only the unbuilt audit server, migrate the four later once proven. Genuinely
attractive — fully reversible, no disruption to four working servers. Rejected deliberately: it
defers every hygiene fix behind building a whole new server, and the measured drift is live now.
The risk is accepted because steps 1–6 of
MIGRATION.mdare all reversible; only issue transfer is not.
Everything. See MIGRATION.md.
The survey in PRIOR-ART.md §0 confirms the monorepo and undercuts the mechanism
this ADR argued for.
Four of four organisations running multiple MCP servers use one repository, so that part stands
without qualification. But AWS runs 62 Python MCP servers with no workspace and a uv.lock per
server. There is no root pyproject.toml. Dependency resolution is deliberately not shared, and
at that scale the reason is clear: one lockfile means one server's unsatisfiable dependency blocks
all 62.
What AWS shares at the root is tooling and writing — a single .ruff.toml, .python-version,
.pre-commit-config.yaml, .gitleaks.toml, trivy.yaml, plus DESIGN_GUIDELINES.md and
DEVELOPER_GUIDE.md.
And that is the direct fix for the drift this ADR cited as its evidence. ruff>=0.6 against
>=0.16 and mypy>=1.11 against >=2.0 are tool configuration divergence. A root .ruff.toml
addresses them head-on; a shared lockfile addresses them as a side effect of coupling everything
else too. The right problem, a heavier instrument than needed.
The markdownify>=1.2 against >=0.13 gap is the one genuinely about runtime dependency floors,
and it is one library mandated by one decision — a dependency policy question rather than an
argument for shared resolution.
What stands: the monorepo, and the claim that invisible drift was the problem.
What is now open: one uv.lock or five. Five servers make the coupling tractable where 62 do
not, so this is not settled by AWS's choice — but it can no longer be presented as the obvious one.
Listed in REVIEW-BRIEF.md and superseded in part by ADR-006.
Date: 2026-10-04 · Status: proposed, pending review
The measured divergence that motivated this repository is mostly tool configuration: two
different ruff floors, two different mypy floors, and a user-facing extra named differently in
one of four servers. ADR-002 proposed to fix this with a single workspace lockfile. The survey shows
the largest Python MCP fleet fixes it with root configuration files instead, and keeps resolution
per-server.
Adopt root tooling configuration regardless of how the lockfile question resolves:
.ruff.toml one lint configuration for every package
.python-version 3.14, which all four already use
.pre-commit-config.yaml one hook set
.gitleaks.toml secret scanning, matching the public-repo standard
docs/CONVENTIONS.md the tier-3 convention document
Per-package pyproject.toml keeps only what is genuinely per-package: the dependency set, the
coverage gate, the entry points.
It is the cheaper instrument for the measured problem, it is independent of the lockfile decision
so it cannot be blocked by it, and it is what AWS does at twelve times our server count. It also
gives the convention tier (ADR-004) a concrete home rather than leaving it an argument — Cloudflare
has implementation-guides/, AWS has DESIGN_GUIDELINES.md and DEVELOPER_GUIDE.md, Microsoft has
core/ plus eng/.
- Rely on the shared lockfile to constrain tool versions. That is ADR-002's original position. Rejected as indirect: a lockfile pins what is installed, while the divergence is in declared floors, and it couples five packages' resolution to fix a lint-config problem.
- Leave tool config per-package and review it periodically. Rejected — that is exactly the state that drifted, and the drift was invisible rather than tolerated.
Date: 2026-10-03 · Status: proposed, pending review
csa-mcp lives in the workspace as a path dependency. No PyPI project, no version number offered to
anyone, no API stability claim. It gets published when a server outside this repository needs
it.
It is being extracted, which means its shape is still being discovered. Publishing converts every early guess into a public contract: a stability statement, a tombstone obligation on every major, a version number people pin.
CSA's own standard for publishing a library is demanding on purpose — "the same questions, pointed at us". Meeting it for a 300-line library with four in-repo consumers buys nothing and costs the freedom to change it.
- Publish from day one. Would let the servers install independently of the monorepo and force API discipline early. Rejected as premature: there is no consumer that needs it, and the discipline it forces is discipline about decisions not yet made.
- Vendor it into each server instead. That is the status quo — it is what the 96% copy is. Rejected; the point is to stop.
Servers depend on csa-mcp by workspace path. Their published wheels must not reference it until it
is published — so extraction has to keep csa-mcp code inside each published distribution, or the
publish step has to wait. This is an open question flagged in
REVIEW-BRIEF.md.
Date: 2026-10-03 · Status: proposed, pending review
server.py, cli.py and _tools/_base.py appear in three servers each, do corresponding jobs, and
measure 10–15% similar.
A documented layout plus a scaffold/ template a new server is copied from. No base class, no
framework entry point, no importable abstraction over this tier.
10–15% similarity means the resemblance is structural, not behavioural. An abstraction over it abstracts the shape rather than any shared logic, and servers then write code to escape it — which is how a small library becomes a framework its own authors fight.
A convention that is copied and then diverges is working correctly: the divergence is the server expressing something true about its vendor. A base class that is inherited and then worked around is not.
- An abstract
CSAServerbase class. The instinctive design. Rejected on the measurement. - Nothing at all — let each server do as it likes. Rejected: the shapes are similar, and a new server (the audit server, next) benefits from being told the layout. Documentation captures that without forcing it.
Date: 2026-10-04 · Status: proposed, pending review
Partially supersedes: ADR-004's reach, and a claim in SECURITY-RESOURCES.md.
ADR-004 reads low similarity as evidence against extraction: server.py at 10% is a structural
resemblance, so sharing it would abstract the resemblance rather than any behaviour. That is right
for server.py.
Applying the same rule to _untrusted.py gave the wrong answer. Measured 2026-10-04, the
untrusted-content boundary is 1%, 2% and 11% similar across three servers, with a fourth having
no module at all and one of the three sitting at a different architectural layer. It is the least
consistent control in the fleet — less consistent than auth.py (16%) or policy.py (3%) — and it
is the only one whose failure lets injected content act with the user's credential.
Low similarity is not self-interpreting. The test is: does a single correct behaviour exist?
- No — the variation is legitimate and the code stays put. "What should my CLI do?", "how is this server configured?" Extraction would force unlike things into one mould.
- Yes — low similarity is evidence of a missing shared primitive, and the cost of leaving it is highest exactly where the control is safety-critical. "How do I mark vendor content so a model treats it as data rather than instructions?" has one right answer.
So csa-mcp takes the untrusted-content wrapping primitive. Vendor content shapes — what a
Zendesk comment or a Gmail MIME part looks like — stay with their vendors.
ADR-001's ≥95%-or-no-incumbent bar is a good default and it is a floor on evidence, not a ceiling on judgement. A control implemented four ways where one way is correct is not four pieces of evidence that it should stay split; it is four chances to have got it wrong, and 241 tool registrations' worth of surface behind it.
- Keep the ≥95% bar absolute. Consistent and simple. Rejected: it would leave the fleet's most safety-critical control as four independent implementations, which is the outcome the bar exists to prevent, reached by obeying the bar.
- Extract
policy.pytoo, by the same argument. Rejected — policy at 3% is genuinely vendor-specific: Zendesk allowlists and Skilljar capability gates are not the same control wearing different clothes. The vocabulary for reporting a refusal should converge; the rules should not. - Rewrite all four to match before extracting. Rejected as the expensive order. Extract the primitive, then converge onto it one server at a time, per ADR-001.
csa-mcp gains a fifth seeded module. SECURITY-RESOURCES.md carries the correction.
research/enforcement/ is the evidence.
Date: 2026-10-03 · Status: proposed, pending review
DEC-012 requires one isolated environment per server, after a uv rebuild turned four working
servers into husks in a single run. A shared workspace reads like a direct contradiction.
It is not one, and the distinction is recorded so it is not re-litigated: DEC-012 governs
installation, this governs development. End users run uv tool install csa-zendesk and get four
isolated runtime environments; a bad resolve in one still cannot reach another. The workspace is one
lockfile and one dev environment for people working in this repository.
- Four independent dev environments inside the monorepo. Would satisfy the letter of DEC-012 and discard the entire benefit — the single lockfile is the fix for the measured drift.
- Ask for DEC-012 to be amended. Unnecessary; it already says what it means. Only the apparent conflict needed recording.