Skip to content
52 changes: 50 additions & 2 deletions src/scenarios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ import {

import { DNSRebindingProtectionScenario } from './server/dns-rebinding';

// IncompleteResult scenarios from (SEP-2322)
import {
IncompleteResultBasicElicitationScenario,
IncompleteResultBasicSamplingScenario,
IncompleteResultBasicListRootsScenario,
IncompleteResultRequestStateScenario,
IncompleteResultMultipleInputRequestsScenario,
IncompleteResultMultiRoundScenario,
IncompleteResultMissingInputResponseScenario,
IncompleteResultNonToolRequestScenario
} from './server/incomplete-result';

import {
IncompleteResultTaskBasicScenario,
IncompleteResultTaskBadInputResponseScenario,
IncompleteResultTaskInputResponseIncompleteScenario
} from './server/incomplete-result-tasks';

import {
authScenariosList,
backcompatScenariosList,
Expand All @@ -69,7 +87,22 @@ const pendingClientScenariosList: ClientScenario[] = [

// On hold until server-side SSE improvements are made
// https://github.com/modelcontextprotocol/typescript-sdk/pull/1129
new ServerSSEPollingScenario()
new ServerSSEPollingScenario(),

// IncompleteResult scenarios (SEP-2322) — pending until a conformance test
// server implements IncompleteResult tools. These are draft spec scenarios
// intended to be run via `--spec-version draft` against capable servers.
new IncompleteResultBasicElicitationScenario(),
new IncompleteResultBasicSamplingScenario(),
new IncompleteResultBasicListRootsScenario(),
new IncompleteResultRequestStateScenario(),
new IncompleteResultMultipleInputRequestsScenario(),
new IncompleteResultMultiRoundScenario(),
new IncompleteResultMissingInputResponseScenario(),
new IncompleteResultNonToolRequestScenario(),
new IncompleteResultTaskBasicScenario(),
new IncompleteResultTaskBadInputResponseScenario(),
new IncompleteResultTaskInputResponseIncompleteScenario()
];

// All client scenarios
Expand Down Expand Up @@ -124,7 +157,22 @@ const allClientScenariosList: ClientScenario[] = [
new PromptsGetWithImageScenario(),

// Security scenarios
new DNSRebindingProtectionScenario()
new DNSRebindingProtectionScenario(),

// IncompleteResult scenarios (SEP-2322)
new IncompleteResultBasicElicitationScenario(),
new IncompleteResultBasicSamplingScenario(),
new IncompleteResultBasicListRootsScenario(),
new IncompleteResultRequestStateScenario(),
new IncompleteResultMultipleInputRequestsScenario(),
new IncompleteResultMultiRoundScenario(),
new IncompleteResultMissingInputResponseScenario(),
new IncompleteResultNonToolRequestScenario(),

// IncompleteResult Task scenarios (SEP-2322)
new IncompleteResultTaskBasicScenario(),
new IncompleteResultTaskBadInputResponseScenario(),
new IncompleteResultTaskInputResponseIncompleteScenario()
];

// Active client scenarios (excludes pending)
Expand Down
139 changes: 136 additions & 3 deletions src/scenarios/server/client-helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
/**
* Helper utilities for creating MCP clients to test servers
* Helper utilities for creating MCP clients to test servers.
*
* Provides two connection modes:
* 1. SDK-based (connectToServer) — uses the MCP TypeScript SDK for standard
* protocol operations.
* 2. Raw JSON-RPC (RawMcpSession) — uses undici HTTP for draft/experimental
* features that the SDK does not yet support.
*
* Both modes share the same SDK-based initialize handshake and session ID.
*/

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
Expand All @@ -8,14 +16,27 @@ import {
LoggingMessageNotificationSchema,
ProgressNotificationSchema
} from '@modelcontextprotocol/sdk/types.js';
import { request } from 'undici';

// ─── JSON-RPC Types ──────────────────────────────────────────────────────────

export interface JsonRpcResponse {
jsonrpc: '2.0';
id: number;
result?: Record<string, unknown>;
error?: { code: number; message: string; data?: unknown };
}

// ─── SDK-based Connection ────────────────────────────────────────────────────

export interface MCPClientConnection {
client: Client;
transport: StreamableHTTPClientTransport;
close: () => Promise<void>;
}

/**
* Create and connect an MCP client to a server
* Create and connect an MCP client to a server using the SDK.
*/
export async function connectToServer(
serverUrl: string
Expand All @@ -40,15 +61,127 @@ export async function connectToServer(

return {
client,
transport,
close: async () => {
await client.close();
}
};
}

// ─── Raw JSON-RPC Session ────────────────────────────────────────────────────

/**
* A raw MCP session for testing draft/experimental protocol features that the
* SDK does not yet support. Uses the SDK for the standard initialize handshake,
* then sends raw JSON-RPC over HTTP via undici for subsequent requests.
*
* Usage:
* const session = await createRawSession(serverUrl);
* const response = await session.send('tools/call', { name: 'my-tool', arguments: {} });
*/
export class RawMcpSession {
private nextId = 1;
private serverUrl: string;
private connection: MCPClientConnection | null = null;

constructor(serverUrl: string) {
this.serverUrl = serverUrl;
}

/**
* Initialize the MCP session using the SDK's connectToServer(),
* then extract the session ID for subsequent raw requests.
*/
async initialize(): Promise<void> {
this.connection = await connectToServer(this.serverUrl);
}

/**
* Send a JSON-RPC request via raw HTTP.
* Automatically manages session ID and auto-incrementing JSON-RPC IDs.
* Handles both JSON and SSE response formats.
*/
async send(
method: string,
params?: Record<string, unknown>
): Promise<JsonRpcResponse> {
const id = this.nextId++;

const headers: Record<string, string> = {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream'
};

const body = JSON.stringify({
jsonrpc: '2.0',
id,
method,
params
});

const response = await request(this.serverUrl, {
method: 'POST',
headers,
body
});

const contentType = response.headers['content-type'] ?? '';

// Handle SSE responses — parse the last JSON-RPC message from the stream
// Not doing proper handling of SSE here since none of the MRTR features under test currently require it.
// This can be expanded if necessary for new features.
if (contentType.includes('text/event-stream')) {
const text = await response.body.text();
return parseSseResponse(text);
}

// Handle direct JSON responses
return (await response.body.json()) as JsonRpcResponse;
}

/**
* Close the underlying SDK connection.
*/
async close(): Promise<void> {
if (this.connection) {
await this.connection.close();
this.connection = null;
}
}
}

/**
* Create an initialized raw MCP session ready for testing.
*/
export async function createRawSession(
serverUrl: string
): Promise<RawMcpSession> {
const session = new RawMcpSession(serverUrl);
await session.initialize();
return session;
}

/**
* Helper to collect notifications (logging and progress)
* Parse the last JSON-RPC message from an SSE response body.
*/
export function parseSseResponse(sseText: string): JsonRpcResponse {
const lines = sseText.split('\n');
let lastData: string | null = null;

for (const line of lines) {
if (line.startsWith('data: ')) {
lastData = line.slice(6);
}
}

if (!lastData) {
throw new Error('No data found in SSE stream');
}

return JSON.parse(lastData) as JsonRpcResponse;
}

// ─── Notification Collector ──────────────────────────────────────────────────
export class NotificationCollector {
private loggingNotifications: any[] = [];
private progressNotifications: any[] = [];
Expand Down
117 changes: 117 additions & 0 deletions src/scenarios/server/incomplete-result-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Helpers for SEP-2322 conformance tests.
*
* Uses RawMcpSession from client-helper.ts for connection management and
* raw JSON-RPC transport. This file adds IncompleteResult-specific type
* guards and mock response builders.
*/

import { RawMcpSession, JsonRpcResponse } from './client-helper';

export type { RawMcpSession, JsonRpcResponse };

// ─── IncompleteResult Types ──────────────────────────────────────────────────

export interface IncompleteResult {
result_type?: 'incomplete';
inputRequests?: Record<string, InputRequestObject>;
requestState?: string;
_meta?: Record<string, unknown>;
[key: string]: unknown;
}

export interface InputRequestObject {
method: string;
params?: Record<string, unknown>;
}

// ─── Type Guards ─────────────────────────────────────────────────────────────

/**
* Check if a JSON-RPC result is an IncompleteResult.
*/
export function isIncompleteResult(
result: Record<string, unknown> | undefined
): result is IncompleteResult {
if (!result) return false;
if (result.result_type === 'incomplete') return true;
// Also detect by presence of IncompleteResult fields
return 'inputRequests' in result || 'requestState' in result;
}

/**
* Check if a JSON-RPC result is a complete result (not incomplete).
* complete is the default so if result_type is missing we assume it's complete.
*/
export function isCompleteResult(
result: Record<string, unknown> | undefined
): boolean {
if (!result) return false;
if (result.result_type === 'complete') return true;
if (!('result_type' in result)) return true;
return !isIncompleteResult(result);
}

/**
* Extract inputRequests from an IncompleteResult.
*/
export function getInputRequests(
result: IncompleteResult
): Record<string, InputRequestObject> | undefined {
return result.inputRequests;
}

// ─── Mock Response Builders ──────────────────────────────────────────────────

/**
* Build a mock elicitation response (ElicitResult).
*/
export function mockElicitResponse(
content: Record<string, unknown>
): Record<string, unknown> {
return {
action: 'accept',
content
};
}

/**
* Build a mock sampling response (CreateMessageResult).
*/
export function mockSamplingResponse(text: string): Record<string, unknown> {
return {
role: 'assistant',
content: {
type: 'text',
text
},
model: 'test-model',
stopReason: 'endTurn'
};
}

/**
* Build a mock list roots response (ListRootsResult).
*/
export function mockListRootsResponse(): Record<string, unknown> {
return {
roots: [
{
uri: 'file:///test/root',
name: 'Test Root'
}
]
};
}

// ─── Spec References ─────────────────────────────────────────────────────────

/**
* SEP reference for IncompleteResult / MRTR tests.
*/
export const MRTR_SPEC_REFERENCES = [
{
id: 'SEP-2322',
url: 'https://github.com/modelcontextprotocol/specification/pull/2322'
}
];
Loading
Loading