Implement smoke test for clients and servers (#76)
This commit adds `tests/smoke-test.sh`, which performs a smoke test of all clients and servers. The smoke test tests that the clients and servers run and that list tools is called. This commit also adds a GitHub Actions workflow to run the smoke test on all pull requests and pushes to main. For practicality, this commit modifies the TypeScript and Python clients to lazily initialize the Anthropic client, thus allowing tests to skip setting an API key. This change also provides a friendly error when users forget to set an API key. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Alex Akimov <alexeyakimov@gmail.com>
This commit is contained in:
parent
d5d455dcc3
commit
bd27d306c0
8 changed files with 1677 additions and 10 deletions
115
tests/README.md
Normal file
115
tests/README.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# MCP Quickstart Smoke Tests
|
||||
|
||||
This directory contains smoke tests for the MCP quickstart examples. These tests verify that all example servers and clients can start and respond correctly, without calling external APIs.
|
||||
|
||||
## Overview
|
||||
|
||||
The smoke tests verify:
|
||||
|
||||
- **Servers**: Each weather server (Python, TypeScript, Rust) can start and respond to MCP protocol requests
|
||||
- **Clients**: Each MCP client (Python, TypeScript) can connect to a mock server and list tools
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
./tests/smoke-test.sh
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Node.js** 16+
|
||||
- **npm** (for Node.js dependencies)
|
||||
- **Python** 3.10+
|
||||
- **uv** (Python package manager)
|
||||
- **Rust** stable
|
||||
- **Cargo** (for Rust builds)
|
||||
|
||||
## How It Works
|
||||
|
||||
### Server Tests
|
||||
|
||||
Each server test:
|
||||
|
||||
1. Builds/prepares the server if needed
|
||||
2. Uses `mcp-test-client.ts` to connect to the server via stdio
|
||||
3. Sends MCP initialize and `tools/list` requests
|
||||
4. Verifies the server responds with a valid tool list
|
||||
5. Reports pass/fail
|
||||
|
||||
### Client Tests
|
||||
|
||||
Each client test:
|
||||
|
||||
1. Builds/prepares the client if needed
|
||||
2. Runs the client CLI without an ANTHROPIC_API_KEY
|
||||
3. The client connects to a mock server, lists tools, and exits gracefully
|
||||
4. Verifies the client can connect and communicate via MCP protocol
|
||||
5. Reports pass/fail
|
||||
|
||||
**Note**: Client tests run the actual CLI programs without an Anthropic API key. The clients are designed to handle missing API keys gracefully by listing available tools and exiting, which is perfect for smoke testing the MCP connectivity without requiring external API calls.
|
||||
|
||||
## Test Helpers
|
||||
|
||||
### mcp-test-client.ts
|
||||
|
||||
A minimal MCP client that connects to a server, initializes the session, and lists available tools. Used to test servers without requiring a full client implementation.
|
||||
|
||||
**Usage**:
|
||||
|
||||
```bash
|
||||
node tests/helpers/build/mcp-test-client.js <command> [args...]
|
||||
```
|
||||
|
||||
**Example**:
|
||||
|
||||
```bash
|
||||
node tests/helpers/build/mcp-test-client.js python weather.py
|
||||
```
|
||||
|
||||
### mock-mcp-server.ts
|
||||
|
||||
A minimal MCP server that verifies clients call the `tools/list` method and returns an empty tool list. Used to test clients without requiring a real weather server. Exits with an error if the client doesn't call `tools/list`.
|
||||
|
||||
**Usage**:
|
||||
|
||||
```bash
|
||||
node tests/helpers/build/mock-mcp-server.js
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
Tests run automatically on pull requests via GitHub Actions. See `.github/workflows/ci.yml` for the CI configuration.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Dependencies missing
|
||||
|
||||
Install required dependencies:
|
||||
|
||||
```bash
|
||||
# Python/uv
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# Node.js (via nvm)
|
||||
nvm install 18
|
||||
|
||||
# Rust
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
```
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
To add a new test:
|
||||
|
||||
1. Add a new test function in `smoke-test.sh` (e.g., `test_new_feature()`)
|
||||
2. Include dependency checks, builds, and test execution in the function
|
||||
3. Add a `run_test` call in the "Run all tests" section
|
||||
4. Update this README
|
||||
|
||||
## Maintenance
|
||||
|
||||
These tests are designed to be simple and low-maintenance:
|
||||
|
||||
- Shell scripts for orchestration (language-agnostic)
|
||||
- Minimal TypeScript helpers for test infrastructure
|
||||
- No external API dependencies
|
||||
61
tests/helpers/mcp-test-client.ts
Normal file
61
tests/helpers/mcp-test-client.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Minimal MCP Test Client for testing servers
|
||||
* Connects to a server, initializes, and lists tools
|
||||
*/
|
||||
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
|
||||
async function testServer(command: string, args: string[]) {
|
||||
console.error(`Testing server: ${command} ${args.join(" ")}`);
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command,
|
||||
args,
|
||||
});
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: "mcp-test-client",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {},
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
// Connect to server
|
||||
await client.connect(transport);
|
||||
console.error("✓ Connected to server");
|
||||
|
||||
// List tools
|
||||
const { tools } = await client.listTools();
|
||||
console.error(`✓ Listed ${tools.length} tools`);
|
||||
|
||||
// Success
|
||||
console.error("✓ Server test passed");
|
||||
await client.close();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error(`✗ Server test failed: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length < 1) {
|
||||
console.error("Usage: mcp-test-client <command> [args...]");
|
||||
console.error("Example: mcp-test-client node server.js");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const command = args[0];
|
||||
const commandArgs = args.slice(1);
|
||||
|
||||
testServer(command, commandArgs).catch((error) => {
|
||||
console.error(`Fatal error: ${error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
49
tests/helpers/mock-mcp-server.ts
Normal file
49
tests/helpers/mock-mcp-server.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Mock MCP Server for testing clients
|
||||
* Verifies that clients call the tools/list method and returns an empty tool list
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
const server = new McpServer(
|
||||
{
|
||||
name: "mock-test-server",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Track whether tools/list was called
|
||||
let toolsListCalled = false;
|
||||
|
||||
// Override the default tools/list handler to track calls
|
||||
server.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
toolsListCalled = true;
|
||||
return { tools: [] };
|
||||
});
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Mock MCP Server running on stdio");
|
||||
}
|
||||
|
||||
// Verify that tools/list was called when the connection closes
|
||||
process.stdin.on("end", () => {
|
||||
if (!toolsListCalled) {
|
||||
console.error("Error: Client did not call tools/list");
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Server error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
1133
tests/helpers/package-lock.json
generated
Normal file
1133
tests/helpers/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
20
tests/helpers/package.json
Normal file
20
tests/helpers/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "mcp-test-helpers",
|
||||
"version": "1.0.0",
|
||||
"description": "Test helpers for MCP quickstart examples",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.4",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
||||
21
tests/helpers/tsconfig.json
Normal file
21
tests/helpers/tsconfig.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"build"
|
||||
]
|
||||
}
|
||||
116
tests/smoke-test.sh
Executable file
116
tests/smoke-test.sh
Executable file
|
|
@ -0,0 +1,116 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Source utilities
|
||||
source "$(dirname "$0")/utils.sh"
|
||||
|
||||
print_header "MCP Quickstart Smoke Tests"
|
||||
|
||||
# Get project root
|
||||
PROJECT_ROOT=$(get_project_root)
|
||||
TESTS_DIR="${PROJECT_ROOT}/tests"
|
||||
|
||||
# Setup common test variables
|
||||
TEST_CLIENT="${PROJECT_ROOT}/tests/helpers/build/mcp-test-client.js"
|
||||
MOCK_SERVER="${PROJECT_ROOT}/tests/helpers/build/mock-mcp-server.js"
|
||||
|
||||
# Track test results
|
||||
FAILED_TESTS=()
|
||||
PASSED_TESTS=()
|
||||
|
||||
# Build test helpers
|
||||
ensure_helpers_built
|
||||
|
||||
# Helper function to run a test and track results
|
||||
run_test() {
|
||||
local test_name=$1
|
||||
echo ""
|
||||
print_header "Testing ${test_name}"
|
||||
|
||||
if "${@:2}"; then
|
||||
PASSED_TESTS+=("${test_name}")
|
||||
print_success "${test_name} test passed"
|
||||
else
|
||||
FAILED_TESTS+=("${test_name}")
|
||||
print_error "${test_name} test failed"
|
||||
fi
|
||||
}
|
||||
|
||||
# Test: Python weather server
|
||||
test_weather_server_python() {
|
||||
check_dependency uv
|
||||
local server_dir="${PROJECT_ROOT}/weather-server-python"
|
||||
node "${TEST_CLIENT}" uv --directory "${server_dir}" run weather.py
|
||||
}
|
||||
|
||||
# Test: TypeScript weather server
|
||||
test_weather_server_typescript() {
|
||||
check_dependency node
|
||||
check_dependency npm
|
||||
local server_dir="${PROJECT_ROOT}/weather-server-typescript"
|
||||
ensure_built "${server_dir}"
|
||||
node "${TEST_CLIENT}" node "${server_dir}/build/index.js"
|
||||
}
|
||||
|
||||
# Test: Rust weather server
|
||||
test_weather_server_rust() {
|
||||
check_dependency cargo
|
||||
local server_dir="${PROJECT_ROOT}/weather-server-rust"
|
||||
ensure_built "${server_dir}"
|
||||
|
||||
# Determine which binary to use
|
||||
if [ -f "${server_dir}/target/release/weather" ]; then
|
||||
local server_bin="${server_dir}/target/release/weather"
|
||||
else
|
||||
local server_bin="${server_dir}/target/debug/weather"
|
||||
fi
|
||||
|
||||
node "${TEST_CLIENT}" "${server_bin}"
|
||||
}
|
||||
|
||||
# Test: Python MCP client
|
||||
test_mcp_client_python() {
|
||||
check_dependency uv
|
||||
local client_dir="${PROJECT_ROOT}/mcp-client-python"
|
||||
uv --directory "${client_dir}" run python "${client_dir}/client.py" "${MOCK_SERVER}" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Test: TypeScript MCP client
|
||||
test_mcp_client_typescript() {
|
||||
check_dependency node
|
||||
check_dependency npm
|
||||
local client_dir="${PROJECT_ROOT}/mcp-client-typescript"
|
||||
ensure_built "${client_dir}"
|
||||
node "${client_dir}/build/index.js" "${MOCK_SERVER}" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Run all tests
|
||||
print_header "Running smoke tests"
|
||||
run_test "weather-server-python" test_weather_server_python
|
||||
run_test "weather-server-typescript" test_weather_server_typescript
|
||||
run_test "weather-server-rust" test_weather_server_rust
|
||||
run_test "mcp-client-python" test_mcp_client_python
|
||||
run_test "mcp-client-typescript" test_mcp_client_typescript
|
||||
|
||||
# Print summary
|
||||
echo ""
|
||||
print_header "Test Summary"
|
||||
echo "Passed: ${#PASSED_TESTS[@]}"
|
||||
for test in "${PASSED_TESTS[@]}"; do
|
||||
print_success "${test}"
|
||||
done
|
||||
|
||||
if [ ${#FAILED_TESTS[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
echo "Failed: ${#FAILED_TESTS[@]}"
|
||||
for test in "${FAILED_TESTS[@]}"; do
|
||||
print_error "${test}"
|
||||
done
|
||||
echo ""
|
||||
print_error "Some tests failed"
|
||||
exit 1
|
||||
else
|
||||
echo ""
|
||||
print_success "All tests passed!"
|
||||
exit 0
|
||||
fi
|
||||
86
tests/utils.sh
Normal file
86
tests/utils.sh
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
#!/bin/bash
|
||||
# Shared utilities for test scripts
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Print functions
|
||||
print_header() {
|
||||
echo -e "${YELLOW}==== $1 ====${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗ $1${NC}"
|
||||
}
|
||||
|
||||
# Get project root directory
|
||||
get_project_root() {
|
||||
cd "$(dirname "$0")/.." && pwd
|
||||
}
|
||||
|
||||
# Check if command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Check dependency and exit if not found
|
||||
check_dependency() {
|
||||
local cmd=$1
|
||||
if ! command_exists "${cmd}"; then
|
||||
print_error "${cmd} is not installed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Setup test environment
|
||||
setup_test() {
|
||||
local test_name=$1
|
||||
print_header "Testing ${test_name}"
|
||||
|
||||
PROJECT_ROOT=$(get_project_root)
|
||||
SERVER_DIR="${PROJECT_ROOT}/${test_name}"
|
||||
CLIENT_DIR="${PROJECT_ROOT}/${test_name}"
|
||||
TEST_CLIENT="${PROJECT_ROOT}/tests/helpers/build/mcp-test-client.js"
|
||||
MOCK_SERVER="${PROJECT_ROOT}/tests/helpers/build/mock-mcp-server.js"
|
||||
}
|
||||
|
||||
# Ensure test helpers are built
|
||||
ensure_helpers_built() {
|
||||
if [ ! -f "${TEST_CLIENT}" ] || [ ! -f "${MOCK_SERVER}" ]; then
|
||||
print_error "Test helpers not built"
|
||||
print_header "Building test helpers..."
|
||||
cd "${PROJECT_ROOT}/tests/helpers" || exit 1
|
||||
npm install >/dev/null 2>&1
|
||||
npm run build >/dev/null 2>&1
|
||||
cd - >/dev/null || exit 1
|
||||
print_success "Test helpers built"
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure a project directory is built (TypeScript/Rust)
|
||||
ensure_built() {
|
||||
local dir=$1
|
||||
cd "${dir}" || exit 1
|
||||
|
||||
# Install npm dependencies if needed
|
||||
if [ -f "package.json" ] && [ ! -d "node_modules" ]; then
|
||||
npm install >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Build TypeScript if needed
|
||||
if [ -f "tsconfig.json" ] && [ ! -f "build/index.js" ]; then
|
||||
npm run build >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Build Rust if needed
|
||||
if [ -f "Cargo.toml" ] && [ ! -f "target/release/weather" ] && [ ! -f "target/debug/weather" ]; then
|
||||
cargo build --release >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
Loading…
Reference in a new issue