first passing test
This commit is contained in:
parent
98bf5aaf87
commit
fec2c90778
5 changed files with 165 additions and 205 deletions
|
|
@ -50,7 +50,7 @@ export function createServerInfoCheck(
|
||||||
return {
|
return {
|
||||||
id: 'server-info',
|
id: 'server-info',
|
||||||
name: 'ServerInfo',
|
name: 'ServerInfo',
|
||||||
description: 'Validates server implementation includes required name and version',
|
description: 'Test server info returned to client',
|
||||||
status,
|
status,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
specReferences: [
|
specReferences: [
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { Scenario } from '../types.js';
|
import { Scenario } from '../types.js';
|
||||||
|
import { initializeScenario } from './initialize/index.js';
|
||||||
|
|
||||||
export const scenarios = new Map<string, Scenario>();
|
export const scenarios = new Map<string, Scenario>([
|
||||||
|
['initialize', initializeScenario],
|
||||||
|
]);
|
||||||
|
|
||||||
export function registerScenario(name: string, scenario: Scenario): void {
|
export function registerScenario(name: string, scenario: Scenario): void {
|
||||||
scenarios.set(name, scenario);
|
scenarios.set(name, scenario);
|
||||||
|
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
# Initialize Scenario Implementation TODO
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The `initialize` scenario tests the MCP client initialization handshake according to the MCP specification lifecycle requirements.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Scenario Interface
|
|
||||||
|
|
||||||
Each scenario provides server setup/teardown and result retrieval:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface Scenario {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
// Start test server(s), return URLs
|
|
||||||
start(): Promise<ScenarioUrls>;
|
|
||||||
|
|
||||||
// Stop test server(s) and cleanup
|
|
||||||
stop(): Promise<void>;
|
|
||||||
|
|
||||||
// Get conformance check results from server
|
|
||||||
getChecks(): ConformanceCheck[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ScenarioUrls {
|
|
||||||
serverUrl: string; // MCP server URL (required)
|
|
||||||
authUrl?: string; // Auth server URL (optional, for auth scenarios)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Flow Control (Runner's Responsibility)
|
|
||||||
|
|
||||||
The runner orchestrates:
|
|
||||||
1. **Start Scenario**: Call `scenario.start()` to get server URL(s)
|
|
||||||
2. **Execute Client**: Spawn client process with server URL appended
|
|
||||||
3. **Capture Output**: Collect stdout/stderr from client
|
|
||||||
4. **Wait for Completion**: Wait for client to exit (or timeout)
|
|
||||||
5. **Get Results**: Call `scenario.getChecks()` to retrieve conformance checks
|
|
||||||
6. **Stop Scenario**: Call `scenario.stop()` to shutdown server(s)
|
|
||||||
7. **Write Results**: Save checks, stdout, stderr to `results/<scenario>-<timestamp>/`
|
|
||||||
|
|
||||||
### Server Responsibilities
|
|
||||||
|
|
||||||
The test server:
|
|
||||||
- Captures MCP protocol interactions
|
|
||||||
- **Runs conformance checks internally** as interactions occur
|
|
||||||
- Stores checks for retrieval via `getChecks()`
|
|
||||||
- Supports clean shutdown
|
|
||||||
|
|
||||||
## Implementation Tasks
|
|
||||||
|
|
||||||
### 1. Create Test Server (`server.ts`)
|
|
||||||
|
|
||||||
- [ ] Implement `InitializeTestServer` class with:
|
|
||||||
- `constructor()` - set up server
|
|
||||||
- `start(): Promise<number>` - start listening on random port, return port number
|
|
||||||
- `stop(): Promise<void>` - close server and connections
|
|
||||||
- `getChecks(): ConformanceCheck[]` - return conformance check results
|
|
||||||
|
|
||||||
- [ ] Server behavior:
|
|
||||||
- Accept HTTP connections (dummy for now, MCP later)
|
|
||||||
- Capture the `initialize` request from client
|
|
||||||
- **Run conformance checks immediately** using existing check functions:
|
|
||||||
- `createClientInitializationCheck(initializeRequest)`
|
|
||||||
- Store checks in internal array
|
|
||||||
- Respond with valid `initialize` response:
|
|
||||||
- `protocolVersion: "2025-06-18"`
|
|
||||||
- `serverInfo: { name: "test-server", version: "1.0.0" }`
|
|
||||||
- `capabilities: {}`
|
|
||||||
- After response, optionally run server info check:
|
|
||||||
- `createServerInfoCheck(serverInfo)` (validates we provided proper info)
|
|
||||||
|
|
||||||
### 2. Create Scenario Class (`index.ts`)
|
|
||||||
|
|
||||||
- [ ] Implement `InitializeScenario` class:
|
|
||||||
```typescript
|
|
||||||
export class InitializeScenario implements Scenario {
|
|
||||||
name = 'initialize';
|
|
||||||
description = 'Tests MCP client initialization handshake';
|
|
||||||
|
|
||||||
private server: InitializeTestServer | null = null;
|
|
||||||
|
|
||||||
async start(): Promise<ScenarioUrls> {
|
|
||||||
this.server = new InitializeTestServer();
|
|
||||||
const port = await this.server.start();
|
|
||||||
return {
|
|
||||||
serverUrl: `http://localhost:${port}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
|
||||||
if (this.server) {
|
|
||||||
await this.server.stop();
|
|
||||||
this.server = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getChecks(): ConformanceCheck[] {
|
|
||||||
if (!this.server) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return this.server.getChecks();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Export singleton instance: `export const initializeScenario = new InitializeScenario();`
|
|
||||||
|
|
||||||
### 3. Scenario Registry (`src/scenarios/index.ts`)
|
|
||||||
|
|
||||||
- [ ] Create scenario registry:
|
|
||||||
```typescript
|
|
||||||
import { initializeScenario } from './initialize/index.js';
|
|
||||||
|
|
||||||
export const scenarios = new Map<string, Scenario>([
|
|
||||||
['initialize', initializeScenario],
|
|
||||||
// future scenarios here
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function getScenario(name: string): Scenario | undefined {
|
|
||||||
return scenarios.get(name);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/scenarios/
|
|
||||||
├── index.ts (scenario registry/map)
|
|
||||||
└── initialize/
|
|
||||||
├── TODO.md (this file - delete when complete)
|
|
||||||
├── index.ts (InitializeScenario class)
|
|
||||||
└── server.ts (InitializeTestServer class)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example Usage
|
|
||||||
|
|
||||||
From runner:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { getScenario } from './scenarios/index.js';
|
|
||||||
|
|
||||||
const scenario = getScenario('initialize');
|
|
||||||
if (!scenario) {
|
|
||||||
throw new Error('Unknown scenario: initialize');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start server
|
|
||||||
const urls = await scenario.start();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Execute client (runner's responsibility)
|
|
||||||
const clientOutput = await executeClient(clientCommand, urls.serverUrl);
|
|
||||||
|
|
||||||
// Get checks from scenario
|
|
||||||
const checks = scenario.getChecks();
|
|
||||||
|
|
||||||
// Write results (runner's responsibility)
|
|
||||||
await writeResults(outputDir, { checks, ...clientOutput });
|
|
||||||
} finally {
|
|
||||||
// Stop server
|
|
||||||
await scenario.stop();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Conformance Checks
|
|
||||||
|
|
||||||
The server runs these checks (from `src/checks.ts`):
|
|
||||||
1. `createClientInitializationCheck(initializeRequest)` - validates client's initialize request
|
|
||||||
2. `createServerInfoCheck(serverInfo)` - validates server info was provided (optional)
|
|
||||||
|
|
||||||
Additional checks to consider:
|
|
||||||
- Client exit code validation (runner handles this)
|
|
||||||
- Connection established successfully
|
|
||||||
- No protocol errors
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- `http` for HTTP server (dummy for prototyping)
|
|
||||||
- Existing check functions from `src/checks.ts`
|
|
||||||
|
|
||||||
Note: The runner handles `child_process.spawn` and `fs/promises` for file writing.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
After implementation:
|
|
||||||
- [ ] Test with example client: `tsx examples/clients/typescript/test1.ts`
|
|
||||||
- [ ] Verify checks array is populated correctly
|
|
||||||
- [ ] Verify server starts and stops cleanly
|
|
||||||
- [ ] Test with broken client to ensure checks catch issues
|
|
||||||
- [ ] Use runner to execute full test flow
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- **Runner executes the client**, not the scenario
|
|
||||||
- **Scenario only manages server lifecycle** (start/stop) and provides results (getChecks)
|
|
||||||
- Server does checks internally and stores them
|
|
||||||
- Clean shutdown is critical - runner should use try/finally for scenario.stop()
|
|
||||||
- Start with dummy HTTP server, upgrade to MCP later
|
|
||||||
- For auth scenarios, return both `serverUrl` and `authUrl` from `start()`
|
|
||||||
33
src/scenarios/initialize/index.ts
Normal file
33
src/scenarios/initialize/index.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Scenario, ScenarioUrls, ConformanceCheck } from '../../types.js';
|
||||||
|
import { InitializeTestServer } from './server.js';
|
||||||
|
|
||||||
|
export class InitializeScenario implements Scenario {
|
||||||
|
name = 'initialize';
|
||||||
|
description = 'Tests MCP client initialization handshake';
|
||||||
|
|
||||||
|
private server: InitializeTestServer | null = null;
|
||||||
|
|
||||||
|
async start(): Promise<ScenarioUrls> {
|
||||||
|
this.server = new InitializeTestServer();
|
||||||
|
const port = await this.server.start();
|
||||||
|
return {
|
||||||
|
serverUrl: `http://localhost:${port}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
if (this.server) {
|
||||||
|
await this.server.stop();
|
||||||
|
this.server = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getChecks(): ConformanceCheck[] {
|
||||||
|
if (!this.server) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.server.getChecks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initializeScenario = new InitializeScenario();
|
||||||
127
src/scenarios/initialize/server.ts
Normal file
127
src/scenarios/initialize/server.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import http from 'http';
|
||||||
|
import { ConformanceCheck } from '../../types.js';
|
||||||
|
import { createClientInitializationCheck, createServerInfoCheck } from '../../checks.js';
|
||||||
|
|
||||||
|
export class InitializeTestServer {
|
||||||
|
private server: http.Server | null = null;
|
||||||
|
private checks: ConformanceCheck[] = [];
|
||||||
|
private port: number = 0;
|
||||||
|
|
||||||
|
async start(): Promise<number> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.server = http.createServer((req, res) => {
|
||||||
|
this.handleRequest(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server.on('error', reject);
|
||||||
|
|
||||||
|
this.server.listen(0, () => {
|
||||||
|
const address = this.server!.address();
|
||||||
|
if (address && typeof address === 'object') {
|
||||||
|
this.port = address.port;
|
||||||
|
resolve(this.port);
|
||||||
|
} else {
|
||||||
|
reject(new Error('Failed to get server address'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.server) {
|
||||||
|
this.server.close((err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
this.server = null;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getChecks(): ConformanceCheck[] {
|
||||||
|
return this.checks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||||
|
let body = '';
|
||||||
|
|
||||||
|
req.on('data', (chunk) => {
|
||||||
|
body += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
const request = JSON.parse(body);
|
||||||
|
|
||||||
|
if (request.method === 'initialize') {
|
||||||
|
this.handleInitialize(request, res);
|
||||||
|
} else if (request.method === 'tools/list') {
|
||||||
|
this.handleToolsList(request, res);
|
||||||
|
} else {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: request.id,
|
||||||
|
result: {}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
error: {
|
||||||
|
code: -32700,
|
||||||
|
message: 'Parse error'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleInitialize(request: any, res: http.ServerResponse): void {
|
||||||
|
const initializeRequest = request.params;
|
||||||
|
|
||||||
|
const check = createClientInitializationCheck(initializeRequest);
|
||||||
|
this.checks.push(check);
|
||||||
|
|
||||||
|
const serverInfo = {
|
||||||
|
name: 'test-server',
|
||||||
|
version: '1.0.0'
|
||||||
|
};
|
||||||
|
|
||||||
|
const serverInfoCheck = createServerInfoCheck(serverInfo);
|
||||||
|
this.checks.push(serverInfoCheck);
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: request.id,
|
||||||
|
result: {
|
||||||
|
protocolVersion: '2025-06-18',
|
||||||
|
serverInfo,
|
||||||
|
capabilities: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleToolsList(request: any, res: http.ServerResponse): void {
|
||||||
|
const response = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: request.id,
|
||||||
|
result: {
|
||||||
|
tools: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue