Establishing MCP Communication
Build an MCP Server in Node.js or .NET and connect it to VS Code Copilot
This workshop is available in two languages. Use the tabs on each code block to switch between the Node.js and .NET implementation. The MCP protocol, the VS Code client configuration, and the workflow are identical — only the SDK and language change.
Project Setup
Step 1 - Create your project folder
mkdir devcon-workshop
cd devcon-workshopStep 2 - Initialise the project
npm init -ydotnet new web -n DevconWorkshop -o .Step 3 - Configure the project
{
"name": "devcon-workshop",
"version": "1.0.0",
"type": "module"
}<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.3.0" />
</ItemGroup>
</Project>Node.js — Why
"type": "module"? The MCP SDK uses ES module syntax (import/export). Without this flag Node.js will reject it. If yourpackage.jsonalready contains"type": "commonjs"or any other value, replace that line, don't add a second"type"entry.
.NET — Why
Microsoft.NET.Sdk.Web? The MCP server runs on top of ASP.NET Core's HTTP pipeline. The Web SDK gives usWebApplication.CreateBuilderand the routing infrastructure needed to expose the/mcpendpoint.
Step 4 - Install dependencies
npm install @modelcontextprotocol/sdk@latest zod@latestdotnet add package ModelContextProtocol.AspNetCore --version 1.3.0| Package | Link | Purpose |
|---|---|---|
@modelcontextprotocol/sdk | @modelcontextprotocol/sdk | Anthropic's official MCP SDK - handles the full protocol |
zod | zod | Schema validation for tool inputs |
ModelContextProtocol.AspNetCore | ModelContextProtocol.AspNetCore | Official MCP SDK for ASP.NET Core - protocol + transport |
⚠️ Node.js — Zod compatibility: If you encounter errors with
import { z } from "zod", your environment may needimport { z } from "zod/v4"instead. This can happen when Zod 4+ is installed. Check the Zod docs for migration guidance.
Your project folder should now look like this:
devcon-workshop/
├── node_modules/
├── package.json
└── package-lock.jsondevcon-workshop/
├── Properties/
│ └── launchSettings.json
├── appsettings.Development.json
├── appsettings.json
├── DevconWorkshop.csproj
└── Program.csBuild the MCP Server
We start by creating the server. The server is the core of an MCP system. It is responsible for exposing the tools that clients can call, receiving incoming requests, executing the right tool handler, and sending the response back. Everything flows through it.
In Node.js, create a new file called server.js. In .NET, rename the scaffolded Program.cs to Server.cs (or delete it and create a fresh Server.cs).
devcon-workshop/
├── node_modules/
├── package.json
├── package-lock.json
└── server.js ← newdevcon-workshop/
├── Properties/
│ └── launchSettings.json
├── appsettings.Development.json
├── appsettings.json
├── DevconWorkshop.csproj
└── Server.cs ← newThe Imports / Usings - What Each One Does
Why Streamable HTTP? MCP supports three transports: stdio (subprocess-based, used by Claude Desktop), SSE (Server-Sent Events, legacy), and Streamable HTTP (the current standard). We use Streamable HTTP because it works over the network, supports streaming responses, and is the recommended transport for HTTP-first architectures.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
import http from "node:http";using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;Node.js — what each import does
| Import | What it does |
|---|---|
McpServer | Creates and manages your MCP server instance. Handles capability negotiation and tool routing automatically. |
StreamableHTTPServerTransport | Wires the MCP server to HTTP. Manages sessions, handles POST requests from clients, and supports Server-Sent Events (SSE) for streaming responses back. |
z (Zod) | Defines and validates the input schema for each tool. Ensures the client sends the right types before your tool function runs. |
http | Node.js built-in HTTP module. Creates the server that listens for incoming requests and routes them to the MCP transport. |
.NET — what each using does
| Using | What it does |
|---|---|
ModelContextProtocol.Server | Brings in AddMcpServer, WithHttpTransport, [McpServerTool], and [McpServerToolType] — the full MCP SDK surface. |
System.ComponentModel | Provides [Description] so each tool and parameter can carry the metadata LLMs use to pick the right tool. |
System.Text.Json + Serialization | Used to register JsonStringEnumConverter so enums are serialised as snake_case strings on the wire, not integers. |
Section 1 - Create the MCP Server
function createServer() {
const server = new McpServer({
name: "devcon-workshop-server",
version: "1.0.0",
});
// ... register tools here ...
return server;
}var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<JsonSerializerOptions>(o =>
o.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower)));
builder.Services
.AddMcpServer(o =>
{
o.ServerInfo = new() { Name = "devcon-workshop-server", Version = "1.0.0" };
})
.WithHttpTransport(options => options.Stateless = true)
.WithTools<MathTools>()
.WithTools<GreetingTools>();- The server takes a
nameandversion— these are advertised to any client that connects during the handshake. - Node.js: the server is wrapped in a factory function so that each incoming HTTP request gets its own fresh instance. The MCP SDK only allows one active transport per server instance, so without this the server would crash on the second request.
- .NET:
WithHttpTransport(options => options.Stateless = true)plays the same role — each request gets its own session, no shared transport state. - .NET:
JsonStringEnumConverterwithSnakeCaseLowermeans an enum value likeLanguage.Englishis sent as"english"on the wire, matching the schema clients expect.
Section 2 - Register Tools
We'll register two tools to demonstrate the pattern:
add: takes two numbers (aandb) and returns their sum. A minimal example that shows the basic structure without any business logic in the way.greet: takes anameand alanguage(english,french, orspanish) and returns a personalised greeting in the chosen language. Demonstrates how to constrain a string input to a fixed set of allowed values.
// Tool 1: add two numbers
server.registerTool(
"add",
{
description: "Adds two numbers together",
inputSchema: {
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
},
},
async ({ a, b }) => ({
content: [{ type: "text", text: `Result: ${a + b}` }],
}),
);
// Tool 2: greet someone
server.registerTool(
"greet",
{
description: "Returns a greeting in the chosen language",
inputSchema: {
name: z.string().describe("Name of the person to greet"),
language: z.enum(["english", "french", "spanish"]).describe("Language"),
},
},
async ({ name, language }) => {
const greetings = {
english: `Hello, ${name}! Welcome to the DevCon MCP Workshop.`,
french: `Bonjour, ${name} ! Bienvenue au Workshop MCP DevCon.`,
spanish: `¡Hola, ${name}! Bienvenido al Workshop MCP de DevCon.`,
};
return {
content: [{ type: "text", text: greetings[language] }],
};
},
);// ---------------------------------------------------------------------------
// Tool 1: add
// ---------------------------------------------------------------------------
public class MathTools
{
[McpServerTool(Name = "add"), Description("Adds two numbers together")]
public static string Add(
[Description("First number")] double a,
[Description("Second number")] double b)
=> $"Result: {a + b}";
}
public enum Language
{
English,
French,
Spanish,
}
// ---------------------------------------------------------------------------
// Tool 2: greet
// ---------------------------------------------------------------------------
[McpServerToolType]
public class GreetingTools
{
[McpServerTool(Name = "greet"), Description("Returns a greeting in the chosen language")]
public static string Greet(
[Description("Name of the person to greet")] string name,
[Description("Language")] Language language)
=> language switch
{
Language.English => $"Hello, {name}! Welcome to the DevCon MCP Workshop.",
Language.French => $"Bonjour, {name} ! Bienvenue au Workshop MCP DevCon.",
Language.Spanish => $"¡Hola, {name}! Bienvenido al Workshop MCP de DevCon.",
_ => throw new ArgumentOutOfRangeException(nameof(language)),
};
}Node.js — server.registerTool() takes three arguments:
- Name - the identifier clients use to call this tool
- Config object - contains
description(used by LLMs to decide which tool to call) andinputSchema(Zod object defining and validating each input) - Handler - the async function that runs when the tool is called; must return a
contentarray
.NET — attribute-driven registration:
[McpServerToolType]marks a class as containing MCP tools.[McpServerTool(Name = "...")]on a static method registers it as a tool with that name.[Description("...")]on the method and each parameter provides the metadata LLMs use to choose and call the tool — equivalent to Node.js'sdescriptionand.describe().- Enums like
Languageare how .NET constrains a string to a fixed set of values — the SDK turns them into a JSON schemaenumautomatically.
Section 3 - Create the HTTP Server and Start Listening
const PORT = 3000;
const httpServer = http.createServer(async (req, res) => {
if (req.url !== "/mcp") {
res.writeHead(404).end("Not found");
return;
}
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
res.on("close", () => transport.close());
const server = createServer();
await server.connect(transport);
await transport.handleRequest(req, res);
});
httpServer.listen(PORT, () => {
console.log(`MCP server running at http://localhost:${PORT}/mcp`);
});var app = builder.Build();
app.Urls.Add("http://localhost:3000");
app.MapMcp("/mcp");
Console.WriteLine("MCP server running at http://localhost:3000/mcp");
app.Run();Node.js
sessionIdGenerator: undefined- sets the server to stateless mode. Each request is self-contained.- A fresh
StreamableHTTPServerTransportis created per request. res.on("close", () => transport.close())ensures the transport is cleaned up when the HTTP connection closes.transport.handleRequest(req, res)does all MCP message parsing, routing, and response writing, no manual body parsing needed.
.NET
app.MapMcp("/mcp")wires the MCP transport onto the ASP.NET Core routing pipeline at the/mcppath. The SDK handles message parsing, tool dispatch, and SSE streaming for you.app.Urls.Add("http://localhost:3000")binds the server to port 3000 — matching the port the VS Code client expects.Stateless = true(set in Section 1) is the equivalent of Node'ssessionIdGenerator: undefined.
View complete server.js in Source Code → · View complete Server.cs in Source Code →
Connect VS Code as Your MCP Client
VS Code with GitHub Copilot can act as an MCP client. It connects to your running server, discovers its tools, and lets you call them through natural language. All you need to do is tell VS Code where to find the server.
Terminology: In MCP, the client (VS Code, Cursor, Claude Desktop) connects to an MCP server and calls its tools. Some documentation uses "host" to refer to the application that embeds the client. In practice the terms are used interchangeably.
Create a .vscode folder in your project and add mcp.json. This file is identical for both Node.js and .NET — the client only cares about the URL, not what's behind it.
devcon-workshop/
├── .vscode/
│ └── mcp.json ← new
├── node_modules/
├── package.json
├── package-lock.json
└── server.jsdevcon-workshop/
├── .vscode/
│ └── mcp.json ← new
├── Properties/
│ └── launchSettings.json
├── appsettings.Development.json
├── appsettings.json
├── DevconWorkshop.csproj
└── Server.cs{
"servers": {
"devcon-workshop": {
"type": "http",
"url": "http://localhost:3000/mcp"
}
}
}| Field | What it does |
|---|---|
servers | A map of named MCP servers VS Code should know about |
devcon-workshop | The display name shown in VS Code's MCP server list |
type: http | Tells VS Code to connect over HTTP |
url | The endpoint VS Code sends MCP requests to, must match where your server is listening |
Run It
Step 1 - Start the server
node server.jsdotnet runExpected output:
MCP server running at http://localhost:3000/mcpStep 2 - Connect VS Code
Open the Command Palette (Ctrl+Shift+P on Windows/Linux, Cmd+Shift+P on Mac, or View → Command Palette) and run MCP: List Servers. You should see devcon-workshop listed with a Start button. Click it, VS Code will connect to your running server.
Step 3 - Verify tool calls through Copilot Chat
Open Copilot Chat (Ctrl+Alt+I on Windows/Linux, Cmd+Ctrl+I on Mac, or click the Copilot icon in the Activity Bar) and switch to Agent mode by clicking the mode selector next to the model name. MCP tools are only available in Agent mode, the other modes don't support external tool calls.
To reference your MCP tools explicitly, use the # prefix:
Use `#add` to add 12 and 30Use `#greet` to greet Nabil in SpanishYou will see a tool call indicator appear in the chat. Copilot shows which tool it invoked and the result before writing its final answer. That confirms your server is live and the MCP connection is working.
Challenge
Add a third tool called estimate_cost that takes:
material(enum:"concrete","steel","timber","glass") - the building materialvolume(number) - volume in cubic metres
And returns a cost estimate using these hardcoded rates:
| Material | Rate per m³ |
|---|---|
concrete | $150 |
steel | $950 |
timber | $400 |
glass | $1,200 |
Expected output for estimate_cost({ material: "concrete", volume: 25 }): Cost estimate: 25 m³ of concrete at $150/m³ = $3,750
Then ask VS Code Copilot:
Estimate the cost of 25 cubic metres of concrete.Tip: After adding or modifying tools, you need to restart both the server and the MCP connection in VS Code.
- Node.js:
Ctrl+Cthennode server.js- .NET:
Ctrl+Cthendotnet runYou can restart the connection by opening
.vscode/mcp.jsonand clicking the Restart button that appears inline above each server entry, or via the Command Palette: MCP: List Servers → Restart.
View complete solution server.js (adds estimate_cost) → · View complete solution Server.cs (adds estimate_cost) →
Quick Reference
| Command / Action | What it does |
|---|---|
node server.js (Node.js) | Starts the MCP server on port 3000 |
dotnet run (.NET) | Starts the MCP server on port 3000 |
| VS Code MCP: List Servers → Start | Connects VS Code Copilot to the running server |
| Copilot Chat → Agent mode | Calls tools via natural language |
server.registerTool(name, config, handler) | Registers a new tool on the server (Node.js) |
[McpServerTool] + .WithTools<T>() | Registers a new tool on the server (.NET) |
Looking for a higher-level API? This workshop uses the official MCP SDK directly so you understand every layer. If you want less boilerplate, check out FastMCP (Python) or FastMCP (TypeScript) — they wrap the same protocol in ~20-25 lines instead of ~70.