Consuming MCP Servers from .NET: When Your App Becomes the Client A developer demonstrated how to consume MCP servers from a .NET application, turning the app into a client. The post shows connecting to the Aurora Coffee Co. server via the ModelContextProtocol SDK, listing tools, and invoking them, reducing a hand-rolled agent loop to three lines. It emphasizes tool discovery and the security boundary where the server owns execution. Last time we built an MCP server in C https://dev.to/jgomezdev/build-an-mcp-server-in-c-write-your-tools-once-use-them-in-every-claude-4oa1 and plugged it into Claude Desktop and Claude Code. Both of those are somebody else's app. You wrote the tools, handed them over, and a client you didn't write got all the benefit. So here's the other half of the story: what happens when your .NET app is the thing that needs those tools? Not Claude Desktop — your internal support console, your worker service, your CLI. This post writes the client side, connects it to the same Aurora Coffee Co. server from last time, and ends somewhere satisfying: the hand-rolled agent loop from the tool-use post https://dev.to/jgomezdev/build-a-claude-tool-use-agent-in-c-not-a-chatbot-on-steroids-3e9g — the one we wrote by hand in about sixty lines — collapses into three. A server post talks about exposing capabilities. A client post is the mirror image, and it's just three verbs: That third step is worth pausing on, because it's the same security boundary from the tool-use post viewed from the opposite side. Back then, your code owned execution and Claude only got to ask. Now you're the one asking, and the server owns execution. You send a name and arguments; what actually runs is entirely the server's business. The discovery step is what makes this different from just calling an API client. You don't have a generated proxy class with GetOrderStatus string on it. You have a list of tools that arrived at runtime, and code that has to be okay with that. Start a console app and add the SDK. The client lives in ModelContextProtocol.Core if you want minimal dependencies, or the full ModelContextProtocol package if this same app also hosts a server: dotnet new console -o AuroraCoffee.Support cd AuroraCoffee.Support dotnet add package ModelContextProtocol.Core For a local server, the stdio transport launches it as a child process — exactly what Claude Desktop was doing for you via that JSON config last time, except now you're the one spawning it: js using ModelContextProtocol.Client; var transport = new StdioClientTransport new StdioClientTransportOptions { Name = "aurora-coffee", Command = "dotnet", Arguments = "run", "--project", "../AuroraCoffee.Mcp" , } ; await using var client = await McpClient.CreateAsync transport ; That's the whole connection. McpClient.CreateAsync starts the process, performs the protocol handshake, and hands you a live client. Note the await using — the client owns a child process, and letting it go undisposed leaves an orphaned dotnet running. Ask me how many stray processes it took to internalize that. Now the part people skip and then debug for an hour. Print the tool list before you write a single CallToolAsync: js foreach var tool in await client.ListToolsAsync { Console.WriteLine $"{tool.Name} — {tool.Description}" ; } The reason isn't ceremony. Tool names are derived from your server's method names by the SDK, so the exact string the protocol exposes may not be the C identifier you typed. Discovery is the contract; the C source on the server side is an implementation detail. Print the list, copy the real names, then write your calls against those. Guessing the name gets you a runtime error with no compiler to save you. With a real name in hand, invoking it is a name plus a dictionary of arguments: js using ModelContextProtocol.Protocol; var result = await client.CallToolAsync "get order status", new Dictionary