MCP (Model Context Protocol) servers are how you extend Claude and other AI assistants with custom tools, data sources, and APIs. Publishing one to npm means anyone can add your server to their Claude Desktop config in one line. Here's the complete guide — from scaffold to published package.
What You're Building
An MCP server is a process that speaks the MCP protocol over stdio (or HTTP with SSE for remote servers). Claude connects to it and can call your tools like function calls. A published npm package means users install it with:
{
"mcpServers": {
"your-server": {
"command": "npx",
"args": ["-y", "your-mcp-server-name"]
}
}
}
No global installs, no cloning repos — npx -y handles everything.
Scaffold
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
},
"include": ["src"]
}
Write the Server
// src/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
const server = new Server(
{ name: 'my-mcp-server', version: '1.0.0' },
{ capabilities: { tools: {} } }
)
// Declare your tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'get_weather',
description: 'Get current weather for a city',
inputSchema: {
type: 'object',
properties: {
city: { type: 'string', description: 'City name' },
},
required: ['city'],
},
},
],
}))
// Handle tool calls
const GetWeatherInput = z.object({ city: z.string() })
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'get_weather') {
const { city } = GetWeatherInput.parse(request.params.arguments)
// Your actual implementation
const response = await fetch(
`https://wttr.in/${encodeURIComponent(city)}?format=j1`
)
const data = await response.json()
const temp = data.current_condition[0].temp_F
const desc = data.current_condition[0].weatherDesc[0].value
return {
content: [{
type: 'text',
text: `${city}: ${temp}°F, ${desc}`,
}],
}
}
throw new Error(`Unknown tool: ${request.params.name}`)
})
// Start
const transport = new StdioServerTransport()
await server.connect(transport)
Package.json Setup for npm
This is where most guides fall short. The bin field is critical:
{
"name": "my-mcp-server",
"version": "1.0.0",
"description": "MCP server for weather data",
"main": "dist/index.js",
"bin": {
"my-mcp-server": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"prepublishOnly": "npm run build"
},
"files": ["dist"],
"engines": { "node": ">=18" },
"keywords": ["mcp", "claude", "ai", "model-context-protocol"]
}
Then add the shebang to your built file — but do it at the source level so TypeScript doesn't complain:
// src/index.ts — first line
#!/usr/bin/env node
TypeScript will error on the shebang. Fix: add it to the compiled output instead via a build step:
"build": "tsc && echo '#!/usr/bin/env node' | cat - dist/index.js > dist/index.tmp.js && mv dist/index.tmp.js dist/index.js && chmod +x dist/index.js"
Test Locally Before Publishing
npm run build
# Test via Claude Desktop
# Add to ~/Library/Application\ Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"my-mcp-server-dev": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/dist/index.js"]
}
}
}
Restart Claude Desktop. You should see your tool appear when Claude starts a new conversation.
Alternatively, test with the MCP Inspector:
npx @modelcontextprotocol/inspector node dist/index.js
This opens a browser UI where you can call tools directly without Claude.
Publish to npm
npm login
npm publish --access public
For scoped packages (@yourname/mcp-server-name):
npm publish --access public # scoped packages default to private
After publishing, test the npx install path:
npx -y my-mcp-server
# Should start the server and wait for MCP connections
Ctrl+C
Handling API Keys
Most useful MCP servers need credentials. The convention is environment variables passed through the config:
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["-y", "my-mcp-server"],
"env": {
"API_KEY": "user-api-key-here"
}
}
}
}
In your server:
const apiKey = process.env.API_KEY
if (!apiKey) {
console.error('API_KEY environment variable required')
process.exit(1)
}
Fail fast with a clear error message — users copy the config template from your README and often miss the env vars.
README Template
The README is what shows up on npmjs.com and what users copy from. Include:
- One-line description of what the server does
- The exact JSON snippet to add to Claude Desktop config
- List of tools with brief descriptions
- Any required env vars
Users should be able to go from README to working in under 2 minutes.
What to Build
High-value MCP servers that don't exist yet (as of April 2026):
- Company-internal tools (Notion, Linear, Jira) with auth already handled
- Domain-specific APIs (weather, finance, maps) with smart formatting
- Local file system helpers scoped to specific directories
- Database introspection servers that let Claude explore schema and run safe queries
The MCP ecosystem is still early. A good server in an underserved niche gets picked up quickly.
Already built an MCP server? Check out the MCP Security Scanner — automated vulnerability detection for MCP server codebases. Also: Workflow Automator MCP and Trading Signals MCP are live examples of production MCP servers you can reference.
Top comments (0)