Skip to content

Commit 10e281e

Browse files
Implement smoke test for clients and servers
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>
1 parent 811d462 commit 10e281e

File tree

12 files changed

+1679
-14
lines changed

12 files changed

+1679
-14
lines changed

.github/workflows/ci.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Smoke Tests
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
push:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout repository
15+
uses: actions/checkout@v4
16+
17+
- name: Set up Node.js
18+
uses: actions/setup-node@v4
19+
with:
20+
node-version: "18"
21+
22+
- name: Set up Python
23+
uses: actions/setup-python@v5
24+
with:
25+
python-version: "3.10"
26+
27+
- name: Install uv
28+
run: |
29+
curl -LsSf https://astral.sh/uv/install.sh | sh
30+
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
31+
32+
- name: Set up Rust
33+
uses: actions-rust-lang/setup-rust-toolchain@v1
34+
with:
35+
toolchain: stable
36+
37+
- name: Run smoke tests
38+
run: ./tests/smoke-test.sh

mcp-client-python/client.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import os
23
from typing import Optional
34
from contextlib import AsyncExitStack
45

@@ -19,7 +20,14 @@ def __init__(self):
1920
# Initialize session and client objects
2021
self.session: Optional[ClientSession] = None
2122
self.exit_stack = AsyncExitStack()
22-
self.anthropic = Anthropic()
23+
self._anthropic: Optional[Anthropic] = None
24+
25+
@property
26+
def anthropic(self) -> Anthropic:
27+
"""Lazy-initialize Anthropic client when needed"""
28+
if self._anthropic is None:
29+
self._anthropic = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
30+
return self._anthropic
2331

