adjust prettier to use more defaults (#8)
* simplify prettier to use more defaults * apply new prettier
This commit is contained in:
parent
05df535e40
commit
b377251b15
33 changed files with 12352 additions and 12153 deletions
36
.github/workflows/ci.yml
vendored
36
.github/workflows/ci.yml
vendored
|
|
@ -1,27 +1,27 @@
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
- run: npm run lint
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
- run: npm run lint
|
||||
|
|
|
|||
|
|
@ -1,20 +1,12 @@
|
|||
{
|
||||
"printWidth": 140,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"proseWrap": "always",
|
||||
"arrowParens": "avoid",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "**/*.md",
|
||||
"options": {
|
||||
"printWidth": 280
|
||||
}
|
||||
}
|
||||
]
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "**/*.md",
|
||||
"options": {
|
||||
"printWidth": 280
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,9 +56,9 @@ Results are saved to `results/<scenario>-<timestamp>/`:
|
|||
## Available Scenarios
|
||||
|
||||
- **initialize** - Tests MCP client initialization handshake
|
||||
- Validates protocol version
|
||||
- Validates clientInfo (name and version)
|
||||
- Validates server response handling
|
||||
- Validates protocol version
|
||||
- Validates clientInfo (name and version)
|
||||
- Validates server response handling
|
||||
|
||||
## Architecture
|
||||
|
||||
|
|
|
|||
|
|
@ -83,14 +83,14 @@ Your server MUST declare these capabilities during initialization:
|
|||
|
||||
```json
|
||||
{
|
||||
"protocolVersion": "2025-06-18",
|
||||
"serverInfo": {
|
||||
"name": "mcp-conformance-test-server",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"capabilities": {
|
||||
/* as above */
|
||||
}
|
||||
"protocolVersion": "2025-06-18",
|
||||
"serverInfo": {
|
||||
"name": "mcp-conformance-test-server",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"capabilities": {
|
||||
/* as above */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -116,9 +116,9 @@ Your server MUST declare these capabilities during initialization:
|
|||
|
||||
- Return array of all available tools
|
||||
- Each tool MUST have:
|
||||
- `name` (string)
|
||||
- `description` (string)
|
||||
- `inputSchema` (valid JSON Schema object)
|
||||
- `name` (string)
|
||||
- `description` (string)
|
||||
- `inputSchema` (valid JSON Schema object)
|
||||
|
||||
### 2.2. Call Tool
|
||||
|
||||
|
|
@ -143,7 +143,7 @@ Implement these tools with exact names:
|
|||
|
||||
```typescript
|
||||
{
|
||||
content: [{ type: 'text', text: 'This is a simple text response for testing.' }];
|
||||
content: [{ type: 'text', text: 'This is a simple text response for testing.' }];
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -155,13 +155,13 @@ Implement these tools with exact names:
|
|||
|
||||
```typescript
|
||||
{
|
||||
content: [
|
||||
{
|
||||
type: 'image',
|
||||
data: '<base64-encoded-png>',
|
||||
mimeType: 'image/png'
|
||||
}
|
||||
];
|
||||
content: [
|
||||
{
|
||||
type: 'image',
|
||||
data: '<base64-encoded-png>',
|
||||
mimeType: 'image/png'
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -175,13 +175,13 @@ Implement these tools with exact names:
|
|||
|
||||
```typescript
|
||||
{
|
||||
content: [
|
||||
{
|
||||
type: 'audio',
|
||||
data: '<base64-encoded-wav>',
|
||||
mimeType: 'audio/wav'
|
||||
}
|
||||
];
|
||||
content: [
|
||||
{
|
||||
type: 'audio',
|
||||
data: '<base64-encoded-wav>',
|
||||
mimeType: 'audio/wav'
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -195,16 +195,16 @@ Implement these tools with exact names:
|
|||
|
||||
```typescript
|
||||
{
|
||||
content: [
|
||||
{
|
||||
type: 'resource',
|
||||
resource: {
|
||||
uri: 'test://embedded-resource',
|
||||
mimeType: 'text/plain',
|
||||
text: 'This is an embedded resource content.'
|
||||
}
|
||||
}
|
||||
];
|
||||
content: [
|
||||
{
|
||||
type: 'resource',
|
||||
resource: {
|
||||
uri: 'test://embedded-resource',
|
||||
mimeType: 'text/plain',
|
||||
text: 'This is an embedded resource content.'
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -216,18 +216,18 @@ Implement these tools with exact names:
|
|||
|
||||
```typescript
|
||||
{
|
||||
content: [
|
||||
{ type: 'text', text: 'Multiple content types test:' },
|
||||
{ type: 'image', data: '<base64>', mimeType: 'image/png' },
|
||||
{
|
||||
type: 'resource',
|
||||
resource: {
|
||||
uri: 'test://mixed-content-resource',
|
||||
mimeType: 'application/json',
|
||||
text: '{"test":"data","value":123}'
|
||||
}
|
||||
}
|
||||
];
|
||||
content: [
|
||||
{ type: 'text', text: 'Multiple content types test:' },
|
||||
{ type: 'image', data: '<base64>', mimeType: 'image/png' },
|
||||
{
|
||||
type: 'resource',
|
||||
resource: {
|
||||
uri: 'test://mixed-content-resource',
|
||||
mimeType: 'application/json',
|
||||
text: '{"test":"data","value":123}'
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -284,10 +284,10 @@ If no progress token provided, just execute with delays.
|
|||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": -32000,
|
||||
"message": "This tool intentionally returns an error for testing"
|
||||
}
|
||||
"error": {
|
||||
"code": -32000,
|
||||
"message": "This tool intentionally returns an error for testing"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -388,10 +388,10 @@ If no progress token provided, just execute with delays.
|
|||
- Return array of all available **direct resources** (not templates)
|
||||
- Support optional cursor-based pagination (see Section 6.1)
|
||||
- Each resource MUST have:
|
||||
- `uri` (string)
|
||||
- `name` (string)
|
||||
- `description` (string)
|
||||
- `mimeType` (string, optional)
|
||||
- `uri` (string)
|
||||
- `name` (string)
|
||||
- `description` (string)
|
||||
- `mimeType` (string, optional)
|
||||
|
||||
**Note**: Resource templates are listed via separate `resources/templates/list` endpoint (see 3.1a)
|
||||
|
||||
|
|
@ -404,10 +404,10 @@ If no progress token provided, just execute with delays.
|
|||
- Return array of all available **resource templates**
|
||||
- Support optional cursor-based pagination (see Section 6.1)
|
||||
- Each template MUST have:
|
||||
- `uriTemplate` (string) - RFC 6570 URI template
|
||||
- `name` (string)
|
||||
- `description` (string)
|
||||
- `mimeType` (string, optional)
|
||||
- `uriTemplate` (string) - RFC 6570 URI template
|
||||
- `name` (string)
|
||||
- `description` (string)
|
||||
- `mimeType` (string, optional)
|
||||
|
||||
### 3.2. Read Resource
|
||||
|
||||
|
|
@ -443,13 +443,13 @@ Implement these resources with exact URIs:
|
|||
|
||||
```typescript
|
||||
{
|
||||
contents: [
|
||||
{
|
||||
uri: 'test://static-text',
|
||||
mimeType: 'text/plain',
|
||||
text: 'This is the content of the static text resource.'
|
||||
}
|
||||
];
|
||||
contents: [
|
||||
{
|
||||
uri: 'test://static-text',
|
||||
mimeType: 'text/plain',
|
||||
text: 'This is the content of the static text resource.'
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -472,13 +472,13 @@ Implement these resources with exact URIs:
|
|||
|
||||
```typescript
|
||||
{
|
||||
contents: [
|
||||
{
|
||||
uri: 'test://static-binary',
|
||||
mimeType: 'image/png',
|
||||
blob: '<base64-encoded-png>'
|
||||
}
|
||||
];
|
||||
contents: [
|
||||
{
|
||||
uri: 'test://static-binary',
|
||||
mimeType: 'image/png',
|
||||
blob: '<base64-encoded-png>'
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -503,13 +503,13 @@ Implement these resources with exact URIs:
|
|||
|
||||
```typescript
|
||||
{
|
||||
contents: [
|
||||
{
|
||||
uri: 'test://template/123/data',
|
||||
mimeType: 'application/json',
|
||||
text: '{"id":"123","templateTest":true,"data":"Data for ID: 123"}'
|
||||
}
|
||||
];
|
||||
contents: [
|
||||
{
|
||||
uri: 'test://template/123/data',
|
||||
mimeType: 'application/json',
|
||||
text: '{"id":"123","templateTest":true,"data":"Data for ID: 123"}'
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -534,13 +534,13 @@ Implement these resources with exact URIs:
|
|||
|
||||
```typescript
|
||||
{
|
||||
contents: [
|
||||
{
|
||||
uri: 'test://watched-resource',
|
||||
mimeType: 'text/plain',
|
||||
text: 'Watched resource content'
|
||||
}
|
||||
];
|
||||
contents: [
|
||||
{
|
||||
uri: 'test://watched-resource',
|
||||
mimeType: 'text/plain',
|
||||
text: 'Watched resource content'
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -559,10 +559,10 @@ Implement these resources with exact URIs:
|
|||
|
||||
```json
|
||||
{
|
||||
"method": "resources/subscribe",
|
||||
"params": {
|
||||
"uri": "test://watched-resource"
|
||||
}
|
||||
"method": "resources/subscribe",
|
||||
"params": {
|
||||
"uri": "test://watched-resource"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -587,9 +587,9 @@ Implement these resources with exact URIs:
|
|||
|
||||
- Return array of all available prompts
|
||||
- Each prompt MUST have:
|
||||
- `name` (string)
|
||||
- `description` (string)
|
||||
- `arguments` (array, optional) - list of required arguments
|
||||
- `name` (string)
|
||||
- `description` (string)
|
||||
- `arguments` (array, optional) - list of required arguments
|
||||
|
||||
### 4.2. Get Prompt
|
||||
|
||||
|
|
@ -614,15 +614,15 @@ Implement these prompts with exact names:
|
|||
|
||||
```typescript
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: 'This is a simple prompt for testing.'
|
||||
}
|
||||
}
|
||||
];
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: 'This is a simple prompt for testing.'
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -650,15 +650,15 @@ Implement these prompts with exact names:
|
|||
|
||||
```typescript
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: "Prompt with arguments: arg1='hello', arg2='world'"
|
||||
}
|
||||
}
|
||||
];
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: "Prompt with arguments: arg1='hello', arg2='world'"
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -672,26 +672,26 @@ Implement these prompts with exact names:
|
|||
|
||||
```typescript
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'resource',
|
||||
resource: {
|
||||
uri: '<resourceUri from arguments>',
|
||||
mimeType: 'text/plain',
|
||||
text: 'Embedded resource content for testing.'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: 'Please process the embedded resource above.'
|
||||
}
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'resource',
|
||||
resource: {
|
||||
uri: '<resourceUri from arguments>',
|
||||
mimeType: 'text/plain',
|
||||
text: 'Embedded resource content for testing.'
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: 'Please process the embedded resource above.'
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -703,23 +703,23 @@ Implement these prompts with exact names:
|
|||
|
||||
```typescript
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'image',
|
||||
data: '<base64-encoded-png>',
|
||||
mimeType: 'image/png'
|
||||
}
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: 'Please analyze the image above.'
|
||||
}
|
||||
}
|
||||
];
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'image',
|
||||
data: '<base64-encoded-png>',
|
||||
mimeType: 'image/png'
|
||||
}
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: 'Please analyze the image above.'
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -756,9 +756,9 @@ Implement these prompts with exact names:
|
|||
|
||||
- Send log notifications during operations
|
||||
- Each log MUST have:
|
||||
- `level` (string) - one of the log levels above
|
||||
- `data` (any) - log message or structured data
|
||||
- `logger` (string, optional) - logger name
|
||||
- `level` (string) - one of the log levels above
|
||||
- `data` (any) - log message or structured data
|
||||
- `logger` (string, optional) - logger name
|
||||
|
||||
**Example**:
|
||||
|
||||
|
|
@ -777,13 +777,13 @@ Implement these prompts with exact names:
|
|||
|
||||
```typescript
|
||||
mcpServer.server
|
||||
.notification({
|
||||
method: 'notifications/message',
|
||||
params: { level, logger, data }
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore error if no client is connected
|
||||
});
|
||||
.notification({
|
||||
method: 'notifications/message',
|
||||
params: { level, logger, data }
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore error if no client is connected
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -5,16 +5,19 @@ import tseslint from 'typescript-eslint';
|
|||
import eslintConfigPrettier from 'eslint-config-prettier/flat';
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: false
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-explicit-any': 'off'
|
||||
}
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: false
|
||||
},
|
||||
eslintConfigPrettier
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_' }
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'off'
|
||||
}
|
||||
},
|
||||
eslintConfigPrettier
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,55 +3,55 @@
|
|||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const serverUrl = process.argv[2];
|
||||
const serverUrl = process.argv[2];
|
||||
|
||||
if (!serverUrl) {
|
||||
console.error('Usage: test-broken-client <server-url>');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!serverUrl) {
|
||||
console.error('Usage: test-broken-client <server-url>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Connecting to MCP server at: ${serverUrl}`);
|
||||
console.log(`Connecting to MCP server at: ${serverUrl}`);
|
||||
|
||||
try {
|
||||
const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
|
||||
try {
|
||||
const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
|
||||
|
||||
await transport.start();
|
||||
await transport.start();
|
||||
|
||||
const initRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2025-06-18',
|
||||
clientInfo: {
|
||||
version: '1.0.0'
|
||||
},
|
||||
capabilities: {}
|
||||
}
|
||||
};
|
||||
const initRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2025-06-18',
|
||||
clientInfo: {
|
||||
version: '1.0.0'
|
||||
},
|
||||
capabilities: {}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch(serverUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(initRequest)
|
||||
});
|
||||
const response = await fetch(serverUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(initRequest)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('✅ Received response:', JSON.stringify(result, null, 2));
|
||||
const result = await response.json();
|
||||
console.log('✅ Received response:', JSON.stringify(result, null, 2));
|
||||
|
||||
await transport.close();
|
||||
console.log('✅ Connection closed');
|
||||
await transport.close();
|
||||
console.log('✅ Connection closed');
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('Unhandled error:', error);
|
||||
process.exit(1);
|
||||
main().catch((error) => {
|
||||
console.error('Unhandled error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,45 +4,45 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const serverUrl = process.argv[2];
|
||||
const serverUrl = process.argv[2];
|
||||
|
||||
if (!serverUrl) {
|
||||
console.error('Usage: test-client <server-url>');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!serverUrl) {
|
||||
console.error('Usage: test-client <server-url>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Connecting to MCP server at: ${serverUrl}`);
|
||||
console.log(`Connecting to MCP server at: ${serverUrl}`);
|
||||
|
||||
try {
|
||||
const client = new Client(
|
||||
{
|
||||
name: 'test-client',
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
capabilities: {}
|
||||
}
|
||||
);
|
||||
try {
|
||||
const client = new Client(
|
||||
{
|
||||
name: 'test-client',
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
capabilities: {}
|
||||
}
|
||||
);
|
||||
|
||||
const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
|
||||
const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
|
||||
|
||||
await client.connect(transport);
|
||||
console.log('✅ Successfully connected to MCP server');
|
||||
await client.connect(transport);
|
||||
console.log('✅ Successfully connected to MCP server');
|
||||
|
||||
await client.listTools();
|
||||
console.log('✅ Successfully listed tools');
|
||||
await client.listTools();
|
||||
console.log('✅ Successfully listed tools');
|
||||
|
||||
await transport.close();
|
||||
console.log('✅ Connection closed successfully');
|
||||
await transport.close();
|
||||
console.log('✅ Connection closed successfully');
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to connect to MCP server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to connect to MCP server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('Unhandled error:', error);
|
||||
process.exit(1);
|
||||
main().catch((error) => {
|
||||
console.error('Unhandled error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,48 +4,48 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const serverUrl = process.argv[2];
|
||||
const serverUrl = process.argv[2];
|
||||
|
||||
if (!serverUrl) {
|
||||
console.error('Usage: test-client <server-url>');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!serverUrl) {
|
||||
console.error('Usage: test-client <server-url>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Connecting to MCP server at: ${serverUrl}`);
|
||||
console.log(`Connecting to MCP server at: ${serverUrl}`);
|
||||
|
||||
try {
|
||||
const client = new Client(
|
||||
{
|
||||
name: 'test-client',
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
capabilities: {}
|
||||
}
|
||||
);
|
||||
try {
|
||||
const client = new Client(
|
||||
{
|
||||
name: 'test-client',
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
capabilities: {}
|
||||
}
|
||||
);
|
||||
|
||||
const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
|
||||
const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
|
||||
|
||||
await client.connect(transport);
|
||||
console.log('✅ Successfully connected to MCP server');
|
||||
await client.connect(transport);
|
||||
console.log('✅ Successfully connected to MCP server');
|
||||
|
||||
await client.listTools();
|
||||
console.log('✅ Successfully listed tools');
|
||||
await client.listTools();
|
||||
console.log('✅ Successfully listed tools');
|
||||
|
||||
await client.callTool({ name: 'add_numbers', arguments: { a: 5, b: 10 } });
|
||||
console.log('✅ Successfully called add_numbers tool');
|
||||
await client.callTool({ name: 'add_numbers', arguments: { a: 5, b: 10 } });
|
||||
console.log('✅ Successfully called add_numbers tool');
|
||||
|
||||
await transport.close();
|
||||
console.log('✅ Connection closed successfully');
|
||||
await transport.close();
|
||||
console.log('✅ Connection closed successfully');
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to connect to MCP server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to connect to MCP server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('Unhandled error:', error);
|
||||
process.exit(1);
|
||||
main().catch((error) => {
|
||||
console.error('Unhandled error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
3350
examples/servers/typescript/package-lock.json
generated
3350
examples/servers/typescript/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,27 +1,27 @@
|
|||
{
|
||||
"name": "mcp-conformance-test-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Reference implementation of MCP server for conformance testing",
|
||||
"type": "module",
|
||||
"main": "everything-server.ts",
|
||||
"scripts": {
|
||||
"start": "tsx everything-server.ts",
|
||||
"dev": "tsx watch everything-server.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"model-context-protocol",
|
||||
"conformance",
|
||||
"testing"
|
||||
],
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"@types/cors": "^2.8.19",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
"name": "mcp-conformance-test-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Reference implementation of MCP server for conformance testing",
|
||||
"type": "module",
|
||||
"main": "everything-server.ts",
|
||||
"scripts": {
|
||||
"start": "tsx everything-server.ts",
|
||||
"dev": "tsx watch everything-server.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"model-context-protocol",
|
||||
"conformance",
|
||||
"testing"
|
||||
],
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"@types/cors": "^2.8.19",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["*.ts"]
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["*.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
export default {
|
||||
preset: 'ts-jest/presets/default-esm',
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
testEnvironment: 'node',
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1'
|
||||
}
|
||||
preset: 'ts-jest/presets/default-esm',
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
testEnvironment: 'node',
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1'
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
# Uncomment to add pre-commit hook for automatic linting on staged files:
|
||||
pre-commit:
|
||||
commands:
|
||||
lint:
|
||||
run: npm run lint:fix {staged_files}
|
||||
stage_fixed: true
|
||||
commands:
|
||||
lint:
|
||||
run: npm run lint:fix {staged_files}
|
||||
stage_fixed: true
|
||||
|
|
|
|||
52
lefthook.yml
52
lefthook.yml
|
|
@ -4,36 +4,36 @@
|
|||
assert_lefthook_installed: true
|
||||
|
||||
output:
|
||||
- meta # Print lefthook version
|
||||
- summary # Print summary block (successful and failed steps)
|
||||
- empty_summary # Print summary heading when there are no steps to run
|
||||
- success # Print successful steps
|
||||
- failure # Print failed steps printing
|
||||
- execution # Print any execution logs (but prints if the execution failed)
|
||||
- execution_out # Print execution output (but still prints failed commands output)
|
||||
- execution_info # Print `EXECUTE > ...` logging
|
||||
- skips # Print "skip" (i.e. no files matched)
|
||||
- meta # Print lefthook version
|
||||
- summary # Print summary block (successful and failed steps)
|
||||
- empty_summary # Print summary heading when there are no steps to run
|
||||
- success # Print successful steps
|
||||
- failure # Print failed steps printing
|
||||
- execution # Print any execution logs (but prints if the execution failed)
|
||||
- execution_out # Print execution output (but still prints failed commands output)
|
||||
- execution_info # Print `EXECUTE > ...` logging
|
||||
- skips # Print "skip" (i.e. no files matched)
|
||||
|
||||
pre-push:
|
||||
follow: true
|
||||
parallel: true
|
||||
jobs:
|
||||
- name: 'Code Formatting'
|
||||
run: npm run lint:fix_check
|
||||
stage_fixed: true
|
||||
fail_text: |
|
||||
💡 To catch linting issues earlier, enable the pre-commit hook:
|
||||
cp lefthook-local.example.yml lefthook-local.yml
|
||||
follow: true
|
||||
parallel: true
|
||||
jobs:
|
||||
- name: 'Code Formatting'
|
||||
run: npm run lint:fix_check
|
||||
stage_fixed: true
|
||||
fail_text: |
|
||||
💡 To catch linting issues earlier, enable the pre-commit hook:
|
||||
cp lefthook-local.example.yml lefthook-local.yml
|
||||
|
||||
- name: 'Test'
|
||||
run: npm run test
|
||||
- name: 'Test'
|
||||
run: npm run test
|
||||
|
||||
post-checkout:
|
||||
jobs:
|
||||
- name: 'Install Dependencies'
|
||||
run: npm install
|
||||
jobs:
|
||||
- name: 'Install Dependencies'
|
||||
run: npm install
|
||||
|
||||
post-merge:
|
||||
jobs:
|
||||
- name: 'Install Dependencies'
|
||||
run: npm install
|
||||
jobs:
|
||||
- name: 'Install Dependencies'
|
||||
run: npm install
|
||||
|
|
|
|||
13837
package-lock.json
generated
13837
package-lock.json
generated
File diff suppressed because it is too large
Load diff
62
package.json
62
package.json
|
|
@ -1,33 +1,33 @@
|
|||
{
|
||||
"name": "conformance",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "tsx src/runner.ts",
|
||||
"test:server": "tsx src/server-runner.ts",
|
||||
"test": "jest",
|
||||
"lint": "eslint src/ && prettier --check .",
|
||||
"lint:fix": "eslint src/ --fix && prettier --write .",
|
||||
"lint:fix_check": "npm run lint:fix && git diff --exit-code --quiet"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@jest/globals": "^29.7.0",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/jest": "^29.5.8",
|
||||
"eslint": "^9.8.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "3.6.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "^8.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"express": "^5.1.0",
|
||||
"lefthook": "^2.0.2",
|
||||
"zod": "^3.25.76"
|
||||
}
|
||||
"name": "conformance",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "tsx src/runner.ts",
|
||||
"test:server": "tsx src/server-runner.ts",
|
||||
"test": "jest",
|
||||
"lint": "eslint src/ && prettier --check .",
|
||||
"lint:fix": "eslint src/ --fix && prettier --write .",
|
||||
"lint:fix_check": "npm run lint:fix && git diff --exit-code --quiet"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@jest/globals": "^29.7.0",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/jest": "^29.5.8",
|
||||
"eslint": "^9.8.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "3.6.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "^8.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"express": "^5.1.0",
|
||||
"lefthook": "^2.0.2",
|
||||
"zod": "^3.25.76"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,84 +1,84 @@
|
|||
import { createClientInitializationCheck } from './client';
|
||||
|
||||
describe('createClientInitializationCheck', () => {
|
||||
it('should return SUCCESS for a valid initialize request', () => {
|
||||
const validRequest = {
|
||||
protocolVersion: '2025-06-18',
|
||||
clientInfo: {
|
||||
name: 'TestClient',
|
||||
version: '1.0.0'
|
||||
}
|
||||
};
|
||||
it('should return SUCCESS for a valid initialize request', () => {
|
||||
const validRequest = {
|
||||
protocolVersion: '2025-06-18',
|
||||
clientInfo: {
|
||||
name: 'TestClient',
|
||||
version: '1.0.0'
|
||||
}
|
||||
};
|
||||
|
||||
const check = createClientInitializationCheck(validRequest);
|
||||
expect(check.status).toBe('SUCCESS');
|
||||
expect(check.errorMessage).toBeUndefined();
|
||||
});
|
||||
const check = createClientInitializationCheck(validRequest);
|
||||
expect(check.status).toBe('SUCCESS');
|
||||
expect(check.errorMessage).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return FAILURE when protocol version is missing', () => {
|
||||
const invalidRequest = {
|
||||
clientInfo: {
|
||||
name: 'TestClient',
|
||||
version: '1.0.0'
|
||||
}
|
||||
};
|
||||
it('should return FAILURE when protocol version is missing', () => {
|
||||
const invalidRequest = {
|
||||
clientInfo: {
|
||||
name: 'TestClient',
|
||||
version: '1.0.0'
|
||||
}
|
||||
};
|
||||
|
||||
const check = createClientInitializationCheck(invalidRequest);
|
||||
expect(check.status).toBe('FAILURE');
|
||||
expect(check.errorMessage).toContain('Protocol version not provided');
|
||||
});
|
||||
const check = createClientInitializationCheck(invalidRequest);
|
||||
expect(check.status).toBe('FAILURE');
|
||||
expect(check.errorMessage).toContain('Protocol version not provided');
|
||||
});
|
||||
|
||||
it('should return FAILURE when protocol version does not match', () => {
|
||||
const invalidRequest = {
|
||||
protocolVersion: '2024-11-05',
|
||||
clientInfo: {
|
||||
name: 'TestClient',
|
||||
version: '1.0.0'
|
||||
}
|
||||
};
|
||||
it('should return FAILURE when protocol version does not match', () => {
|
||||
const invalidRequest = {
|
||||
protocolVersion: '2024-11-05',
|
||||
clientInfo: {
|
||||
name: 'TestClient',
|
||||
version: '1.0.0'
|
||||
}
|
||||
};
|
||||
|
||||
const check = createClientInitializationCheck(invalidRequest);
|
||||
expect(check.status).toBe('FAILURE');
|
||||
expect(check.errorMessage).toContain('Version mismatch');
|
||||
});
|
||||
const check = createClientInitializationCheck(invalidRequest);
|
||||
expect(check.status).toBe('FAILURE');
|
||||
expect(check.errorMessage).toContain('Version mismatch');
|
||||
});
|
||||
|
||||
it('should return FAILURE when client name is missing', () => {
|
||||
const invalidRequest = {
|
||||
protocolVersion: '2025-06-18',
|
||||
clientInfo: {
|
||||
version: '1.0.0'
|
||||
}
|
||||
};
|
||||
it('should return FAILURE when client name is missing', () => {
|
||||
const invalidRequest = {
|
||||
protocolVersion: '2025-06-18',
|
||||
clientInfo: {
|
||||
version: '1.0.0'
|
||||
}
|
||||
};
|
||||
|
||||
const check = createClientInitializationCheck(invalidRequest);
|
||||
expect(check.status).toBe('FAILURE');
|
||||
expect(check.errorMessage).toContain('Client name missing');
|
||||
});
|
||||
const check = createClientInitializationCheck(invalidRequest);
|
||||
expect(check.status).toBe('FAILURE');
|
||||
expect(check.errorMessage).toContain('Client name missing');
|
||||
});
|
||||
|
||||
it('should return FAILURE when client version is missing', () => {
|
||||
const invalidRequest = {
|
||||
protocolVersion: '2025-06-18',
|
||||
clientInfo: {
|
||||
name: 'TestClient'
|
||||
}
|
||||
};
|
||||
it('should return FAILURE when client version is missing', () => {
|
||||
const invalidRequest = {
|
||||
protocolVersion: '2025-06-18',
|
||||
clientInfo: {
|
||||
name: 'TestClient'
|
||||
}
|
||||
};
|
||||
|
||||
const check = createClientInitializationCheck(invalidRequest);
|
||||
expect(check.status).toBe('FAILURE');
|
||||
expect(check.errorMessage).toContain('Client version missing');
|
||||
});
|
||||
const check = createClientInitializationCheck(invalidRequest);
|
||||
expect(check.status).toBe('FAILURE');
|
||||
expect(check.errorMessage).toContain('Client version missing');
|
||||
});
|
||||
|
||||
it('should support custom expected spec version', () => {
|
||||
const request = {
|
||||
protocolVersion: '2024-11-05',
|
||||
clientInfo: {
|
||||
name: 'TestClient',
|
||||
version: '1.0.0'
|
||||
}
|
||||
};
|
||||
it('should support custom expected spec version', () => {
|
||||
const request = {
|
||||
protocolVersion: '2024-11-05',
|
||||
clientInfo: {
|
||||
name: 'TestClient',
|
||||
version: '1.0.0'
|
||||
}
|
||||
};
|
||||
|
||||
const check = createClientInitializationCheck(request, '2024-11-05');
|
||||
expect(check.status).toBe('SUCCESS');
|
||||
expect(check.errorMessage).toBeUndefined();
|
||||
});
|
||||
const check = createClientInitializationCheck(request, '2024-11-05');
|
||||
expect(check.status).toBe('SUCCESS');
|
||||
expect(check.errorMessage).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,47 +1,67 @@
|
|||
import { ConformanceCheck, CheckStatus } from '../types.js';
|
||||
|
||||
export function createServerInfoCheck(serverInfo: { name: string; version: string }): ConformanceCheck {
|
||||
return {
|
||||
id: 'server-info',
|
||||
name: 'ServerInfo',
|
||||
description: 'Test server info returned to client',
|
||||
status: 'INFO',
|
||||
timestamp: new Date().toISOString(),
|
||||
specReferences: [{ id: 'MCP-Lifecycle', url: 'https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle' }],
|
||||
details: {
|
||||
serverName: serverInfo.name,
|
||||
serverVersion: serverInfo.version
|
||||
}
|
||||
};
|
||||
export function createServerInfoCheck(serverInfo: {
|
||||
name: string;
|
||||
version: string;
|
||||
}): ConformanceCheck {
|
||||
return {
|
||||
id: 'server-info',
|
||||
name: 'ServerInfo',
|
||||
description: 'Test server info returned to client',
|
||||
status: 'INFO',
|
||||
timestamp: new Date().toISOString(),
|
||||
specReferences: [
|
||||
{
|
||||
id: 'MCP-Lifecycle',
|
||||
url: 'https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle'
|
||||
}
|
||||
],
|
||||
details: {
|
||||
serverName: serverInfo.name,
|
||||
serverVersion: serverInfo.version
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createClientInitializationCheck(initializeRequest: any, expectedSpecVersion: string = '2025-06-18'): ConformanceCheck {
|
||||
const protocolVersionSent = initializeRequest?.protocolVersion;
|
||||
const versionMatch = protocolVersionSent === expectedSpecVersion;
|
||||
export function createClientInitializationCheck(
|
||||
initializeRequest: any,
|
||||
expectedSpecVersion: string = '2025-06-18'
|
||||
): ConformanceCheck {
|
||||
const protocolVersionSent = initializeRequest?.protocolVersion;
|
||||
const versionMatch = protocolVersionSent === expectedSpecVersion;
|
||||
|
||||
const errors: string[] = [];
|
||||
if (!protocolVersionSent) errors.push('Protocol version not provided');
|
||||
if (!versionMatch) errors.push(`Version mismatch: expected ${expectedSpecVersion}, got ${protocolVersionSent}`);
|
||||
if (!initializeRequest?.clientInfo?.name) errors.push('Client name missing');
|
||||
if (!initializeRequest?.clientInfo?.version) errors.push('Client version missing');
|
||||
const errors: string[] = [];
|
||||
if (!protocolVersionSent) errors.push('Protocol version not provided');
|
||||
if (!versionMatch)
|
||||
errors.push(
|
||||
`Version mismatch: expected ${expectedSpecVersion}, got ${protocolVersionSent}`
|
||||
);
|
||||
if (!initializeRequest?.clientInfo?.name) errors.push('Client name missing');
|
||||
if (!initializeRequest?.clientInfo?.version)
|
||||
errors.push('Client version missing');
|
||||
|
||||
const status: CheckStatus = errors.length === 0 ? 'SUCCESS' : 'FAILURE';
|
||||
const status: CheckStatus = errors.length === 0 ? 'SUCCESS' : 'FAILURE';
|
||||
|
||||
return {
|
||||
id: 'mcp-client-initialization',
|
||||
name: 'MCPClientInitialization',
|
||||
description: 'Validates that MCP client properly initializes with server',
|
||||
status,
|
||||
timestamp: new Date().toISOString(),
|
||||
specReferences: [{ id: 'MCP-Lifecycle', url: 'https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle' }],
|
||||
details: {
|
||||
protocolVersionSent,
|
||||
expectedSpecVersion,
|
||||
versionMatch,
|
||||
clientName: initializeRequest?.clientInfo?.name,
|
||||
clientVersion: initializeRequest?.clientInfo?.version
|
||||
},
|
||||
errorMessage: errors.length > 0 ? errors.join('; ') : undefined,
|
||||
logs: errors.length > 0 ? errors : undefined
|
||||
};
|
||||
return {
|
||||
id: 'mcp-client-initialization',
|
||||
name: 'MCPClientInitialization',
|
||||
description: 'Validates that MCP client properly initializes with server',
|
||||
status,
|
||||
timestamp: new Date().toISOString(),
|
||||
specReferences: [
|
||||
{
|
||||
id: 'MCP-Lifecycle',
|
||||
url: 'https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle'
|
||||
}
|
||||
],
|
||||
details: {
|
||||
protocolVersionSent,
|
||||
expectedSpecVersion,
|
||||
versionMatch,
|
||||
clientName: initializeRequest?.clientInfo?.name,
|
||||
clientVersion: initializeRequest?.clientInfo?.version
|
||||
},
|
||||
errorMessage: errors.length > 0 ? errors.join('; ') : undefined,
|
||||
logs: errors.length > 0 ? errors : undefined
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,219 +5,239 @@ import { ConformanceCheck } from '../types.js';
|
|||
import { getScenario } from '../scenarios/index.js';
|
||||
|
||||
export interface ClientExecutionResult {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
timedOut: boolean;
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
timedOut: boolean;
|
||||
}
|
||||
|
||||
async function ensureResultsDir(): Promise<string> {
|
||||
const resultsDir = path.join(process.cwd(), 'results');
|
||||
await fs.mkdir(resultsDir, { recursive: true });
|
||||
return resultsDir;
|
||||
const resultsDir = path.join(process.cwd(), 'results');
|
||||
await fs.mkdir(resultsDir, { recursive: true });
|
||||
return resultsDir;
|
||||
}
|
||||
|
||||
function createResultDir(scenario: string): string {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
return path.join('results', `${scenario}-${timestamp}`);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
return path.join('results', `${scenario}-${timestamp}`);
|
||||
}
|
||||
|
||||
async function executeClient(command: string, serverUrl: string, timeout: number = 30000): Promise<ClientExecutionResult> {
|
||||
const commandParts = command.split(' ');
|
||||
const executable = commandParts[0];
|
||||
const args = [...commandParts.slice(1), serverUrl];
|
||||
async function executeClient(
|
||||
command: string,
|
||||
serverUrl: string,
|
||||
timeout: number = 30000
|
||||
): Promise<ClientExecutionResult> {
|
||||
const commandParts = command.split(' ');
|
||||
const executable = commandParts[0];
|
||||
const args = [...commandParts.slice(1), serverUrl];
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let timedOut = false;
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let timedOut = false;
|
||||
|
||||
return new Promise(resolve => {
|
||||
const process = spawn(executable, args, {
|
||||
shell: true,
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
timedOut = true;
|
||||
process.kill();
|
||||
}, timeout);
|
||||
|
||||
if (process.stdout) {
|
||||
process.stdout.on('data', data => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
}
|
||||
|
||||
if (process.stderr) {
|
||||
process.stderr.on('data', data => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
}
|
||||
|
||||
process.on('close', code => {
|
||||
clearTimeout(timeoutHandle);
|
||||
resolve({
|
||||
exitCode: code || 0,
|
||||
stdout,
|
||||
stderr,
|
||||
timedOut
|
||||
});
|
||||
});
|
||||
|
||||
process.on('error', error => {
|
||||
clearTimeout(timeoutHandle);
|
||||
resolve({
|
||||
exitCode: -1,
|
||||
stdout,
|
||||
stderr: stderr + `\nProcess error: ${error.message}`,
|
||||
timedOut
|
||||
});
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
const process = spawn(executable, args, {
|
||||
shell: true,
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
timedOut = true;
|
||||
process.kill();
|
||||
}, timeout);
|
||||
|
||||
if (process.stdout) {
|
||||
process.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
}
|
||||
|
||||
if (process.stderr) {
|
||||
process.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
}
|
||||
|
||||
process.on('close', (code) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
resolve({
|
||||
exitCode: code || 0,
|
||||
stdout,
|
||||
stderr,
|
||||
timedOut
|
||||
});
|
||||
});
|
||||
|
||||
process.on('error', (error) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
resolve({
|
||||
exitCode: -1,
|
||||
stdout,
|
||||
stderr: stderr + `\nProcess error: ${error.message}`,
|
||||
timedOut
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function runConformanceTest(
|
||||
clientCommand: string,
|
||||
scenarioName: string,
|
||||
timeout: number = 30000
|
||||
clientCommand: string,
|
||||
scenarioName: string,
|
||||
timeout: number = 30000
|
||||
): Promise<{
|
||||
checks: ConformanceCheck[];
|
||||
clientOutput: ClientExecutionResult;
|
||||
resultDir: string;
|
||||
checks: ConformanceCheck[];
|
||||
clientOutput: ClientExecutionResult;
|
||||
resultDir: string;
|
||||
}> {
|
||||
await ensureResultsDir();
|
||||
const resultDir = createResultDir(scenarioName);
|
||||
await fs.mkdir(resultDir, { recursive: true });
|
||||
await ensureResultsDir();
|
||||
const resultDir = createResultDir(scenarioName);
|
||||
await fs.mkdir(resultDir, { recursive: true });
|
||||
|
||||
const scenario = getScenario(scenarioName);
|
||||
if (!scenario) {
|
||||
throw new Error(`Unknown scenario: ${scenarioName}`);
|
||||
}
|
||||
const scenario = getScenario(scenarioName);
|
||||
if (!scenario) {
|
||||
throw new Error(`Unknown scenario: ${scenarioName}`);
|
||||
}
|
||||
|
||||
console.log(`Starting scenario: ${scenarioName}`);
|
||||
const urls = await scenario.start();
|
||||
console.log(`Starting scenario: ${scenarioName}`);
|
||||
const urls = await scenario.start();
|
||||
|
||||
console.log(`Executing client: ${clientCommand} ${urls.serverUrl}`);
|
||||
console.log(`Executing client: ${clientCommand} ${urls.serverUrl}`);
|
||||
|
||||
try {
|
||||
const clientOutput = await executeClient(clientCommand, urls.serverUrl, timeout);
|
||||
try {
|
||||
const clientOutput = await executeClient(
|
||||
clientCommand,
|
||||
urls.serverUrl,
|
||||
timeout
|
||||
);
|
||||
|
||||
const checks = scenario.getChecks();
|
||||
const checks = scenario.getChecks();
|
||||
|
||||
await fs.writeFile(path.join(resultDir, 'checks.json'), JSON.stringify(checks, null, 2));
|
||||
await fs.writeFile(
|
||||
path.join(resultDir, 'checks.json'),
|
||||
JSON.stringify(checks, null, 2)
|
||||
);
|
||||
|
||||
await fs.writeFile(path.join(resultDir, 'stdout.txt'), clientOutput.stdout);
|
||||
await fs.writeFile(path.join(resultDir, 'stdout.txt'), clientOutput.stdout);
|
||||
|
||||
await fs.writeFile(path.join(resultDir, 'stderr.txt'), clientOutput.stderr);
|
||||
await fs.writeFile(path.join(resultDir, 'stderr.txt'), clientOutput.stderr);
|
||||
|
||||
console.log(`Results saved to ${resultDir}`);
|
||||
console.log(`Results saved to ${resultDir}`);
|
||||
|
||||
return {
|
||||
checks,
|
||||
clientOutput,
|
||||
resultDir
|
||||
};
|
||||
} finally {
|
||||
await scenario.stop();
|
||||
}
|
||||
return {
|
||||
checks,
|
||||
clientOutput,
|
||||
resultDir
|
||||
};
|
||||
} finally {
|
||||
await scenario.stop();
|
||||
}
|
||||
}
|
||||
|
||||
async function runInteractiveMode(scenarioName: string): Promise<void> {
|
||||
await ensureResultsDir();
|
||||
const resultDir = createResultDir(scenarioName);
|
||||
await fs.mkdir(resultDir, { recursive: true });
|
||||
await ensureResultsDir();
|
||||
const resultDir = createResultDir(scenarioName);
|
||||
await fs.mkdir(resultDir, { recursive: true });
|
||||
|
||||
const scenario = getScenario(scenarioName);
|
||||
if (!scenario) {
|
||||
throw new Error(`Unknown scenario: ${scenarioName}`);
|
||||
}
|
||||
const scenario = getScenario(scenarioName);
|
||||
if (!scenario) {
|
||||
throw new Error(`Unknown scenario: ${scenarioName}`);
|
||||
}
|
||||
|
||||
console.log(`Starting scenario: ${scenarioName}`);
|
||||
const urls = await scenario.start();
|
||||
console.log(`Starting scenario: ${scenarioName}`);
|
||||
const urls = await scenario.start();
|
||||
|
||||
console.log(`Server URL: ${urls.serverUrl}`);
|
||||
console.log('Press Ctrl+C to stop and save checks...');
|
||||
console.log(`Server URL: ${urls.serverUrl}`);
|
||||
console.log('Press Ctrl+C to stop and save checks...');
|
||||
|
||||
const handleShutdown = async () => {
|
||||
console.log('\nShutting down...');
|
||||
const handleShutdown = async () => {
|
||||
console.log('\nShutting down...');
|
||||
|
||||
const checks = scenario.getChecks();
|
||||
await fs.writeFile(path.join(resultDir, 'checks.json'), JSON.stringify(checks, null, 2));
|
||||
const checks = scenario.getChecks();
|
||||
await fs.writeFile(
|
||||
path.join(resultDir, 'checks.json'),
|
||||
JSON.stringify(checks, null, 2)
|
||||
);
|
||||
|
||||
console.log(`\nChecks:\n${JSON.stringify(checks, null, 2)}`);
|
||||
console.log(`\nChecks saved to ${resultDir}/checks.json`);
|
||||
console.log(`\nChecks:\n${JSON.stringify(checks, null, 2)}`);
|
||||
console.log(`\nChecks saved to ${resultDir}/checks.json`);
|
||||
|
||||
await scenario.stop();
|
||||
process.exit(0);
|
||||
};
|
||||
await scenario.stop();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGINT', handleShutdown);
|
||||
process.on('SIGTERM', handleShutdown);
|
||||
process.on('SIGINT', handleShutdown);
|
||||
process.on('SIGTERM', handleShutdown);
|
||||
|
||||
await new Promise(() => {});
|
||||
await new Promise(() => {});
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = process.argv.slice(2);
|
||||
let command: string | null = null;
|
||||
let scenario: string | null = null;
|
||||
const args = process.argv.slice(2);
|
||||
let command: string | null = null;
|
||||
let scenario: string | null = null;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--command' && i + 1 < args.length) {
|
||||
command = args[i + 1];
|
||||
i++;
|
||||
} else if (args[i] === '--scenario' && i + 1 < args.length) {
|
||||
scenario = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--command' && i + 1 < args.length) {
|
||||
command = args[i + 1];
|
||||
i++;
|
||||
} else if (args[i] === '--scenario' && i + 1 < args.length) {
|
||||
scenario = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!scenario) {
|
||||
console.error('Usage: runner --scenario <scenario> [--command "<command>"]');
|
||||
console.error('Example: runner --scenario initialize --command "tsx examples/clients/typescript/test1.ts"');
|
||||
console.error('Or run without --command for interactive mode');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!command) {
|
||||
try {
|
||||
await runInteractiveMode(scenario);
|
||||
} catch (error) {
|
||||
console.error('Interactive mode error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!scenario) {
|
||||
console.error(
|
||||
'Usage: runner --scenario <scenario> [--command "<command>"]'
|
||||
);
|
||||
console.error(
|
||||
'Example: runner --scenario initialize --command "tsx examples/clients/typescript/test1.ts"'
|
||||
);
|
||||
console.error('Or run without --command for interactive mode');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!command) {
|
||||
try {
|
||||
const result = await runConformanceTest(command, scenario);
|
||||
|
||||
const denominator = result.checks.filter(c => c.status === 'SUCCESS' || c.status == 'FAILURE').length;
|
||||
const passed = result.checks.filter(c => c.status === 'SUCCESS').length;
|
||||
const failed = result.checks.filter(c => c.status === 'FAILURE').length;
|
||||
|
||||
console.log(`Checks:\n${JSON.stringify(result.checks, null, 2)}`);
|
||||
|
||||
console.log(`\nTest Results:`);
|
||||
console.log(`Passed: ${passed}/${denominator}, ${failed} failed`);
|
||||
|
||||
if (failed > 0) {
|
||||
console.log('\nFailed Checks:');
|
||||
result.checks
|
||||
.filter(c => c.status === 'FAILURE')
|
||||
.forEach(c => {
|
||||
console.log(` - ${c.name}: ${c.description}`);
|
||||
if (c.errorMessage) {
|
||||
console.log(` Error: ${c.errorMessage}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
await runInteractiveMode(scenario);
|
||||
} catch (error) {
|
||||
console.error('Test runner error:', error);
|
||||
process.exit(1);
|
||||
console.error('Interactive mode error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await runConformanceTest(command, scenario);
|
||||
|
||||
const denominator = result.checks.filter(
|
||||
(c) => c.status === 'SUCCESS' || c.status == 'FAILURE'
|
||||
).length;
|
||||
const passed = result.checks.filter((c) => c.status === 'SUCCESS').length;
|
||||
const failed = result.checks.filter((c) => c.status === 'FAILURE').length;
|
||||
|
||||
console.log(`Checks:\n${JSON.stringify(result.checks, null, 2)}`);
|
||||
|
||||
console.log(`\nTest Results:`);
|
||||
console.log(`Passed: ${passed}/${denominator}, ${failed} failed`);
|
||||
|
||||
if (failed > 0) {
|
||||
console.log('\nFailed Checks:');
|
||||
result.checks
|
||||
.filter((c) => c.status === 'FAILURE')
|
||||
.forEach((c) => {
|
||||
console.log(` - ${c.name}: ${c.description}`);
|
||||
if (c.errorMessage) {
|
||||
console.log(` Error: ${c.errorMessage}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
} catch (error) {
|
||||
console.error('Test runner error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue