Skip to content

Commit 8d81019

Browse files
fix(client): allowlist inputRequest methods serviced by MRTR loop (sampling/elicitation/roots/ping only)
1 parent 324c93b commit 8d81019

2 files changed

Lines changed: 17 additions & 5 deletions

File tree

packages/client/src/client/client.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@ export type ClientOptions = ProtocolOptions & {
202202

203203
/**
204204
* SEP-2322: maximum number of incomplete-result rounds before failing. Each round
205-
* services the server's `inputRequests` via local handlers and re-sends with
205+
* services the server's `inputRequests` via local handlers (sampling, elicitation,
206+
* roots, ping only; other methods are rejected) and re-sends with
206207
* `params.{inputResponses, requestState}`. Prevents unbounded looping on a server
207208
* that returns `incomplete` forever.
208209
*
@@ -214,9 +215,18 @@ export type ClientOptions = ProtocolOptions & {
214215
const DEFAULT_MRTR_MAX_ROUNDS = 16;
215216

216217
/** SEP-2322 client-side detection. The server signals it cannot complete without client input. */
218+
/**
219+
* Methods the SEP-2322 retry loop will service from `inputRequests`. The server cannot
220+
* use this channel to invoke arbitrary client handlers (e.g. a custom-method handler the
221+
* application registered for unrelated purposes); only the spec-defined client-side
222+
* request methods are dispatched.
223+
*/
224+
const ALLOWED_INPUT_REQUEST_METHODS: ReadonlySet<string> = new Set(['sampling/createMessage', 'elicitation/create', 'roots/list', 'ping']);
225+
217226
function isIncompleteResult(r: unknown): r is IncompleteResult {
218227
if (typeof r !== 'object' || r === null) return false;
219228
const o = r as { resultType?: unknown; inputRequests?: unknown };
229+
// (deliberately narrow guard; the loop body validates method against ALLOWED_INPUT_REQUEST_METHODS)
220230
if (o.resultType !== 'incomplete') return false;
221231
if (o.inputRequests === undefined) return true;
222232
return typeof o.inputRequests === 'object' && o.inputRequests !== null && !Array.isArray(o.inputRequests);
@@ -1176,6 +1186,12 @@ export class Client extends Protocol<ClientContext> {
11761186
const out: Record<string, unknown> = {};
11771187
for (const [key, ir] of Object.entries(reqs)) {
11781188
signal?.throwIfAborted();
1189+
if (!ALLOWED_INPUT_REQUEST_METHODS.has(ir.method)) {
1190+
throw new ProtocolError(
1191+
ProtocolErrorCode.InvalidRequest,
1192+
`inputRequest method '${ir.method}' is not a client-serviceable method`
1193+
);
1194+
}
11791195
const resp = await this._serviceInboundRequest(
11801196
{ jsonrpc: '2.0', id: `mrtr:${key}`, method: ir.method, params: ir.params },
11811197
signal

packages/core/src/exports/public/index.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,6 @@ export { DEFAULT_REQUEST_TIMEOUT_MSEC } from '../../shared/protocol.js';
5555
// are exported via the `export * from types/types.js` below)
5656
export { RequiresInput } from '../../shared/dispatcher.js';
5757

58-
// SEP-2322 multi-round-trip request types (IncompleteResult/InputRequest/InputResponseRequestParams
59-
// are exported via the `export * from types/types.js` below)
60-
export { RequiresInput } from '../../shared/dispatcher.js';
61-
6258
// Task manager types (NOT TaskManager class itself — internal)
6359
export type { RequestTaskStore, TaskContext, TaskManagerOptions, TaskRequestOptions } from '../../shared/taskManager.js';
6460

0 commit comments

Comments
 (0)