Skip to content
Open
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
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
A CLI to evaluate MCP servers performance

[![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io)
[![Version](https://img.shields.io/npm/v/mcp-eval.svg)](https://npmjs.org/package/mcp-eval)
[![Downloads/week](https://img.shields.io/npm/dw/mcp-eval.svg)](https://npmjs.org/package/mcp-eval)
[![Version](https://img.shields.io/npm/v/@alpic-ai/mcp-eval.svg)](https://npmjs.org/package/@alpic-ai/mcp-eval)
[![Downloads/week](https://img.shields.io/npm/dw/@alpic-ai/mcp-eval.svg)](https://npmjs.org/package/@alpic-ai/mcp-eval)

<!-- toc -->
* [Quick start](#quick-start)
Expand Down Expand Up @@ -40,6 +40,12 @@ test_cases:
$ npx -y @alpic-ai/mcp-eval@latest run --url=https://mcp.github.com ./myserver.yml
```

For servers requiring authentication, you can pass custom headers:

```
$ npx -y @alpic-ai/mcp-eval@latest run --url=https://nexus.civic.com/hub/mcp -h "Authorization: Bearer ACCESS_TOKEN" ./myserver.yml
```

- Et voilà 🎉!

# Requirements
Expand Down Expand Up @@ -74,21 +80,24 @@ Run the test suite described in the provided YAML file.

```
USAGE
$ mcp-eval run TESTFILE -u <value> [-a anthropic/claude]
$ mcp-eval run TESTFILE -u <value> [-a anthropic/claude] [-h <value>]

ARGUMENTS
TESTFILE YAML file path containing the test suite

FLAGS
-a, --assistant=<option> [default: anthropic/claude] Assistant configuration to use (impact model and system prompt)
<options: anthropic/claude>
-h, --header=<value>... Custom headers to send with requests (format: 'Header: value')
-u, --url=<value> (required) URL of the MCP server

DESCRIPTION
Run the test suite described in the provided YAML file.

EXAMPLES
$ mcp-eval run
$ mcp-eval run myserver.yml --url=https://mcp.example.com

$ mcp-eval run myserver.yml --url=https://nexus.civic.com/hub/mcp -h "Authorization: Bearer TOKEN"
```

_See code: [src/commands/run.ts](https://github.com/alpic-ai/mcp-eval/blob/v0.8.0/src/commands/run.ts)_
Expand Down
42 changes: 37 additions & 5 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,41 @@ export default class Run extends Command {
description: "URL of the MCP server",
required: true,
}),
header: Flags.string({
char: "h",
description: "Custom headers to send with requests (format: 'Header: value')",
multiple: true,
}),
};

private static parseHeaders(headerStrings?: string[]): HeadersInit | undefined {
if (!headerStrings || headerStrings.length === 0) {
return undefined;
}

return Object.fromEntries(
headerStrings.map((headerString) => {
const colonIndex = headerString.indexOf(':');
if (colonIndex === -1) {
throw new Error(`Invalid header format: "${headerString}". Expected format: "Header: value"`);
}

const key = headerString.slice(0, colonIndex).trim();
const value = headerString.slice(colonIndex + 1).trim();

if (!key) {
throw new Error(`Invalid header format: "${headerString}". Header name cannot be empty`);
}

return [key, value];
})
);
}

public async run(): Promise<void> {
const {
args: { testFile },
flags: { assistant, url },
flags: { assistant, url, header },
} = await this.parse(Run);

const testCaseFileParsingResult = TestCaseSchema.safeParse(testFile);
Expand All @@ -166,7 +195,8 @@ export default class Run extends Command {
const testCases = testCaseFileParsingResult.data.test_cases;
this.log(`📚 Found ${testCases.length} test case(s)`);

await this.connect({ url });
const headers = Run.parseHeaders(header);
await this.connect({ url, headers });

const { tools } = await this.client.listTools();
this.log(
Expand Down Expand Up @@ -243,18 +273,20 @@ export default class Run extends Command {
this.exit(0);
}

private async connect({ url }: { url: URL }) {
private async connect({ url, headers }: { url: URL; headers?: HeadersInit }) {
const transportOptions = headers ? { requestInit: { headers } } : undefined;

try {
this.log("🔍 Connecting to the MCP server over StreamableHTTP...");
const streamableTransport = new StreamableHTTPClientTransport(url);
const streamableTransport = new StreamableHTTPClientTransport(url, transportOptions);
await this.client.connect(streamableTransport);
} catch (streamableError) {
this.log(
`❌ Connection failed over StreamableHTTP: ${streamableError}\n🔍 Fallback - Connecting to the MCP server over SSE...`
);

try {
const sseTransport = new SSEClientTransport(url);
const sseTransport = new SSEClientTransport(url, transportOptions);
await this.client.connect(sseTransport);
} catch (sseError) {
this.error(
Expand Down