npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fib-mcp

v1.3.0

Published

MCP (Model Context Protocol) client/server for fibjs with stdio/http/sse/ws transport

Readme

fib-mcp

MCP (Model Context Protocol) SDK for the fibjs ecosystem.

McpServer and McpClient extend @modelcontextprotocol/sdk directly. fib-mcp adds fibjs-native server transports (sse, ws, http), a fibjs-native client transport for sse, and handler methods for mounting server transports into your own http.Server.

TypeScript runs directly on fibjs; no compile step is required.

Features

  • McpServer extends sdk.McpServer — all SDK methods available as-is
  • McpClient extends sdk.Client — all SDK methods available as-is
  • fibjs-native server transports: sse, ws, http
  • client transports: SDK ws, SDK Streamable HTTP, fibjs-native sse
  • HTTP client uses the SDK Streamable HTTP transport
  • SSE automatic endpoint discovery (standard MCP SSE protocol)
  • Designed to be mounted into user-managed http.Server routes

Installation

fibjs --install fib-mcp

Quick Start

Server - HTTP

import http from 'http';
import { McpServer } from 'fib-mcp';

const server = new McpServer({ name: 'demo-server', version: '1.0.0' });

server.tool('ping', {}, async () => ({
  content: [{ type: 'text', text: 'pong' }],
}));

const svr = new http.Server(3000, {
  '/mcp': server.httpHandler(),
});
svr.start();

Client — HTTP

import { McpClient } from 'fib-mcp';

const client = new McpClient({ name: 'demo-client', version: '1.0.0' });
await client.connect({ transport: 'streamable-http', url: 'http://127.0.0.1:3000/mcp' });

const { tools } = await client.listTools();
const result = await client.callTool({ name: 'ping', arguments: {} });
console.log(result.content[0].text);

await client.close();

Server — WebSocket

import http from 'http';
import { McpServer } from 'fib-mcp';

const server = new McpServer({ name: 'demo-server', version: '1.0.0' });

server.tool('ping', {}, async () => ({
  content: [{ type: 'text', text: 'pong' }],
}));

const svr = new http.Server(3000, {
  '/mcp': server.wsHandler(),
});
svr.start();

Client — WebSocket

import { McpClient } from 'fib-mcp';

const client = new McpClient({ name: 'demo-client', version: '1.0.0' });
await client.connect({ transport: 'ws', url: 'ws://127.0.0.1:3000/mcp' });

const result = await client.callTool({ name: 'ping', arguments: {} });
await client.close();

Server — SSE

import http from 'http';
import { McpServer } from 'fib-mcp';

const server = new McpServer({ name: 'demo-server', version: '1.0.0' });

server.tool('ping', {}, async () => ({
  content: [{ type: 'text', text: 'pong' }],
}));

const svr = new http.Server(3000, {
  '/mcp': server.sseHandlers(),
});
svr.start();

Client — SSE

import { McpClient } from 'fib-mcp';

const client = new McpClient({ name: 'demo-client', version: '1.0.0' });

// messageUrl auto-discovered from server's `endpoint` event:
await client.connect({ transport: 'sse', url: 'http://127.0.0.1:3000/mcp/sse' });

// Or with an explicit messageUrl:
// await client.connect({ transport: 'sse', url: 'http://127.0.0.1:3000/mcp/sse', messageUrl: 'http://127.0.0.1:3000/mcp/message' });

const result = await client.callTool({ name: 'ping', arguments: {} });
await client.close();

Server — stdio

import { McpServer } from 'fib-mcp';

const server = new McpServer({ name: 'demo-server', version: '1.0.0' });

server.tool('ping', {}, async () => ({
  content: [{ type: 'text', text: 'pong' }],
}));

await server.listenStdio();

Client — stdio

import { McpClient } from 'fib-mcp';

const client = new McpClient({ name: 'demo-client', version: '1.0.0' });

// Spawn an MCP server script via stdio:
await client.connect({ transport: 'stdio', path: './my_mcp_server.ts' });

// Or connect to an explicit command:
// await client.connect({ transport: 'stdio', command: 'fibjs', args: ['my_mcp_server.ts'] });

const result = await client.callTool({ name: 'ping', arguments: {} });
await client.close();

API Reference

McpServer

Extends @modelcontextprotocol/sdk McpServer. All SDK methods (tool, resource, prompt, registerTool, connect, close, etc.) are inherited unchanged.

fib-mcp adds the following fibjs-native transport methods:

tool vs registerTool

