From 1c4efe23e80c98d34172f44fec1452371b359664 Mon Sep 17 00:00:00 2001 From: a-akimov Date: Sat, 7 Nov 2026 08:03:28 +0100 Subject: [PATCH] Make the five clients consistent (#269) All clients now take the server as a command plus its arguments, and identify themselves and report usage errors the same way. - Launching: pass the command that starts the server and its arguments, as Go and Rust already did. TypeScript and Ruby no longer start .py servers with bare python3, which failed for the uv-managed Python server - Client info: every client sends mcp-client- / 1.0.0 - Usage errors: printed to stderr, exit status 1 - Tools: listed once at connect time in every client (Python listed them again on each query) - Python: import sys at the top, drop the unused pathlib import - .env.example: add the missing Rust one, empty Go placeholder - Tests: the mock server rejects clients that don't send the expected client info, the smoke test launches it as a command and checks the usage error - READMEs: run commands updated for the command-plus-args form --- README.md | 4 +- mcp-client-go/.env.example | 2 +- mcp-client-go/README.md | 4 +- mcp-client-go/main.go | 6 +-- mcp-client-python/README.md | 6 +-- mcp-client-python/client.py | 61 +++++++++++++---------------- mcp-client-ruby/README.md | 4 +- mcp-client-ruby/client.rb | 33 ++++++---------- mcp-client-rust/.env.example | 1 + mcp-client-rust/src/main.rs | 3 +- mcp-client-typescript/README.md | 4 +- mcp-client-typescript/index.ts | 30 +++++--------- tests/README.md | 2 +- tests/helpers/mock-mcp-server.ts | 21 ++++++++-- tests/smoke-test.sh | 33 +++++++++++++--- weather-server-go/README.md | 2 +- weather-server-python/README.md | 2 +- weather-server-ruby/README.md | 2 +- weather-server-typescript/README.md | 2 +- 19 files changed, 117 insertions(+), 105 deletions(-) create mode 100644 mcp-client-rust/.env.example diff --git a/README.md b/README.md index e684a366..d5f18ba8 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/mcp-client-go/.env.example b/mcp-client-go/.env.example index 7437ef33..8b3e1258 100644 --- a/mcp-client-go/.env.example +++ b/mcp-client-go/.env.example @@ -1 +1 @@ -ANTHROPIC_API_KEY=your_api_key_here +ANTHROPIC_API_KEY= diff --git a/mcp-client-go/README.md b/mcp-client-go/README.md index 641dcdce..9a28db4a 100644 --- a/mcp-client-go/README.md +++ b/mcp-client-go/README.md @@ -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. diff --git a/mcp-client-go/main.go b/mcp-client-go/main.go index 45249efb..67daec37 100644 --- a/mcp-client-go/main.go +++ b/mcp-client-go/main.go @@ -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, ) @@ -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) @@ -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 [args...]") + fmt.Fprintln(os.Stderr, "Usage: go run . [args...]") os.Exit(1) } diff --git a/mcp-client-python/README.md b/mcp-client-python/README.md index 3e52e1c3..024aef78 100644 --- a/mcp-client-python/README.md +++ b/mcp-client-python/README.md @@ -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. diff --git a/mcp-client-python/client.py b/mcp-client-python/client.py index 563c6d35..351a0a68 100644 --- a/mcp-client-python/client.py +++ b/mcp-client-python/client.py @@ -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 @@ -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 @@ -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): @@ -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": @@ -215,12 +210,12 @@ async def cleanup(self): async def main(): if len(sys.argv) < 2: - print("Usage: python client.py ") + print("Usage: uv run client.py [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") @@ -235,6 +230,4 @@ async def main(): if __name__ == "__main__": - import sys - asyncio.run(main()) diff --git a/mcp-client-ruby/README.md b/mcp-client-ruby/README.md index 126e236e..dea936f0 100644 --- a/mcp-client-ruby/README.md +++ b/mcp-client-ruby/README.md @@ -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. diff --git a/mcp-client-ruby/client.rb b/mcp-client-ruby/client.rb index 2331fff3..5f6eae34 100644 --- a/mcp-client-ruby/client.rb +++ b/mcp-client-ruby/client.rb @@ -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 @@ -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 @@ -229,14 +222,14 @@ def anthropic_client end if ARGV.empty? - puts "Usage: ruby client.rb " + warn "Usage: bundle exec ruby client.rb [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? @@ -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 diff --git a/mcp-client-rust/.env.example b/mcp-client-rust/.env.example new file mode 100644 index 00000000..8b3e1258 --- /dev/null +++ b/mcp-client-rust/.env.example @@ -0,0 +1 @@ +ANTHROPIC_API_KEY= diff --git a/mcp-client-rust/src/main.rs b/mcp-client-rust/src/main.rs index 0426a393..d41544d1 100644 --- a/mcp-client-rust/src/main.rs +++ b/mcp-client-rust/src/main.rs @@ -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 @@ -462,7 +463,7 @@ async fn main() -> Result<()> { let server_args: Vec = args.collect(); if server_args.is_empty() { - eprintln!("Usage: cargo run -- [args...]"); + eprintln!("Usage: cargo run -- [args...]"); std::process::exit(1); } diff --git a/mcp-client-typescript/README.md b/mcp-client-typescript/README.md index 5b972933..796b1dec 100644 --- a/mcp-client-typescript/README.md +++ b/mcp-client-typescript/README.md @@ -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.) diff --git a/mcp-client-typescript/index.ts b/mcp-client-typescript/index.ts index 8668a5c7..6a41ae23 100644 --- a/mcp-client-typescript/index.ts +++ b/mcp-client-typescript/index.ts @@ -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" } }, ); } @@ -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 { @@ -294,12 +281,13 @@ class MCPClient { async function main() { if (process.argv.length < 3) { - console.log("Usage: node build/index.js "); + console.error("Usage: node build/index.js [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; diff --git a/tests/README.md b/tests/README.md index 9748b02b..d3916a9e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -130,7 +130,7 @@ node tests/helpers/build/tool-loop-test.js [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 diff --git a/tests/helpers/mock-mcp-server.ts b/tests/helpers/mock-mcp-server.ts index 1ffb57a5..7bf72b68 100644 --- a/tests/helpers/mock-mcp-server.ts +++ b/tests/helpers/mock-mcp-server.ts @@ -2,7 +2,8 @@ /** * Mock MCP Server for testing clients * - * Verifies that clients call `tools/list`, and advertises tools whose output + * Verifies that clients call `tools/list` and identify themselves as + * `mcp-client-` version 1.0.0, and advertises tools whose output * schemas cover both shapes a structured result can take: an object root, and * an array root (allowed as of protocol revision 2026-07-28). * @@ -24,7 +25,7 @@ * exactly what is in this file. */ -import type { Tool } from "@modelcontextprotocol/server"; +import type { Implementation, Tool } from "@modelcontextprotocol/server"; import { Server } from "@modelcontextprotocol/server"; import { serveStdio } from "@modelcontextprotocol/server/stdio"; import { IMAGE_DATA, RESOURCE_TEXT, STRUCTURED_ONLY_VALUE } from "./mock-values.js"; @@ -32,6 +33,10 @@ import { IMAGE_DATA, RESOURCE_TEXT, STRUCTURED_ONLY_VALUE } from "./mock-values. // Track whether tools/list was called let toolsListCalled = false; +/** The client info every quickstart client sends. */ +const CLIENT_NAME = /^mcp-client-(python|typescript|go|rust|ruby)$/; +const CLIENT_VERSION = "1.0.0"; + const TOOLS: Tool[] = [ { name: "get_alerts", @@ -100,7 +105,17 @@ function buildServer(): Server { { capabilities: { tools: {} } }, ); - server.setRequestHandler("tools/list", async () => { + server.setRequestHandler("tools/list", async (_request, ctx) => { + // 2026-07-28 clients send their info with every request; 2025-11-25 + // clients send it once, in initialize. + const envelope = ctx.mcpReq.envelope as Record | undefined; + const info = (envelope?.["io.modelcontextprotocol/clientInfo"] ?? + server.getClientVersion()) as Implementation | undefined; + if (!info || !CLIENT_NAME.test(info.name) || info.version !== CLIENT_VERSION) { + throw new Error( + `Expected client info mcp-client- ${CLIENT_VERSION}, got ${JSON.stringify(info)}`, + ); + } toolsListCalled = true; return { tools: TOOLS }; }); diff --git a/tests/smoke-test.sh b/tests/smoke-test.sh index 51c2e1ea..1860e911 100755 --- a/tests/smoke-test.sh +++ b/tests/smoke-test.sh @@ -97,12 +97,29 @@ test_weather_server_ruby() { # The client tests drive the no-API-key path, where each client prints a notice # and exits. Empty rather than unset: dotenv skips a name already present in # ENV, so a key in the environment or a local .env would start the chat loop. +# Each first runs the client with no server command, which must print usage to +# stderr, nothing to stdout, and exit 1. + +# Usage: check_usage [args...] +check_usage() { + local stdout stderr_file status + stderr_file=$(mktemp) + stdout=$("$@" 2>"${stderr_file}") && status=0 || status=$? + local stderr + stderr=$(cat "${stderr_file}") + rm -f "${stderr_file}" + if [ "${status}" -ne 1 ] || [ -n "${stdout}" ] || [[ "${stderr}" != *Usage* ]]; then + print_error "with no server command, expected usage on stderr and exit 1 (got exit ${status}, stdout '${stdout}', stderr '${stderr}')" + return 1 + fi +} # Test: Python MCP client test_mcp_client_python() { check_dependency uv || return 1 local client_dir="${PROJECT_ROOT}/mcp-client-python" - ANTHROPIC_API_KEY= uv --directory "${client_dir}" run python "${client_dir}/client.py" "${MOCK_SERVER}" >/dev/null 2>&1 + check_usage uv --directory "${client_dir}" run python "${client_dir}/client.py" || return 1 + ANTHROPIC_API_KEY= uv --directory "${client_dir}" run python "${client_dir}/client.py" node "${MOCK_SERVER}" >/dev/null 2>&1 } # Test: TypeScript MCP client @@ -111,7 +128,8 @@ test_mcp_client_typescript() { check_dependency npm || return 1 local client_dir="${PROJECT_ROOT}/mcp-client-typescript" ensure_built "${client_dir}" "" || return 1 - ANTHROPIC_API_KEY= node "${client_dir}/build/index.js" "${MOCK_SERVER}" >/dev/null 2>&1 + check_usage node "${client_dir}/build/index.js" || return 1 + ANTHROPIC_API_KEY= node "${client_dir}/build/index.js" node "${MOCK_SERVER}" >/dev/null 2>&1 } # Test: Ruby MCP client @@ -120,7 +138,8 @@ test_mcp_client_ruby() { check_dependency bundle || return 1 local client_dir="${PROJECT_ROOT}/mcp-client-ruby" ensure_bundled "${client_dir}" || return 1 - (cd "${client_dir}" && ANTHROPIC_API_KEY= bundle exec ruby client.rb "${MOCK_SERVER}") >/dev/null 2>&1 + (cd "${client_dir}" && check_usage bundle exec ruby client.rb) || return 1 + (cd "${client_dir}" && ANTHROPIC_API_KEY= bundle exec ruby client.rb node "${MOCK_SERVER}") >/dev/null 2>&1 } # Test: Go MCP client @@ -135,6 +154,7 @@ test_mcp_client_go() { return 1 } + check_usage "${client_bin}" || return 1 ANTHROPIC_API_KEY= "${client_bin}" node "${MOCK_SERVER}" >/dev/null 2>&1 } @@ -151,6 +171,7 @@ test_mcp_client_rust() { return 1 } + check_usage "${client_bin}" || return 1 ANTHROPIC_API_KEY= "${client_bin}" node "${MOCK_SERVER}" >/dev/null 2>&1 } @@ -167,7 +188,7 @@ test_mcp_client_rust() { test_tool_loop_python() { check_dependency uv || return 1 local client_dir="${PROJECT_ROOT}/mcp-client-python" - node "${TOOL_LOOP_TEST}" uv --directory "${client_dir}" run python "${client_dir}/client.py" "${MOCK_SERVER}" + node "${TOOL_LOOP_TEST}" uv --directory "${client_dir}" run python "${client_dir}/client.py" node "${MOCK_SERVER}" } # Test: TypeScript MCP client tool loop @@ -176,7 +197,7 @@ test_tool_loop_typescript() { check_dependency npm || return 1 local client_dir="${PROJECT_ROOT}/mcp-client-typescript" ensure_built "${client_dir}" "" || return 1 - node "${TOOL_LOOP_TEST}" node "${client_dir}/build/index.js" "${MOCK_SERVER}" + node "${TOOL_LOOP_TEST}" node "${client_dir}/build/index.js" node "${MOCK_SERVER}" } # Test: Ruby MCP client tool loop @@ -185,7 +206,7 @@ test_tool_loop_ruby() { check_dependency bundle || return 1 local client_dir="${PROJECT_ROOT}/mcp-client-ruby" ensure_bundled "${client_dir}" || return 1 - (cd "${client_dir}" && node "${TOOL_LOOP_TEST}" bundle exec ruby client.rb "${MOCK_SERVER}") + (cd "${client_dir}" && node "${TOOL_LOOP_TEST}" bundle exec ruby client.rb node "${MOCK_SERVER}") } # Test: Go MCP client tool loop diff --git a/weather-server-go/README.md b/weather-server-go/README.md index bc2f8c3b..0130e52b 100644 --- a/weather-server-go/README.md +++ b/weather-server-go/README.md @@ -22,7 +22,7 @@ The server communicates over stdio, so running it directly just waits for a clie ```bash cd ../mcp-client-go -go run main.go ../weather-server-go/weather +go run . ../weather-server-go/weather ``` ## Tool annotations diff --git a/weather-server-python/README.md b/weather-server-python/README.md index b0685e69..f07dcea6 100644 --- a/weather-server-python/README.md +++ b/weather-server-python/README.md @@ -24,7 +24,7 @@ The server communicates over stdio, so running it directly just waits for a clie ```bash cd ../mcp-client-python -uv run client.py ../weather-server-python/weather.py +uv run client.py uv --directory ../weather-server-python run weather.py ``` ## Tool annotations diff --git a/weather-server-ruby/README.md b/weather-server-ruby/README.md index 25d40e44..6464f9a2 100644 --- a/weather-server-ruby/README.md +++ b/weather-server-ruby/README.md @@ -24,7 +24,7 @@ The server communicates over stdio, so running it directly just waits for a clie ```bash cd ../mcp-client-ruby bundle install -bundle exec ruby client.rb ../weather-server-ruby/weather.rb +bundle exec ruby client.rb ruby ../weather-server-ruby/weather.rb ``` ## Tool annotations diff --git a/weather-server-typescript/README.md b/weather-server-typescript/README.md index 51b8f970..a5f1ae97 100644 --- a/weather-server-typescript/README.md +++ b/weather-server-typescript/README.md @@ -25,7 +25,7 @@ The server communicates over stdio, so running it directly just waits for a clie ```bash cd ../mcp-client-typescript npm install && npm run build -node build/index.js ../weather-server-typescript/build/index.js +node build/index.js node ../weather-server-typescript/build/index.js ``` ## Tool annotations