← Back to Blog
tutorialmcp-servernodejs

Building Your First MCP Server from Scratch

Write an MCP server in Node.js with a working tool, test it locally, and get it ready for mcpm publishing.

·3 min read·xapable

Building Your First MCP Server from Scratch

Before publishing to mcpm, you need an MCP server. This tutorial builds one from zero — no prior MCP experience required.

What You'll Build

A Todo MCP Server with two tools:

  • add_todo — add a task
  • list_todos — list all tasks

Prerequisites

  • Node.js 18+
  • Basic JavaScript knowledge

Step 1: Initialize the Project

<span class="hljs-built_in">mkdir</span> todo-mcp &amp;&amp; <span class="hljs-built_in">cd</span> todo-mcp
npm init -y
npm install @modelcontextprotocol/sdk

Step 2: Write the Server

Create index.js:

<span class="hljs-keyword">import</span> { <span class="hljs-title class_">Server</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@modelcontextprotocol/sdk/server/index.js&quot;</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">StdioServerTransport</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@modelcontextprotocol/sdk/server/stdio.js&quot;</span>;

<span class="hljs-comment">// In-memory storage (replace with a database in production)</span>
<span class="hljs-keyword">const</span> todos = [];

<span class="hljs-keyword">const</span> server = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Server</span>({
  <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;todo-mcp&quot;</span>,
  <span class="hljs-attr">version</span>: <span class="hljs-string">&quot;1.0.0&quot;</span>,
}, {
  <span class="hljs-attr">capabilities</span>: { <span class="hljs-attr">tools</span>: {} }
});

<span class="hljs-comment">// ── List available tools ──────────────────────────</span>
server.<span class="hljs-title function_">setRequestHandler</span>(<span class="hljs-string">&quot;tools/list&quot;</span>, <span class="hljs-title function_">async</span> () =&gt; ({
  <span class="hljs-attr">tools</span>: [
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;add_todo&quot;</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Add a new todo item to the list&quot;</span>,
      <span class="hljs-attr">inputSchema</span>: {
        <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;object&quot;</span>,
        <span class="hljs-attr">properties</span>: {
          <span class="hljs-attr">task</span>: {
            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;string&quot;</span>,
            <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;The task description&quot;</span>
          },
          <span class="hljs-attr">priority</span>: {
            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;string&quot;</span>,
            <span class="hljs-attr">enum</span>: [<span class="hljs-string">&quot;low&quot;</span>, <span class="hljs-string">&quot;medium&quot;</span>, <span class="hljs-string">&quot;high&quot;</span>],
            <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Task priority (default: medium)&quot;</span>
          }
        },
        <span class="hljs-attr">required</span>: [<span class="hljs-string">&quot;task&quot;</span>]
      }
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;list_todos&quot;</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;List all current todos&quot;</span>,
      <span class="hljs-attr">inputSchema</span>: {
        <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;object&quot;</span>,
        <span class="hljs-attr">properties</span>: {
          <span class="hljs-attr">filter</span>: {
            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;string&quot;</span>,
            <span class="hljs-attr">enum</span>: [<span class="hljs-string">&quot;all&quot;</span>, <span class="hljs-string">&quot;low&quot;</span>, <span class="hljs-string">&quot;medium&quot;</span>, <span class="hljs-string">&quot;high&quot;</span>],
            <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Filter by priority (default: all)&quot;</span>
          }
        }
      }
    }
  ]
}));

