ADR-0014 — mocapi-model translated 1:1 from MCP schema.ts¶
- Status: Accepted
- Date: 2025-07-09
Context¶
The Model Context Protocol publishes its wire format as a TypeScript file
(schema/2025-11-25/schema.ts) that defines every JSON-RPC method's
request and response shape, every resource/tool/prompt descriptor, every
notification, every union type. That file is the spec; everything else
(server SDKs, client SDKs, conformance suites) is downstream.
A Java MCP framework needs a model module — a set of types covering every shape on the wire. The choice is whether that model is:
- A 1:1 translation of
schema.ts, with the same names, the same union shapes, the same nullability, and the same JSON keys; or - An idiomatic Java rewrite that reorganizes for "Java taste" — collapsing unions, renaming fields, swapping naming conventions.
Option 2 looks attractive at first. Option 2 also drifts. Each MCP spec
revision becomes a translation exercise; each translation introduces
an opportunity to misread a ? (optional) as required, to flatten a
sealed hierarchy that mattered, to rename a field that conformance tools
expect verbatim. Six months in, the model is its own thing, and bringing
it back in line costs more than just translating in the first place.
The model module also sits at the bottom of the dependency graph: every other mocapi module depends on it. Pulling Spring, Jackson Databind modules, validation libraries, or anything else heavy into model would force every consumer to inherit those dependencies.
Decision¶
mocapi-model is a 1:1 translation of MCP schema.ts. Same type names,
same field names, same nullability, same union structure. Jackson
annotations are the only annotations on the types; Jackson Databind is
the only non-JDK dependency.
Rules:
- Every
schema.tsinterfacebecomes a Javarecord(immutable, value semantics, equals/hashCode for free). Field names match the TypeScript names with no Java-style renaming. - Every
schema.tsdiscriminated union becomes a Javasealed interfacewith onerecordpermitted variant per union arm. The sealed hierarchy mirrorsschema.tsexactly, including intermediate unions. For example, the spec defines:andtype PrimitiveSchemaDefinition = StringSchema | NumberSchema | BooleanSchema | EnumSchema; type EnumSchema = SingleSelectEnumSchema | MultiSelectEnumSchema | LegacyTitledEnumSchema;mocapi-modelcarries the same two-level sealed hierarchy. Flattening the union on the Java side is forbidden — exhaustiveswitchexpressions onPrimitiveSchemaDefinitionand onEnumSchemaboth need to compile. - Enum constants are uppercase Java idiom
(
Role.USER,Role.ASSISTANT); JSON serialization uses the spec's lowercase form via@JsonValueon a per-enumvalue()accessor. The spec dictates the wire form; Java idiom dictates the source form. - Optional fields use Java's
Optional<T>for primitives where the spec marks them optional; nullable JSON fields tolerate missing input via Jackson's standard handling. - Backward-compatibility variants the spec keeps (notably
LegacyTitledEnumSchema) are kept here too, marked@Deprecated. Code that instantiates them or tests that exercise them carries a narrow@SuppressWarnings("deprecation")with a comment naming the spec section that requires the deprecated form.
Dependencies: mocapi-model depends only on jackson-databind
(and the JDK). No Spring, no validation, no schema generators, no other
mocapi modules.
Consequences¶
What this buys us. Every MCP spec revision is a mechanical
translation — open schema.ts, open mocapi-model, diff. Conformance
suites that send canonical request payloads round-trip through the model
without name mapping. The sealed hierarchies give exhaustive switch
in user code (e.g., handling each PrimitiveSchemaDefinition variant)
with compile-time completeness checking. Consumers — mocapi-server,
mocapi-streamable-http-transport, conformance tooling, third-party
clients — depend on a small Jackson-only jar.
Costs. The model is not "Java-idiomatic" by some definitions — field names follow JSON conventions, sealed hierarchies have more levels than a Java-first design would. We accept the small ergonomic cost for the spec-fidelity payoff. Spec revisions that change a field name break every consumer; that is also the point — the framework fails loudly when the wire format changes, which is what we want.
Non-goals. mocapi-model does not validate, generate schemas,
serialize over the wire, or know about transports. It is the type
layer. Anything richer (constrained builders for elicitation schemas,
tool input-schema generation) lives in higher modules — see
ADR-0015 and
ADR-0016.
Code anchor: mocapi-model/ (every record type maps 1:1 to a schema.ts shape). LegacyTitledEnumSchema carries (since = "0.0.1", forRemoval = false).