DEV Community

Cover image for Build a tiny MCP server in JavaScript -Claude, Codex friendly
Gaurav Kumar Singh
Gaurav Kumar Singh

Posted on

Build a tiny MCP server in JavaScript -Claude, Codex friendly

TL;DR — Ship a tiny server that exposes tools to LLMs using the Model Context Protocol (MCP). This guide shows a minimal, runnable example, explains each part, and covers common pitfalls (ESM imports, transports, adapters).

Why this matters

  • MCP lets models call real tools (fetching data, running commands) instead of hallucinating.
  • It’s a great way to extend models with live data: notifications, GitHub lookups, database queries, etc.

What you'll get

  • A working server (index.js) exposing a single tool: get-repo-stats
  • Quick instructions to run locally (stdio) or remotely (HTTP)
  • Short adapter patterns for OpenAI (Codex/GPT), Google Gemini, and Anthropic Claude

Quick checklist

  • Node 18+
  • npm install (project already depends on @modelcontextprotocol/sdk and zod)
  • Add "type": "module" to package.json for ESM

Quick start (copy-paste)

1) Install deps or verify existing:

npm install
Enter fullscreen mode Exit fullscreen mode

2) Run locally:

node index.js
Enter fullscreen mode Exit fullscreen mode

If you see a MODULE_TYPELESS_PACKAGE_JSON warning, add "type": "module" to package.json (example below).

Minimal index.js (copy to your repo)

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const BASE_URL = "https://api.github.com";

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

