Large language models are capable of reasoning over instructions, but they cannot safely operate your systems just because they can describe them. They need a controlled interface: a contract that defines what actions exist, what data can be read, and how every invocation is validated.
That is the role of the Model Context Protocol (MCP). MCP gives AI clients a standardized way to discover and call tools, read resources, and use reusable prompts. For C# teams, it offers an appealing path: wrap existing application services, databases, internal APIs, and operational workflows in a protocol designed for agent-driven interaction.
The important framing is this: an MCP server is not an AI application. It is a capability boundary. Its job is to translate a model's structured request into a narrow, observable, authorized operation in your system.
This article walks through the architecture, implementation shape, and production concerns of building an MCP server with .NET.
Start with capabilities, not endpoints
Traditional APIs are often designed around resources and HTTP verbs:
GET /customers/{id}POST /invoicesPATCH /orders/{id}
An MCP tool is designed around an intent a model can understand and invoke. Instead of exposing every endpoint, provide a small set of precise capabilities:
find_customerget_open_invoicesdraft_payment_remindercreate_support_case
This difference is subtle but important. A model selects tools based on names, descriptions, and input schemas. A vague tool named run_query creates a broad and risky decision surface. A tool named get_customer_account_summary, with an explicit customer identifier and a documented response shape, creates a predictable one.
Before writing code, build a capability inventory. For each proposed tool, answer these questions:
- What business outcome does it support?
- Is it read-only, advisory, or state-changing?
- Which identity is authorized to invoke it?
- What input values must be constrained?
- What information is safe to return to an AI client?
- How will an operator audit its use?
If a tool cannot answer those questions clearly, it is probably too broad.
The three MCP primitives
MCP servers commonly expose three kinds of functionality.
Tools
Tools perform a computation or action. They can query a service, create a ticket, calculate a quote, or trigger a workflow. A tool should have a clear name, a description written for both humans and models, and a strict input schema.
Resources
Resources expose contextual data that clients can read, often identified by a URI. A resource may represent product documentation, a tenant-specific configuration file, or a generated operational report. Resources are a better fit than tools when the interaction is fundamentally retrieval rather than a parameterized action.
Prompts
Prompts provide reusable instruction templates. They can standardize workflows such as incident triage, code review preparation, or customer escalation summaries. Prompts are useful, but they should not become a substitute for authorization or validation. Instructions can guide a model; server-side code must enforce policy.
For an initial C# implementation, tools are usually the highest-value place to begin.
Create a thin MCP layer over application services
The cleanest architecture keeps protocol concerns at the edge. Your MCP server should not contain all your business logic. It should call existing application services that already understand domain rules, persistence, authorization, and transactional boundaries.
A typical structure looks like this:
The MCP project becomes an adapter. It receives structured arguments, validates them, calls an application service, and maps the result into a deliberately small response object.
Using the .NET MCP SDK packages, a server can register tool-bearing classes through dependency injection. API names can evolve between SDK releases, but the implementation pattern is stable:
The code is intentionally ordinary .NET. That is a strength. Dependency injection, cancellation tokens, structured logging, EF Core, and typed records remain useful because MCP does not replace service architecture. It adds a protocol-facing integration layer.
Treat descriptions as part of the interface
Tool descriptions are not decoration. They are part of the model's routing and planning context.
A weak description says:
Looks up customer information.
A useful description says:
Returns account status and open invoice count for one known customer UUID. Do not use for searching customers by name. Returns no payment details or contact information.
The second version communicates purpose, required context, scope, and exclusions. Those boundaries reduce accidental misuse.
Names should be specific and verb-led. Prefer get_deployment_status over deployment, and prefer create_draft_refund over refund_customer. The latter sounds like an irreversible action; the former makes a review stage explicit.
Input schemas deserve the same care. Avoid accepting free-form JSON when an enum, numeric range, UUID, or constrained string will work. A model is more reliable when its available choices are explicit, and your server is safer when invalid choices are impossible to represent.
Use stdio locally and HTTP deliberately
MCP supports multiple transport models. Standard input/output is a practical default for local integrations: a client launches the server as a child process and exchanges protocol messages through stdin and stdout.
For a stdio server, stdout is protocol territory. Do not write diagnostic text, banners, or exception dumps to it. Route logs to stderr or to a configured telemetry sink. One stray Console.WriteLine can corrupt the message stream.
Remote deployment generally calls for an HTTP-based transport, often through an ASP.NET Core host. This introduces useful operational capabilities such as TLS termination, health checks, authentication middleware, rate limiting, and centralized observability. It also introduces a larger security surface.
A practical progression is:
- Build and test with stdio locally.
- Containerize the server with a non-root runtime user.
- Add remote transport only when a real client topology requires it.
- Put authentication and authorization in front of every protected capability.
Docker is especially useful here because it makes the runtime contract explicit: SDK version, environment variables, network access, mounted credentials, and startup command are all visible and repeatable.
Authorization must happen inside the tool path
A common mistake is assuming that because an MCP client is authenticated, every requested action is authorized. Authentication answers who is calling. Authorization answers what that caller may do in this tenant, for this record, at this time.
For multi-tenant systems, derive tenant context from trusted identity claims or server-side session state. Do not accept a tenantId argument and trust it blindly. If a tool needs a target customer ID, verify that the customer belongs to the caller's permitted tenant before returning anything.
State-changing tools need additional controls:
- require explicit, structured confirmation fields;
- use idempotency keys for retried operations;
- enforce business approval rules server-side;
- return a draft or preview before execution where possible;
- record immutable audit events with actor, target, arguments, outcome, and correlation ID.
A productive pattern is to separate prepare and execute operations. prepare_invoice_void can produce the effect and validation results. execute_invoice_void can require a short-lived approval token created by the preparation step. This keeps the model useful while preserving a meaningful control point for sensitive actions.
Design responses for decisions, not data dumps
An MCP response is context that a model may use in subsequent reasoning. Returning an entire database row, a raw exception, or a hundred-page report is usually counterproductive.
Return compact, typed, decision-ready results. Include stable IDs, statuses, short summaries, and relevant next actions. Omit secrets, unnecessary personal data, internal stack traces, and fields that encourage the model to make unsupported inferences.
For large result sets, add pagination and narrow filters. Better still, expose a purpose-built summary tool. get_sales_pipeline_summary is often more valuable than giving an agent unrestricted access to every opportunity record.
Make failures observable and safe
Models will call tools with incomplete context, malformed values, and occasionally invalid assumptions. Your server should fail predictably.
Classify errors into categories:
- Validation error: the supplied input is malformed or outside allowed bounds.
- Not found: the requested record does not exist or is not visible to the caller.
- Conflict: an action cannot proceed because current state changed.
- Policy denial: the caller is authenticated but not authorized.
- Transient dependency failure: a downstream system is unavailable.
Log the full diagnostic context internally, but return concise client-safe messages. Include a correlation ID so an operator can locate the corresponding trace without exposing internals to the model.
OpenTelemetry is a strong fit for this layer. Create an activity for each tool invocation and attach tool name, tenant-safe identifiers, duration, result category, and downstream dependency timings. Do not casually attach raw prompts, credentials, or sensitive arguments as telemetry attributes.
A server is ready when it can say no well
The most valuable MCP server is not the one with the most tools. It is the one whose capabilities are easy to discover, difficult to misuse, and simple to audit.
Start with a small read-only surface. Put typed application services behind clearly named tools. Make descriptions precise, schemas narrow, outputs compact, and authorization non-negotiable. Then add state-changing operations only after you have confirmation flows, idempotency, audit events, and production telemetry.
C# is well suited to this work because the hard parts are familiar: contracts, dependency injection, validation, domain boundaries, structured logging, and reliable hosting. MCP simply gives those engineering practices a new edge—one where software is operated not only by screens and scripts, but by carefully constrained AI clients.
No comments yet. Be the first to share your thoughts!