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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ Each example lives in its own directory with its own README covering prerequisit

All servers communicate over stdio and expose the same two tools: `get_forecast` and `get_alerts` (or `get-forecast` and `get-alerts` in TypeScript).

All clients launch a server, list its tools, and start an interactive chat loop in which Claude can call those tools and the conversation is kept until you type `clear` or `quit`. The Python, TypeScript, and Ruby clients take a path to a server script; the Go and Rust clients take the command to run, plus any arguments.
All clients launch a server, list its tools, and start an interactive chat loop in which Claude can call those tools and the conversation is kept until you type `clear` or `quit`. Each client takes the command that starts the server, plus any arguments.

Note: These example clients need an `ANTHROPIC_API_KEY` to operate. Without a key, each client still connects and lists the server's tools before exiting, so you can verify the MCP wiring without credentials.

You can mix and match across languages: for example, the Python client can drive the TypeScript server. Each client's README lists which server launch forms it supports.
You can mix and match across languages: for example, the Python client can drive the TypeScript server with `uv run client.py node ../weather-server-typescript/build/index.js`. Each client's README shows how to launch its matching server.

The [`tests`](./tests) directory contains the smoke tests that run in CI against these examples. See [`tests/README.md`](./tests/README.md) for how they work.

Expand Down
2 changes: 1 addition & 1 deletion mcp-client-go/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ANTHROPIC_API_KEY=your_api_key_here
ANTHROPIC_API_KEY=
4 changes: 2 additions & 2 deletions mcp-client-go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ cp .env.example .env
Pass the command that starts the server — a binary or an interpreter plus its arguments:

```bash
go run main.go ../weather-server-go/weather
go run . ../weather-server-go/weather
```

(Build the server first with `go build -o weather` in its directory.) Any server command works, for example the Python server:

```bash
go run main.go uv --directory ../weather-server-python run weather.py
go run . uv --directory ../weather-server-python run weather.py
```

Type a question (for example, "What's the weather in Sacramento?") and Claude answers using the server's tools. The client keeps the conversation for the whole session, so a follow-up such as "And the alerts for that state?" works. Type `clear` to start a new conversation, or `quit` to exit.
Expand Down
6 changes: 3 additions & 3 deletions mcp-client-go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func (c *MCPClient) ConnectToServer(ctx context.Context, serverArgs []string) er
client := mcp.NewClient(
&mcp.Implementation{
Name: "mcp-client-go",
Version: "0.1.0",
Version: "1.0.0",
},
nil,
)
Expand All @@ -149,7 +149,7 @@ func (c *MCPClient) ConnectToServer(ctx context.Context, serverArgs []string) er

c.session = session

// List available tools
// The tools are listed once, here; this client doesn't follow later changes.
toolsResult, err := session.ListTools(ctx, &mcp.ListToolsParams{})
if err != nil {
return fmt.Errorf("failed to list tools: %w", err)
Expand Down Expand Up @@ -394,7 +394,7 @@ func (c *MCPClient) Cleanup() error {

func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "Usage: go run main.go <server_script_or_binary> [args...]")
fmt.Fprintln(os.Stderr, "Usage: go run . <server_command> [args...]")
os.Exit(1)
}

Expand Down
6 changes: 3 additions & 3 deletions mcp-client-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ cp .env.example .env

## Run the client

Pass the path to a server script. `.py` servers are launched with `uv`, `.js` servers with `node`:
Pass the command that starts the server, and its arguments:

```bash
uv run client.py ../weather-server-python/weather.py
uv run client.py uv --directory ../weather-server-python run weather.py
```

or, against the TypeScript server (build it first — see its README):

```bash
uv run client.py ../weather-server-typescript/build/index.js
uv run client.py node ../weather-server-typescript/build/index.js
```

Type a question (for example, "What's the weather in Sacramento?") and Claude answers using the server's tools. The client keeps the conversation for the whole session, so a follow-up such as "And the alerts for that state?" works. Type `clear` to start a new conversation, or `quit` to exit.
Expand Down
61 changes: 27 additions & 34 deletions mcp-client-python/client.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import asyncio
import json
import os
import sys
from contextlib import AsyncExitStack
from pathlib import Path

from anthropic import Anthropic
from dotenv import load_dotenv
from mcp import Client, MCPError, StdioServerParameters
from mcp_types import CallToolResult, EmbeddedResource, ImageContent, TextContent, TextResourceContents
from mcp_types import CallToolResult, EmbeddedResource, ImageContent, Implementation, TextContent, TextResourceContents

load_dotenv() # load environment variables from .env

Expand Down Expand Up @@ -58,6 +58,7 @@ def __init__(self):
self.client: Client | None = None
self.exit_stack = AsyncExitStack()
self._anthropic: Anthropic | None = None
self.tools: list[dict] = []
self.messages: list[dict] = []

@property
Expand All @@ -67,47 +68,41 @@ def anthropic(self) -> Anthropic:
self._anthropic = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
return self._anthropic

