DEV Community

Dilum Darshana
Dilum Darshana

Posted on

Building My First MCP Server with TypeScript: A Beginner’s Journey

Hello backend devs,

As someone deeply interested in LLM-based Agentic workflows, I recently took into a new challenge: building my first Model Context Protocol (MCP) server using TypeScript. This post walks through the process—from zero to a working MCP server—and what I learned along the way. There are many articles in the internet which is explained in Python. Here, I wanted to use my most familiar JavaScript as a programming language.

Why we need MCP?
Extending the capabilities of large language models (LLMs) has become increasingly important. While there are many ways to integrate external tools or services with LLMs, these methods are often ad hoc and lack consistency. When building scalable and reusable solutions, it's crucial to have a standardised way to define and expose tools that LLMs can interact with.

This is where Model Context Protocol (MCP) comes into the picture. MCP provides a protocol-driven approach to tool integration, making it easier to define, discover, and use external tools in a consistent and structured manner—regardless of the environment or framework.

If you’re familiar with RAG (Retrieval-Augmented Generation), you can think of MCP as a more secure and controllable alternative for tool execution. With RAG, the model often has access to unstructured or semi-structured data sources. In contrast, with MCP, tools are invoked by an MCP client, and only the results are passed back to the LLM. This provides a more secure and sandboxed execution flow—keeping tool logic and sensitive operations isolated from the LLM.

In short, MCP helps extend the power of LLMs through a common standard, promoting reusability, interoperability, and better developer experience.

Technical approach
To implement my first MCP server, I chose the official @modelcontextprotocol/sdk npm package. This SDK provides a clean and TypeScript-friendly interface to build MCP-compliant servers and tools. It includes everything needed to handle standard MCP request types, define tools with schema validation, and manage communication over stdin/stdout (which is how many LLM runtimes expect to interact with tools).

The SDK abstracts away many of the low-level details, letting you focus on defining what your tools do, rather than how they are wired into the protocol. This made it a great choice for a first-time MCP implementation.

While I went with the official SDK for simplicity and clarity, there are other approaches available too.

[Documentation]https://github.com/modelcontextprotocol/typescript-sdk

Setting up the project
To keep things simple and focused, I used a minimal folder structure and pnpm as my package manager. Since this was a TypeScript-based CLI-style MCP server, I didn’t need a complex build system or runtime framework.

Here's the basic folder structure I followed:

Folder structure

package.json

{
  "name": "mcp-currency-converter",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "bin": {
    "mcp-currency-converter": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"",
    "inspector": "pnpx @modelcontextprotocol/inspector node ./dist/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "packageManager": "[email protected]",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.15.0",
    "zod": "^3.25.67"
  },
  "devDependencies": {
    "@types/node": "^20",
    "typescript": "^5.8.3"
  }
}
Enter fullscreen mode Exit fullscreen mode

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
Enter fullscreen mode Exit fullscreen mode

Tool
Tools are the heart of an MCP server. Each tool exposes a specific piece of functionality that the LLM can invoke via the MCP client. You can register multiple tools in the same MCP server, each serving different—but often related—business purposes.

In my case, I implemented two tools to demonstrate a basic currency conversion use case:

  1. Currency Converter – Converts a given amount from one currency to another.

  2. List Available Currencies – Returns a list of supported currency codes.

Both tools are defined using the @modelcontextprotocol/sdk along with zod for schema validation.

Here’s how they are implemented:

types.ts

import { z } from 'zod';

export interface MCPTool<TInput extends z.ZodTypeAny = any> {
  name: string;
  description: string;
  schema: TInput;
  handler: (input: z.infer<TInput>) => Promise<any>;
};
Enter fullscreen mode Exit fullscreen mode

tools/convertCurrency.ts


/**
 * Convert given amount from one currency to another using the Free Currency API.
 */
import { z } from 'zod';
import { MCPTool } from '../types';

const schema = z.object({
  fromCurrency: z.string().describe('The currency to convert from (e.g., USD, EUR)'),
  toCurrency: z.string().describe('The currency to convert to (e.g., USD, EUR)'),
  amount: z.number().positive().describe('The amount to convert'),
});