Both are valid, and both register MCP tools.

  • tool(...): high-level helper API; concise and preferred for most examples.
  • registerTool(...): lower-level SDK API; useful when you want explicit control over registration shape (for example, full schema objects and custom handler wiring).

Why the repository has both styles:

  • README quick examples use tool(...) for readability.
  • Some production paths use registerTool(...) because those handlers need the lower-level control surface.

This is expected behavior, not a protocol mismatch.

Note: a single McpServer instance should bind only one network transport (ws, sse, or http). If you need multiple network protocols, create separate McpServer instances.

listenStdio(): Promise<void>

Connect to stdio. Used when the server is spawned by an MCP host.

wsHandler(): Handler

Returns a fibjs WebSocket upgrade handler for use in a route map.

const svr = new http.Server(3000, { '/mcp': server.wsHandler() });

sseHandlers(): Record<string, Handler>

Returns SSE route handlers for nested fibjs routing (SSE GET + POST message endpoint).

const svr = new http.Server(3000, { '/mcp': server.sseHandlers() });

The server automatically sends an endpoint event on connect, so clients can discover the POST URL without it being pre-configured.

httpHandler(options?): Handler

Returns an HTTP POST handler for mounting at a route chosen by your outer router.

const svr = new http.Server(3000, { '/mcp': server.httpHandler() });

Options:

  • timeoutMs?: request timeout in ms (default: 30000)

httpHandlers(options?): Record<string, Handler>

Returns a flat fibjs route map for JSON-RPC over HTTP POST.

const svr = new http.Server(3000, server.httpHandlers({ path: '/mcp' }));

Options:

  • path?: route path (default: /mcp)
  • timeoutMs?: request timeout in ms (default: 30000)

McpClient

Extends @modelcontextprotocol/sdk Client. All SDK methods (callTool, listTools, readResource, listResources, getPrompt, listPrompts, listResourceTemplates, ping, complete, connect, close, etc.) are inherited unchanged with identical signatures and return types.

fib-mcp adds the following transport connection methods:

connect(config | transport): Promise<void>

Unified client entry point.

Use transport descriptor objects aligned with MCP Registry transport style:

  • { transport: 'streamable-http', url, options? }
  • { transport: 'sse', url, messageUrl?, options? }
  • { transport: 'ws' | 'websocket', url }
  • { transport: 'stdio', path, options? }
  • { transport: 'stdio', command, args?, options? }

Passing a transport object still works and is forwarded to the SDK connect(transport).

SSE notes:

If messageUrl is omitted, the client waits for the server's endpoint SSE event and discovers the POST URL automatically (standard MCP SSE protocol).

Options:

  • headers?: extra request headers
  • method?: POST method override (default: POST)

Transport Notes

| Transport | Client | Server | |-----------|--------|--------| | stdio | SDK | SDK | | http | SDK Streamable HTTP | fibjs-native | | sse | fibjs-native | fibjs-native | | ws | SDK | fibjs-native |

Bidirectional Session

BidirectionalSession provides transport-agnostic bidirectional MCP over one connection:

  • Forward calls: local side calls peer tools
  • Reverse calls: peer calls local tools through ctx.client
  • Session-scoped capability negotiation for reverse channel
  • Backward compatible with plain MCP clients
  • Hides internal server implementation details from the public API

Constructor (New API)

BidirectionalSession now uses a single options object.

import { BidirectionalSession } from 'fib-mcp';

const session = new BidirectionalSession({
  serverInfo: { name: 'my-server', version: '1.0.0' },
  clientInfo: { name: 'my-client', version: '1.0.0' },
  clientOptions: {},
  serverOptions: {},
});

Options:

  • serverInfo (required): local server identity
  • clientInfo (optional): local client identity, default bidirectional-client/1.0.0
  • clientOptions (optional): forwarded to internal McpClient
  • serverOptions (optional): forwarded to internal McpServer

Tool Callback Context

BidirectionalSession.tool(...) is the bidirectional wrapper and is the recommended path for reverse-call handlers. Internally it delegates to the underlying MCP server tool registration and injects ctx.client.

If you need MCP standard registration APIs, call them directly on BidirectionalSession (registerTool, registerResource, registerPrompt, etc.).

session.tool('server.proxy', {}, async (_args, ctx) => {
  const nested = await ctx.client.callTool({ name: 'peer.echo', arguments: {} });
  return {
    content: [{ type: 'text', text: nested.content[0].text }],
  };
});

Handler context:

  • ctx.client: peer client bound to current session
  • ctx.extra: MCP request metadata (includes sessionId)

Connection APIs