async def connect_to_server(self, server_script_path: str):
async def connect_to_server(self, server_command: list[str]):
"""Connect to an MCP server

Args:
server_script_path: Path to the server script (.py or .js)
server_command: The command that starts the server, and its arguments
"""
is_python = server_script_path.endswith(".py")
is_js = server_script_path.endswith(".js")
if not (is_python or is_js):
raise ValueError("Server script must be a .py or .js file")

if is_python:
path = Path(server_script_path).resolve()
server_params = StdioServerParameters(
command="uv",
args=["--directory", str(path.parent), "run", path.name],
env=None,
)
else:
server_params = StdioServerParameters(command="node", args=[server_script_path], env=None)
server_params = StdioServerParameters(command=server_command[0], args=server_command[1:])

# Client launches the command itself when given StdioServerParameters.
# "auto" probes server/discover, falling back to the 2025-11-25 handshake.
self.client = await self.exit_stack.enter_async_context(Client(server_params, mode="auto"))
# "auto", the SDK's default, probes server/discover, falling back to the
# 2025-11-25 handshake.
self.client = await self.exit_stack.enter_async_context(
Client(
server_params,
mode="auto",
client_info=Implementation(name="mcp-client-python", version="1.0.0"),
)
)

# List available tools
# The tools are listed once, here; this client doesn't follow later changes.
response = await self.client.list_tools()
tools = response.tools
print(f"\nConnected over protocol {self.client.protocol_version} with tools:", [tool.name for tool in tools])
self.tools = [
{"name": tool.name, "description": tool.description, "input_schema": tool.input_schema}
for tool in response.tools
]
print(
f"\nConnected over protocol {self.client.protocol_version} with tools:",
[tool.name for tool in response.tools],
)

async def process_query(self, query: str) -> str:
"""Process a query using Claude and available tools"""
history_length = len(self.messages)
self.messages.append({"role": "user", "content": query})

tools_response = await self.client.list_tools()
available_tools = [
{"name": tool.name, "description": tool.description, "input_schema": tool.input_schema}
for tool in tools_response.tools
]

final_text = []

for turn in range(MAX_TOOL_TURNS + 1):
Expand All @@ -116,7 +111,7 @@ async def process_query(self, query: str) -> str:
max_tokens=MAX_TOKENS,
system=SYSTEM_PROMPT,
messages=self.messages,
tools=available_tools,
tools=self.tools,
)

if response.stop_reason == "refusal":
Expand Down Expand Up @@ -215,12 +210,12 @@ async def cleanup(self):

async def main():
if len(sys.argv) < 2:
print("Usage: python client.py <path_to_server_script>")
print("Usage: uv run client.py <server_command> [args...]", file=sys.stderr)
sys.exit(1)

client = MCPClient()
try:
await client.connect_to_server(sys.argv[1])
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")
Expand All @@ -235,6 +230,4 @@ async def main():


if __name__ == "__main__":
import sys

asyncio.run(main())
4 changes: 2 additions & 2 deletions mcp-client-ruby/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ cp .env.example .env

## Run the client

Pass the path to a server script. `.rb` servers are launched with `ruby`, `.py` with `python3`, and `.js` with `node`:
Pass the command that starts the server, and its arguments:

```bash
bundle exec ruby client.rb ../weather-server-ruby/weather.rb
bundle exec ruby client.rb ruby ../weather-server-ruby/weather.rb
```

Type a question (for example, "What's the weather in Sacramento?") and Claude answers using the server's tools. The client keeps the conversation for the whole session, so a follow-up such as "And the alerts for that state?" works. Type `clear` to start a new conversation, or `quit` to exit.
Expand Down
33 changes: 13 additions & 20 deletions mcp-client-ruby/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,26 @@ def initialize
@mcp_client = nil
@transport = nil
@anthropic_client = nil
@tools = []
@messages = []
end

def connect_to_server(server_script_path)
command = case File.extname(server_script_path)
when ".rb"
"ruby"
when ".py"
"python3"
when ".js"
"node"
else
raise ArgumentError, "Server script must be a .rb, .py, or .js file."
end

@transport = MCP::Client::Stdio.new(command: command, args: [server_script_path])
def connect_to_server(server_command)
@transport = MCP::Client::Stdio.new(command: server_command.first, args: server_command.drop(1))
@mcp_client = MCP::Client.new(transport: @transport)
@mcp_client.connect
@mcp_client.connect(client_info: { name: "mcp-client-ruby", version: "1.0.0" })

# The tools are listed once, here; this client doesn't follow later changes.
@tools = @mcp_client.tools

# The spec says clients SHOULD validate structured results against the
# schema the tool declares. This SDK's client does not do it, so compile
# each declared schema once here and check results as they arrive.
@output_schemas = @mcp_client.tools.each_with_object({}) do |tool, schemas|
@output_schemas = @tools.each_with_object({}) do |tool, schemas|
schemas[tool.name] = MCP::Tool::OutputSchema.new(tool.output_schema) if tool.output_schema
end

tool_names = @mcp_client.tools.map(&:name)
tool_names = @tools.map(&:name)
puts "\nConnected to server with tools: #{tool_names}"
end

Expand Down Expand Up @@ -122,7 +115,7 @@ def process_query(query)
history_length = @messages.length
@messages << { role: "user", content: query }

available_tools = @mcp_client.tools.map do |tool|
available_tools = @tools.map do |tool|
{ name: tool.name, description: tool.description, input_schema: tool.input_schema }
end

Expand Down Expand Up @@ -229,14 +222,14 @@ def anthropic_client
end

if ARGV.empty?
puts "Usage: ruby client.rb <path_to_server_script>"
warn "Usage: bundle exec ruby client.rb <server_command> [args...]"
exit 1
end

client = MCPClient.new

begin
client.connect_to_server(ARGV[0])
client.connect_to_server(ARGV)

api_key = ENV["ANTHROPIC_API_KEY"]
if api_key.nil? || api_key.empty?
Expand All @@ -249,7 +242,7 @@ def anthropic_client

client.chat_loop
rescue => e
puts "Error: #{e.message}"
warn "Error: #{e.message}"
exit 1
ensure
client.cleanup
Expand Down
1 change: 1 addition & 0 deletions mcp-client-rust/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ANTHROPIC_API_KEY=
3 changes: 2 additions & 1 deletion mcp-client-rust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ impl MCPClient {
)
.await?;

// The tools are listed once, here; this client doesn't follow later changes.
let rmcp_tools = session
.list_all_tools()
.await
Expand Down Expand Up @@ -462,7 +463,7 @@ async fn main() -> Result<()> {
let server_args: Vec<String> = args.collect();

if server_args.is_empty() {
eprintln!("Usage: cargo run -- <server_script_or_binary> [args...]");
eprintln!("Usage: cargo run -- <server_command> [args...]");
std::process::exit(1);
}

Expand Down
4 changes: 2 additions & 2 deletions mcp-client-typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ cp .env.example .env

## Run the client

Pass the path to a server script — `.js` servers are launched with `node`, `.py` servers with `python` (or `python3`, depending on the platform):
Pass the command that starts the server, and its arguments:

```bash
node build/index.js ../weather-server-typescript/build/index.js
node build/index.js node ../weather-server-typescript/build/index.js
```

(Build the server first — see its README.)
Expand Down
30 changes: 9 additions & 21 deletions mcp-client-typescript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class MCPClient {
// 'auto' probes server/discover, falling back to the 2025-11-25 handshake.
// The SDK's default is 'legacy'.
this.mcp = new Client(
{ name: "mcp-client-cli", version: "1.0.0" },
{ name: "mcp-client-typescript", version: "1.0.0" },
{ versionNegotiation: { mode: "auto" } },
);
}
Expand All @@ -88,33 +88,20 @@ class MCPClient {
return this._anthropic ??= new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
}

async connectToServer(serverScriptPath: string) {
async connectToServer(serverCommand: string[]) {
/**
* Connect to an MCP server
*
* @param serverScriptPath - Path to the server script (.py or .js)
* @param serverCommand - The command that starts the server, and its arguments
*/
try {
// Determine script type and appropriate command
const isJs = serverScriptPath.endsWith(".js");
const isPy = serverScriptPath.endsWith(".py");
if (!isJs && !isPy) {
throw new Error("Server script must be a .js or .py file");
}
const command = isPy
? process.platform === "win32"
? "python"
: "python3"
: process.execPath;

// Initialize transport and connect to server
this.transport = new StdioClientTransport({
command,
args: [serverScriptPath],
command: serverCommand[0],
args: serverCommand.slice(1),
});
await this.mcp.connect(this.transport);

// List available tools
// The tools are listed once, here; this client doesn't follow later changes.
const toolsResult = await this.mcp.listTools();
this.tools = toolsResult.tools.map((tool) => {
return {
Expand Down Expand Up @@ -294,12 +281,13 @@ class MCPClient {

async function main() {
if (process.argv.length < 3) {
console.log("Usage: node build/index.js <path_to_server_script>");
console.error("Usage: node build/index.js <server_command> [args...]");
process.exitCode = 1;
return;
}
const mcpClient = new MCPClient();
try {
await mcpClient.connectToServer(process.argv[2]);
await mcpClient.connectToServer(process.argv.slice(2));

// Check if we have a valid API key to continue
const apiKey = process.env.ANTHROPIC_API_KEY;
Expand Down
2 changes: 1 addition & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ node tests/helpers/build/tool-loop-test.js <client command> [args...]
**Example**:

```bash
node tests/helpers/build/tool-loop-test.js node mcp-client-typescript/build/index.js tests/helpers/build/mock-mcp-server.js
node tests/helpers/build/tool-loop-test.js node mcp-client-typescript/build/index.js node tests/helpers/build/mock-mcp-server.js
```

## CI/CD Integration
Expand Down
Loading
Loading