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
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Smoke Tests

on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "18"

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH

- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable

- name: Run smoke tests
run: ./tests/smoke-test.sh
26 changes: 21 additions & 5 deletions mcp-client-python/client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import os
from typing import Optional
from contextlib import AsyncExitStack

Expand All @@ -19,7 +20,14 @@ def __init__(self):
# Initialize session and client objects
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.anthropic = Anthropic()
self._anthropic: Optional[Anthropic] = None

@property
def anthropic(self) -> Anthropic:
"""Lazy-initialize Anthropic client when needed"""
if self._anthropic is None:
self._anthropic = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
return self._anthropic

async def connect_to_server(self, server_script_path: str):
"""Connect to an MCP server
Expand Down Expand Up @@ -65,7 +73,7 @@ async def process_query(self, query: str) -> str:
]

response = await self.session.list_tools()
available_tools = [{
available_tools = [{
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
Expand All @@ -88,7 +96,7 @@ async def process_query(self, query: str) -> str:
elif content.type == 'tool_use':
tool_name = content.name
tool_args = content.input

# Execute tool call
result = await self.session.call_tool(tool_name, tool_args)
final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
Expand All @@ -100,7 +108,7 @@ async def process_query(self, query: str) -> str:
"content": content.text
})
messages.append({
"role": "user",
"role": "user",
"content": result.content
})

Expand Down Expand Up @@ -141,10 +149,18 @@ async def main():
if len(sys.argv) < 2:
print("Usage: python client.py <path_to_server_script>")
sys.exit(1)

client = MCPClient()
try:
await client.connect_to_server(sys.argv[1])

# Check if we have a valid API key to continue
api_key = os.getenv("ANTHROPIC_API_KEY")
if not api_key:
print("\nNo ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:")
print(" export ANTHROPIC_API_KEY=your-api-key-here")
return

await client.chat_loop()
finally:
await client.cleanup()
Expand Down
27 changes: 18 additions & 9 deletions mcp-client-typescript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,23 @@ import dotenv from "dotenv";
dotenv.config(); // load environment variables from .env

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

class MCPClient {
private mcp: Client;
private anthropic: Anthropic;
private _anthropic: Anthropic | null = null;
private transport: StdioClientTransport | null = null;
private tools: Tool[] = [];

constructor() {
// Initialize Anthropic client and MCP client
this.anthropic = new Anthropic({
apiKey: ANTHROPIC_API_KEY,
});
// Initialize MCP client
this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" });
}

private get anthropic(): Anthropic {
// Lazy-initialize Anthropic client when needed
return this._anthropic ??= new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
}

async connectToServer(serverScriptPath: string) {
/**
* Connect to an MCP server
Expand Down Expand Up @@ -182,6 +180,17 @@ async function main() {
const mcpClient = new MCPClient();
try {
await mcpClient.connectToServer(process.argv[2]);

// Check if we have a valid API key to continue
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
console.log(
"\nNo ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:"
);
console.log(" export ANTHROPIC_API_KEY=your-api-key-here");
return;
}

await mcpClient.chatLoop();
} finally {
await mcpClient.cleanup();
Expand Down
1 change: 1 addition & 0 deletions mcp-client-typescript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

115 changes: 115 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 61 additions & 0 deletions tests/helpers/mcp-test-client.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 49 additions & 0 deletions tests/helpers/mock-mcp-server.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading