Skip to main content

Command Palette

Search for a command to run...

Should Your .NET App Expose an MCP Server?

Updated
14 min readView as Markdown
Should Your .NET App Expose an MCP Server?
P
Senior Software Engineer specialising in cloud architecture, distributed systems, and modern .NET development, with over two decades of experience designing and delivering enterprise platforms in financial, insurance, and high-scale commercial environments. My focus is on building systems that are reliable, scalable, and maintainable over the long term. I’ve led modernisation initiatives moving legacy platforms to cloud-native Azure architectures, designed high-throughput streaming solutions to eliminate performance bottlenecks, and implemented secure microservices environments using container-based deployment models and event-driven integration patterns. From an architecture perspective, I have strong practical experience applying approaches such as Vertical Slice Architecture, Domain-Driven Design, Clean Architecture, and Hexagonal Architecture. I’m particularly interested in modular system design that balances delivery speed with long-term sustainability, and I enjoy solving complex problems involving distributed workflows, performance optimisation, and system reliability. I enjoy mentoring engineers, contributing to architectural decisions, and helping teams simplify complex systems into clear, maintainable designs. I’m always open to connecting with other engineers, architects, and technology leaders working on modern cloud and distributed system challenges.

AI features in .NET apps are starting to move past the simple chat box. At first, Devs usually adds one model call. The app sends a prompt, gets a response, and shows it to the user. That is easy enough to understand. Then the feature grows. The model needs to read documents. Then it needs to query internal data. Then it needs to call an API. Then it needs to create a draft ticket, check a workflow state, or look up the latest failed background job. Thats where a lot of AI integrations start to get messy.

Every tool gets its own custom wrapper. Every assistant has its own way of describing what it can call. One integration expects JSON in one shape. Another expects a function schema. Another needs a custom HTTP endpoint. Before long, the AI feature has become a small integration platform hiding inside the application. Model Context Protocol, usually shortened to MCP, is an attempt to standardise that boundary.

For .NET engineers, the interesting part sits beyond the protocol ceremony. MCP changes the architecture of your application because it gives an AI host a new route into your system. That can be useful, it can also be a very neat way to build a back door into production if youre not careful.

What MCP actually gives you

MCP is an open protocol for connecting AI applications to external capabilities. An MCP server can expose tools, resources, and prompts. An MCP client can discover those capabilities and call them through a standard protocol. The current MCP specification is built around JSON-RPC 2.0 messages. That means the basic interaction is still familiar to anyone who has worked with RPC-style systems. A client sends a request with a method name and parameters. The server returns either a result or an error. MCP adds lifecycle handling and capability negotiation. It also defines server features, client features, logging, and authorisation rules for HTTP-based transports. That sounds abstract, so put it in .NET terms.

AI host or agent
        |
        | MCP client
        v
Your MCP server
        |
        v
Your application services

The MCP server is the boundary you expose to the AI world. Behind that boundary should be the same kind of application code you would trust from any other integration. MCP gives you a standard shape for the AI-facing side. It does not remove the need to design the application facing side properly.

Where .NET fits now

The .NET story around MCP is now concrete enough to take seriously. Microsoft has a quickstart for creating a minimal MCP server using C#. It uses the C# SDK for MCP, connects the server to GitHub Copilot, and shows the server being published to NuGet for stdio transport. The template package is still marked as preview, which matters if you are thinking about using it as part of a production platform rather than a developer tool. The template can create a local server using stdio or a remote server using HTTP. In the generated project, Program.cs wires up the MCP server and transport. A tool class exposes callable operations using MCP attributes. The template also includes an example tool so you can verify that Copilot can discover and call the server.

That is the useful bit for .NET engineers. We dont have to treat MCP as something that only exists in TypeScript or Python examples. We can build MCP servers in C#. We can package them, run them locally, expose them over HTTP where appropriate, and connect them to the rest of the .NET application stack.

The shape is simple enough:

The important box in that diagram is the policy boundary between the tool call and your application services.

Local MCP and remote MCP are different decisions

A local MCP server is usually a developer productivity tool. It might inspect a solution, list projects, or check test failures. It might analyse NuGet packages or explain why an architectural rule failed. It runs on the developer machine. It is normally connected to an IDE or agent running in that same development context. That can still be risky. A local server may have file access, environment variables, source code, and local credentials. But the blast radius is usually different from a remote service touching shared business systems. A remote MCP server is a production integration point. Once you put it behind HTTP and connect it to live systems, it needs to be treated like any other public or internal API. It needs authentication. It needs authorisation. It needs rate limits. It needs audit logs. It needs tenant boundaries. It needs a clear answer to a boring but important question: who is the tool call acting on behalf of? That last question is where a lot of agent architectures get vague. If a user asks an assistant to fetch a claim summary, the MCP server should not magically inherit full backend permissions. The call should be checked against the user, tenant, role, and business context. If the user cannot fetch that claim through the normal UI or API, the agent should not be able to fetch it through MCP.

