Establishing MCP Communication
Build an MCP Server in Node.js and connect it to VS Code Copilot
Project Setup
Step 1 - Create your project folder
mkdir devcon-workshop
cd devcon-workshopStep 2 - Initialise the project
npm init -yStep 3 - Enable ES Modules
Open package.json and set "type": "module":
{
"name": "devcon-workshop",
"version": "1.0.0",
"type": "module"
}Why? The MCP SDK uses ES module syntax (
import/export). Without this flag Node.js will reject it.
Already have a
"type"field? If yourpackage.jsonalready contains"type": "commonjs"or any other value, replace that line with"type": "module", don't add a second"type"entry.
Step 4 - Install dependencies
npm install @modelcontextprotocol/sdk@latest zod@latest| Package | Link | Purpose |
|---|---|---|
@modelcontextprotocol/sdk | @modelcontextprotocol/sdk | Anthropic's official MCP SDK - handles the full protocol |
zod | zod | Schema validation for tool inputs |
⚠️ 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.jsonBuild 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.
Create a new file called server.js in your project folder.
devcon-workshop/
├── node_modules/
├── package.json
├── package-lock.json
└── server.js ← newThe Imports - 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";| 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. |
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;
}McpServertakes anameandversion- these are advertised to any client that connects during the handshake.- 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.
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 usez.enum()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] }],
};
},
);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
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`);
});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.
View complete server.js 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:
devcon-workshop/
├── .vscode/
│ └── mcp.json ← new
├── node_modules/
├── package.json
├── package-lock.json
└── server.js{
"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.jsExpected 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 to server.js 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 Node.js server (
Ctrl+Cthennode server.js) and the MCP connection in VS Code. You 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) →
Quick Reference
| Command / Action | What it does |
|---|---|
node server.js | 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 |
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.