tutorials2026-04-098 min read

How We Tuned a Todo API for Better MCP Tool Selection

A concrete walkthrough of how we changed the Todo sample API, Emcy's hosted tool parser, and openapi-to-mcp so MCP agents could find the right completion tool more reliably.

E

Emcy Team

Engineering

We hit a familiar MCP failure mode in our Todo sample:

The update tool existed, but the embedded agent sometimes replied as if it didn't.

This wasn't a raw "the model is bad" problem. The bigger issue was that the OpenAPI surface and the generated MCP metadata were not giving the selection system enough semantic signal.

This post walks through the exact code changes we made to fix that.

It covers three layers:

  1. the Todo API surface
  2. Emcy's hosted OpenAPI-to-tool parser
  3. the openapi-to-mcp generator

If you're building with Emcy, these are the kinds of changes that materially improve both hosted agents and generated MCP runtimes.

The Problem

The original Todo API exposed a legacy completion route that looked like this:

app.MapPost("/api/todos/{id:guid}/toggle", async (
    Guid id,
    HttpContext httpContext,
    SqlOSAuthService authService,
    TodoFgaService todoFgaService,
    ISqlOSFgaAuthService fgaAuthService,
    IOptions<TodoSampleOptions> sampleOptions,
    TodoSampleDbContext dbContext,
    CancellationToken cancellationToken) =>
{
    // ...
    item.IsCompleted = !item.IsCompleted;
    item.CompletedAt = item.IsCompleted ? DateTime.UtcNow : null;
    await dbContext.SaveChangesAsync(cancellationToken);
    return TypedResults.Ok(ToTodoItemResponse(item));
});

That route is fine for a local UI button. It is not great MCP metadata.

For an agent, POST /api/todos/{id}/toggle has a few problems:

  • toggle is weaker than complete, reopen, or set completion
  • the desired state is implicit instead of explicit
  • a generic tool key derived from method and path ends up looking like post_api_todos_by_id_toggle
  • if the rest of the metadata is thin, semantic search can miss it for requests like "mark this task done"

This matters even more in Emcy because tool selection is not just "send every tool to the model." We do semantic pre-filtering before the main LLM call. If the tool is a poor semantic match, it may never reach the model at all.

Change 1: Make the Mutation Explicit

We kept the legacy toggle route for the local UI, but removed it from the OpenAPI tool surface with ExcludeFromDescription().

Then we added an explicit completion route for agents:

app.MapPatch("/api/todos/{id:guid}/completion", async (
    Guid id,
    UpdateTodoCompletionRequest request,
    HttpContext httpContext,
    SqlOSAuthService authService,
    TodoFgaService todoFgaService,
    ISqlOSFgaAuthService fgaAuthService,
    IOptions<TodoSampleOptions> sampleOptions,
    TodoSampleDbContext dbContext,
    CancellationToken cancellationToken) =>
{
    // ...
    item.IsCompleted = request.IsCompleted;
    item.CompletedAt = request.IsCompleted
        ? item.CompletedAt ?? DateTime.UtcNow
        : null;

    await dbContext.SaveChangesAsync(cancellationToken);
    return TypedResults.Ok(ToTodoItemResponse(item));
})
.WithName("update_todo_completion_status");

This single change improves multiple signals at once:

  • the HTTP method is now PATCH, which communicates mutation
  • the path now includes /completion, which helps canonical tool naming
  • the request body now carries the target state explicitly with isCompleted
  • the operation ID becomes update_todo_completion_status

For a generator that canonicalizes tools from method and path, that means the tool key becomes much closer to the user intent:

Before

post_api_todos_by_id_toggle

After

patch_api_todos_by_id_completion

That is a much better match for requests like:

  • "mark the car estimate task done"
  • "complete that todo"
  • "reopen the task"

Change 2: Write Summaries and Descriptions for the Selector

We added explicit OpenAPI summaries and descriptions to every task-management endpoint.

For the completion endpoint:

.WithOpenApi(operation =>
{
    ApplyOpenApiOperationMetadata(
        operation,
        "update_todo_completion_status",
        "Set whether a todo item is completed",
        "Use this when the user asks to mark a task done, complete it, reopen it, mark it open, or uncheck it. Set `isCompleted` to `true` to complete the task and `false` to reopen it. Prefer this explicit update endpoint over toggle-style routes when the desired state is known.");
    DescribeOpenApiParameter(
        operation,
        "id",
        "Unique identifier of the todo item whose completion status should be updated.");
    DescribeJsonRequestBody(
        operation,
        "Desired completion state for the selected todo item.",
        new Dictionary<string, string>
        {
            ["isCompleted"] = "Set to true to mark the todo complete. Set to false to reopen it."
        },
        "isCompleted");
    return operation;
});

The important part is not just "document your API."

The important part is what kind of language you use:

  • name the action directly
  • include the user intents that should trigger the tool
  • describe the parameter in product language, not internal implementation language

For MCP tool selection, a description like:

