Skip to content

[wip] Client auth compatibility checker #694

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions auth-compat/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
77 changes: 77 additions & 0 deletions auth-compat/JEST_USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Jest Test Suite Usage

## Running Tests with Jest

The MCP Auth Compliance tests are now available as Jest test suites, providing a standard testing experience with better IDE integration and reporting.

### Basic Usage

```bash
# Run all tests with default client
npm test

# Run specific test suite
npm run test:basic # Basic MCP connection tests
npm run test:oauth # OAuth compliance tests
npm run test:metadata # Metadata location tests
npm run test:behavior # Client behavior validation tests

# Run with verbose output
npm run test:verbose

# Run with coverage report
npm run test:coverage

# Watch mode for development
npm run test:watch
```

### Using Custom Client Commands

By default, tests use the example TypeScript client. To test your own client implementation:

```bash
# Set CLIENT_COMMAND environment variable
CLIENT_COMMAND="node my-client.js" npm test

# Or use the test:with-client script
npm run test:with-client --client="python my-client.py"
```

### Environment Variables

- `CLIENT_COMMAND`: Command to execute your MCP client (receives server URL as last argument)
- `VERBOSE`: Set to `true` for detailed output including HTTP traces

### Test Structure

Each test suite corresponds to the original compliance test scenarios:

1. **basic-compliance.test.ts**: Tests basic MCP protocol compliance without authentication
2. **oauth-compliance.test.ts**: Tests OAuth2/OIDC authorization flow
3. **metadata-location.test.ts**: Tests different OAuth metadata discovery scenarios
4. **client-behavior.test.ts**: Validates specific client behaviors and error handling

### Jest Features

The Jest implementation provides:
- Parallel test execution for faster runs
- Built-in coverage reporting
- Better error messages and stack traces
- IDE integration for debugging
- Watch mode for test-driven development
- Custom matchers like `toHavePassedCompliance()`

### Comparison with CLI Runner

Both testing methods are maintained:
- **Jest**: Better for development, CI/CD integration, and detailed reporting
- **CLI Runner**: Better for standalone validation and specific scenario testing

```bash
# Jest approach
npm test

# CLI approach (still available)
npm run cli -- --command "node my-client.js" --suite all
```
136 changes: 136 additions & 0 deletions auth-compat/auth-compliance-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# MCP Authorization Compliance Checker Design

## Overview

A compliance testing system for MCP (Model Context Protocol) clients to validate their authorization implementation against a reference server. The system runs client implementations against a validation server that tests compliance with MCP authorization specifications.

## System Architecture

### Core Components

1. **Test Runner** - Main orchestration script that:
- Accepts a command to run the client implementation
- Spawns the validation server
- Executes the client with the validation server URL
- Collects and reports test results

2. **Validation Server** - MCP server implementation that:
- Implements test methods for all authorization endpoints
- Tracks client interactions and validates behavior
- Generates compliance reports

3. **Mock Authorization Server** - OAuth2/OIDC server that:
- Returns static, predictable authorization responses
- Validates PKCE parameters
- Verifies client authorization flow compliance

## Client Requirements

Clients being tested must:
- Accept a single command-line argument: the MCP server URL
- Connect to the provided MCP server
- Execute authorization flow if required
- Exit with code 0 on success, 1 on failure

## Test Flow

1. Test runner starts validation server on a dynamic port
2. Test runner starts mock authorization server
3. Test runner executes client command with validation server URL
4. Client connects to validation server
5. Validation server presents authorization metadata
6. Client follows authorization flow with mock auth server
7. Client completes MCP initialization
8. Client exits with appropriate status code
9. Test runner collects results from validation server
10. Test runner generates compliance report


## Mock Authorization Server Design

### Static Responses
- Authorization endpoint: Always returns fixed authorization code
- Token endpoint: Returns predictable access/refresh tokens
- JWKS endpoint: Provides static signing keys

### Validation Features
- PKCE code verifier validation
- State parameter tracking
- Redirect URI validation
- Client ID verification

### Simplifications
- No persistent storage required
- No real user authentication
- Fixed token expiration times
- Static signing keys


### Error Details
- Specific validation failures
- Protocol violations
- Missing required parameters
- Timing information

## Directory Structure

