Build Your First MCP Server

Build a TypeScript MCP server and connect it to GitHub Copilot in VS Code.

Introduction

30 Second Summary

AI assistants are powerful, but they can only work with what they can see. The Model Context Protocol (MCP) is the open standard that lets you give an AI access to your own tools, data, and services.

In this project, you will build your own MCP server in TypeScript that gives an AI assistant three custom abilities: doing math calculations, managing a to-do list, and generating random numbers.

What You'll Build

A personal toolkit MCP server with three registered tools, connected to VS Code so GitHub Copilot can discover and call them in real-time.

By the end of this project, you'll have:

  • A working MCP server written in TypeScript with tools for calculations, to-do management, and random number generation.
  • A VS Code workspace configured to connect your server via mcp.json so GitHub Copilot can discover your tools automatically.
  • A live demo where you chat with Copilot in Agent mode and watch it call your tools to answer questions.
  • Secret Mission: Add a read-only resource that exposes your to-do list data to the AI without requiring a tool call.

Are there any prerequisites?

None. This guide walks you through installing everything you need, including Node.js, VS Code, and GitHub Copilot Free. All you need is a Windows computer and a free GitHub account.

Install Node.js and VS Code

To build your MCP server, you need two foundation tools on your machine: Node.js (a JavaScript runtime that executes your server code) and VS Code (a code editor with built-in AI features).

In this step, you'll install both tools and activate GitHub Copilot Free. Copilot is the AI assistant that will discover and call your MCP tools later in the project.

In this step, get ready to:
  • Install Node.js v24 LTS and confirm it works in your terminal.
  • Install VS Code and sign in with a GitHub account.
  • Activate GitHub Copilot Free and confirm Agent mode is available.
Install Node.js

Node.js is the runtime that lets you execute JavaScript and TypeScript code outside a browser. Your MCP server is a Node.js program, so you need it installed before you can build anything.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.
  • Check if Node.js is already installed by running this command:
node --version

✔️ I see version 24 or higher

Your Node.js is up to date. Skip ahead to the npm check below.

ⓧ I see an older version

You have Node.js installed, but your MCP server needs v24 or higher. The Windows installer will upgrade your existing version in place.

  • Open https://nodejs.org/en/download in your browser.
  • Download the Windows installer (.msi file). The page should show v24.18.0 LTS or newer.
  • Run the installer and accept all default options.
  • Close your current PowerShell window and open a new one (the old window still has the old version in its path).
  • Run node --version again. You should see v24 or higher.

ⓧ Command not found

Node.js is not installed yet. Let's fix that now.

  • Open https://nodejs.org/en/download in your browser.
  • Download the Windows installer (.msi file). The page should show v24.18.0 LTS or newer.
  • Run the installer and accept all default options.
  • Close your current PowerShell window and open a new one (so the system picks up the new installation path).
  • Run node --version again. You should now see v24 or higher.
  • Confirm npm is also available by running:
npm --version

You should see a version number printed. npm is Node.js's package manager. You'll use it to install your project's dependencies in the next step.

npm command not found?

If node --version works but npm --version does not, your installer may not have completed successfully. Re-run the .msi installer and make sure every checkbox stays at its default value.

Help me fix npm not being found

Install VS Code and sign in with GitHub

VS Code is the code editor you'll write your MCP server in. It also acts as the AI host that connects to your server later.

  • Open https://code.visualstudio.com in your browser.
  • Click the download button for Windows and run the installer.
  • Accept all default options during installation.

Already have VS Code installed?

If you already have VS Code, make sure it is updated to the latest version. Open VS Code, then go to Help and select Check for Updates. MCP support requires VS Code 1.102 or later.

  • Press the Windows key to open your search bar.
  • Type Visual Studio Code and press Enter to open it.

Now you need to sign in with a GitHub account to activate Copilot Free. If you do not have a GitHub account, create one for free at github.com before continuing.

  • In VS Code, hover over the Copilot icon in the bottom Status Bar.
  • Select Use AI Features.
  • When prompted, choose Sign in with GitHub.
  • A browser window opens asking you to authorize VS Code. Sign in to your GitHub account and approve the request.
  • Return to VS Code. If you do not already have a Copilot subscription, VS Code signs you up for the Copilot Free plan automatically.

What is GitHub Copilot Free?

Copilot Free gives you a monthly allowance of AI chat interactions and code suggestions at no cost. All you need is a GitHub account. No credit card required.

