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