[KongchangAI]
· 3 min read· 1,790 words

Designing APIs for AI Agents: 5 Practical Rules for .NET Developers

Designing APIs for AI Agents: 5 Practical Rules for .NET Developers

5 practical API design principles for .NET developers building backends that AI Agents can reliably consume.

This article, written from a .NET backend developer's perspective, addresses how AI Agents consume APIs differently from humans — relying entirely on machine-readable specs, unable to debug, and retrying at high speed. It distills five core principles: annotate every field with complete type and constraint info; maintain global naming and status consistency; use RFC 9457 structured errors with field names and valid values; enforce idempotency via Idempotency-Key with atomic checks; and expose focused composite operations rather than raw CRUD endpoints. The article also introduces MCP as a higher-level Agent tool protocol with a C# SDK example, and closes with a practical self-review checklist.

Why Developers Should Rethink API Design

APIs have long been designed for frontend teams, mobile clients, or other developer groups. But a new type of consumer is emerging — AI Agents. This article is based on the learning notes of a .NET backend developer, and the core question is straightforward: when someone says "an AI Agent will be calling your API," what does that actually mean? And what do our APIs need to change?

The answer starts with understanding the fundamental differences between Agents and human developers. Human developers read documentation, make educated guesses about ambiguous fields, actively debug when errors occur, and generally work at a measured pace. AI Agents are the complete opposite: they rely almost entirely on OpenAPI specs and field descriptions to understand interfaces, tend to make incorrect inferences about ambiguous fields, repeat the same mistakes when error messages lack clear guidance, and retry extremely fast — which means they can fail at an equally rapid pace.

AI Agent API Design Learning Notes

These differences are precisely what drive the following set of Agent-oriented API design principles.

Principle 1: Complete Schema with Meaningful Descriptions

Agents don't browse documentation the way humans do — they rely almost entirely on machine-readable interface descriptions. Every field should therefore be annotated with its type, format, constraints, and a description. In .NET, simply adding attributes to your request model will automatically surface this information in the OpenAPI spec:

public record PlaceOrderRequest(
    [property: Description("The ID of the customer placing the order")]
    Guid CustomerId,

    [property: Description("The ID of the product")]
    Guid ProductId,

    [property: Range(1, 1000)]
    [property: Description("Quantity of the product (integer, minimum 1)")]
    int Quantity);

Endpoint descriptions are equally critical. Compared to something vague like "Gets the data," a description like "Returns a paginated list of invoices filtered by status and date range" gives an Agent a much stronger signal for decision-making. A hypothetical scenario in the original article illustrates this well: if quantity has no type annotation in the spec, an Agent might send the string "2", and if the server responds with just {"error": "Invalid input"}, the Agent has no way to determine what went wrong — so it keeps sending the same request, getting stuck in an infinite loop.

Principle 2: Globally Consistent Conventions

