Skip to content

CameronTofer/bare-mcp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 Content

SSE 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 stream

WebSocket (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/mcp

Cursor 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/mcp

Clients that use the legacy SSE transport should connect to:

http://your-host:3000/sse

MCP 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 tools
  • addResource(resource) / addResources(resources[]) — Register resources
  • addResourceTemplate(template) — Register URI template
  • readResource(uri) — Read a resource
  • notify(method, params) — Send notification
  • notifyResourceUpdated(uri) — Notify resource changed
  • notifyResourceListChanged() — Notify resources added/removed
  • notifyToolListChanged() — Notify tools added/removed
  • notifyProgress(token, progress, total?) — Send progress
  • handleRequest(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 address
  • httpServer — Node.js HTTP server
  • wss — WebSocket server
  • broadcast(message) — Send to all clients
  • close() — 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 ws

For Bare/Pear:

npm install bare-mcp bare-http1

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published