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.
McpServer extends sdk.McpServer— all SDK methods available as-isMcpClient extends sdk.Client— all SDK methods available as-is- fibjs-native server transports:
sse,ws,http - client transports: SDK
ws, SDK Streamable HTTP, fibjs-nativesse - HTTP client uses the SDK Streamable HTTP transport
- SSE automatic endpoint discovery (standard MCP SSE protocol)
- Designed to be mounted into user-managed
http.Serverroutes
fibjs --install fib-mcpimport 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();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();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();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();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();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();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();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();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:
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.
Connect to stdio. Used when the server is spawned by an MCP host.
Returns a fibjs WebSocket upgrade handler for use in a route map.
const svr = new http.Server(3000, { '/mcp': server.wsHandler() });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.
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)
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)
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:
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 headersmethod?: POST method override (default:POST)
| Transport | Client | Server |
|---|---|---|
| stdio | SDK | SDK |
| http | SDK Streamable HTTP | fibjs-native |
| sse | fibjs-native | fibjs-native |
| ws | SDK | fibjs-native |
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
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 identityclientInfo(optional): local client identity, defaultbidirectional-client/1.0.0clientOptions(optional): forwarded to internalMcpClientserverOptions(optional): forwarded to internalMcpServer
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 sessionctx.extra: MCP request metadata (includessessionId)
WebSocket convenience:
wsHandler()for server route mountingconnect({ transport: 'ws', url })for active side over websocket
Stdio convenience:
connect({ transport: 'stdio', path, options? })for active side stdio script launchconnect({ transport: 'stdio', command, args?, options? })for active side stdio command launchlistenStdio()for passive side stdio accept
Generic transport:
connect(config)orconnect(transport)active sideaccept(transport)passive side
Both return BidirectionalConnection:
connection.sessionIdconnection.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.
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 sessionserver -> gateway: reverse MCP handled locally by app tools throughReverseMcpEndpoint
Default behavior:
- client-side
initializeis 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();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
.codeproperty)
Intercepts JSON-RPC requests arriving from the downstream MCP client.
ctx fields:
ctx.session— current session state (includesserverConnection,authContext, etc.)ctx.message— the raw JSON-RPC request messagectx.reply(result)— respond locally without forwarding; alternative tonext()
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();
},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
},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);
},Called when a downstream client disconnects. Use for cleanup (e.g. releasing local state keyed on session.clientSessionId).
onClientDisconnect: (session) => {
releaseResources(session.clientSessionId);
},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.
| 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 |
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);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);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);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
Custom transport should implement SDK Transport behavior:
start()send(message, options?)close()onmessage(message, extra?)onerror(error)onclose()
Notification flow works on both normal MCP transports and BidirectionalSession.
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