Skip to main content
Glama
CameronTofer

bare-mcp

by CameronTofer
README.md
# MCP Server Library

A minimal, general-purpose implementation of the [Model Context Protocol](https://modelcontextprotocol.io/) (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

```javascript
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.

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```json
{
  "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

```javascript
mcp.addResource({
  uri: 'config://app',
  name: 'App Configuration',
  description: 'Application settings',
  mimeType: 'application/json',
  text: JSON.stringify({ theme: 'dark', version: '1.0' })
})
```

### Dynamic Resource

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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.

```javascript
// 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`](https://github.com/nicolo-ribaudo/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

```javascript
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.

```javascript
// 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:

```javascript
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.

```javascript
// 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.

```javascript
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:

```javascript
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`:

```json
{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["/path/to/server.js"]
    }
  }
}
```

### Claude Desktop (HTTP)

```json
{
  "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.

```javascript
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.

```javascript
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.

```javascript
const transport = await createStdioTransport(mcp, {
  onActivity: (entry) => {},
  onClose: () => {}
})
```

## Bare Runtime (Pear)

This library uses [`which-runtime`](https://github.com/nicolo-ribaudo/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:

```javascript
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:
```bash
npm install bare-mcp ws
```

For Bare/Pear:
```bash
npm install bare-mcp bare-http1
```

## License

MIT