Agents learn patterns from one endpoint and apply them to others. If one endpoint uses pending for a status value, another uses IN_PROGRESS, and a third uses done, the Agent will get confused. Consistency needs to span multiple dimensions:

  • Naming style (camelCase or snake_case — pick one, don't mix)
  • ID format (e.g., always use UUID)
  • Pagination approach
  • Error structure
  • Status value casing conventions

Consistency is a "nice-to-have" for human developers, but for Agents it's often the difference between getting it right and getting it wrong.

Principle 3: Structured Error Responses

Error messages shouldn't just signal that something went wrong — they should serve as corrective guidance for the Agent: which field was wrong, what value was received, and what values are actually valid. ASP.NET Core has built-in support for RFC 9457 (Problem Details), which you can extend with custom fields:

return Results.Problem(
    title: "Invalid field value",
    detail: "Field 'status' received invalid value 'processing'.",
    statusCode: StatusCodes.Status400BadRequest,
    extensions: new Dictionary<string, object?>
    {
        ["field"] = "status",
        ["receivedValue"] = "processing",
        ["allowedValues"] = new[] { "pending", "shipped", "delivered", "cancelled" }
    });

The resulting JSON clearly tells the caller: the problematic field is status, the value received was processing, and valid values are pending/shipped/delivered/cancelled. Compared to {"error": "Invalid input"}, an Agent that receives the structured response can immediately send the correct value on the next call, rather than retrying blindly.

RFC 9457 (Problem Details for HTTP APIs) is an IETF standard officially published in 2023, succeeding RFC 7807. It defines a common JSON error response format with five standard fields: type (error type URI), title (human-readable title), status (HTTP status code), detail (specific description), and instance (the request URI that triggered the error), with support for extension fields carrying domain-specific information. ASP.NET Core has treated Problem Details as the built-in default error format since version 8.0, enabled globally via the AddProblemDetails() middleware. The value for Agents lies in the structured extensions fields (such as allowedValues), which can be parsed directly by an Agent's reasoning logic to correct parameters on the next request — no natural language understanding required to interpret vague error text. This reduces error handling from a "semantic understanding" problem to a "structured data reading" problem, significantly improving reliability.

Principle 4: Idempotency for Safe Retries

Agents (and any client, really) will automatically retry after a network timeout. If POST /orders is sent twice, you might end up with two orders. The solution is to have clients include an Idempotency-Key request header. The server stores this key, and when the same key arrives again, it doesn't create a new order — it returns the previous result.

The original article provides a simplified example for conceptual understanding (in-memory only; production use requires Redis or a database):

var seenKeys = new ConcurrentDictionary<string, bool>();

app.MapPost("/orders", (HttpRequest http, PlaceOrderRequest body) =>
{
    var key = http.Headers["Idempotency-Key"].ToString();

    // TryAdd is atomic: only one of two concurrent requests with the same key succeeds
    if (!string.IsNullOrEmpty(key) && !seenKeys.TryAdd(key, true))
        return Results.Conflict("A request with this key has already been processed.");

    // ... create order ...
    return Results.Ok();
});

Two key points are worth emphasizing here. First, atomic checking: splitting this into "check if it exists, then write" creates a race condition where two concurrent requests could both pass the check. Redis's SET NX or a database unique constraint exist precisely for this reason. Second, in a real system you should store and return the previous successful response rather than just returning a Conflict — the code above is purely illustrative.

Idempotency refers to the property where executing the same request once or multiple times produces identical results. By HTTP specification, GET, PUT, and DELETE are naturally idempotent, while POST is not by default. The Idempotency-Key pattern was popularized by Stripe in payment processing and has since become standard practice in financial and e-commerce APIs. Production-grade implementations typically store the key alongside the serialized response in Redis with a reasonable TTL (e.g., 24 hours), returning the full cached response when the same key arrives again — rather than a Conflict status code — making the behavior completely transparent to callers who can't distinguish between a "first success" and an "idempotent replay." Note that responsibility for generating the key lies with the client (the Agent), typically using UUID v4; the server only handles checking and storage. For high-frequency, unsupervised callers like AI Agents, lacking idempotency protection can result in a large number of duplicate records being created in a short period — far worse consequences than a human making the same mistake.

Principle 5: Thoughtfully Designed Operations, Not Just CRUD

If every endpoint is exposed as a tool for an Agent, it faces too many choices, and the probability of selecting the wrong one increases accordingly. A smaller set of clearly described operations is far more practical.

For example, if placing an order requires three calls — "fetch customer → check inventory → create order" — it's better to encapsulate this into a single PlaceOrder operation. The Agent doesn't have to orchestrate the steps itself, and there are fewer intermediate points where things can go wrong. This reduces the Agent's reasoning burden while improving overall reliability.

MCP: A Further Evolution Toward Agent-Readiness

MCP (Model Context Protocol) is an open protocol for connecting AI Agents to external tools and data — described in the original article as "a USB port for AI." Through MCP, you can expose operations to MCP-compatible Agents, making them easier to discover and use. .NET offers an official C# SDK, and a tool definition looks roughly like this:

[McpServerToolType]
public class OrderTools
{
    [McpServerTool, Description("Creates a new order for the specified customer.")]
    public async Task<string> PlaceOrder(
        [Description("Customer ID")] Guid customerId,
        [Description("Product ID")] Guid productId,
        [Description("Quantity, minimum 1")] int quantity)
    {
        // Call your existing application logic here
        return "created";
    }
}

A word of caution: the SDK changes frequently, so refer to the official repository for the latest setup code. Principle 5 applies here as well — don't turn every endpoint into an MCP tool. Only expose the ones Agents actually need.

MCP was released and open-sourced by Anthropic in late 2024, and subsequently adopted by major AI vendors including OpenAI and Google, quickly becoming the de facto standard for Agent tool invocation. It defines a standardized JSON-RPC communication protocol that allows AI models to discover and invoke external Tools, read Resources, and use Prompt templates in a unified way. Compared to calling REST APIs directly, MCP's advantage lies in stronger tool self-description — each tool carries a structured name, description, and parameter schema, so Agents don't need to know the calling details in advance and can dynamically decide at runtime which tool to use. MCP supports two transport layers: stdio (suitable for local inter-process communication) and SSE/HTTP (suitable for remote services), making it viable both as an embedded local development tool and as a network service. Understanding MCP's role helps clarify its relationship with OpenAPI: OpenAPI describes the structure of HTTP interfaces, while MCP is a higher-level abstraction protocol for "Agent-callable capabilities." The two can coexist — an MCP server can continue calling your existing REST APIs internally.

A Practical Checklist

The original article also provides a useful self-review checklist:

  • Is the OpenAPI spec complete, with every field having a type, format, and constraints?
  • Do descriptions convey intent, not just inputs and outputs?
  • Are errors structured, including field names and valid values?
  • Do state-modifying endpoints support idempotency?
  • Are responses small and focused?
  • Are composite operations used where appropriate?
  • Are pagination, naming, and status values globally consistent?
  • Are authentication and rate limiting documented in the spec?

The author's final recommendation is to put this into practice: spend one to two hours building a POST /orders endpoint with intentionally vague types and errors, call it with an MCP-compatible client, observe where it gets stuck, then apply these five principles one by one and note how the behavior changes each time.

Closing Thoughts: Good APIs Work for Both Humans and Agents

APIs have traditionally been designed for developers — but Agents are now a new class of consumer. They don't guess, and they don't debug. An API with a clear schema, actionable error guidance, and safe retry semantics is a better design for both Agents and humans alike. This may well be the engineering intuition that backend developers need to rebuild for the age of AI Agents.

#AI Agent API design#.NET API best practices#MCP Model Context Protocol#idempotency API#OpenAPI schema#RFC 9457 Problem Details#ASP.NET Core Agent
Share:

Related articles