bare-mcp
v1.1.0
Published
Minimal, general-purpose MCP (Model Context Protocol) server library
Downloads
265
Maintainers
Readme
MCP Server Library
A minimal, general-purpose implementation of the Model Context Protocol (MCP).
Works on Node.js and Bare runtime (Pear/Holepunch).
Features
- 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
Quick Start
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
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])Tool Annotations
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 |
Tool Results with Annotations
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
})
})Error Handling
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
Resources expose data that clients can read.
Static Resource
mcp.addResource({
uri: 'config://app',
name: 'App Configuration',
description: 'Application settings',
mimeType: 'application/json',
text: JSON.stringify({ theme: 'dark', version: '1.0' })
})Dynamic Resource
mcp.addResource({
uri: 'stats://live',
name: 'Live Statistics',
mimeType: 'application/json',
read: async () => JSON.stringify({
uptime: process.uptime(),
memory: process.memoryUsage()
})
})Resource Annotations
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 |
Resource Templates
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.Notifications
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' })Transports
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.
HTTP Transport
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 |
Starting the Server
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)
})HTTP Endpoints
| 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) |
Streamable HTTP (Recommended)
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 ContentSSE Transport (Legacy)
The 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 streamWebSocket (Node.js only)
Full 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)
}
}Transport Return Object
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 |
stdio Transport
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)
})Configuring MCP Clients
Claude Desktop (stdio)
~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/server.js"]
}
}
}Claude Desktop (HTTP)
{
"mcpServers": {
"my-server": {
"url": "http://localhost:3000/mcp"
}
}
}Cursor (HTTP)
In Cursor Settings → MCP, add the server URL:
http://localhost:3000/mcpCursor uses the Streamable HTTP transport (POST /mcp).
Generic MCP Client (HTTP)
Any MCP client that supports Streamable HTTP can connect by pointing at the /mcp endpoint:
http://your-host:3000/mcpClients that use the legacy SSE transport should connect to:
http://your-host:3000/sseMCP Methods
| 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 |
Notification Types
| 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 |
API Reference
createMCPServer(options)
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
createHttpTransport(mcp, options)
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
createStdioTransport(mcp, options)
Start stdio transport for CLI usage.
const transport = await createStdioTransport(mcp, {
onActivity: (entry) => {},
onClose: () => {}
})Bare Runtime (Pear)
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 })Explicit Imports
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 |
Transport Differences
| | 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 |
Dependencies
For Node.js:
npm install bare-mcp wsFor Bare/Pear:
npm install bare-mcp bare-http1License
MIT