Thats the standard I would use before putting any remote MCP server near real business data.

Expose capabilities, not internals

The easiest way to build a dangerous MCP server is to expose generic power. A tool called run_sql_query looks convenient. It also gives the model a path straight past your application layer. Even if you try to limit it with instructions, you have still created a tool whose purpose is to bypass business rules. A tool called get_claim_summary is much safer. It can call an application service. The service can validate the claim reference and check the current user. It can apply tenant rules, shape the response, and hide fields the caller should not see.

The same applies to write operations. A tool called update_policy_field is too broad. A tool called request_policy_address_change is narrower and easier to reason about. It can create a pending request, attach evidence, and require approval before anything mutates live state.

Thats the design rule I would keep coming back to:

Expose business capabilities through MCP. Do not expose your internals.

The tool name should describe a thing your application intentionally allows. The input contract should be narrow. The output should be shaped for the agent and safe for the user. The implementation should sit on top of your normal application layer. Tool descriptions help the model choose the right capability. They are not a security boundary.

A small C# example

A minimal tool class can look harmless:

using System.ComponentModel;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;

[McpServerToolType]
public sealed class ClaimTools(
    IClaimLookupService claims,
    ICurrentUserContext currentUser,
    ILogger<ClaimTools> logger)
{
    [McpServerTool]
    [Description("Gets a short claim summary for a claim reference the current user is allowed to view.")]
    public async Task<ClaimSummaryResponse> GetClaimSummaryAsync(
        [Description("The claim reference, for example CLM-12345.")]
        string claimReference,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(claimReference))
        {
            throw new ArgumentException(
                "Claim reference is required.",
                nameof(claimReference));
        }

        var result = await claims.GetSummaryForUserAsync(
            userId: currentUser.UserId,
            claimReference: claimReference.Trim(),
            cancellationToken: cancellationToken);

        if (result is null)
        {
            logger.LogWarning(
                "Claim summary lookup failed for claim {ClaimReference} and user {UserId}.",
                claimReference,
                currentUser.UserId);

            throw new InvalidOperationException(
                "The claim was not found or the current user cannot access it.");
        }

        return result;
    }
}

The attribute is not the interesting part.

The important detail is that the tool does not query the database directly. It calls IClaimLookupService. That service should already know how to enforce the rules of the application.

You can make this even cleaner by modelling the input and output explicitly:

public sealed record ClaimSummaryResponse(
    string ClaimReference,
    string Status,
    string AssignedHandler,
    IReadOnlyList<string> RiskFlags);

The output is small on purpose. An AI-facing tool does not need to dump your entire entity graph. It should return the data needed for the task, in a shape that is safe to share with the calling context.

The tool boundary is where the risk lives

Most teams worry about prompts first. That is understandable because prompts are visible and easy to change. But once an agent can call tools, the prompt is only one part of the risk. The tool call is where something actually happens. A model can ask for the wrong claim. It can pass an ambiguous reference. It can retry an operation. It can call tools in an odd order. It can misunderstand a user request. It can combine stale context with fresh data. It can produce a plausible explanation for a tool result that does not quite support that explanation.

This is why the MCP server needs boring production controls. Start with read-only tools. Add write operations later. When you do add writes, prefer draft or request based operations. Let the user confirm the action. Keep an audit record of the prompt, selected tool, arguments, and result. Also record the final user action. For sensitive operations, the agent should prepare the work rather than complete it silently.

That distinction changes the shape of the tool.

Bad:   close_claim
Better: create_claim_closure_request

The second operation gives the system somewhere to attach evidence. It gives a human or workflow a chance to approve the action. It also gives support teams something useful to inspect when a user asks why the assistant suggested it.

How MCP fits with Semantic Kernel and Agent Framework

MCP is the protocol boundary. Semantic Kernel or Microsoft Agent Framework can sit above that boundary and use the tools the server exposes. Semantic Kernel supports adding plugins from MCP servers to agents. The documentation covers local MCP servers and streamable HTTP plugin connections. That is useful when you already have a server exposing capabilities and want to make those available to an agent without writing a custom plugin for every tool.

Agent Framework has MCP integration as well. The .NET guidance shows creating an MCP client, listing available tools from a server, converting those tools so they can be supplied to an agent, and then invoking them through function calling. That gives a reasonable split of responsibilities. Your MCP server exposes carefully designed capabilities. Your agent framework decides when to call them. Your application services enforce the rules. Your telemetry tells you what happened.

