Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions src/scenarios/server/lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { ServerInitializeScenario } from './lifecycle';
import { connectToServer } from './client-helper';

vi.mock('./client-helper', () => ({
connectToServer: vi.fn()
}));

describe('ServerInitializeScenario', () => {
const serverUrl = 'http://localhost:3000/mcp';
const closeMock = vi.fn().mockResolvedValue(undefined);
const fetchMock = vi.fn();

beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal('fetch', fetchMock);
vi.mocked(connectToServer).mockResolvedValue({
client: {} as any,
close: closeMock
});
});

afterEach(() => {
vi.unstubAllGlobals();
});

it('returns INFO when the server does not provide an MCP-Session-Id header', async () => {
fetchMock.mockResolvedValue(new Response(null));

const checks = await new ServerInitializeScenario().run(serverUrl);

expect(connectToServer).toHaveBeenCalledWith(serverUrl);
expect(closeMock).toHaveBeenCalled();
expect(fetchMock).toHaveBeenCalledWith(
serverUrl,
expect.objectContaining({
method: 'POST'
})
);

expect(checks).toHaveLength(2);
expect(checks[0]?.id).toBe('server-initialize');
expect(checks[0]?.status).toBe('SUCCESS');
expect(checks[1]).toMatchObject({
id: 'server-session-id-visible-ascii',
status: 'INFO',
details: {
message:
'Server did not provide an MCP-Session-Id header (session ID is optional)'
}
});
});

it('returns SUCCESS when the server provides a visible ASCII session ID', async () => {
fetchMock.mockResolvedValue(
new Response(null, {
headers: {
'mcp-session-id': 'session-123_ABC'
}
})
);

const checks = await new ServerInitializeScenario().run(serverUrl);

expect(checks[1]).toMatchObject({
id: 'server-session-id-visible-ascii',
status: 'SUCCESS',
details: {
sessionId: 'session-123_ABC'
}
});
});

it('returns FAILURE when the server provides a non-ASCII session ID', async () => {
fetchMock.mockResolvedValue(
new Response(null, {
headers: {
'mcp-session-id': 'session-123-é'
}
})
);

const checks = await new ServerInitializeScenario().run(serverUrl);

expect(checks[1]).toMatchObject({
id: 'server-session-id-visible-ascii',
status: 'FAILURE',
errorMessage:
'Session ID contains characters outside visible ASCII range (0x21-0x7E)',
details: {
sessionId: 'session-123-é'
}
});
});
});
98 changes: 97 additions & 1 deletion src/scenarios/server/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types';
import { connectToServer } from './client-helper';

const VISIBLE_ASCII_REGEX = /^[\x21-\x7E]+$/;

const SESSION_SPEC_REFERENCES = [
{
id: 'MCP-Session-Management',
url: 'https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management'
}
];

export class ServerInitializeScenario implements ClientScenario {
name = 'server-initialize';
specVersions: SpecVersion[] = ['2025-06-18', '2025-11-25'];
Expand All @@ -18,8 +27,10 @@ export class ServerInitializeScenario implements ClientScenario {
- Accept \`initialize\` request with client info and capabilities
- Return valid initialize response with server info, protocol version, and capabilities
- Accept \`initialized\` notification from client after handshake
- If a session ID is assigned, it MUST only contain visible ASCII characters (0x21 to 0x7E)

This test verifies the server can complete the two-phase initialization handshake successfully.`;
This test verifies the server can complete the two-phase initialization handshake successfully,
and validates session ID format if one is assigned.`;

async run(serverUrl: string): Promise<ConformanceCheck[]> {
const checks: ConformanceCheck[] = [];
Expand Down Expand Up @@ -65,6 +76,91 @@ This test verifies the server can complete the two-phase initialization handshak
}
]
});
return checks;
}

// Check: Session ID visible ASCII validation
// Use a raw fetch to inspect the MCP-Session-Id response header,
// since the SDK client transport does not expose it.
try {
const response = await fetch(serverUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream',
'mcp-protocol-version': '2025-11-25'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2025-11-25',
capabilities: {},
clientInfo: {
name: 'conformance-session-id-test',
version: '1.0.0'
}
}
})
});

const sessionId = response.headers.get('mcp-session-id');

if (!sessionId) {
checks.push({
id: 'server-session-id-visible-ascii',
name: 'ServerSessionIdVisibleAscii',
description:
'Server-provided session ID uses only visible ASCII characters',
status: 'INFO',
timestamp: new Date().toISOString(),
specReferences: SESSION_SPEC_REFERENCES,
details: {
message:
'Server did not provide an MCP-Session-Id header (session ID is optional)'
}
});
} else if (VISIBLE_ASCII_REGEX.test(sessionId)) {
checks.push({
id: 'server-session-id-visible-ascii',
name: 'ServerSessionIdVisibleAscii',
description:
'Server-provided session ID uses only visible ASCII characters',
status: 'SUCCESS',
timestamp: new Date().toISOString(),
specReferences: SESSION_SPEC_REFERENCES,
details: {
sessionId
}
});
} else {
checks.push({
id: 'server-session-id-visible-ascii',
name: 'ServerSessionIdVisibleAscii',
description:
'Server-provided session ID uses only visible ASCII characters',
status: 'FAILURE',
timestamp: new Date().toISOString(),
errorMessage:
'Session ID contains characters outside visible ASCII range (0x21-0x7E)',
specReferences: SESSION_SPEC_REFERENCES,
details: {
sessionId
}
});
}
} catch (error) {
checks.push({
id: 'server-session-id-visible-ascii',
name: 'ServerSessionIdVisibleAscii',
description:
'Server-provided session ID uses only visible ASCII characters',
status: 'FAILURE',
timestamp: new Date().toISOString(),
errorMessage: `Failed to send initialize request for session ID check: ${error instanceof Error ? error.message : String(error)}`,
specReferences: SESSION_SPEC_REFERENCES
});
}

return checks;
Expand Down
Loading