Schema Design: API-first, OpenAPI, and MCP
Schema design is the foundation of any API-first system. In HAPI, your OpenAPI schema defines not just endpoints, but also the structure, validation, and semantics of every request and response.
Principles of Good Schema Design
- Explicitness: Define all fields, types, and constraints clearly.
- Validation: Use OpenAPI's built-in validation keywords (
type,format,enum,pattern, etc.). - Reusability: Use
$refandcomponents/schemasto avoid duplication. - Extensibility: Leverage OpenAPI extensions (e.g.,
x-hapi) for MCP-specific needs.
Design your schemas with future agents and tools in mind. Well-structured schemas make it easier for MCP-powered agents to reason about and use your API.
Example: User Schema
components:
schemas:
User:
type: object
required:
- id
- name
properties:
id:
type: string
name:
type: string
email:
type: string
format: email
roles:
type: array
items:
type: string
Using Enums and Constraints
components:
schemas:
Status:
type: string
enum: [active, inactive, pending]
Avoid using overly generic types (like object). This can make validation and documentation less effective.
Request bodies and MCP tool arguments
In OpenAPI 3, an operation payload is described by requestBody. Unlike a
parameter, an inline requestBody does not have its own name. HAPI must give
that payload a name when it creates an MCP tool input schema, so that body
fields never collide with path, query, or header parameters.
This naming applies to the MCP tool's arguments object only. The request sent
to your API still uses the raw JSON payload defined by the OpenAPI document.
Inline request bodies
For an inline body, HAPI uses <operationId>Body. Give every payload-bearing
operation a stable, unique operationId so the tool contract is predictable.
paths:
/orders:
post:
operationId: placeOrder
summary: Place an order
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [customer, tacos]
properties:
customer:
type: object
tacos:
type: array
items:
type: object
responses:
'201':
description: Order created
HAPI exposes this as the placeOrder MCP tool. Call it with the body nested
under placeOrderBody:
{
"name": "placeOrder",
"arguments": {
"placeOrderBody": {
"customer": { "name": "Ada" },
"tacos": [{ "id": "taco-1", "quantity": 2 }]
}
}
}
If an operation has no operationId, HAPI generates one from its method and
path. Its generated ...Body property is visible in tools/list, but defining
an operationId yourself is recommended because it makes the tool and body
names stable as the API evolves.
Reusable request bodies
When several operations use the same payload, define it once in
components.requestBodies and reference it. In this case the component key is
the MCP argument name. This provides an explicit, reusable name instead of an
operation-specific one.
components:
requestBodies:
PlaceOrderBody:
description: Customer and taco selections for a new order
required: true
content:
application/json:
schema:
type: object
required: [customer, tacos]
properties:
customer:
type: object
tacos:
type: array
items:
type: object
paths:
/orders:
post:
operationId: placeOrder
requestBody:
$ref: '#/components/requestBodies/PlaceOrderBody'
responses:
'201':
description: Order created
The resulting MCP call uses PlaceOrderBody:
{
"name": "placeOrder",
"arguments": {
"PlaceOrderBody": {
"customer": { "name": "Ada" },
"tacos": [{ "id": "taco-1", "quantity": 2 }]
}
}
}
Use a clear, unique component key such as CreateInvoiceBody or
PlaceOrderBody. It becomes part of the MCP tool contract when the reusable
body is referenced. Use an operationId-based inline body when the payload is
specific to one operation.
OpenAPI uses requestBody.content to map media types to their schemas, and
supports reusable bodies through components.requestBodies and $ref. See
Swagger's request-body guide and the
OpenAPI 3.0 Request Body Object
for the standard syntax.
oneOf, What is it? When and How to use it?
JSON Schema is a powerful tool for validating complex data structures. The oneOf keyword allows you to specify that a value must match exactly one of the given schemas, valid against exactly one (XOR) of the subschemas.
components:
schemas:
User:
type: object
properties:
id:
type: string
type:
oneOf:
- const: "admin"
- const: "user"
When you design your schemas, consider how they will be used by both humans and machines. Clear, concise schemas improve developer experience and reduce errors. For instance, if your API defines a path parameter that can accept multiple types, using oneOf can make this explicit. Unfortunately, in the MCP context, the use of oneOf can be limited by the need for more specific type information, agents (human-like) need to understand the context better.
Approaches for Handling oneOf Schemas in MCP Input Generation
When creating JSON schemas for tools in the Model Context Protocol (MCP), using the oneOf keyword can be challenging. If your API needs to accept different object types in the requestBody, oneOf helps you list all allowed types. However, you must define every possible type in advance, both on the client and server sides. At runtime, choosing the correct type can be difficult.
In the HAPI Server, we solved this by combining client-side type hints with server-side validation. Here are the options we considered:
- Use a Discriminator Property
- Generate Separate Tool Definitions
- Merge All Properties into One Schema
- Resolve Schemas Dynamically
We chose option 2: Generate Separate Tool Definitions because:
- It does not change the original schema.
- API consumers do not need to make any changes.
- It keeps each type exclusive, so invalid combinations are avoided.
- It allows tools to be created as needed, making the system flexible.
This approach makes it easier for both humans and machines to work with the API, and helps prevent errors.
Tool Definition example
# ... remove other methods for simplicity
post:
# ... removed content for simplicity
requestBody:
required: true
content:
application/json:
schema:
oneOf: # <-- This indicates that the request body must match exactly one of the specified schemas
- title: 'Person'
properties:
name:
type: string
age:
type: integer
- title: 'Company'
properties:
name:
type: string
registrationNumber:
type: string
format: uuid
# ... removed content for simplicity
In this example, the oneOf construct is used to define two possible schemas for the request body: one for a Person and another for a Company. This creates separate tool definitions for each schema variant:
Tool 1: createPerson
Tool 2: createCompany
Each with its own schema definition, instead of trying to represent a union type.
Best Practices
- Use
$refto share common models across endpoints. - Document all fields, even optional ones.
- Use OpenAPI's
descriptionfields for clarity. - Validate with tools like Swagger Editor or openapi-generator.