Don't see the Copilot icon?

Make sure VS Code is fully up to date. Go to Help then Check for Updates. Restart VS Code after updating.

Help me activate GitHub Copilot Free

Verify Agent mode is available

Agent mode is the Copilot feature that can discover and call MCP tools. You need to confirm it's available in your Chat view before moving on.

  • Press Ctrl+Shift+I to open the Copilot Chat view.
  • Look at the chat input area. You should see a mode selector dropdown that lets you switch between modes (such as Agent, Edit, or Ask).
  • Select Agent from the dropdown if it is not already selected.

You should see the Chat view open with Agent mode active. This is where your MCP tools will appear once your server is connected.

Don't see the Agent option?

Make sure agents are enabled in your VS Code settings. Open the Command Palette with Ctrl+Shift+P, type chat.agent.enabled, and confirm the setting is checked (true).

Help me enable Agent mode in Copilot

Your development environment is ready. Next, you'll create your MCP server project and write your first lines of TypeScript.

Set Up Your MCP Server Project

Your development environment is ready. Now it's time to create the actual project that will become your MCP server.

Every MCP server is a standalone program. In this step, you will create a project folder, install the packages your server needs, configure TypeScript, and write the server skeleton that all your tools will live inside.

In this step, get ready to:
  • Create a project folder and install the MCP SDK and Zod packages.
  • Configure TypeScript and ES modules for your project.
  • Write a basic MCP server skeleton and confirm it compiles.
Create your project folder and install dependencies

Your MCP server needs its own folder with a few key packages. The @modelcontextprotocol/server package provides the server framework, and Zod handles input validation for the tools you will build later.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (the backtick key, above Tab).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir my-mcp-server
cd my-mcp-server
  • Now open this folder in VS Code. Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the my-mcp-server folder, and click Select Folder.

Seeing a trust prompt?