Use this when the user asks to mark a task done, complete it, reopen it, mark it open, or uncheck it.

is dramatically better than:

Toggle todo

Change 3: Keep Non-Tools Out of the Tool Catalog

The Todo sample also exposes helper endpoints for:

  • sample configuration
  • protected-resource metadata
  • portable client metadata

Those routes are useful for the product. They are not useful as agent tools.

So we hid them from the OpenAPI description surface:

app.MapGet("/sample/config", ...).ExcludeFromDescription();
app.MapGet("/.well-known/oauth-protected-resource", ...).ExcludeFromDescription();
app.MapGet(sampleConfig.PortableClientPath, ...).ExcludeFromDescription();

This is a simple but important rule:

If you don't want a route to become an MCP tool, don't leave it in the OpenAPI surface you're feeding into tool generation.

Tool selection quality improves when the catalog contains fewer distractors.

Change 4: Preserve Request-Body Descriptions in openapi-to-mcp

The next issue was downstream.

Even if the OpenAPI request body had a useful description, openapi-to-mcp was flattening it into a generic placeholder:

description: "The JSON request body."

We changed the parser and mapper so request-body descriptions survive into the generated MCP input schema:

properties.requestBody = {
  ...endpoint.requestBody.schema,
  description:
    endpoint.requestBody.description
    || endpoint.requestBody.schema.description
    || "The JSON request body.",
};

That means a generated MCP tool can now carry forward request-body guidance like:

Desired completion state for the selected todo item.

instead of the model seeing a totally generic requestBody.

This is especially useful for patch-style endpoints where the nested fields matter.

Change 5: Align Emcy's Hosted Tool Parser With the OpenAPI Metadata

We also updated the hosted Emcy-side parser that builds the tool catalog from OpenAPI.

Previously it effectively did this:

Description = summary ?? description ?? $"Executes {method} {path}"

That throws away the long description whenever a summary exists.

We changed it to combine both:

if (!string.IsNullOrWhiteSpace(summary) &&
    !string.IsNullOrWhiteSpace(description) &&
    !string.Equals(summary, description, StringComparison.Ordinal))
{
    return $"{summary}\n\n{description}";
}

We also merged parameter and request-body descriptions into the stored schema when those descriptions exist in OpenAPI.

That matters because Emcy's hosted tool-selection stack embeds and filters tools before the main LLM turn. If you drop the richer description before it reaches the catalog, you lose the benefit of the better OpenAPI annotations.

What the Result Looks Like

After the changes, the completion tool now has strong signals at every layer:

API shape

PATCH /api/todos/{id}/completion

Operation ID

update_todo_completion_status

Summary

Set whether a todo item is completed

Description

Use this when the user asks to mark a task done, complete it, reopen it, mark it open, or uncheck it.

Parameter metadata

id = Unique identifier of the todo item whose completion status should be updated.

Request body metadata

isCompleted = Set to true to mark the todo complete. Set to false to reopen it.

This gives you three concrete outcomes:

  1. better semantic retrieval before the model call
  2. better model understanding once the tool is attached
  3. less ambiguity between "toggle" and "set to done"

The Practical Rules

If you want developers to get the most out of MCPs, especially through Emcy, these are the patterns worth copying:

1. Prefer explicit state-setting routes over toggle routes

Bad for agents:

POST /things/{id}/toggle

Better:

PATCH /things/{id}/status
PATCH /things/{id}/completion
PATCH /things/{id}/visibility

2. Treat operationId, summary, and description as retrieval metadata

They're not just docs.

They are ranking features for the tool selector.

3. Put user-intent language in descriptions

Include phrases like:

  • use this when the user asks to...
  • mark done / reopen / cancel / archive
  • add / create / remember / remove

4. Describe request-body fields, not just query params

If the tool takes a body, the model needs to know what the fields mean.

5. Remove helper endpoints from the OpenAPI surface used for MCP generation

Auth metadata, health checks, config endpoints, and client manifests usually make poor tools.

Why This Matters for Emcy

Emcy does semantic tool filtering before the main LLM turn. That keeps token usage down and improves reliability at larger tool counts, but it also means poor metadata hurts earlier in the pipeline.

If your API surface is vague, the tool may never be attached.

If your API surface is precise, the model sees the right tool more often, with better argument guidance when it does.

That is the difference between:

I don't see a way to update this task.

and:

Calling update_todo_completion_status with isCompleted=true.

If you're building an MCP from OpenAPI, this is the bar to aim for:

  • explicit operation shapes
  • action-oriented route semantics
  • intent-rich summaries and descriptions
  • descriptive request-body fields
  • a tool catalog that contains only real tools

The Todo sample now follows that pattern, and the same guidance applies to real APIs in production.

For broader guidance, read How to Make Your OpenAPI Spec MCP-Friendly and Token-Efficient Tool Selection: How We Scaled From 10 Tools to 300.

Tags
MCP
OpenAPI
tool selection
semantic search
agent engineering
Emcy