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