2432
async def connect_to_server(self, server_script_path: str):
2533
"""Connect to an MCP server
@@ -65,7 +73,7 @@ async def process_query(self, query: str) -> str:
6573
]
6674

6775
response = await self.session.list_tools()
68-
available_tools = [{
76+
available_tools = [{
6977
"name": tool.name,
7078
"description": tool.description,
7179
"input_schema": tool.inputSchema
@@ -88,7 +96,7 @@ async def process_query(self, query: str) -> str:
8896
elif content.type == 'tool_use':
8997
tool_name = content.name
9098
tool_args = content.input
91-
99+
92100
# Execute tool call
93101
result = await self.session.call_tool(tool_name, tool_args)
94102
final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
@@ -100,7 +108,7 @@ async def process_query(self, query: str) -> str:
100108
"content": content.text
101109
})
102110
messages.append({
103-
"role": "user",
111+
"role": "user",
104112
"content": result.content
105113
})
106114

@@ -141,10 +149,18 @@ async def main():
141149
if len(sys.argv) < 2:
142150
print("Usage: python client.py <path_to_server_script>")
143151
sys.exit(1)
144-
152+
145153
client = MCPClient()
146154
try:
147155
await client.connect_to_server(sys.argv[1])
156+
157+
# Check if we have a valid API key to continue
158+
api_key = os.getenv("ANTHROPIC_API_KEY")
159+
if not api_key:
160+
print("\nNo ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:")
161+
print(" export ANTHROPIC_API_KEY=your-api-key-here")
162+
return
163+
148164
await client.chat_loop()
149165
finally:
150166
await client.cleanup()

mcp-client-typescript/index.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,23 @@ import dotenv from "dotenv";
1313
dotenv.config(); // load environment variables from .env
1414

1515
const ANTHROPIC_MODEL = "claude-sonnet-4-5";
16-
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
17-
if (!ANTHROPIC_API_KEY) {
18-
throw new Error("ANTHROPIC_API_KEY is not set");
19-
}
2016

2117
class MCPClient {
2218
private mcp: Client;
23-
private anthropic: Anthropic;
19+
private _anthropic: Anthropic | null = null;
2420
private transport: StdioClientTransport | null = null;
2521
private tools: Tool[] = [];
2622

2723
constructor() {
28-
// Initialize Anthropic client and MCP client
29-
this.anthropic = new Anthropic({
30-
apiKey: ANTHROPIC_API_KEY,
31-
});
24+
// Initialize MCP client
3225
this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" });
3326
}
3427

28+
private get anthropic(): Anthropic {
29+
// Lazy-initialize Anthropic client when needed
30+
return this._anthropic ??= new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
31+
}
32+
3533
async connectToServer(serverScriptPath: string) {
3634
/**
3735
* Connect to an MCP server
@@ -182,6 +180,17 @@ async function main() {
182180
const mcpClient = new MCPClient();
183181
try {
184182
await mcpClient.connectToServer(process.argv[2]);
183+
184+
// Check if we have a valid API key to continue
185+
const apiKey = process.env.ANTHROPIC_API_KEY;
186+
if (!apiKey) {
187+
console.log(
188+
"\nNo ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:"
189+
);
190+
console.log(" export ANTHROPIC_API_KEY=your-api-key-here");
191+
return;
192+
}
193+
185194
await mcpClient.chatLoop();
186195
} catch (e) {
187196
console.error("Error:", e);

tests/README.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# MCP Quickstart Smoke Tests
2+
3+
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.
4+
5+
## Overview
6+
7+
The smoke tests verify:
8+
9+
- **Servers**: Each weather server (Python, TypeScript, Rust) can start and respond to MCP protocol requests
10+
- **Clients**: Each MCP client (Python, TypeScript) can connect to a mock server and list tools
11+
12+
## Running Tests
13+
14+
```bash
15+
./tests/smoke-test.sh
16+
```
17+
18+
## Requirements
19+
20+
- **Node.js** 16+
21+
- **npm** (for Node.js dependencies)
22+
- **Python** 3.10+
23+
- **uv** (Python package manager)
24+
- **Rust** stable
25+
- **Cargo** (for Rust builds)
26+
27+
## How It Works
28+
29+
### Server Tests
30+
31+
Each server test:
32+
33+
1. Builds/prepares the server if needed
34+
2. Uses `mcp-test-client.ts` to connect to the server via stdio
35+
3. Sends MCP initialize and `tools/list` requests
36+
4. Verifies the server responds with a valid tool list
37+
5. Reports pass/fail
38+
39+
### Client Tests
40+
41+
Each client test:
42+
43+
1. Builds/prepares the client if needed
44+
2. Runs the client CLI without an ANTHROPIC_API_KEY
45+
3. The client connects to a mock server, lists tools, and exits gracefully
46+
4. Verifies the client can connect and communicate via MCP protocol
47+
5. Reports pass/fail
48+
49+
**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.
50+
51+
## Test Helpers
52+
53+
### mcp-test-client.ts
54+
55+
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.
56+
57+
**Usage**:
58+
59+
```bash
60+
node tests/helpers/build/mcp-test-client.js <command> [args...]
61+
```
62+
63+
**Example**:
64+
65+
```bash
66+
node tests/helpers/build/mcp-test-client.js python weather.py
67+
```
68+
69+
### mock-mcp-server.ts
70+
71+
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`.
72+
73+
**Usage**:
74+
75+
```bash
76+
node tests/helpers/build/mock-mcp-server.js
77+
```
78+
79+
## CI/CD Integration
80+
81+
Tests run automatically on pull requests via GitHub Actions. See `.github/workflows/ci.yml` for the CI configuration.
82+
83+
## Troubleshooting
84+
85+
### Dependencies missing
86+
87+
Install required dependencies:
88+
89+
```bash
90+
# Python/uv
91+
curl -LsSf https://astral.sh/uv/install.sh | sh
92+
93+
# Node.js (via nvm)
94+
nvm install 18
95+
96+
# Rust
97+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
98+
```
99+
100+
## Adding New Tests
101+
102+
To add a new test:
103+
104+
1. Add a new test function in `smoke-test.sh` (e.g., `test_new_feature()`)
105+
2. Include dependency checks, builds, and test execution in the function
106+
3. Add a `run_test` call in the "Run all tests" section
107+
4. Update this README
108+
109+
## Maintenance
110+
111+
These tests are designed to be simple and low-maintenance:
112+
113+
- Shell scripts for orchestration (language-agnostic)
114+
- Minimal TypeScript helpers for test infrastructure
115+
- No external API dependencies

tests/helpers/mcp-test-client.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Minimal MCP Test Client for testing servers
4+
* Connects to a server, initializes, and lists tools
5+
*/
6+
7+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
9+
10+
async function testServer(command: string, args: string[]) {
11+
console.error(`Testing server: ${command} ${args.join(" ")}`);
12+
13+
const transport = new StdioClientTransport({
14+
command,
15+
args,
16+
});
17+
18+
const client = new Client(
19+
{
20+
name: "mcp-test-client",
21+
version: "1.0.0",
22+
},
23+
{
24+
capabilities: {},
25+
}
26+
);
27+
28+
try {
29+
// Connect to server
30+
await client.connect(transport);
31+
console.error("✓ Connected to server");
32+
33+
// List tools
34+
const { tools } = await client.listTools();
35+
console.error(`✓ Listed ${tools.length} tools`);
36+
37+
// Success
38+
console.error("✓ Server test passed");
39+
await client.close();
40+
process.exit(0);
41+
} catch (error) {
42+
console.error(`✗ Server test failed: ${error}`);
43+
process.exit(1);
44+
}
45+
}
46+
47+
// Parse command line arguments
48+
const args = process.argv.slice(2);
49+
if (args.length < 1) {
50+
console.error("Usage: mcp-test-client <command> [args...]");
51+
console.error("Example: mcp-test-client node server.js");
52+
process.exit(1);
53+
}
54+
55+
const command = args[0];
56+
const commandArgs = args.slice(1);
57+
58+
testServer(command, commandArgs).catch((error) => {
59+
console.error(`Fatal error: ${error}`);
60+
process.exit(1);
61+
});

tests/helpers/mock-mcp-server.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Mock MCP Server for testing clients
4+
* Verifies that clients call the tools/list method and returns an empty tool list
5+
*/
6+
7+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9+
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
10+
11+
const server = new McpServer(
12+
{
13+
name: "mock-test-server",
14+
version: "1.0.0",
15+
},
16+
{
17+
capabilities: {
18+
tools: {},
19+
},
20+
}
21+
);
22+
23+
// Track whether tools/list was called
24+
let toolsListCalled = false;
25+
26+
// Override the default tools/list handler to track calls
27+
server.server.setRequestHandler(ListToolsRequestSchema, async () => {
28+
toolsListCalled = true;
29+
return { tools: [] };
30+
});
31+
32+
async function main() {
33+
const transport = new StdioServerTransport();
34+
await server.connect(transport);
35+
console.error("Mock MCP Server running on stdio");
36+
}
37+
38+
// Verify that tools/list was called when the connection closes
39+
process.stdin.on("end", () => {
40+
if (!toolsListCalled) {
41+
console.error("Error: Client did not call tools/list");
42+
process.exit(1);
43+
}
44+
});
45+
46+
main().catch((error) => {
47+
console.error("Server error:", error);
48+
process.exit(1);
49+
});

0 commit comments

Comments
 (0)