export const convertCurrencyTool: MCPTool<typeof schema> = {
  name: 'convert-currency',
  description: 'Converts an amount from one currency to another',
  schema,
  handler: async ({ fromCurrency, toCurrency, amount }: any) => {
    const currencyFinderKey = process.env.FREE_CURRENCY_KEY;
    if (!currencyFinderKey) throw new Error('Missing FREE_CURRENCY_KEY');

    console.log(`Converting ${amount} ${fromCurrency} to ${toCurrency}`);

    const response = await fetch(
      `https://api.freecurrencyapi.com/v1/latest?apikey=${currencyFinderKey}&base_currency=${fromCurrency}&currencies=${toCurrency}`
    );

    const data = await response.json();
    const exchangeRate = data.data?.[toCurrency];

    if (!exchangeRate) throw new Error('Invalid exchange rate');

    const convertedAmount = exchangeRate * amount;

    return {
      content: [
        {
          type: 'text',
          text: `Converted ${amount} ${fromCurrency} to ${toCurrency}: ${convertedAmount} ${toCurrency}`,
        },
      ],
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

tools/listCurrencies.ts

/**
 * List avaible currencies tool. Available currencies are fetched from freecurrencyapi.com
 */
import { z } from 'zod';
import { MCPTool } from '../types';

// Schema
const schema = z.object({});

// List currencies tool
export const listCurrenciesTool: MCPTool<typeof schema> = {
  name: 'list-currencies',
  description: 'Lists all supported currencies',
  schema,
  handler: async () => {
    const currencyFinderKey = process.env.FREE_CURRENCY_KEY;
    if (!currencyFinderKey) throw new Error('Missing FREE_CURRENCY_KEY');

    console.log('Listing supported currencies');

    const response = await fetch(
      `https://api.freecurrencyapi.com/v1/currencies?apikey=${currencyFinderKey}`
    );
    const data = await response.json();

    return {
      content: [
        {
          type: 'text',
          text: `Supported currencies: ${Object.keys(data).join(', ')}`,
        },
      ],
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

Wiring Up the MCP Server
Now that the tools are ready, the next step is to create the MCP server and integrate those tools using the @modelcontextprotocol/sdk.

In this setup, I defined a simple MCP server using the StdioServerTransport, which allows tools to be called locally through stdin and stdout—ideal for testing or local agent interactions.

After initialising the server, I registered both tools so they can be listed and executed via standard MCP requests.

Here is the implementation:

index.ts

#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { convertCurrencyTool } from './tools/convertCurrency.js';
import { listCurrenciesTool } from './tools/listCurrencies.js';

const server = new McpServer({
  name: 'currencyConverter',
  version: '1.0.0',
});

// Tool no: 1
server.tool(
  convertCurrencyTool.name,
  convertCurrencyTool.description,
  convertCurrencyTool.schema.shape,
  convertCurrencyTool.handler,
);

// Tool no: 2
server.tool(
  listCurrenciesTool.name,
  listCurrenciesTool.description,
  listCurrenciesTool.schema.shape,
  listCurrenciesTool.handler,
);

const transport = new StdioServerTransport();

server.connect(transport);
Enter fullscreen mode Exit fullscreen mode

Testing it
That is it! Now it’s time to test whether everything works as expected.

For this, I used the official modelcontextprotocol/inspector tool. It provides a simple way to inspect and interact with your local MCP server via a CLI interface.

I added it as a dependency and defined a script in package.json for convenience. To run the test, navigate into your project directory and execute the following command:

$ pnpm inspector
Enter fullscreen mode Exit fullscreen mode

This will launch the inspector (See the console output for the URL), which connects to your MCP server and lists the available tools. You can then invoke each tool, provide input, and view the structured output—all directly from your terminal.

It’s a great way to quickly validate your server’s setup and ensure tool execution is working correctly.

In here, we can type different inputs to see how it works:

Step 1: Connect the Inspector
Click the "Connect" button in the bottom left corner of the UI.

Inspector connect

Step 2: Load and Test Tools
After connecting, go to the “Tools” tab.

Click on “List Tools” to load the available tools from your MCP server.

You can now select a tool (e.g., convertCurrency or listCurrencies), enter the required parameters, and run it.

If everything is working correctly, you should receive a valid and expected response from your tool.

💡 Note: If your MCP server relies on any environment variables (e.g., API keys), make sure to set them inside the Inspector UI before running the tools.
In my case, I had to set FREE_CURRENCY_KEY for the currency conversion API to work properly.

Inspector response

Key takeaways

  • MCP encourages clean, reusable, and structured tool development.

  • The @modelcontextprotocol/sdk abstracts a lot, but understanding what's happening under the hood helps when debugging.

  • The Inspector tool is incredibly helpful—but only if the server and tools are defined correctly.

  • Type safety and schema validation are your best friends—don’t skip on them!

Conclusion
Building my first MCP server in TypeScript was a rewarding experience. It not only helped me understand how to expose tools to LLMs in a structured way, but also gave me a glimpse into the future of AI integrations—where standards like MCP can bring consistency, security, and reusability to the developer ecosystem.

The @modelcontextprotocol/sdk made it approachable, and using tools like the Inspector helped validate everything with confidence.

If you're working with LLMs and looking for a clean way to plug in external capabilities, I highly recommend exploring MCP. Start small, build a simple tool, and see how easily it can scale.

What's Next?
In my next post, I'll walk through how to use the MCP server from code using an MCP client—so you can see how to invoke tools programmatically and integrate them into your own AI workflows.

Cheers... Happy coding!!!

Top comments (0)