VS Code may show a banner asking if you trust the authors of this folder. Since you just created it yourself, click the option to trust the folder. This allows extensions and the terminal to work normally.

  • VS Code reloads with your project folder open. Open a new integrated terminal by pressing Ctrl+`.
  • Initialize your project by running this command:
npm init -y

What does this command do?

The npm init -y command creates a package.json file that tracks your project's name, version, and dependencies. The -y flag accepts all the defaults so you skip the interactive prompts.

  • You should see a package.json printed in your terminal. Now install the MCP server package and Zod by running:
npm install @modelcontextprotocol/server zod
  • Install TypeScript and Node.js type definitions as dev dependencies by running:
npm install -D @types/node typescript
  • You should see output confirming packages were added. A node_modules folder and package-lock.json file now appear in the Explorer sidebar on the left.

Seeing an error during install?

Make sure your terminal is inside the my-mcp-server folder. Run pwd to check your current directory. If you are not in my-mcp-server, run cd ~/Desktop/my-mcp-server and try the install command again.

If you see a network error, check your internet connection and retry.

Help me fix my npm install error.

Configure TypeScript and ES modules

The MCP SDK uses modern JavaScript modules (ES modules) and is written for TypeScript. You need to tell your project to use both of these.

  • In the Explorer sidebar, click on package.json to open it.
  • Select all the text in the file and replace it with the following:
{
  "name": "my-mcp-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc"
  },
  "dependencies": {
    "@modelcontextprotocol/server": "^1.29.0",
    "zod": "^4.4.3"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "typescript": "^5.8.0"
  }
}

What changed here?

You added "type": "module" which tells Node.js to treat your files as ES modules (using import and export syntax).

You also added a "build" script that runs tsc (the TypeScript compiler). This lets you compile your code with a single npm run build command.

  • Save package.json by pressing Ctrl+S.

Next, TypeScript needs its own configuration file to know how to compile your code.

  • Create a new file by clicking the new file icon at the top of the Explorer sidebar (or press Ctrl+N, then Ctrl+S to save it).
  • Name the file tsconfig.json and save it in the root of your my-mcp-server folder.
  • Paste in the following configuration:
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

What does this configuration do?

The outDir setting tells TypeScript to put compiled JavaScript files into a build folder. The rootDir setting says your source code lives in src.

The module: "NodeNext" setting ensures TypeScript generates code compatible with Node.js ES modules. The include array tells TypeScript to compile everything inside the src folder.

  • Save tsconfig.json by pressing Ctrl+S.

File saved in the wrong place?

Both package.json and tsconfig.json should be at the top level of your my-mcp-server folder. Check the Explorer sidebar to confirm they are not nested inside another folder.

Help me check my project structure is correct.

Create the MCP server skeleton

With configuration in place, you can write the actual server code. This skeleton creates a running MCP server that listens for tool calls over stdio (standard input/output).

  • Create a src folder inside your project. Right-click in the Explorer sidebar and select New Folder.
  • Name the folder src.
  • Right-click the src folder and select New File.
  • Name the file index.ts.
  • Paste the following code into src/index.ts:
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";

// Create server instance
const server = new McpServer({
  name: "my-toolkit",
  version: "1.0.0",
});

// Start the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((error) => {
  console.error("Server error:", error);
  process.exit(1);
});

What does this code do?

The first three lines import the packages you installed. McpServer is the core class that manages tool registration and message handling. StdioServerTransport lets the server communicate over standard input/output, which is how VS Code will talk to it.

The main() function creates a transport, connects the server to it, and starts listening. The .catch() at the end handles any startup errors gracefully.

  • Save src/index.ts by pressing Ctrl+S.
  • Build your project to confirm everything compiles by running this command in the terminal:
npm run build
  • The command should complete with no errors. You should see a new build folder appear in the Explorer sidebar containing index.js.

Seeing a compile error?

If you see an error about missing modules, make sure the import paths in src/index.ts match exactly what is shown above. The path @modelcontextprotocol/server/stdio must include the /stdio at the end.

If you see an error about tsconfig.json, double-check that the file is in the root of your project (next to package.json) and not inside the src folder.

Help me fix my TypeScript build error.

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files before moving on.

ⓧ I'd like to double check the full code

Here are all three files as they should look at this point:

{
  "name": "my-mcp-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc"
  },
  "dependencies": {
    "@modelcontextprotocol/server": "^1.29.0",
    "zod": "^4.4.3"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "typescript": "^5.8.0"
  }
}
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";

// Create server instance
const server = new McpServer({
  name: "my-toolkit",
  version: "1.0.0",
});

// Start the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((error) => {
  console.error("Server error:", error);
  process.exit(1);
});

Your MCP server compiles and is ready to accept tools. Next up, you will register your first tool and teach the server to do math.

Build Your First Tool (Calculator)

Your server skeleton is running, but right now it does nothing useful. An MCP server's value comes from the tools it exposes. Tools are functions that an AI assistant can discover and call on demand.

In this step, you will register two tools on your server: a calculator that performs basic math and a random number generator. Each tool declares its own input schema so the AI knows exactly what arguments to send.

In this step, get ready to:
  • Register a calculator tool with a validated input schema.
  • Add a random number generator tool using the same pattern.
  • Build the project to confirm everything compiles.
Register the calculate tool

Every MCP tool has three parts: a name, a metadata object describing what inputs it accepts, and a callback function that runs when the AI calls it. The input schema uses Zod, a validation library that lets you describe the shape of data your tool expects.

  • In src/index.ts, find the comment that says // Start the server.
  • Add the following code directly above that comment:
// Tool 1: Calculator
server.registerTool(
  "calculate",
  {
    title: "Calculator",
    description: "Perform a math calculation with two numbers",
    inputSchema: z.object({
      operation: z.enum(["add", "subtract", "multiply", "divide"]).describe("The math operation to perform"),
      a: z.number().describe("First number"),
      b: z.number().describe("Second number"),
    }),
  },
  async ({ operation, a, b }) => {
    let result: number;
    switch (operation) {
      case "add":
        result = a + b;
        break;
      case "subtract":
        result = a - b;
        break;
      case "multiply":
        result = a * b;
        break;
      case "divide":
        if (b === 0) {
          return {
            content: [{ type: "text" as const, text: "Error: Cannot divide by zero" }],
          };
        }
        result = a / b;
        break;
    }
    return {
      content: [{ type: "text" as const, text: `${a} ${operation} ${b} = ${result}` }],
    };
  }
);

What does this code do?

The first argument "calculate" is the tool's unique name. The AI uses this name to identify and call the tool.

The inputSchema uses Zod to define exactly what the AI must provide: an operation (one of four choices) and two numbers. The SDK converts this into JSON Schema automatically so the AI knows the rules.

The callback receives the validated inputs, performs the math, and returns the result wrapped in a content array. This is the standard format MCP uses to send text responses back to the AI.

  • Save src/index.ts.
  • You won't see a visible change yet. The next substep adds a second tool, and then you will build the project to confirm both tools compile.

Seeing a red underline in VS Code?

If VS Code shows a squiggly red underline under z.enum or z.object, make sure the import at the top of your file reads import * as z from "zod/v4" (not just "zod").

Also confirm the code is placed above the // Start the server comment, not inside the main() function.

Help me fix a red underline error in my MCP server code

Add the random-number tool

The second tool follows the same three-part pattern: a name, an input schema, and a callback. This time the schema is simpler because the tool only needs two numbers.

  • In src/index.ts, add the following code directly below the closing ); of the calculate tool (still above the // Start the server comment):
// Tool 2: Random Number Generator
server.registerTool(
  "random-number",
  {
    title: "Random Number Generator",
    description: "Generate a random integer between min and max (inclusive)",
    inputSchema: z.object({
      min: z.number().describe("Minimum value (inclusive)"),
      max: z.number().describe("Maximum value (inclusive)"),
    }),
  },
  async ({ min, max }) => {
    const result = Math.floor(Math.random() * (max - min + 1)) + min;
    return {
      content: [{ type: "text" as const, text: `Random number between ${min} and ${max}: ${result}` }],
    };
  }
);

What does this code do?

This tool accepts a min and max value, then returns a random integer within that range. The .describe() calls on each field tell the AI what each parameter means, helping it fill in the right values when a user asks for a random number.

  • Save src/index.ts.
  • You won't see a change yet. The next substep builds the project and confirms both tools compile correctly.

Red underline on the callback parameters?

Make sure you closed the calculate tool's server.registerTool() call with ); before starting the random-number tool. A missing closing parenthesis or semicolon throws off everything below it.

Help me fix a TypeScript error in my random-number tool

Build the project to verify
  • Compile your TypeScript into JavaScript by running this command in the integrated terminal:
npm run build
  • You should see the command complete with no errors. If TypeScript found problems, it prints them in the terminal.

Seeing TypeScript errors?

Common issues include a missing comma between properties inside the inputSchema object, or a mismatched bracket. Count your opening and closing braces carefully in the calculate tool's switch statement.

If you see an error mentioning as const, make sure the string "text" as const is written exactly as shown. This tells TypeScript to treat the value as a literal type.

Help me fix my TypeScript build errors

✔️ Awesome, I've got everything!

Great. Double check you have saved src/index.ts and that your build completed without errors.

ⓧ I'd like to double check the full code

Here is the complete src/index.ts file as it should look at the end of this step:

import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";

// Create server instance
const server = new McpServer({
  name: "my-toolkit",
  version: "1.0.0",
});

// Tool 1: Calculator
server.registerTool(
  "calculate",
  {
    title: "Calculator",
    description: "Perform a math calculation with two numbers",
    inputSchema: z.object({
      operation: z.enum(["add", "subtract", "multiply", "divide"]).describe("The math operation to perform"),
      a: z.number().describe("First number"),
      b: z.number().describe("Second number"),
    }),
  },
  async ({ operation, a, b }) => {
    let result: number;
    switch (operation) {
      case "add":
        result = a + b;
        break;
      case "subtract":
        result = a - b;
        break;
      case "multiply":
        result = a * b;
        break;
      case "divide":
        if (b === 0) {
          return {
            content: [{ type: "text" as const, text: "Error: Cannot divide by zero" }],
          };
        }
        result = a / b;
        break;
    }
    return {
      content: [{ type: "text" as const, text: `${a} ${operation} ${b} = ${result}` }],
    };
  }
);

// Tool 2: Random Number Generator
server.registerTool(
  "random-number",
  {
    title: "Random Number Generator",
    description: "Generate a random integer between min and max (inclusive)",
    inputSchema: z.object({
      min: z.number().describe("Minimum value (inclusive)"),
      max: z.number().describe("Maximum value (inclusive)"),
    }),
  },
  async ({ min, max }) => {
    const result = Math.floor(Math.random() * (max - min + 1)) + min;
    return {
      content: [{ type: "text" as const, text: `Random number between ${min} and ${max}: ${result}` }],
    };
  }
);

// Start the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((error) => {
  console.error("Server error:", error);
  process.exit(1);
});

Your server now has two working tools. Next up, you will add a to-do list tool that stores data between calls, giving your AI assistant the ability to remember things.

Add the To-Do List Tool

Your server already handles math and random numbers. Those are useful, but they're stateless. Every time the AI calls them, the result is independent of anything that came before.

A to-do list is different. It needs to remember things between calls. In this step, you'll add tools that write to and read from an in-memory list, giving your server its first taste of persistent state.

In this step, get ready to:
  • Create an in-memory array for storing to-do items.
  • Register an add-todo tool that accepts a task and saves it.
  • Register a list-todos tool that returns all saved tasks.
Create the to-do list storage and register the add-todo tool

Before registering a tool that adds items to a list, you need the list itself. A simple array at module level persists for the lifetime of the server process.

  • In src/index.ts, find the comment that says // Tool 1: Calculator.
  • Add the following code on a new line directly above that comment:
// In-memory to-do list
const todos: string[] = [];

Why declare the array here?

Placing todos at the module level means it lives for the entire time the server is running. Every tool call can read from and write to the same array. When the server shuts down, the data disappears.

  • Now scroll down past the random-number tool registration. Find the comment // Start the server.
  • Add the add-todo tool by pasting this code above the // Start the server comment:
// Tool 3: Add To-Do
server.registerTool(
  "add-todo",
  {
    title: "Add To-Do",
    description: "Add a task to the to-do list",
    inputSchema: z.object({
      task: z.string().describe("The task to add to the to-do list"),
    }),
  },
  async ({ task }) => {
    todos.push(task);
    return {
      content: [
        {
          type: "text" as const,
          text: `Added "${task}" to the to-do list. Current list:\n${todos.map((t, i) => `${i + 1}. ${t}`).join("\n")}`,
        },
      ],
    };
  }
);

What does this code do?

  • The inputSchema uses z.string() because a to-do item is just a text string. No numbers or enums needed.
  • The callback pushes the new task onto the todos array, then returns a confirmation message that includes the entire current list.
  • The template literal formats the list as numbered lines (1. Buy milk, 2. Walk the dog, etc.) so the AI can relay it clearly.
  • Save src/index.ts.

Seeing a red underline on todos?

Make sure the const todos: string[] = []; line is above all the tool registrations, not inside one of them. It needs to be at the top level of the file.

Also check that the line is placed after the server creation block but before the first server.registerTool() call.

Help me fix the red underline on my todos array in index.ts

Register the list-todos tool

Adding items is only half the picture. The AI also needs a way to retrieve the full list without modifying it.

  • In src/index.ts, add this code directly below the add-todo tool registration and above the // Start the server comment:
// Tool 4: List To-Dos
server.registerTool(
  "list-todos",
  {
    title: "List To-Dos",
    description: "Show all tasks in the to-do list",
    inputSchema: z.object({}),
  },
  async () => {
    if (todos.length === 0) {
      return {
        content: [{ type: "text" as const, text: "The to-do list is empty." }],
      };
    }
    return {
      content: [
        {
          type: "text" as const,
          text: `To-do list:\n${todos.map((t, i) => `${i + 1}. ${t}`).join("\n")}`,
        },
      ],
    };
  }
);

What does this code do?

  • The inputSchema: z.object({}) means this tool takes no inputs. The AI just calls it to read the current state.
  • The if (todos.length === 0) check handles the empty case gracefully so the AI gets a clear message instead of a blank response.
  • When the list has items, it formats them as numbered lines, identical to the format used in the add-todo confirmation.
  • Save src/index.ts.

TypeScript error on the empty object schema?

If you see an error related to z.object({}), make sure you're using the exact import import * as z from "zod/v4" at the top of your file. An empty object schema is valid and tells the MCP client this tool needs no parameters.

Help me fix my list-todos tool TypeScript error

Build the project to verify everything compiles
  • Compile your updated server by running this in the terminal:
npm run build
  • You should see the command complete with no errors. If TypeScript prints error messages, read the line numbers to find what went wrong.

Seeing compile errors?

Check that the add-todo and list-todos tool registrations are placed after the closing ); of the random-number tool. A missing closing parenthesis or brace from the previous tool will cascade errors into the new code.

Also confirm that const todos: string[] = []; appears before any tool that references it.

Help me fix my MCP server build errors

✔️ Awesome, I've got everything!

Great. Make sure you've saved src/index.ts and the build completed without errors.

ⓧ I'd like to double check the full code

Here is the complete src/index.ts file with all four tools registered:

import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";

// Create server instance
const server = new McpServer({
  name: "my-toolkit",
  version: "1.0.0",
});

// In-memory to-do list
const todos: string[] = [];

// Tool 1: Calculator
server.registerTool(
  "calculate",
  {
    title: "Calculator",
    description: "Perform a math calculation with two numbers",
    inputSchema: z.object({
      operation: z.enum(["add", "subtract", "multiply", "divide"]).describe("The math operation to perform"),
      a: z.number().describe("First number"),
      b: z.number().describe("Second number"),
    }),
  },
  async ({ operation, a, b }) => {
    let result: number;
    switch (operation) {
      case "add":
        result = a + b;
        break;
      case "subtract":
        result = a - b;
        break;
      case "multiply":
        result = a * b;
        break;
      case "divide":
        if (b === 0) {
          return {
            content: [{ type: "text" as const, text: "Error: Cannot divide by zero" }],
          };
        }
        result = a / b;
        break;
    }
    return {
      content: [{ type: "text" as const, text: `${a} ${operation} ${b} = ${result}` }],
    };
  }
);

// Tool 2: Random Number Generator
server.registerTool(
  "random-number",
  {
    title: "Random Number Generator",
    description: "Generate a random integer between min and max (inclusive)",
    inputSchema: z.object({
      min: z.number().describe("Minimum value (inclusive)"),
      max: z.number().describe("Maximum value (inclusive)"),
    }),
  },
  async ({ min, max }) => {
    const result = Math.floor(Math.random() * (max - min + 1)) + min;
    return {
      content: [{ type: "text" as const, text: `Random number between ${min} and ${max}: ${result}` }],
    };
  }
);

// Tool 3: Add To-Do
server.registerTool(
  "add-todo",
  {
    title: "Add To-Do",
    description: "Add a task to the to-do list",
    inputSchema: z.object({
      task: z.string().describe("The task to add to the to-do list"),
    }),
  },
  async ({ task }) => {
    todos.push(task);
    return {
      content: [
        {
          type: "text" as const,
          text: `Added "${task}" to the to-do list. Current list:\n${todos.map((t, i) => `${i + 1}. ${t}`).join("\n")}`,
        },
      ],
    };
  }
);

// Tool 4: List To-Dos
server.registerTool(
  "list-todos",
  {
    title: "List To-Dos",
    description: "Show all tasks in the to-do list",
    inputSchema: z.object({}),
  },
  async () => {
    if (todos.length === 0) {
      return {
        content: [{ type: "text" as const, text: "The to-do list is empty." }],
      };
    }
    return {
      content: [
        {
          type: "text" as const,
          text: `To-do list:\n${todos.map((t, i) => `${i + 1}. ${t}`).join("\n")}`,
        },
      ],
    };
  }
);

// Start the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((error) => {
  console.error("Server error:", error);
  process.exit(1);
});

Your server now has four tools and can remember information between calls. Next up, you'll connect it to VS Code so GitHub Copilot can discover and call these tools live.

Connect Your Server to VS Code and Test

You have a working MCP server with four tools that compiles without errors. The missing piece is connecting it to an AI application that can actually discover and call those tools.

In this step, you will create a configuration file that tells VS Code where your server lives. Then you will start the server, verify your tools are discovered, and chat with GitHub Copilot to watch it call your tools live.

In this step, get ready to:
  • Create the MCP configuration file that points VS Code to your server.
  • Start your server and verify all four tools are discovered.
  • Test your tools by chatting with Copilot in Agent mode.
Create the MCP configuration file

VS Code needs a configuration file to know how to start your server. This file lives at .vscode/mcp.json inside your project folder.

  • In the VS Code Explorer sidebar, right-click in the empty space below your files.
  • Select New Folder and name it .vscode.
  • Right-click the new .vscode folder and select New File.
  • Name the file mcp.json.
  • Paste the following configuration into mcp.json:
{
  "servers": {
    "my-toolkit": {
      "type": "stdio",
      "command": "node",
      "args": ["build/index.js"],
      "cwd": "${workspaceFolder}"
    }
  }
}

What does this configuration do?

This file tells VS Code how to launch your MCP server. Each key inside servers defines one server.

  • "type": "stdio" means VS Code communicates with the server over standard input/output (the stdio transport).
  • "command": "node" tells VS Code to run Node.js.
  • "args": ["build/index.js"] passes your compiled server file as the argument.
  • "cwd": "${workspaceFolder}" sets the working directory to your project root so the paths resolve correctly.
  • Save .vscode/mcp.json.
  • Confirm the file exists by checking the Explorer sidebar. You should see a .vscode folder containing mcp.json.

Don't see the .vscode folder?

Make sure the folder name starts with a dot (.vscode, not vscode). Some file explorers hide dotfiles by default, but VS Code's built-in Explorer always shows them.

Also confirm you created the folder at the project root level, not inside the src folder.

Help me troubleshoot my .vscode/mcp.json file setup.

Start the server and verify your tools

With the configuration file saved, VS Code can now detect your MCP server. You need to start it and confirm it discovers all four tools.

  • Open the Command Palette by pressing Ctrl+Shift+P.
  • Type MCP: List Servers and select it from the dropdown.
  • You should see my-toolkit in the list. Select it.
  • Select Start Server.

First-time trust prompt

VS Code shows a trust dialog the first time you start a workspace MCP server. This is a safety feature. Review the server configuration shown in the dialog, then confirm that you trust it.

  • After confirming trust, VS Code starts your server and discovers its tools. You should see your four tools (calculate, random-number, add-todo, list-todos) listed under the server.

Server not showing up or failing to start?

  • Make sure you ran npm run build successfully in the previous step. The server needs the compiled build/index.js file to exist.
  • Check that your .vscode/mcp.json has valid JSON. A missing comma or extra bracket breaks the config.
  • Select Show Output from the server's action menu to view error logs.

Help me debug why my MCP server isn't starting in VS Code.

Test your tools with Copilot in Agent mode

Your server is running and your tools are discovered. Now you will switch Copilot to Agent mode so it can actually call your tools during a conversation.

  • Open the Copilot Chat panel by pressing Ctrl+Shift+I.
  • In the chat panel, find the mode selector dropdown at the top.
  • Switch from the default mode to Agent mode.

What is Agent mode?

Agent mode gives Copilot the ability to use tools, run commands, and take actions on your behalf. In the default chat mode, Copilot can only generate text responses. In Agent mode, it can discover and call any tools provided by your MCP servers.

  • Try your first prompt. Type this into the chat:
What is 42 multiplied by 7?
  • Copilot recognizes this as a math question and calls your calculate tool. You should see it display the tool call and the result (294).
  • Now try adding a to-do item. Type:
Add buy groceries to my to-do list
  • Copilot calls your add-todo tool. You should see a confirmation that the task was added.
  • Finally, try the random number generator. Type:
Give me a random number between 1 and 100
  • Copilot calls your random-number tool and returns a random integer.

Why does Copilot ask for permission?

When Copilot wants to use a tool, it shows you which tool it plans to call and asks you to confirm. This is a safety feature. You can click Continue to approve each tool call, or choose to always allow a specific tool.

Copilot not calling your tools?

  • Confirm you switched to Agent mode in the chat panel. Tools are not available in the default Ask or Edit modes.
  • Check that the server is still running. Open the Command Palette (Ctrl+Shift+P), run MCP: List Servers, and verify your server shows as started.
  • If the server stopped, restart it from the same menu.

Help me figure out why Copilot isn't using my MCP tools.

✔️ Awesome, I've got everything!

Great. Double check you have saved .vscode/mcp.json and that all four tools respond when you chat with Copilot.

ⓧ I'd like to double check the full code

Here is the complete .vscode/mcp.json file:

{
  "servers": {
    "my-toolkit": {
      "type": "stdio",
      "command": "node",
      "args": ["build/index.js"],
      "cwd": "${workspaceFolder}"
    }
  }
}

And here is the full src/index.ts for reference:

import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";

// Create server instance
const server = new McpServer({
  name: "my-toolkit",
  version: "1.0.0",
});

// In-memory to-do list
const todos: string[] = [];

// Tool 1: Calculator
server.registerTool(
  "calculate",
  {
    title: "Calculator",
    description: "Perform a math calculation with two numbers",
    inputSchema: z.object({
      operation: z.enum(["add", "subtract", "multiply", "divide"]).describe("The math operation to perform"),
      a: z.number().describe("First number"),
      b: z.number().describe("Second number"),
    }),
  },
  async ({ operation, a, b }) => {
    let result: number;
    switch (operation) {
      case "add":
        result = a + b;
        break;
      case "subtract":
        result = a - b;
        break;
      case "multiply":
        result = a * b;
        break;
      case "divide":
        if (b === 0) {
          return {
            content: [{ type: "text" as const, text: "Error: Cannot divide by zero" }],
          };
        }
        result = a / b;
        break;
    }
    return {
      content: [{ type: "text" as const, text: `${a} ${operation} ${b} = ${result}` }],
    };
  }
);

// Tool 2: Random Number Generator
server.registerTool(
  "random-number",
  {
    title: "Random Number Generator",
    description: "Generate a random integer between min and max (inclusive)",
    inputSchema: z.object({
      min: z.number().describe("Minimum value (inclusive)"),
      max: z.number().describe("Maximum value (inclusive)"),
    }),
  },
  async ({ min, max }) => {
    const result = Math.floor(Math.random() * (max - min + 1)) + min;
    return {
      content: [{ type: "text" as const, text: `Random number between ${min} and ${max}: ${result}` }],
    };
  }
);

// Tool 3: Add To-Do
server.registerTool(
  "add-todo",
  {
    title: "Add To-Do",
    description: "Add a task to the to-do list",
    inputSchema: z.object({
      task: z.string().describe("The task to add to the to-do list"),
    }),
  },
  async ({ task }) => {
    todos.push(task);
    return {
      content: [
        {
          type: "text" as const,
          text: `Added "${task}" to the to-do list. Current list:\n${todos.map((t, i) => `${i + 1}. ${t}`).join("\n")}`,
        },
      ],
    };
  }
);

// Tool 4: List To-Dos
server.registerTool(
  "list-todos",
  {
    title: "List To-Dos",
    description: "Show all tasks in the to-do list",
    inputSchema: z.object({}),
  },
  async () => {
    if (todos.length === 0) {
      return {
        content: [{ type: "text" as const, text: "The to-do list is empty." }],
      };
    }
    return {
      content: [
        {
          type: "text" as const,
          text: `To-do list:\n${todos.map((t, i) => `${i + 1}. ${t}`).join("\n")}`,
        },
      ],
    };
  }
);

// Start the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((error) => {
  console.error("Server error:", error);
  process.exit(1);
});

Your MCP server is live and your AI assistant is calling your custom tools. You have built a complete end-to-end integration. Next up, tackle the Secret Mission to add a read-only resource that exposes data to the AI without needing a tool call.

Secret mission

Add a Resource

Your server currently exposes tools that the AI must call to get information. But what if the AI could read data directly, without calling a function? MCP resources give your server the ability to expose read-only data that the AI can pull into its context at any time.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • my-mcp-server project folder on your Desktop.
  • Node.js v24 LTS installation.
  • VS Code with GitHub Copilot Free.

Keep everything running

No action needed. Choose this if you plan to keep building MCP servers or working with Node.js projects.

  • Node.js and VS Code stay installed and ready for future projects.
  • Your MCP server only runs when VS Code starts it via the mcp.json configuration. There are no background processes consuming resources.

Pause - I'll come back to this later

Your MCP server only runs when VS Code starts it. There is nothing to pause or shut down.

  • If you want to stop VS Code from auto-starting the server, delete the .vscode/mcp.json file inside your project folder.
  • Your source code in src/index.ts remains untouched. You can recreate mcp.json later to reconnect.

Delete - I don't want to use this again

Remove all project files and optionally uninstall the tools you installed during this project.

  • Close VS Code if it is open.
  • Delete the project folder by opening a terminal (Command Prompt or PowerShell) and running:
Remove-Item -Recurse -Force "$HOME\Desktop\my-mcp-server"
  • To uninstall Node.js, open Settings, then Apps, then Installed apps. Search for Node.js and click Uninstall.
  • To uninstall VS Code, follow the same path in Settings > Apps > Installed apps. Search for Visual Studio Code and click Uninstall.

Nice Work!

Nice Work!

You just built a working MCP server from scratch and connected it to a real AI assistant. GitHub Copilot can now discover and call your custom tools on demand.

You've learned how to:

  • Build a TypeScript MCP server with four registered tools (calculate, random-number, add-todo, and list-todos) that an AI can call.
  • Connect your server to VS Code via mcp.json configuration and test tools live by chatting with Copilot in Agent mode.
  • Understand the MCP architecture: hosts, clients, servers, and the stdio transport that connects them.
  • Secret Mission: Added a read-only resource that exposes data to the AI without requiring a tool call.

Ready to quiz yourself?