* clean up constructor calls for server lifecycle * add back compat test * fix dup logging, rm prm * avoid fallbacks * better error from example client * add final endpoint method * add test for OASM priority order * s/prepack/prepare/ for npx * try switching to .js * make tsdown version happy default * fix lock * fix prmPath issue * back to main package-lock * fix bin path * Add comprehensive auth scenario test suite - Add index.test.ts to test all auth scenarios with auth-test.ts client - Upgrade @modelcontextprotocol/sdk from 1.20.1 to 1.22.0 - All 5 auth scenarios now pass including auth/basic-metadata-var2 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * consolidate tests into index.test.ts * rename metadata * fix comments * rework auth scenario listing * add 3rd variation of metadata * fix some type issues introduced by version bump * add reusable spec references * more refs * fix up spec references --------- Co-authored-by: Claude <noreply@anthropic.com>
497 lines
15 KiB
TypeScript
497 lines
15 KiB
TypeScript
/**
|
|
* Resources test scenarios for MCP servers
|
|
*/
|
|
|
|
import { ClientScenario, ConformanceCheck } from '../../types.js';
|
|
import { connectToServer } from './client-helper.js';
|
|
import {
|
|
TextResourceContents,
|
|
BlobResourceContents
|
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
|
|
export class ResourcesListScenario implements ClientScenario {
|
|
name = 'resources-list';
|
|
description = `Test listing available resources.
|
|
|
|
**Server Implementation Requirements:**
|
|
|
|
**Endpoint**: \`resources/list\`
|
|
|
|
**Requirements**:
|
|
- Return array of all available **direct resources** (not templates)
|
|
- Each resource MUST have:
|
|
- \`uri\` (string)
|
|
- \`name\` (string)
|
|
- \`description\` (string)
|
|
- \`mimeType\` (string, optional)`;
|
|
|
|
async run(serverUrl: string): Promise<ConformanceCheck[]> {
|
|
const checks: ConformanceCheck[] = [];
|
|
|
|
try {
|
|
const connection = await connectToServer(serverUrl);
|
|
|
|
const result = await connection.client.listResources();
|
|
|
|
// Validate response structure
|
|
const errors: string[] = [];
|
|
if (!result.resources) {
|
|
errors.push('Missing resources array');
|
|
} else {
|
|
if (!Array.isArray(result.resources)) {
|
|
errors.push('resources is not an array');
|
|
}
|
|
|
|
result.resources.forEach((resource, index) => {
|
|
if (!resource.uri) errors.push(`Resource ${index}: missing uri`);
|
|
if (!resource.name) errors.push(`Resource ${index}: missing name`);
|
|
});
|
|
}
|
|
|
|
checks.push({
|
|
id: 'resources-list',
|
|
name: 'ResourcesList',
|
|
description: 'Server lists available resources with valid structure',
|
|
status: errors.length === 0 ? 'SUCCESS' : 'FAILURE',
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: errors.length > 0 ? errors.join('; ') : undefined,
|
|
specReferences: [
|
|
{
|
|
id: 'MCP-Resources-List',
|
|
url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#listing-resources'
|
|
}
|
|
],
|
|
details: {
|
|
resourceCount: result.resources?.length || 0,
|
|
resources: result.resources?.map((r) => r.uri)
|
|
}
|
|
});
|
|
|
|
await connection.close();
|
|
} catch (error) {
|
|
checks.push({
|
|
id: 'resources-list',
|
|
name: 'ResourcesList',
|
|
description: 'Server lists available resources with valid structure',
|
|
status: 'FAILURE',
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
specReferences: [
|
|
{
|
|
id: 'MCP-Resources-List',
|
|
url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#listing-resources'
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
return checks;
|
|
}
|
|
}
|
|
|
|
export class ResourcesReadTextScenario implements ClientScenario {
|
|
name = 'resources-read-text';
|
|
description = `Test reading text resource.
|
|
|
|
**Server Implementation Requirements:**
|
|
|
|
Implement resource \`test://static-text\` that returns:
|
|
|
|
\`\`\`json
|
|
{
|
|
"contents": [
|
|
{
|
|
"uri": "test://static-text",
|
|
"mimeType": "text/plain",
|
|
"text": "This is the content of the static text resource."
|
|
}
|
|
]
|
|
}
|
|
\`\`\``;
|
|
|
|
async run(serverUrl: string): Promise<ConformanceCheck[]> {
|
|
const checks: ConformanceCheck[] = [];
|
|
|
|
try {
|
|
const connection = await connectToServer(serverUrl);
|
|
|
|
const result = await connection.client.readResource({
|
|
uri: 'test://static-text'
|
|
});
|
|
|
|
// Validate response
|
|
const errors: string[] = [];
|
|
if (!result.contents) errors.push('Missing contents array');
|
|
if (!Array.isArray(result.contents))
|
|
errors.push('contents is not an array');
|
|
if (result.contents.length === 0) errors.push('contents array is empty');
|
|
|
|
const content = result.contents[0] as TextResourceContents | undefined;
|
|
if (content) {
|
|
if (!content.uri) errors.push('Content missing uri');
|
|
if (!content.mimeType) errors.push('Content missing mimeType');
|
|
if (!content.text) errors.push('Content missing text field');
|
|
}
|
|
|
|
checks.push({
|
|
id: 'resources-read-text',
|
|
name: 'ResourcesReadText',
|
|
description: 'Read text resource successfully',
|
|
status: errors.length === 0 ? 'SUCCESS' : 'FAILURE',
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: errors.length > 0 ? errors.join('; ') : undefined,
|
|
specReferences: [
|
|
{
|
|
id: 'MCP-Resources-Read',
|
|
url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#reading-resources'
|
|
}
|
|
],
|
|
details: {
|
|
uri: content?.uri,
|
|
mimeType: content?.mimeType,
|
|
hasText: !!content?.text
|
|
}
|
|
});
|
|
|
|
await connection.close();
|
|
} catch (error) {
|
|
checks.push({
|
|
id: 'resources-read-text',
|
|
name: 'ResourcesReadText',
|
|
description: 'Read text resource successfully',
|
|
status: 'FAILURE',
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
specReferences: [
|
|
{
|
|
id: 'MCP-Resources-Read',
|
|
url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#reading-resources'
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
return checks;
|
|
}
|
|
}
|
|
|
|
export class ResourcesReadBinaryScenario implements ClientScenario {
|
|
name = 'resources-read-binary';
|
|
description = `Test reading binary resource.
|
|
|
|
**Server Implementation Requirements:**
|
|
|
|
Implement resource \`test://static-binary\` that returns:
|
|
|
|
\`\`\`json
|
|
{
|
|
"contents": [
|
|
{
|
|
"uri": "test://static-binary",
|
|
"mimeType": "image/png",
|
|
"blob": "<base64-encoded-png>"
|
|
}
|
|
]
|
|
}
|
|
\`\`\``;
|
|
|
|
async run(serverUrl: string): Promise<ConformanceCheck[]> {
|
|
const checks: ConformanceCheck[] = [];
|
|
|
|
try {
|
|
const connection = await connectToServer(serverUrl);
|
|
|
|
const result = await connection.client.readResource({
|
|
uri: 'test://static-binary'
|
|
});
|
|
|
|
// Validate response
|
|
const errors: string[] = [];
|
|
if (!result.contents) errors.push('Missing contents array');
|
|
if (result.contents.length === 0) errors.push('contents array is empty');
|
|
|
|
const content = result.contents[0] as BlobResourceContents | undefined;
|
|
if (content) {
|
|
if (!content.uri) errors.push('Content missing uri');
|
|
if (!content.mimeType) errors.push('Content missing mimeType');
|
|
if (!content.blob) errors.push('Content missing blob field');
|
|
}
|
|
|
|
checks.push({
|
|
id: 'resources-read-binary',
|
|
name: 'ResourcesReadBinary',
|
|
description: 'Read binary resource successfully',
|
|
status: errors.length === 0 ? 'SUCCESS' : 'FAILURE',
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: errors.length > 0 ? errors.join('; ') : undefined,
|
|
specReferences: [
|
|
{
|
|
id: 'MCP-Resources-Read',
|
|
url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#reading-resources'
|
|
}
|
|
],
|
|
details: {
|
|
uri: content?.uri,
|
|
mimeType: content?.mimeType,
|
|
hasBlob: !!content?.blob
|
|
}
|
|
});
|
|
|
|
await connection.close();
|
|
} catch (error) {
|
|
checks.push({
|
|
id: 'resources-read-binary',
|
|
name: 'ResourcesReadBinary',
|
|
description: 'Read binary resource successfully',
|
|
status: 'FAILURE',
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
specReferences: [
|
|
{
|
|
id: 'MCP-Resources-Read',
|
|
url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#reading-resources'
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
return checks;
|
|
}
|
|
}
|
|
|
|
export class ResourcesTemplateReadScenario implements ClientScenario {
|
|
name = 'resources-templates-read';
|
|
description = `Test reading resource from template.
|
|
|
|
**Server Implementation Requirements:**
|
|
|
|
Implement resource template \`test://template/{id}/data\` that substitutes parameters.
|
|
|
|
**Behavior**: When client requests \`test://template/123/data\`, substitute \`{id}\` with \`123\`
|
|
|
|
Returns (for \`uri: "test://template/123/data"\`):
|
|
|
|
\`\`\`json
|
|
{
|
|
"contents": [
|
|
{
|
|
"uri": "test://template/123/data",
|
|
"mimeType": "application/json",
|
|
"text": "{"id":"123","templateTest":true,"data":"Data for ID: 123"}"
|
|
}
|
|
]
|
|
}
|
|
\`\`\``;
|
|
|
|
async run(serverUrl: string): Promise<ConformanceCheck[]> {
|
|
const checks: ConformanceCheck[] = [];
|
|
|
|
try {
|
|
const connection = await connectToServer(serverUrl);
|
|
|
|
const result = await connection.client.readResource({
|
|
uri: 'test://template/123/data'
|
|
});
|
|
|
|
// Validate response
|
|
const errors: string[] = [];
|
|
if (!result.contents) errors.push('Missing contents array');
|
|
if (result.contents.length === 0) errors.push('contents array is empty');
|
|
|
|
const content = result.contents[0];
|
|
if (content) {
|
|
if (!content.uri) errors.push('Content missing uri');
|
|
const hasText = 'text' in content;
|
|
const hasBlob = 'blob' in content;
|
|
if (!hasText && !hasBlob) errors.push('Content missing text or blob');
|
|
|
|
const text = hasText
|
|
? (content as TextResourceContents).text
|
|
: hasBlob
|
|
? '[binary]'
|
|
: '';
|
|
if (typeof text === 'string' && !text.includes('123')) {
|
|
errors.push('Parameter substitution not reflected in content');
|
|
}
|
|
}
|
|
|
|
checks.push({
|
|
id: 'resources-templates-read',
|
|
name: 'ResourcesTemplateRead',
|
|
description: 'Read resource from template with parameter substitution',
|
|
status: errors.length === 0 ? 'SUCCESS' : 'FAILURE',
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: errors.length > 0 ? errors.join('; ') : undefined,
|
|
specReferences: [
|
|
{
|
|
id: 'MCP-Resources-Templates',
|
|
url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#resource-templates'
|
|
}
|
|
],
|
|
details: {
|
|
uri: content?.uri,
|
|
content: content
|
|
? 'text' in content
|
|
? (content as TextResourceContents).text
|
|
: (content as BlobResourceContents).blob
|
|
: undefined
|
|
}
|
|
});
|
|
|
|
await connection.close();
|
|
} catch (error) {
|
|
checks.push({
|
|
id: 'resources-templates-read',
|
|
name: 'ResourcesTemplateRead',
|
|
description: 'Read resource from template with parameter substitution',
|
|
status: 'FAILURE',
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
specReferences: [
|
|
{
|
|
id: 'MCP-Resources-Templates',
|
|
url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#resource-templates'
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
return checks;
|
|
}
|
|
}
|
|
|
|
export class ResourcesSubscribeScenario implements ClientScenario {
|
|
name = 'resources-subscribe';
|
|
description = `Test subscribing to resource updates.
|
|
|
|
**Server Implementation Requirements:**
|
|
|
|
**Endpoint**: \`resources/subscribe\`
|
|
|
|
**Requirements**:
|
|
- Accept subscription request with URI
|
|
- Track subscribed URIs
|
|
- Return empty object \`{}\`
|
|
|
|
Example request:
|
|
|
|
\`\`\`json
|
|
{
|
|
"method": "resources/subscribe",
|
|
"params": {
|
|
"uri": "test://watched-resource"
|
|
}
|
|
}
|
|
\`\`\``;
|
|
|
|
async run(serverUrl: string): Promise<ConformanceCheck[]> {
|
|
const checks: ConformanceCheck[] = [];
|
|
|
|
try {
|
|
const connection = await connectToServer(serverUrl);
|
|
|
|
await connection.client.subscribeResource({
|
|
uri: 'test://watched-resource'
|
|
});
|
|
|
|
checks.push({
|
|
id: 'resources-subscribe',
|
|
name: 'ResourcesSubscribe',
|
|
description: 'Subscribe to resource successfully',
|
|
status: 'SUCCESS',
|
|
timestamp: new Date().toISOString(),
|
|
specReferences: [
|
|
{
|
|
id: 'MCP-Resources-Subscribe',
|
|
url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#resource-subscriptions'
|
|
}
|
|
]
|
|
});
|
|
|
|
await connection.close();
|
|
} catch (error) {
|
|
checks.push({
|
|
id: 'resources-subscribe',
|
|
name: 'ResourcesSubscribe',
|
|
description: 'Subscribe to resource successfully',
|
|
status: 'FAILURE',
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
specReferences: [
|
|
{
|
|
id: 'MCP-Resources-Subscribe',
|
|
url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#resource-subscriptions'
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
return checks;
|
|
}
|
|
}
|
|
|
|
export class ResourcesUnsubscribeScenario implements ClientScenario {
|
|
name = 'resources-unsubscribe';
|
|
description = `Test unsubscribing from resource.
|
|
|
|
**Server Implementation Requirements:**
|
|
|
|
**Endpoint**: \`resources/unsubscribe\`
|
|
|
|
**Requirements**:
|
|
- Accept unsubscribe request with URI
|
|
- Remove URI from subscriptions
|
|
- Stop sending update notifications for that URI
|
|
- Return empty object \`{}\``;
|
|
|
|
async run(serverUrl: string): Promise<ConformanceCheck[]> {
|
|
const checks: ConformanceCheck[] = [];
|
|
|
|
try {
|
|
const connection = await connectToServer(serverUrl);
|
|
|
|
// First subscribe
|
|
await connection.client.subscribeResource({
|
|
uri: 'test://watched-resource'
|
|
});
|
|
|
|
// Then unsubscribe
|
|
await connection.client.unsubscribeResource({
|
|
uri: 'test://watched-resource'
|
|
});
|
|
|
|
checks.push({
|
|
id: 'resources-unsubscribe',
|
|
name: 'ResourcesUnsubscribe',
|
|
description: 'Unsubscribe from resource successfully',
|
|
status: 'SUCCESS',
|
|
timestamp: new Date().toISOString(),
|
|
specReferences: [
|
|
{
|
|
id: 'MCP-Resources-Subscribe',
|
|
url: 'https://modelcontextprotocol.io/specification/2025-06-18/schema#unsubscriberequest'
|
|
}
|
|
]
|
|
});
|
|
|
|
await connection.close();
|
|
} catch (error) {
|
|
checks.push({
|
|
id: 'resources-unsubscribe',
|
|
name: 'ResourcesUnsubscribe',
|
|
description: 'Unsubscribe from resource successfully',
|
|
status: 'FAILURE',
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
specReferences: [
|
|
{
|
|
id: 'MCP-Resources-Subscribe',
|
|
url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#resource-subscriptions'
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
return checks;
|
|
}
|
|
}
|