<span class="hljs-comment">// ── Handle tool calls ────────────────────────────</span>
server.<span class="hljs-title function_">setRequestHandler</span>(<span class="hljs-string">&quot;tools/call&quot;</span>, <span class="hljs-title function_">async</span> (request) =&gt; {
  <span class="hljs-keyword">const</span> { name, <span class="hljs-attr">arguments</span>: args } = request.<span class="hljs-property">params</span>;

  <span class="hljs-keyword">switch</span> (name) {
    <span class="hljs-keyword">case</span> <span class="hljs-string">&quot;add_todo&quot;</span>: {
      <span class="hljs-keyword">const</span> todo = {
        <span class="hljs-attr">id</span>: todos.<span class="hljs-property">length</span> + <span class="hljs-number">1</span>,
        <span class="hljs-attr">task</span>: args.<span class="hljs-property">task</span>,
        <span class="hljs-attr">priority</span>: args.<span class="hljs-property">priority</span> || <span class="hljs-string">&quot;medium&quot;</span>,
        <span class="hljs-attr">created</span>: <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>().<span class="hljs-title function_">toISOString</span>()
      };
      todos.<span class="hljs-title function_">push</span>(todo);
      <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">content</span>: [{
          <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>,
          <span class="hljs-attr">text</span>: <span class="hljs-string">`✅ Added: &quot;<span class="hljs-subst">${args.task}</span>&quot; (priority: <span class="hljs-subst">${todo.priority}</span>)`</span>
        }]
      };
    }

    <span class="hljs-keyword">case</span> <span class="hljs-string">&quot;list_todos&quot;</span>: {
      <span class="hljs-keyword">const</span> filter = args.<span class="hljs-property">filter</span> || <span class="hljs-string">&quot;all&quot;</span>;
      <span class="hljs-keyword">const</span> filtered = filter === <span class="hljs-string">&quot;all&quot;</span>
        ? todos
        : todos.<span class="hljs-title function_">filter</span>(<span class="hljs-function"><span class="hljs-params">t</span> =&gt;</span> t.<span class="hljs-property">priority</span> === filter);

      <span class="hljs-keyword">if</span> (filtered.<span class="hljs-property">length</span> === <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">return</span> {
          <span class="hljs-attr">content</span>: [{ <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>, <span class="hljs-attr">text</span>: <span class="hljs-string">&quot;No todos found. 📭&quot;</span> }]
        };
      }

      <span class="hljs-keyword">const</span> list = filtered
        .<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">t</span> =&gt;</span> <span class="hljs-string">`[<span class="hljs-subst">${t.priority.toUpperCase()}</span>] <span class="hljs-subst">${t.task}</span>`</span>)
        .<span class="hljs-title function_">join</span>(<span class="hljs-string">&quot;\n&quot;</span>);

      <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">content</span>: [{
          <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>,
          <span class="hljs-attr">text</span>: <span class="hljs-string">`📋 Todos:\n<span class="hljs-subst">${list}</span>`</span>
        }]
      };
    }

    <span class="hljs-attr">default</span>:
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(<span class="hljs-string">`Unknown tool: <span class="hljs-subst">${name}</span>`</span>);
  }
});

<span class="hljs-comment">// ── Start the server ─────────────────────────────</span>
<span class="hljs-keyword">const</span> transport = <span class="hljs-keyword">new</span> <span class="hljs-title class_">StdioServerTransport</span>();
<span class="hljs-keyword">await</span> server.<span class="hljs-title function_">connect</span>(transport);

Step 3: Test Locally

Run the server:

node index.js

The server starts and listens on stdio. To test with a real MCP client, configure Claude Desktop or Cursor to point to this server.

Claude Desktop Configuration

Add to claude_desktop_config.json:

<span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;mcpServers&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;todo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;command&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;node&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;args&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;/absolute/path/to/todo-mcp/index.js&quot;</span><span class="hljs-punctuation">]</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>

Restart Claude Desktop. Ask: "Add a todo: buy groceries with high priority." Then: "List all todos."

Step 4: Prepare for mcpm

Write a README (see our Publishing tutorial) and ensure your package.json is complete. Then:

mcpm-dev publish

Key Takeaways

  1. tools/list tells clients what tools are available
  2. tools/call handles actual invocations
  3. inputSchema defines parameters — make them clear and descriptive
  4. Return structured content — not raw text

Next Steps

  • Add more tools (delete, update, search)
  • Replace in-memory storage with SQLite or PostgreSQL
  • Add authentication
  • Publish to mcpm

#MCP #Tutorial #NodeJS #mcpm #AIAgents