```
auth-compat/
├── package.json # Main package configuration
├── tsconfig.json # TypeScript configuration
├── README.md # Project documentation
├── auth-compliance-design.md # This design document
├── src/
│ ├── cli/
│ │ ├── index.ts # CLI entry point & test runner
│ │ ├── commands.ts # CLI command handlers
│ │ ├── config.ts # CLI configuration parser
│ │ └── reporter.ts # Test report generator
│ │
│ ├── server/
│ │ ├── validation/
│ │ │ └── index.ts # Validation server entry
│ │ │
│ │ └── auth/
│ │ └── index.ts # Mock auth server entry
│ │
│ └── types.ts # Shared type definitions
├── examples/
│ └── typescript-client/
│ ├── README.md # Example documentation
│ ├── package.json # Example dependencies
│ ├── test-client.ts # Example TypeScript client
│ └── tsconfig.json # Example TS config
└── reports/ # Generated test reports
└── ...
```

## Implementation Phases

### Phase 1: Basic Framework
- Test runner implementation
- Basic validation server
- Simple pass/fail reporting

### Phase 2: Authorization Testing
- Mock authorization server
- PKCE validation
- OAuth2 flow testing

### Phase 3: Advanced Scenarios
- Multiple test suites
- Comprehensive reporting
- Error scenario testing

### Phase 4: Extensions
- Token refresh testing
- Custom grant types
- Performance metrics


### Future todo

- Need to handle client info, e.g. what redirect url the client will expect responses on.
23 changes: 23 additions & 0 deletions auth-compat/examples/typescript-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# TypeScript MCP Client Example

This is an example TypeScript client that demonstrates how to implement a test client for the MCP Authorization Compliance Checker.

## Requirements

The client must:
1. Accept a single command-line argument: the MCP server URL
2. Connect to the provided MCP server
3. Exit with code 0 on success, 1 on failure

## Usage

```bash
# Install dependencies
npm install

# Run the test client
npm run test-client <server-url>

# Or run directly with tsx
npx tsx test-client.ts <server-url>
```
96 changes: 96 additions & 0 deletions auth-compat/examples/typescript-client/oauth-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
import {
OAuthClientInformation,
OAuthClientInformationFull,
OAuthClientMetadata,
OAuthTokens
} from '@modelcontextprotocol/sdk/shared/auth.js';

export class InMemoryOAuthClientProvider implements OAuthClientProvider {
private _clientInformation?: OAuthClientInformationFull;
private _tokens?: OAuthTokens;
private _codeVerifier?: string;
private _authCode?: string;
private _authCodePromise?: Promise<string>;

constructor(
private readonly _redirectUrl: string | URL,
private readonly _clientMetadata: OAuthClientMetadata,
private readonly _clientMetadataUrl?: string | URL,
) {
}

get redirectUrl(): string | URL {
return this._redirectUrl;
}

get clientMetadata(): OAuthClientMetadata {
return this._clientMetadata;
}

clientInformation(): OAuthClientInformation | undefined {
if (this._clientMetadataUrl) {
console.log("Using client ID metadata URL");
return {
client_id: this._clientMetadataUrl.toString(),
}
}
return this._clientInformation;
}

saveClientInformation(clientInformation: OAuthClientInformationFull): void {
this._clientInformation = clientInformation;
}

tokens(): OAuthTokens | undefined {
return this._tokens;
}

saveTokens(tokens: OAuthTokens): void {
this._tokens = tokens;
}

async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
try {
const response = await fetch(authorizationUrl.toString(), {
redirect: 'manual', // Don't follow redirects automatically
});

// Get the Location header which contains the redirect with auth code
const location = response.headers.get('location');
if (location) {
const redirectUrl = new URL(location);
const code = redirectUrl.searchParams.get('code');
if (code) {
this._authCode = code;
return;
} else {
throw new Error('No auth code in redirect URL');
}
} else {
throw new Error('No redirect location received');
}
} catch (error) {
console.error('Failed to fetch authorization URL:', error);
throw error;
}
}

async getAuthCode(): Promise<string> {
if (this._authCode) {
return this._authCode;
}
throw new Error('No authorization code');
}

saveCodeVerifier(codeVerifier: string): void {
this._codeVerifier = codeVerifier;
}

codeVerifier(): string {
if (!this._codeVerifier) {
throw new Error('No code verifier saved');
}
return this._codeVerifier;
}
}
Loading
Loading