Skip to content

ZJONSSON/simple-mcp

Repository files navigation

simple-mcp

Tiny, streaming-first, stateless Model Context Protocol server for Node.js. No typescript, fully typed with JsDoc.

Status

Early preview (v0.0.1). APIs may change.

Why not the mainstream TypeScript SDK?

  • SSE coupling complicates multi-node deployments: The standard SDK maintains an in-memory map to match POSTs to an SSE stream, which breaks without sticky sessions.
  • Server-centric client state: Tool lists and other client-specific settings live on the server instance, making per-client variation awkward unless you run one server per connection.
  • Base64 responses require full buffering: Returning base64 forces building the entire payload in memory before responding.
  • Unnecessarily large surface area: Too much code and too many moving parts when all you need is a tight streaming pipeline.

What this library is

  • Streaming-first: A single Transform you can write JSON-RPC requests into and read responses out of. Works with stdio and HTTP.
  • Stateless by design: No transport map. Each request is self-contained. Push notifications are delivered via SSE when negotiated or handed to a notify hook you can back with pub/sub.
  • Codec-swappable: NDJSON, JSON, and SSE encoders/decoders without changing server logic.
  • No large buffers: Responses can embed Node Readable streams; text streams flow as JSON strings, binary streams are base64-encoded on the fly.

Install

npm i @zjonsson/simple-mcp

Quick taste

import { SimpleMCP, StdioTransport } from '@zjonsson/simple-mcp'

const server = new SimpleMCP({
  tools: [{
    name: 'echo',
    description: 'Echo text',
    inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
    fn: async ({ text }) => ({ content: [{ type: 'text', text }] })
  }]
});

await new StdioTransport(server).connect();

HTTP works too. If the client requests SSE, the response stays open for server-initiated notifications; otherwise, you get a notify hook to publish out-of-band.

Express example

import express from 'express'
import { SimpleMCP, HttpTransport } from '@zjonsson/simple-mcp'

const app = express()

// Important: do not attach JSON body parsers to the MCP route.
// The transport reads the raw request stream.

const server = new SimpleMCP({
  tools: [{ name: 'ping', description: 'Ping', inputSchema: { type: 'object', properties: {} }, fn: async () => ({}) }]
})

// Regular MCP endpoint (request/response)
app.post('/mcp', (req, res) => {
  const transport = new HttpTransport(server)
  transport.connect(req, res)
})

app.listen(3000)

SSE stream (client includes mcp-session-id to bind to a session). The default notify emits on HttpTransport.events using the sessionId as the event name. This route subscribes to that channel and writes SSE frames to the client. For distributed systems, override notify to publish to your central pub/sub, and have subscribers re-emit to HttpTransport.events using the sessionId as the event name. (The server will also set an mcp-session-id response header upon initialize.)

app.get('/mcp/sse', (req, res) => {
  const sessionId = String(req.headers['mcp-session-id'] || '')
  if (!sessionId) return res.status(400).send('Missing mcp-session-id header')

  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache, no-transform',
    Connection: 'keep-alive'
  })

  let id = 0
  const listener = (message) => {
    res.write(`id: ${id++}\nevent: message\ndata: ${JSON.stringify(message)}\n\n`)
  }
  HttpTransport.events.on(sessionId, listener)
  req.once('close', () => HttpTransport.events.removeListener(sessionId, listener))
})

Core ideas

  • One abstraction: Transport extends Node Transform in object mode. Feed requests in, get responses out, with backpressure.
  • Negotiated over HTTP: Content-Type/Accept select JSON, NDJSON, or SSE. Stdio defaults to NDJSON.
  • Notifications:
    • Incoming notification/* from clients don’t produce a body.
    • Outgoing notifications go to SSE when available, or to your notify hook for pub/sub fanout.

When to use this

  • You want a small, composable MCP server that scales horizontally without sticky sessions.
  • You need to stream big outputs without buffering the whole response.
  • You prefer standard Node streams and minimal surface area.

About

Simple mcp-server using node-streams

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published