An API operation can receive input from several places.
Path parameters identify the record. Query parameters filter or paginate the result. Headers carry metadata or authentication. The request body contains structured data for create and update operations.
An MCP tool should give the AI client one clear input schema.
That is the mapping problem:
HTTP API inputs
path + query + headers + body
become
MCP tool input
one structured schema the AI client can understand
This tutorial walks through that mapping with practical examples. The goal is to make the tool easy for an AI client to call without hiding the real API contract.
Imagine a project-management API with this endpoint:
PATCH /workspaces/{workspace_id}/projects/{project_id}/tasks/{task_id}
It updates one task.
The API accepts:
workspace_id
, project_id
, and task_id
;notify_assignee
;Idempotency-Key
.A shortened OpenAPI-style version might look like this:
paths:
/workspaces/{workspace_id}/projects/{project_id}/tasks/{task_id}:
patch:
operationId: updateTask
summary: Update a task
description: "Update the title, status, assignee, or due date for one task."
parameters:
- name: workspace_id
in: path
required: true
schema:
type: string
- name: project_id
in: path
required: true
schema:
type: string
- name: task_id
in: path
required: true
schema:
type: string
- name: notify_assignee
in: query
required: false
schema:
type: boolean
default: false
- name: Idempotency-Key
in: header
required: false
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
title: ""
type: string
status:
type: string
enum: [todo, in_progress, blocked, done]
assignee_id:
type: string
due_date:
type: string
format: date
minProperties: 1
security:
- bearerAuth: []
The API shape is split across the HTTP request. The MCP tool should present the editable parts in one schema.
The route is useful for the adapter, but it is not a good tool name.
This is weak:
{
"name": "patch_workspaces_projects_tasks"
}
This is clearer:
{
"name": "update_task"
}
If your API has several update operations, use the object and action to remove ambiguity:
update_task
update_task_status
assign_task
reschedule_task
The right name depends on what the endpoint actually does. If the endpoint updates many fields, update_task
may be correct. If the endpoint only changes status, update_task_status
is better.
Path parameters usually identify the exact resource being addressed. In an MCP tool schema, they are usually required fields.
From the route:
/workspaces/{workspace_id}/projects/{project_id}/tasks/{task_id}
The tool needs:
{
"workspace_id": "wrk_123",
"project_id": "prj_456",
"task_id": "tsk_789"
}
In the MCP tool schema:
{
"type": "object",
"properties": {
"workspace_id": {
"type": "string",
"description": "The workspace that contains the project."
},
"project_id": {
"type": "string",
"description": "The project that contains the task."
},
"task_id": {
"type": "string",
"description": "The task to update."
}
},
"required": ["workspace_id", "project_id", "task_id"]
}
Keep the path identifiers explicit. Do not collapse them into one generic id
field if the API needs all three values. The AI client should not guess which ID belongs to which level.
Query parameters often change how the operation behaves:
In the example, notify_assignee
controls whether the API sends a notification after the update.
That should appear as an optional tool input:
{
"notify_assignee": {
"type": "boolean",
"description": "Whether to notify the assigned user after the task is updated.",
"default": false
}
}
For list operations, query parameters may be the main tool inputs:
GET /customers/{customer_id}/tickets?status=open&limit=20&cursor=abc
The MCP schema might expose:
{
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "The customer whose tickets should be listed."
},
"status": {
"type": "string",
"enum": ["open", "pending", "closed"],
"description": "Optional ticket status filter."
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 20,
"description": "Maximum number of tickets to return."
},
"cursor": {
"type": "string",
"description": "Pagination cursor from a previous response."
}
},
"required": ["customer_id"]
}
Good query mapping keeps list tools bounded. If a search endpoint accepts unlimited free-form parameters, the agent may produce slow, broad, or invalid calls.
Headers are tricky because some are normal inputs and some are credentials.
Authentication headers should not become normal tool inputs.
Do not expose this:
{
"authorization": {
"type": "string",
"description": "Bearer token for the API request."
}
}
That would make the credential model-visible.
Instead, the tool input should stay focused on the business operation:
{
"workspace_id": "wrk_123",
"project_id": "prj_456",
"task_id": "tsk_789",
"status": "blocked"
}
The adapter or hosted MCP runtime should receive credentials through the authentication path and forward them to the upstream API:
Authorization: Bearer <runtime credential>
The upstream API still enforces identity, tenant, role, record, and action permissions. The MCP schema should not become a place where the model supplies secrets.
0mcp supports API key, Bearer token, and OAuth pass-through. Credentials are supplied through the MCP client at request time and passed to the original API rather than stored by 0mcp. That keeps the generated tool schema focused on the task inputs.
Some headers are not secrets. They may still matter.
Examples:
Idempotency-Key
;X-Request-Id
;Accept-Language
;If-Match
;X-Client-Version
.Do not automatically expose every header to the AI client. Ask what role the header plays.
Expose a header as a tool input when:
For Idempotency-Key
, you may decide to generate it inside the MCP server instead of asking the AI client to provide it. That reduces friction and avoids duplicate-write bugs.
For Accept-Language
, you might expose language
as a business-friendly field rather than the raw HTTP header:
{
"language": {
"type": "string",
"enum": ["en", "es", "fr"],
"description": "Preferred language for localized response text."
}
}
Then the adapter maps it to:
Accept-Language: en
The schema should describe the product-level input, not force the agent to think in low-level HTTP details when a cleaner field works.
For create and update operations, the request body often becomes the largest part of the tool schema.
From the PATCH /tasks/{task_id}
example, the request body allows:
title
;status
;assignee_id
;due_date
.The combined MCP input schema can include path, query, and body fields together:
{
"name": "update_task",
"description": "Update the title, status, assignee, or due date for one task. Use this only after the user has identified the workspace, project, task, and requested change.",
"inputSchema": {
"type": "object",
"properties": {
"workspace_id": {
"type": "string",
"description": "The workspace that contains the project."
},
"project_id": {
"type": "string",
"description": "The project that contains the task."
},
"task_id": {
"type": "string",
"description": "The task to update."
},
"title": {
"type": "string",
"description": "New task title."
},
"status": {
"type": "string",
"enum": ["todo", "in_progress", "blocked", "done"],
"description": "New task status."
},
"assignee_id": {
"type": "string",
"description": "User ID of the new assignee."
},
"due_date": {
"type": "string",
"format": "date",
"description": "New due date in YYYY-MM-DD format."
},
"notify_assignee": {
"type": "boolean",
"default": false,
"description": "Whether to notify the assignee after the update."
}
},
"required": ["workspace_id", "project_id", "task_id"]
}
}
Notice that the body fields are optional in the schema because this is a patch operation. But the API should still reject a request that updates nothing. You can express that with validation logic if the schema format you use cannot represent it cleanly.
For example:
const updateFields = ["title", "status", "assignee_id", "due_date"];
if (!updateFields.some((field) => input[field] !== undefined)) {
throw new Error("Provide at least one task field to update.");
}
The tool should guide the model toward valid updates without making every field required.
Naming conflicts happen often when you combine path, query, header, and body inputs into one schema.
Example:
PATCH /projects/{id}
Request body:
{
"id": "external-project-id",
"name": "New project name"
}
Now there are two id
values:
id
, which identifies the project being updated;id
, which might represent an external ID or imported ID.Do not expose both as id
.
Use names that preserve meaning:
{
"project_id": "prj_123",
"external_project_id": "ext_999",
"name": "New project name"
}
Other common conflicts:
user_id
in both path and body;status
in query and body;version
in header and body;limit
in query and nested request body;id
fields inside nested objects.When in doubt, name the field by its role in the operation. The model should know whether it is selecting a resource, filtering a result, updating a value, or controlling request behavior.
After the AI client sends the MCP tool input, the handler maps it back to the API request.
For update_task
, the handler might do this:
async function updateTaskTool(input, auth) {
validateUpdateTaskInput(input);
const url = new URL(
`/workspaces/${input.workspace_id}/projects/${input.project_id}/tasks/${input.task_id}`,
API_BASE_URL
);
if (input.notify_assignee !== undefined) {
url.searchParams.set("notify_assignee", String(input.notify_assignee));
}
const body = {};
for (const field of ["title", "status", "assignee_id", "due_date"]) {
if (input[field] !== undefined) {
body[field] = input[field];
}
}
const response = await fetch(url, {
method: "PATCH",
headers: {
Authorization: `Bearer ${auth.accessToken}`,
"Content-Type": "application/json",
Accept: "application/json"
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(10000)
});
return mapTaskResponse(response);
}
This keeps the direction clear:
Avoid handlers that accept arbitrary paths, methods, headers, or bodies from the model. That turns a structured MCP tool back into a generic API proxy.
Validation should happen before the adapter sends the API request.
At minimum, check:
Some validation belongs in the MCP schema. Some belongs in code. Some still belongs in the upstream API.
The upstream API remains the final enforcement point for business rules. The MCP layer should prevent obvious bad calls and make errors easier to understand, but it should not replace the API's authorization and data validation.
For a successful update, the API might return:
{
"id": "tsk_789",
"status": "blocked",
"title": "Fix webhook retries",
"assignee_id": "usr_456",
"updated_at": "2026-08-26T10:00:00Z"
}
The MCP tool result should preserve the useful fields:
{
"task_id": "tsk_789",
"status": "blocked",
"title": "Fix webhook retries",
"assignee_id": "usr_456",
"updated_at": "2026-08-26T10:00:00Z"
}
For errors, keep the categories distinct:
400
means the request was invalid;401
means authentication failed;403
means the caller lacks permission;404
means the resource was not found;409
may mean a version or state conflict;429
means the API rate limit was hit;5xx
means the upstream API failed.Do not turn every error into "Tool failed." The agent and the developer both need the failure to say what kind of problem happened.
Test each input location separately.
For path parameters:
workspace_id
;project_id
with an invalid task_id
;For query parameters:
notify_assignee: true
;"yes"
instead of true
;For headers:
For request bodies:
For responses:
Testing should answer a larger question than "does the API return 200?" An AI client has to discover the tool, send valid structured input, get a useful result, and understand failures.
With 0mcp, the same mapping starts from a supported API definition or Postman collection.
The hosted workflow is:
0mcp currently supports hosted Streamable HTTP servers, not local stdio
servers. The original API remains responsible for business logic, authorization, pagination, rate limits, and data validation.
If your OpenAPI contract has weak parameter definitions, fix the source document first. The OpenAPI requirements guide covers the checks that matter before import.
Keep credentials out of the schema. Use runtime authentication and pass credentials to the upstream API from the server side.
id
Use workspace_id
, project_id
, task_id
, and similar names when the hierarchy matters. The AI client should not guess which ID goes where.
For update operations, identifiers are usually required, but editable fields may be optional. Add validation that requires at least one change instead of requiring every possible update field.
List and search tools need limits, cursors, allowed filters, and clear defaults. An unbounded query tool is hard to test and easy to misuse.
Map authentication, authorization, validation, not-found, conflict, rate-limit, timeout, and upstream errors separately. Vague failures slow down debugging.
Before publishing a parameter-mapped MCP tool, check:
Good MCP tool schemas do not make the AI client think in raw HTTP. They give the client a clear set of product-level inputs, then let the adapter place each value in the correct part of the API request.