An MCP server is not merely an API with a new transport. It is a translation layer between probabilistic language models and deterministic software. That distinction changes how a C# developer should think about its design.
A conventional API is typically consumed by code written against documentation, generated clients, or strongly typed contracts. An MCP server is often consumed by a model that infers intent from names, descriptions, schemas, and prior conversation. The interface is still technical, but it must also be legible. Every tool definition becomes part capability, part documentation, and part safety boundary.
The most useful mental model is a workshop wall: each tool should be clearly labeled, easy to select, difficult to misuse, and sized for one kind of work. If an AI client sees a wall covered with vague switches labeled Run, Process, and Execute, it will make unreliable choices. If it sees search_customer_orders, get_order_status, and request_order_refund, it has a much better map.
This post focuses on the design work that makes an MCP server in C# dependable: choosing the right MCP primitive, shaping tool contracts, isolating infrastructure, and testing the interface as an AI-facing product.
Start with the three MCP primitives
The Model Context Protocol gives servers several ways to expose capabilities. Treating all of them as tools is a common design mistake.
Tools are actions
A tool is invoked by the client or model to perform a computation or operation. Good candidates include searching a repository, calculating a quote, querying an internal service, creating a draft, or requesting a deployment.
Tools should have explicit inputs, meaningful descriptions, and structured outputs. They are the right place for operations where parameters vary per request.
Resources are addressable context
A resource represents information the client can read, usually through a stable URI. A product catalog, a service runbook, a generated architecture diagram, or a tenant-specific policy document may fit naturally as a resource.
If the information is primarily something to inspect rather than something to execute, a resource is usually clearer than a tool. This matters because it communicates intent: reading docs://incident-response/on-call is conceptually different from calling search_runbooks(query).
Prompts are reusable interaction patterns
Prompts provide named templates that guide users or clients through repeatable workflows. For example, a review_pull_request prompt may accept a repository and pull request number, then establish a review format and request the required context.
Prompts do not replace application logic. They package a useful conversational starting point. Use them when the value lies in consistent framing rather than a backend operation.
Choosing among these primitives makes the server easier to discover. A healthy MCP catalog does not expose every capability through one overloaded tool namespace.
Make tool names read like small commitments
Tool naming is one of the highest-leverage design decisions. A name should tell a model what the operation does, what domain it belongs to, and whether it changes state.
Compare these examples:
They leave important questions unanswered. Does lookup search customers, invoices, or logs? Is customer_data a read operation or an update? Can manage_invoice send a payment reminder or void an invoice?
Prefer names that describe one bounded capability:
The verbs are intentional. find, get, and list imply reads. create, update, send, cancel, and delete indicate mutations. This vocabulary gives models and human operators an immediate risk signal.
Descriptions should add operational detail that a name cannot carry. Include prerequisites, units, defaults, constraints, and side effects. For a mutation, say what changes and what does not.
The description is not decorative metadata. It is the operating manual available at the moment a client decides whether to call the tool.
Design schemas for ambiguity, not just compilation
C# types provide an excellent foundation for tool schemas, but a type that compiles is not necessarily a contract that an AI client can use safely.
Avoid parameter bags with loosely typed strings when the domain can be modeled directly. A string request parameter forces the model to invent an unstructured mini-language. A record with named fields gives it constraints.
This shape makes optionality visible and prevents accidental combinations from being hidden in a free-form text field. Where possible, use enums for finite choices, numeric types for quantities, and dedicated value objects for identifiers with meaningful rules.
However, schemas need semantic validation after deserialization. A type can represent a date range where CreatedAfter is later than CreatedBefore; it should not silently permit it. Validate at the boundary and return an actionable error.
Useful error messages identify the invalid field, explain the violated rule, and suggest a correction. “Validation failed” is technically true but operationally unhelpful.
Keep output contracts structured too. A prose paragraph may be pleasant to read, but clients often need stable fields they can reason about. Return identifiers, statuses, timestamps, warnings, and next actions explicitly. You can include a human-readable summary alongside those fields without making it the only result.
Put an application boundary behind every tool
An MCP tool method should be thin. It should translate the protocol-facing request into an application command or query, invoke domain logic, and map the result into a protocol-facing response.
Do not place SQL, HTTP retries, authorization decisions, and business rules directly inside attributed tool methods. Those methods become difficult to test, difficult to reuse, and too tightly coupled to a specific protocol.
A practical C# structure looks like this:
The MCP host is an adapter. Your application service should remain useful from a REST API, background worker, or command-line process. This separation also reduces the risk of protocol changes leaking through the entire codebase.
Dependency injection makes this straightforward. Register database contexts, typed HttpClient instances, repositories, and application services in the host, but keep tool constructors focused on interfaces that express application intent.
Cancellation deserves special attention. Tool calls may be interrupted because the user changed direction, the client timed out, or the connection ended. Pass CancellationToken through every asynchronous layer: EF Core queries, outbound HTTP requests, queue operations, and long-running calculations. Cancellation is not an edge case in interactive AI workflows; it is normal control flow.
Treat authorization as a capability decision
A model choosing a tool does not imply that the caller is authorized to perform the operation. Authentication identifies the client or user; authorization decides what that identity may do.
For read tools, scope results to the authenticated tenant and role. Never accept a tenant ID from the model and trust it as the authorization boundary. For write tools, make the required permission explicit and apply it before side effects occur.
High-impact operations should usually be designed as staged workflows. Instead of one delete_customer tool, expose a read-only inspection tool, a create_customer_deletion_request tool, and an approval process managed by your application. The additional step is not friction for its own sake. It creates an audit trail and makes irreversible actions intentional.
This is especially important because natural-language systems can be influenced by untrusted text. Tool descriptions, resource content, and retrieved documents may all contain instructions. Your server must enforce policy in code, not assume that the client will always select tools prudently.
Test the catalog, not only the implementation
Unit tests should cover domain services and validation rules, but MCP servers need an additional layer of testing: contract and discoverability testing.
First, inspect the advertised tool catalog. Are names unique? Are descriptions specific? Do schemas expose required fields correctly? Are mutation tools clearly marked through naming and documentation?
Second, test representative invocations at the protocol boundary. Include valid calls, missing required inputs, malformed identifiers, unauthorized identities, cancellation, upstream timeouts, and idempotency behavior for writes.
Third, perform scenario tests using realistic user phrasing. Ask whether the available tools make the right path obvious. If a user wants to know why an invoice is overdue, should the client call a broad get_invoice tool, a search_events tool, or several tools in sequence? If the answer is unclear to a human reviewer, it will likely be unclear to a model.
Logging should capture tool name, duration, outcome category, correlation ID, and authenticated principal. Be careful with inputs: redact credentials, personal data, and sensitive business fields. Observability should help diagnose behavior without becoming a second data leak.
The interface is the product
The C# code behind an MCP server may be elegant, but the server succeeds or fails at its boundary. Models need a capability surface that is precise enough to navigate and constrained enough to trust.
Build tools as narrow commitments. Use resources for readable context and prompts for repeatable conversational workflows. Model inputs and outputs as explicit schemas. Keep protocol code thin, enforce authorization independently, propagate cancellation, and test what clients can actually discover.
When these choices are made deliberately, an MCP server stops being a collection of callable methods. It becomes a well-designed interface between language, intent, and the systems your organization depends on.
No comments yet. Be the first to share your thoughts!