WebSocket convenience:

  • wsHandler() for server route mounting
  • connect({ transport: 'ws', url }) for active side over websocket

Stdio convenience:

  • connect({ transport: 'stdio', path, options? }) for active side stdio script launch
  • connect({ transport: 'stdio', command, args?, options? }) for active side stdio command launch
  • listenStdio() for passive side stdio accept

Generic transport:

  • connect(config) or connect(transport) active side
  • accept(transport) passive side

Both return BidirectionalConnection:

  • connection.sessionId
  • connection.callTool(...)
  • connection.listTools(...)
  • connection.readResource(...)
  • connection.listResources(...)
  • connection.listPrompts(...)
  • connection.getPrompt(...)
  • connection.close()

Note: internal client/server instances are intentionally hidden from the public API.

Forwarding Gateway

ForwardingGateway is a client/app/server gateway that proxies MCP over WebSocket:

  • client -> gateway: normal MCP over WebSocket (any MCP client — browser, CLI, etc.)
  • gateway -> server: raw JSON-RPC request / notification relay over one bidirectional session
  • server -> gateway: reverse MCP handled locally by app tools through ReverseMcpEndpoint

Default behavior:

  • client-side initialize is terminated locally by the gateway
  • client → server requests are forwarded as raw JSON-RPC requests
  • client → server notifications are forwarded as raw JSON-RPC notifications
  • server → client notifications are forwarded by the gateway default path
  • server → gateway reverse MCP calls use gateway-local tools via ctx.client

Minimal shape:

import http from 'http';
import { ForwardingGateway } from 'fib-mcp';

const gateway = new ForwardingGateway({
  appInfo: { name: 'app-gateway', version: '1.0.0' },
  connectServer: async () => ({ transport: 'ws', url: 'ws://127.0.0.1:9001/mcp' }),
});

gateway.tool('app.greet', {}, async () => ({
  content: [{ type: 'text', text: 'hello-from-app' }],
}));

const svr = new http.Server(3000, {
  '/mcp': gateway.wsHandler(),
});
svr.start();

Hooks — Middleware API

All three hooks use the same (ctx, next) middleware signature:

  • Call next() — execute the default behavior (forward the original message)
  • Call next(modifiedMessage) — execute the default behavior with a rewritten message
  • Return without calling next() — suppress the default behavior entirely
  • Throw an error — send a JSON-RPC error response (attach a numeric .code property)

onClientRequest(ctx, next)

Intercepts JSON-RPC requests arriving from the downstream MCP client.

ctx fields:

  • ctx.session — current session state (includes serverConnection, authContext, etc.)
  • ctx.message — the raw JSON-RPC request message
  • ctx.reply(result) — respond locally without forwarding; alternative to next()
const gateway = new ForwardingGateway({
  appInfo: { name: 'app-gateway', version: '1.0.0' },
  connectServer: async () => ({ transport: 'ws', url: 'ws://127.0.0.1:9001/mcp' }),
  onClientRequest: async (ctx, next) => {
    // Auth check: only allow certain methods
    if ((ctx.message as any).method === 'tools/call') {
      const allowed = checkPermission(ctx.session.authContext);
      if (!allowed) {
        const err: any = new Error('forbidden');
        err.code = -32001;
        throw err;
      }
    }
    return next(); // continue with default forwarding
  },
});

Rewrite a request before forwarding:

onClientRequest: async (ctx, next) => {
  const msg = ctx.message as any;
  if (msg.method === 'agent.session.open') {
    return next({
      ...ctx.message,
      params: { ...msg.params, agentId: resolveAgentId(ctx.session.authContext) },
    } as any);
  }
  return next();
},

Respond locally without touching the upstream server:

onClientRequest: async (ctx, next) => {
  if ((ctx.message as any).method === 'local.ping') {
    return ctx.reply({ pong: true });
  }
  return next();
},

onClientNotification(ctx, next)

Intercepts JSON-RPC notifications arriving from the downstream MCP client.

onClientNotification: async (ctx, next) => {
  // log and pass through
  console.log('client notification:', (ctx.message as any).method);
  return next();
},

Suppress forwarding by not calling next():

onClientNotification: async (_ctx, _next) => {
  // drop all client notifications
},

onServerNotification(ctx, next)

Intercepts JSON-RPC notifications arriving from the upstream MCP server.

onServerNotification: async (ctx, next) => {
  // Inject an extra field before forwarding to the client
  const msg = ctx.message as any;
  return next({
    ...ctx.message,
    params: { ...msg.params, _gatewayId: 'my-app' },
  } as any);
},

