Writing OpenAPI Specs
LockMCP converts each OpenAPI operation into an MCP tool. The spec is the single source of truth — operation summaries become the tool descriptions the AI reads when deciding what to call, parameter schemas become the tool's typed input fields, and x-mcp-inject markers enable per-user scope injection. A well-written spec directly improves the quality of AI tool calls.
LockMCP accepts OpenAPI 3.0.x YAML or JSON. OpenAPI 2.0 (Swagger) is not supported. Each operationId in your spec becomes one MCP tool — operations without an operationId are skipped during parsing.
What LockMCP reads from your spec
| Spec field | How LockMCP uses it |
|---|---|
operationId | Becomes the MCP tool name. Optional — one is generated from the method and path when absent, but naming it yourself keeps tool names stable across re-uploads. |
summary | The tool's one-line description shown to the AI client. Make it specific. |
description (operation level) | Extended context appended to the tool description. Use it for caveats, units, and date formats. |
parameters[].schema | Becomes the tool's JSON Schema input field. |
parameters[].description | Shown to the AI alongside the field name. |
requestBody.content[type].schema | Merged into the tool's input schema as body fields. application/json, vendor +json types and application/x-www-form-urlencoded are supported; allOf, oneOf and anyOf are flattened: for oneOf/anyOf, a field several alternatives declare offers every alternative's values, a field all of them require is required, and a field only some require says so in its description (e.g. "Required when status is APPROVED"). Operations whose only body is multipart/form-data are skipped with a warning. |
responses (the first 2xx with a JSON body: 200, then 201, then other 2xx) | Becomes the tool's output schema, listed to AI clients so they know what a call returns instead of guessing. example/examples are dropped. A schema over 8 KB is left out with an upload warning — the tool still works. Tools imported earlier get it on their next Re-sync (it shows as "response schema changed"), then publish. |
servers[0].url | The base URL LockMCP proxies calls to. Use your production URL — not localhost. |
security + securitySchemes | Tells LockMCP how to forward credentials. Use bearerAuth for JWT. |
x-mcp-inject on a param schema | Triggers role-scoped enum injection — replaced with a per-user enum at call time. |
Minimal working spec
The smallest spec that LockMCP can parse has an info block, one server, and at least one operation with an operationId:
openapi: 3.0.3
info:
title: My Service API # shown in the hub dashboard
version: 1.0.0
servers:
- url: https://api.myservice.com # base URL for all proxied calls
security:
- bearerAuth: [] # forward the user's JWT on every request
paths:
/items:
get:
operationId: listItems # becomes the MCP tool name
summary: List all items # what the AI sees in the tool list
responses:
'200':
description: OK
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWTUpload this and LockMCP creates one tool: listItems.
Full annotated example
The sections below walk through a production-quality spec. All examples are drawn from the JHipster sample app spec.
info — spec metadata
info:
title: JHipster Sample App API
description: >
REST API for the JHipster 9 Spring Boot monolith. Covers self-service
account management, admin user management, and BankAccount / Label /
Operation CRUD. All endpoints require a JWT Bearer token obtained from
the application's login endpoint.
version: 1.0.0The title and description are displayed in the hub dashboard. The version field is for your own records and does not affect tool naming.
servers — base URL
servers:
- url: http://localhost:8080
description: Local developmentLockMCP proxies every tool call to <servers[0].url> + <path>. Use your public HTTPS URL in production:
servers:
- url: https://api.myservice.com
description: Production
- url: http://localhost:8080
description: Local developmentOnly the first server entry is used for proxying. Additional entries are ignored.
security and securitySchemes
security:
- bearerAuth: [] # applies to all operations by default
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWTWhen bearerAuth is declared, LockMCP forwards the authenticated user's JWT as Authorization: Bearer <token> on every proxied call. Your backend validates it exactly as it would any direct client request.
tags — grouping operations
tags:
- name: bankAccounts
description: Bank account CRUD (ROLE_USER)
- name: admin
description: Admin user management (ROLE_ADMIN)Tags group tools in the LockMCP dashboard. Tag each operation with the relevant group:
paths:
/api/bank-accounts:
get:
tags: [bankAccounts]
operationId: getAllBankAccounts
summary: List all bank accountspaths — one operation = one tool
Each operationId under paths becomes one MCP tool. Write it once per operation:
paths:
/api/bank-accounts:
get:
tags: [bankAccounts]
operationId: getAllBankAccounts # tool name
summary: List all bank accounts # AI description
parameters:
- name: eagerload
in: query
description: Eagerly load associated operations
schema:
type: boolean
default: true
responses:
'200':
description: List of bank accounts
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/BankAccount'
post:
tags: [bankAccounts]
operationId: createBankAccount
summary: Create a bank account
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BankAccount'
responses:
'201':
description: Bank account createdcomponents/parameters — reusable parameter refs
Define shared parameters once and $ref them across operations to avoid duplication:
components:
parameters:
Page:
name: page
in: query
description: Zero-based page index
schema:
type: integer
format: int32
default: 0
minimum: 0
Size:
name: size
in: query
description: Number of records per page
schema:
type: integer
format: int32
default: 20
minimum: 1
maximum: 200Reference them in any operation:
paths:
/api/admin/users:
get:
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/Size'components/schemas — shared object types
Define request/response bodies as named schemas and compose them with $ref and allOf:
components:
schemas:
AdminUserDTO:
type: object
properties:
id:
type: integer
format: int64
login:
type: string
minLength: 1
maxLength: 50
email:
type: string
format: email
required:
- login
ManagedUserVM:
description: AdminUserDTO extended with a plaintext password
allOf:
- $ref: '#/components/schemas/AdminUserDTO'
- type: object
properties:
password:
type: string
minLength: 4
maxLength: 100Writing descriptions the AI will use
Claude reads summary and description when deciding which tool to call and how to fill its parameters. Vague text leads to wrong tool selection and malformed calls.
Treat your summary and description fields as prompts. The AI has no other context about what your API does.
summary — one line, action + resource:
| Avoid | Prefer |
|---|---|
Do operation | List all bank accounts for the current user |
Update | Fully update a bank account by ID |
Get data | Get an operation (transaction) by numeric ID |
description (operation level) — when to use it and key constraints:
description: >
Returns a paginated list of all financial operations (transactions) for
the authenticated user. Use the `page` and `size` parameters to paginate.
Dates are ISO-8601 strings (e.g. "2024-03-15T10:00:00Z"). Amounts are
decimal numbers; negative values represent debits.parameters[].description — valid values, formats, required constraints:
- name: sort
in: query
description: >
Sort criteria in the format `property,(asc|desc)`.
Multiple values allowed. Example: `id,asc` or `date,desc`.
schema:
type: string
example: id,ascRole-scoped enum injection
For parameters that should be constrained to the specific resource IDs a user can access, add the x-mcp-inject extension to the parameter's schema:
components:
parameters:
BankAccountId:
name: id
in: path
required: true
description: >-
Bank account identifier. LockMCP replaces this with an enum of the
bank-account IDs the current user owns.
schema:
type: integer
format: int64
x-mcp-inject: "bankAccountIds" # key name from your bootstrap responseThe value of x-mcp-inject is the key the hub looks up in your bootstrap payload. At call time, LockMCP replaces the schema with enum: [<ids the user owns>] and strips the marker — Claude never sees x-mcp-inject.
Common key name patterns:
| Domain | Parameter | x-mcp-inject value |
|---|---|---|
| Finance | bank_account_id | bankAccountIds |
| Property management | property_id | propertyIds |
| Project tools | project_id | projectIds |
| Healthcare | organisation_id | organisationIds |
| E-commerce | store_id | storeIds |
For the full setup — returning the bootstrap payload from your validate response, setting inject keys in the dashboard, and understanding multi-role union behaviour — see Enum Injection.
Uploading to LockMCP
- Go to Dashboard → API Specs and click Upload Spec
- Paste your YAML or JSON, or drag-and-drop the file
- LockMCP parses the spec and lists the operations it found — review the extracted tool names and descriptions
- Click Save to store the spec in draft state
- Go to Dashboard → Tools, find the tools from your spec, and click Publish to make them available to MCP clients
Re-uploading a spec does not change what your MCP clients receive until you publish.
A re-upload re-parses the spec and records a new draft version for each tool that changed. Your published tools keep serving the version they were already serving — LockMCP never pushes an unreviewed schema change live.
Tools in this state show as Update pending in Dashboard → Tools. Filter by that status, select them, and use Publish to promote them all in one go. (Or promote one at a time in Tool Studio → Versions → Make active.)
Common mistakes
Missing operationId — LockMCP skips operations without an operationId. Every operation that should become a tool must have one. Make them globally unique within the spec (e.g. getAllBankAccounts, not getAll).
servers.url pointing to localhost — Proxied calls will fail in production. Set servers[0].url to your public HTTPS URL before uploading.
Vague summary — "Get data" or "Do thing" causes the AI to pick the wrong tool or ask the user for clarification. Use the pattern: <verb> <resource> [qualifier].
Omitting required: true on path parameters — LockMCP infers path parameters are required, but being explicit prevents ambiguity. Always set required: true on in: path parameters.
requestBody without content — A requestBody with no content key is invalid and will cause a parse error. Always include at least one media type (typically application/json).