- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Connecting MCP Tools in C# MCP Server
Your platform team wants engineers to ask an AI agent 'what's blocking the payments release?' and get a real answer pulled from live systems. You are building DevPulse, the MCP server that makes that possible. It must run two ways: as a STDIO process a developer's local agent can spawn directly, and as an ASP.NET Core HTTP service that a centrally hosted agent can call. It exposes focused tools, searching tickets in a database and checking build status from an internal API, and logs every call for tracing. Because tickets and builds contain data scoped to individual engineers, the HTTP transport must authenticate callers with bearer tokens and hide or block admin-only tools from ordinary viewers. An agent client you build must forward each user's token on every tool call so downstream data stays scoped correctly, and the server must also expose a prompt an agent can fetch alongside its tools. Finally, DevPulse needs to advertise who published it and which version is running, the same metadata a developer would check in VS Code's MCP server list or a NuGet package page before trusting it.
Lab Info
Table of Contents
-
Challenge
Step 1: Orientation to the DevPulse MCP server
Your platform team wants an AI coding agent to answer "what's blocking the payments release?" by actually querying live systems, not guessing from a stale wiki page. That means giving the agent tools it can call: search open tickets, check a build's status, and pull a short summary prompt. The Model Context Protocol (MCP) is the wire format that lets any agent -- whether it's a CLI a developer spawns locally or a hosted assistant a whole team shares -- discover and invoke those tools the same way, regardless of which language or framework wrote them. Over this lab you'll build DevPulse, the MCP server that plays that role for your team's ticket and build data. Every MCP request, whatever transport carries it, boils down to the same shape: a method name (
initialize,tools/list,tools/call,prompts/get) plus a bag of parameters. DevPulse centers on a singleMcpDispatcherthat takes that shape and routes it to a registered handler -- a tool method, a prompt method, or the built-in handshake. Because the dispatcher doesn't know or care whether the request arrived over a STDIO pipe or an HTTP POST, you write the routing logic exactly once and reuse it behind both transports. That reuse is the architectural decision the rest of the lab leans on: transports translate bytes into requests, the dispatcher decides what happens next. Tickets and build data are scoped to individual engineers, so DevPulse can't just accept any HTTP call that shows up. You'll issue and validate OAuth-style bearer tokens, filter which tools a caller's role is even allowed to see intools/list, and enforce that same rule if someone tries to invoke a hidden tool directly by name. Every tool call, successful or not, also gets recorded by a trace recorder so a platform engineer debugging a bad answer can see exactly which method ran, with what parameters, and how long it took -- observability that STDIO's local, single-user model doesn't force on you but a shared HTTP service does. The steps ahead mirror how you'd actually build this: Step 2 stands up the dispatcher and gives it both a STDIO loop and an ASP.NET Core HTTP endpoint. Step 3 adds the two real tools -- ticket search backed by EF Core, build status backed by a simulated internal API -- and wraps every call in tracing. Step 4 locks the HTTP transport behind bearer tokens and makes tool discovery role-aware. Step 5 builds an agent client that forwards each user's token downstream, adds a summarization prompt alongside the tools, and publishes the server metadata a developer would check before trusting DevPulse in their editor. By the end, DevPulse is a server you could actually hand to a team -- and in Step 6 you'll hand it to a real AI model and watch it answer the payments question with the tools you built.If you get stuck, you can refer to the provided solution code for each task, available in the
solutionfolder. -
Challenge
Step 2: Stand up STDIO and HTTP transports for the MCP dispatcher
MCP's handshake starts with an
initializecall: before an agent invokes any tool it asks the server what it supports, and the server answers with a protocol version and a capabilities summary. Every other call -- a tool invocation, a prompt fetch -- names a method and carries parameters, and the server is expected to route it to whatever handles that method.McpDispatcheris where DevPulse makes that routing decision once, so both transports you're about to build can call the exact same method and get the exact same behavior. With routing in place, DevPulse needs its first front door. A developer running an agent locally spawns your server as a child process and talks to it over its standard input and output -- no sockets, no ports, just newline-delimited JSON flowing in and out. That's the STDIO transport MCP defines for exactly this case: one process, one trusted caller, minimal setup.StdioTransportwrapsMcpDispatcherin a loop that reads a request per line and writes a response per line. A centrally hosted agent can't spawn a process on someone else's machine, so it needs the same dispatcher reachable over HTTP instead. ASP.NET Core lets you handle that without a full MVC controller: a single handler reads a JSON body offHttpContext, hands it to the dispatcher, and writes back whatever the dispatcher returns -- the same result shape, whether it came from a tool call or theinitializehandshake. Getting both transports to answerinitializeidentically now is what lets you add tools once in Step 3 and have them show up correctly no matter which door an agent walked through. -
Challenge
Step 3: Design focused tools and trace their protocol traffic
An MCP tool should do one thing an agent can reason about: "search tickets," not "do anything with the ticket system." DevPulse's first tool,
SearchTickets, reads from aTicketsDb-- an EF CoreDbContextbacked by the in-memory provider, seeded with a handful of tickets so you can query it exactly like a real SQL-backed context without standing up a database. Keeping the tool method itself thin -- query, project, return -- is what makes it easy to trace and secure later; the tool doesn't know anything about HTTP, STDIO, or auth, it just answers one question. Build status doesn't live in your ticket database -- it comes from an internal CI system reachable over its own API.BuildApiClientsimulates that call so the lab doesn't depend on a real network, but the shape is the one you'd write against a live service: an async call that can return null for an unknown project.GetBuildStatusis the second tool, and giving it its own client dependency instead of reusingTicketsDbkeeps the two tools independently testable and independently securable. Two tools now exist, and a hosted server means calls arrive from callers you can't watch over someone's shoulder. Wrapping every tool invocation with a trace recorder -- capturing the method name, its parameters, and how long it took -- turns "the agent got a weird answer" into something you can actually debug. Exposing that log through its own tool means an agent, or a developer testing it, can ask "what just happened here" without reaching for a separate log file. -
Challenge
Step 4: Protect MCP endpoints with OAuth and role-aware discovery
Ticket and build data is scoped per engineer, so the moment DevPulse's HTTP transport is reachable by more than one person, it needs to know who's calling. Bearer tokens are the standard answer: a caller presents a token in an
Authorization: Bearer <token>header, and the server -- without any external identity provider in this lab -- issues and validates those tokens itself through an in-processTokenService. Validating a token means turning it back into aClaimsPrincipalcarrying the caller's user id and role, the same shape ASP.NET Core authorization already expects. ATokenServicethat can validate tokens is only useful if something actually calls it on every request.AuthMiddlewareis that gate: it reads theAuthorizationheader off the incomingHttpContext, validates it, and either attaches the resulting principal to the context or short-circuits the request with a 401 before the dispatcher ever sees it. This is the point where an unauthenticated HTTP call to DevPulse stops being possible -- STDIO stays trusted-by-default since it's one local process, but HTTP now checks every time. Authentication answers "who is this," authorization answers "what are they allowed to do" -- and for DevPulse that means an ordinary viewer shouldn't even see admin-only tools intools/list, let alone invoke one by guessing its name.ToolRegistryholds the metadata that says which role a tool requires, and a single rule --IsToolAllowedForRole-- needs to back both the filtered discovery list and the enforcement check ontools/call, so there's exactly one place that decision is made and exactly one place to get it right. -
Challenge
Step 5: Connect a secured agent and package the server for distribution
Tools aren't the only thing MCP servers expose -- a prompt is a piece of reusable instructional text a server hands an agent, the same way a tool hands it data. DevPulse's
SummarizePendingTicketsprompt takes the ticketsSearchTicketsalready knows how to find and turns them into a message an agent's model can read directly, so "summarize what's pending" doesn't require the agent to invent its own phrasing every time. Every tool you've built so far assumes it's already running inside DevPulse with access toTicketsDbandBuildApiClientdirectly. An agent that calls DevPulse over HTTP is a different caller entirely, and it needs to carry the human user's identity with it -- not DevPulse's own service credentials -- so that a viewer using the agent still only sees what a viewer is allowed to see.AgentClientis that caller: it holds the user's bearer token and must attach it to every tool and prompt request it sends, the same headerAuthMiddlewareis already checking for on the way in. Before a developer lets any MCP server touch their editor, they check who published it and what version is running -- the same information VS Code's MCP panel or a NuGet package page surfaces. DevPulse advertises that through theinitializehandshake's server info: a name, a version, a publisher, and a live count of what it currently offers, so trusting DevPulse doesn't require reading its source first. -
Challenge
Step 6: Put an AI agent in front of DevPulse
Every task is done and graded. Now you'll put DevPulse in front of a real AI model and watch it answer the question this lab started with -- "what's blocking the payments release?" -- using your transports, your auth layer, your tools, and your agent client, all at once.
The agent is already written for you in
src/DevPulseAgent.cs, and it's short enough to read in a minute. It asks DevPulse which tools the signed-in user may use (tools/list), offers exactly those tools to the model, and lets the model decide which to call. Every call the model makes goes back to DevPulse astools/callthrough yourAgentClient, carrying that user's bearer token -- so the model can never do more than the user it's working for.Add your API key
The model is reached through the lab's AI service. Your API key can be found in the top center of your screen.
- Open the
.envfile at the root of the workspace. - Paste the key after
LAB_API_KEY=.
Then confirm everything is connected. In the Terminal tab, run:
dotnet run --project src/Lab.csproj -- --agent --checkYou should see three green check marks: the key was found, DevPulse answered
tools/list, and the AI service replied. A yellow line says what to fix -- if DevPulse itself isn't answering, it names the task to revisit. ### Ask the question DevPulse was built forStart the agent:
dotnet run --project src/Lab.csproj -- --agentThis starts DevPulse's HTTP transport inside the same process and signs you in as
demo-user, who has the viewer role. At theask>prompt, type:what's blocking the payments release?The colored lines printed before the answer are the whole lab working together:
→ model calls ...is the model choosing one of your tools and its arguments, based only on the names, descriptions, and input schemastools/listreturned.[DevPulse] request from demo-user (role: viewer)is your HTTP transport accepting the bearer tokenAgentClientforwarded --ValidateTokenandAuthorizeRequestat work.← ...is whatSearchTicketsandGetBuildStatussent back to the model.
The answer that follows should mention the certificate ticket and the failed payments build. The model's wording changes from run to run; the tool calls are what to watch. ### Follow the trace and the prompt
Type
/traceto print DevPulse's trace log: everytools/list,tools/call, andprompts/getthe agent made, with its parameters and timing, recorded by yourRecordInvocation. This is what a platform engineer reads when an agent gives a strange answer.Type
/promptto fetch yoursummarize_pending_ticketsprompt withprompts/get. The agent prints the prompt's text and sends it to the model as your message, so you can see yourGetPromptoutput steering the model. ### See role-aware discovery from the model's sideStill signed in as the viewer, ask:
please clear the trace logThe model can't do it, and it should say so:
tools/listnever showed itpurge_trace_log, becauseIsToolAllowedForRolehides admin-only tools from viewers. Even a client that guessed the tool's name would be refused, becausetools/callruns the same check.Now switch to the admin user and ask again:
/as admin please clear the trace logThis time DevPulse offers three tools, and the model calls
purge_trace_logwith the admin's token. Type/traceonce more to see the log start over. - Open the
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.