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
2) Run locally:
node index.js
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);
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: '...' } ] }
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"
}
}
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
If the server needs environment variables:
codex mcp add my-local-server --env -- node /path/to/your/project/index.js
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"]
Verify the Server
Restart Codex, then check active MCP servers from the Codex TUI:
/mcp
You can also inspect available MCP commands with:
codex mcp --help
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 } })
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
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"]
}
}
}
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.jsimports. - MODULE_TYPELESS_PACKAGE_JSON? Add "type": "module" to package.json.
- Run
node --trace-warnings index.jsfor 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
- SDK examples: node_modules/@modelcontextprotocol/sdk/dist/esm/examples
- Official docs: https://modelcontextprotocol.io
- example code : https://github.com/gaurav101/ai-experiment
Top comments (0)