Skip to content

Commit 43fbf60

Browse files
authored
Conformance Tests for SEP-2322 MRTR (#188)
1 parent 513abdb commit 43fbf60

10 files changed

Lines changed: 3507 additions & 3 deletions

File tree

examples/clients/typescript/everything-client.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,149 @@ registerScenario(
726726
runEnterpriseManagedAuthorization
727727
);
728728

729+
// ============================================================================
730+
// MRTR client conformance (SEP-2322)
731+
// ============================================================================
732+
733+
async function runMRTRClient(serverUrl: string): Promise<void> {
734+
let nextId = 1;
735+
736+
async function sendRpc(
737+
method: string,
738+
params?: Record<string, unknown>
739+
): Promise<{
740+
id: number;
741+
result?: Record<string, unknown>;
742+
error?: { code: number; message: string };
743+
}> {
744+
const id = nextId++;
745+
const body: Record<string, unknown> = {
746+
jsonrpc: '2.0',
747+
id,
748+
method
749+
};
750+
if (params) body.params = params;
751+
752+
const resp = await fetch(serverUrl, {
753+
method: 'POST',
754+
headers: { 'Content-Type': 'application/json' },
755+
body: JSON.stringify(body)
756+
});
757+
758+
if (resp.status === 204) return { id, result: {} };
759+
return (await resp.json()) as {
760+
id: number;
761+
result?: Record<string, unknown>;
762+
error?: { code: number; message: string };
763+
};
764+
}
765+
766+
// List tools
767+
const toolsResp = await sendRpc('tools/list');
768+
const tools =
769+
(toolsResp.result as { tools: Array<{ name: string }> })?.tools ?? [];
770+
logger.debug(
771+
'Available tools:',
772+
tools.map((t) => t.name)
773+
);
774+
775+
// Tool 1: test_mrtr_echo_state — call, get InputRequiredResult with requestState, retry
776+
const r1 = await sendRpc('tools/call', {
777+
name: 'test_mrtr_echo_state',
778+
arguments: {}
779+
});
780+
781+
const r1Result = r1.result as Record<string, unknown> | undefined;
782+
if (r1Result?.resultType === 'input_required') {
783+
const inputRequests = r1Result.inputRequests as Record<string, unknown>;
784+
const requestState = r1Result.requestState as string | undefined;
785+
786+
// Build inputResponses by fulfilling each inputRequest
787+
const inputResponses: Record<string, unknown> = {};
788+
for (const [key, req] of Object.entries(inputRequests)) {
789+
const request = req as { method: string; params: unknown };
790+
if (request.method === 'elicitation/create') {
791+
inputResponses[key] = {
792+
action: 'accept',
793+
content: { confirmed: true }
794+
};
795+
}
796+
}
797+
798+
// Call an unrelated tool BEFORE retrying — must NOT carry over inputResponses/requestState
799+
await sendRpc('tools/call', {
800+
name: 'test_mrtr_unrelated',
801+
arguments: {}
802+
});
803+
logger.debug(
804+
'test_mrtr_unrelated: called without MRTR state (isolation check)'
805+
);
806+
807+
// Retry with inputResponses + requestState echoed back unchanged
808+
const retryParams: Record<string, unknown> = {
809+
name: 'test_mrtr_echo_state',
810+
arguments: {},
811+
inputResponses
812+
};
813+
if (requestState !== undefined) {
814+
retryParams.requestState = requestState;
815+
}
816+
817+
await sendRpc('tools/call', retryParams);
818+
logger.debug('test_mrtr_echo_state: MRTR flow completed');
819+
}
820+
821+
// Tool 2: test_mrtr_no_state — call, get InputRequiredResult WITHOUT requestState, retry without it
822+
const r2 = await sendRpc('tools/call', {
823+
name: 'test_mrtr_no_state',
824+
arguments: {}
825+
});
826+
827+
const r2Result = r2.result as Record<string, unknown> | undefined;
828+
if (r2Result?.resultType === 'input_required') {
829+
const inputRequests = r2Result.inputRequests as Record<string, unknown>;
830+
831+
// Build inputResponses
832+
const inputResponses: Record<string, unknown> = {};
833+
for (const [key, req] of Object.entries(inputRequests)) {
834+
const request = req as { method: string; params: unknown };
835+
if (request.method === 'elicitation/create') {
836+
inputResponses[key] = {
837+
action: 'accept',
838+
content: { confirmed: true }
839+
};
840+
}
841+
}
842+
843+
// Retry WITHOUT requestState (server didn't send one)
844+
await sendRpc('tools/call', {
845+
name: 'test_mrtr_no_state',
846+
arguments: {},
847+
inputResponses
848+
});
849+
logger.debug('test_mrtr_no_state: MRTR flow completed');
850+
}
851+
852+
// Tool 3: test_mrtr_no_result_type — returns result without resultType field
853+
// Client must treat it as complete (default) and NOT retry
854+
const r3 = await sendRpc('tools/call', {
855+
name: 'test_mrtr_no_result_type',
856+
arguments: {}
857+
});
858+
859+
const r3Result = r3.result as Record<string, unknown> | undefined;
860+
if (r3Result && !r3Result.resultType) {
861+
// No resultType means default to "complete" — do nothing, don't retry
862+
logger.debug(
863+
'test_mrtr_no_result_type: result has no resultType, treating as complete'
864+
);
865+
}
866+
867+
logger.debug('MRTR client scenario completed');
868+
}
869+
870+
registerScenario('sep-2322-client-request-state', runMRTRClient);
871+
729872
// ============================================================================
730873
// Main entry point
731874
// ============================================================================

0 commit comments

Comments
 (0)