Skip to content

Commit 8644ff4

Browse files
feat: add message middleware support for Protocol
Add a middleware pattern that allows transforming JSON-RPC messages before sending and after receiving. This provides a clean way to extend protocol messages (e.g., adding custom capabilities to initialize requests) without needing to subclass or override protocol methods. Middleware functions receive a JSON-RPC message and return a (possibly transformed) message. Both sync and async middleware are supported. Usage example: const client = new Client({ name: 'my-client', version: '1.0.0' }, { sendMiddleware: [ (message) => { // Transform outgoing message if (isJSONRPCRequest(message) && message.method === 'initialize') { // Add custom capabilities } return message; } ], receiveMiddleware: [ (message) => { // Transform incoming message return message; } ] }); Changes: - Add MessageMiddleware type and JSONRPCMessageLike type alias - Add sendMiddleware and receiveMiddleware to ProtocolOptions - Apply middleware in connect() for incoming messages - Apply middleware in request() and notification() for outgoing messages
1 parent 2edbe8c commit 8644ff4

1 file changed

Lines changed: 96 additions & 26 deletions

File tree

packages/core/src/shared/protocol.ts

Lines changed: 96 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ import type { Transport, TransportSendOptions } from './transport.js';
6060
*/
6161
export type ProgressCallback = (progress: Progress) => void;
6262

63+
/**
64+
* A JSON-RPC message that can be transformed by middleware.
65+
*/
66+
export type JSONRPCMessageLike = JSONRPCRequest | JSONRPCNotification | JSONRPCResponse | JSONRPCResultResponse | JSONRPCErrorResponse;
67+
68+
/**
69+
* Middleware function for transforming JSON-RPC messages.
70+
* Can be sync (returns message) or async (returns Promise<message>).
71+
*/
72+
export type MessageMiddleware = (message: JSONRPCMessageLike) => JSONRPCMessageLike | Promise<JSONRPCMessageLike>;
73+
6374
/**
6475
* Additional initialization options.
6576
*/
@@ -102,6 +113,16 @@ export type ProtocolOptions = {
102113
* appropriately (e.g., by failing the task, dropping messages, etc.).
103114
*/
104115
maxTaskQueueSize?: number;
116+
/**
117+
* Middleware functions to apply to outgoing messages before sending.
118+
* Middleware is applied in order, with each function receiving the output of the previous.
119+
*/
120+
sendMiddleware?: MessageMiddleware[];
121+
/**
122+
* Middleware functions to apply to incoming messages after receiving.
123+
* Middleware is applied in order, with each function receiving the output of the previous.
124+
*/
125+
receiveMiddleware?: MessageMiddleware[];
105126
};
106127

107128
/**
@@ -603,6 +624,25 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
603624
}
604625
}
605626

627+
/**
628+
* Applies a list of middleware functions to a message.
629+
* Middleware is applied in order, with each function receiving the output of the previous.
630+
*/
631+
private async _applyMiddleware<T extends JSONRPCMessageLike>(
632+
message: T,
633+
middlewareList: MessageMiddleware[] | undefined
634+
): Promise<T> {
635+
if (!middlewareList || middlewareList.length === 0) {
636+
return message;
637+
}
638+
639+
let result: JSONRPCMessageLike = message;
640+
for (const middleware of middlewareList) {
641+
result = await middleware(result);
642+
}
643+
return result as T;
644+
}
645+
606646
/**
607647
* Attaches to the given transport, starts it, and starts listening for messages.
608648
*
@@ -625,15 +665,21 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
625665
const _onmessage = this._transport?.onmessage;
626666
this._transport.onmessage = (message, extra) => {
627667
_onmessage?.(message, extra);
628-
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
629-
this._onresponse(message);
630-
} else if (isJSONRPCRequest(message)) {
631-
this._onrequest(message, extra);
632-
} else if (isJSONRPCNotification(message)) {
633-
this._onnotification(message);
634-
} else {
635-
this._onerror(new Error(`Unknown message type: ${JSON.stringify(message)}`));
636-
}
668+
669+
// Apply receive middleware and then route the message
670+
this._applyMiddleware(message, this._options?.receiveMiddleware)
671+
.then(transformedMessage => {
672+
if (isJSONRPCResultResponse(transformedMessage) || isJSONRPCErrorResponse(transformedMessage)) {
673+
this._onresponse(transformedMessage);
674+
} else if (isJSONRPCRequest(transformedMessage)) {
675+
this._onrequest(transformedMessage, extra);
676+
} else if (isJSONRPCNotification(transformedMessage)) {
677+
this._onnotification(transformedMessage);
678+
} else {
679+
this._onerror(new Error(`Unknown message type: ${JSON.stringify(transformedMessage)}`));
680+
}
681+
})
682+
.catch(error => this._onerror(new Error(`Receive middleware error: ${error}`)));
637683
};
638684

639685
await this._transport.start();
@@ -1211,23 +1257,38 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
12111257
};
12121258
this._requestResolvers.set(messageId, responseResolver);
12131259

1214-
this._enqueueTaskMessage(relatedTaskId, {
1215-
type: 'request',
1216-
message: jsonrpcRequest,
1217-
timestamp: Date.now()
1218-
}).catch(error => {
1219-
this._cleanupTimeout(messageId);
1220-
reject(error);
1221-
});
1260+
// Apply send middleware before queuing
1261+
this._applyMiddleware(jsonrpcRequest, this._options?.sendMiddleware)
1262+
.then(transformedRequest => {
1263+
this._enqueueTaskMessage(relatedTaskId, {
1264+
type: 'request',
1265+
message: transformedRequest as JSONRPCRequest,
1266+
timestamp: Date.now()
1267+
}).catch(error => {
1268+
this._cleanupTimeout(messageId);
1269+
reject(error);
1270+
});
1271+
})
1272+
.catch(error => {
1273+
this._cleanupTimeout(messageId);
1274+
reject(error);
1275+
});
12221276

12231277
// Don't send through transport - queued messages are delivered via tasks/result only
12241278
// This prevents duplicate delivery for bidirectional transports
12251279
} else {
1226-
// No related task - send through transport normally
1227-
this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => {
1228-
this._cleanupTimeout(messageId);
1229-
reject(error);
1230-
});
1280+
// No related task - apply send middleware and send through transport normally
1281+
this._applyMiddleware(jsonrpcRequest, this._options?.sendMiddleware)
1282+
.then(transformedRequest => {
1283+
this._transport!.send(transformedRequest as JSONRPCRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => {
1284+
this._cleanupTimeout(messageId);
1285+
reject(error);
1286+
});
1287+
})
1288+
.catch(error => {
1289+
this._cleanupTimeout(messageId);
1290+
reject(error);
1291+
});
12311292
}
12321293
});
12331294
}
@@ -1302,9 +1363,12 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
13021363
}
13031364
};
13041365

1366+
// Apply send middleware before queuing
1367+
const transformedNotification = await this._applyMiddleware(jsonrpcNotification, this._options?.sendMiddleware);
1368+
13051369
await this._enqueueTaskMessage(relatedTaskId, {
13061370
type: 'notification',
1307-
message: jsonrpcNotification,
1371+
message: transformedNotification as JSONRPCNotification,
13081372
timestamp: Date.now()
13091373
});
13101374

@@ -1358,9 +1422,13 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
13581422
};
13591423
}
13601424

1361-
// Send the notification, but don't await it here to avoid blocking.
1425+
// Apply send middleware and send the notification.
13621426
// Handle potential errors with a .catch().
1363-
this._transport?.send(jsonrpcNotification, options).catch(error => this._onerror(error));
1427+
this._applyMiddleware(jsonrpcNotification, this._options?.sendMiddleware)
1428+
.then(transformedNotification => {
1429+
this._transport?.send(transformedNotification as JSONRPCNotification, options).catch(error => this._onerror(error));
1430+
})
1431+
.catch(error => this._onerror(error));
13641432
});
13651433

13661434
// Return immediately.
@@ -1386,7 +1454,9 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
13861454
};
13871455
}
13881456

1389-
await this._transport.send(jsonrpcNotification, options);
1457+
// Apply send middleware before sending
1458+
const transformedNotification = await this._applyMiddleware(jsonrpcNotification, this._options?.sendMiddleware);
1459+
await this._transport.send(transformedNotification as JSONRPCNotification, options);
13901460
}
13911461

13921462
/**

0 commit comments

Comments
 (0)