A minimal, general-purpose implementation of the Model Context Protocol (MCP).
Works on Node.js and Bare runtime (Pear/Holepunch).
- Tools — Register functions that AI clients can call
- Resources — Expose data that clients can read (static or dynamic)
- Resource Templates — URI patterns with parameters (
user://{id}) - Annotations — Metadata hints for tools and content (MCP 2025-11-25)
- Notifications — Push updates to connected clients
- Subscriptions — Clients can subscribe to resource changes
- Multiple Transports — HTTP, WebSocket, SSE, stdio
import { createMCPServer } from 'bare-mcp'
import { createHttpTransport } from 'bare-mcp/http'
// Create server
const mcp = createMCPServer({
name: 'my-server',
version: '1.0.0'
})
// Register a tool
mcp.addTool({
name: 'greet',
description: 'Say hello to someone',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Name to greet' }
},
required: ['name']
},
execute: async ({ name }) => `Hello, ${name}!`
})
// Start HTTP server (works on both Node.js and Bare)
await createHttpTransport(mcp, { port: 3000 })Tools are functions that AI clients can invoke.
mcp.addTool({
name: 'calculate',
description: 'Perform arithmetic',
inputSchema: {
type: 'object',
properties: {
a: { type: 'number' },
b: { type: 'number' },
op: { type: 'string', enum: ['add', 'subtract', 'multiply', 'divide'] }
},
required: ['a', 'b', 'op']
},
execute: async ({ a, b, op }) => {
const ops = { add: a + b, subtract: a - b, multiply: a * b, divide: a / b }
return JSON.stringify({ result: ops[op] })
}
})
// Register multiple tools
mcp.addTools([tool1, tool2, tool3])Tools can include annotations that describe their behavior:
mcp.addTool({
name: 'search',
description: 'Search the web',
inputSchema: {
type: 'object',
properties: { query: { type: 'string' } },
required: ['query']
},
execute: async ({ query }) => `Results for: ${query}`,
annotations: {
title: 'Web Search', // Human-readable title
readOnlyHint: true, // Doesn't modify environment (default: false)
openWorldHint: true // Interacts with external systems (default: true)
}
})
// Destructive tool example
mcp.addTool({
name: 'delete_file',
description: 'Delete a file',
inputSchema: {
type: 'object',
properties: { path: { type: 'string' } },
required: ['path']
},
execute: async ({ path }) => { /* ... */ },
annotations: {
title: 'Delete File',
readOnlyHint: false, // Modifies environment
destructiveHint: true, // May destroy data (default: true)
idempotentHint: true, // Repeated calls have same effect (default: false)
openWorldHint: false // Only affects local system
}
})| Annotation | Default | Description |
|---|---|---|
title |
— | Human-readable display name |
readOnlyHint |
false |
If true, tool doesn't modify its environment |
destructiveHint |
true |
If true, tool may destroy data (only when readOnlyHint=false) |
idempotentHint |
false |
If true, repeated calls have no extra effect (only when readOnlyHint=false) |
openWorldHint |
true |
If true, interacts with external systems |
Tools can return rich content with annotations:
mcp.addTool({
name: 'analyze',
execute: async () => [{
type: 'text',
text: 'Analysis results...',
annotations: {
audience: ['user'], // Who content is for: 'user', 'assistant', or both
priority: 0.9 // Importance: 0.0 (optional) to 1.0 (required)
}
}]
})
// Return error with content
mcp.addTool({
name: 'fetch',
execute: async () => ({
content: [{ type: 'text', text: 'Connection timeout' }],
isError: true
})
})Tools can throw MCPError with specific error codes:
import { MCPError, ErrorCode } from 'bare-mcp'
mcp.addTool({
name: 'get_user',
inputSchema: {
type: 'object',
properties: { id: { type: 'string' } },
required: ['id']
},
execute: async ({ id }) => {
const user = await db.findUser(id)
if (!user) {
throw new MCPError(
ErrorCode.INVALID_PARAMS,
`User not found: ${id}`,
{ userId: id } // Optional data field
)
}
return JSON.stringify(user)
}
})
// Custom error codes (use values > -32000)
mcp.addTool({
name: 'rate_limited_api',
execute: async () => {
throw new MCPError(-32001, 'Rate limit exceeded', { retryAfter: 60 })
}
})Standard Error Codes:
| Code | Name | Description |
|---|---|---|
| -32700 | PARSE_ERROR |
Invalid JSON |
| -32600 | INVALID_REQUEST |
Not a valid JSON-RPC request |
| -32601 | METHOD_NOT_FOUND |
Method does not exist |
| -32602 | INVALID_PARAMS |
Invalid parameters (validation, missing args) |
| -32603 | INTERNAL_ERROR |
Internal server error |
| -32002 | RESOURCE_NOT_FOUND |
Resource not found |
Error responses follow the JSON-RPC 2.0 spec:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "User not found: abc123",
"data": { "userId": "abc123" }
}
}Resources expose data that clients can read.
mcp.addResource({
uri: 'config://app',
name: 'App Configuration',
description: 'Application settings',
mimeType: 'application/json',
text: JSON.stringify({ theme: 'dark', version: '1.0' })
})mcp.addResource({
uri: 'stats://live',
name: 'Live Statistics',
mimeType: 'application/json',
read: async () => JSON.stringify({
uptime: process.uptime(),
memory: process.memoryUsage()
})
})Resources support annotations for display hints and content metadata:
mcp.addResource({
uri: 'doc://readme',
name: 'README',
title: 'Project Documentation', // Human-readable title
mimeType: 'text/markdown',
text: '# My Project',
annotations: {
audience: ['user'], // Who content is for
priority: 0.9, // Importance (0.0 to 1.0)
lastModified: '2025-01-15T10:00:00Z'
}
})Dynamic resources can return annotations per-read:
mcp.addResource({
uri: 'cache://data',
name: 'Cached Data',
read: async () => ({
text: JSON.stringify(getCachedData()),
annotations: {
lastModified: new Date().toISOString(),
audience: ['assistant']
}
})
})| Annotation | Type | Description |
|---|---|---|
audience |
string[] |
Who content is for: ["user"], ["assistant"], or ["user", "assistant"] |
priority |
number |
Importance: 0.0 (optional) to 1.0 (required) |
lastModified |
string |
ISO 8601 timestamp of last modification |
URI patterns that extract parameters:
mcp.addResourceTemplate({
uriTemplate: 'user://{id}',
name: 'User by ID',
description: 'Fetch user details',
mimeType: 'application/json',
read: async ({ id }) => {
const user = await db.getUser(id)
return JSON.stringify(user)
}
})
// Client can read: user://alice, user://bob, etc.Push updates to connected clients.
// Resource was modified
mcp.notifyResourceUpdated('stats://live')
// Resource list changed (added/removed)
mcp.notifyResourceListChanged()
// Tool list changed
mcp.notifyToolListChanged()
// Progress update for long operations
mcp.notifyProgress('upload-token', 50, 100)
// Custom notification
mcp.notify('notifications/custom', { data: 'anything' })Runtime detection is automatic — bare-mcp/http and bare-mcp/stdio use which-runtime to pick the correct implementation (Node.js or Bare) at import time. Downstream packages never need to worry about it.
The HTTP transport supports three connection modes, all served from the same server:
| Mode | Endpoint | Protocol | Direction |
|---|---|---|---|
| Streamable HTTP | POST /mcp |
JSON-RPC over HTTP | Request → Response |
| SSE | GET /sse + POST /message |
JSON-RPC over SSE | Bidirectional |
| WebSocket | ws://host:port |
JSON-RPC over WS | Bidirectional |
import { createMCPServer } from 'bare-mcp'
import { createHttpTransport } from 'bare-mcp/http'
const mcp = createMCPServer({ name: 'my-server', version: '1.0.0' })
mcp.addTool({
name: 'greet',
description: 'Say hello',
inputSchema: {
type: 'object',
properties: { name: { type: 'string' } },
required: ['name']
},
execute: async ({ name }) => `Hello, ${name}!`
})
const transport = await createHttpTransport(mcp, {
port: 3000,
host: '0.0.0.0',
websocket: true, // Enable WebSocket (default: true, Node.js only)
verbose: false, // Log requests/notifications to stderr (default: false)
onActivity: (entry) => console.log('Tool called:', entry.tool)
})| Method | Path | Description |
|---|---|---|
POST |
/mcp or / |
JSON-RPC endpoint (Streamable HTTP) |
GET |
/sse |
SSE stream (bidirectional MCP transport) |
POST |
/message?sessionId=... |
SSE message endpoint (paired with /sse) |
GET |
/health |
Health check ({ status, server, version, requestCount }) |
GET |
/activity |
Recent tool call activity log |
POST |
/activity/clear |
Clear activity log |
WS |
ws://host:port |
WebSocket (Node.js only) |
The simplest mode. Clients send a JSON-RPC request via POST and receive the response in the HTTP body. This is the transport that Cursor, Claude Code, and most modern MCP clients use.
// Client sends a request
const res = await fetch('http://localhost:3000/mcp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'initialize',
params: {
protocolVersion: '2025-03-26',
capabilities: {},
clientInfo: { name: 'my-client', version: '1.0.0' }
},
id: 1
})
})
const { result } = await res.json()
// result.serverInfo, result.capabilities, etc.
// Call a tool
const toolRes = await fetch('http://localhost:3000/mcp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/call',
params: { name: 'greet', arguments: { name: 'World' } },
id: 2
})
})
const { result: toolResult } = await toolRes.json()
// toolResult.content[0].text === 'Hello, World!'Notifications (no id field) receive a 204 No Content response:
await fetch('http://localhost:3000/mcp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'notifications/initialized'
})
})
// 204 No ContentThe MCP SSE transport is bidirectional. The client opens an SSE stream, receives a POST endpoint URL, then sends JSON-RPC requests to that URL. Responses and server notifications arrive on the SSE stream.
// 1. Open SSE connection
const events = new EventSource('http://localhost:3000/sse')
let messageEndpoint = null
// 2. Wait for the endpoint event (sent immediately on connect)
events.addEventListener('endpoint', (e) => {
messageEndpoint = e.data
// e.g. "http://localhost:3000/message?sessionId=client-1-1234567890"
})
// 3. Listen for responses and notifications on the SSE stream
events.addEventListener('message', (e) => {
const msg = JSON.parse(e.data)
if (msg.id) {
// Response to a request you sent
console.log('Response:', msg.result)
} else if (msg.method) {
// Server-initiated notification
console.log('Notification:', msg.method, msg.params)
}
})
// 4. Send JSON-RPC requests by POSTing to the endpoint
await fetch(messageEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/call',
params: { name: 'greet', arguments: { name: 'World' } },
id: 1
})
})
// HTTP response is 202 Accepted — the actual result arrives on the SSE streamFull bidirectional JSON-RPC over WebSocket. Supports subscriptions and real-time notifications.
const ws = new WebSocket('ws://localhost:3000')
ws.onopen = () => {
// Initialize
ws.send(JSON.stringify({
jsonrpc: '2.0',
method: 'initialize',
params: {
protocolVersion: '2025-03-26',
capabilities: {},
clientInfo: { name: 'ws-client', version: '1.0.0' }
},
id: 1
}))
// Call a tool
ws.send(JSON.stringify({
jsonrpc: '2.0',
method: 'tools/call',
params: { name: 'greet', arguments: { name: 'World' } },
id: 2
}))
// Subscribe to resource updates
ws.send(JSON.stringify({
jsonrpc: '2.0',
method: 'resources/subscribe',
params: { uri: 'stats://live' },
id: 3
}))
}
ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
if (msg.type === 'connected') {
// Initial connection status
console.log('Connected as:', msg.clientId)
} else if (msg.id) {
// Response to a request
console.log('Response:', msg.result)
} else if (msg.method) {
// Server notification (resource updates, progress, etc.)
console.log('Notification:', msg.method, msg.params)
}
}createHttpTransport() returns:
| Property | Type | Description |
|---|---|---|
port |
number |
Bound port |
host |
string |
Bound host |
httpServer |
http.Server |
Underlying HTTP server |
wss |
WebSocketServer | null |
WebSocket server (Node.js, if enabled) |
wsClients |
Map |
Connected WebSocket clients |
sseClients |
Map |
Connected SSE clients |
activityLog |
Array |
Recent tool call activity |
requestCount() |
function |
Returns total request count |
broadcast(msg) |
function |
Send to all clients (WS + SSE) |
close() |
async function |
Graceful shutdown |
For Claude Desktop and similar clients that communicate over stdin/stdout:
import { createMCPServer } from 'bare-mcp'
import { createStdioTransport } from 'bare-mcp/stdio'
const mcp = createMCPServer({ name: 'my-server', version: '1.0.0' })
mcp.addTool({ /* ... */ })
await createStdioTransport(mcp, {
onActivity: (entry) => console.error('Tool:', entry.tool),
onClose: () => process.exit(0)
})~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/server.js"]
}
}
}{
"mcpServers": {
"my-server": {
"url": "http://localhost:3000/mcp"
}
}
}In Cursor Settings → MCP, add the server URL:
http://localhost:3000/mcp
Cursor uses the Streamable HTTP transport (POST /mcp).
Any MCP client that supports Streamable HTTP can connect by pointing at the /mcp endpoint:
http://your-host:3000/mcp
Clients that use the legacy SSE transport should connect to:
http://your-host:3000/sse
| Method | Description |
|---|---|
initialize |
Initialize connection, get capabilities |
tools/list |
List available tools |
tools/call |
Execute a tool |
resources/list |
List available resources |
resources/templates/list |
List resource templates |
resources/read |
Read a resource by URI |
resources/subscribe |
Subscribe to resource updates |
resources/unsubscribe |
Unsubscribe from updates |
ping |
Health check |
| Method | Description |
|---|---|
notifications/resources/updated |
A resource's content changed |
notifications/resources/list_changed |
Resources added/removed |
notifications/tools/list_changed |
Tools added/removed |
notifications/progress |
Progress update |
Create an MCP server instance.
const mcp = createMCPServer({
name: 'my-server', // Server name
version: '1.0.0', // Server version
protocolVersion: '2025-11-25' // MCP protocol version
})Returns an object with:
addTool(tool)/addTools(tools[])— Register toolsaddResource(resource)/addResources(resources[])— Register resourcesaddResourceTemplate(template)— Register URI templatereadResource(uri)— Read a resourcenotify(method, params)— Send notificationnotifyResourceUpdated(uri)— Notify resource changednotifyResourceListChanged()— Notify resources added/removednotifyToolListChanged()— Notify tools added/removednotifyProgress(token, progress, total?)— Send progresshandleRequest(method, params)— Handle JSON-RPC request
Start HTTP server with WebSocket and SSE support.
const transport = await createHttpTransport(mcp, {
port: 3000,
host: '0.0.0.0',
websocket: true,
onActivity: (entry) => {}
})Returns:
port,host— Bound addresshttpServer— Node.js HTTP serverwss— WebSocket serverbroadcast(message)— Send to all clientsclose()— Shutdown server
Start stdio transport for CLI usage.
const transport = await createStdioTransport(mcp, {
onActivity: (entry) => {},
onClose: () => {}
})This library uses which-runtime to automatically detect whether you're on Node.js or Bare and load the correct transport implementation. Your code is the same either way:
import { createMCPServer } from 'bare-mcp'
import { createHttpTransport } from 'bare-mcp/http' // Auto-detects runtime
import { createStdioTransport } from 'bare-mcp/stdio' // Auto-detects runtime
const mcp = createMCPServer({ name: 'my-app' })
mcp.addTool({ /* ... */ })
await createHttpTransport(mcp, { port: 3000 })If you need to bypass runtime detection and target a specific implementation:
| Transport | Node.js | Bare |
|---|---|---|
| HTTP | bare-mcp/http-node |
bare-mcp/http-bare |
| stdio | bare-mcp/stdio-node |
bare-mcp/stdio-bare |
| Node.js | Bare | |
|---|---|---|
| HTTP | node:http + ws — Streamable HTTP, SSE, WebSocket |
bare-http1 — Streamable HTTP, SSE (no WebSocket) |
| stdio | node:readline |
Raw process.stdin/stdout |
For Node.js:
npm install bare-mcp wsFor Bare/Pear:
npm install bare-mcp bare-http1MIT