Do not let those responsibilities blur together. The moment your tool implementation starts doing everything in one place, the architecture will become hard to test. Orchestration needs a clear boundary. Policy does too. So do data access, prompt shaping, and workflow decisions.

A realistic .NET use case

Imagine a claims system with an internal assistant.

The first version should avoid writes completely. Give the assistant a few safe tools:

get_claim_summary
get_open_authority_approvals
get_movement_status
get_recent_claim_notes

Each tool calls a normal application service. Each service checks the current user. Each response is deliberately small.

Now the assistant can answer questions like:

Why is this claim waiting for review?

The agent might call get_claim_summary, then get_movement_status, then get_open_authority_approvals. It can explain that the claim is blocked because an authority approval is still open. That is genuinely useful, and it does not require the agent to have broad access to the database.

Later, you might add a write-shaped operation:

create_authority_approval_follow_up_request

That still should not directly approve anything. It creates a request. The normal workflow handles the rest.

This is where MCP can be a good fit. You get a standard way for the agent to discover and call capabilities, but you keep the business rules in the system that already owns them.

Where I would avoid MCP

I would avoid MCP for ordinary service-to-service integration. If one .NET service needs to call another .NET service, use the boring tools we already understand. A typed HTTP client, a queue, or a normal internal API is usually a better fit. I would also avoid using MCP to mirror an entire REST API. That gives the agent far too many choices and creates a large permissions problem. Most agents work better with fewer, clearer tools anyway.

Id be careful with generic diagnostic tools. A tool that reads logs may sound harmless until you remember that logs often contain identifiers, payload fragments, and operational details. A safer design is to expose a specific support operation, such as get_recent_failed_imports, and shape the response before the model sees it.

The same applies to database access. If the tool accepts arbitrary SQL, you have already lost the architecture argument. The model should not be writing queries against your production schema.

What production readiness looks like

A production MCP server needs the same discipline as the rest of your platform. It should have authentication at the transport boundary. It should enforce authorisation inside the application layer. It should log tool calls in a way support teams can understand. It should reject invalid inputs before they reach domain code. It should use narrow responses by default. It also needs versioning. Changing a tool name, parameter description, or response shape can change agent behaviour. That is easy to underestimate. A human developer sees a small schema change. An agent may see a completely different tool affordance. I would version tool contracts the same way I would version public integration contracts. Keep old tools around when you need compatibility. Add new tools when behaviour changes materially. Deprecate deliberately. Watch the telemetry before removing anything.

Tool descriptions deserve review too. They guide how the model chooses tools. A vague description can cause the agent to call a tool in the wrong context. A too-broad description can make one tool look suitable for everything. That doesnt mean descriptions should become essays. Keep them short, specific, and honest about what the tool does.

Testing an MCP server

You can unit test the application service behind the tool. That part should be normal .NET testing. The MCP layer needs a different kind of test. You want to know whether the server exposes the tools you expect, whether the schemas are stable, and whether invalid arguments fail safely. For tool behaviour, test through the public method and through the MCP client path if possible.

For an agent using the server, do not expect deterministic behaviour from the model. Use scenario tests with expected outcomes. Check that the agent chooses acceptable tools. Check that it refuses unsafe requests. Check that write-shaped operations require confirmation or create pending requests rather than mutating state directly.

There is still a place for manual testing in an IDE like Visual Studio or VS Code, especially for local developer tools. But manual testing should not be the only thing protecting a remote MCP server.

The part that still feels like magic

MCP can make an agent feel much more capable because the agent can suddenly do things. That is exactly why engineers need to be suspicious of the magic. When a model only generates text, the failure mode is usually a bad answer. When a model can call tools, the failure mode can be a bad action. That is a different class of problem. Good MCP design is mostly about reducing the damage a confused agent can do. Narrow tools help. Application services help. Read only first helps. Human confirmation helps. Audit logs help. None of these are new ideas. MCP just gives us a new place where we need to apply them.

MCP is worth paying attention to if you are building AI features in .NET. It gives you a standard way to expose capabilities to agents, IDEs, assistants, and other AI hosts. That is useful because the alternative is a pile of custom tool integrations that all behave slightly differently. But the protocol is the easy part. The hard part is deciding what the agent should be allowed to do. That decision belongs in your architecture, not in a prompt. Build the MCP server as a deliberate application boundary. Keep the tool surface small. Use your existing application services. Treat model output as untrusted input. Log the boring details.

That is how MCP becomes useful rather than just another layer of magic.