onClientDisconnect(session)

Called when a downstream client disconnects. Use for cleanup (e.g. releasing local state keyed on session.clientSessionId).

onClientDisconnect: (session) => {
  releaseResources(session.clientSessionId);
},

Authentication

Use authenticate to validate the connection and attach context:

const gateway = new ForwardingGateway({
  appInfo: { name: 'app-gateway', version: '1.0.0' },
  authenticate: async ({ request, initializeRequest }) => {
    const token = request?.headers?.['authorization'];
    const user = await verifyToken(token);
    if (!user) throw new Error('unauthorized');
    return { user };
  },
  connectServer: async ({ authContext }) => ({
    transport: 'ws',
    url: `ws://127.0.0.1:9001/mcp`,
    headers: { 'x-user-id': authContext.user.id },
  }),
});

The resolved authContext is available on ctx.session.authContext in all hook callbacks.

Options Reference

| Option | Type | Description | |--------|------|-------------| | appInfo | ClientInfo | Gateway identity (name, version) | | clientCapabilities | object? | Capabilities advertised to downstream clients | | instructions | string? | Instructions string sent in initialize response | | authenticate | fn? | Validate connection; return value becomes session.authContext | | connectServer | fn? | Return transport config for the upstream server connection | | onClientRequest | fn? | Middleware for client → server requests | | onClientNotification | fn? | Middleware for client → server notifications | | onServerNotification | fn? | Middleware for server → client notifications | | onClientDisconnect | fn? | Called when a client session ends | | serverClientInfo | ClientInfo? | Identity used for the outbound server connection | | serverClientOptions | object? | Options forwarded to the outbound client | | reverseServerOptions | object? | Options for the local reverse-MCP server endpoint |

WebSocket Example

import http from 'http';
import { BidirectionalSession } from 'fib-mcp';

const accepted = new BidirectionalSession({
  serverInfo: { name: 'accepted-server', version: '1.0.0' },
  clientInfo: { name: 'accepted-client', version: '1.0.0' },
});

accepted.tool('server.ping', {}, async () => ({
  content: [{ type: 'text', text: 'pong-from-accepted' }],
}));

const host = new http.Server(3000, {
  '/mcp': accepted.wsHandler(),
});
host.start();

const peer = new BidirectionalSession({
  serverInfo: { name: 'peer-server', version: '1.0.0' },
  clientInfo: { name: 'peer-client', version: '1.0.0' },
});

const conn = await peer.connect({ transport: 'ws', url: 'ws://127.0.0.1:3000/mcp' });
const pong = await conn.callTool({ name: 'server.ping', arguments: {} });
console.log(pong.content[0].text);

Stdio Example

import { BidirectionalSession } from 'fib-mcp';

const parent = new BidirectionalSession({
  serverInfo: { name: 'parent-server', version: '1.0.0' },
  clientInfo: { name: 'parent-client', version: '1.0.0' },
});

parent.tool('parent.greet', {}, async () => ({
  content: [{ type: 'text', text: 'hello-from-parent' }],
}));

const conn = await parent.connect({ transport: 'stdio', command: 'fibjs', args: ['./child.ts'] });
const echo = await conn.callTool({ name: 'child.echo', arguments: {} });
console.log(echo.content[0].text);

In-Memory Example

import { BidirectionalSession } from 'fib-mcp';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';

const left = new BidirectionalSession({
  serverInfo: { name: 'left-server', version: '1.0.0' },
  clientInfo: { name: 'left-client', version: '1.0.0' },
});

const right = new BidirectionalSession({
  serverInfo: { name: 'right-server', version: '1.0.0' },
  clientInfo: { name: 'right-client', version: '1.0.0' },
});

const [leftTransport, rightTransport] = InMemoryTransport.createLinkedPair();
const leftConn = await left.connect(leftTransport);
const rightConn = await right.accept(rightTransport);

Backward Compatibility

Plain MCP clients are supported:

  • Forward calls work as normal
  • Reverse calls are blocked if peer does not advertise reverse capability
  • Mixed plain and bidirectional clients can coexist on the same server

Transport Contract

Custom transport should implement SDK Transport behavior:

  • start()
  • send(message, options?)
  • close()
  • onmessage(message, extra?)
  • onerror(error)
  • onclose()

Notifications

Notification flow works on both normal MCP transports and BidirectionalSession.

Testing

fibjs test/all.test.ts
fibjs --test test/integration_test.ts
fibjs --test test/edge_cases_test.ts
fibjs --test test/bidirectional_provider_test.ts