server.tool(
  'get-repo-stats',
  'Get star count, fork count, and open issues for a GitHub repo',
  {
    owner: z.string().describe("GitHub username or org"),
    repo: z.string().describe("Repository name"),
  },
  async ({ owner, repo }) => {
    const res = await fetch(`${BASE_URL}/repos/${owner}/${repo}`);
    if (!res.ok) {
      return { content: [{ type: 'text', text: `Could not find repo ${owner}/${repo}` }] };
    }
    const data = await res.json();
    return {
      content: [{ type: 'text', text: `${owner}/${repo} — ⭐ ${data.stargazers_count} · 🍴 ${data.forks_count} · 🐛 ${data.open_issues_count}` }]
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Enter fullscreen mode Exit fullscreen mode

Code walkthrough

  • McpServer: high-level API to register tools and handle protocol
  • server.tool(name, description, schema, handler): register callable tools. Use zod for input validation.

  • Handler returns a result shaped like

{ content: [ { type: 'text', text: '...' } ] }

Enter fullscreen mode Exit fullscreen mode
  • StdioServerTransport: easiest local transport — reads/writes over stdin/stdout

  • Call server.connect(transport) after registering tools so the initialization advertises available tools

ESM / import gotchas

  • Add "type": "module" to package.json so Node treats your files as ESM and avoids MODULE_TYPELESS_PACKAGE_JSON warning.

  • Use package export entrypoints and include .js extension:

    • Good: import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    • Bad: deep imports to dist/esm/… — Node may fail to resolve or duplicate path segments.

Example package.json snippet

{
  "name": "mcp",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.29.0",
    "zod": "^3.25"
  }
}
Enter fullscreen mode Exit fullscreen mode

Transports at a glance

  • stdio: local adapters (Claude Desktop, local testing). Simpler to run.
  • StreamableHTTP: model host calls an HTTP endpoint (preferred for cloud deployments).
  • WebSocket: long-lived bidirectional connections for advanced flows.

how to connect an LLM

OpenAI / Codex (function-calling flow)

Add a Local MCP Server to Codex

Codex can use local MCP servers through STDIO. MCP configuration is stored in
~/.codex/config.toml by default, or in a project-level .codex/config.toml
for trusted projects.

Option 1: Add with the Codex CLI
codex mcp add my-local-git-stat-server -- node /path/to/your/project/index.js
Enter fullscreen mode Exit fullscreen mode

If the server needs environment variables:

codex mcp add my-local-server --env -- node /path/to/your/project/index.js
Enter fullscreen mode Exit fullscreen mode
Option 2: Add Manually in config.toml

Open ~/.codex/config.toml and add:

[mcp_servers.my-local-server]
command = "node"
args = ["/path/to/your/project/index.js"]

Enter fullscreen mode Exit fullscreen mode

Verify the Server

Restart Codex, then check active MCP servers from the Codex TUI:

/mcp
Enter fullscreen mode Exit fullscreen mode

You can also inspect available MCP commands with:

codex mcp --help
Enter fullscreen mode Exit fullscreen mode

Google Gemini

  • If the hosting environment supports HTTP callbacks, use StreamableHTTPClientTransport. Otherwise, run a bridge process that translates Gemini function calls into MCP client calls.

Anthropic Claude

  • For local Claude Desktop: the desktop app can spawn your server process and use stdio. Configure the desktop app to run node /path/index.js.
  • For hosted Claude integrations, prefer HTTP transport.

Small adapter sketch (pseudo)

// spawn server then connect client-facing logic
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

const transport = new StdioClientTransport({ command: 'node', args: ['index.js'] });
await transport.start();
const client = new Client({ name: 'bridge', version: '1.0.0' });
await client.connect(transport);

// When model asks for a tool: client.callTool({ method: 'get-repo-stats', params: { owner, repo } })
Enter fullscreen mode Exit fullscreen mode

Using with OpenAI (Codex / GPT function-calling)

Recommended patterns

  • Local/testing: use stdio transport + StdioClientTransport (spawn the server as a child process from your adapter).
  • Hosted: expose your MCP server with StreamableHTTPTransport and let your adapter call it via StreamableHTTPClientTransport.

Minimal adapter sketch (pseudo)

// adapter-openai.js — high level sketch
import OpenAI from 'openai'; // or your HTTP wrapper
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; // or StreamableHTTPClientTransport


// spawn local server and connect client
const transport = new StdioClientTransport({ command: 'node', args: ['index.js'] });
await transport.start();
const client = new Client({ name: 'openai-bridge', version: '1.0.0' });
await client.connect(transport);

// When a model function call arrives (pseudo):
async function handleModelFunctionCall(name, params) {
  // Map function name to your tool method (e.g., 'get-repo-stats')
  const toolResult = await client.callTool({ method: `tools/${name}`, params });
  // Convert the MCP result into the expected function-response string or JSON and return it
  return toolResult;
}

// Use your OpenAI SDK to call model and route function-calls through handleModelFunctionCall
Enter fullscreen mode Exit fullscreen mode

Notes

  • Keep the mapping from model function names to MCP tool names explicit and documented.
  • Don't commit API keys — use env vars, and sanitize when spawning processes.

Using with Anthropic Claude (Claude Desktop)

Local desktop (stdio) — easiest

1. Add your server to Claude Desktop configuration (absolute path to your index.js):

{
  "mcpServers": {
    "github-stats": {
      "command": "node",
      "args": ["/absolute/path/to/index.js"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Ensure your project has "type": "module" in package.json and your imports use .js extensions.

3. Start / restart Claude Desktop. The desktop app will spawn your server and talk over stdin/stdout. Ask Claude: "What are the stats for facebook/react?" and it should call the get-repo-stats tool.

Hosted Claude / remote

  • If your Claude hosting supports HTTP callbacks, host the MCP server behind an HTTP wrapper and use StreamableHTTPClientTransport on the model side.

Testing tips

  • Run the server with node --trace-warnings index.js when debugging import/ESM issues.
  • Use a small script that calls client.listTools() after connecting to verify the advertised tools.

Debugging checklist

  • ERR_MODULE_NOT_FOUND for deep path? Switch to @modelcontextprotocol/sdk/server/mcp.js imports.
  • MODULE_TYPELESS_PACKAGE_JSON? Add "type": "module" to package.json.
  • Run node --trace-warnings index.js for resolution traces.

Security & deployment (brief)

  • If exposing HTTP, require auth (API key, OAuth) and use TLS.
  • When spawning child processes, sanitize environment and avoid leaking secrets.
  • Limit resource usage (timeouts, concurrency) on tools that call external services.

Make it yours (next steps)

  • Add more tools (issues, releases, search)
  • Add authentication for private GitHub repos
  • Wrap as an npm CLI and add a config file for easy local installs
References & where to look

Top comments (0)