ADR-0015 — Constrained elicitation schema builder; no escape hatch¶
- Status: Accepted
- Date: 2026-04-13
Context¶
MCP elicitation lets a server pause an in-flight tool call to ask the client for additional input. The protocol constrains the elicitation schema severely: it must be a flat object whose properties are primitives (string, integer, number, boolean) or enum-typed select / multi-select. No nested objects. No arbitrary JSON Schema. The constraint is in the spec for a reason — clients render elicitation as forms, and forms can't usefully render arbitrary nesting.
A naive Java API for elicitation would accept "any JSON Schema" — a
JsonNode or a Jackson tree — and let the user assemble whatever they
want. That always produces the same bug: a tool author writes a nested
schema, the client rejects it (or worse, the spec-conformant client
silently flattens or errors), and the failure surfaces deep into
testing.
A second tempting shape is bean-mode elicitation — the user passes a
Java type (MyForm.class or a TypeReference<MyForm>), the framework
generates a schema from the bean, sends elicitation, and deserializes
the response back into the bean. That's convenient until you remember:
the spec restricts the schema. Bean-mode generation has to either
faithfully reproduce that restriction (in which case the bean has to be
flat, with carefully chosen field types — at which point a builder is
clearer) or generate over-rich schemas the protocol forbids.
Sampling — the dual of elicitation, where the server asks the client to generate text — wants typed responses too, and the same constrained schema shape covers it.
Decision¶
Mocapi ships a single elicitation entry point: a constrained, type-safe builder that mirrors MCP's flat-object schema restriction exactly. There is no escape hatch.
The entry point on McpStreamContext:
ElicitationResult elicit(String message, Consumer<ElicitationSchemaBuilder> schema);
ElicitationSchemaBuilder exposes only methods that produce
spec-compliant schemas:
- Primitives:
string,integer,number,bool(each with and without a default value). - Single-select enums:
choose(name, Class<? extends Enum<?>> enumType[, defaultValue]). The enum constants generate the wire-levelenumarray;Enum.values()ordinal order;toString()for display names. IftoString()differs fromname(), generates a titled enum schema viaoneOfwithconst/title. - Multi-select enums:
chooseMany(...)— the same enum-to-schema logic wrapped in"type": "array", "items": {...}. - Required:
required(String... names).
The builder produces the appropriate sealed-variant schema record from
mocapi-model (ADR-0014) — there
is no untyped JsonNode path through the builder.
Bean-mode elicitation is removed. The two earlier overloads
(elicitForm(String, Class<T>) and elicitForm(String, TypeReference<T>))
are deleted, along with the bean-result type and the schema generation
that backed them. The constrained builder covers every elicitation use
case the spec supports without dragging schema generation into the
elicitation path.
Sampling reuses the same builder for typed sampling responses. One schema-shape vocabulary spans both server-to-client request kinds.
Consequences¶
What this buys us. A tool author cannot accidentally produce a
non-spec-compliant elicitation schema — the type system rejects it at
compile time. Test surface shrinks: there's exactly one elicitation API
to test, not three. The elicitation path doesn't depend on a JSON
Schema generator, which keeps the dependency graph of mocapi-server
slimmer (ADR-0016 confines
schema generation to the tool path, where the spec is permissive
enough to need it). Sampling and elicitation share one mental model.
Costs. Tool authors with a complex form previously expressible as a nested bean must redesign for the flat-property constraint. That constraint is in the spec — we'd have to enforce it eventually anyway — but it is a real ergonomic step relative to "pass me a class with fields."
Non-goals. No elicit(String, JsonNode customSchema) overload. No
elicit(String, Class<?>) bean overload. No "advanced builder" with
arbitrary-JSON-Schema methods. If a future MCP spec revision relaxes
the elicitation schema constraints, the builder grows to match — at
that point, and not before.
Code anchors: mocapi-server/.../elicitation/RequestedSchemaBuilder.java. Landed in commit 842be533 (2026-04-13).