Follow Discord
Sweep 22 Oct 2026 · 04:32Z Build v2.1.308 527 read Stable v2.1.308 Latest v2.1.308 Next v2.1.308 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One capture · mcp

One read of Model Context Protocolmcp-20261020T200724Z

306 pages moved out of 357 read.

Pages moved 306 significant first
Pages read 357 in this capture
Captured 20:07 UTC
Corpus hash 077a51951f96 corpus-hash

What this read moved

76-100 of 306, page 4 of 13

This capture is too large to show at once. Changes 76-100 of 306 are below, significant first; the rest are on the following screens.

docs/2026-07-28/getting-started/intro Changed · +19 / -27 lines

from line 6
66 
77Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect electronic devices, MCP provides a standardized way to connect AI applications to external systems.
88 
9<Frame>
10 <img src="https://mintcdn.com/mcp/bEUxYpZqie0DsluH/images/mcp-simple-diagram.png?fit=max&auto=format&n=bEUxYpZqie0DsluH&q=85&s=35268aa0ad50b8c385913810e7604550" width="3840" height="1500" data-path="images/mcp-simple-diagram.png" />
11</Frame>
9![](https://mintcdn.com/mcp/bEUxYpZqie0DsluH/images/mcp-simple-diagram.png?fit=max\&auto=format\&n=bEUxYpZqie0DsluH\&q=85\&s=35268aa0ad50b8c385913810e7604550)
1210 
1311## What can MCP enable?
1412 
from line 29
3129 
3230## Start Building
3331 
34<CardGroup cols={2}>
35 <Card title="Build servers" icon="server" href="/docs/2026-07-28/develop/build-server">
36 Create MCP servers to expose your data and tools
37 </Card>
32> **[Build servers](/docs/2026-07-28/develop/build-server)**
33>
34> Create MCP servers to expose your data and tools
3835 
39 <Card title="Build clients" icon="computer" href="/docs/2026-07-28/develop/build-client">
40 Develop applications that connect to MCP servers
41 </Card>
36> **[Build clients](/docs/2026-07-28/develop/build-client)**
37>
38> Develop applications that connect to MCP servers
4239 
43 <Card title="Build MCP Apps" icon="puzzle-piece" href="/extensions/apps/overview">
44 Build interactive apps that run inside AI clients
45 </Card>
46</CardGroup>
40> **[Build MCP Apps](/extensions/apps/overview)**
41>
42> Build interactive apps that run inside AI clients
4743 
4844## Learn more
4945 
50<CardGroup cols={2}>
51 <Card title="Architecture" icon="book" href="/docs/2026-07-28/learn/architecture">
52 Learn the core concepts and architecture of MCP
53 </Card>
46> **[Architecture](/docs/2026-07-28/learn/architecture)**
47>
48> Learn the core concepts and architecture of MCP
5449 
55 <Card title="Security" icon="lock" href="/docs/2026-07-28/tutorials/security/security_best_practices">
56 Understand the security considerations and best practices for MCP
57 </Card>
58</CardGroup>
50> **[Security](/docs/2026-07-28/tutorials/security/security_best_practices)**
51>
52> Understand the security considerations and best practices for MCP
5953 
6054## Community
6155 
62<CardGroup cols={2}>
63 <Card title="Contributing" icon="users" href="/community/contributing">
64 Learn how to get involved and contribute to MCP
65 </Card>
66</CardGroup>
56> **[Contributing](/community/contributing)**
57>
58> Learn how to get involved and contribute to MCP
6759 

docs/2026-07-28/learn/architecture Changed · +399 / -407 lines

#### Understanding the Discovery Exchange #### How This Works in AI Applications #### Understanding the Tool Discovery Request #### Understanding the Tool Discovery Response #### How This Works in AI Applications #### Understanding the Tool Execution Request #### Key Elements of Tool Execution #### Understanding the Tool Execution Response #### How This Works in AI Applications #### Subscribing to Changes #### Understanding Tool List Change Notifications #### Key Features of MCP Notifications #### Client Response to Notifications #### Why Notifications Matter #### How This Works in AI Applications

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 15
1515* **MCP Development Tools**: Tools for developing MCP servers and clients, including the [MCP Inspector](https://github.com/modelcontextprotocol/inspector)
1616* [MCP Reference Server Implementations](https://github.com/modelcontextprotocol/servers): Reference implementations of MCP servers.
1717 
18<Note>
19 MCP focuses solely on the protocol for context exchange—it does not dictate
20 how AI applications use LLMs or manage the provided context.
21</Note>
18> [!NOTE]
19>
20> MCP focuses solely on the protocol for context exchange—it does not dictate
21> how AI applications use LLMs or manage the provided context.
2222 
2323## Concepts of MCP
2424 
from line 104
104104 
105105#### Statelessness and discovery
106106 
107MCP is a <Tooltip tip="Every request contains all the information needed to process it, so servers infer nothing from previous requests">stateless protocol</Tooltip>. Every request carries the protocol version and the <Tooltip tip="Features and operations that a client or server supports, such as tools, resources, or prompts">capabilities</Tooltip> relevant to that request in its `_meta` field, so the server can process each request on its own. Clients should also identify themselves in the same field unless configured not to. Servers advertise their supported versions and capabilities through the mandatory [`server/discover`](/specification/2026-07-28/server/discover) request, which clients may send before any other request. Detailed information can be found in the [specification](/specification/2026-07-28/basic/index#statelessness), and the [example](#example) showcases the per-request metadata and the discovery sequence.
107MCP is a stateless protocol (Every request contains all the information needed to process it, so servers infer nothing from previous requests). Every request carries the protocol version and the capabilities (Features and operations that a client or server supports, such as tools, resources, or prompts) relevant to that request in its `_meta` field, so the server can process each request on its own. Clients should also identify themselves in the same field unless configured not to. Servers advertise their supported versions and capabilities through the mandatory [`server/discover`](/specification/2026-07-28/server/discover) request, which clients may send before any other request. Detailed information can be found in the [specification](/specification/2026-07-28/basic/index#statelessness), and the [example](#example) showcases the per-request metadata and the discovery sequence.
108108 
109109#### Primitives
110110 
from line 148
148148 
149149This section provides a step-by-step walkthrough of an MCP client-server interaction, focusing on the data layer protocol. We'll demonstrate discovery, tool operations, and notifications using JSON-RPC 2.0 messages.
150150 
151<Steps>
152 <Step title="Discovery">
153 As described in the [statelessness and discovery](#statelessness-and-discovery) section, every MCP request carries the protocol version and client capabilities in its `_meta` field, and clients should also include their identity there. A client that wants to learn what a server supports before issuing other requests sends a `server/discover` request, which every server must implement. The discovery response is typically cacheable, meaning it can be re-used so the discovery flow does not need to be performed for every request.
154 
155 <CodeGroup>
156 ```json Discover Request theme={null}
151**1. Discovery**
152 
153As described in the [statelessness and discovery](#statelessness-and-discovery) section, every MCP request carries the protocol version and client capabilities in its `_meta` field, and clients should also include their identity there. A client that wants to learn what a server supports before issuing other requests sends a `server/discover` request, which every server must implement. The discovery response is typically cacheable, meaning it can be re-used so the discovery flow does not need to be performed for every request.
154 
155```json Discover Request theme={null}
156{
157 "jsonrpc": "2.0",
158 "id": 1,
159 "method": "server/discover",
160 "params": {
161 "_meta": {
162 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
163 "io.modelcontextprotocol/clientInfo": {
164 "name": "example-client",
165 "version": "1.0.0"
166 },
167 "io.modelcontextprotocol/clientCapabilities": {
168 "elicitation": {}
169 }
170 }
171 }
172}
173```
174 
175```json Discover Response theme={null}
176{
177 "jsonrpc": "2.0",
178 "id": 1,
179 "result": {
180 "resultType": "complete",
181 "supportedVersions": ["2026-07-28"],
182 "capabilities": {
183 "tools": {
184 "listChanged": true
185 },
186 "resources": {}
187 },
188 "_meta": {
189 "io.modelcontextprotocol/serverInfo": {
190 "name": "example-server",
191 "version": "1.0.0"
192 }
193 },
194 "ttlMs": 3600000,
195 "cacheScope": "public"
196 }
197}
198```
199 
200#### Understanding the Discovery Exchange
201 
202The `_meta` fields and the discovery response together serve several purposes:
203 
2041. **Protocol Version Selection**: The `io.modelcontextprotocol/protocolVersion` field declares the version the client is speaking on this request, and `supportedVersions` in the response lists the versions the server accepts. If a server does not support the requested version, it rejects the request with an `UnsupportedProtocolVersionError` listing the versions it does support, and the client retries with a mutually supported version.
205 
2062. **Capability Discovery**: The client declares its capabilities in `io.modelcontextprotocol/clientCapabilities` on every request, and the server returns its own `capabilities` object from `server/discover`. This tells each party which [primitives](#primitives) the other can handle (tools, resources, prompts) and whether change [notifications](#notifications) are available, so unsupported operations are never attempted.
207 
2083. **Identity Exchange**: The `io.modelcontextprotocol/clientInfo` field in the request's `_meta` and the `io.modelcontextprotocol/serverInfo` field in the result's `_meta` provide identification and versioning information for debugging and compatibility purposes.
209 
210In this example, the exchange demonstrates how MCP capabilities are declared:
211 
212**Client Capabilities**:
213 
214* `"elicitation": {}` - The client declares it can gather additional input from the user when the server requests it
215 
216**Server Capabilities**:
217 
218* `"tools": {"listChanged": true}` - The server supports the tools primitive and can honor a `toolsListChanged` filter in [`subscriptions/listen`](/specification/2026-07-28/basic/patterns/subscriptions). Clients that request this filter receive `notifications/tools/list_changed` when the tool list changes.
219* `"resources": {}` - The server also supports the resources primitive (can handle `resources/list` and `resources/read` methods)
220 
221Calling `server/discover` is optional. Because every request carries the same `_meta` fields, a client is free to send any request directly and handle a version error if one comes back. Discovery is a convenient way to fetch the server's identity, capabilities, and supported versions in a single request.
222 
223#### How This Works in AI Applications
224 
225The AI application's MCP client manager connects to configured servers and stores their discovered capabilities for later use. The application uses this information to determine which servers can provide specific types of functionality (tools, resources, prompts) and whether they support real-time updates. In the Python SDK, discovery happens while the client connects. The results are then available on the client object.
226 
227```python Pseudo-code for AI application discovery theme={null}
228# Pseudo Code
229async with Client(stdio_client(server_config)) as client:
230 if client.server_capabilities.tools:
231 app.register_mcp_server(client, supports_tools=True)
232 app.set_server_ready(client)
233```
234 
235**2. Tool Discovery (Primitives)**
236 
237The client can discover available tools by sending a `tools/list` request. This request is fundamental to MCP's tool discovery mechanism: it allows clients to understand what tools are available on the server before attempting to use them.
238 
239```json Tools List Request theme={null}
240{
241 "jsonrpc": "2.0",
242 "id": 2,
243 "method": "tools/list",
244 "params": {
245 "_meta": {
246 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
247 "io.modelcontextprotocol/clientInfo": {
248 "name": "example-client",
249 "version": "1.0.0"
250 },
251 "io.modelcontextprotocol/clientCapabilities": {
252 "elicitation": {}
253 }
254 }
255 }
256}
257```
258 
259```json Tools List Response theme={null}
260{
261 "jsonrpc": "2.0",
262 "id": 2,
263 "result": {
264 "resultType": "complete",
265 "tools": [
157266 {
158 "jsonrpc": "2.0",
159 "id": 1,
160 "method": "server/discover",
161 "params": {
162 "_meta": {
163 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
164 "io.modelcontextprotocol/clientInfo": {
165 "name": "example-client",
166 "version": "1.0.0"
167 },
168 "io.modelcontextprotocol/clientCapabilities": {
169 "elicitation": {}
170 }
171 }
172 }
173 }
174 ```
175 
176 ```json Discover Response theme={null}
177 {
178 "jsonrpc": "2.0",
179 "id": 1,
180 "result": {
181 "resultType": "complete",
182 "supportedVersions": ["2026-07-28"],
183 "capabilities": {
184 "tools": {
185 "listChanged": true
186 },
187 "resources": {}
188 },
189 "_meta": {
190 "io.modelcontextprotocol/serverInfo": {
191 "name": "example-server",
192 "version": "1.0.0"
267 "name": "calculator_arithmetic",
268 "title": "Calculator",
269 "description": "Perform mathematical calculations including basic arithmetic, trigonometric functions, and algebraic operations",
270 "inputSchema": {
271 "type": "object",
272 "properties": {
273 "expression": {
274 "type": "string",
275 "description": "Mathematical expression to evaluate (e.g., '2 + 3 * 4', 'sin(30)', 'sqrt(16)')"
193276 }
194277 },
195 "ttlMs": 3600000,
196 "cacheScope": "public"
278 "required": ["expression"]
197279 }
198 }
199 ```
200 </CodeGroup>
201 
202 #### Understanding the Discovery Exchange
203 
204 The `_meta` fields and the discovery response together serve several purposes:
205 
206 1. **Protocol Version Selection**: The `io.modelcontextprotocol/protocolVersion` field declares the version the client is speaking on this request, and `supportedVersions` in the response lists the versions the server accepts. If a server does not support the requested version, it rejects the request with an `UnsupportedProtocolVersionError` listing the versions it does support, and the client retries with a mutually supported version.
207 
208 2. **Capability Discovery**: The client declares its capabilities in `io.modelcontextprotocol/clientCapabilities` on every request, and the server returns its own `capabilities` object from `server/discover`. This tells each party which [primitives](#primitives) the other can handle (tools, resources, prompts) and whether change [notifications](#notifications) are available, so unsupported operations are never attempted.
209 
210 3. **Identity Exchange**: The `io.modelcontextprotocol/clientInfo` field in the request's `_meta` and the `io.modelcontextprotocol/serverInfo` field in the result's `_meta` provide identification and versioning information for debugging and compatibility purposes.
211 
212 In this example, the exchange demonstrates how MCP capabilities are declared:
213 
214 **Client Capabilities**:
215 
216 * `"elicitation": {}` - The client declares it can gather additional input from the user when the server requests it
217 
218 **Server Capabilities**:
219 
220 * `"tools": {"listChanged": true}` - The server supports the tools primitive and can honor a `toolsListChanged` filter in [`subscriptions/listen`](/specification/2026-07-28/basic/patterns/subscriptions). Clients that request this filter receive `notifications/tools/list_changed` when the tool list changes.
221 * `"resources": {}` - The server also supports the resources primitive (can handle `resources/list` and `resources/read` methods)
222 
223 Calling `server/discover` is optional. Because every request carries the same `_meta` fields, a client is free to send any request directly and handle a version error if one comes back. Discovery is a convenient way to fetch the server's identity, capabilities, and supported versions in a single request.
224 
225 #### How This Works in AI Applications
226 
227 The AI application's MCP client manager connects to configured servers and stores their discovered capabilities for later use. The application uses this information to determine which servers can provide specific types of functionality (tools, resources, prompts) and whether they support real-time updates. In the Python SDK, discovery happens while the client connects. The results are then available on the client object.
228 
229 ```python Pseudo-code for AI application discovery theme={null}
230 # Pseudo Code
231 async with Client(stdio_client(server_config)) as client:
232 if client.server_capabilities.tools:
233 app.register_mcp_server(client, supports_tools=True)
234 app.set_server_ready(client)
235 ```
236 </Step>
237 
238 <Step title="Tool Discovery (Primitives)">
239 The client can discover available tools by sending a `tools/list` request. This request is fundamental to MCP's tool discovery mechanism: it allows clients to understand what tools are available on the server before attempting to use them.
240 
241 <CodeGroup>
242 ```json Tools List Request theme={null}
280 },
243281 {
244 "jsonrpc": "2.0",
245 "id": 2,
246 "method": "tools/list",
247 "params": {
248 "_meta": {
249 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
250 "io.modelcontextprotocol/clientInfo": {
251 "name": "example-client",
252 "version": "1.0.0"
282 "name": "weather_current",
283 "title": "Weather Information",
284 "description": "Get current weather information for any location worldwide",
285 "inputSchema": {
286 "type": "object",
287 "properties": {
288 "location": {
289 "type": "string",
290 "description": "City name, address, or coordinates (latitude,longitude)"
253291 },
254 "io.modelcontextprotocol/clientCapabilities": {
255 "elicitation": {}
292 "units": {
293 "type": "string",
294 "enum": ["metric", "imperial", "kelvin"],
295 "description": "Temperature units to use in response",
296 "default": "metric"
256297 }
257 }
298 },
299 "required": ["location"]
258300 }
259301 }
260 ```
261 
262 ```json Tools List Response theme={null}
302 ],
303 "ttlMs": 300000,
304 "cacheScope": "public"
305 }
306}
307```
308 
309#### Understanding the Tool Discovery Request
310 
311The `tools/list` request requires no parameters beyond the standard `_meta` fields that accompany every MCP request. It also accepts an optional `cursor` parameter for [pagination](/specification/2026-07-28/server/utilities/pagination), which the example above omits.
312 
313#### Understanding the Tool Discovery Response
314 
315The response contains a `tools` array that provides comprehensive metadata about each available tool. This array-based structure allows servers to expose multiple tools simultaneously while maintaining clear boundaries between different functionalities.
316 
317Each tool object in the response includes several key fields:
318 
319* **`name`**: A unique identifier for the tool within the server's namespace. This serves as the primary key for tool execution and should follow a clear naming pattern (e.g., `calculator_arithmetic` rather than just `calculate`)
320* **`title`**: A human-readable display name for the tool that clients can show to users
321* **`description`**: Detailed explanation of what the tool does and when to use it
322* **`inputSchema`**: A JSON Schema that defines the expected input parameters, enabling type validation and providing clear documentation about required and optional parameters
323 
324The result is marked `"resultType": "complete"` and carries two caching fields. `ttlMs` is a freshness hint in milliseconds, so this tool list can be cached for five minutes. `cacheScope` indicates who may reuse the response. The specification's [caching utility](/specification/2026-07-28/server/utilities/caching) defines the full rules.
325 
326#### How This Works in AI Applications
327 
328The AI application fetches available tools from all connected MCP servers and combines them into a unified tool registry that the language model can access. This allows the LLM to understand what actions it can perform and automatically generates the appropriate tool calls during conversations.
329 
330```python Pseudo-code for AI application tool discovery theme={null}
331# Pseudo-code using MCP Python SDK patterns
332available_tools = []
333for client in app.mcp_clients():
334 tools_response = await client.list_tools()
335 available_tools.extend(tools_response.tools)
336conversation.register_available_tools(available_tools)
337```
338 
339Clients that federate many servers can use [progressive tool discovery](/docs/2026-07-28/develop/clients/client-best-practices#progressive-tool-discovery) rather than loading every tool upfront.
340 
341**3. Tool Execution (Primitives)**
342 
343The client can now execute a tool using the `tools/call` method. This demonstrates how MCP primitives are used in practice: after discovering available tools, the client can invoke them with appropriate arguments.
344 
345#### Understanding the Tool Execution Request
346 
347The `tools/call` request follows a structured format that ensures type safety and clear communication between client and server. Note that we're using the proper tool name from the discovery response (`weather_current`) rather than a simplified name:
348 
349```json Tool Call Request theme={null}
350{
351 "jsonrpc": "2.0",
352 "id": 3,
353 "method": "tools/call",
354 "params": {
355 "name": "weather_current",
356 "arguments": {
357 "location": "San Francisco",
358 "units": "imperial"
359 },
360 "_meta": {
361 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
362 "io.modelcontextprotocol/clientInfo": {
363 "name": "example-client",
364 "version": "1.0.0"
365 },
366 "io.modelcontextprotocol/clientCapabilities": {
367 "elicitation": {}
368 }
369 }
370 }
371}
372```
373 
374```json Tool Call Response theme={null}
375{
376 "jsonrpc": "2.0",
377 "id": 3,
378 "result": {
379 "resultType": "complete",
380 "content": [
263381 {
264 "jsonrpc": "2.0",
265 "id": 2,
266 "result": {
267 "resultType": "complete",
268 "tools": [
269 {
270 "name": "calculator_arithmetic",
271 "title": "Calculator",
272 "description": "Perform mathematical calculations including basic arithmetic, trigonometric functions, and algebraic operations",
273 "inputSchema": {
274 "type": "object",
275 "properties": {
276 "expression": {
277 "type": "string",
278 "description": "Mathematical expression to evaluate (e.g., '2 + 3 * 4', 'sin(30)', 'sqrt(16)')"
279 }
280 },
281 "required": ["expression"]
282 }
283 },
284 {
285 "name": "weather_current",
286 "title": "Weather Information",
287 "description": "Get current weather information for any location worldwide",
288 "inputSchema": {
289 "type": "object",
290 "properties": {
291 "location": {
292 "type": "string",
293 "description": "City name, address, or coordinates (latitude,longitude)"
294 },
295 "units": {
296 "type": "string",
297 "enum": ["metric", "imperial", "kelvin"],
298 "description": "Temperature units to use in response",
299 "default": "metric"
300 }
301 },
302 "required": ["location"]
303 }
304 }
305 ],
306 "ttlMs": 300000,
307 "cacheScope": "public"
308 }
309 }
310 ```
311 </CodeGroup>
312 
313 #### Understanding the Tool Discovery Request
314 
315 The `tools/list` request requires no parameters beyond the standard `_meta` fields that accompany every MCP request. It also accepts an optional `cursor` parameter for [pagination](/specification/2026-07-28/server/utilities/pagination), which the example above omits.
316 
317 #### Understanding the Tool Discovery Response
318 
319 The response contains a `tools` array that provides comprehensive metadata about each available tool. This array-based structure allows servers to expose multiple tools simultaneously while maintaining clear boundaries between different functionalities.
320 
321 Each tool object in the response includes several key fields:
322 
323 * **`name`**: A unique identifier for the tool within the server's namespace. This serves as the primary key for tool execution and should follow a clear naming pattern (e.g., `calculator_arithmetic` rather than just `calculate`)
324 * **`title`**: A human-readable display name for the tool that clients can show to users
325 * **`description`**: Detailed explanation of what the tool does and when to use it
326 * **`inputSchema`**: A JSON Schema that defines the expected input parameters, enabling type validation and providing clear documentation about required and optional parameters
327 
328 The result is marked `"resultType": "complete"` and carries two caching fields. `ttlMs` is a freshness hint in milliseconds, so this tool list can be cached for five minutes. `cacheScope` indicates who may reuse the response. The specification's [caching utility](/specification/2026-07-28/server/utilities/caching) defines the full rules.
329 
330 #### How This Works in AI Applications
331 
332 The AI application fetches available tools from all connected MCP servers and combines them into a unified tool registry that the language model can access. This allows the LLM to understand what actions it can perform and automatically generates the appropriate tool calls during conversations.
333 
334 ```python Pseudo-code for AI application tool discovery theme={null}
335 # Pseudo-code using MCP Python SDK patterns
336 available_tools = []
337 for client in app.mcp_clients():
338 tools_response = await client.list_tools()
339 available_tools.extend(tools_response.tools)
340 conversation.register_available_tools(available_tools)
341 ```
342 
343 Clients that federate many servers can use [progressive tool discovery](/docs/2026-07-28/develop/clients/client-best-practices#progressive-tool-discovery) rather than loading every tool upfront.
344 </Step>
345 
346 <Step title="Tool Execution (Primitives)">
347 The client can now execute a tool using the `tools/call` method. This demonstrates how MCP primitives are used in practice: after discovering available tools, the client can invoke them with appropriate arguments.
348 
349 #### Understanding the Tool Execution Request
350 
351 The `tools/call` request follows a structured format that ensures type safety and clear communication between client and server. Note that we're using the proper tool name from the discovery response (`weather_current`) rather than a simplified name:
352 
353 <CodeGroup>
354 ```json Tool Call Request theme={null}
355 {
356 "jsonrpc": "2.0",
357 "id": 3,
358 "method": "tools/call",
359 "params": {
360 "name": "weather_current",
361 "arguments": {
362 "location": "San Francisco",
363 "units": "imperial"
364 },
365 "_meta": {
366 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
367 "io.modelcontextprotocol/clientInfo": {
368 "name": "example-client",
369 "version": "1.0.0"
370 },
371 "io.modelcontextprotocol/clientCapabilities": {
372 "elicitation": {}
373 }
374 }
375 }
376 }
377 ```
378 
379 ```json Tool Call Response theme={null}
380 {
381 "jsonrpc": "2.0",
382 "id": 3,
383 "result": {
384 "resultType": "complete",
385 "content": [
386 {
387 "type": "text",
388 "text": "Current weather in San Francisco: 68°F, partly cloudy with light winds from the west at 8 mph. Humidity: 65%"
389 }
390 ]
391 }
392 }
393 ```
394 </CodeGroup>
395 
396 #### Key Elements of Tool Execution
397 
398 The request structure includes several important components:
399 
400 1. **`name`**: Must match exactly the tool name from the discovery response (`weather_current`). This ensures the server can correctly identify which tool to execute.
401 
402 2. **`arguments`**: Contains the input parameters as defined by the tool's `inputSchema`. In this example:
403 * `location`: "San Francisco" (required parameter)
404 * `units`: "imperial" (optional parameter, defaults to "metric" if not specified)
405 
406 3. **`_meta`**: Carries the standard per-request fields: the protocol version and client capabilities that every MCP request must include, plus the client's identity, which clients should include unless configured not to.
407 
408 4. **JSON-RPC Structure**: Uses standard JSON-RPC 2.0 format with unique `id` for request-response correlation.
409 
410 #### Understanding the Tool Execution Response
411 
412 The response demonstrates MCP's flexible content system:
413 
414 1. **`content` Array**: Tool responses return an array of content objects, allowing for rich, multi-format responses (text, images, resources, etc.)
415 
416 2. **Content Types**: Each content object has a `type` field. In this example, `"type": "text"` indicates plain text content, but MCP supports various content types for different use cases.
417 
418 3. **Structured Output**: The response provides actionable information that the AI application can use as context for language model interactions.
419 
420 This execution pattern allows AI applications to dynamically invoke server functionality and receive structured responses that can be integrated into conversations with language models.
421 
422 #### How This Works in AI Applications
423 
424 When the language model decides to use a tool during a conversation, the AI application intercepts the tool call, routes it to the appropriate MCP server, executes it, and returns the results back to the LLM as part of the conversation flow. This enables the LLM to access real-time data and perform actions in the external world.
425 
426 ```python theme={null}
427 # Pseudo-code for AI application tool execution
428 async def handle_tool_call(conversation, tool_name, arguments):
429 client = app.find_mcp_client_for_tool(tool_name)
430 result = await client.call_tool(tool_name, arguments)
431 conversation.add_tool_result(result.content)
432 ```
433 </Step>
434 
435 <Step title="Real-time Updates (Notifications)">
436 MCP supports real-time notifications that enable servers to inform clients about changes without being polled for them. This demonstrates the notification system, a key feature that keeps clients synchronized and responsive.
437 
438 #### Subscribing to Changes
439 
440 Change notifications are opt-in. To receive them, the client opens a long-lived notification stream by sending a [`subscriptions/listen`](/specification/2026-07-28/basic/patterns/subscriptions) request with a `notifications` filter naming the event types it wants. Here the client asks for tool list changes:
441 
442 ```json Listen Request theme={null}
443 {
444 "jsonrpc": "2.0",
445 "id": 4,
446 "method": "subscriptions/listen",
447 "params": {
448 "_meta": {
449 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
450 "io.modelcontextprotocol/clientInfo": {
451 "name": "example-client",
452 "version": "1.0.0"
453 },
454 "io.modelcontextprotocol/clientCapabilities": {
455 "elicitation": {}
456 }
457 },
458 "notifications": {
459 "toolsListChanged": true
460 }
461 }
462 }
463 ```
464 
465 Every client request carries the `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` fields in `_meta`, and normally `io.modelcontextprotocol/clientInfo` as well, so the server can identify the client without relying on connection state.
466 
467 The server acknowledges the subscription with `notifications/subscriptions/acknowledged`, which is the first message carrying that subscription's ID in `_meta` (the server sends no other notification for that subscription before it). Its `notifications` field reflects the subset of the requested filter the server agreed to honor, with unsupported notification types omitted:
468 
469 ```json Acknowledgment theme={null}
470 {
471 "jsonrpc": "2.0",
472 "method": "notifications/subscriptions/acknowledged",
473 "params": {
474 "_meta": {
475 "io.modelcontextprotocol/subscriptionId": 4
476 },
477 "notifications": {
478 "toolsListChanged": true
479 }
480 }
481 }
482 ```
483 
484 #### Understanding Tool List Change Notifications
485 
486 After the acknowledgment, when the server's available tools change (for example, when new functionality becomes available, existing tools are modified, or tools become temporarily unavailable), the server delivers a notification on that stream:
487 
488 ```json Notification theme={null}
489 {
490 "jsonrpc": "2.0",
491 "method": "notifications/tools/list_changed",
492 "params": {
493 "_meta": {
494 "io.modelcontextprotocol/subscriptionId": 4
495 }
496 }
497 }
498 ```
499 
500 #### Key Features of MCP Notifications
501 
502 1. **No Response Required**: Notice there's no `id` field in the notification. This follows JSON-RPC 2.0 notification semantics where no response is expected or sent.
503 
504 2. **Opt-In Based**: This notification is only sent to clients that requested `"toolsListChanged": true` in their `subscriptions/listen` filter, and it is only available from servers that declared `"listChanged": true` in their tools capability (as shown in Step 1).
505 
506 3. **Subscription-ID Tagging**: Every notification on the stream carries `io.modelcontextprotocol/subscriptionId` in `_meta`. The value is the JSON-RPC ID of the `subscriptions/listen` request that opened the stream (`4` in this example), so clients can correlate each notification with the subscription that produced it.
507 
508 4. **Event-Driven**: The server decides when to send notifications based on internal state changes, making MCP connections dynamic and responsive.
509 
510 5. **Best Effort**: There are no guarantees that every notification will be sent or received, particularly across transport reconnects. Clients should also rely on polling to preserve freshness of results.
511 
512 #### Client Response to Notifications
513 
514 Upon receiving this notification, the client typically reacts by requesting the updated tool list. This creates a refresh cycle that keeps the client's understanding of available tools current:
515 
516 ```json Request theme={null}
517 {
518 "jsonrpc": "2.0",
519 "id": 5,
520 "method": "tools/list",
521 "params": {
522 "_meta": {
523 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
524 "io.modelcontextprotocol/clientInfo": {
525 "name": "example-client",
526 "version": "1.0.0"
527 },
528 "io.modelcontextprotocol/clientCapabilities": {
529 "elicitation": {}
530 }
531 }
532 }
533 }
534 ```
535 
536 #### Why Notifications Matter
537 
538 This notification system is crucial for several reasons:
539 
540 1. **Dynamic Environments**: Tools may come and go based on server state, external dependencies, or user permissions
541 2. **Efficiency**: Clients don't need to poll for changes; they're notified when updates occur
542 3. **Consistency**: Ensures clients always have accurate information about available server capabilities
543 4. **Real-time Collaboration**: Enables responsive AI applications that can adapt to changing contexts
544 
545 This notification pattern extends beyond tools to other MCP primitives, enabling comprehensive real-time synchronization between clients and servers.
546 
547 #### How This Works in AI Applications
548 
549 The AI application keeps a notification stream open for the changes it cares about. When one arrives, it immediately refreshes its tool registry and updates the LLM's available capabilities. This ensures that ongoing conversations always have access to the most current set of tools, and the LLM can dynamically adapt to new functionality as it becomes available.
550 
551 ```python theme={null}
552 # Pseudo-code for AI application notification handling
553 async def follow_tool_changes(client):
554 async with client.listen(tools_list_changed=True) as sub:
555 async for _event in sub:
556 tools_response = await client.list_tools()
557 app.update_available_tools(client, tools_response.tools)
558 if app.conversation.is_active():
559 app.conversation.notify_llm_of_new_capabilities()
560 ```
561 </Step>
562</Steps>
382 "type": "text",
383 "text": "Current weather in San Francisco: 68°F, partly cloudy with light winds from the west at 8 mph. Humidity: 65%"
384 }
385 ]
386 }
387}
388```
389 
390#### Key Elements of Tool Execution
391 
392The request structure includes several important components:
393 
3941. **`name`**: Must match exactly the tool name from the discovery response (`weather_current`). This ensures the server can correctly identify which tool to execute.
395 
3962. **`arguments`**: Contains the input parameters as defined by the tool's `inputSchema`. In this example:
397 * `location`: "San Francisco" (required parameter)
398 * `units`: "imperial" (optional parameter, defaults to "metric" if not specified)
399 
4003. **`_meta`**: Carries the standard per-request fields: the protocol version and client capabilities that every MCP request must include, plus the client's identity, which clients should include unless configured not to.
401 
4024. **JSON-RPC Structure**: Uses standard JSON-RPC 2.0 format with unique `id` for request-response correlation.
403 
404#### Understanding the Tool Execution Response
405 
406The response demonstrates MCP's flexible content system:
407 
4081. **`content` Array**: Tool responses return an array of content objects, allowing for rich, multi-format responses (text, images, resources, etc.)
409 
4102. **Content Types**: Each content object has a `type` field. In this example, `"type": "text"` indicates plain text content, but MCP supports various content types for different use cases.
411 
4123. **Structured Output**: The response provides actionable information that the AI application can use as context for language model interactions.
413 
414This execution pattern allows AI applications to dynamically invoke server functionality and receive structured responses that can be integrated into conversations with language models.
415 
416#### How This Works in AI Applications
417 
418When the language model decides to use a tool during a conversation, the AI application intercepts the tool call, routes it to the appropriate MCP server, executes it, and returns the results back to the LLM as part of the conversation flow. This enables the LLM to access real-time data and perform actions in the external world.
419 
420```python theme={null}
421# Pseudo-code for AI application tool execution
422async def handle_tool_call(conversation, tool_name, arguments):
423 client = app.find_mcp_client_for_tool(tool_name)
424 result = await client.call_tool(tool_name, arguments)
425 conversation.add_tool_result(result.content)
426```
427 
428**4. Real-time Updates (Notifications)**
429 
430MCP supports real-time notifications that enable servers to inform clients about changes without being polled for them. This demonstrates the notification system, a key feature that keeps clients synchronized and responsive.
431 
432#### Subscribing to Changes
433 
434Change notifications are opt-in. To receive them, the client opens a long-lived notification stream by sending a [`subscriptions/listen`](/specification/2026-07-28/basic/patterns/subscriptions) request with a `notifications` filter naming the event types it wants. Here the client asks for tool list changes:
435 
436```json Listen Request theme={null}
437{
438 "jsonrpc": "2.0",
439 "id": 4,
440 "method": "subscriptions/listen",
441 "params": {
442 "_meta": {
443 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
444 "io.modelcontextprotocol/clientInfo": {
445 "name": "example-client",
446 "version": "1.0.0"
447 },
448 "io.modelcontextprotocol/clientCapabilities": {
449 "elicitation": {}
450 }
451 },
452 "notifications": {
453 "toolsListChanged": true
454 }
455 }
456}
457```
458 
459Every client request carries the `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` fields in `_meta`, and normally `io.modelcontextprotocol/clientInfo` as well, so the server can identify the client without relying on connection state.
460 
461The server acknowledges the subscription with `notifications/subscriptions/acknowledged`, which is the first message carrying that subscription's ID in `_meta` (the server sends no other notification for that subscription before it). Its `notifications` field reflects the subset of the requested filter the server agreed to honor, with unsupported notification types omitted:
462 
463```json Acknowledgment theme={null}
464{
465 "jsonrpc": "2.0",
466 "method": "notifications/subscriptions/acknowledged",
467 "params": {
468 "_meta": {
469 "io.modelcontextprotocol/subscriptionId": 4
470 },
471 "notifications": {
472 "toolsListChanged": true
473 }
474 }
475}
476```
477 
478#### Understanding Tool List Change Notifications
479 
480After the acknowledgment, when the server's available tools change (for example, when new functionality becomes available, existing tools are modified, or tools become temporarily unavailable), the server delivers a notification on that stream:
481 
482```json Notification theme={null}
483{
484 "jsonrpc": "2.0",
485 "method": "notifications/tools/list_changed",
486 "params": {
487 "_meta": {
488 "io.modelcontextprotocol/subscriptionId": 4
489 }
490 }
491}
492```
493 
494#### Key Features of MCP Notifications
495 
4961. **No Response Required**: Notice there's no `id` field in the notification. This follows JSON-RPC 2.0 notification semantics where no response is expected or sent.
497 
4982. **Opt-In Based**: This notification is only sent to clients that requested `"toolsListChanged": true` in their `subscriptions/listen` filter, and it is only available from servers that declared `"listChanged": true` in their tools capability (as shown in Step 1).
499 
5003. **Subscription-ID Tagging**: Every notification on the stream carries `io.modelcontextprotocol/subscriptionId` in `_meta`. The value is the JSON-RPC ID of the `subscriptions/listen` request that opened the stream (`4` in this example), so clients can correlate each notification with the subscription that produced it.
501 
5024. **Event-Driven**: The server decides when to send notifications based on internal state changes, making MCP connections dynamic and responsive.
503 
5045. **Best Effort**: There are no guarantees that every notification will be sent or received, particularly across transport reconnects. Clients should also rely on polling to preserve freshness of results.
505 
506#### Client Response to Notifications
507 
508Upon receiving this notification, the client typically reacts by requesting the updated tool list. This creates a refresh cycle that keeps the client's understanding of available tools current:
509 
510```json Request theme={null}
511{
512 "jsonrpc": "2.0",
513 "id": 5,
514 "method": "tools/list",
515 "params": {
516 "_meta": {
517 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
518 "io.modelcontextprotocol/clientInfo": {
519 "name": "example-client",
520 "version": "1.0.0"
521 },
522 "io.modelcontextprotocol/clientCapabilities": {
523 "elicitation": {}
524 }
525 }
526 }
527}
528```
529 
530#### Why Notifications Matter
531 
532This notification system is crucial for several reasons:
533 
5341. **Dynamic Environments**: Tools may come and go based on server state, external dependencies, or user permissions
5352. **Efficiency**: Clients don't need to poll for changes; they're notified when updates occur
5363. **Consistency**: Ensures clients always have accurate information about available server capabilities
5374. **Real-time Collaboration**: Enables responsive AI applications that can adapt to changing contexts
538 
539This notification pattern extends beyond tools to other MCP primitives, enabling comprehensive real-time synchronization between clients and servers.
540 
541#### How This Works in AI Applications
542 
543The AI application keeps a notification stream open for the changes it cares about. When one arrives, it immediately refreshes its tool registry and updates the LLM's available capabilities. This ensures that ongoing conversations always have access to the most current set of tools, and the LLM can dynamically adapt to new functionality as it becomes available.
544 
545```python theme={null}
546# Pseudo-code for AI application notification handling
547async def follow_tool_changes(client):
548 async with client.listen(tools_list_changed=True) as sub:
549 async for _event in sub:
550 tools_response = await client.list_tools()
551 app.update_available_tools(client, tools_response.tools)
552 if app.conversation.is_active():
553 app.conversation.notify_llm_of_new_capabilities()
554```
563555 

docs/2026-07-28/learn/client-concepts Changed · +11 / -11 lines

from line 113
113113 
114114### Roots
115115 
116<Warning>
117 Roots are [deprecated](/specification/2026-07-28/deprecated) as of protocol
118 version `2026-07-28` and scheduled for removal. New implementations should
119 pass directories or files via tool parameters, resource URIs, or server
120 configuration instead.
121</Warning>
116> [!WARNING]
117>
118> Roots are [deprecated](/specification/2026-07-28/deprecated) as of protocol
119> version `2026-07-28` and scheduled for removal. New implementations should
120> pass directories or files via tool parameters, resource URIs, or server
121> configuration instead.
122122 
123123Roots define filesystem boundaries for server operations, allowing clients to specify which directories servers should focus on.
124124 
from line 169
169169 
170170### Sampling
171171 
172<Warning>
173 Sampling is [deprecated](/specification/2026-07-28/deprecated) as of protocol
174 version `2026-07-28` and scheduled for removal. New implementations should
175 integrate directly with LLM provider APIs instead.
176</Warning>
172> [!WARNING]
173>
174> Sampling is [deprecated](/specification/2026-07-28/deprecated) as of protocol
175> version `2026-07-28` and scheduled for removal. New implementations should
176> integrate directly with LLM provider APIs instead.
177177 
178178Sampling allows servers to request language model completions through the client, enabling agentic behaviors while maintaining security and user control.
179179 

docs/2026-07-28/learn/server-concepts Changed · +3 / -3 lines

from line 10
1010 
1111| Feature | Explanation | Examples | Who controls it |
1212| - | - | - | - |
13| **Tools** | Functions that your LLM can actively call, and decides when to use them based on user requests. Tools can write to databases, call external APIs, modify files, or trigger other logic. | Search flights<br />Send messages<br />Create calendar events | Model |
14| **Resources** | Passive data sources that provide read-only access to information for context, such as file contents, database schemas, or API documentation. | Retrieve documents<br />Access knowledge bases<br />Read calendars | Application |
15| **Prompts** | Pre-built instruction templates that tell the model to work with specific tools and resources. | Plan a vacation<br />Summarize my meetings<br />Draft an email | User |
13| **Tools** | Functions that your LLM can actively call, and decides when to use them based on user requests. Tools can write to databases, call external APIs, modify files, or trigger other logic. | Search flights Send messages Create calendar events | Model |
14| **Resources** | Passive data sources that provide read-only access to information for context, such as file contents, database schemas, or API documentation. | Retrieve documents Access knowledge bases Read calendars | Application |
15| **Prompts** | Pre-built instruction templates that tell the model to work with specific tools and resources. | Plan a vacation Summarize my meetings Draft an email | User |
1616 
1717We will use a hypothetical scenario to demonstrate the role of each of these features, and show how they can work together.
1818 

docs/2026-07-28/learn/versioning Changed · +5 / -5 lines

from line 3
33The Model Context Protocol uses string-based version identifiers following the format
44`YYYY-MM-DD`, to indicate the last date backwards incompatible changes were made.
55 
6<Info>
7 The protocol version will *not* be incremented when the
8 protocol is updated, as long as the changes maintain backwards compatibility. This allows
9 for incremental improvements while preserving interoperability.
10</Info>
6> [!NOTE]
7>
8> The protocol version will *not* be incremented when the
9> protocol is updated, as long as the changes maintain backwards compatibility. This allows
10> for incremental improvements while preserving interoperability.
1111 
1212## Revisions
1313 

docs/2026-07-28/sdk Changed · +16 / -18 lines

from line 8
88 
99| SDK | Repository | Tier |
1010| :- | :- | -: |
11| <Icon icon="square-js" size={24} />   [TypeScript](https://ts.sdk.modelcontextprotocol.io) | [modelcontextprotocol/typescript-sdk](https://github.com/modelcontextprotocol/typescript-sdk) | <Badge color="blue" shape="pill">Tier 1</Badge> |
12| <Icon icon="python" size={24} />   [Python](https://py.sdk.modelcontextprotocol.io) | [modelcontextprotocol/python-sdk](https://github.com/modelcontextprotocol/python-sdk) | <Badge color="blue" shape="pill">Tier 1</Badge> |
13| <Icon icon="square-c" size={24} />   [C#](https://csharp.sdk.modelcontextprotocol.io) | [modelcontextprotocol/csharp-sdk](https://github.com/modelcontextprotocol/csharp-sdk) | <Badge color="blue" shape="pill">Tier 1</Badge> |
14| <Icon icon="golang" size={24} />   [Go](https://go.sdk.modelcontextprotocol.io) | [modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk) | <Badge color="blue" shape="pill">Tier 1</Badge> |
15| <Icon icon="rust" size={24} />   [Rust](https://rust.sdk.modelcontextprotocol.io) | [modelcontextprotocol/rust-sdk](https://github.com/modelcontextprotocol/rust-sdk) | <Badge color="blue" shape="pill">Tier 1</Badge> |
16| <Icon icon="gem" size={24} />   [Ruby](https://ruby.sdk.modelcontextprotocol.io) | [modelcontextprotocol/ruby-sdk](https://github.com/modelcontextprotocol/ruby-sdk) | <Badge color="blue" shape="pill">Tier 1</Badge> |
17| <Icon icon="java" size={24} />   [Java](https://java.sdk.modelcontextprotocol.io) | [modelcontextprotocol/java-sdk](https://github.com/modelcontextprotocol/java-sdk) | <Badge color="purple" shape="pill">Tier 2</Badge> |
18| <Icon icon="swift" size={24} />   Swift | [modelcontextprotocol/swift-sdk](https://github.com/modelcontextprotocol/swift-sdk) | <Badge color="orange" shape="pill">Tier 3</Badge> |
19| <Icon icon="php" size={24} />   [PHP](https://php.sdk.modelcontextprotocol.io) | [modelcontextprotocol/php-sdk](https://github.com/modelcontextprotocol/php-sdk) | <Badge color="orange" shape="pill">Tier 3</Badge> |
20| <Icon icon="square-k" size={24} />   [Kotlin](https://kotlin.sdk.modelcontextprotocol.io) | [modelcontextprotocol/kotlin-sdk](https://github.com/modelcontextprotocol/kotlin-sdk) | <Badge color="orange" shape="pill">Tier 3</Badge> |
11|   [TypeScript](https://ts.sdk.modelcontextprotocol.io) | [modelcontextprotocol/typescript-sdk](https://github.com/modelcontextprotocol/typescript-sdk) | Tier 1 |
12|   [Python](https://py.sdk.modelcontextprotocol.io) | [modelcontextprotocol/python-sdk](https://github.com/modelcontextprotocol/python-sdk) | Tier 1 |
13|   [C#](https://csharp.sdk.modelcontextprotocol.io) | [modelcontextprotocol/csharp-sdk](https://github.com/modelcontextprotocol/csharp-sdk) | Tier 1 |
14|   [Go](https://go.sdk.modelcontextprotocol.io) | [modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk) | Tier 1 |
15|   [Rust](https://rust.sdk.modelcontextprotocol.io) | [modelcontextprotocol/rust-sdk](https://github.com/modelcontextprotocol/rust-sdk) | Tier 1 |
16|   [Ruby](https://ruby.sdk.modelcontextprotocol.io) | [modelcontextprotocol/ruby-sdk](https://github.com/modelcontextprotocol/ruby-sdk) | Tier 1 |
17|   [Java](https://java.sdk.modelcontextprotocol.io) | [modelcontextprotocol/java-sdk](https://github.com/modelcontextprotocol/java-sdk) | Tier 2 |
18|   Swift | [modelcontextprotocol/swift-sdk](https://github.com/modelcontextprotocol/swift-sdk) | Tier 3 |
19|   [PHP](https://php.sdk.modelcontextprotocol.io) | [modelcontextprotocol/php-sdk](https://github.com/modelcontextprotocol/php-sdk) | Tier 3 |
20|   [Kotlin](https://kotlin.sdk.modelcontextprotocol.io) | [modelcontextprotocol/kotlin-sdk](https://github.com/modelcontextprotocol/kotlin-sdk) | Tier 3 |
2121 
2222See [SDK Tiering System](/community/sdk-tiers) for details on what each tier means.
2323 
from line 36
3636 
3737Ready to start building with MCP? Choose your path:
3838 
39<CardGroup cols={2}>
40 <Card title="Build a Server" icon="server" href="/docs/2026-07-28/develop/build-server">
41 Learn how to create your first MCP server
42 </Card>
39> **[Build a Server](/docs/2026-07-28/develop/build-server)**
40>
41> Learn how to create your first MCP server
4342 
44 <Card title="Build a Client" icon="computer" href="/docs/2026-07-28/develop/build-client">
45 Create applications that connect to MCP servers
46 </Card>
47</CardGroup>
43> **[Build a Client](/docs/2026-07-28/develop/build-client)**
44>
45> Create applications that connect to MCP servers
4846 

docs/2026-07-28/tools/debugging Changed · +57 / -65 lines

from line 33
3333logged to stderr (standard error) will be captured by the host application
3434automatically.
3535 
36<Warning>
37 Local MCP servers should not log messages to stdout (standard out), as this
38 will interfere with protocol operation.
39</Warning>
36> [!WARNING]
37>
38> Local MCP servers should not log messages to stdout (standard out), as this
39> will interfere with protocol operation.
4040 
4141For servers using the
4242[Streamable HTTP transport](/specification/2026-07-28/basic/transports/streamable-http),
from line 45
4545tooling (curl, browser DevTools Network panel) to inspect requests and SSE
4646streams.
4747 
48<Warning>
49 The `notifications/message` mechanism below is deprecated as of protocol
50 version `2026-07-28`. It remains available during the deprecation window.
51</Warning>
48> [!WARNING]
49>
50> The `notifications/message` mechanism below is deprecated as of protocol
51> version `2026-07-28`. It remains available during the deprecation window.
5252 
5353For all [transports](/specification/latest/basic/transports), record what the
5454server is doing as it runs:
5555 
56<CodeGroup>
57 ```python Python theme={null}
58 import logging
56```python Python theme={null}
57import logging
5958 
60 from mcp.server import MCPServer
59from mcp.server import MCPServer
6160 
62 logger = logging.getLogger(__name__)
61logger = logging.getLogger(__name__)
6362 
64 mcp = MCPServer("reports")
63mcp = MCPServer("reports")
6564 
6665 
67 @mcp.tool()
68 async def fetch_report(report_id: str) -> str:
69 """Fetch a report by id."""
70 logger.info("Fetching report %s", report_id)
71 return f"Report {report_id} is ready."
72 ```
66@mcp.tool()
67async def fetch_report(report_id: str) -> str:
68 """Fetch a report by id."""
69 logger.info("Fetching report %s", report_id)
70 return f"Report {report_id} is ready."
71```
7372 
74 ```typescript TypeScript theme={null}
75 await server.sendLoggingMessage({
76 level: "info",
77 data: "Server started successfully",
78 });
79 ```
80</CodeGroup>
73```typescript TypeScript theme={null}
74await server.sendLoggingMessage({
75 level: "info",
76 data: "Server started successfully",
77});
78```
8179 
8280MCP defines eight
8381[RFC 5424 severity levels](/specification/latest/server/utilities/logging#log-levels)
from line 209
211209hover over the **Connectors** menu to see connected servers and available
212210tools.
213211 
214<img src="https://mintcdn.com/mcp/zNouQwo2h8cbxlDS/images/available-mcp-tools.png?fit=max&auto=format&n=zNouQwo2h8cbxlDS&q=85&s=e2ace1ac88895a5fe30ebd8d01456bc3" alt="Available MCP tools" width="437" height="244" data-path="images/available-mcp-tools.png" />
212![Available MCP tools](https://mintcdn.com/mcp/zNouQwo2h8cbxlDS/images/available-mcp-tools.png?fit=max\&auto=format\&n=zNouQwo2h8cbxlDS\&q=85\&s=e2ace1ac88895a5fe30ebd8d01456bc3)
215213 
216214### Viewing logs
217215 
from line 218
220218* macOS: `~/Library/Logs/Claude`
221219* Windows: `%APPDATA%\Claude\logs`
222220 
223<CodeGroup>
224 ```bash macOS theme={null}
225 tail -n 20 -F ~/Library/Logs/Claude/mcp*.log
226 ```
221```bash macOS theme={null}
222tail -n 20 -F ~/Library/Logs/Claude/mcp*.log
223```
227224 
228 ```powershell Windows theme={null}
229 type "$env:AppData\Claude\logs\mcp*.log"
230 ```
231</CodeGroup>
225```powershell Windows theme={null}
226type "$env:AppData\Claude\logs\mcp*.log"
227```
232228 
233229The logs capture:
234230 
from line 240
244240 
2452411. Create a `developer_settings.json` file with `allowDevTools` set to true:
246242 
247<CodeGroup>
248 ```bash macOS theme={null}
249 echo '{"allowDevTools": true}' > ~/Library/Application\ Support/Claude/developer_settings.json
250 ```
243```bash macOS theme={null}
244echo '{"allowDevTools": true}' > ~/Library/Application\ Support/Claude/developer_settings.json
245```
251246 
252 ```powershell Windows theme={null}
253 '{"allowDevTools": true}' | Set-Content "$env:AppData\Claude\developer_settings.json"
254 ```
255</CodeGroup>
247```powershell Windows theme={null}
248'{"allowDevTools": true}' | Set-Content "$env:AppData\Claude\developer_settings.json"
249```
256250 
2572512. Open DevTools: `Command-Option-I` (macOS) or `Ctrl+Alt+I` (Windows)
258252 
from line 347
353347 
354348## Next steps
355349 
356<CardGroup cols={2}>
357 <Card title="MCP Inspector" icon="magnifying-glass" href="/docs/2026-07-28/tools/inspector">
358 Learn to use the MCP Inspector
359 </Card>
350> **[MCP Inspector](/docs/2026-07-28/tools/inspector)**
351>
352> Learn to use the MCP Inspector
360353 
361 <Card title="Build an MCP server" icon="code" href="/docs/2026-07-28/develop/build-server">
362 Walk through building a server from scratch
363 </Card>
354> **[Build an MCP server](/docs/2026-07-28/develop/build-server)**
355>
356> Walk through building a server from scratch
364357 
365 <Card title="Connect local servers" icon="plug" href="/docs/2026-07-28/develop/connect-local-servers">
366 Full claude\_desktop\_config.json reference and troubleshooting
367 </Card>
368</CardGroup>
358> **[Connect local servers](/docs/2026-07-28/develop/connect-local-servers)**
359>
360> Full claude\_desktop\_config.json reference and troubleshooting
369361 

docs/2026-07-28/tools/inspector Changed · +99 / -107 lines

from line 14
1414 
1515The package also installs **`mcpdo`**, an experimental [connection client](/docs/2026-07-28/tools/inspector/mcpdo) that connects to a server once and keeps it open across many commands, on the same core.
1616 
17<Frame caption="The MCP Inspector web client after a tool call, with the monitoring sidebar pinned and widened so the JSON-RPC exchange stays readable while you work.">
18 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/web-monitor-sidebar.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=2ced9eeefa020be53fe4fe957b7d07c6" width="3840" height="2160" data-path="images/inspector/web-monitor-sidebar.png" />
19</Frame>
17![](https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/web-monitor-sidebar.png?fit=max\&auto=format\&n=pUebPdrb6PY5_mfH\&q=85\&s=2ced9eeefa020be53fe4fe957b7d07c6)
2018 
2119## Quickstart
2220 
2321The Inspector requires **Node 22.19.0 or newer** and runs directly through `npx`. No installation is required:
2422 
25<Tabs>
26 <Tab title="Web">
27 ```bash theme={null}
28 # Launch the web UI and connect to a local stdio server
29 npx @modelcontextprotocol/inspector node path/to/server/index.js
23**Web**
3024 
31 # Or launch with no target and add servers from the UI
32 npx @modelcontextprotocol/inspector
33 ```
25```bash theme={null}
26# Launch the web UI and connect to a local stdio server
27npx @modelcontextprotocol/inspector node path/to/server/index.js
3428 
35 The command prints a URL carrying a per-launch API token and opens it in your browser (set `MCP_AUTO_OPEN_ENABLED=false` to stop it opening). See [Web client](/docs/2026-07-28/tools/inspector/web).
36 </Tab>
29# Or launch with no target and add servers from the UI
30npx @modelcontextprotocol/inspector
31```
3732 
38 <Tab title="CLI">
39 ```bash theme={null}
40 # List a server's tools and exit
41 npx @modelcontextprotocol/inspector --cli node path/to/server/index.js --method tools/list
33The command prints a URL carrying a per-launch API token and opens it in your browser (set `MCP_AUTO_OPEN_ENABLED=false` to stop it opening). See [Web client](/docs/2026-07-28/tools/inspector/web).
4234 
43 # Call a tool and pipe the result into jq
44 npx @modelcontextprotocol/inspector --cli https://api.example.com/mcp --transport http \
45 --method tools/call --tool-name get_weather --tool-arg city=Boston --format json | jq .result
46 ```
35**CLI**
4736 
48 See [CLI client](/docs/2026-07-28/tools/inspector/cli).
49 </Tab>
37```bash theme={null}
38# List a server's tools and exit
39npx @modelcontextprotocol/inspector --cli node path/to/server/index.js --method tools/list
5040 
51 <Tab title="TUI">
52 ```bash theme={null}
53 npx @modelcontextprotocol/inspector --tui node path/to/server/index.js
54 ```
41# Call a tool and pipe the result into jq
42npx @modelcontextprotocol/inspector --cli https://api.example.com/mcp --transport http \
43 --method tools/call --tool-name get_weather --tool-arg city=Boston --format json | jq .result
44```
5545 
56 See [TUI client](/docs/2026-07-28/tools/inspector/tui).
57 </Tab>
58</Tabs>
46See [CLI client](/docs/2026-07-28/tools/inspector/cli).
5947 
48**TUI**
49 
50```bash theme={null}
51npx @modelcontextprotocol/inspector --tui node path/to/server/index.js
52```
53 
54See [TUI client](/docs/2026-07-28/tools/inspector/tui).
55 
6056### Inspecting published servers
6157 
6258Pass the command that launches the server as the Inspector's arguments, or point it at a remote server with `--server-url`:
6359 
64<Tabs>
65 <Tab title="npm package">
66 ```bash theme={null}
67 npx -y @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem ~/Desktop
68 ```
69 </Tab>
60**npm package**
7061 
71 <Tab title="PyPI package">
72 ```bash theme={null}
73 npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git
74 ```
75 </Tab>
62```bash theme={null}
63npx -y @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem ~/Desktop
64```
7665 
77 <Tab title="Remote HTTP server">
78 ```bash theme={null}
79 npx @modelcontextprotocol/inspector --server-url https://api.example.com/mcp --transport http
80 ```
81 </Tab>
82</Tabs>
66**PyPI package**
8367 
68```bash theme={null}
69npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git
70```
71 
72**Remote HTTP server**
73 
74```bash theme={null}
75npx @modelcontextprotocol/inspector --server-url https://api.example.com/mcp --transport http
76```
77 
8478Always read a server's own README first, since every server requires different commands and arguments.
8579 
86<Warning>
87 **The Inspector stores secrets (OAuth tokens, OAuth client secrets, and stdio
88 `env:` values) in the OS keychain when one is available.** On a machine with
89 no keychain (Linux without libsecret or a Secret Service, headless and SSH
90 sessions, Termux, and containers with a mounted volume) they are saved to
91 `~/.mcp-inspector/secrets.json` instead, **unencrypted unless you supply a
92 key**. See [Where secrets are
93 stored](/docs/2026-07-28/tools/inspector/configuration#where-secrets-are-stored)
94 for how to get a keychain back, encrypt the file, or keep secrets in memory
95 only.
96</Warning>
80> [!WARNING]
81>
82> **The Inspector stores secrets (OAuth tokens, OAuth client secrets, and stdio
83> `env:` values) in the OS keychain when one is available.** On a machine with
84> no keychain (Linux without libsecret or a Secret Service, headless and SSH
85> sessions, Termux, and containers with a mounted volume) they are saved to
86> `~/.mcp-inspector/secrets.json` instead, **unencrypted unless you supply a
87> key**. See [Where secrets are
88> stored](/docs/2026-07-28/tools/inspector/configuration#where-secrets-are-stored)
89> for how to get a keychain back, encrypt the file, or keep secrets in memory
90> only.
9791 
9892## Launcher flags vs. client flags
9993 
from line 98
10498 
10599Everything else (`--catalog`, `--config`, `--server-url`, `--transport`, `--method`, the OAuth flags) is defined by the *client*, not the launcher, and the clients do not all define the same set. The [Configuration and flags](/docs/2026-07-28/tools/inspector/configuration) page is organized that way, by owner.
106100 
107<Note>
108 Mode flags are recognized only at the front of the command line: the first token that isn't `--web` / `--cli` / `--tui` ends launcher parsing, and everything after it is forwarded to the client unchanged. That's what lets a literal `--cli` appear later as one of your server's own arguments:
101> [!NOTE]
102>
103> Mode flags are recognized only at the front of the command line: the first token that isn't `--web` / `--cli` / `--tui` ends launcher parsing, and everything after it is forwarded to the client unchanged. That's what lets a literal `--cli` appear later as one of your server's own arguments:
104>
105> ```bash theme={null}
106> mcp-inspector --cli node server.js --cli # mode is CLI; the trailing --cli goes to server.js
107> ```
109108 
110 ```bash theme={null}
111 mcp-inspector --cli node server.js --cli # mode is CLI; the trailing --cli goes to server.js
112 ```
113</Note>
109> [!NOTE]
110>
111> `--help` behaves differently with and without a mode flag. Without one,
112> `-h`/`--help` anywhere on the command line (even after a server command)
113> prints the launcher's help and exits. With a mode flag it is forwarded, so
114> `mcp-inspector --cli --help` prints the CLI's full flag reference instead.
114115 
115<Note>
116 `--help` behaves differently with and without a mode flag. Without one,
117 `-h`/`--help` anywhere on the command line (even after a server command)
118 prints the launcher's help and exits. With a mode flag it is forwarded, so
119 `mcp-inspector --cli --help` prints the CLI's full flag reference instead.
120</Note>
121 
122116## Where to go next
123117 
124<CardGroup cols={2}>
125 <Card title="Web client" icon="browser" href="/docs/2026-07-28/tools/inspector/web">
126 A tab-by-tab walkthrough of the graphical inspector.
127 </Card>
118> **[Web client](/docs/2026-07-28/tools/inspector/web)**
119>
120> A tab-by-tab walkthrough of the graphical inspector.
128121 
129 <Card title="CLI client" icon="terminal" href="/docs/2026-07-28/tools/inspector/cli">
130 Method reference, output formats, exit codes, and CI recipes.
131 </Card>
122> **[CLI client](/docs/2026-07-28/tools/inspector/cli)**
123>
124> Method reference, output formats, exit codes, and CI recipes.
132125 
133 <Card title="TUI client" icon="table-columns" href="/docs/2026-07-28/tools/inspector/tui">
134 Terminal navigation and keyboard reference.
135 </Card>
126> **[TUI client](/docs/2026-07-28/tools/inspector/tui)**
127>
128> Terminal navigation and keyboard reference.
136129 
137 <Card title="Configuration and flags" icon="sliders" href="/docs/2026-07-28/tools/inspector/configuration">
138 Catalog vs. config files, the full per-client flag reference, and
139 environment variables.
140 </Card>
130> **[Configuration and flags](/docs/2026-07-28/tools/inspector/configuration)**
131>
132> Catalog vs. config files, the full per-client flag reference, and
133> environment variables.
141134 
142 <Card title="Authorization" icon="lock" href="/docs/2026-07-28/tools/inspector/authorization">
143 The OAuth flow end to end, mid-session re-authorization, and loopback
144 callbacks.
145 </Card>
135> **[Authorization](/docs/2026-07-28/tools/inspector/authorization)**
136>
137> The OAuth flow end to end, mid-session re-authorization, and loopback
138> callbacks.
146139 
147 <Card title="Protocol eras" icon="code-branch" href="/docs/2026-07-28/tools/inspector/protocol-eras">
148 Legacy vs. modern (2026-07-28) operation, and how every tab changes between
149 protocol eras.
150 </Card>
140> **[Protocol eras](/docs/2026-07-28/tools/inspector/protocol-eras)**
141>
142> Legacy vs. modern (2026-07-28) operation, and how every tab changes between
143> protocol eras.
151144 
152 <Card title="mcpdo connection client" icon="plug" href="/docs/2026-07-28/tools/inspector/mcpdo">
153 Connect once, then run many commands against a named connection.
154 </Card>
145> **[mcpdo connection client](/docs/2026-07-28/tools/inspector/mcpdo)**
146>
147> Connect once, then run many commands against a named connection.
155148 
156 <Card title="Security" icon="shield-halved" href="/docs/2026-07-28/tools/inspector/security">
157 The threat model: the web backend's token, Docker, secret storage, stdio
158 servers, and the mcpdo daemon.
159 </Card>
149> **[Security](/docs/2026-07-28/tools/inspector/security)**
150>
151> The threat model: the web backend's token, Docker, secret storage, stdio
152> servers, and the mcpdo daemon.
160153 
161 <Card title="Recipes" icon="book" href="/docs/2026-07-28/tools/inspector/recipes">
162 Importing client configs, reviewing MCP Apps, Docker, and network hosting.
163 </Card>
154> **[Recipes](/docs/2026-07-28/tools/inspector/recipes)**
155>
156> Importing client configs, reviewing MCP Apps, Docker, and network hosting.
164157 
165 <Card title="Debugging guide" icon="bug" href="/docs/2026-07-28/tools/debugging">
166 Broader debugging strategies beyond the Inspector.
167 </Card>
168</CardGroup>
158> **[Debugging guide](/docs/2026-07-28/tools/debugging)**
159>
160> Broader debugging strategies beyond the Inspector.
169161 

docs/2026-07-28/tools/inspector/authorization Changed · +58 / -62 lines

from line 6
66 
77## The flow, end to end
88 
9<Steps>
10 <Step title="Connect, and get refused">
11 The Inspector connects to the server URL. The server answers `401`. When the
12 response carries a `WWW-Authenticate` header, it points at the
13 protected-resource metadata URL (`resource_metadata`) and, optionally, the
14 scopes the request requires.
15 </Step>
9**1. Connect, and get refused**
1610 
17 <Step title="Discover the authorization server">
18 The Inspector fetches the server's [protected-resource and
19 authorization-server
20 metadata](/specification/latest/basic/authorization/authorization-server-discovery)
21 to learn the endpoints and the supported grants.
22 </Step>
11The Inspector connects to the server URL. The server answers `401`. When the
12response carries a `WWW-Authenticate` header, it points at the
13protected-resource metadata URL (`resource_metadata`) and, optionally, the
14scopes the request requires.
2315 
24 <Step title="Register or identify the client">
25 The Inspector identifies itself to the authorization server through
26 whichever mechanism is configured: [dynamic client
27 registration](/specification/latest/basic/authorization/client-registration#dynamic-client-registration),
28 a pre-registered static client (`--client-id` / `--client-secret`), a
29 [Client ID Metadata
30 Document](/specification/latest/basic/authorization/client-registration#client-id-metadata-documents)
31 (`--client-metadata-url`), or an [enterprise-managed
32 IdP](/extensions/auth/enterprise-managed-authorization).
33 </Step>
16**2. Discover the authorization server**
3417 
35 <Step title="Authorize in the browser">
36 The Inspector opens the authorization URL. You sign in and consent.
37 </Step>
18The Inspector fetches the server's [protected-resource and
19authorization-server
20metadata](/specification/latest/basic/authorization/authorization-server-discovery)
21to learn the endpoints and the supported grants.
3822 
39 <Step title="Receive the callback">
40 The authorization server redirects to the Inspector's callback URL, carrying
41 the authorization code.
42 </Step>
23**3. Register or identify the client**
4324 
44 <Step title="Exchange and retry">
45 The code is exchanged for tokens, the tokens are persisted, and the original
46 connect is retried. For a [mid-session
47 challenge](#mid-session-re-authorization), the CLI retries the refused
48 request automatically, and the web client asks you to retry the action.
49 </Step>
50</Steps>
25The Inspector identifies itself to the authorization server through
26whichever mechanism is configured: [dynamic client
27registration](/specification/latest/basic/authorization/client-registration#dynamic-client-registration),
28a pre-registered static client (`--client-id` / `--client-secret`), a
29[Client ID Metadata
30Document](/specification/latest/basic/authorization/client-registration#client-id-metadata-documents)
31(`--client-metadata-url`), or an [enterprise-managed
32IdP](/extensions/auth/enterprise-managed-authorization).
5133 
52<Frame caption="Connection Info after a completed OAuth flow: the authorization status, the dynamically registered client, and the granted scopes.">
53 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/auth-connection-info.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=01a8d11058c2d33e0069b8dec98591b5" width="3840" height="2160" data-path="images/inspector/auth-connection-info.png" />
54</Frame>
34**4. Authorize in the browser**
5535 
36The Inspector opens the authorization URL. You sign in and consent.
37 
38**5. Receive the callback**
39 
40The authorization server redirects to the Inspector's callback URL, carrying
41the authorization code.
42 
43**6. Exchange and retry**
44 
45The code is exchanged for tokens, the tokens are persisted, and the original
46connect is retried. For a [mid-session
47challenge](#mid-session-re-authorization), the CLI retries the refused
48request automatically, and the web client asks you to retry the action.
49 
50![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/auth-connection-info.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=01a8d11058c2d33e0069b8dec98591b5)
51 
5652## Callback URLs
5753 
5854The web app listens for the OAuth callback on its own URL, while the CLI and TUI deliberately share a second one:
from line 63
6763 
6864For the CLI and TUI, override it with `--callback-url` or `MCP_OAUTH_CALLBACK_URL`. The web callback always follows the page's origin, so `localhost` and `127.0.0.1` produce different redirect URIs there too.
6965 
70<Warning>
71 The callback URL **must bind a loopback host**: `localhost`, `127.0.0.0/8`, or
72 `[::1]`. The listener receives the authorization code over plaintext `http`,
73 so a non-loopback host is rejected with an error and there is no flag to
74 override that. If your browser runs on a different machine, forward the
75 callback port to it, or complete the login in a web Inspector instead:
76 `--print-handoff` (below) prints a `portForwardCmd` for the web Inspector's
77 ports.
78</Warning>
66> [!WARNING]
67>
68> The callback URL **must bind a loopback host**: `localhost`, `127.0.0.0/8`, or
69> `[::1]`. The listener receives the authorization code over plaintext `http`,
70> so a non-loopback host is rejected with an error and there is no flag to
71> override that. If your browser runs on a different machine, forward the
72> callback port to it, or complete the login in a web Inspector instead:
73> `--print-handoff` (below) prints a `portForwardCmd` for the web Inspector's
74> ports.
7975 
80<Note>
81 Redirect URIs must match your registration **exactly**. `http://localhost:6276/...` and `http://127.0.0.1:6276/...` are different URIs to an authorization server, even though they reach the same listener.
76> [!NOTE]
77>
78> Redirect URIs must match your registration **exactly**. `http://localhost:6276/...` and `http://127.0.0.1:6276/...` are different URIs to an authorization server, even though they reach the same listener.
79>
80> Only one process can hold the default port at a time; a second concurrent flow fails with `EADDRINUSE`. Use a different fixed port per instance, or `http://127.0.0.1:0/oauth/callback` for an OS-assigned ephemeral port when your authorization server supports dynamic redirect-URI registration.
8281 
83 Only one process can hold the default port at a time; a second concurrent flow fails with `EADDRINUSE`. Use a different fixed port per instance, or `http://127.0.0.1:0/oauth/callback` for an OS-assigned ephemeral port when your authorization server supports dynamic redirect-URI registration.
84</Note>
85 
8682## Where credentials live
8783 
8884| File | Contents |
from line 143
147143 
148144The `deepLink` in the handoff block navigates a browser straight to a *connected* Inspector; see [Deep links](/docs/2026-07-28/tools/inspector/web#deep-links).
149145 
150<Note>
151 Because the stored entry records no expiry, a stored refresh token is
152 exercised on **every** `--use-stored-auth` run. With rotating (single-use)
153 refresh tokens that opens two narrow failure windows: two concurrent
154 invocations against the same state file can race for the token, and a crash
155 between a successful refresh and the write-back leaves the rotated token
156 unsaved. Both are unlikely; re-authorize in the web client to recover.
157</Note>
146> [!NOTE]
147>
148> Because the stored entry records no expiry, a stored refresh token is
149> exercised on **every** `--use-stored-auth` run. With rotating (single-use)
150> refresh tokens that opens two narrow failure windows: two concurrent
151> invocations against the same state file can race for the token, and a crash
152> between a successful refresh and the write-back leaves the rotated token
153> unsaved. Both are unlikely; re-authorize in the web client to recover.
158154 
159155## Inspecting auth state
160156 

docs/2026-07-28/tools/inspector/cli Changed · +25 / -25 lines

from line 8
88npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list
99```
1010 
11<Tip>
12 Running several commands against the same server, or a flow that spans one
13 session (tasks, subscriptions, elicitation)? The [mcpdo connection
14 client](/docs/2026-07-28/tools/inspector/mcpdo) connects once and keeps the
15 connection open between commands.
16</Tip>
11> [!TIP]
12>
13> Running several commands against the same server, or a flow that spans one
14> session (tasks, subscriptions, elicitation)? The [mcpdo connection
15> client](/docs/2026-07-28/tools/inspector/mcpdo) connects once and keeps the
16> connection open between commands.
1717 
1818The examples below use the installed `mcp-inspector` binary. Without a global install, prefix each command with `npx @modelcontextprotocol/inspector` instead, as above.
1919 
from line 36
3636 
3737Later examples abbreviate whichever of these forms you use, along with its `--transport` or `--config`/`--server` flags, as `<server>`.
3838 
39<Note>
40 **The config file is the only durable way to give a run its
41 [roots](/specification/draft/client/roots):** there is no roots flag, and the
42 CLI has no `roots/set` method. Roots configured for a server are advertised at
43 connect, so a server that calls `roots/list` (as
44 `@modelcontextprotocol/server-filesystem` does, to learn its allowed
45 directories) gets them.
46</Note>
39> [!NOTE]
40>
41> **The config file is the only durable way to give a run its
42> [roots](/specification/draft/client/roots):** there is no roots flag, and the
43> CLI has no `roots/set` method. Roots configured for a server are advertised at
44> connect, so a server that calls `roots/list` (as
45> `@modelcontextprotocol/server-filesystem` does, to learn its allowed
46> directories) gets them.
4747 
4848See [Configuration and flags](/docs/2026-07-28/tools/inspector/configuration) for `--catalog` vs. `--config`, the `--` separator, and the shared server-selection flags.
4949 
from line 112
112112 
113113The CLI can't render an App, so by default it doesn't advertise the MCP Apps extension at `initialize`. A server that exposes its App tools only to clients claiming App support will then look app-less (exit `2`). Add `--advertise-apps` to claim the extension for that run.
114114 
115<Note>
116 `tools/list --app-info` always emits NDJSON (one line per tool) regardless of
117 `--format`; `--format json` reshapes only the single-tool output of
118 `tools/call --app-info`.
119</Note>
115> [!NOTE]
116>
117> `tools/list --app-info` always emits NDJSON (one line per tool) regardless of
118> `--format`; `--format json` reshapes only the single-tool output of
119> `tools/call --app-info`.
120120 
121121## Exit codes and error envelopes
122122 
from line 219
219219mcp-inspector --cli --catalog ~/.mcp-inspector/mcp.json --method servers/show --server my-server
220220```
221221 
222<Warning>
223 `servers/show` redacts secret-bearing fields (`env` values, sensitive headers,
224 OAuth client secrets), but it does **not** scrub credentials embedded in a
225 server `url` (userinfo or query tokens) or in stdio `args`. Treat raw URL and
226 `detail` fields as sensitive before pasting them into an issue.
227</Warning>
222> [!WARNING]
223>
224> `servers/show` redacts secret-bearing fields (`env` values, sensitive headers,
225> OAuth client secrets), but it does **not** scrub credentials embedded in a
226> server `url` (userinfo or query tokens) or in stdio `args`. Treat raw URL and
227> `detail` fields as sensitive before pasting them into an issue.
228228 
229229### Editing the catalog
230230 

docs/2026-07-28/tools/inspector/configuration Changed · +55 / -55 lines

from line 13
1313 
1414Everything below belongs to a client.
1515 
16<Note>
17 The package also installs a second, separate bin, `mcpdo`, the experimental
18 [connection client](/docs/2026-07-28/tools/inspector/mcpdo). It is not reached
19 through the launcher and takes no mode flag.
20</Note>
16> [!NOTE]
17>
18> The package also installs a second, separate bin, `mcpdo`, the experimental
19> [connection client](/docs/2026-07-28/tools/inspector/mcpdo). It is not reached
20> through the launcher and takes no mode flag.
2121 
2222## Choosing servers
2323 
from line 35
3535 
3636The two are **mutually exclusive**, and neither combines with an ad-hoc target. Passing both is rejected identically by all three clients. The web client is stricter in one respect: it also rejects `--header` and `--protocol-era` alongside a file, where the CLI and TUI apply them on top of the file's settings.
3737 
38<Note>
39 **What a freshly seeded catalog contains depends on the client.** The web backend seeds three sample servers, so a first launch has something to connect to immediately:
38> [!NOTE]
39>
40> **What a freshly seeded catalog contains depends on the client.** The web backend seeds three sample servers, so a first launch has something to connect to immediately:
41>
42> ```json theme={null}
43> {
44> "mcpServers": {
45> "filesystem-server-default": {
46> "type": "stdio",
47> "command": "npx",
48> "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
49> },
50> "everything-server-default": {
51> "type": "stdio",
52> "command": "npx",
53> "args": ["-y", "@modelcontextprotocol/server-everything"]
54> },
55> "example-server-default": {
56> "type": "streamable-http",
57> "url": "https://example-server.modelcontextprotocol.io/mcp"
58> }
59> }
60> }
61> ```
62>
63> The CLI and TUI seed an empty `{ "mcpServers": {} }` instead: they are non-interactive or list-driven, so sample entries would be noise rather than a starting point.
64>
65> Either way, seeding happens only when the file does not exist yet, and a read-only `--config` is never seeded at all.
4066 
41 ```json theme={null}
42 {
43 "mcpServers": {
44 "filesystem-server-default": {
45 "type": "stdio",
46 "command": "npx",
47 "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
48 },
49 "everything-server-default": {
50 "type": "stdio",
51 "command": "npx",
52 "args": ["-y", "@modelcontextprotocol/server-everything"]
53 },
54 "example-server-default": {
55 "type": "streamable-http",
56 "url": "https://example-server.modelcontextprotocol.io/mcp"
57 }
58 }
59 }
60 ```
67> [!NOTE]
68>
69> `--config` is what you want when pointing the Inspector at a config file you
70> didn't write: a coworker's, a client application's, or one checked into a
71> repo. It guarantees the Inspector will not touch the file.
6172 
62 The CLI and TUI seed an empty `{ "mcpServers": {} }` instead: they are non-interactive or list-driven, so sample entries would be noise rather than a starting point.
63 
64 Either way, seeding happens only when the file does not exist yet, and a read-only `--config` is never seeded at all.
65</Note>
66 
67<Note>
68 `--config` is what you want when pointing the Inspector at a config file you
69 didn't write: a coworker's, a client application's, or one checked into a
70 repo. It guarantees the Inspector will not touch the file.
71</Note>
72 
7373### Ad-hoc targets
7474 
7575Instead of a file you can name one server directly, either as a positional command (stdio) or a URL:
from line 186
186186| `MCP_CATALOG_PATH`, `MCP_STORAGE_DIR` | As described under [CLI and TUI](#cli-and-tui). |
187187| `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | Standard proxy routing for outbound MCP connections. |
188188 
189<Warning>
190 Never combine `DANGEROUSLY_OMIT_AUTH` and `DANGEROUSLY_BIND_ALL_INTERFACES`.
191 The web backend spawns processes and holds OAuth tokens, so anyone who can
192 reach it can drive it.
193</Warning>
189> [!WARNING]
190>
191> Never combine `DANGEROUSLY_OMIT_AUTH` and `DANGEROUSLY_BIND_ALL_INTERFACES`.
192> The web backend spawns processes and holds OAuth tokens, so anyone who can
193> reach it can drive it.
194194 
195195What the token does and does not protect is laid out under [The web backend and its API token](/docs/2026-07-28/tools/inspector/security#the-web-backend-and-its-api-token).
196196 
from line 235
235235| Container with **no volume** on the secrets directory | Memory | No, this session only |
236236| Container **with** a volume on the secrets directory | File | Yes |
237237 
238<Warning>
239 **With no keychain, secrets go to a plaintext file, and you did not have to ask for it.** On a host where the keychain probe fails (Linux without libsecret or a running Secret Service, a headless server or SSH session with no D-Bus session, Android/Termux), the Inspector **automatically** stores secrets in `~/.mcp-inspector/secrets.json`. Unless you supply a key, that file is **unencrypted**. Mode `0600` keeps out other non-root users, but not root, not backups or copies of your home directory, and not any program running as you, including the stdio servers the Inspector starts.
240 
241 Pick one:
242 
243 * **Get a keychain back:** install libsecret and run a Secret Service (for example `gnome-keyring`), or run the Inspector inside a desktop session. On the next start the Inspector moves the file's secrets into the keychain and deletes the file.
244 * **Encrypt the file:** supply a generated key with `MCP_INSPECTOR_SECRET_KEY_FILE` (preferred) or `MCP_INSPECTOR_SECRET_KEY`.
245 * **Don't write secrets to disk at all:** `MCP_INSPECTOR_SECRET_STORE=memory`, and re-enter them each session.
246 
247 Even encrypted, secrets on disk carry moderate risk. See [what the file store protects against](/docs/2026-07-28/tools/inspector/security#what-the-file-store-protects-against).
248</Warning>
238> [!WARNING]
239>
240> **With no keychain, secrets go to a plaintext file, and you did not have to ask for it.** On a host where the keychain probe fails (Linux without libsecret or a running Secret Service, a headless server or SSH session with no D-Bus session, Android/Termux), the Inspector **automatically** stores secrets in `~/.mcp-inspector/secrets.json`. Unless you supply a key, that file is **unencrypted**. Mode `0600` keeps out other non-root users, but not root, not backups or copies of your home directory, and not any program running as you, including the stdio servers the Inspector starts.
241>
242> Pick one:
243>
244> * **Get a keychain back:** install libsecret and run a Secret Service (for example `gnome-keyring`), or run the Inspector inside a desktop session. On the next start the Inspector moves the file's secrets into the keychain and deletes the file.
245> * **Encrypt the file:** supply a generated key with `MCP_INSPECTOR_SECRET_KEY_FILE` (preferred) or `MCP_INSPECTOR_SECRET_KEY`.
246> * **Don't write secrets to disk at all:** `MCP_INSPECTOR_SECRET_STORE=memory`, and re-enter them each session.
247>
248> Even encrypted, secrets on disk carry moderate risk. See [what the file store protects against](/docs/2026-07-28/tools/inspector/security#what-the-file-store-protects-against).
249249 
250250[mcpdo](/docs/2026-07-28/tools/inspector/mcpdo) never falls back to `memory`: its commands and its daemon are separate processes, so it uses the file store instead.
251251 

docs/2026-07-28/tools/inspector/protocol-eras Changed · +87 / -115 lines

from line 14
1414| `auto` | Probe `server/discover` first, and fall back to `initialize` on any non-modern outcome. |
1515| `modern` | Pin exactly `2026-07-28`. No fallback, so a non-modern server fails loudly. |
1616 
17<Note>
18 **Why `legacy` is the default, and not `auto`.** A debugging tool must not
19 auto-probe. A `server/discover` probe stalls against silent legacy stdio
20 servers, and it pollutes the recorded transcript you came here to read. Opting
21 into `auto` or `modern` is a deliberate act, so what you see in the Protocol
22 tab is what your server would have seen from a client behaving the way you
23 configured.
24</Note>
17> [!NOTE]
18>
19> **Why `legacy` is the default, and not `auto`.** A debugging tool must not
20> auto-probe. A `server/discover` probe stalls against silent legacy stdio
21> servers, and it pollutes the recorded transcript you came here to read. Opting
22> into `auto` or `modern` is a deliberate act, so what you see in the Protocol
23> tab is what your server would have seen from a client behaving the way you
24> configured.
2525 
2626Once connected, the negotiated era is shown as a badge on the **Protocol** tab's Messages header and in **Connection Info**. On a modern connection, `server/discover` also supplies `capabilities` (including `extensions`), `instructions`, and the list of `supportedVersions`. The server's name and version arrive in the result `_meta` under `io.modelcontextprotocol/serverInfo`.
2727 
28<Frame caption="Server Settings: the Protocol Era selector, with all three choices.">
29 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/settings-protocol-era.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=34566c45f97c8af0e2c0d9ee0493b572" width="3840" height="2160" data-path="images/inspector/settings-protocol-era.png" />
30</Frame>
28![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/settings-protocol-era.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=34566c45f97c8af0e2c0d9ee0493b572)
3129 
3230## Reproducing each era locally
3331 
from line 49
5149 
5250## Logging
5351 
54<Tabs>
55 <Tab title="Legacy">
56 Logging is **session-scoped**. The client sends `logging/setLevel` once, and the server emits `notifications/message` at or above that level for the rest of the session.
52**Legacy**
5753 
58 The **Logs** tab shows a **Set Active Level** selector plus a **Set** button. Choose a level, click Set, and subsequent server logs stream into the panel.
54Logging is **session-scoped**. The client sends `logging/setLevel` once, and the server emits `notifications/message` at or above that level for the rest of the session.
5955 
60 Reproduce with `test-servers/configs/logging-legacy-http.json`.
61 </Tab>
56The **Logs** tab shows a **Set Active Level** selector plus a **Set** button. Choose a level, click Set, and subsequent server logs stream into the panel.
6257 
63 <Tab title="Modern">
64 `logging/setLevel` is **gone**. Instead the client opts in **per request**, by stamping `_meta["io.modelcontextprotocol/logLevel"]` on each outgoing request. A server MUST NOT emit `notifications/message` for a request that did not opt in.
58Reproduce with `test-servers/configs/logging-legacy-http.json`.
6559 
66 The **Logs** tab therefore shows a **Log Level per Request** control instead. Pick a level and every subsequent request carries the stamp, visible in the Network tab's request body. Logs emitted while handling a request ride that request's SSE response stream.
60**Modern**
6761 
68 Set the control to **Off** and the `logLevel` key is omitted entirely, so the same tool call produces no logs at all. That silence is correct behavior, not a bug.
62`logging/setLevel` is **gone**. Instead the client opts in **per request**, by stamping `_meta["io.modelcontextprotocol/logLevel"]` on each outgoing request. A server MUST NOT emit `notifications/message` for a request that did not opt in.
6963 
70 The per-server default is `debug` (opted in at the most verbose level, since the Inspector is a debugging tool); set `modernLogLevel: "off"` on a server to opt back out by default.
64The **Logs** tab therefore shows a **Log Level per Request** control instead. Pick a level and every subsequent request carries the stamp, visible in the Network tab's request body. Logs emitted while handling a request ride that request's SSE response stream.
7165 
72 Reproduce with `test-servers/configs/logging-modern-http.json`.
73 </Tab>
74</Tabs>
66Set the control to **Off** and the `logLevel` key is omitted entirely, so the same tool call produces no logs at all. That silence is correct behavior, not a bug.
7567 
76<Frame caption="Legacy: the Logs tab offers a session-scoped Set Active Level control, and a log arrives after calling send_notification.">
77 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/logs-legacy.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=ec5fddd86d1fe7ecc1ef83549798f33e" width="3840" height="2160" data-path="images/inspector/logs-legacy.png" />
78</Frame>
68The per-server default is `debug` (opted in at the most verbose level, since the Inspector is a debugging tool); set `modernLogLevel: "off"` on a server to opt back out by default.
7969 
80<Frame caption="Modern: the same tab instead offers Log Level per Request. The level is stamped on every outgoing request, and the log rides that request's stream.">
81 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/logs-modern.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=85a1d7118a1e693930aefab8ca6b21a3" width="3840" height="2160" data-path="images/inspector/logs-modern.png" />
82</Frame>
70Reproduce with `test-servers/configs/logging-modern-http.json`.
8371 
72![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/logs-legacy.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=ec5fddd86d1fe7ecc1ef83549798f33e)
73 
74![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/logs-modern.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=85a1d7118a1e693930aefab8ca6b21a3)
75 
8476***
8577 
8678## Resource subscriptions
8779 
88<Tabs>
89 <Tab title="Legacy">
90 Clicking **Subscribe** on a resource sends `resources/subscribe`. The Subscriptions section lists the URI with no stream chrome. When the resource changes, the server emits `notifications/resources/updated` and the subscribed tile's last-updated time is stamped.
80**Legacy**
9181 
92 Reproduce with `test-servers/configs/subscriptions-legacy-http.json`, which also serves an `update_resource` tool so you can drive the notification round-trip yourself.
93 </Tab>
82Clicking **Subscribe** on a resource sends `resources/subscribe`. The Subscriptions section lists the URI with no stream chrome. When the resource changes, the server emits `notifications/resources/updated` and the subscribed tile's last-updated time is stamped.
9483 
95 <Tab title="Modern">
96 The same **Subscribe** button instead sends **`subscriptions/listen`**, with a filter carrying `resourceSubscriptions` plus whichever `*ListChanged` opt-ins apply (such as `resourcesListChanged`). The subscription is confirmed when the server sends `notifications/subscriptions/acknowledged`.
84Reproduce with `test-servers/configs/subscriptions-legacy-http.json`, which also serves an `update_resource` tool so you can drive the notification round-trip yourself.
9785 
98 Because the subscription is now a long-lived stream rather than a session flag, the Subscriptions section grows a **stream-status badge** in its header that moves from `Connecting...` to `Listening`. If the stream drops, the badge shows `Reconnecting...` while the Inspector re-sends `subscriptions/listen`. It shows `Stream ended` once the stream closes for good. It shows `Not acknowledged` when the server answers the listen with a plain result and never sends the acknowledgement; the Inspector does not retry in that case.
86**Modern**
9987 
100 Reproduce with `test-servers/configs/subscriptions-modern-http.json`. To see the `Not acknowledged` state, use `test-servers/configs/subscriptions-never-acknowledged-http.json`. That server acknowledges your first subscription, then refuses every later listen, so subscribing to a second resource trips the badge.
101 </Tab>
102</Tabs>
88The same **Subscribe** button instead sends **`subscriptions/listen`**, with a filter carrying `resourceSubscriptions` plus whichever `*ListChanged` opt-ins apply (such as `resourcesListChanged`). The subscription is confirmed when the server sends `notifications/subscriptions/acknowledged`.
10389 
104<Frame caption="A modern subscription: the Subscriptions section carries a LISTENING stream-status badge that a legacy subscription has no need for.">
105 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/resources-subscriptions-modern.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=5ca4c64c6783eb210fe54248f03de144" width="3840" height="2160" data-path="images/inspector/resources-subscriptions-modern.png" />
106</Frame>
90Because the subscription is now a long-lived stream rather than a session flag, the Subscriptions section grows a **stream-status badge** in its header that moves from `Connecting...` to `Listening`. If the stream drops, the badge shows `Reconnecting...` while the Inspector re-sends `subscriptions/listen`. It shows `Stream ended` once the stream closes for good. It shows `Not acknowledged` when the server answers the listen with a plain result and never sends the acknowledgement; the Inspector does not retry in that case.
10791 
92Reproduce with `test-servers/configs/subscriptions-modern-http.json`. To see the `Not acknowledged` state, use `test-servers/configs/subscriptions-never-acknowledged-http.json`. That server acknowledges your first subscription, then refuses every later listen, so subscribing to a second resource trips the badge.
93 
94![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/resources-subscriptions-modern.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=5ca4c64c6783eb210fe54248f03de144)
95 
10896***
10997 
11098## Tasks
from line 99
11199 
112100Tasks change the most between protocol eras, including *how the Inspector UI tab is gated*.
113101 
114<Tabs>
115 <Tab title="Legacy">
116 The **Tasks** tab appears when the server advertises `capabilities.tasks`. Run a tool with **Run as task** enabled and the tab lists it, populated by `tasks/list` and polled with `tasks/get`. The completed payload is fetched with a **blocking `tasks/result`**, and **Cancel** sends `tasks/cancel`.
102**Legacy**
117103 
118 Reproduce with `test-servers/configs/tasks-legacy-http.json`.
119 </Tab>
104The **Tasks** tab appears when the server advertises `capabilities.tasks`. Run a tool with **Run as task** enabled and the tab lists it, populated by `tasks/list` and polled with `tasks/get`. The completed payload is fetched with a **blocking `tasks/result`**, and **Cancel** sends `tasks/cancel`.
120105 
121 <Tab title="Modern">
122 Tasks are an **extension** (`io.modelcontextprotocol/tasks`, [SEP-2663](/seps/2663-tasks-extension)), so the tab is gated on the server *advertising that extension* rather than on `capabilities.tasks`.
106Reproduce with `test-servers/configs/tasks-legacy-http.json`.
123107 
124 Run a tool as a task and `tools/call` returns a `CreateTaskResult` (`resultType: "task"`, visible in the Protocol and Network tabs). The Inspector polls **`tasks/get`** only; there is no `tasks/list`, so **Refresh** re-polls the handles the client already knows about. A completed task **inlines its result**, with no blocking `tasks/result` call.
108**Modern**
125109 
126 A task that needs more information moves to `input_required` and surfaces an embedded [elicitation](/specification/draft/client/elicitation) in the pending-request modal (the dialog the web client opens whenever a request is waiting on you). Answering it sends **`tasks/update`** carrying the `inputResponses`, and the next poll completes.
110Tasks are an **extension** (`io.modelcontextprotocol/tasks`, [SEP-2663](/seps/2663-tasks-extension)), so the tab is gated on the server *advertising that extension* rather than on `capabilities.tasks`.
127111 
128 Reproduce with `test-servers/configs/tasks-modern-http.json` (tools `modern_task` and `modern_input_task`).
129 </Tab>
130</Tabs>
112Run a tool as a task and `tools/call` returns a `CreateTaskResult` (`resultType: "task"`, visible in the Protocol and Network tabs). The Inspector polls **`tasks/get`** only; there is no `tasks/list`, so **Refresh** re-polls the handles the client already knows about. A completed task **inlines its result**, with no blocking `tasks/result` call.
131113 
132<Frame caption="Legacy: the Tasks tab is populated from tasks/list, and the payload is fetched with a blocking tasks/result.">
133 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tasks-legacy.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=777729bfc1b58b359808c8120d9597d3" width="3840" height="2160" data-path="images/inspector/tasks-legacy.png" />
134</Frame>
114A task that needs more information moves to `input_required` and surfaces an embedded [elicitation](/specification/draft/client/elicitation) in the pending-request modal (the dialog the web client opens whenever a request is waiting on you). Answering it sends **`tasks/update`** carrying the `inputResponses`, and the next poll completes.
135115 
136<Frame caption="Modern: the client polls tasks/get on handles it already holds, and the completed task inlines its result; note resultType: complete in the full task object.">
137 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tasks-modern.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=8e90d128528463fd672537c79db2e1fe" width="3840" height="2160" data-path="images/inspector/tasks-modern.png" />
138</Frame>
116Reproduce with `test-servers/configs/tasks-modern-http.json` (tools `modern_task` and `modern_input_task`).
139117 
118![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tasks-legacy.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=777729bfc1b58b359808c8120d9597d3)
119 
120![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tasks-modern.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=8e90d128528463fd672537c79db2e1fe)
121 
140122***
141123 
142124## Multi-round tool results (MRTR)
from line 139
157139| `mrtr_empty` | One elicitation round, then completes with an empty result (no `content`, no `structuredContent`). |
158140| `mrtr_loop` | Never completes, so the client stops at its `MRTR_MAX_ROUNDS` limit. |
159141 
160<Note>
161 The legacy pattern of a server calling `server.elicitInput` (the test servers'
162 `collect_elicitation` preset) **errors** on a 2026-07-28 connection, because
163 server-to-client requests aren't allowed there. MRTR is its modern
164 replacement.
165</Note>
142> [!NOTE]
143>
144> The legacy pattern of a server calling `server.elicitInput` (the test servers'
145> `collect_elicitation` preset) **errors** on a 2026-07-28 connection, because
146> server-to-client requests aren't allowed there. MRTR is its modern
147> replacement.
166148 
167<Frame caption="An MRTR round paused at the pending-request modal, tagged input_required. Answering it retries the original request.">
168 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/mrtr-pending-request.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=99f8acb7f845a12aed42bcea4d310fee" width="3840" height="2160" data-path="images/inspector/mrtr-pending-request.png" />
169</Frame>
149![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/mrtr-pending-request.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=99f8acb7f845a12aed42bcea4d310fee)
170150 
171<Frame caption="The monitoring sidebar's Protocol tab after mrtr_confirm completes. Both tools/call rounds sit inside one MRTR conversation, Round 1 tagged input_required and Round 2 complete, while unrelated traffic stays outside it.">
172 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/mrtr-protocol-conversation.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=b083d8127a7a67028fcd9702b25aeac1" width="3840" height="2160" data-path="images/inspector/mrtr-protocol-conversation.png" />
173</Frame>
151![](https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/mrtr-protocol-conversation.png?fit=max\&auto=format\&n=pUebPdrb6PY5_mfH\&q=85\&s=b083d8127a7a67028fcd9702b25aeac1)
174152 
175153***
176154 
from line 163
185163 
186164Reproduce with `test-servers/configs/xmcpheader-modern-http.json`.
187165 
188<Note>
189 On a modern connection the Inspector mirrors `x-mcp-header` arguments into
190 `Mcp-Param-*` headers itself, in all three clients. In the **web** client the
191 headers are added by the Inspector's Node backend, which issues the upstream
192 request, so they reach the server even though the browser never sends them.
193</Note>
166> [!NOTE]
167>
168> On a modern connection the Inspector mirrors `x-mcp-header` arguments into
169> `Mcp-Param-*` headers itself, in all three clients. In the **web** client the
170> headers are added by the Inspector's Node backend, which issues the upstream
171> request, so they reach the server even though the browser never sends them.
194172 
195<Frame caption="get_weather shows its mirrored city -> Mcp-Param-City header, and the Network sidebar confirms the call carried mcp-param-city: Boston. invalid_header_tool is struck through under the Excluded (SEP-2243) divider.">
196 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/tools-sep2243.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=0e011c3012332c0a74d0f57fc84a5cf2" width="3840" height="2160" data-path="images/inspector/tools-sep2243.png" />
197</Frame>
173![](https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/tools-sep2243.png?fit=max\&auto=format\&n=pUebPdrb6PY5_mfH\&q=85\&s=0e011c3012332c0a74d0f57fc84a5cf2)
198174 
199175### `-32602` error panels
200176 
from line 199
223199| `trigger_unsupported_version` | `400` | `-32022` | Unsupported version; supported versions in `data.supported`. |
224200| `trigger_method_not_found` | `404` | `-32601` | Method not found. |
225201 
226<Frame caption="The Network tab shows the HTTP layer; here, the 400 Bad Request the strict server answered with.">
227 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/network-modern-headers.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=7df4c01f5ab68aa7632ac3b5a5866b42" width="3840" height="2160" data-path="images/inspector/network-modern-headers.png" />
228</Frame>
202![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/network-modern-headers.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=7df4c01f5ab68aa7632ac3b5a5866b42)
229203 
230<Frame caption="The Protocol tab renders the same failure as a typed spec error: -32022 UnsupportedProtocolVersion, with the versions the server does support.">
231 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/protocol-modern-error.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=d578ea8262ff327e4d61d3697937900b" width="3840" height="2160" data-path="images/inspector/protocol-modern-error.png" />
232</Frame>
204![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/protocol-modern-error.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=d578ea8262ff327e4d61d3697937900b)
233205 
234206***
235207 

docs/2026-07-28/tools/inspector/recipes Changed · +148 / -156 lines

from line 52
5252 
5353`--config` guarantees the file is served as-is and never written, seeded, or migrated.
5454 
55<Frame caption="Add Servers offers import from an existing client config or from a registry server.json.">
56 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/import-config.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=c9f5229c2d827f4bcab37938879f21f2" width="3840" height="2160" data-path="images/inspector/import-config.png" />
57</Frame>
55![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/import-config.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=c9f5229c2d827f4bcab37938879f21f2)
5856 
5957## Reviewing an MCP App
6058 
6159[MCP Apps](/extensions/apps/overview) are tools that carry a UI widget. For an automated reviewer (CI or an agent), use the CLI for every check that returns JSON, and open a browser only to inspect the rendered widget.
6260 
63<Steps>
64 <Step title="Probe the security posture without calling the tool">
65 ```bash theme={null}
66 mcp-inspector --cli --transport http --server-url https://example.com/mcp \
67 --method tools/call --tool-name <tool> --app-info --advertise-apps
68 ```
61**1. Probe the security posture without calling the tool**
6962 
70 `--advertise-apps` makes the CLI claim MCP Apps support at `initialize`. It is off by default because the CLI cannot render an app, but a server that shows its app tools only to app-capable clients would otherwise report no app.
63```bash theme={null}
64mcp-inspector --cli --transport http --server-url https://example.com/mcp \
65 --method tools/call --tool-name <tool> --app-info --advertise-apps
66```
7167 
72 One JSON line on stdout; exit `0` if the tool has an app, `2` if not, so an `&&` chain short-circuits:
68`--advertise-apps` makes the CLI claim MCP Apps support at `initialize`. It is off by default because the CLI cannot render an app, but a server that shows its app tools only to app-capable clients would otherwise report no app.
7369 
74 ```json theme={null}
75 {
76 "hasApp": true,
77 "toolName": "get_pros",
78 "resourceUri": "ui://pros/view.html",
79 "csp": { "connectDomains": ["https://api.example.com"] },
80 "permissions": { "clipboard": false },
81 "prefersBorder": true,
82 "resourceMimeType": "text/html"
83 }
84 ```
70One JSON line on stdout; exit `0` if the tool has an app, `2` if not, so an `&&` chain short-circuits:
8571 
86 `csp` and `permissions` (and `domain`, when the resource declares one) live on the UI **resource** rather than the tool, so `--app-info` reads that resource. The tool is never called.
87 </Step>
72```json theme={null}
73{
74 "hasApp": true,
75 "toolName": "get_pros",
76 "resourceUri": "ui://pros/view.html",
77 "csp": { "connectDomains": ["https://api.example.com"] },
78 "permissions": { "clipboard": false },
79 "prefersBorder": true,
80 "resourceMimeType": "text/html"
81}
82```
8883 
89 <Step title="Get the full result payload, still with no browser">
90 ```bash theme={null}
91 mcp-inspector --cli --transport http --server-url https://example.com/mcp \
92 --method tools/call --tool-name <tool> --tool-args-json '{"zip":"10001"}' --format json
93 ```
94 </Step>
84`csp` and `permissions` (and `domain`, when the resource declares one) live on the UI **resource** rather than the tool, so `--app-info` reads that resource. The tool is never called.
9585 
96 <Step title="Launch the web Inspector once, loopback-only">
97 ```bash theme={null}
98 TOKEN="$(openssl rand -hex 24)"
99 HOST=127.0.0.1 CLIENT_PORT=6274 MCP_SANDBOX_PORT=6275 \
100 MCP_AUTO_OPEN_ENABLED=false MCP_INSPECTOR_API_TOKEN="$TOKEN" \
101 mcp-inspector --web &
102 ```
86**2. Get the full result payload, still with no browser**
10387 
104 Pinning `MCP_SANDBOX_PORT` keeps the address explicit: the app's UI is served from a separate sandbox port, and your automation needs to know where it is.
105 </Step>
88```bash theme={null}
89mcp-inspector --cli --transport http --server-url https://example.com/mcp \
90 --method tools/call --tool-name <tool> --tool-args-json '{"zip":"10001"}' --format json
91```
10692 
107 <Step title="Navigate one deep link to a rendered widget">
108 ```
109 http://127.0.0.1:6274/?serverUrl=<encoded url>&transport=http&autoConnect=<TOKEN>&openApp=<tool>&appArgs=<base64url(JSON)>&autoOpen=<TOKEN>
110 ```
93**3. Launch the web Inspector once, loopback-only**
11194 
112 `appArgs` is the tool's arguments as base64url-encoded JSON, and every deep-link parameter is described under [Deep links](/docs/2026-07-28/tools/inspector/web#deep-links). `autoConnect` and `autoOpen` must both equal the session token, since `autoOpen` fires a tool call straight from the URL and needs the same gate as `autoConnect`.
113 </Step>
95```bash theme={null}
96TOKEN="$(openssl rand -hex 24)"
97HOST=127.0.0.1 CLIENT_PORT=6274 MCP_SANDBOX_PORT=6275 \
98MCP_AUTO_OPEN_ENABLED=false MCP_INSPECTOR_API_TOKEN="$TOKEN" \
99mcp-inspector --web &
100```
114101 
115 <Step title="Wait on a deterministic signal instead of sleeping">
116 The Apps screen exposes a stable automation contract. Poll these attributes instead of sleeping:
102Pinning `MCP_SANDBOX_PORT` keeps the address explicit: the app's UI is served from a separate sandbox port, and your automation needs to know where it is.
117103 
118 | Selector | Attribute | Values |
119 | - | - | - |
120 | `[data-testid="apps-form"]` | `data-app-status` | `idle`, then `loading`, then `ready` or `error` (on `error`, `data-app-error` carries the reason) |
121 | `[data-testid="connection-status"]` | `data-status` | `disconnected`, `connecting`, then `connected` or `error` (`data-error-message` has the detail) |
122 | `[data-testid="connection-status"]` | `data-deeplink` | `parsed`, `rejected`, or `none` (`none` means no deep link was given, `rejected` means one was refused) |
123 </Step>
124</Steps>
104**4. Navigate one deep link to a rendered widget**
125105 
106```
107http://127.0.0.1:6274/?serverUrl=<encoded url>&transport=http&autoConnect=<TOKEN>&openApp=<tool>&appArgs=<base64url(JSON)>&autoOpen=<TOKEN>
108```
109 
110`appArgs` is the tool's arguments as base64url-encoded JSON, and every deep-link parameter is described under [Deep links](/docs/2026-07-28/tools/inspector/web#deep-links). `autoConnect` and `autoOpen` must both equal the session token, since `autoOpen` fires a tool call straight from the URL and needs the same gate as `autoConnect`.
111 
112**5. Wait on a deterministic signal instead of sleeping**
113 
114The Apps screen exposes a stable automation contract. Poll these attributes instead of sleeping:
115 
116| Selector | Attribute | Values |
117| - | - | - |
118| `[data-testid="apps-form"]` | `data-app-status` | `idle`, then `loading`, then `ready` or `error` (on `error`, `data-app-error` carries the reason) |
119| `[data-testid="connection-status"]` | `data-status` | `disconnected`, `connecting`, then `connected` or `error` (`data-error-message` has the detail) |
120| `[data-testid="connection-status"]` | `data-deeplink` | `parsed`, `rejected`, or `none` (`none` means no deep link was given, `rejected` means one was refused) |
121 
126122## Docker
127123 
128124A container image is published to GitHub Container Registry for `linux/amd64` and `linux/arm64`:
from line 131
135131 
136132The image defaults to `--web`, bound to `0.0.0.0:6274` with browser auto-open off, and runs as the non-root `node` user (uid `1000`). It sets `DANGEROUSLY_BIND_ALL_INTERFACES=true` because a container must bind the wildcard address to be reachable through `-p`.
137133 
138<Warning>
139 **Keep the `127.0.0.1:` prefix on every published port.** A bare `-p
140 6274:6274` publishes on every host interface, putting a backend that spawns
141 processes, and the page that discloses its token, on your local network. The
142 image's `DANGEROUSLY_BIND_ALL_INTERFACES` covers the container's interfaces,
143 not the host's. See [Publish the port on loopback
144 only](/docs/2026-07-28/tools/inspector/security#publish-the-port-on-loopback-only).
145</Warning>
134> [!WARNING]
135>
136> **Keep the `127.0.0.1:` prefix on every published port.** A bare `-p
137> 6274:6274` publishes on every host interface, putting a backend that spawns
138> processes, and the page that discloses its token, on your local network. The
139> image's `DANGEROUSLY_BIND_ALL_INTERFACES` covers the container's interfaces,
140> not the host's. See [Publish the port on loopback
141> only](/docs/2026-07-28/tools/inspector/security#publish-the-port-on-loopback-only).
146142 
147143To use the **Apps** tab, also publish the MCP Apps sandbox port, `6275`, and `6278` for an app that declares `_meta.ui.domain`. Publish each on the same port number inside and out, since the browser is handed the in-container port:
148144 
from line 166
170166 
171167If you bind-mount a host directory instead of a named volume, it keeps its host ownership, so on Linux add `--user "$(id -u):$(id -g)"` or `chown` it to uid `1000`, or saves fail with `EACCES`. Don't bind-mount the secrets file on its own: it isn't recognized as durable, and it can't be replaced atomically.
172168 
173<Warning>
174 **Mounting that volume turns on file storage of secrets, and without a key the file is plaintext.** Every OAuth token acquired, and every client secret and stdio `env:` value you save, is then written to `secrets.json` on the volume. It is readable by root and every member of the host's `docker` group, and by anyone who gets a backup, snapshot or copy of the volume.
169> [!WARNING]
170>
171> **Mounting that volume turns on file storage of secrets, and without a key the file is plaintext.** Every OAuth token acquired, and every client secret and stdio `env:` value you save, is then written to `secrets.json` on the volume. It is readable by root and every member of the host's `docker` group, and by anyone who gets a backup, snapshot or copy of the volume.
172>
173> Give it a key, generated into a file that only you can read and that sits outside the volume, its backups and any repository:
174>
175> ```bash theme={null}
176> mkdir -p ~/.config/mcp-inspector
177> (umask 077 && openssl rand -base64 32 > ~/.config/mcp-inspector/secret-key)
178> ```
179>
180> Even encrypted, secrets on disk carry moderate risk. See [what the file store protects against](/docs/2026-07-28/tools/inspector/security#what-the-file-store-protects-against).
175181 
176 Give it a key, generated into a file that only you can read and that sits outside the volume, its backups and any repository:
182Hand the key to the container **as a file** with `MCP_INSPECTOR_SECRET_KEY_FILE`, not as an environment variable. A key passed with `-e MCP_INSPECTOR_SECRET_KEY=…` is readable by anyone who can run `docker inspect` or `docker exec`.
177183 
178 ```bash theme={null}
179 mkdir -p ~/.config/mcp-inspector
180 (umask 077 && openssl rand -base64 32 > ~/.config/mcp-inspector/secret-key)
181 ```
184**docker run**
182185 
183 Even encrypted, secrets on disk carry moderate risk. See [what the file store protects against](/docs/2026-07-28/tools/inspector/security#what-the-file-store-protects-against).
184</Warning>
186```bash theme={null}
187docker run --rm -p 127.0.0.1:6274:6274 \
188 -v mcp-inspector-data:/home/node/.mcp-inspector \
189 -v "$HOME/.config/mcp-inspector/secret-key:/run/secrets/mcp_inspector_secret_key:ro" \
190 -e MCP_INSPECTOR_SECRET_KEY_FILE=/run/secrets/mcp_inspector_secret_key \
191 ghcr.io/modelcontextprotocol/inspector
192```
185193 
186Hand the key to the container **as a file** with `MCP_INSPECTOR_SECRET_KEY_FILE`, not as an environment variable. A key passed with `-e MCP_INSPECTOR_SECRET_KEY=…` is readable by anyone who can run `docker inspect` or `docker exec`.
194**Compose secrets**
187195 
188<Tabs>
189 <Tab title="docker run">
190 ```bash theme={null}
191 docker run --rm -p 127.0.0.1:6274:6274 \
192 -v mcp-inspector-data:/home/node/.mcp-inspector \
193 -v "$HOME/.config/mcp-inspector/secret-key:/run/secrets/mcp_inspector_secret_key:ro" \
194 -e MCP_INSPECTOR_SECRET_KEY_FILE=/run/secrets/mcp_inspector_secret_key \
195 ghcr.io/modelcontextprotocol/inspector
196 ```
197 </Tab>
196```yaml theme={null}
197services:
198 inspector:
199 image: ghcr.io/modelcontextprotocol/inspector
200 ports: ["127.0.0.1:6274:6274"]
201 volumes: ["mcp-inspector-data:/home/node/.mcp-inspector"]
202 environment:
203 MCP_INSPECTOR_SECRET_KEY_FILE: /run/secrets/mcp_inspector_secret_key
204 secrets: [mcp_inspector_secret_key]
205secrets:
206 mcp_inspector_secret_key:
207 file: ${HOME}/.config/mcp-inspector/secret-key
208volumes:
209 mcp-inspector-data:
210```
198211 
199 <Tab title="Compose secrets">
200 ```yaml theme={null}
201 services:
202 inspector:
203 image: ghcr.io/modelcontextprotocol/inspector
204 ports: ["127.0.0.1:6274:6274"]
205 volumes: ["mcp-inspector-data:/home/node/.mcp-inspector"]
206 environment:
207 MCP_INSPECTOR_SECRET_KEY_FILE: /run/secrets/mcp_inspector_secret_key
208 secrets: [mcp_inspector_secret_key]
209 secrets:
210 mcp_inspector_secret_key:
211 file: ${HOME}/.config/mcp-inspector/secret-key
212 volumes:
213 mcp-inspector-data:
214 ```
215 </Tab>
216</Tabs>
217 
218212Without Swarm, Compose secrets are bind mounts that keep the host file's owner and mode, so the `0600` key file must be owned by uid `1000`. On a Linux host where your uid is different, run `sudo chown 1000 ~/.config/mcp-inspector/secret-key` rather than loosening its mode. Supply the **same** key on every run.
219213 
220214If the key file is missing, unreadable or empty, or both key variables are set, the Inspector **refuses to read or write the secrets file** rather than falling back to plaintext, and says why in the log and in the settings dialogs' footer. Everything else about the store (selection order, location, permissions) is under [Where secrets are stored](/docs/2026-07-28/tools/inspector/configuration#where-secrets-are-stored).
from line 223
229223docker run --rm ghcr.io/modelcontextprotocol/inspector --cli <target> --method tools/list
230224```
231225 
232<Warning>
233 **If you remap the published port, set `ALLOWED_ORIGINS`.** With `-p
234 127.0.0.1:8080:6274` the browser's origin becomes `http://localhost:8080`,
235 which no longer matches the in-container port, and connects will `403`. Either
236 run `-e CLIENT_PORT=8080 -p 127.0.0.1:8080:8080`, or set `-e
237 ALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080`.
238</Warning>
226> [!WARNING]
227>
228> **If you remap the published port, set `ALLOWED_ORIGINS`.** With `-p
229> 127.0.0.1:8080:6274` the browser's origin becomes `http://localhost:8080`,
230> which no longer matches the in-container port, and connects will `403`. Either
231> run `-e CLIENT_PORT=8080 -p 127.0.0.1:8080:8080`, or set `-e
232> ALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080`.
239233 
240234## Hosting on a network
241235 
from line 243
249243| **Behind TLS or a reverse proxy** | The browser's `Origin` becomes the public origin, which won't match the bind host. Set `ALLOWED_ORIGINS=https://inspector.example.com`, and for MCP Apps set `MCP_SANDBOX_FULL_ADDRESS` (see below). |
250244| **Wildcard bind (containers)** | Set `DANGEROUSLY_BIND_ALL_INTERFACES=true`. Loopback access still works out of the box; reaching it at a non-loopback address needs `ALLOWED_ORIGINS`. |
251245 
252<Warning>
253 `ALLOWED_ORIGINS` **replaces** the default list rather than merging with it. List every origin you'll browse from, including the loopback forms you want to keep:
246> [!WARNING]
247>
248> `ALLOWED_ORIGINS` **replaces** the default list rather than merging with it. List every origin you'll browse from, including the loopback forms you want to keep:
249>
250> ```
251> ALLOWED_ORIGINS=http://localhost:6274,http://127.0.0.1:6274,http://192.168.1.50:6274
252> ```
253>
254> Each entry must include the scheme; a scheme-less value is dropped with a warning. A blank value does **not** disable the check; it falls back to the default. There is no knob to turn origin validation off.
254255 
255 ```
256 ALLOWED_ORIGINS=http://localhost:6274,http://127.0.0.1:6274,http://192.168.1.50:6274
257 ```
258 
259 Each entry must include the scheme; a scheme-less value is dropped with a warning. A blank value does **not** disable the check; it falls back to the default. There is no knob to turn origin validation off.
260</Warning>
261 
262256Two further caveats when going off loopback:
263257 
264258* **MCP Apps need their sandbox port reachable too.** It's a separate listener (`6275` by default, set with `MCP_SANDBOX_PORT`), so expose or forward it alongside the web port.
from line 281
287281 
288282A loop that works well in practice:
289283 
290<Steps>
291 <Step title="Start with the CLI">
292 `--method initialize` confirms the server starts, handshakes, and reports
293 the capabilities you expect, in one second, with a machine-readable answer.
294 Most "it doesn't work" turns out to be here.
295 </Step>
284**1. Start with the CLI**
296285 
297 <Step title="Move to the web client for exploration">
298 Schema-driven forms, rendered results, and the Protocol tab beside them make
299 it fast to find the case where a tool misbehaves.
300 </Step>
286`--method initialize` confirms the server starts, handshakes, and reports
287the capabilities you expect, in one second, with a machine-readable answer.
288Most "it doesn't work" turns out to be here.
301289 
302 <Step title="Test the edges">
303 Invalid inputs, missing required prompt arguments, concurrent calls, and,
304 for HTTP servers, both protocol eras. Verify the *errors* are as intentional
305 as the successes.
306 </Step>
290**2. Move to the web client for exploration**
307291 
308 <Step title="Lock it in with the CLI">
309 Turn what you found into a CI assertion: pipe the CLI's `--format json`
310 output to `jq -e` with `--stored-auth-only`, so a missing token fails fast
311 instead of starting interactive OAuth. See [Verify a server in
312 CI](/docs/2026-07-28/tools/inspector/cli#verify-a-server-in-ci) for the full
313 command.
314 </Step>
315</Steps>
292Schema-driven forms, rendered results, and the Protocol tab beside them make
293it fast to find the case where a tool misbehaves.
294 
295**3. Test the edges**
296 
297Invalid inputs, missing required prompt arguments, concurrent calls, and,
298for HTTP servers, both protocol eras. Verify the *errors* are as intentional
299as the successes.
300 
301**4. Lock it in with the CLI**
302 
303Turn what you found into a CI assertion: pipe the CLI's `--format json`
304output to `jq -e` with `--stored-auth-only`, so a missing token fails fast
305instead of starting interactive OAuth. See [Verify a server in
306CI](/docs/2026-07-28/tools/inspector/cli#verify-a-server-in-ci) for the full
307command.
316308 

docs/2026-07-28/tools/inspector/security Changed · +16 / -16 lines

from line 26
2626 
2727The backend binds `127.0.0.1` by default. Binding every interface (`0.0.0.0`, `::` and equivalent spellings) is refused unless you set `DANGEROUSLY_BIND_ALL_INTERFACES=true`. Binding one specific address is allowed without that opt-in, because it is one deliberate exposure rather than all of them at once. See [Hosting on a network](/docs/2026-07-28/tools/inspector/recipes#hosting-on-a-network).
2828 
29<Warning>
30 Never combine `DANGEROUSLY_OMIT_AUTH` with a non-loopback bind. If you need
31 the Inspector reachable by others, put a real access-control boundary in front
32 of it: an authenticating reverse proxy, an SSH tunnel, or a private network.
33</Warning>
29> [!WARNING]
30>
31> Never combine `DANGEROUSLY_OMIT_AUTH` with a non-loopback bind. If you need
32> the Inspector reachable by others, put a real access-control boundary in front
33> of it: an authenticating reverse proxy, an SSH tunnel, or a private network.
3434 
3535## Docker
3636 
from line 68
6868 
6969### The automatic plaintext fallback
7070 
71<Warning>
72 On a host where the OS keychain cannot be reached (Linux without libsecret or a running Secret Service, a headless server or SSH session with no D-Bus session, Android/Termux), the Inspector **automatically** stores secrets in `~/.mcp-inspector/secrets.json`. You do not have to ask for it, and unless you supply a key that file is **unencrypted**.
71> [!WARNING]
72>
73> On a host where the OS keychain cannot be reached (Linux without libsecret or a running Secret Service, a headless server or SSH session with no D-Bus session, Android/Termux), the Inspector **automatically** stores secrets in `~/.mcp-inspector/secrets.json`. You do not have to ask for it, and unless you supply a key that file is **unencrypted**.
74>
75> The only signs are a warning on stderr when the store is selected and a footer in the web client's settings dialogs.
7376 
74 The only signs are a warning on stderr when the store is selected and a footer in the web client's settings dialogs.
75</Warning>
76 
7777Treat that as a risk, not just a configuration fact. The file is written with mode `0600`, which keeps out other non-root users and nothing else. To close it, do one of the following:
7878 
7979* get a keychain back (install libsecret and run a Secret Service such as `gnome-keyring`, or run inside a desktop session);
from line 145
145145 
146146Tokens are compared in constant time. A request line larger than 1 MiB is rejected. A command presenting the wrong token to a live daemon fails loudly. It never replaces that daemon.
147147 
148<Note>
149 Private mode separates connections and daemon state between shells. It is
150 **not** a security boundary against other processes running as your user:
151 anything with your UID that learns the daemon directory can read its token.
152 For a hard boundary, use a separate user account or a container.
153</Note>
148> [!NOTE]
149>
150> Private mode separates connections and daemon state between shells. It is
151> **not** a security boundary against other processes running as your user:
152> anything with your UID that learns the daemon directory can read its token.
153> For a hard boundary, use a separate user account or a container.
154154 
155155### What it holds in memory
156156 

docs/2026-07-28/tools/inspector/tui Changed · +6 / -10 lines

from line 8
88npx @modelcontextprotocol/inspector --tui node build/index.js # with an ad-hoc stdio server
99```
1010 
11<Frame caption="The TUI connected to a server, on the Tools tab, showing a tool's input schema.">
12 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tui-tools.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=5d11d64f4b98df8c26e7ac048f38576f" width="2986" height="1832" data-path="images/inspector/tui-tools.png" />
13</Frame>
11![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tui-tools.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=5d11d64f4b98df8c26e7ac048f38576f)
1412 
1513## Choosing servers
1614 
from line 72
7472 
7573The trade-off is that only one TUI OAuth flow can hold the port at a time; a second concurrent flow fails with `EADDRINUSE`. To override it, pass `--callback-url` or set `MCP_OAUTH_CALLBACK_URL`: use a different fixed port per instance, or `http://127.0.0.1:0/oauth/callback` for an OS-assigned ephemeral port when your authorization server registers redirect URIs dynamically.
7674 
77<Warning>
78 Redirect URIs must match **exactly** what you registered. `localhost` and
79 `127.0.0.1` are different URIs as far as an authorization server is concerned.
80</Warning>
75> [!WARNING]
76>
77> Redirect URIs must match **exactly** what you registered. `localhost` and
78> `127.0.0.1` are different URIs as far as an authorization server is concerned.
8179 
8280Per-server OAuth fields in the catalog (static client id/secret, scopes, the enterprise-managed flag) are applied automatically. Install-wide settings (CIMD, enterprise IdP) come from `~/.mcp-inspector/storage/client.json`, the same file the web client's **Client Settings** dialog writes. Point at a different one with `--client-config` or `MCP_CLIENT_CONFIG_PATH`.
8381 
8482See [Authorization](/docs/2026-07-28/tools/inspector/authorization) for the full picture.
8583 
86<Frame caption="The Auth tab. It shows the same OAuth fields as the web client's Connection Info or, before any authorization has happened, says there is no OAuth information yet.">
87 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tui-auth.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=2e5ef574e80e81c4b49fa2ef0eac0528" width="2986" height="1832" data-path="images/inspector/tui-auth.png" />
88</Frame>
84![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tui-auth.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=2e5ef574e80e81c4b49fa2ef0eac0528)
8985 
9086## Requirements
9187 

docs/2026-07-28/tools/inspector/web Changed · +8 / -24 lines

from line 50
5050 
5151**Network** and **Console** never appear together. Legacy and modern eras are described in [Protocol eras](/docs/2026-07-28/tools/inspector/protocol-eras).
5252 
53<Frame caption="The tab bar on a connected server. Which tabs appear depends on the capabilities the server reported.">
54 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-tab-bar.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=04bb61c4a45ff12e8337c701e195c386" width="3840" height="2400" data-path="images/inspector/web-tab-bar.png" />
55</Frame>
53![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-tab-bar.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=04bb61c4a45ff12e8337c701e195c386)
5654 
5755### The monitoring sidebar
5856 
5957**Tasks**, **Logs**, **Protocol**, **Network**, and **Console** form a *monitor group*. Pin the group and they leave the tab bar and move into a resizable right-hand column, so you can watch traffic while working in Tools or Resources. Drag the column's edge to resize it, from 320 to 720 pixels wide. The column width and the selected monitor tab persist across reloads.
6058 
61<Frame caption="The monitoring sidebar pinned beside the Tools screen and dragged to its full width, with the tool call's request and response expanded in the Protocol stream.">
62 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/web-monitor-sidebar.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=2ced9eeefa020be53fe4fe957b7d07c6" width="3840" height="2160" data-path="images/inspector/web-monitor-sidebar.png" />
63</Frame>
59![](https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/web-monitor-sidebar.png?fit=max\&auto=format\&n=pUebPdrb6PY5_mfH\&q=85\&s=2ced9eeefa020be53fe4fe957b7d07c6)
6460 
6561## Servers
6662 
from line 85
8985 
9086A footer at the bottom of **Server Settings**, **Client Settings** and the **Add / Edit / Clone server** dialogs names the secret store in use, so you see it where you type a secret. It turns into a warning when secrets are memory-only (lost on restart), in an unencrypted file, in a file with loose permissions, or in a file that can't be read.
9187 
92<Frame caption="Server Settings with Advertised Extensions expanded. Unchecking one changes what the Inspector declares at connect.">
93 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-server-settings.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=d42be09ee8de7e45e58a8ff1a444ba52" width="3840" height="2160" data-path="images/inspector/web-server-settings.png" />
94</Frame>
88![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-server-settings.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=d42be09ee8de7e45e58a8ff1a444ba52)
9589 
9690## Tools
9791 
from line 93
9993 
10094On modern-era servers this screen also shows mirrored `Mcp-Param-*` headers, excluded tools, and distinct `-32602` error panels, all covered in [Protocol eras](/docs/2026-07-28/tools/inspector/protocol-eras#tools-mirrored-headers-and-excluded-tools).
10195 
102<Frame caption="A tool call and its rendered result. The argument form collapses into the result panel once the call returns.">
103 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-tools.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=7ef469a969f398ac0ec70cf019c133da" width="3840" height="2160" data-path="images/inspector/web-tools.png" />
104</Frame>
96![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-tools.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=7ef469a969f398ac0ec70cf019c133da)
10597 
10698## Resources
10799 
108100Lists resources and resource templates with their MIME types and descriptions, reads content on selection, and offers **Subscribe** on servers that support subscriptions. The subscription mechanics differ by era; see [Resource subscriptions](/docs/2026-07-28/tools/inspector/protocol-eras#resource-subscriptions).
109101 
110<Frame caption="A resource read, with an active subscription listed below the resource list.">
111 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-resources.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=1a94ea452e1ef8aaf2c9f486ed810b28" width="3840" height="2160" data-path="images/inspector/web-resources.png" />
112</Frame>
102![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-resources.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=1a94ea452e1ef8aaf2c9f486ed810b28)
113103 
114104## Prompts
115105 
116106Lists prompt templates with their arguments, and renders the generated messages for the arguments you supply, which is the fastest way to confirm a prompt produces what you intended.
117107 
118<Frame caption="A prompt rendered with the arguments supplied.">
119 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-prompts.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=81b16312b1adff5601622a72444b0f92" width="3840" height="2160" data-path="images/inspector/web-prompts.png" />
120</Frame>
108![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-prompts.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=81b16312b1adff5601622a72444b0f92)
121109 
122110## Apps
123111 
from line 117
129117 
130118See [Recipes](/docs/2026-07-28/tools/inspector/recipes#reviewing-an-mcp-app) for the CLI-first automated review flow.
131119 
132<Frame caption="An MCP App rendered in its sandboxed frame, with the app's own logs below it.">
133 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-apps.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=bd311848514f7d251640012986abfc4d" width="3840" height="2160" data-path="images/inspector/web-apps.png" />
134</Frame>
120![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-apps.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=bd311848514f7d251640012986abfc4d)
135121 
136122## Protocol, Network, and Console
137123 
from line 129
143129 
144130Secrets in Network headers and bodies are masked, with a control to reveal them; Protocol and Console show traffic as sent. Entries can be cleared or exported.
145131 
146<Frame caption="The Protocol tab with an entry expanded, showing the full JSON-RPC exchange.">
147 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-protocol.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=f31338c83a389c5588f11c0d5b2b97ed" width="3840" height="2160" data-path="images/inspector/web-protocol.png" />
148</Frame>
132![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-protocol.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=f31338c83a389c5588f11c0d5b2b97ed)
149133 
150134## Deep links
151135 

docs/2026-07-28/tutorials/security/authorization Changed · +949 / -969 lines

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 16
1616* You're building for enterprise environments with strict access controls
1717* You want to implement rate limiting or usage tracking per user
1818 
19<Tip>
20 **Authorization for Local MCP Servers**
21 
22 For MCP servers using the [STDIO transport](/specification/latest/basic/transports#stdio), you can use environment-based credentials or credentials provided by third-party libraries embedded directly in the MCP server instead. Because a STDIO-built MCP server runs locally, it has access to a range of flexible options when it comes to acquiring user credentials that may or may not rely on in-browser authentication and authorization flows.
23 
24 OAuth flows, in turn, are designed for HTTP-based transports where the MCP server is remotely-hosted and the client uses OAuth to establish that a user is authorized to access said remote server.
25</Tip>
19> [!TIP]
20>
21> **Authorization for Local MCP Servers**
22>
23> For MCP servers using the [STDIO transport](/specification/latest/basic/transports#stdio), you can use environment-based credentials or credentials provided by third-party libraries embedded directly in the MCP server instead. Because a STDIO-built MCP server runs locally, it has access to a range of flexible options when it comes to acquiring user credentials that may or may not rely on in-browser authentication and authorization flows.
24>
25> OAuth flows, in turn, are designed for HTTP-based transports where the MCP server is remotely-hosted and the client uses OAuth to establish that a user is authorized to access said remote server.
2626 
2727## The Authorization Flow: Step by Step
2828 
2929Let's walk through what happens when a client wants to connect to your protected MCP server:
3030 
31<Steps>
32 <Step title="Initial Handshake">
33 When your MCP client first tries to connect, your server responds with a `401 Unauthorized` and tells the client where to find authorization information, captured in a [Protected Resource Metadata (PRM) document](https://datatracker.ietf.org/doc/html/rfc9728). The document is hosted by the MCP server, follows a predictable path pattern, and is provided to the client in the `resource_metadata` parameter within the `WWW-Authenticate` header.
34 
35 ```http theme={null}
36 HTTP/1.1 401 Unauthorized
37 WWW-Authenticate: Bearer realm="mcp",
38 resource_metadata="https://your-server.com/.well-known/oauth-protected-resource"
39 ```
40 
41 This tells the client that authorization is required for the MCP server and where to get the necessary information to kickstart the authorization flow.
42 </Step>
43 
44 <Step title="Protected Resource Metadata Discovery">
45 With the URI pointer to the PRM document, the client will fetch the metadata to learn about the authorization server, supported scopes, and other resource information. The data is typically encapsulated in a JSON blob, similar to the one below.
46 
47 ```json theme={null}
48 {
49 "resource": "https://your-server.com/mcp",
50 "authorization_servers": ["https://auth.your-server.com"],
51 "scopes_supported": ["mcp:tools", "mcp:resources"]
52 }
53 ```
54 
55 You can see a more comprehensive example in [RFC 9728 Section 3.2](https://datatracker.ietf.org/doc/html/rfc9728#name-protected-resource-metadata-r).
56 </Step>
57 
58 <Step title="Authorization Server Discovery">
59 Next, the client discovers what the authorization server can do by fetching its metadata. If the PRM document lists more than one authorization server, the client can decide which one to use.
60 
61 With an authorization server selected, the client will then construct a standard metadata URI and issue a request to the [OpenID Connect (OIDC) Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) or [OAuth 2.0 Auth Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414) endpoints (depending on authorization server support)
62 and retrieve another set of metadata properties that will allow it to know the endpoints it needs to complete the authorization flow.
63 
64 ```json theme={null}
65 {
66 "issuer": "https://auth.your-server.com",
67 "authorization_endpoint": "https://auth.your-server.com/authorize",
68 "token_endpoint": "https://auth.your-server.com/token",
69 "registration_endpoint": "https://auth.your-server.com/register"
70 }
71 ```
72 </Step>
73 
74 <Step title="Client Registration">
75 With all the metadata out of the way, the client now needs to make sure that it's registered with the authorization server. This can be done in two ways.
76 
77 First, the client can be **pre-registered** with a given authorization server, in which case it can have embedded client registration information that it uses to complete the authorization flow.
78 
79 Alternatively, the client can use **Dynamic Client Registration** (DCR) to dynamically register itself with the authorization server. The latter scenario requires the authorization server to support DCR. If the authorization server does support DCR, the client will send a request to the `registration_endpoint` with its information:
80 
81 ```json theme={null}
82 {
83 "client_name": "My MCP Client",
84 "redirect_uris": ["http://localhost:3000/callback"],
85 "grant_types": ["authorization_code", "refresh_token"],
86 "response_types": ["code"]
87 }
88 ```
89 
90 If the registration succeeds, the authorization server will return a JSON blob with client registration information.
91 
92 <Tip>
93 **No DCR or Pre-Registration**
94 
95 In case an MCP client connects to an MCP server that doesn't use an authorization server that supports DCR and the client is not pre-registered with said authorization server, it's the responsibility of the client developer to provide an affordance for the end-user to enter client information manually.
96 </Tip>
97 </Step>
98 
99 <Step title="User Authorization">
100 The client will now need to open a browser to the `/authorize` endpoint, where the user can log in and grant the required permissions. The authorization server will then redirect back to the client with an authorization code that the client exchanges for tokens:
101 
102 ```json theme={null}
103 {
104 "access_token": "eyJhbGciOiJSUzI1NiIs...",
105 "refresh_token": "def502...",
106 "token_type": "Bearer",
107 "expires_in": 3600
108 }
109 ```
110 
111 The access token is what the client will use to authenticate requests to the MCP server. This step follows standard [OAuth 2.1 authorization code with PKCE](https://oauth.net/2/grant-types/authorization-code/) conventions.
112 </Step>
113 
114 <Step title="Making Authenticated Requests">
115 Finally, the client can make requests to your MCP server using the access token embedded in the `Authorization` header:
116 
117 ```http theme={null}
118 GET /mcp HTTP/1.1
119 Host: your-server.com
120 Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
121 ```
122 
123 The MCP server will need to validate the token and process the request if the token is valid and has the required permissions.
124 </Step>
125</Steps>
31**1. Initial Handshake**
32 
33When your MCP client first tries to connect, your server responds with a `401 Unauthorized` and tells the client where to find authorization information, captured in a [Protected Resource Metadata (PRM) document](https://datatracker.ietf.org/doc/html/rfc9728). The document is hosted by the MCP server, follows a predictable path pattern, and is provided to the client in the `resource_metadata` parameter within the `WWW-Authenticate` header.
34 
35```http theme={null}
36HTTP/1.1 401 Unauthorized
37WWW-Authenticate: Bearer realm="mcp",
38 resource_metadata="https://your-server.com/.well-known/oauth-protected-resource"
39```
40 
41This tells the client that authorization is required for the MCP server and where to get the necessary information to kickstart the authorization flow.
42 
43**2. Protected Resource Metadata Discovery**
44 
45With the URI pointer to the PRM document, the client will fetch the metadata to learn about the authorization server, supported scopes, and other resource information. The data is typically encapsulated in a JSON blob, similar to the one below.
46 
47```json theme={null}
48{
49 "resource": "https://your-server.com/mcp",
50 "authorization_servers": ["https://auth.your-server.com"],
51 "scopes_supported": ["mcp:tools", "mcp:resources"]
52}
53```
54 
55You can see a more comprehensive example in [RFC 9728 Section 3.2](https://datatracker.ietf.org/doc/html/rfc9728#name-protected-resource-metadata-r).
56 
57**3. Authorization Server Discovery**
58 
59Next, the client discovers what the authorization server can do by fetching its metadata. If the PRM document lists more than one authorization server, the client can decide which one to use.
60 
61With an authorization server selected, the client will then construct a standard metadata URI and issue a request to the [OpenID Connect (OIDC) Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) or [OAuth 2.0 Auth Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414) endpoints (depending on authorization server support)
62and retrieve another set of metadata properties that will allow it to know the endpoints it needs to complete the authorization flow.
63 
64```json theme={null}
65{
66 "issuer": "https://auth.your-server.com",
67 "authorization_endpoint": "https://auth.your-server.com/authorize",
68 "token_endpoint": "https://auth.your-server.com/token",
69 "registration_endpoint": "https://auth.your-server.com/register"
70}
71```
72 
73**4. Client Registration**
74 
75With all the metadata out of the way, the client now needs to make sure that it's registered with the authorization server. This can be done in two ways.
76 
77First, the client can be **pre-registered** with a given authorization server, in which case it can have embedded client registration information that it uses to complete the authorization flow.
78 
79Alternatively, the client can use **Dynamic Client Registration** (DCR) to dynamically register itself with the authorization server. The latter scenario requires the authorization server to support DCR. If the authorization server does support DCR, the client will send a request to the `registration_endpoint` with its information:
80 
81```json theme={null}
82{
83 "client_name": "My MCP Client",
84 "redirect_uris": ["http://localhost:3000/callback"],
85 "grant_types": ["authorization_code", "refresh_token"],
86 "response_types": ["code"]
87}
88```
89 
90If the registration succeeds, the authorization server will return a JSON blob with client registration information.
91 
92> [!TIP]
93>
94> **No DCR or Pre-Registration**
95>
96> In case an MCP client connects to an MCP server that doesn't use an authorization server that supports DCR and the client is not pre-registered with said authorization server, it's the responsibility of the client developer to provide an affordance for the end-user to enter client information manually.
97 
98**5. User Authorization**
99 
100The client will now need to open a browser to the `/authorize` endpoint, where the user can log in and grant the required permissions. The authorization server will then redirect back to the client with an authorization code that the client exchanges for tokens:
101 
102```json theme={null}
103{
104 "access_token": "eyJhbGciOiJSUzI1NiIs...",
105 "refresh_token": "def502...",
106 "token_type": "Bearer",
107 "expires_in": 3600
108}
109```
110 
111The access token is what the client will use to authenticate requests to the MCP server. This step follows standard [OAuth 2.1 authorization code with PKCE](https://oauth.net/2/grant-types/authorization-code/) conventions.
112 
113**6. Making Authenticated Requests**
114 
115Finally, the client can make requests to your MCP server using the access token embedded in the `Authorization` header:
116 
117```http theme={null}
118GET /mcp HTTP/1.1
119Host: your-server.com
120Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
121```
122 
123The MCP server will need to validate the token and process the request if the token is valid and has the required permissions.
126124 
127125## Implementation Example
128126 
from line 138
140138 
141139This command will pull the Keycloak container image locally and bootstrap the basic configuration. It will run on port `8080` and have an `admin` user with `admin` password.
142140 
143<Warning>
144 **Not for Production**
145 
146 The configuration above may be suitable for testing and experimentation; however, you should never use it in production. Refer to the [Configuring Keycloak for production](https://www.keycloak.org/server/configuration-production) guide for additional details on how to deploy the authorization server for scenarios that require reliability, security, and high availability.
147</Warning>
141> [!WARNING]
142>
143> **Not for Production**
144>
145> The configuration above may be suitable for testing and experimentation; however, you should never use it in production. Refer to the [Configuring Keycloak for production](https://www.keycloak.org/server/configuration-production) guide for additional details on how to deploy the authorization server for scenarios that require reliability, security, and high availability.
148146 
149147You will be able to access the Keycloak authorization server from your browser at `http://localhost:8080`.
150148 
151<Frame>
152 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-browser.png?fit=max&auto=format&n=sAd4SGUO-cEUqgzn&q=85&s=cba689d986e113cbe937d732ac0558b6" alt="Keycloak admin dashboard authentication dialog." width="1834" height="1450" data-path="images/tutorial-authorization/keycloak-browser.png" />
153</Frame>
149![Keycloak admin dashboard authentication dialog.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-browser.png?fit=max\&auto=format\&n=sAd4SGUO-cEUqgzn\&q=85\&s=cba689d986e113cbe937d732ac0558b6)
154150 
155151When running with the default configuration, Keycloak will already support many of the capabilities that we need for MCP servers, including Dynamic Client Registration. You can check this by looking at the OIDC configuration, available at:
156152 
from line 158
162158 
163159Go to **Client scopes** in the Keycloak dashboard and create a new `mcp:tools` scope. We will use this to access all of the tools on our MCP server.
164160 
165<Frame>
166 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-scopes.png?fit=max&auto=format&n=sAd4SGUO-cEUqgzn&q=85&s=3cd49dc2e070027609ae495751e0db58" alt="Configuring Keycloak scopes." width="1999" height="1710" data-path="images/tutorial-authorization/keycloak-scopes.png" />
167</Frame>
161![Configuring Keycloak scopes.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-scopes.png?fit=max\&auto=format\&n=sAd4SGUO-cEUqgzn\&q=85\&s=3cd49dc2e070027609ae495751e0db58)
168162 
169163After creating the scope, make sure that you assign its type to **Default** and have flipped the **Include in token scope** switch, as this will be needed for token validation.
170164 
from line 166
172166 
173167To do this, open your `mcp:tools` client scope and click on **Mappers**, followed by **Configure a new mapper**. Select **Audience**.
174168 
175<Frame>
176 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/scope-add-audience.gif?s=6ea9cf20c397f4c79c491c2e39019272" alt="Configuring an audience for a token in Keycloak." width="1080" height="921" data-path="images/tutorial-authorization/scope-add-audience.gif" />
177</Frame>
169![Configuring an audience for a token in Keycloak.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/scope-add-audience.gif?s=6ea9cf20c397f4c79c491c2e39019272)
178170 
179171For **Name**, use `audience-config`. Add a value for **Included Custom Audience**, set to `http://localhost:3000`. This will be the URI of our test server.
180172 
181<Warning>
182 **Not for Production**
183 
184 The audience configuration above is meant for testing. For production scenarios, additional set-up and configuration will be required to ensure that audiences are properly constrained for issued tokens. Specifically, the audience needs to be based on the resource parameter passed from the client, not a fixed value.
185</Warning>
173> [!WARNING]
174>
175> **Not for Production**
176>
177> The audience configuration above is meant for testing. For production scenarios, additional set-up and configuration will be required to ensure that audiences are properly constrained for issued tokens. Specifically, the audience needs to be based on the resource parameter passed from the client, not a fixed value.
186178 
187179Now, navigate to **Clients**, then **Client registration**, and then **Trusted Hosts**. Disable the **Client URIs Must Match** setting and add the hosts from which you're testing. You can get your current host IP by running the `ifconfig` command on Linux or macOS, or `ipconfig` on Windows. You can see the IP address you need to add by looking at the keycloak logs for a line that looks like `Failed to verify remote host : 192.168.215.1`. Check that the IP address is associated with your host. This may be for a bridge network depending on your docker setup.
188180 
189<Frame>
190 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-client.gif?s=b5d40b36a5f1ea1e818821bb8ea77f6b" alt="Setting up client registration details in Keycloak." width="1199" height="1027" data-path="images/tutorial-authorization/keycloak-client.gif" />
191</Frame>
192 
193<Warning>
194 **Getting the Host**
195 
196 If you are running Keycloak from a container, you will also be able to see the host IP from the Terminal in the container logs.
197</Warning>
181![Setting up client registration details in Keycloak.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-client.gif?s=b5d40b36a5f1ea1e818821bb8ea77f6b)
182 
183> [!WARNING]
184>
185> **Getting the Host**
186>
187> If you are running Keycloak from a container, you will also be able to see the host IP from the Terminal in the container logs.
198188 
199189Lastly, we need to register a new client that we can use with the **MCP server itself** to talk to Keycloak for things like [token introspection](https://oauth.net/2/token-introspection/). To do that:
200190 
from line 198
208198 
209199When you open the client details, go to **Credentials** and take note of the **Client Secret**.
210200 
211<Frame>
212 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-client-auth.gif?s=7152c41a5746994fd399024bc4659e40" alt="Creating a new client in Keycloak." width="1200" height="1023" data-path="images/tutorial-authorization/keycloak-client-auth.gif" />
213</Frame>
214 
215<Warning>
216 **Handling Secrets**
217 
218 Never embed client credentials directly in your code. We recommend using environment variables or specialized solutions for secret storage.
219</Warning>
201![Creating a new client in Keycloak.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-client-auth.gif?s=7152c41a5746994fd399024bc4659e40)
202 
203> [!WARNING]
204>
205> **Handling Secrets**
206>
207> Never embed client credentials directly in your code. We recommend using environment variables or specialized solutions for secret storage.
220208 
221209With Keycloak configured, every time the authorization flow is triggered, your MCP server will receive a token like this:
222210 
from line 234
246234}.[Signature]
247235```
248236 
249<Warning>
250 **Embedded Audience**
251 
252 Notice the `aud` claim embedded in the token - it's currently set to be the URI of the test MCP server and it's inferred from the scope that we've previously configured. This will be important in our implementation to validate.
253</Warning>
237> [!WARNING]
238>
239> **Embedded Audience**
240>
241> Notice the `aud` claim embedded in the token - it's currently set to be the URI of the test MCP server and it's inferred from the scope that we've previously configured. This will be important in our implementation to validate.
254242 
255243### MCP Server Setup
256244 
from line 246
258246 
259247For our testing purposes, we will create an extremely simple MCP server that exposes two tools - one for addition and another for multiplication. The server will require authorization to access these.
260248 
261<Tabs>
262 <Tab title="TypeScript">
263 You can see the complete TypeScript project in the [sample repository](https://github.com/localden/min-ts-mcp-auth).
264 
265 Prior to running the code below, ensure that you have a `.env` file with the following content:
266 
267 ```env theme={null}
268 # Server host/port
269 HOST=localhost
270 PORT=3000
271 
272 # Auth server location
273 AUTH_HOST=localhost
274 AUTH_PORT=8080
275 AUTH_REALM=master
276 
277 # Keycloak OAuth client credentials
278 OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
279 OAUTH_CLIENT_SECRET=<YOUR_SERVER_CLIENT_SECRET>
280 ```
281 
282 `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier.
283 
284 In addition to implementing the MCP authorization specification, the server below also does token introspection via Keycloak to make sure that the token it receives from the client is valid. It also implements basic logging to allow you to easily diagnose any issues.
285 
286 ```typescript theme={null}
287 import "dotenv/config";
288 import express from "express";
289 import { randomUUID } from "node:crypto";
290 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
291 import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
292 import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
293 import { z } from "zod";
294 import cors from "cors";
295 import {
296 mcpAuthMetadataRouter,
297 getOAuthProtectedResourceMetadataUrl,
298 } from "@modelcontextprotocol/sdk/server/auth/router.js";
299 import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
300 import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
301 import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
302 const CONFIG = {
303 host: process.env.HOST || "localhost",
304 port: Number(process.env.PORT) || 3000,
305 auth: {
306 host: process.env.AUTH_HOST || process.env.HOST || "localhost",
307 port: Number(process.env.AUTH_PORT) || 8080,
308 realm: process.env.AUTH_REALM || "master",
309 clientId: process.env.OAUTH_CLIENT_ID || "mcp-server",
310 clientSecret: process.env.OAUTH_CLIENT_SECRET || "",
311 },
312 };
313 
314 function createOAuthUrls() {
315 const authBaseUrl = new URL(
316 `http://${CONFIG.auth.host}:${CONFIG.auth.port}/realms/${CONFIG.auth.realm}/`,
317 );
318 return {
319 issuer: authBaseUrl.toString(),
320 introspection_endpoint: new URL(
321 "protocol/openid-connect/token/introspect",
322 authBaseUrl,
323 ).toString(),
324 authorization_endpoint: new URL(
325 "protocol/openid-connect/auth",
326 authBaseUrl,
327 ).toString(),
328 token_endpoint: new URL(
329 "protocol/openid-connect/token",
330 authBaseUrl,
331 ).toString(),
332 };
333 }
334 
335 function createRequestLogger() {
336 return (req: any, res: any, next: any) => {
337 const start = Date.now();
338 res.on("finish", () => {
339 const ms = Date.now() - start;
340 console.log(
341 `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
342 );
343 });
344 next();
345 };
346 }
347 
348 const app = express();
349 
350 app.use(
351 express.json({
352 verify: (req: any, _res, buf) => {
353 req.rawBody = buf?.toString() ?? "";
354 },
355 }),
356 );
357 
358 app.use(
359 cors({
360 origin: "*",
361 exposedHeaders: ["Mcp-Session-Id"],
362 }),
363 );
364 
365 app.use(createRequestLogger());
366 
367 const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
368 const oauthUrls = createOAuthUrls();
369 
370 const oauthMetadata: OAuthMetadata = {
371 ...oauthUrls,
372 response_types_supported: ["code"],
373 };
374 
375 const tokenVerifier = {
376 verifyAccessToken: async (token: string) => {
377 const endpoint = oauthMetadata.introspection_endpoint;
378 
379 if (!endpoint) {
380 console.error("[auth] no introspection endpoint in metadata");
381 throw new Error("No token verification endpoint available in metadata");
382 }
383 
384 const params = new URLSearchParams({
385 token: token,
386 client_id: CONFIG.auth.clientId,
387 });
388 
389 if (CONFIG.auth.clientSecret) {
390 params.set("client_secret", CONFIG.auth.clientSecret);
391 }
392 
393 let response: Response;
394 try {
395 response = await fetch(endpoint, {
396 method: "POST",
397 headers: {
398 "Content-Type": "application/x-www-form-urlencoded",
399 },
400 body: params.toString(),
401 });
402 } catch (e) {
403 console.error("[auth] introspection fetch threw", e);
404 throw e;
405 }
406 
407 if (!response.ok) {
408 const txt = await response.text();
409 console.error("[auth] introspection non-OK", { status: response.status });
410 
411 try {
412 const obj = JSON.parse(txt);
413 console.log(JSON.stringify(obj, null, 2));
414 } catch {
415 console.error(txt);
416 }
417 throw new Error(`Invalid or expired token: ${txt}`);
418 }
419 
420 let data: any;
421 try {
422 data = await response.json();
423 } catch (e) {
424 const txt = await response.text();
425 console.error("[auth] failed to parse introspection JSON", {
426 error: String(e),
427 body: txt,
428 });
429 throw e;
430 }
431 
432 if (data.active === false) {
433 throw new Error("Inactive token");
434 }
435 
436 if (!data.aud) {
437 throw new Error("Resource indicator (aud) missing");
438 }
439 
440 const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
441 const allowed = audiences.some((a) => {
442 try {
443 return checkResourceAllowed({
444 requestedResource: a,
445 configuredResource: mcpServerUrl,
446 });
447 } catch {
448 // Keycloak tokens include non-URL audiences (e.g. "account", "test-client").
449 // Those are never our resource, so treat them as "no match" instead of crashing.
450 return false;
451 }
452 });
453 if (!allowed) {
454 throw new Error(
455 `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
456 );
457 }
458 
459 return {
460 token,
461 clientId: data.client_id,
462 scopes: data.scope ? data.scope.split(" ") : [],
463 expiresAt: data.exp,
464 };
465 },
466 };
467 app.use(
468 mcpAuthMetadataRouter({
469 oauthMetadata,
470 resourceServerUrl: mcpServerUrl,
471 scopesSupported: ["mcp:tools"],
472 resourceName: "MCP Demo Server",
473 }),
474 );
475 
476 const authMiddleware = requireBearerAuth({
477 verifier: tokenVerifier,
478 requiredScopes: [],
479 resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
480 });
481 
482 const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
483 
484 function createMcpServer() {
485 const server = new McpServer({
486 name: "example-server",
487 version: "1.0.0",
488 });
489 
490 server.registerTool(
491 "add",
492 {
493 title: "Addition Tool",
494 description: "Add two numbers together",
495 inputSchema: {
496 a: z.number().describe("First number to add"),
497 b: z.number().describe("Second number to add"),
498 },
499 },
500 async ({ a, b }) => ({
501 content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
502 }),
503 );
504 
505 server.registerTool(
506 "multiply",
507 {
508 title: "Multiplication Tool",
509 description: "Multiply two numbers together",
510 inputSchema: {
511 x: z.number().describe("First number to multiply"),
512 y: z.number().describe("Second number to multiply"),
513 },
514 },
515 async ({ x, y }) => ({
516 content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
517 }),
518 );
519 
520 return server;
521 }
522 
523 const mcpPostHandler = async (req: express.Request, res: express.Response) => {
524 const sessionId = req.headers["mcp-session-id"] as string | undefined;
525 let transport: StreamableHTTPServerTransport;
526 
527 if (sessionId && transports[sessionId]) {
528 transport = transports[sessionId];
529 } else if (!sessionId && isInitializeRequest(req.body)) {
530 transport = new StreamableHTTPServerTransport({
531 sessionIdGenerator: () => randomUUID(),
532 onsessioninitialized: (sessionId) => {
533 transports[sessionId] = transport;
534 },
535 });
536 
537 transport.onclose = () => {
538 if (transport.sessionId) {
539 delete transports[transport.sessionId];
540 }
541 };
542 
543 const server = createMcpServer();
544 await server.connect(transport);
545 } else {
546 res.status(400).json({
547 jsonrpc: "2.0",
548 error: {
549 code: -32000,
550 message: "Bad Request: No valid session ID provided",
551 },
552 id: null,
553 });
554 return;
555 }
556 
557 await transport.handleRequest(req, res, req.body);
558 };
559 
560 const handleSessionRequest = async (
561 req: express.Request,
562 res: express.Response,
563 ) => {
564 const sessionId = req.headers["mcp-session-id"] as string | undefined;
565 if (!sessionId || !transports[sessionId]) {
566 res.status(400).send("Invalid or missing session ID");
567 return;
568 }
569 
570 const transport = transports[sessionId];
571 await transport.handleRequest(req, res);
572 };
573 
574 app.post("/", authMiddleware, mcpPostHandler);
575 app.get("/", authMiddleware, handleSessionRequest);
576 app.delete("/", authMiddleware, handleSessionRequest);
577 
578 app.listen(CONFIG.port, CONFIG.host, () => {
579 console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
580 console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
249**TypeScript**
250 
251You can see the complete TypeScript project in the [sample repository](https://github.com/localden/min-ts-mcp-auth).
252 
253Prior to running the code below, ensure that you have a `.env` file with the following content:
254 
255```env theme={null}
256# Server host/port
257HOST=localhost
258PORT=3000
259 
260# Auth server location
261AUTH_HOST=localhost
262AUTH_PORT=8080
263AUTH_REALM=master
264 
265# Keycloak OAuth client credentials
266OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
267OAUTH_CLIENT_SECRET=<YOUR_SERVER_CLIENT_SECRET>
268```
269 
270`OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier.
271 
272In addition to implementing the MCP authorization specification, the server below also does token introspection via Keycloak to make sure that the token it receives from the client is valid. It also implements basic logging to allow you to easily diagnose any issues.
273 
274```typescript theme={null}
275import "dotenv/config";
276import express from "express";
277import { randomUUID } from "node:crypto";
278import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
279import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
280import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
281import { z } from "zod";
282import cors from "cors";
283import {
284 mcpAuthMetadataRouter,
285 getOAuthProtectedResourceMetadataUrl,
286} from "@modelcontextprotocol/sdk/server/auth/router.js";
287import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
288import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
289import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
290const CONFIG = {
291 host: process.env.HOST || "localhost",
292 port: Number(process.env.PORT) || 3000,
293 auth: {
294 host: process.env.AUTH_HOST || process.env.HOST || "localhost",
295 port: Number(process.env.AUTH_PORT) || 8080,
296 realm: process.env.AUTH_REALM || "master",
297 clientId: process.env.OAUTH_CLIENT_ID || "mcp-server",
298 clientSecret: process.env.OAUTH_CLIENT_SECRET || "",
299 },
300};
301 
302function createOAuthUrls() {
303 const authBaseUrl = new URL(
304 `http://${CONFIG.auth.host}:${CONFIG.auth.port}/realms/${CONFIG.auth.realm}/`,
305 );
306 return {
307 issuer: authBaseUrl.toString(),
308 introspection_endpoint: new URL(
309 "protocol/openid-connect/token/introspect",
310 authBaseUrl,
311 ).toString(),
312 authorization_endpoint: new URL(
313 "protocol/openid-connect/auth",
314 authBaseUrl,
315 ).toString(),
316 token_endpoint: new URL(
317 "protocol/openid-connect/token",
318 authBaseUrl,
319 ).toString(),
320 };
321}
322 
323function createRequestLogger() {
324 return (req: any, res: any, next: any) => {
325 const start = Date.now();
326 res.on("finish", () => {
327 const ms = Date.now() - start;
581328 console.log(
582 `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
329 `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
583330 );
584331 });
585 ```
586 
587 When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
588 
589 For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
590 </Tab>
591 
592 <Tab title="Python">
593 You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
594 
595 To simplify our authorization interaction, in Python scenarios we rely on the `MCPServer` class from the [Python SDK](https://py.sdk.modelcontextprotocol.io/v2/run/authorization/). It publishes the Protected Resource Metadata document, answers unauthenticated requests with a `401` whose `WWW-Authenticate` header points back at that document, and hands every bearer token to a verifier that we supply. Many of the conventions around authorization, like the endpoints and token validation logic, are consistent across languages, but some offer simpler ways of integrating them in production scenarios.
596 
597 Prior to writing the actual server, we need to set up our configuration in `config.py` - the contents are entirely based on your local server setup:
598 
599 ```python theme={null}
600 """Configuration settings for the MCP auth server."""
601 
602 import os
603 
604 
605 class Config:
606 """Configuration class that loads from environment variables with sensible defaults."""
607 
608 # Server settings
609 HOST: str = os.getenv("HOST", "localhost")
610 PORT: int = int(os.getenv("PORT", "3000"))
611 
612 # Auth server settings
613 AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
614 AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
615 AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
616 
617 # OAuth client settings
618 OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "test-client")
619 OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "")
620 
621 # Scope required on every token
622 MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
623 
624 @property
625 def server_url(self) -> str:
626 """Build the server URL."""
627 return f"http://{self.HOST}:{self.PORT}"
628 
629 @property
630 def auth_base_url(self) -> str:
631 """Build the auth server base URL."""
632 return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
633 
634 
635 # Global configuration instance
636 config = Config()
637 ```
638 
639 `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier. Set them in your environment before starting the server.
640 
641 The server implementation is as follows:
642 
643 ```python theme={null}
644 import datetime
645 import logging
646 from typing import Any
647 from urllib.parse import urljoin
648 
649 from pydantic import AnyHttpUrl
650 
651 from mcp.server import MCPServer
652 from mcp.server.auth.settings import AuthSettings
653 
654 from .config import config
655 from .token_verifier import IntrospectionTokenVerifier
656 
657 logger = logging.getLogger(__name__)
658 
659 
660 def create_oauth_urls() -> dict[str, str]:
661 """Create OAuth URLs based on configuration (Keycloak-style)."""
662 auth_base_url = config.auth_base_url
663 
332 next();
333 };
334}
335 
336const app = express();
337 
338app.use(
339 express.json({
340 verify: (req: any, _res, buf) => {
341 req.rawBody = buf?.toString() ?? "";
342 },
343 }),
344);
345 
346app.use(
347 cors({
348 origin: "*",
349 exposedHeaders: ["Mcp-Session-Id"],
350 }),
351);
352 
353app.use(createRequestLogger());
354 
355const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
356const oauthUrls = createOAuthUrls();
357 
358const oauthMetadata: OAuthMetadata = {
359 ...oauthUrls,
360 response_types_supported: ["code"],
361};
362 
363const tokenVerifier = {
364 verifyAccessToken: async (token: string) => {
365 const endpoint = oauthMetadata.introspection_endpoint;
366 
367 if (!endpoint) {
368 console.error("[auth] no introspection endpoint in metadata");
369 throw new Error("No token verification endpoint available in metadata");
370 }
371 
372 const params = new URLSearchParams({
373 token: token,
374 client_id: CONFIG.auth.clientId,
375 });
376 
377 if (CONFIG.auth.clientSecret) {
378 params.set("client_secret", CONFIG.auth.clientSecret);
379 }
380 
381 let response: Response;
382 try {
383 response = await fetch(endpoint, {
384 method: "POST",
385 headers: {
386 "Content-Type": "application/x-www-form-urlencoded",
387 },
388 body: params.toString(),
389 });
390 } catch (e) {
391 console.error("[auth] introspection fetch threw", e);
392 throw e;
393 }
394 
395 if (!response.ok) {
396 const txt = await response.text();
397 console.error("[auth] introspection non-OK", { status: response.status });
398 
399 try {
400 const obj = JSON.parse(txt);
401 console.log(JSON.stringify(obj, null, 2));
402 } catch {
403 console.error(txt);
404 }
405 throw new Error(`Invalid or expired token: ${txt}`);
406 }
407 
408 let data: any;
409 try {
410 data = await response.json();
411 } catch (e) {
412 const txt = await response.text();
413 console.error("[auth] failed to parse introspection JSON", {
414 error: String(e),
415 body: txt,
416 });
417 throw e;
418 }
419 
420 if (data.active === false) {
421 throw new Error("Inactive token");
422 }
423 
424 if (!data.aud) {
425 throw new Error("Resource indicator (aud) missing");
426 }
427 
428 const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
429 const allowed = audiences.some((a) => {
430 try {
431 return checkResourceAllowed({
432 requestedResource: a,
433 configuredResource: mcpServerUrl,
434 });
435 } catch {
436 // Keycloak tokens include non-URL audiences (e.g. "account", "test-client").
437 // Those are never our resource, so treat them as "no match" instead of crashing.
438 return false;
439 }
440 });
441 if (!allowed) {
442 throw new Error(
443 `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
444 );
445 }
446 
447 return {
448 token,
449 clientId: data.client_id,
450 scopes: data.scope ? data.scope.split(" ") : [],
451 expiresAt: data.exp,
452 };
453 },
454};
455app.use(
456 mcpAuthMetadataRouter({
457 oauthMetadata,
458 resourceServerUrl: mcpServerUrl,
459 scopesSupported: ["mcp:tools"],
460 resourceName: "MCP Demo Server",
461 }),
462);
463 
464const authMiddleware = requireBearerAuth({
465 verifier: tokenVerifier,
466 requiredScopes: [],
467 resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
468});
469 
470const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
471 
472function createMcpServer() {
473 const server = new McpServer({
474 name: "example-server",
475 version: "1.0.0",
476 });
477 
478 server.registerTool(
479 "add",
480 {
481 title: "Addition Tool",
482 description: "Add two numbers together",
483 inputSchema: {
484 a: z.number().describe("First number to add"),
485 b: z.number().describe("Second number to add"),
486 },
487 },
488 async ({ a, b }) => ({
489 content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
490 }),
491 );
492 
493 server.registerTool(
494 "multiply",
495 {
496 title: "Multiplication Tool",
497 description: "Multiply two numbers together",
498 inputSchema: {
499 x: z.number().describe("First number to multiply"),
500 y: z.number().describe("Second number to multiply"),
501 },
502 },
503 async ({ x, y }) => ({
504 content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
505 }),
506 );
507 
508 return server;
509}
510 
511const mcpPostHandler = async (req: express.Request, res: express.Response) => {
512 const sessionId = req.headers["mcp-session-id"] as string | undefined;
513 let transport: StreamableHTTPServerTransport;
514 
515 if (sessionId && transports[sessionId]) {
516 transport = transports[sessionId];
517 } else if (!sessionId && isInitializeRequest(req.body)) {
518 transport = new StreamableHTTPServerTransport({
519 sessionIdGenerator: () => randomUUID(),
520 onsessioninitialized: (sessionId) => {
521 transports[sessionId] = transport;
522 },
523 });
524 
525 transport.onclose = () => {
526 if (transport.sessionId) {
527 delete transports[transport.sessionId];
528 }
529 };
530 
531 const server = createMcpServer();
532 await server.connect(transport);
533 } else {
534 res.status(400).json({
535 jsonrpc: "2.0",
536 error: {
537 code: -32000,
538 message: "Bad Request: No valid session ID provided",
539 },
540 id: null,
541 });
542 return;
543 }
544 
545 await transport.handleRequest(req, res, req.body);
546};
547 
548const handleSessionRequest = async (
549 req: express.Request,
550 res: express.Response,
551) => {
552 const sessionId = req.headers["mcp-session-id"] as string | undefined;
553 if (!sessionId || !transports[sessionId]) {
554 res.status(400).send("Invalid or missing session ID");
555 return;
556 }
557 
558 const transport = transports[sessionId];
559 await transport.handleRequest(req, res);
560};
561 
562app.post("/", authMiddleware, mcpPostHandler);
563app.get("/", authMiddleware, handleSessionRequest);
564app.delete("/", authMiddleware, handleSessionRequest);
565 
566app.listen(CONFIG.port, CONFIG.host, () => {
567 console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
568 console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
569 console.log(
570 `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
571 );
572});
573```
574 
575When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
576 
577For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
578 
579**Python**
580 
581You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
582 
583To simplify our authorization interaction, in Python scenarios we rely on the `MCPServer` class from the [Python SDK](https://py.sdk.modelcontextprotocol.io/v2/run/authorization/). It publishes the Protected Resource Metadata document, answers unauthenticated requests with a `401` whose `WWW-Authenticate` header points back at that document, and hands every bearer token to a verifier that we supply. Many of the conventions around authorization, like the endpoints and token validation logic, are consistent across languages, but some offer simpler ways of integrating them in production scenarios.
584 
585Prior to writing the actual server, we need to set up our configuration in `config.py` - the contents are entirely based on your local server setup:
586 
587```python theme={null}
588"""Configuration settings for the MCP auth server."""
589 
590import os
591 
592 
593class Config:
594 """Configuration class that loads from environment variables with sensible defaults."""
595 
596 # Server settings
597 HOST: str = os.getenv("HOST", "localhost")
598 PORT: int = int(os.getenv("PORT", "3000"))
599 
600 # Auth server settings
601 AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
602 AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
603 AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
604 
605 # OAuth client settings
606 OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "test-client")
607 OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "")
608 
609 # Scope required on every token
610 MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
611 
612 @property
613 def server_url(self) -> str:
614 """Build the server URL."""
615 return f"http://{self.HOST}:{self.PORT}"
616 
617 @property
618 def auth_base_url(self) -> str:
619 """Build the auth server base URL."""
620 return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
621 
622 
623# Global configuration instance
624config = Config()
625```
626 
627`OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier. Set them in your environment before starting the server.
628 
629The server implementation is as follows:
630 
631```python theme={null}
632import datetime
633import logging
634from typing import Any
635from urllib.parse import urljoin
636 
637from pydantic import AnyHttpUrl
638 
639from mcp.server import MCPServer
640from mcp.server.auth.settings import AuthSettings
641 
642from .config import config
643from .token_verifier import IntrospectionTokenVerifier
644 
645logger = logging.getLogger(__name__)
646 
647 
648def create_oauth_urls() -> dict[str, str]:
649 """Create OAuth URLs based on configuration (Keycloak-style)."""
650 auth_base_url = config.auth_base_url
651 
652 return {
653 "issuer": auth_base_url,
654 "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
655 "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
656 "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
657 }
658 
659 
660def create_server() -> MCPServer:
661 """Create and configure the MCP server."""
662 
663 oauth_urls = create_oauth_urls()
664 
665 token_verifier = IntrospectionTokenVerifier(
666 introspection_endpoint=oauth_urls["introspection_endpoint"],
667 server_url=config.server_url,
668 client_id=config.OAUTH_CLIENT_ID,
669 client_secret=config.OAUTH_CLIENT_SECRET,
670 )
671 
672 app = MCPServer(
673 name="MCP Resource Server",
674 instructions="Resource Server that validates tokens via Authorization Server introspection",
675 debug=True,
676 token_verifier=token_verifier,
677 auth=AuthSettings(
678 issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
679 required_scopes=[config.MCP_SCOPE],
680 resource_server_url=AnyHttpUrl(config.server_url),
681 ),
682 )
683 
684 @app.tool()
685 async def add_numbers(a: float, b: float) -> dict[str, Any]:
686 """
687 Add two numbers together.
688 This tool demonstrates basic arithmetic operations with OAuth authentication.
689 
690 Args:
691 a: The first number to add
692 b: The second number to add
693 """
694 result = a + b
664695 return {
665 "issuer": auth_base_url,
666 "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
667 "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
668 "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
696 "operation": "addition",
697 "operand_a": a,
698 "operand_b": b,
699 "result": result,
700 "timestamp": datetime.datetime.now().isoformat(),
669701 }
670702 
671 
672 def create_server() -> MCPServer:
673 """Create and configure the MCP server."""
674 
675 oauth_urls = create_oauth_urls()
676 
677 token_verifier = IntrospectionTokenVerifier(
678 introspection_endpoint=oauth_urls["introspection_endpoint"],
679 server_url=config.server_url,
680 client_id=config.OAUTH_CLIENT_ID,
681 client_secret=config.OAUTH_CLIENT_SECRET,
703 @app.tool()
704 async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
705 """
706 Multiply two numbers together.
707 This tool demonstrates basic arithmetic operations with OAuth authentication.
708 
709 Args:
710 x: The first number to multiply
711 y: The second number to multiply
712 """
713 result = x * y
714 return {
715 "operation": "multiplication",
716 "operand_x": x,
717 "operand_y": y,
718 "result": result,
719 "timestamp": datetime.datetime.now().isoformat(),
720 }
721 
722 return app
723 
724 
725def main() -> int:
726 """
727 Run the MCP Resource Server.
728 
729 This server:
730 - Provides RFC 9728 Protected Resource Metadata
731 - Validates tokens via Authorization Server introspection
732 - Serves MCP tools requiring authentication
733 
734 Configuration is loaded from config.py and environment variables.
735 """
736 logging.basicConfig(level=logging.INFO)
737 
738 oauth_urls = create_oauth_urls()
739 
740 try:
741 mcp_server = create_server()
742 
743 logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
744 logger.info("Authorization Server: %s", oauth_urls["issuer"])
745 
746 mcp_server.run(
747 transport="streamable-http",
748 host=config.HOST,
749 port=config.PORT,
750 streamable_http_path="/",
682751 )
683 
684 app = MCPServer(
685 name="MCP Resource Server",
686 instructions="Resource Server that validates tokens via Authorization Server introspection",
687 debug=True,
688 token_verifier=token_verifier,
689 auth=AuthSettings(
690 issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
691 required_scopes=[config.MCP_SCOPE],
692 resource_server_url=AnyHttpUrl(config.server_url),
693 ),
694 )
695 
696 @app.tool()
697 async def add_numbers(a: float, b: float) -> dict[str, Any]:
698 """
699 Add two numbers together.
700 This tool demonstrates basic arithmetic operations with OAuth authentication.
701 
702 Args:
703 a: The first number to add
704 b: The second number to add
705 """
706 result = a + b
707 return {
708 "operation": "addition",
709 "operand_a": a,
710 "operand_b": b,
711 "result": result,
712 "timestamp": datetime.datetime.now().isoformat(),
752 return 0
753 
754 except Exception:
755 logger.exception("Server error")
756 return 1
757 
758 
759if __name__ == "__main__":
760 exit(main())
761```
762 
763Lastly, the token verification logic is delegated entirely to `token_verifier.py`, ensuring that we can use the Keycloak introspection endpoint to verify the validity of any credential artifacts.
764 
765```python theme={null}
766"""Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
767 
768import logging
769from typing import Any
770 
771import httpx2
772 
773from mcp.server.auth.provider import AccessToken, TokenVerifier
774from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
775 
776logger = logging.getLogger(__name__)
777 
778 
779class IntrospectionTokenVerifier(TokenVerifier):
780 """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662)."""
781 
782 def __init__(
783 self,
784 introspection_endpoint: str,
785 server_url: str,
786 client_id: str,
787 client_secret: str,
788 ):
789 self.introspection_endpoint = introspection_endpoint
790 self.server_url = server_url
791 self.client_id = client_id
792 self.client_secret = client_secret
793 self.resource_url = resource_url_from_server_url(server_url)
794 
795 async def verify_token(self, token: str) -> AccessToken | None:
796 """Verify token via introspection endpoint."""
797 if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
798 return None
799 
800 timeout = httpx2.Timeout(10.0, connect=5.0)
801 limits = httpx2.Limits(max_connections=10, max_keepalive_connections=5)
802 
803 async with httpx2.AsyncClient(
804 timeout=timeout,
805 limits=limits,
806 verify=True,
807 ) as client:
808 try:
809 form_data = {
810 "token": token,
811 "client_id": self.client_id,
812 }
813 # Only send client_secret when one is configured
814 # Public clients authenticate with client_id alone.
815 if self.client_secret:
816 form_data["client_secret"] = self.client_secret
817 headers = {"Content-Type": "application/x-www-form-urlencoded"}
818 
819 response = await client.post(
820 self.introspection_endpoint,
821 data=form_data,
822 headers=headers,
823 )
824 
825 if response.status_code != 200:
826 return None
827 
828 data = response.json()
829 if not data.get("active", False):
830 return None
831 
832 if not self._validate_resource(data):
833 return None
834 
835 return AccessToken(
836 token=token,
837 client_id=data.get("client_id", "unknown"),
838 scopes=data.get("scope", "").split() if data.get("scope") else [],
839 expires_at=data.get("exp"),
840 # AccessToken.resource is `str | None`. Keycloak returns `aud`
841 # as a *list* here (e.g. ["test-client", "http://localhost:3000",
842 # "account"]); passing that list straight in raises a pydantic
843 # ValidationError that the broad `except` below turns into a
844 # silent 401. We already confirmed this server's resource is a
845 # valid audience in `_validate_resource`, so record that.
846 resource=self.resource_url,
847 subject=data.get("sub"), # RFC 7662 subject (resource owner)
848 claims=data,
849 )
850 
851 except Exception:
852 logger.exception("Token introspection failed")
853 return None
854 
855 def _validate_resource(self, token_data: dict[str, Any]) -> bool:
856 """Validate token was issued for this resource server.
857 
858 Rules:
859 - Reject if 'aud' missing.
860 - Accept if any audience entry matches the derived resource URL.
861 - Supports string or list forms per JWT spec.
862 """
863 if not self.server_url or not self.resource_url:
864 return False
865 
866 aud: list[str] | str | None = token_data.get("aud")
867 if isinstance(aud, list):
868 return any(self._is_valid_resource(a) for a in aud)
869 if isinstance(aud, str):
870 return self._is_valid_resource(aud)
871 return False
872 
873 def _is_valid_resource(self, resource: str) -> bool:
874 """Check if the given resource matches our server."""
875 return check_resource_allowed(requested_resource=self.resource_url, configured_resource=resource)
876```
877 
878For more details, see below or the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
879 
880**Python MCP Server**
881 
882In the server's root have a `pyproject.toml` file and a `mcp_server` folder. Put all the Python files in the `mcp_server` folder, and fill the `pyproject.toml` file like:
883 
884```toml theme={null}
885[project]
886name = "mcp-simple-auth"
887version = "0.1.0"
888description = "A simple MCP server demonstrating OAuth authentication"
889requires-python = ">=3.10"
890authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
891license = { text = "MIT" }
892dependencies = [
893 "httpx2>=2.5.0",
894 "mcp>=2.0.0rc1",
895 "pydantic>=2.0",
896]
897 
898[project.scripts]
899mcp-simple-auth-rs = "mcp_server.server:main"
900 
901[build-system]
902requires = ["hatchling"]
903build-backend = "hatchling.build"
904 
905[tool.hatch.build.targets.wheel]
906packages = ["mcp_server"]
907 
908[dependency-groups]
909dev = ["pyright>=1.1.391", "pytest>=8.3.4", "ruff>=0.8.5"]
910```
911 
912Then run the commands below to start the server.
913 
914```bash theme={null}
915uv sync
916uv run mcp-simple-auth-rs
917```
918 
919**C#**
920 
921You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
922 
923To set up authorization in your MCP server using the MCP C# SDK, you can lean on the standard ASP.NET Core builder pattern. Instead of using the introspection endpoint provided by Keycloak, we will use built-in ASP.NET Core capabilities for token validation.
924 
925In the root of your server folder, create two files, `Program.cs` and `ProtectedMcpServer.csproj`, and a `Tools` folder. Fill `Program.cs` with:
926 
927```csharp theme={null}
928using Microsoft.AspNetCore.Authentication.JwtBearer;
929using Microsoft.IdentityModel.Tokens;
930using ModelContextProtocol.AspNetCore.Authentication;
931using ProtectedMcpServer.Tools;
932using System.Security.Claims;
933 
934var builder = WebApplication.CreateBuilder(args);
935 
936var serverUrl = "http://localhost:3000/";
937var authorizationServerUrl = "http://localhost:8080/realms/master/";
938 
939builder.Services.AddAuthentication(options =>
940{
941 options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
942 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
943})
944.AddJwtBearer(options =>
945{
946 options.Authority = authorizationServerUrl;
947 var normalizedServerAudience = serverUrl.TrimEnd('/');
948 options.TokenValidationParameters = new TokenValidationParameters
949 {
950 ValidIssuer = authorizationServerUrl,
951 ValidAudiences = new[] { normalizedServerAudience, serverUrl },
952 AudienceValidator = (audiences, securityToken, validationParameters) =>
953 {
954 if (audiences == null) return false;
955 foreach (var aud in audiences)
956 {
957 if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
958 {
959 return true;
960 }
713961 }
714 
715 @app.tool()
716 async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
717 """
718 Multiply two numbers together.
719 This tool demonstrates basic arithmetic operations with OAuth authentication.
720 
721 Args:
722 x: The first number to multiply
723 y: The second number to multiply
724 """
725 result = x * y
726 return {
727 "operation": "multiplication",
728 "operand_x": x,
729 "operand_y": y,
730 "result": result,
731 "timestamp": datetime.datetime.now().isoformat(),
732 }
733 
734 return app
735 
736 
737 def main() -> int:
738 """
739 Run the MCP Resource Server.
740 
741 This server:
742 - Provides RFC 9728 Protected Resource Metadata
743 - Validates tokens via Authorization Server introspection
744 - Serves MCP tools requiring authentication
745 
746 Configuration is loaded from config.py and environment variables.
747 """
748 logging.basicConfig(level=logging.INFO)
749 
750 oauth_urls = create_oauth_urls()
751 
752 try:
753 mcp_server = create_server()
754 
755 logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
756 logger.info("Authorization Server: %s", oauth_urls["issuer"])
757 
758 mcp_server.run(
759 transport="streamable-http",
760 host=config.HOST,
761 port=config.PORT,
762 streamable_http_path="/",
763 )
764 return 0
765 
766 except Exception:
767 logger.exception("Server error")
768 return 1
769 
770 
771 if __name__ == "__main__":
772 exit(main())
773 ```
774 
775 Lastly, the token verification logic is delegated entirely to `token_verifier.py`, ensuring that we can use the Keycloak introspection endpoint to verify the validity of any credential artifacts.
776 
777 ```python theme={null}
778 """Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
779 
780 import logging
781 from typing import Any
782 
783 import httpx2
784 
785 from mcp.server.auth.provider import AccessToken, TokenVerifier
786 from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
787 
788 logger = logging.getLogger(__name__)
789 
790 
791 class IntrospectionTokenVerifier(TokenVerifier):
792 """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662)."""
793 
794 def __init__(
795 self,
796 introspection_endpoint: str,
797 server_url: str,
798 client_id: str,
799 client_secret: str,
800 ):
801 self.introspection_endpoint = introspection_endpoint
802 self.server_url = server_url
803 self.client_id = client_id
804 self.client_secret = client_secret
805 self.resource_url = resource_url_from_server_url(server_url)
806 
807 async def verify_token(self, token: str) -> AccessToken | None:
808 """Verify token via introspection endpoint."""
809 if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
810 return None
811 
812 timeout = httpx2.Timeout(10.0, connect=5.0)
813 limits = httpx2.Limits(max_connections=10, max_keepalive_connections=5)
814 
815 async with httpx2.AsyncClient(
816 timeout=timeout,
817 limits=limits,
818 verify=True,
819 ) as client:
820 try:
821 form_data = {
822 "token": token,
823 "client_id": self.client_id,
824 }
825 # Only send client_secret when one is configured
826 # Public clients authenticate with client_id alone.
827 if self.client_secret:
828 form_data["client_secret"] = self.client_secret
829 headers = {"Content-Type": "application/x-www-form-urlencoded"}
830 
831 response = await client.post(
832 self.introspection_endpoint,
833 data=form_data,
834 headers=headers,
835 )
836 
837 if response.status_code != 200:
838 return None
839 
840 data = response.json()
841 if not data.get("active", False):
842 return None
843 
844 if not self._validate_resource(data):
845 return None
846 
847 return AccessToken(
848 token=token,
849 client_id=data.get("client_id", "unknown"),
850 scopes=data.get("scope", "").split() if data.get("scope") else [],
851 expires_at=data.get("exp"),
852 # AccessToken.resource is `str | None`. Keycloak returns `aud`
853 # as a *list* here (e.g. ["test-client", "http://localhost:3000",
854 # "account"]); passing that list straight in raises a pydantic
855 # ValidationError that the broad `except` below turns into a
856 # silent 401. We already confirmed this server's resource is a
857 # valid audience in `_validate_resource`, so record that.
858 resource=self.resource_url,
859 subject=data.get("sub"), # RFC 7662 subject (resource owner)
860 claims=data,
861 )
862 
863 except Exception:
864 logger.exception("Token introspection failed")
865 return None
866 
867 def _validate_resource(self, token_data: dict[str, Any]) -> bool:
868 """Validate token was issued for this resource server.
869 
870 Rules:
871 - Reject if 'aud' missing.
872 - Accept if any audience entry matches the derived resource URL.
873 - Supports string or list forms per JWT spec.
874 """
875 if not self.server_url or not self.resource_url:
876 return False
877 
878 aud: list[str] | str | None = token_data.get("aud")
879 if isinstance(aud, list):
880 return any(self._is_valid_resource(a) for a in aud)
881 if isinstance(aud, str):
882 return self._is_valid_resource(aud)
883 return False
884 
885 def _is_valid_resource(self, resource: str) -> bool:
886 """Check if the given resource matches our server."""
887 return check_resource_allowed(requested_resource=self.resource_url, configured_resource=resource)
888 ```
889 
890 For more details, see below or the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
891 
892 **Python MCP Server**
893 
894 In the server's root have a `pyproject.toml` file and a `mcp_server` folder. Put all the Python files in the `mcp_server` folder, and fill the `pyproject.toml` file like:
895 
896 ```toml theme={null}
897 [project]
898 name = "mcp-simple-auth"
899 version = "0.1.0"
900 description = "A simple MCP server demonstrating OAuth authentication"
901 requires-python = ">=3.10"
902 authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
903 license = { text = "MIT" }
904 dependencies = [
905 "httpx2>=2.5.0",
906 "mcp>=2.0.0rc1",
907 "pydantic>=2.0",
908 ]
909 
910 [project.scripts]
911 mcp-simple-auth-rs = "mcp_server.server:main"
912 
913 [build-system]
914 requires = ["hatchling"]
915 build-backend = "hatchling.build"
916 
917 [tool.hatch.build.targets.wheel]
918 packages = ["mcp_server"]
919 
920 [dependency-groups]
921 dev = ["pyright>=1.1.391", "pytest>=8.3.4", "ruff>=0.8.5"]
922 ```
923 
924 Then run the commands below to start the server.
925 
926 ```bash theme={null}
927 uv sync
928 uv run mcp-simple-auth-rs
929 ```
930 </Tab>
931 
932 <Tab title="C#">
933 You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
934 
935 To set up authorization in your MCP server using the MCP C# SDK, you can lean on the standard ASP.NET Core builder pattern. Instead of using the introspection endpoint provided by Keycloak, we will use built-in ASP.NET Core capabilities for token validation.
936 
937 In the root of your server folder, create two files, `Program.cs` and `ProtectedMcpServer.csproj`, and a `Tools` folder. Fill `Program.cs` with:
938 
939 ```csharp theme={null}
940 using Microsoft.AspNetCore.Authentication.JwtBearer;
941 using Microsoft.IdentityModel.Tokens;
942 using ModelContextProtocol.AspNetCore.Authentication;
943 using ProtectedMcpServer.Tools;
944 using System.Security.Claims;
945 
946 var builder = WebApplication.CreateBuilder(args);
947 
948 var serverUrl = "http://localhost:3000/";
949 var authorizationServerUrl = "http://localhost:8080/realms/master/";
950 
951 builder.Services.AddAuthentication(options =>
962 return false;
963 }
964 };
965 
966 options.RequireHttpsMetadata = false; // Set to true in production
967 
968 options.Events = new JwtBearerEvents
952969 {
953 options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
954 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
955 })
956 .AddJwtBearer(options =>
970 OnTokenValidated = context =>
971 {
972 var name = context.Principal?.Identity?.Name ?? "unknown";
973 var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
974 Console.WriteLine($"Token validated for: {name} ({email})");
975 return Task.CompletedTask;
976 },
977 OnAuthenticationFailed = context =>
978 {
979 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
980 return Task.CompletedTask;
981 },
982 };
983})
984.AddMcp(options =>
985{
986 options.ResourceMetadata = new()
957987 {
958 options.Authority = authorizationServerUrl;
959 var normalizedServerAudience = serverUrl.TrimEnd('/');
960 options.TokenValidationParameters = new TokenValidationParameters
961 {
962 ValidIssuer = authorizationServerUrl,
963 ValidAudiences = new[] { normalizedServerAudience, serverUrl },
964 AudienceValidator = (audiences, securityToken, validationParameters) =>
965 {
966 if (audiences == null) return false;
967 foreach (var aud in audiences)
968 {
969 if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
970 {
971 return true;
972 }
973 }
974 return false;
975 }
976 };
977 
978 options.RequireHttpsMetadata = false; // Set to true in production
979 
980 options.Events = new JwtBearerEvents
981 {
982 OnTokenValidated = context =>
983 {
984 var name = context.Principal?.Identity?.Name ?? "unknown";
985 var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
986 Console.WriteLine($"Token validated for: {name} ({email})");
987 return Task.CompletedTask;
988 },
989 OnAuthenticationFailed = context =>
990 {
991 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
992 return Task.CompletedTask;
993 },
994 };
995 })
996 .AddMcp(options =>
988 Resource = serverUrl,
989 ResourceDocumentation = "https://docs.example.com/api/math",
990 AuthorizationServers = { authorizationServerUrl },
991 ScopesSupported = ["mcp:tools"]
992 };
993});
994 
995builder.Services.AddAuthorization();
996 
997builder.Services.AddHttpContextAccessor();
998builder.Services.AddMcpServer()
999 .WithTools<MathTools>()
1000 .WithHttpTransport();
1001 
1002var app = builder.Build();
1003 
1004app.UseAuthentication();
1005app.UseAuthorization();
1006 
1007app.MapMcp().RequireAuthorization();
1008 
1009Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
1010Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
1011Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
1012Console.WriteLine("Exposed Math tools: Add, Multiply");
1013Console.WriteLine("Press Ctrl+C to stop the server");
1014 
1015app.Run(serverUrl);
1016```
1017 
1018Fill `ProtectedMcpServer.csproj` with:
1019 
1020```xml theme={null}
1021<Project Sdk="Microsoft.NET.Sdk.Web">
1022 
1023 <PropertyGroup>
1024 <TargetFramework>net9.0</TargetFramework>
1025 <Nullable>enable</Nullable>
1026 <ImplicitUsings>enable</ImplicitUsings>
1027 <!-- Identifier for the local secret store, not a secret itself. -->
1028 <UserSecretsId>local-authorization-mcp-server</UserSecretsId>
1029 </PropertyGroup>
1030 
1031 <ItemGroup>
1032 <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.18" />
1033 <PackageReference Include="ModelContextProtocol" Version="2.0.0" />
1034 <PackageReference Include="ModelContextProtocol.AspNetCore" Version="2.0.0" />
1035 </ItemGroup>
1036 
1037</Project>
1038```
1039 
1040In the `Tools` folder, create `MathTools.cs` and fill it with:
1041 
1042```csharp theme={null}
1043using System.ComponentModel;
1044using ModelContextProtocol.Server;
1045 
1046namespace ProtectedMcpServer.Tools;
1047 
1048[McpServerToolType]
1049public sealed class MathTools
1050{
1051 [McpServerTool, Description("Add two numbers together.")]
1052 public Task<double> Add(
1053 [Description("First operand")] double a,
1054 [Description("Second operand")] double b)
9971055 {
998 options.ResourceMetadata = new()
999 {
1000 Resource = serverUrl,
1001 ResourceDocumentation = "https://docs.example.com/api/math",
1002 AuthorizationServers = { authorizationServerUrl },
1003 ScopesSupported = ["mcp:tools"]
1004 };
1005 });
1006 
1007 builder.Services.AddAuthorization();
1008 
1009 builder.Services.AddHttpContextAccessor();
1010 builder.Services.AddMcpServer()
1011 .WithTools<MathTools>()
1012 .WithHttpTransport();
1013 
1014 var app = builder.Build();
1015 
1016 app.UseAuthentication();
1017 app.UseAuthorization();
1018 
1019 app.MapMcp().RequireAuthorization();
1020 
1021 Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
1022 Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
1023 Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
1024 Console.WriteLine("Exposed Math tools: Add, Multiply");
1025 Console.WriteLine("Press Ctrl+C to stop the server");
1026 
1027 app.Run(serverUrl);
1028 ```
1029 
1030 Fill `ProtectedMcpServer.csproj` with:
1031 
1032 ```xml theme={null}
1033 <Project Sdk="Microsoft.NET.Sdk.Web">
1034 
1035 <PropertyGroup>
1036 <TargetFramework>net9.0</TargetFramework>
1037 <Nullable>enable</Nullable>
1038 <ImplicitUsings>enable</ImplicitUsings>
1039 <!-- Identifier for the local secret store, not a secret itself. -->
1040 <UserSecretsId>local-authorization-mcp-server</UserSecretsId>
1041 </PropertyGroup>
1042 
1043 <ItemGroup>
1044 <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.18" />
1045 <PackageReference Include="ModelContextProtocol" Version="2.0.0" />
1046 <PackageReference Include="ModelContextProtocol.AspNetCore" Version="2.0.0" />
1047 </ItemGroup>
1048 
1049 </Project>
1050 ```
1051 
1052 In the `Tools` folder, create `MathTools.cs` and fill it with:
1053 
1054 ```csharp theme={null}
1055 using System.ComponentModel;
1056 using ModelContextProtocol.Server;
1057 
1058 namespace ProtectedMcpServer.Tools;
1059 
1060 [McpServerToolType]
1061 public sealed class MathTools
1056 return Task.FromResult(a + b);
1057 }
1058 
1059 [McpServerTool, Description("Multiply two numbers together.")]
1060 public Task<double> Multiply(
1061 [Description("First operand")] double a,
1062 [Description("Second operand")] double b)
10621063 {
1063 [McpServerTool, Description("Add two numbers together.")]
1064 public Task<double> Add(
1065 [Description("First operand")] double a,
1066 [Description("Second operand")] double b)
1067 {
1068 return Task.FromResult(a + b);
1069 }
1070 
1071 [McpServerTool, Description("Multiply two numbers together.")]
1072 public Task<double> Multiply(
1073 [Description("First operand")] double a,
1074 [Description("Second operand")] double b)
1075 {
1076 return Task.FromResult(a * b);
1077 }
1064 return Task.FromResult(a * b);
10781065 }
1079 ```
1080 
1081 Then from the server's root, run:
1082 
1083 ```bash theme={null}
1084 dotnet run
1085 ```
1086 
1087 For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
1088 </Tab>
1089</Tabs>
1066}
1067```
1068 
1069Then from the server's root, run:
1070 
1071```bash theme={null}
1072dotnet run
1073```
1074 
1075For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
10901076 
10911077## Testing the MCP Server
10921078 
from line 1089
11031089 
11041090On connection, you will be taken to the browser, where you will be prompted to consent to Visual Studio Code having access to the `mcp:tools` scope.
11051091 
1106<Frame>
1107 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-vscode.png?fit=max&auto=format&n=sAd4SGUO-cEUqgzn&q=85&s=d5183fb7c257993aed1b2246f0bbbb27" alt="Keycloak consent form for VS Code." width="1915" height="1536" data-path="images/tutorial-authorization/keycloak-vscode.png" />
1108</Frame>
1092![Keycloak consent form for VS Code.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-vscode.png?fit=max\&auto=format\&n=sAd4SGUO-cEUqgzn\&q=85\&s=d5183fb7c257993aed1b2246f0bbbb27)
11091093 
11101094After consenting, you will see the tools listed right above the server entry in `mcp.json`.
11111095 
1112<Frame>
1113 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/tools-vs-code.png?fit=max&auto=format&n=sAd4SGUO-cEUqgzn&q=85&s=f7c34d1bf115fe6934e01b4a5a91168b" alt="Tools listed in VS Code." width="496" height="160" data-path="images/tutorial-authorization/tools-vs-code.png" />
1114</Frame>
1096![Tools listed in VS Code.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/tools-vs-code.png?fit=max\&auto=format\&n=sAd4SGUO-cEUqgzn\&q=85\&s=f7c34d1bf115fe6934e01b4a5a91168b)
11151097 
11161098You will be able to invoke individual tools with the help of the `#` sign in the chat view.
11171099 
1118<Frame>
1119 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/tools-vs-code-invoke.png?fit=max&auto=format&n=sAd4SGUO-cEUqgzn&q=85&s=76cbef68e48821a3c5467bd20c7e89fe" alt="Invoking MCP tools in VS Code." width="1276" height="396" data-path="images/tutorial-authorization/tools-vs-code-invoke.png" />
1120</Frame>
1100![Invoking MCP tools in VS Code.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/tools-vs-code-invoke.png?fit=max\&auto=format\&n=sAd4SGUO-cEUqgzn\&q=85\&s=76cbef68e48821a3c5467bd20c7e89fe)
11211101 
11221102## Common Pitfalls and How to Avoid Them
11231103 
11241104 

docs/2026-07-28/tutorials/security/local-server-security Changed · +45 / -47 lines

from line 15
1515network connections to anywhere. None of that is governed by the protocol.
1616It is governed by how you run the process.
1717 
18<Note>
19 **Scope**: this guide is about servers running on a machine you (or your
20 organization's users) control — a laptop or workstation, typically launched
21 over stdio. Deploying and hardening remote or hosted servers is a different
22 topic with different controls.
23</Note>
18> [!NOTE]
19>
20> **Scope**: this guide is about servers running on a machine you (or your
21> organization's users) control — a laptop or workstation, typically launched
22> over stdio. Deploying and hardening remote or hosted servers is a different
23> topic with different controls.
2424 
25<Tip>
26 **If you do one thing after reading this**: open your client's MCP
27 configuration, remove the servers you no longer use, and make sure you know
28 where each remaining one comes from and that its version is pinned. Everything
29 else in this guide builds on knowing what you are actually running.
30</Tip>
25> [!TIP]
26>
27> **If you do one thing after reading this**: open your client's MCP
28> configuration, remove the servers you no longer use, and make sure you know
29> where each remaining one comes from and that its version is pinned. Everything
30> else in this guide builds on knowing what you are actually running.
3131 
3232## What a Local Server Can Touch
3333 
from line 285
285285 directory to do a narrow job is overreaching. Grant a narrower directory,
286286 or pick a different server.
287287 
288<Warning>
289 Configuration-level allowlists are **cooperative boundaries**: a well-behaved
290 server respects them, but nothing in the protocol stops a malicious or
291 compromised one from reading whatever the process can reach. Treat them as
292 guardrails for honest servers. Enforcement comes from the isolation layer
293 above — a sandbox or container where the allowed directory is the only
294 filesystem that exists.
295</Warning>
288> [!WARNING]
289>
290> Configuration-level allowlists are **cooperative boundaries**: a well-behaved
291> server respects them, but nothing in the protocol stops a malicious or
292> compromised one from reading whatever the process can reach. Treat them as
293> guardrails for honest servers. Enforcement comes from the isolation layer
294> above — a sandbox or container where the allowed directory is the only
295> filesystem that exists.
296296 
297297**Going further**: combine allowlists with isolation, so the only filesystem
298298the server can even see is the mount you chose.
from line 316
316316 restricts them to their expected destinations and logs the attempts that
317317 fall outside it.
318318 
319<Note>
320 Egress is not the only network concern. Local MCP plumbing that listens on a
321 port — bridges to remote servers, development tools, locally hosted HTTP
322 servers — is reachable by other local processes and, through your browser, by
323 websites. Binding to 127.0.0.1 is not an authentication boundary: such
324 components need real authentication and origin validation, and they need
325 prompt updates when advisories land.
326</Note>
319> [!NOTE]
320>
321> Egress is not the only network concern. Local MCP plumbing that listens on a
322> port — bridges to remote servers, development tools, locally hosted HTTP
323> servers — is reachable by other local processes and, through your browser, by
324> websites. Binding to 127.0.0.1 is not an authentication boundary: such
325> components need real authentication and origin validation, and they need
326> prompt updates when advisories land.
327327 
328328**Going further**: give each server its own egress allowlist via a proxy,
329329and alert on destinations outside it — unexpected egress is often the first
from line 419
419419 
420420## Next Steps
421421 
422<CardGroup cols={2}>
423 <Card title="Security Best Practices" icon="shield" href="/docs/2026-07-28/tutorials/security/security_best_practices">
424 The protocol threat model: confused deputy, token passthrough, and session
425 hijacking
426 </Card>
422> **[Security Best Practices](/docs/2026-07-28/tutorials/security/security_best_practices)**
423>
424> The protocol threat model: confused deputy, token passthrough, and session
425> hijacking
427426 
428 <Card title="Authorization" icon="key" href="/docs/2026-07-28/tutorials/security/authorization">
429 How OAuth-based authorization works for remote MCP servers
430 </Card>
427> **[Authorization](/docs/2026-07-28/tutorials/security/authorization)**
428>
429> How OAuth-based authorization works for remote MCP servers
431430 
432 <Card title="SEP-1024" icon="file-shield" href="/seps/1024-mcp-client-security-requirements-for-local-server-">
433 Client security requirements for local server installation consent
434 </Card>
431> **[SEP-1024](/seps/1024-mcp-client-security-requirements-for-local-server-)**
432>
433> Client security requirements for local server installation consent
435434 
436 <Card title="Connect to local servers" icon="plug" href="/docs/2026-07-28/develop/connect-local-servers">
437 Set up local servers in your client — the workflow this guide secures
438 </Card>
439</CardGroup>
435> **[Connect to local servers](/docs/2026-07-28/develop/connect-local-servers)**
436>
437> Set up local servers in your client — the workflow this guide secures
440438 

docs/2026-07-28/tutorials/security/security_best_practices Changed · +26 / -26 lines

from line 442
442442* Link-local: `169.254.0.0/16` (including cloud metadata endpoints)
443443* Private IPv6 ranges: `fc00::/7`, `fe80::/10`
444444 
445<Note>
446 Avoid implementing IP validation manually. Attackers exploit encoding tricks
447 (octal, hex, IPv4-mapped IPv6) that custom parsers often miss.
448</Note>
445> [!NOTE]
446>
447> Avoid implementing IP validation manually. Attackers exploit encoding tricks
448> (octal, hex, IPv4-mapped IPv6) that custom parsers often miss.
449449 
450450**Validate Redirect Targets**
451451 
from line 946
946946process, minimizing user friction while following the principle of
947947least privilege.
948948 
949<Note>
950 Scope accumulation across operations is a client-side responsibility. Clients
951 **SHOULD** compute the union of previously requested scopes and newly
952 challenged scopes when initiating re-authorization, as described in [Step-Up
953 Authorization
954 Flow](/specification/2026-07-28/basic/authorization#step-up-authorization-flow).
955 This allows servers to remain stateless with respect to client scope sets
956 while ensuring clients do not lose previously granted permissions.
957</Note>
949> [!NOTE]
950>
951> Scope accumulation across operations is a client-side responsibility. Clients
952> **SHOULD** compute the union of previously requested scopes and newly
953> challenged scopes when initiating re-authorization, as described in [Step-Up
954> Authorization
955> Flow](/specification/2026-07-28/basic/authorization#step-up-authorization-flow).
956> This allows servers to remain stateless with respect to client scope sets
957> while ensuring clients do not lose previously granted permissions.
958958 
959<Note>
960 **Hierarchical scopes**: Some authorization servers define scope hierarchies
961 where a broader scope implies narrower ones (for example, an `admin` scope
962 that subsumes `read`). When accumulating scopes, the client's union may
963 contain semantically redundant entries. For example, a token previously
964 granted a broad scope may be challenged with a narrower one it already
965 implies. Clients need not deduplicate hierarchically; authorization servers
966 typically normalize such redundancy during token issuance. Servers, for their
967 part, must account for hierarchy when deciding whether a token is sufficient
968 for an operation, but this does not affect the scopes they emit in a
969 challenge.
970</Note>
959> [!NOTE]
960>
961> **Hierarchical scopes**: Some authorization servers define scope hierarchies
962> where a broader scope implies narrower ones (for example, an `admin` scope
963> that subsumes `read`). When accumulating scopes, the client's union may
964> contain semantically redundant entries. For example, a token previously
965> granted a broad scope may be challenged with a narrower one it already
966> implies. Clients need not deduplicate hierarchically; authorization servers
967> typically normalize such redundancy during token issuance. Servers, for their
968> part, must account for hierarchy when deciding whether a token is sufficient
969> for an operation, but this does not affect the scopes they emit in a
970> challenge.
971971 
972972#### Common Mistakes
973973 

docs/draft/develop/build-client Changed · +2436 / -2452 lines

## System Requirements ## Setting Up Your Environment ## Setting Up Your API Key ## Creating the Client ### Imports and Setup ### Server Connection Management ### Query Processing Logic ### Interactive Chat Interface ### Main Entry Point ## Key Components Explained ### 1. Client Initialization ### 2. Server Connection ### 3. Query Processing ### 4. Interactive Interface ### 5. Resource Management ## Common Customization Points ## Running the Client ## How It Works ## Best practices ## Troubleshooting ### Server Path Issues ### Response Timing ### Common Error Messages ## System Requirements ## Setting Up Your Environment ## Setting Up Your API Key ## Creating the Client ### Basic Client Structure ### Server Connection Management ### Query Processing Logic ### Interactive Chat Interface ### Main Entry Point ## Running the Client ## How It Works ## Best practices ## Troubleshooting ### Server Path Issues ### Response Timing ### Common Error Messages ## System Requirements ## Setting Up Your Environment ## How it Works ### MCP Client Configuration ### Chat Implementation ### Build and run ### Advanced Configuration ## System Requirements ## Setting up your environment ## Setting up your API key ## Creating the Client ### Basic Client Structure ### Server connection management ### Query processing logic ### Interactive chat ### Main entry point ## Running the client ## How it works ## Best practices ## Troubleshooting ### Server Path Issues ### Build Issues ### Response Timing ### Common Error Messages ## System Requirements ## Setting up your environment ## Setting up your API key ## Creating the Client ### Basic Client Structure ### Query processing logic ## Key Components Explained ### 1. Client Initialization ### 2. Server Connection ### 3. Query Processing ## Running the Client ## System Requirements ## Setting Up Your Environment ## Setting Up Your API Key ## Creating the Client ### Basic Client Structure ### Server Connection Management ### Query Processing Logic ### Interactive Chat Interface ### Main Entry Point ## Key Components Explained ### 1. Client Initialization ### 2. Server Connection ### 3. Query Processing ### 4. Interactive Interface ### 5. Resource Management ## Running the Client ## How It Works ## Best practices ## Troubleshooting ### Server Path Issues ### Response Timing ### Common Error Messages ## System Requirements ## Setting Up Your Environment ## Setting Up Your API Key ## Creating the Client ### Imports and Client Structure ### Client Initialization ### Server Connection Management ### Converting MCP Tools ### Sending Model Requests ### Query Processing Logic ### Interactive Chat Interface ### Cleanup ### Main Entry Point ### Verify the Complete File ## Running the Client ## How It Works ## Best Practices ## Troubleshooting ### Server Command Issues ### Environment File Issues ### Tool and Response Errors

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 6
66 
77Before you begin, it helps to have gone through our [Build an MCP Server](/docs/draft/develop/build-server) tutorial so you can understand how clients and servers communicate.
88 
9<Tabs>
10 <Tab title="Python">
11 [You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-python)
12 
13 ## System Requirements
14 
15 Before starting, ensure your system meets these requirements:
16 
17 * Mac or Windows computer
18 * Latest Python version installed
19 * Latest version of `uv` installed
20 * You must use the Python MCP SDK 2.0.0 or higher
21 
22 ## Setting Up Your Environment
23 
24 First, create a new Python project with `uv`:
25 
26 <CodeGroup>
27 ```bash macOS/Linux theme={null}
28 # Create project directory
29 uv init mcp-client
30 cd mcp-client
31 
32 # Create virtual environment
33 uv venv
34 
35 # Activate virtual environment
36 source .venv/bin/activate
37 
38 # Install required packages
39 uv add mcp anthropic python-dotenv
40 
41 # Remove boilerplate files
42 rm main.py
43 
44 # Create our main file
45 touch client.py
46 ```
47 
48 ```powershell Windows theme={null}
49 # Create project directory
50 uv init mcp-client
51 cd mcp-client
52 
53 # Create virtual environment
54 uv venv
55 
56 # Activate virtual environment
57 .venv\Scripts\activate
58 
59 # Install required packages
60 uv add mcp anthropic python-dotenv
61 
62 # Remove boilerplate files
63 del main.py
64 
65 # Create our main file
66 new-item client.py
67 ```
68 </CodeGroup>
69 
70 ## Setting Up Your API Key
71 
72 You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
73 
74 Create a `.env` file to store it:
75 
76 ```bash theme={null}
77 echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
78 ```
79 
80 Add `.env` to your `.gitignore`:
81 
82 ```bash theme={null}
83 echo ".env" >> .gitignore
84 ```
85 
86 <Warning>
87 Make sure you keep your `ANTHROPIC_API_KEY` secure!
88 </Warning>
89 
90 ## Creating the Client
91 
92 ### Imports and Setup
93 
94 First, let's set up our imports and the pieces the rest of the file shares:
95 
96 ```python theme={null}
97 import asyncio
98 import sys
99 
100 from mcp import Client, StdioServerParameters
101 from mcp.client.stdio import stdio_client
102 from mcp_types import TextContent
103 
104 from anthropic import Anthropic
105 from dotenv import load_dotenv
106 
107 load_dotenv() # load environment variables from .env
108 
109 MODEL = "claude-opus-5"
110 anthropic = Anthropic()
111 ```
112 
113 `Client` is the single object your program talks to the server through. Listing the tools, calling one, reading a resource: each of those is a method on it.
114 
115 ### Server Connection Management
116 
117 Next, we'll work out which process to launch for a given server script:
118 
119 ```python theme={null}
120 def server_params(server_script_path: str) -> StdioServerParameters:
121 """Describe the subprocess that runs an MCP server
122 
123 Args:
124 server_script_path: Path to the server script (.py or .js)
125 """
126 if server_script_path.endswith(".py"):
127 command = "python"
128 elif server_script_path.endswith(".js"):
129 command = "node"
130 else:
131 raise ValueError("Server script must be a .py or .js file")
132 
133 return StdioServerParameters(command=command, args=[server_script_path])
134 ```
135 
136 `StdioServerParameters` is configuration, not a connection. `stdio_client()` turns it into a stdio transport, and `Client` opens that transport when you enter its `async with` block. We'll do both in `main()`.
137 
138 ### Query Processing Logic
139 
140 Now let's add the core functionality for processing queries and handling tool calls:
141 
142 ```python theme={null}
143 async def process_query(client: Client, query: str) -> str:
144 """Process a query using Claude and available tools"""
145 messages = [
146 {
147 "role": "user",
148 "content": query
149 }
150 ]
151 
152 tool_list = await client.list_tools()
153 available_tools = [{
154 "name": tool.name,
155 "description": tool.description,
156 "input_schema": tool.input_schema
157 } for tool in tool_list.tools]
158 
159 # Initial Claude API call
9**Python**
10 
11[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-python)
12 
13## System Requirements
14 
15Before starting, ensure your system meets these requirements:
16 
17* Mac or Windows computer
18* Latest Python version installed
19* Latest version of `uv` installed
20* You must use the Python MCP SDK 2.0.0 or higher
21 
22## Setting Up Your Environment
23 
24First, create a new Python project with `uv`:
25 
26```bash macOS/Linux theme={null}
27# Create project directory
28uv init mcp-client
29cd mcp-client
30 
31# Create virtual environment
32uv venv
33 
34# Activate virtual environment
35source .venv/bin/activate
36 
37# Install required packages
38uv add mcp anthropic python-dotenv
39 
40# Remove boilerplate files
41rm main.py
42 
43# Create our main file
44touch client.py
45```
46 
47```powershell Windows theme={null}
48# Create project directory
49uv init mcp-client
50cd mcp-client
51 
52# Create virtual environment
53uv venv
54 
55# Activate virtual environment
56.venv\Scripts\activate
57 
58# Install required packages
59uv add mcp anthropic python-dotenv
60 
61# Remove boilerplate files
62del main.py
63 
64# Create our main file
65new-item client.py
66```
67 
68## Setting Up Your API Key
69 
70You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
71 
72Create a `.env` file to store it:
73 
74```bash theme={null}
75echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
76```
77 
78Add `.env` to your `.gitignore`:
79 
80```bash theme={null}
81echo ".env" >> .gitignore
82```
83 
84> [!WARNING]
85>
86> Make sure you keep your `ANTHROPIC_API_KEY` secure!
87 
88## Creating the Client
89 
90### Imports and Setup
91 
92First, let's set up our imports and the pieces the rest of the file shares:
93 
94```python theme={null}
95import asyncio
96import sys
97 
98from mcp import Client, StdioServerParameters
99from mcp.client.stdio import stdio_client
100from mcp_types import TextContent
101 
102from anthropic import Anthropic
103from dotenv import load_dotenv
104 
105load_dotenv() # load environment variables from .env
106 
107MODEL = "claude-opus-5"
108anthropic = Anthropic()
109```
110 
111`Client` is the single object your program talks to the server through. Listing the tools, calling one, reading a resource: each of those is a method on it.
112 
113### Server Connection Management
114 
115Next, we'll work out which process to launch for a given server script:
116 
117```python theme={null}
118def server_params(server_script_path: str) -> StdioServerParameters:
119 """Describe the subprocess that runs an MCP server
120 
121 Args:
122 server_script_path: Path to the server script (.py or .js)
123 """
124 if server_script_path.endswith(".py"):
125 command = "python"
126 elif server_script_path.endswith(".js"):
127 command = "node"
128 else:
129 raise ValueError("Server script must be a .py or .js file")
130 
131 return StdioServerParameters(command=command, args=[server_script_path])
132```
133 
134`StdioServerParameters` is configuration, not a connection. `stdio_client()` turns it into a stdio transport, and `Client` opens that transport when you enter its `async with` block. We'll do both in `main()`.
135 
136### Query Processing Logic
137 
138Now let's add the core functionality for processing queries and handling tool calls:
139 
140```python theme={null}
141async def process_query(client: Client, query: str) -> str:
142 """Process a query using Claude and available tools"""
143 messages = [
144 {
145 "role": "user",
146 "content": query
147 }
148 ]
149 
150 tool_list = await client.list_tools()
151 available_tools = [{
152 "name": tool.name,
153 "description": tool.description,
154 "input_schema": tool.input_schema
155 } for tool in tool_list.tools]
156 
157 # Initial Claude API call
158 response = anthropic.messages.create(
159 model=MODEL,
160 max_tokens=1000,
161 messages=messages,
162 tools=available_tools
163 )
164 
165 # Process response and handle tool calls
166 final_text = []
167 tool_results = []
168 
169 for content in response.content:
170 if content.type == 'text':
171 final_text.append(content.text)
172 elif content.type == 'tool_use':
173 tool_name = content.name
174 tool_args = content.input
175 
176 # Execute tool call
177 result = await client.call_tool(tool_name, tool_args)
178 final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
179 
180 tool_results.append({
181 "type": "tool_result",
182 "tool_use_id": content.id,
183 "content": "\n".join(
184 block.text
185 for block in result.content
186 if isinstance(block, TextContent)
187 ),
188 "is_error": result.is_error
189 })
190 
191 if tool_results:
192 messages.append({"role": "assistant", "content": response.content})
193 messages.append({"role": "user", "content": tool_results})
194 
195 # Get next response from Claude
160196 response = anthropic.messages.create(
161197 model=MODEL,
162198 max_tokens=1000,
from line 200
164200 tools=available_tools
165201 )
166202 
167 # Process response and handle tool calls
168 final_text = []
169 tool_results = []
170 
171203 for content in response.content:
172204 if content.type == 'text':
173205 final_text.append(content.text)
174 elif content.type == 'tool_use':
175 tool_name = content.name
176 tool_args = content.input
177 
178 # Execute tool call
179 result = await client.call_tool(tool_name, tool_args)
180 final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
181 
182 tool_results.append({
183 "type": "tool_result",
184 "tool_use_id": content.id,
185 "content": "\n".join(
186 block.text
187 for block in result.content
188 if isinstance(block, TextContent)
189 ),
190 "is_error": result.is_error
191 })
192 
193 if tool_results:
194 messages.append({"role": "assistant", "content": response.content})
195 messages.append({"role": "user", "content": tool_results})
196 
197 # Get next response from Claude
198 response = anthropic.messages.create(
199 model=MODEL,
200 max_tokens=1000,
201 messages=messages,
202 tools=available_tools
203 )
204 
205 for content in response.content:
206 if content.type == 'text':
207 final_text.append(content.text)
208 
209 return "\n".join(final_text)
210 ```
211 
212 `call_tool` returns a `CallToolResult`. Its `content` is a list of blocks, which is why we narrow to `TextContent` before reading `.text`. A tool that raises does not raise here: it answers with `is_error` set, and passing that flag on lets Claude read the message and try something else.
213 
214 ### Interactive Chat Interface
215 
216 Now we'll add the chat loop:
217 
218 ```python theme={null}
219 async def chat_loop(client: Client) -> None:
220 """Run an interactive chat loop"""
221 print("\nMCP Client Started!")
222 print("Type your queries or 'quit' to exit.")
223 
224 while True:
225 try:
226 query = (await asyncio.to_thread(input, "\nQuery: ")).strip()
227 except EOFError:
228 break
229 
230 if query.lower() == 'quit':
231 break
232 
233 try:
234 response = await process_query(client, query)
235 print("\n" + response)
236 except Exception as e:
237 print(f"\nError: {e}")
238 ```
239 
240 `input()` blocks, so it runs on a worker thread. That keeps the event loop free to service the connection while you type.
241 
242 ### Main Entry Point
243 
244 Finally, we'll add the main execution logic:
245 
246 ```python theme={null}
247 async def main() -> None:
248 if len(sys.argv) < 2:
249 print("Usage: python client.py <path_to_server_script>")
250 sys.exit(1)
251 
252 async with Client(stdio_client(server_params(sys.argv[1]))) as client:
253 tool_list = await client.list_tools()
254 tool_names = [tool.name for tool in tool_list.tools]
255 print("\nConnected to server with tools:", tool_names)
256 
257 await chat_loop(client)
258 
259 
260 if __name__ == "__main__":
261 asyncio.run(main())
262 ```
263 
264 That `async with` is the entire connection lifecycle. Entering it launches the server and agrees a protocol version with it; leaving it disconnects and shuts the subprocess down. There is nothing to close by hand.
265 
266 You can find the complete `client.py` file [here](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-python/client.py).
267 
268 ## Key Components Explained
269 
270 ### 1. Client Initialization
271 
272 * A single `Client` carries the connection, and `async with` is its whole lifecycle
273 * There is no connect/close pair to call and nothing to clean up afterwards
274 * Configures the Anthropic client for Claude interactions
275 
276 ### 2. Server Connection
277 
278 * Supports both Python and Node.js servers
279 * Validates server script type
280 * Launches the server as a subprocess and speaks stdio to it
281 * Lists the available tools once the connection is open
282 
283 ### 3. Query Processing
284 
285 * Maintains conversation context
286 * Handles Claude's responses and tool calls
287 * Manages the message flow between Claude and tools
288 * Combines results into a coherent response
289 
290 ### 4. Interactive Interface
291 
292 * Provides a simple command-line interface
293 * Handles user input and displays responses
294 * Includes basic error handling
295 * Allows graceful exit
296 
297 ### 5. Resource Management
298 
299 * Leaving the `async with` block disconnects and shuts the server subprocess down
300 * A failing query is reported without ending the session
301 * Typing `quit`, or closing standard input, exits cleanly
302 
303 ## Common Customization Points
304 
305 1. **Tool Handling**
306 * Modify `process_query()` to handle specific tool types
307 * Add custom error handling for tool calls
308 * Implement tool-specific response formatting
309 
310 2. **Response Processing**
311 * Customize how tool results are formatted
312 * Add response filtering or transformation
313 * Implement custom logging
314 
315 3. **User Interface**
316 * Add a GUI or web interface
317 * Implement rich console output
318 * Add command history or auto-completion
319 
320 ## Running the Client
321 
322 To run your client with any MCP server:
323 
324 ```bash theme={null}
325 uv run client.py path/to/server.py # python server
326 uv run client.py path/to/build/index.js # node server
327 ```
328 
329 <Note>
330 If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python), your command might look something like this: `python client.py .../quickstart-resources/weather-server-python/weather.py`
331 </Note>
332 
333 The client will:
334 
335 1. Connect to the specified server
336 2. List available tools
337 3. Start an interactive chat session where you can:
338 * Enter queries
339 * See tool executions
340 * Get responses from Claude
341 
342 Here's an example of what it should look like if connected to the weather server from the server quickstart:
343 
344 <Frame>
345 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/client-claude-cli-python.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=686d6e0ae7c54f807827db111eaed7d4" width="1932" height="1739" data-path="images/client-claude-cli-python.png" />
346 </Frame>
347 
348 ## How It Works
349 
350 When you submit a query:
351 
352 1. The client gets the list of available tools from the server
353 2. Your query is sent to Claude along with tool descriptions
354 3. Claude decides which tools (if any) to use
355 4. The client executes any requested tool calls through the server
356 5. Results are sent back to Claude
357 6. Claude provides a natural language response
358 7. The response is displayed to you
359 
360 ## Best practices
361 
362 1. **Error Handling**
363 * Check `result.is_error` rather than expecting a failing tool to raise
364 * Provide meaningful error messages
365 * Gracefully handle connection issues
366 
367 2. **Resource Management**
368 * Let the `async with` block own the connection
369 * Keep it open for as long as you need the server
370 * Handle server disconnections
371 
372 3. **Security**
373 * Store API keys securely in `.env`
374 * Validate server responses
375 * Be cautious with tool permissions
376 
377 4. **Tool Names**
378 * Tool names can be validated according to the format specified [here](/specification/draft/server/tools#tool-names)
379 * If a tool name conforms to the specified format, it should not fail validation by an MCP client
380 
381 ## Troubleshooting
382 
383 ### Server Path Issues
384 
385 * Double-check the path to your server script is correct
386 * Use the absolute path if the relative path isn't working
387 * For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
388 * Verify the server file has the correct extension (.py for Python or .js for Node.js)
389 
390 Example of correct path usage:
391 
392 ```bash theme={null}
393 # Relative path
394 uv run client.py ./server/weather.py
395 
396 # Absolute path
397 uv run client.py /Users/username/projects/mcp-server/weather.py
398 
399 # Windows path (either format works)
400 uv run client.py C:/projects/mcp-server/weather.py
401 uv run client.py C:\\projects\\mcp-server\\weather.py
402 ```
403 
404 ### Response Timing
405 
406 * The first response might take up to 30 seconds to return
407 * This is normal and happens while:
408 * The server initializes
409 * Claude processes the query
410 * Tools are being executed
411 * Subsequent responses are typically faster
412 * Don't interrupt the process during this initial waiting period
413 
414 ### Common Error Messages
415 
416 If you see:
417 
418 * `FileNotFoundError`: Check your server path
419 * `Connection refused`: Ensure the server is running and the path is correct
420 * `Tool execution failed`: Verify the tool's required environment variables are set
421 * `Timeout error`: Consider raising `read_timeout_seconds` on the `Client`
422 </Tab>
423 
424 <Tab title="TypeScript">
425 [You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-typescript)
426 
427 ## System Requirements
428 
429 Before starting, ensure your system meets these requirements:
430 
431 * Mac or Windows computer
432 * Node.js 20 or higher installed
433 * Latest version of `npm` installed
434 * Anthropic API key (Claude)
435 
436 ## Setting Up Your Environment
437 
438 First, let's create and set up our project:
439 
440 <CodeGroup>
441 ```bash macOS/Linux theme={null}
442 # Create project directory
443 mkdir mcp-client-typescript
444 cd mcp-client-typescript
445 
446 # Initialize npm project
447 npm init -y
448 
449 # Install dependencies
450 npm install @anthropic-ai/sdk @modelcontextprotocol/client dotenv
451 
452 # Install dev dependencies
453 npm install -D @types/node typescript
454 
455 # Create source file
456 touch index.ts
457 ```
458 
459 ```powershell Windows theme={null}
460 # Create project directory
461 md mcp-client-typescript
462 cd mcp-client-typescript
463 
464 # Initialize npm project
465 npm init -y
466 
467 # Install dependencies
468 npm install @anthropic-ai/sdk @modelcontextprotocol/client dotenv
469 
470 # Install dev dependencies
471 npm install -D @types/node typescript
472 
473 # Create source file
474 new-item index.ts
475 ```
476 </CodeGroup>
477 
478 Update your `package.json` to set `type: "module"` and a build script:
479 
480 ```json package.json theme={null}
206 
207 return "\n".join(final_text)
208```
209 
210`call_tool` returns a `CallToolResult`. Its `content` is a list of blocks, which is why we narrow to `TextContent` before reading `.text`. A tool that raises does not raise here: it answers with `is_error` set, and passing that flag on lets Claude read the message and try something else.
211 
212### Interactive Chat Interface
213 
214Now we'll add the chat loop:
215 
216```python theme={null}
217async def chat_loop(client: Client) -> None:
218 """Run an interactive chat loop"""
219 print("\nMCP Client Started!")
220 print("Type your queries or 'quit' to exit.")
221 
222 while True:
223 try:
224 query = (await asyncio.to_thread(input, "\nQuery: ")).strip()
225 except EOFError:
226 break
227 
228 if query.lower() == 'quit':
229 break
230 
231 try:
232 response = await process_query(client, query)
233 print("\n" + response)
234 except Exception as e:
235 print(f"\nError: {e}")
236```
237 
238`input()` blocks, so it runs on a worker thread. That keeps the event loop free to service the connection while you type.
239 
240### Main Entry Point
241 
242Finally, we'll add the main execution logic:
243 
244```python theme={null}
245async def main() -> None:
246 if len(sys.argv) < 2:
247 print("Usage: python client.py <path_to_server_script>")
248 sys.exit(1)
249 
250 async with Client(stdio_client(server_params(sys.argv[1]))) as client:
251 tool_list = await client.list_tools()
252 tool_names = [tool.name for tool in tool_list.tools]
253 print("\nConnected to server with tools:", tool_names)
254 
255 await chat_loop(client)
256 
257 
258if __name__ == "__main__":
259 asyncio.run(main())
260```
261 
262That `async with` is the entire connection lifecycle. Entering it launches the server and agrees a protocol version with it; leaving it disconnects and shuts the subprocess down. There is nothing to close by hand.
263 
264You can find the complete `client.py` file [here](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-python/client.py).
265 
266## Key Components Explained
267 
268### 1. Client Initialization
269 
270* A single `Client` carries the connection, and `async with` is its whole lifecycle
271* There is no connect/close pair to call and nothing to clean up afterwards
272* Configures the Anthropic client for Claude interactions
273 
274### 2. Server Connection
275 
276* Supports both Python and Node.js servers
277* Validates server script type
278* Launches the server as a subprocess and speaks stdio to it
279* Lists the available tools once the connection is open
280 
281### 3. Query Processing
282 
283* Maintains conversation context
284* Handles Claude's responses and tool calls
285* Manages the message flow between Claude and tools
286* Combines results into a coherent response
287 
288### 4. Interactive Interface
289 
290* Provides a simple command-line interface
291* Handles user input and displays responses
292* Includes basic error handling
293* Allows graceful exit
294 
295### 5. Resource Management
296 
297* Leaving the `async with` block disconnects and shuts the server subprocess down
298* A failing query is reported without ending the session
299* Typing `quit`, or closing standard input, exits cleanly
300 
301## Common Customization Points
302 
3031. **Tool Handling**
304 * Modify `process_query()` to handle specific tool types
305 * Add custom error handling for tool calls
306 * Implement tool-specific response formatting
307 
3082. **Response Processing**
309 * Customize how tool results are formatted
310 * Add response filtering or transformation
311 * Implement custom logging
312 
3133. **User Interface**
314 * Add a GUI or web interface
315 * Implement rich console output
316 * Add command history or auto-completion
317 
318## Running the Client
319 
320To run your client with any MCP server:
321 
322```bash theme={null}
323uv run client.py path/to/server.py # python server
324uv run client.py path/to/build/index.js # node server
325```
326 
327> [!NOTE]
328>
329> If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python), your command might look something like this: `python client.py .../quickstart-resources/weather-server-python/weather.py`
330 
331The client will:
332 
3331. Connect to the specified server
3342. List available tools
3353. Start an interactive chat session where you can:
336 * Enter queries
337 * See tool executions
338 * Get responses from Claude
339 
340Here's an example of what it should look like if connected to the weather server from the server quickstart:
341 
342![](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/client-claude-cli-python.png?fit=max\&auto=format\&n=4ZXF1PrDkEaJvXpn\&q=85\&s=686d6e0ae7c54f807827db111eaed7d4)
343 
344## How It Works
345 
346When you submit a query:
347 
3481. The client gets the list of available tools from the server
3492. Your query is sent to Claude along with tool descriptions
3503. Claude decides which tools (if any) to use
3514. The client executes any requested tool calls through the server
3525. Results are sent back to Claude
3536. Claude provides a natural language response
3547. The response is displayed to you
355 
356## Best practices
357 
3581. **Error Handling**
359 * Check `result.is_error` rather than expecting a failing tool to raise
360 * Provide meaningful error messages
361 * Gracefully handle connection issues
362 
3632. **Resource Management**
364 * Let the `async with` block own the connection
365 * Keep it open for as long as you need the server
366 * Handle server disconnections
367 
3683. **Security**
369 * Store API keys securely in `.env`
370 * Validate server responses
371 * Be cautious with tool permissions
372 
3734. **Tool Names**
374 * Tool names can be validated according to the format specified [here](/specification/draft/server/tools#tool-names)
375 * If a tool name conforms to the specified format, it should not fail validation by an MCP client
376 
377## Troubleshooting
378 
379### Server Path Issues
380 
381* Double-check the path to your server script is correct
382* Use the absolute path if the relative path isn't working
383* For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
384* Verify the server file has the correct extension (.py for Python or .js for Node.js)
385 
386Example of correct path usage:
387 
388```bash theme={null}
389# Relative path
390uv run client.py ./server/weather.py
391 
392# Absolute path
393uv run client.py /Users/username/projects/mcp-server/weather.py
394 
395# Windows path (either format works)
396uv run client.py C:/projects/mcp-server/weather.py
397uv run client.py C:\\projects\\mcp-server\\weather.py
398```
399 
400### Response Timing
401 
402* The first response might take up to 30 seconds to return
403* This is normal and happens while:
404 * The server initializes
405 * Claude processes the query
406 * Tools are being executed
407* Subsequent responses are typically faster
408* Don't interrupt the process during this initial waiting period
409 
410### Common Error Messages
411 
412If you see:
413 
414* `FileNotFoundError`: Check your server path
415* `Connection refused`: Ensure the server is running and the path is correct
416* `Tool execution failed`: Verify the tool's required environment variables are set
417* `Timeout error`: Consider raising `read_timeout_seconds` on the `Client`
418 
419**TypeScript**
420 
421[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-typescript)
422 
423## System Requirements
424 
425Before starting, ensure your system meets these requirements:
426 
427* Mac or Windows computer
428* Node.js 20 or higher installed
429* Latest version of `npm` installed
430* Anthropic API key (Claude)
431 
432## Setting Up Your Environment
433 
434First, let's create and set up our project:
435 
436```bash macOS/Linux theme={null}
437# Create project directory
438mkdir mcp-client-typescript
439cd mcp-client-typescript
440 
441# Initialize npm project
442npm init -y
443 
444# Install dependencies
445npm install @anthropic-ai/sdk @modelcontextprotocol/client dotenv
446 
447# Install dev dependencies
448npm install -D @types/node typescript
449 
450# Create source file
451touch index.ts
452```
453 
454```powershell Windows theme={null}
455# Create project directory
456md mcp-client-typescript
457cd mcp-client-typescript
458 
459# Initialize npm project
460npm init -y
461 
462# Install dependencies
463npm install @anthropic-ai/sdk @modelcontextprotocol/client dotenv
464 
465# Install dev dependencies
466npm install -D @types/node typescript
467 
468# Create source file
469new-item index.ts
470```
471 
472Update your `package.json` to set `type: "module"` and a build script:
473 
474```json package.json theme={null}
475{
476 "type": "module",
477 "scripts": {
478 "build": "tsc && chmod 755 build/index.js"
479 }
480}
481```
482 
483Create a `tsconfig.json` in the root of your project:
484 
485```json tsconfig.json theme={null}
486{
487 "compilerOptions": {
488 "target": "ES2022",
489 "module": "Node16",
490 "moduleResolution": "Node16",
491 "types": ["node"],
492 "outDir": "./build",
493 "rootDir": "./",
494 "strict": true,
495 "esModuleInterop": true,
496 "skipLibCheck": true,
497 "forceConsistentCasingInFileNames": true
498 },
499 "include": ["index.ts"],
500 "exclude": ["node_modules"]
501}
502```
503 
504## Setting Up Your API Key
505 
506You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
507 
508Create a `.env` file to store it:
509 
510```bash theme={null}
511echo "ANTHROPIC_API_KEY=<your key here>" > .env
512```
513 
514Add `.env` to your `.gitignore`:
515 
516```bash theme={null}
517echo ".env" >> .gitignore
518```
519 
520> [!WARNING]
521>
522> Make sure you keep your `ANTHROPIC_API_KEY` secure!
523 
524## Creating the Client
525 
526### Basic Client Structure
527 
528First, let's set up our imports and create the basic client class in `index.ts`:
529 
530```typescript theme={null}
531import { Anthropic } from "@anthropic-ai/sdk";
532import {
533 MessageParam,
534 Tool,
535} from "@anthropic-ai/sdk/resources/messages/messages.mjs";
536import { Client } from "@modelcontextprotocol/client";
537import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
538import readline from "readline/promises";
539import dotenv from "dotenv";
540 
541dotenv.config();
542 
543const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
544if (!ANTHROPIC_API_KEY) {
545 throw new Error("ANTHROPIC_API_KEY is not set");
546}
547 
548class MCPClient {
549 private mcp: Client;
550 private anthropic: Anthropic;
551 private transport: StdioClientTransport | null = null;
552 private tools: Tool[] = [];
553 
554 constructor() {
555 this.anthropic = new Anthropic({
556 apiKey: ANTHROPIC_API_KEY,
557 });
558 this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" });
559 }
560 // methods will go here
561}
562```
563 
564### Server Connection Management
565 
566Next, we'll implement the method to connect to an MCP server:
567 
568```typescript theme={null}
569async connectToServer(serverScriptPath: string) {
570 try {
571 const isJs = serverScriptPath.endsWith(".js");
572 const isPy = serverScriptPath.endsWith(".py");
573 if (!isJs && !isPy) {
574 throw new Error("Server script must be a .js or .py file");
575 }
576 const command = isPy
577 ? process.platform === "win32"
578 ? "python"
579 : "python3"
580 : process.execPath;
581 
582 this.transport = new StdioClientTransport({
583 command,
584 args: [serverScriptPath],
585 });
586 await this.mcp.connect(this.transport);
587 
588 const toolsResult = await this.mcp.listTools();
589 this.tools = toolsResult.tools.map((tool) => {
590 return {
591 name: tool.name,
592 description: tool.description,
593 input_schema: tool.inputSchema,
594 };
595 });
596 console.log(
597 "Connected to server with tools:",
598 this.tools.map(({ name }) => name)
599 );
600 } catch (e) {
601 console.log("Failed to connect to MCP server: ", e);
602 throw e;
603 }
604}
605```
606 
607### Query Processing Logic
608 
609Now let's add the core functionality for processing queries and handling tool calls:
610 
611```typescript theme={null}
612async processQuery(query: string) {
613 const messages: MessageParam[] = [
481614 {
482 "type": "module",
483 "scripts": {
484 "build": "tsc && chmod 755 build/index.js"
485 }
486 }
487 ```
488 
489 Create a `tsconfig.json` in the root of your project:
490 
491 ```json tsconfig.json theme={null}
492 {
493 "compilerOptions": {
494 "target": "ES2022",
495 "module": "Node16",
496 "moduleResolution": "Node16",
497 "types": ["node"],
498 "outDir": "./build",
499 "rootDir": "./",
500 "strict": true,
501 "esModuleInterop": true,
502 "skipLibCheck": true,
503 "forceConsistentCasingInFileNames": true
504 },
505 "include": ["index.ts"],
506 "exclude": ["node_modules"]
507 }
508 ```
509 
510 ## Setting Up Your API Key
511 
512 You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
513 
514 Create a `.env` file to store it:
515 
516 ```bash theme={null}
517 echo "ANTHROPIC_API_KEY=<your key here>" > .env
518 ```
519 
520 Add `.env` to your `.gitignore`:
521 
522 ```bash theme={null}
523 echo ".env" >> .gitignore
524 ```
525 
526 <Warning>
527 Make sure you keep your `ANTHROPIC_API_KEY` secure!
528 </Warning>
529 
530 ## Creating the Client
531 
532 ### Basic Client Structure
533 
534 First, let's set up our imports and create the basic client class in `index.ts`:
535 
536 ```typescript theme={null}
537 import { Anthropic } from "@anthropic-ai/sdk";
538 import {
539 MessageParam,
540 Tool,
541 } from "@anthropic-ai/sdk/resources/messages/messages.mjs";
542 import { Client } from "@modelcontextprotocol/client";
543 import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
544 import readline from "readline/promises";
545 import dotenv from "dotenv";
546 
547 dotenv.config();
548 
549 const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
550 if (!ANTHROPIC_API_KEY) {
551 throw new Error("ANTHROPIC_API_KEY is not set");
552 }
553 
554 class MCPClient {
555 private mcp: Client;
556 private anthropic: Anthropic;
557 private transport: StdioClientTransport | null = null;
558 private tools: Tool[] = [];
559 
560 constructor() {
561 this.anthropic = new Anthropic({
562 apiKey: ANTHROPIC_API_KEY,
563 });
564 this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" });
565 }
566 // methods will go here
567 }
568 ```
569 
570 ### Server Connection Management
571 
572 Next, we'll implement the method to connect to an MCP server:
573 
574 ```typescript theme={null}
575 async connectToServer(serverScriptPath: string) {
576 try {
577 const isJs = serverScriptPath.endsWith(".js");
578 const isPy = serverScriptPath.endsWith(".py");
579 if (!isJs && !isPy) {
580 throw new Error("Server script must be a .js or .py file");
581 }
582 const command = isPy
583 ? process.platform === "win32"
584 ? "python"
585 : "python3"
586 : process.execPath;
587 
588 this.transport = new StdioClientTransport({
589 command,
590 args: [serverScriptPath],
591 });
592 await this.mcp.connect(this.transport);
593 
594 const toolsResult = await this.mcp.listTools();
595 this.tools = toolsResult.tools.map((tool) => {
596 return {
597 name: tool.name,
598 description: tool.description,
599 input_schema: tool.inputSchema,
600 };
601 });
602 console.log(
603 "Connected to server with tools:",
604 this.tools.map(({ name }) => name)
605 );
606 } catch (e) {
607 console.log("Failed to connect to MCP server: ", e);
608 throw e;
609 }
610 }
611 ```
612 
613 ### Query Processing Logic
614 
615 Now let's add the core functionality for processing queries and handling tool calls:
616 
617 ```typescript theme={null}
618 async processQuery(query: string) {
619 const messages: MessageParam[] = [
620 {
621 role: "user",
622 content: query,
623 },
624 ];
615 role: "user",
616 content: query,
617 },
618 ];
619 
620 const response = await this.anthropic.messages.create({
621 model: "claude-opus-5",
622 max_tokens: 1000,
623 messages,
624 tools: this.tools,
625 });
626 
627 const finalText = [];
628 
629 for (const content of response.content) {
630 if (content.type === "text") {
631 finalText.push(content.text);
632 } else if (content.type === "tool_use") {
633 const toolName = content.name;
634 const toolArgs = content.input as { [x: string]: unknown } | undefined;
635 
636 const result = await this.mcp.callTool({
637 name: toolName,
638 arguments: toolArgs,
639 });
640 finalText.push(
641 `[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`
642 );
643 
644 messages.push({
645 role: "user",
646 content: result.content
647 .filter((block) => block.type === "text")
648 .map((block) => block.text)
649 .join("\n"),
650 });
625651 
626652 const response = await this.anthropic.messages.create({
627653 model: "claude-opus-5",
628654 max_tokens: 1000,
629655 messages,
630 tools: this.tools,
631656 });
632657 
633 const finalText = [];
634 
635 for (const content of response.content) {
636 if (content.type === "text") {
637 finalText.push(content.text);
638 } else if (content.type === "tool_use") {
639 const toolName = content.name;
640 const toolArgs = content.input as { [x: string]: unknown } | undefined;
641 
642 const result = await this.mcp.callTool({
643 name: toolName,
644 arguments: toolArgs,
645 });
646 finalText.push(
647 `[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`
648 );
649 
650 messages.push({
651 role: "user",
652 content: result.content
653 .filter((block) => block.type === "text")
654 .map((block) => block.text)
655 .join("\n"),
656 });
657 
658 const response = await this.anthropic.messages.create({
659 model: "claude-opus-5",
660 max_tokens: 1000,
661 messages,
662 });
663 
664 finalText.push(
665 response.content[0].type === "text" ? response.content[0].text : ""
666 );
667 }
658 finalText.push(
659 response.content[0].type === "text" ? response.content[0].text : ""
660 );
661 }
662 }
663 
664 return finalText.join("\n");
665}
666```
667 
668### Interactive Chat Interface
669 
670Now we'll add the chat loop and cleanup functionality:
671 
672```typescript theme={null}
673async chatLoop() {
674 const rl = readline.createInterface({
675 input: process.stdin,
676 output: process.stdout,
677 });
678 
679 try {
680 console.log("\nMCP Client Started!");
681 console.log("Type your queries or 'quit' to exit.");
682 
683 while (true) {
684 const message = await rl.question("\nQuery: ");
685 if (message.toLowerCase() === "quit") {
686 break;
668687 }
669 
670 return finalText.join("\n");
688 const response = await this.processQuery(message);
689 console.log("\n" + response);
671690 }
672 ```
673 
674 ### Interactive Chat Interface
675 
676 Now we'll add the chat loop and cleanup functionality:
677 
678 ```typescript theme={null}
679 async chatLoop() {
680 const rl = readline.createInterface({
681 input: process.stdin,
682 output: process.stdout,
683 });
684 
685 try {
686 console.log("\nMCP Client Started!");
687 console.log("Type your queries or 'quit' to exit.");
688 
689 while (true) {
690 const message = await rl.question("\nQuery: ");
691 if (message.toLowerCase() === "quit") {
692 break;
693 }
694 const response = await this.processQuery(message);
695 console.log("\n" + response);
696 }
697 } finally {
698 rl.close();
691 } finally {
692 rl.close();
693 }
694}
695 
696async cleanup() {
697 await this.mcp.close();
698}
699```
700 
701### Main Entry Point
702 
703Finally, we'll add the main execution logic:
704 
705```typescript theme={null}
706async function main() {
707 if (process.argv.length < 3) {
708 console.log("Usage: node index.ts <path_to_server_script>");
709 return;
710 }
711 const mcpClient = new MCPClient();
712 try {
713 await mcpClient.connectToServer(process.argv[2]);
714 await mcpClient.chatLoop();
715 } catch (e) {
716 console.error("Error:", e);
717 await mcpClient.cleanup();
718 process.exit(1);
719 } finally {
720 await mcpClient.cleanup();
721 process.exit(0);
722 }
723}
724 
725main();
726```
727 
728## Running the Client
729 
730To run your client with any MCP server:
731 
732```bash theme={null}
733# Build TypeScript
734npm run build
735 
736# Run the client
737node build/index.js path/to/server.py # python server
738node build/index.js path/to/build/index.js # node server
739```
740 
741> [!NOTE]
742>
743> If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-typescript), your command might look something like this: `node build/index.js .../quickstart-resources/weather-server-typescript/build/index.js`
744 
745**The client will:**
746 
7471. Connect to the specified server
7482. List available tools
7493. Start an interactive chat session where you can:
750 * Enter queries
751 * See tool executions
752 * Get responses from Claude
753 
754## How It Works
755 
756When you submit a query:
757 
7581. The client gets the list of available tools from the server
7592. Your query is sent to Claude along with tool descriptions
7603. Claude decides which tools (if any) to use
7614. The client executes any requested tool calls through the server
7625. Results are sent back to Claude
7636. Claude provides a natural language response
7647. The response is displayed to you
765 
766## Best practices
767 
7681. **Error Handling**
769 * Use TypeScript's type system for better error detection
770 * Wrap tool calls in try-catch blocks
771 * Provide meaningful error messages
772 * Gracefully handle connection issues
773 
7742. **Security**
775 * Store API keys securely in `.env`
776 * Validate server responses
777 * Be cautious with tool permissions
778 
779## Troubleshooting
780 
781### Server Path Issues
782 
783* Double-check the path to your server script is correct
784* Use the absolute path if the relative path isn't working
785* For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
786* Verify the server file has the correct extension (.js for Node.js or .py for Python)
787 
788Example of correct path usage:
789 
790```bash theme={null}
791# Relative path
792node build/index.js ./server/build/index.js
793 
794# Absolute path
795node build/index.js /Users/username/projects/mcp-server/build/index.js
796 
797# Windows path (either format works)
798node build/index.js C:/projects/mcp-server/build/index.js
799node build/index.js C:\\projects\\mcp-server\\build\\index.js
800```
801 
802### Response Timing
803 
804* The first response might take up to 30 seconds to return
805* This is normal and happens while:
806 * The server initializes
807 * Claude processes the query
808 * Tools are being executed
809* Subsequent responses are typically faster
810* Don't interrupt the process during this initial waiting period
811 
812### Common Error Messages
813 
814If you see:
815 
816* `Error: Cannot find module`: Check your build folder and ensure TypeScript compilation succeeded
817* `Connection refused`: Ensure the server is running and the path is correct
818* `Tool execution failed`: Verify the tool's required environment variables are set
819* `ANTHROPIC_API_KEY is not set`: Check your .env file and environment variables
820* `TypeError`: Ensure you're using the correct types for tool arguments
821* `BadRequestError`: Ensure you have enough credits to access the Anthropic API
822 
823**Java**
824 
825> [!NOTE]
826>
827> This is a quickstart demo based on Spring AI MCP auto-configuration and boot starters.
828> To learn how to create sync and async MCP Clients manually, consult the [Java SDK Client](https://java.sdk.modelcontextprotocol.io/) documentation.
829 
830This example demonstrates how to build an interactive chatbot that combines Spring AI's Model Context Protocol (MCP) with the [Brave Search MCP Server](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/brave-search). The application creates a conversational interface powered by Anthropic's Claude AI model that can perform internet searches through Brave Search, enabling natural language interactions with real-time web data.
831[You can find the complete code for this tutorial here.](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/web-search/brave-chatbot)
832 
833## System Requirements
834 
835Before starting, ensure your system meets these requirements:
836 
837* Java 17 or higher
838* Maven 3.6+
839* npx package manager
840* Anthropic API key (Claude)
841* Brave Search API key
842 
843## Setting Up Your Environment
844 
8451. Install npx (Node Package eXecute):
846 First, make sure to install [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
847 and then run:
848 
849 ```bash theme={null}
850 npm install -g npx
851 ```
852 
8532. Clone the repository:
854 
855 ```bash theme={null}
856 git clone https://github.com/spring-projects/spring-ai-examples.git
857 cd model-context-protocol/web-search/brave-chatbot
858 ```
859 
8603. Set up your API keys:
861 
862 ```bash theme={null}
863 export ANTHROPIC_API_KEY='your-anthropic-api-key-here'
864 export BRAVE_API_KEY='your-brave-api-key-here'
865 ```
866 
8674. Build the application:
868 
869 ```bash theme={null}
870 ./mvnw clean install
871 ```
872 
8735. Run the application using Maven:
874 ```bash theme={null}
875 ./mvnw spring-boot:run
876 ```
877 
878> [!WARNING]
879>
880> Make sure you keep your `ANTHROPIC_API_KEY` and `BRAVE_API_KEY` keys secure!
881 
882## How it Works
883 
884The application integrates Spring AI with the Brave Search MCP server through several components:
885 
886### MCP Client Configuration
887 
8881. Required dependencies in pom.xml:
889 
890```xml theme={null}
891<dependency>
892 <groupId>org.springframework.ai</groupId>
893 <artifactId>spring-ai-starter-mcp-client</artifactId>
894</dependency>
895<dependency>
896 <groupId>org.springframework.ai</groupId>
897 <artifactId>spring-ai-starter-model-anthropic</artifactId>
898</dependency>
899```
900 
9012. Application properties (application.yml):
902 
903```yml theme={null}
904spring:
905 ai:
906 mcp:
907 client:
908 enabled: true
909 name: brave-search-client
910 version: 1.0.0
911 type: SYNC
912 request-timeout: 20s
913 stdio:
914 root-change-notification: true
915 servers-configuration: classpath:/mcp-servers-config.json
916 toolcallback:
917 enabled: true
918 anthropic:
919 api-key: ${ANTHROPIC_API_KEY}
920```
921 
922This activates the `spring-ai-starter-mcp-client` to create one or more `McpClient`s based on the provided server configuration.
923The `spring.ai.mcp.client.toolcallback.enabled=true` property enables the tool callback mechanism, that automatically registers all MCP tool as spring ai tools.
924It is disabled by default.
925 
9263. MCP Server Configuration (`mcp-servers-config.json`):
927 
928```json theme={null}
929{
930 "mcpServers": {
931 "brave-search": {
932 "command": "npx",
933 "args": ["-y", "@modelcontextprotocol/server-brave-search"],
934 "env": {
935 "BRAVE_API_KEY": "<PUT YOUR BRAVE API KEY>"
699936 }
700937 }
701 
702 async cleanup() {
703 await this.mcp.close();
938 }
939}
940```
941 
942### Chat Implementation
943 
944The chatbot is implemented using Spring AI's ChatClient with MCP tool integration:
945 
946```java theme={null}
947var chatClient = chatClientBuilder
948 .defaultSystem("You are useful assistant, expert in AI and Java.")
949 .defaultToolCallbacks((Object[]) mcpToolAdapter.toolCallbacks())
950 .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
951 .build();
952```
953 
954Key features:
955 
956* Uses Claude AI model for natural language understanding
957* Integrates Brave Search through MCP for real-time web search capabilities
958* Maintains conversation memory using InMemoryChatMemory
959* Runs as an interactive command-line application
960 
961### Build and run
962 
963```bash theme={null}
964./mvnw clean install
965java -jar ./target/ai-mcp-brave-chatbot-0.0.1-SNAPSHOT.jar
966```
967 
968or
969 
970```bash theme={null}
971./mvnw spring-boot:run
972```
973 
974The application will start an interactive chat session where you can ask questions. The chatbot will use Brave Search when it needs to find information from the internet to answer your queries.
975 
976The chatbot can:
977 
978* Answer questions using its built-in knowledge
979* Perform web searches when needed using Brave Search
980* Remember context from previous messages in the conversation
981* Combine information from multiple sources to provide comprehensive answers
982 
983### Advanced Configuration
984 
985The MCP client supports additional configuration options:
986 
987* Client customization through `McpClientCustomizer<McpClient.SyncSpec>` or `McpClientCustomizer<McpClient.AsyncSpec>` beans
988* Multiple clients with multiple transport types: `STDIO` and Streamable HTTP
989* Integration with Spring AI's tool execution framework
990* Automatic client initialization and lifecycle management
991 
992To connect to a remote MCP server over Streamable HTTP, configure a connection URL:
993 
994```properties theme={null}
995spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080
996```
997 
998For WebFlux-based applications, you can use the WebFlux starter instead:
999 
1000```xml theme={null}
1001<dependency>
1002 <groupId>org.springframework.ai</groupId>
1003 <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
1004</dependency>
1005```
1006 
1007This provides similar functionality but uses a WebFlux-based Streamable HTTP transport implementation, recommended for production deployments.
1008 
1009**Kotlin**
1010 
1011[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/samples/kotlin-mcp-client)
1012 
1013## System Requirements
1014 
1015Before starting, ensure your system meets these requirements:
1016 
1017* JDK 11 or higher
1018* Anthropic API key (Claude)
1019 
1020## Setting up your environment
1021 
1022First, let's install `java` and `gradle` if you haven't already.
1023You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
1024Verify your `java` installation:
1025 
1026```bash theme={null}
1027java --version
1028```
1029 
1030Now, let's create and set up your project:
1031 
1032```bash macOS/Linux theme={null}
1033# Create a new directory for our project
1034mkdir kotlin-mcp-client
1035cd kotlin-mcp-client
1036 
1037# Initialize a new kotlin project
1038gradle init
1039```
1040 
1041```powershell Windows theme={null}
1042# Create a new directory for our project
1043md kotlin-mcp-client
1044cd kotlin-mcp-client
1045# Initialize a new kotlin project
1046gradle init
1047```
1048 
1049After running `gradle init`, select **Application** as the project type, **Kotlin** as the programming language.
1050 
1051Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
1052 
1053After creating the project, replace the contents of your `build.gradle.kts` with:
1054 
1055```kotlin build.gradle.kts theme={null}
1056// Check latest versions at https://github.com/modelcontextprotocol/kotlin-sdk/releases
1057val mcpVersion = "0.9.0"
1058val ktorVersion = "3.2.3"
1059val anthropicVersion = "2.15.0"
1060val slf4jVersion = "2.0.17"
1061 
1062plugins {
1063 kotlin("jvm") version "2.3.20"
1064 id("com.gradleup.shadow") version "8.3.9"
1065 application
1066}
1067 
1068application {
1069 mainClass.set("MainKt")
1070}
1071 
1072dependencies {
1073 implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
1074 implementation("io.ktor:ktor-client-cio:$ktorVersion")
1075 implementation("com.anthropic:anthropic-java:$anthropicVersion")
1076 implementation("org.slf4j:slf4j-simple:$slf4jVersion")
1077}
1078```
1079 
1080Verify that everything is set up correctly:
1081 
1082```bash theme={null}
1083./gradlew build
1084```
1085 
1086## Setting up your API key
1087 
1088You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
1089 
1090Set up your API key:
1091 
1092```bash theme={null}
1093export ANTHROPIC_API_KEY='your-anthropic-api-key-here'
1094```
1095 
1096> [!WARNING]
1097>
1098> Make sure you keep your `ANTHROPIC_API_KEY` secure!
1099 
1100## Creating the Client
1101 
1102### Basic Client Structure
1103 
1104First, let's create the basic client class:
1105 
1106```kotlin theme={null}
1107class MCPClient(apiKey: String) : AutoCloseable {
1108 private val anthropic = AnthropicOkHttpClient.builder()
1109 .apiKey(apiKey)
1110 .build()
1111 
1112 private val mcp: Client = Client(
1113 clientInfo = Implementation(name = "mcp-client-cli", version = "1.0.0")
1114 )
1115 private var serverProcess: Process? = null
1116 private lateinit var tools: List<ToolUnion>
1117 
1118 // methods will go here
1119 
1120 override fun close() {
1121 runBlocking {
1122 mcp.close()
1123 }
1124 serverProcess?.destroy()
1125 anthropic.close()
7041126 }
705 ```
706 
707 ### Main Entry Point
708 
709 Finally, we'll add the main execution logic:
710 
711 ```typescript theme={null}
712 async function main() {
713 if (process.argv.length < 3) {
714 console.log("Usage: node index.ts <path_to_server_script>");
715 return;
716 }
717 const mcpClient = new MCPClient();
718 try {
719 await mcpClient.connectToServer(process.argv[2]);
720 await mcpClient.chatLoop();
721 } catch (e) {
722 console.error("Error:", e);
723 await mcpClient.cleanup();
724 process.exit(1);
725 } finally {
726 await mcpClient.cleanup();
727 process.exit(0);
728 }
1127}
1128```
1129 
1130### Server connection management
1131 
1132Next, we'll implement the method to connect to an MCP server:
1133 
1134```kotlin theme={null}
1135suspend fun connectToServer(serverScriptPath: String) {
1136 val command = buildList {
1137 when (serverScriptPath.substringAfterLast(".")) {
1138 "js" -> add("node")
1139 "py" -> add(if (System.getProperty("os.name").lowercase().contains("win")) "python" else "python3")
1140 "jar" -> addAll(listOf("java", "-jar"))
1141 else -> throw IllegalArgumentException("Server script must be a .js, .py or .jar file")
1142 }
1143 add(serverScriptPath)
7291144 }
7301145 
731 main();
732 ```
733 
734 ## Running the Client
735 
736 To run your client with any MCP server:
737 
738 ```bash theme={null}
739 # Build TypeScript
740 npm run build
741 
742 # Run the client
743 node build/index.js path/to/server.py # python server
744 node build/index.js path/to/build/index.js # node server
745 ```
746 
747 <Note>
748 If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-typescript), your command might look something like this: `node build/index.js .../quickstart-resources/weather-server-typescript/build/index.js`
749 </Note>
750 
751 **The client will:**
752 
753 1. Connect to the specified server
754 2. List available tools
755 3. Start an interactive chat session where you can:
756 * Enter queries
757 * See tool executions
758 * Get responses from Claude
759 
760 ## How It Works
761 
762 When you submit a query:
763 
764 1. The client gets the list of available tools from the server
765 2. Your query is sent to Claude along with tool descriptions
766 3. Claude decides which tools (if any) to use
767 4. The client executes any requested tool calls through the server
768 5. Results are sent back to Claude
769 6. Claude provides a natural language response
770 7. The response is displayed to you
771 
772 ## Best practices
773 
774 1. **Error Handling**
775 * Use TypeScript's type system for better error detection
776 * Wrap tool calls in try-catch blocks
777 * Provide meaningful error messages
778 * Gracefully handle connection issues
779 
780 2. **Security**
781 * Store API keys securely in `.env`
782 * Validate server responses
783 * Be cautious with tool permissions
784 
785 ## Troubleshooting
786 
787 ### Server Path Issues
788 
789 * Double-check the path to your server script is correct
790 * Use the absolute path if the relative path isn't working
791 * For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
792 * Verify the server file has the correct extension (.js for Node.js or .py for Python)
793 
794 Example of correct path usage:
795 
796 ```bash theme={null}
797 # Relative path
798 node build/index.js ./server/build/index.js
799 
800 # Absolute path
801 node build/index.js /Users/username/projects/mcp-server/build/index.js
802 
803 # Windows path (either format works)
804 node build/index.js C:/projects/mcp-server/build/index.js
805 node build/index.js C:\\projects\\mcp-server\\build\\index.js
806 ```
807 
808 ### Response Timing
809 
810 * The first response might take up to 30 seconds to return
811 * This is normal and happens while:
812 * The server initializes
813 * Claude processes the query
814 * Tools are being executed
815 * Subsequent responses are typically faster
816 * Don't interrupt the process during this initial waiting period
817 
818 ### Common Error Messages
819 
820 If you see:
821 
822 * `Error: Cannot find module`: Check your build folder and ensure TypeScript compilation succeeded
823 * `Connection refused`: Ensure the server is running and the path is correct
824 * `Tool execution failed`: Verify the tool's required environment variables are set
825 * `ANTHROPIC_API_KEY is not set`: Check your .env file and environment variables
826 * `TypeError`: Ensure you're using the correct types for tool arguments
827 * `BadRequestError`: Ensure you have enough credits to access the Anthropic API
828 </Tab>
829 
830 <Tab title="Java">
831 <Note>
832 This is a quickstart demo based on Spring AI MCP auto-configuration and boot starters.
833 To learn how to create sync and async MCP Clients manually, consult the [Java SDK Client](https://java.sdk.modelcontextprotocol.io/) documentation.
834 </Note>
835 
836 This example demonstrates how to build an interactive chatbot that combines Spring AI's Model Context Protocol (MCP) with the [Brave Search MCP Server](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/brave-search). The application creates a conversational interface powered by Anthropic's Claude AI model that can perform internet searches through Brave Search, enabling natural language interactions with real-time web data.
837 [You can find the complete code for this tutorial here.](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/web-search/brave-chatbot)
838 
839 ## System Requirements
840 
841 Before starting, ensure your system meets these requirements:
842 
843 * Java 17 or higher
844 * Maven 3.6+
845 * npx package manager
846 * Anthropic API key (Claude)
847 * Brave Search API key
848 
849 ## Setting Up Your Environment
850 
851 1. Install npx (Node Package eXecute):
852 First, make sure to install [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
853 and then run:
854 
855 ```bash theme={null}
856 npm install -g npx
857 ```
858 
859 2. Clone the repository:
860 
861 ```bash theme={null}
862 git clone https://github.com/spring-projects/spring-ai-examples.git
863 cd model-context-protocol/web-search/brave-chatbot
864 ```
865 
866 3. Set up your API keys:
867 
868 ```bash theme={null}
869 export ANTHROPIC_API_KEY='your-anthropic-api-key-here'
870 export BRAVE_API_KEY='your-brave-api-key-here'
871 ```
872 
873 4. Build the application:
874 
875 ```bash theme={null}
876 ./mvnw clean install
877 ```
878 
879 5. Run the application using Maven:
880 ```bash theme={null}
881 ./mvnw spring-boot:run
882 ```
883 
884 <Warning>
885 Make sure you keep your `ANTHROPIC_API_KEY` and `BRAVE_API_KEY` keys secure!
886 </Warning>
887 
888 ## How it Works
889 
890 The application integrates Spring AI with the Brave Search MCP server through several components:
891 
892 ### MCP Client Configuration
893 
894 1. Required dependencies in pom.xml:
895 
896 ```xml theme={null}
897 <dependency>
898 <groupId>org.springframework.ai</groupId>
899 <artifactId>spring-ai-starter-mcp-client</artifactId>
900 </dependency>
901 <dependency>
902 <groupId>org.springframework.ai</groupId>
903 <artifactId>spring-ai-starter-model-anthropic</artifactId>
904 </dependency>
905 ```
906 
907 2. Application properties (application.yml):
908 
909 ```yml theme={null}
910 spring:
911 ai:
912 mcp:
913 client:
914 enabled: true
915 name: brave-search-client
916 version: 1.0.0
917 type: SYNC
918 request-timeout: 20s
919 stdio:
920 root-change-notification: true
921 servers-configuration: classpath:/mcp-servers-config.json
922 toolcallback:
923 enabled: true
924 anthropic:
925 api-key: ${ANTHROPIC_API_KEY}
926 ```
927 
928 This activates the `spring-ai-starter-mcp-client` to create one or more `McpClient`s based on the provided server configuration.
929 The `spring.ai.mcp.client.toolcallback.enabled=true` property enables the tool callback mechanism, that automatically registers all MCP tool as spring ai tools.
930 It is disabled by default.
931 
932 3. MCP Server Configuration (`mcp-servers-config.json`):
933 
934 ```json theme={null}
935 {
936 "mcpServers": {
937 "brave-search": {
938 "command": "npx",
939 "args": ["-y", "@modelcontextprotocol/server-brave-search"],
940 "env": {
941 "BRAVE_API_KEY": "<PUT YOUR BRAVE API KEY>"
942 }
943 }
944 }
945 }
946 ```
947 
948 ### Chat Implementation
949 
950 The chatbot is implemented using Spring AI's ChatClient with MCP tool integration:
951 
952 ```java theme={null}
953 var chatClient = chatClientBuilder
954 .defaultSystem("You are useful assistant, expert in AI and Java.")
955 .defaultToolCallbacks((Object[]) mcpToolAdapter.toolCallbacks())
956 .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
957 .build();
958 ```
959 
960 Key features:
961 
962 * Uses Claude AI model for natural language understanding
963 * Integrates Brave Search through MCP for real-time web search capabilities
964 * Maintains conversation memory using InMemoryChatMemory
965 * Runs as an interactive command-line application
966 
967 ### Build and run
968 
969 ```bash theme={null}
970 ./mvnw clean install
971 java -jar ./target/ai-mcp-brave-chatbot-0.0.1-SNAPSHOT.jar
972 ```
973 
974 or
975 
976 ```bash theme={null}
977 ./mvnw spring-boot:run
978 ```
979 
980 The application will start an interactive chat session where you can ask questions. The chatbot will use Brave Search when it needs to find information from the internet to answer your queries.
981 
982 The chatbot can:
983 
984 * Answer questions using its built-in knowledge
985 * Perform web searches when needed using Brave Search
986 * Remember context from previous messages in the conversation
987 * Combine information from multiple sources to provide comprehensive answers
988 
989 ### Advanced Configuration
990 
991 The MCP client supports additional configuration options:
992 
993 * Client customization through `McpClientCustomizer<McpClient.SyncSpec>` or `McpClientCustomizer<McpClient.AsyncSpec>` beans
994 * Multiple clients with multiple transport types: `STDIO` and Streamable HTTP
995 * Integration with Spring AI's tool execution framework
996 * Automatic client initialization and lifecycle management
997 
998 To connect to a remote MCP server over Streamable HTTP, configure a connection URL:
999 
1000 ```properties theme={null}
1001 spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080
1002 ```
1003 
1004 For WebFlux-based applications, you can use the WebFlux starter instead:
1005 
1006 ```xml theme={null}
1007 <dependency>
1008 <groupId>org.springframework.ai</groupId>
1009 <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
1010 </dependency>
1011 ```
1012 
1013 This provides similar functionality but uses a WebFlux-based Streamable HTTP transport implementation, recommended for production deployments.
1014 </Tab>
1015 
1016 <Tab title="Kotlin">
1017 [You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/samples/kotlin-mcp-client)
1018 
1019 ## System Requirements
1020 
1021 Before starting, ensure your system meets these requirements:
1022 
1023 * JDK 11 or higher
1024 * Anthropic API key (Claude)
1025 
1026 ## Setting up your environment
1027 
1028 First, let's install `java` and `gradle` if you haven't already.
1029 You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
1030 Verify your `java` installation:
1031 
1032 ```bash theme={null}
1033 java --version
1034 ```
1035 
1036 Now, let's create and set up your project:
1037 
1038 <CodeGroup>
1039 ```bash macOS/Linux theme={null}
1040 # Create a new directory for our project
1041 mkdir kotlin-mcp-client
1042 cd kotlin-mcp-client
1043 
1044 # Initialize a new kotlin project
1045 gradle init
1046 ```
1047 
1048 ```powershell Windows theme={null}
1049 # Create a new directory for our project
1050 md kotlin-mcp-client
1051 cd kotlin-mcp-client
1052 # Initialize a new kotlin project
1053 gradle init
1054 ```
1055 </CodeGroup>
1056 
1057 After running `gradle init`, select **Application** as the project type, **Kotlin** as the programming language.
1058 
1059 Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
1060 
1061 After creating the project, replace the contents of your `build.gradle.kts` with:
1062 
1063 ```kotlin build.gradle.kts theme={null}
1064 // Check latest versions at https://github.com/modelcontextprotocol/kotlin-sdk/releases
1065 val mcpVersion = "0.9.0"
1066 val ktorVersion = "3.2.3"
1067 val anthropicVersion = "2.15.0"
1068 val slf4jVersion = "2.0.17"
1069 
1070 plugins {
1071 kotlin("jvm") version "2.3.20"
1072 id("com.gradleup.shadow") version "8.3.9"
1073 application
1074 }
1075 
1076 application {
1077 mainClass.set("MainKt")
1078 }
1079 
1080 dependencies {
1081 implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
1082 implementation("io.ktor:ktor-client-cio:$ktorVersion")
1083 implementation("com.anthropic:anthropic-java:$anthropicVersion")
1084 implementation("org.slf4j:slf4j-simple:$slf4jVersion")
1085 }
1086 ```
1087 
1088 Verify that everything is set up correctly:
1089 
1090 ```bash theme={null}
1091 ./gradlew build
1092 ```
1093 
1094 ## Setting up your API key
1095 
1096 You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
1097 
1098 Set up your API key:
1099 
1100 ```bash theme={null}
1101 export ANTHROPIC_API_KEY='your-anthropic-api-key-here'
1102 ```
1103 
1104 <Warning>
1105 Make sure you keep your `ANTHROPIC_API_KEY` secure!
1106 </Warning>
1107 
1108 ## Creating the Client
1109 
1110 ### Basic Client Structure
1111 
1112 First, let's create the basic client class:
1113 
1114 ```kotlin theme={null}
1115 class MCPClient(apiKey: String) : AutoCloseable {
1116 private val anthropic = AnthropicOkHttpClient.builder()
1117 .apiKey(apiKey)
1118 .build()
1119 
1120 private val mcp: Client = Client(
1121 clientInfo = Implementation(name = "mcp-client-cli", version = "1.0.0")
1122 )
1123 private var serverProcess: Process? = null
1124 private lateinit var tools: List<ToolUnion>
1125 
1126 // methods will go here
1127 
1128 override fun close() {
1129 runBlocking {
1130 mcp.close()
1131 }
1132 serverProcess?.destroy()
1133 anthropic.close()
1134 }
1135 }
1136 ```
1137 
1138 ### Server connection management
1139 
1140 Next, we'll implement the method to connect to an MCP server:
1141 
1142 ```kotlin theme={null}
1143 suspend fun connectToServer(serverScriptPath: String) {
1144 val command = buildList {
1145 when (serverScriptPath.substringAfterLast(".")) {
1146 "js" -> add("node")
1147 "py" -> add(if (System.getProperty("os.name").lowercase().contains("win")) "python" else "python3")
1148 "jar" -> addAll(listOf("java", "-jar"))
1149 else -> throw IllegalArgumentException("Server script must be a .js, .py or .jar file")
1150 }
1151 add(serverScriptPath)
1152 }
1153 
1154 val process = ProcessBuilder(command).start()
1155 serverProcess = process
1156 
1157 val transport = StdioClientTransport(
1158 input = process.inputStream.asSource().buffered(),
1159 output = process.outputStream.asSink().buffered(),
1160 )
1161 
1162 mcp.connect(transport)
1163 
1164 val toolsResult = mcp.listTools()
1165 tools = toolsResult.tools.map { tool ->
1166 ToolUnion.ofTool(
1167 Tool.builder()
1168 .name(tool.name)
1169 .description(tool.description ?: "")
1170 .inputSchema(
1171 Tool.InputSchema.builder()
1172 .type(JsonValue.from(tool.inputSchema.type))
1173 .properties(tool.inputSchema.properties?.toJsonValue() ?: EmptyJsonObject.toJsonValue())
1174 .putAdditionalProperty("required", JsonValue.from(tool.inputSchema.required))
1175 .build(),
1176 )
1177 .build(),
1178 )
1179 }
1180 println("Connected to server with tools: ${tools.joinToString(", ") { it.tool().get().name() }}")
1181 }
1182 ```
1183 
1184 <Accordion title="JsonObject.toJsonValue() helper">
1185 This helper converts a kotlinx.serialization `JsonObject` to an Anthropic SDK `JsonValue` using Jackson:
1186 
1187 ```kotlin theme={null}
1188 private fun JsonObject.toJsonValue(): JsonValue {
1189 val mapper = ObjectMapper()
1190 val node = mapper.readTree(this.toString())
1191 return JsonValue.fromJsonNode(node)
1192 }
1193 ```
1194 </Accordion>
1195 
1196 ### Query processing logic
1197 
1198 Now let's add the core functionality for processing queries and handling tool calls:
1199 
1200 ```kotlin theme={null}
1201 suspend fun processQuery(query: String): String {
1202 val messages = mutableListOf(
1203 MessageParam.builder()
1204 .role(MessageParam.Role.USER)
1205 .content(query)
1146 val process = ProcessBuilder(command).start()
1147 serverProcess = process
1148 
1149 val transport = StdioClientTransport(
1150 input = process.inputStream.asSource().buffered(),
1151 output = process.outputStream.asSink().buffered(),
1152 )
1153 
1154 mcp.connect(transport)
1155 
1156 val toolsResult = mcp.listTools()
1157 tools = toolsResult.tools.map { tool ->
1158 ToolUnion.ofTool(
1159 Tool.builder()
1160 .name(tool.name)
1161 .description(tool.description ?: "")
1162 .inputSchema(
1163 Tool.InputSchema.builder()
1164 .type(JsonValue.from(tool.inputSchema.type))
1165 .properties(tool.inputSchema.properties?.toJsonValue() ?: EmptyJsonObject.toJsonValue())
1166 .putAdditionalProperty("required", JsonValue.from(tool.inputSchema.required))
1167 .build(),
1168 )
12061169 .build(),
12071170 )
1208 
1209 val response = anthropic.messages().create(
1210 MessageCreateParams.builder()
1211 .model("claude-opus-5")
1212 .maxTokens(1024)
1213 .messages(messages)
1214 .tools(tools)
1215 .build(),
1216 )
1217 
1218 val finalText = mutableListOf<String>()
1219 response.content().forEach { content ->
1220 when {
1221 content.isText() -> finalText.add(content.text().get().text())
1222 
1223 content.isToolUse() -> {
1224 val toolName = content.toolUse().get().name()
1225 val toolArgs =
1226 content.toolUse().get()._input().convert(object : TypeReference<Map<String, JsonValue>>() {})
1227 
1228 val result = mcp.callTool(
1229 name = toolName,
1230 arguments = toolArgs ?: emptyMap(),
1231 )
1232 finalText.add("[Calling tool $toolName with args $toolArgs]")
1233 
1234 messages.add(
1235 MessageParam.builder()
1236 .role(MessageParam.Role.USER)
1237 .content(
1238 result.content
1239 .filterIsInstance<TextContent>()
1240 .joinToString("\n") { it.text }
1241 )
1242 .build(),
1243 )
1244 
1245 val aiResponse = anthropic.messages().create(
1246 MessageCreateParams.builder()
1247 .model("claude-opus-5")
1248 .maxTokens(1024)
1249 .messages(messages)
1250 .build(),
1251 )
1252 
1253 finalText.add(aiResponse.content().first().text().get().text())
1254 }
1255 }
1256 }
1257 
1258 return finalText.joinToString("\n")
12591171 }
1260 ```
1261 
1262 ### Interactive chat
1263 
1264 We'll add the chat loop:
1265 
1266 ```kotlin theme={null}
1267 suspend fun chatLoop() {
1268 println("\nMCP Client Started!")
1269 println("Type your queries or 'quit' to exit.")
1270 
1271 while (true) {
1272 print("\nQuery: ")
1273 val message = readlnOrNull() ?: break
1274 if (message.trim().lowercase() == "quit") break
1275 
1276 try {
1277 val response = processQuery(message)
1278 println("\n$response")
1279 } catch (e: Exception) {
1280 println("\nError: ${e.message}")
1172 println("Connected to server with tools: ${tools.joinToString(", ") { it.tool().get().name() }}")
1173}
1174```
1175 
1176> **JsonObject.toJsonValue() helper**
1177>
1178> This helper converts a kotlinx.serialization `JsonObject` to an Anthropic SDK `JsonValue` using Jackson:
1179>
1180> ```kotlin theme={null}
1181> private fun JsonObject.toJsonValue(): JsonValue {
1182> val mapper = ObjectMapper()
1183> val node = mapper.readTree(this.toString())
1184> return JsonValue.fromJsonNode(node)
1185> }
1186> ```
1187 
1188### Query processing logic
1189 
1190Now let's add the core functionality for processing queries and handling tool calls:
1191 
1192```kotlin theme={null}
1193suspend fun processQuery(query: String): String {
1194 val messages = mutableListOf(
1195 MessageParam.builder()
1196 .role(MessageParam.Role.USER)
1197 .content(query)
1198 .build(),
1199 )
1200 
1201 val response = anthropic.messages().create(
1202 MessageCreateParams.builder()
1203 .model("claude-opus-5")
1204 .maxTokens(1024)
1205 .messages(messages)
1206 .tools(tools)
1207 .build(),
1208 )
1209 
1210 val finalText = mutableListOf<String>()
1211 response.content().forEach { content ->
1212 when {
1213 content.isText() -> finalText.add(content.text().get().text())
1214 
1215 content.isToolUse() -> {
1216 val toolName = content.toolUse().get().name()
1217 val toolArgs =
1218 content.toolUse().get()._input().convert(object : TypeReference<Map<String, JsonValue>>() {})
1219 
1220 val result = mcp.callTool(
1221 name = toolName,
1222 arguments = toolArgs ?: emptyMap(),
1223 )
1224 finalText.add("[Calling tool $toolName with args $toolArgs]")
1225 
1226 messages.add(
1227 MessageParam.builder()
1228 .role(MessageParam.Role.USER)
1229 .content(
1230 result.content
1231 .filterIsInstance<TextContent>()
1232 .joinToString("\n") { it.text }
1233 )
1234 .build(),
1235 )
1236 
1237 val aiResponse = anthropic.messages().create(
1238 MessageCreateParams.builder()
1239 .model("claude-opus-5")
1240 .maxTokens(1024)
1241 .messages(messages)
1242 .build(),
1243 )
1244 
1245 finalText.add(aiResponse.content().first().text().get().text())
12811246 }
12821247 }
12831248 }
1284 ```
1285 
1286 ### Main entry point
1287 
1288 Finally, we'll add the main execution function:
1289 
1290 ```kotlin theme={null}
1291 fun main(args: Array<String>) = runBlocking {
1292 require(args.isNotEmpty()) { "Usage: java -jar <path> <path_to_server_script>" }
1293 
1294 val apiKey = System.getenv("ANTHROPIC_API_KEY")
1295 require(!apiKey.isNullOrBlank()) { "ANTHROPIC_API_KEY environment variable is not set" }
1296 
1297 val client = MCPClient(apiKey)
1298 client.use {
1299 client.connectToServer(args.first())
1300 client.chatLoop()
1249 
1250 return finalText.joinToString("\n")
1251}
1252```
1253 
1254### Interactive chat
1255 
1256We'll add the chat loop:
1257 
1258```kotlin theme={null}
1259suspend fun chatLoop() {
1260 println("\nMCP Client Started!")
1261 println("Type your queries or 'quit' to exit.")
1262 
1263 while (true) {
1264 print("\nQuery: ")
1265 val message = readlnOrNull() ?: break
1266 if (message.trim().lowercase() == "quit") break
1267 
1268 try {
1269 val response = processQuery(message)
1270 println("\n$response")
1271 } catch (e: Exception) {
1272 println("\nError: ${e.message}")
13011273 }
13021274 }
1303 ```
1304 
1305 ## Running the client
1306 
1307 To run your client with any MCP server:
1308 
1309 ```bash theme={null}
1310 ./gradlew build
1311 
1312 # Run the client
1313 java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar path/to/server.jar # JVM server
1314 java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar path/to/server.py # Python server
1315 java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar path/to/build/index.js # Node server
1316 ```
1317 
1318 Alternatively, you can run directly with Gradle:
1319 
1320 ```bash theme={null}
1321 ./gradlew run --args="path/to/server.jar"
1322 ```
1323 
1324 <Note>
1325 If you're continuing the weather tutorial from the server quickstart, your command might look something like this: `java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar .../samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar`
1326 </Note>
1327 
1328 **The client will:**
1329 
1330 1. Connect to the specified server
1331 2. List available tools
1332 3. Start an interactive chat session where you can:
1333 * Enter queries
1334 * See tool executions
1335 * Get responses from Claude
1336 
1337 ## How it works
1338 
1339 Here's a high-level workflow schema:
1340 
1341 ```mermaid theme={null}
1342 ---
1343 config:
1344 theme: neutral
1345 ---
1346 sequenceDiagram
1347 actor User
1348 participant Client
1349 participant Claude
1350 participant MCP_Server as MCP Server
1351 participant Tools
1352 
1353 User->>Client: Send query
1354 Client<<->>MCP_Server: Get available tools
1355 Client->>Claude: Send query with tool descriptions
1356 Claude-->>Client: Decide tool execution
1357 Client->>MCP_Server: Request tool execution
1358 MCP_Server->>Tools: Execute chosen tools
1359 Tools-->>MCP_Server: Return results
1360 MCP_Server-->>Client: Send results
1361 Client->>Claude: Send tool results
1362 Claude-->>Client: Provide final response
1363 Client-->>User: Display response
1364 ```
1365 
1366 When you submit a query:
1367 
1368 1. The client gets the list of available tools from the server
1369 2. Your query is sent to Claude along with tool descriptions
1370 3. Claude decides which tools (if any) to use
1371 4. The client executes any requested tool calls through the server
1372 5. Results are sent back to Claude
1373 6. Claude provides a natural language response
1374 7. The response is displayed to you
1375 
1376 ## Best practices
1377 
1378 1. **Error Handling**
1379 * Leverage Kotlin's type system to model errors explicitly
1380 * Wrap external tool and API calls in `try-catch` blocks when exceptions are possible
1381 * Provide clear and meaningful error messages
1382 * Handle network timeouts and connection issues gracefully
1383 
1384 2. **Security**
1385 * Store API keys and secrets securely in `local.properties`, environment variables, or secret managers
1386 * Validate all external responses to avoid unexpected or unsafe data usage
1387 * Be cautious with permissions and trust boundaries when using tools
1388 
1389 3. **Environment**
1390 * Set `ANTHROPIC_API_KEY` through environment variables rather than hardcoding
1391 * Use `.env` files with appropriate `.gitignore` rules for local development
1392 
1393 ## Troubleshooting
1394 
1395 ### Server Path Issues
1396 
1397 * Double-check the path to your server script is correct
1398 * Use the absolute path if the relative path isn't working
1399 * For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
1400 * Make sure that the required runtime is installed (java for Java, npm for Node.js, or uv for Python)
1401 * Verify the server file has the correct extension (.jar for Java, .js for Node.js or .py for Python)
1402 
1403 Example of correct path usage:
1404 
1405 ```bash theme={null}
1406 # Relative path
1407 java -jar build/libs/client.jar ./server/build/libs/server.jar
1408 
1409 # Absolute path
1410 java -jar build/libs/client.jar /Users/username/projects/mcp-server/build/libs/server.jar
1411 
1412 # Windows path (either format works)
1413 java -jar build/libs/client.jar C:/projects/mcp-server/build/libs/server.jar
1414 java -jar build/libs/client.jar C:\\projects\\mcp-server\\build\\libs\\server.jar
1415 ```
1416 
1417 ### Build Issues
1418 
1419 * Use `./gradlew build` or `./gradlew shadowJar` (not `./gradlew jar`) to create the shadow JAR with all dependencies
1420 * If you get JDK version errors, ensure your installed JDK version matches or exceeds the `jvmToolchain` setting in `build.gradle.kts`
1421 
1422 ### Response Timing
1423 
1424 * The first response might take up to 30 seconds to return
1425 * This is normal and happens while:
1426 * The server initializes
1427 * Claude processes the query
1428 * Tools are being executed
1429 * Subsequent responses are typically faster
1430 * Don't interrupt the process during this initial waiting period
1431 
1432 ### Common Error Messages
1433 
1434 If you see:
1435 
1436 * `Connection refused`: Ensure the server is running and the path is correct
1437 * `Tool execution failed`: Verify the tool's required environment variables are set
1438 * `ANTHROPIC_API_KEY is not set`: Check your environment variables
1439 </Tab>
1440 
1441 <Tab title="C#">
1442 [You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/QuickstartClient)
1443 
1444 ## System Requirements
1445 
1446 Before starting, ensure your system meets these requirements:
1447 
1448 * .NET 8.0 or higher
1449 * Anthropic API key (Claude)
1450 * Windows, Linux, or macOS
1451 
1452 ## Setting up your environment
1453 
1454 First, create a new .NET project:
1455 
1456 ```bash theme={null}
1457 dotnet new console -n QuickstartClient
1458 cd QuickstartClient
1459 ```
1460 
1461 Then, add the required dependencies to your project:
1462 
1463 ```bash theme={null}
1464 dotnet add package ModelContextProtocol --prerelease
1465 dotnet add package Anthropic.SDK
1466 dotnet add package Microsoft.Extensions.Hosting
1467 dotnet add package Microsoft.Extensions.AI
1468 ```
1469 
1470 ## Setting up your API key
1471 
1472 You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
1473 
1474 ```bash theme={null}
1475 dotnet user-secrets init
1476 dotnet user-secrets set "ANTHROPIC_API_KEY" "<your key here>"
1477 ```
1478 
1479 ## Creating the Client
1480 
1481 ### Basic Client Structure
1482 
1483 First, let's setup the basic client class in the file `Program.cs`:
1484 
1485 ```csharp theme={null}
1486 using Anthropic.SDK;
1487 using Microsoft.Extensions.AI;
1488 using Microsoft.Extensions.Configuration;
1489 using Microsoft.Extensions.Hosting;
1490 using ModelContextProtocol.Client;
1491 using ModelContextProtocol.Protocol.Transport;
1492 
1493 var builder = Host.CreateApplicationBuilder(args);
1494 
1495 builder.Configuration
1496 .AddEnvironmentVariables()
1497 .AddUserSecrets<Program>();
1498 ```
1499 
1500 This creates the beginnings of a .NET console application that can read the API key from user secrets.
1501 
1502 Next, we'll setup the MCP Client:
1503 
1504 ```csharp theme={null}
1505 var (command, arguments) = GetCommandAndArguments(args);
1506 
1507 var clientTransport = new StdioClientTransport(new()
1275}
1276```
1277 
1278### Main entry point
1279 
1280Finally, we'll add the main execution function:
1281 
1282```kotlin theme={null}
1283fun main(args: Array<String>) = runBlocking {
1284 require(args.isNotEmpty()) { "Usage: java -jar <path> <path_to_server_script>" }
1285 
1286 val apiKey = System.getenv("ANTHROPIC_API_KEY")
1287 require(!apiKey.isNullOrBlank()) { "ANTHROPIC_API_KEY environment variable is not set" }
1288 
1289 val client = MCPClient(apiKey)
1290 client.use {
1291 client.connectToServer(args.first())
1292 client.chatLoop()
1293 }
1294}
1295```
1296 
1297## Running the client
1298 
1299To run your client with any MCP server:
1300 
1301```bash theme={null}
1302./gradlew build
1303 
1304# Run the client
1305java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar path/to/server.jar # JVM server
1306java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar path/to/server.py # Python server
1307java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar path/to/build/index.js # Node server
1308```
1309 
1310Alternatively, you can run directly with Gradle:
1311 
1312```bash theme={null}
1313./gradlew run --args="path/to/server.jar"
1314```
1315 
1316> [!NOTE]
1317>
1318> If you're continuing the weather tutorial from the server quickstart, your command might look something like this: `java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar .../samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar`
1319 
1320**The client will:**
1321 
13221. Connect to the specified server
13232. List available tools
13243. Start an interactive chat session where you can:
1325 * Enter queries
1326 * See tool executions
1327 * Get responses from Claude
1328 
1329## How it works
1330 
1331Here's a high-level workflow schema:
1332 
1333```mermaid theme={null}
1334---
1335config:
1336 theme: neutral
1337---
1338sequenceDiagram
1339 actor User
1340 participant Client
1341 participant Claude
1342 participant MCP_Server as MCP Server
1343 participant Tools
1344 
1345 User->>Client: Send query
1346 Client<<->>MCP_Server: Get available tools
1347 Client->>Claude: Send query with tool descriptions
1348 Claude-->>Client: Decide tool execution
1349 Client->>MCP_Server: Request tool execution
1350 MCP_Server->>Tools: Execute chosen tools
1351 Tools-->>MCP_Server: Return results
1352 MCP_Server-->>Client: Send results
1353 Client->>Claude: Send tool results
1354 Claude-->>Client: Provide final response
1355 Client-->>User: Display response
1356```
1357 
1358When you submit a query:
1359 
13601. The client gets the list of available tools from the server
13612. Your query is sent to Claude along with tool descriptions
13623. Claude decides which tools (if any) to use
13634. The client executes any requested tool calls through the server
13645. Results are sent back to Claude
13656. Claude provides a natural language response
13667. The response is displayed to you
1367 
1368## Best practices
1369 
13701. **Error Handling**
1371 * Leverage Kotlin's type system to model errors explicitly
1372 * Wrap external tool and API calls in `try-catch` blocks when exceptions are possible
1373 * Provide clear and meaningful error messages
1374 * Handle network timeouts and connection issues gracefully
1375 
13762. **Security**
1377 * Store API keys and secrets securely in `local.properties`, environment variables, or secret managers
1378 * Validate all external responses to avoid unexpected or unsafe data usage
1379 * Be cautious with permissions and trust boundaries when using tools
1380 
13813. **Environment**
1382 * Set `ANTHROPIC_API_KEY` through environment variables rather than hardcoding
1383 * Use `.env` files with appropriate `.gitignore` rules for local development
1384 
1385## Troubleshooting
1386 
1387### Server Path Issues
1388 
1389* Double-check the path to your server script is correct
1390* Use the absolute path if the relative path isn't working
1391* For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
1392* Make sure that the required runtime is installed (java for Java, npm for Node.js, or uv for Python)
1393* Verify the server file has the correct extension (.jar for Java, .js for Node.js or .py for Python)
1394 
1395Example of correct path usage:
1396 
1397```bash theme={null}
1398# Relative path
1399java -jar build/libs/client.jar ./server/build/libs/server.jar
1400 
1401# Absolute path
1402java -jar build/libs/client.jar /Users/username/projects/mcp-server/build/libs/server.jar
1403 
1404# Windows path (either format works)
1405java -jar build/libs/client.jar C:/projects/mcp-server/build/libs/server.jar
1406java -jar build/libs/client.jar C:\\projects\\mcp-server\\build\\libs\\server.jar
1407```
1408 
1409### Build Issues
1410 
1411* Use `./gradlew build` or `./gradlew shadowJar` (not `./gradlew jar`) to create the shadow JAR with all dependencies
1412* If you get JDK version errors, ensure your installed JDK version matches or exceeds the `jvmToolchain` setting in `build.gradle.kts`
1413 
1414### Response Timing
1415 
1416* The first response might take up to 30 seconds to return
1417* This is normal and happens while:
1418 * The server initializes
1419 * Claude processes the query
1420 * Tools are being executed
1421* Subsequent responses are typically faster
1422* Don't interrupt the process during this initial waiting period
1423 
1424### Common Error Messages
1425 
1426If you see:
1427 
1428* `Connection refused`: Ensure the server is running and the path is correct
1429* `Tool execution failed`: Verify the tool's required environment variables are set
1430* `ANTHROPIC_API_KEY is not set`: Check your environment variables
1431 
1432**C#**
1433 
1434[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/QuickstartClient)
1435 
1436## System Requirements
1437 
1438Before starting, ensure your system meets these requirements:
1439 
1440* .NET 8.0 or higher
1441* Anthropic API key (Claude)
1442* Windows, Linux, or macOS
1443 
1444## Setting up your environment
1445 
1446First, create a new .NET project:
1447 
1448```bash theme={null}
1449dotnet new console -n QuickstartClient
1450cd QuickstartClient
1451```
1452 
1453Then, add the required dependencies to your project:
1454 
1455```bash theme={null}
1456dotnet add package ModelContextProtocol --prerelease
1457dotnet add package Anthropic.SDK
1458dotnet add package Microsoft.Extensions.Hosting
1459dotnet add package Microsoft.Extensions.AI
1460```
1461 
1462## Setting up your API key
1463 
1464You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
1465 
1466```bash theme={null}
1467dotnet user-secrets init
1468dotnet user-secrets set "ANTHROPIC_API_KEY" "<your key here>"
1469```
1470 
1471## Creating the Client
1472 
1473### Basic Client Structure
1474 
1475First, let's setup the basic client class in the file `Program.cs`:
1476 
1477```csharp theme={null}
1478using Anthropic.SDK;
1479using Microsoft.Extensions.AI;
1480using Microsoft.Extensions.Configuration;
1481using Microsoft.Extensions.Hosting;
1482using ModelContextProtocol.Client;
1483using ModelContextProtocol.Protocol.Transport;
1484 
1485var builder = Host.CreateApplicationBuilder(args);
1486 
1487builder.Configuration
1488 .AddEnvironmentVariables()
1489 .AddUserSecrets<Program>();
1490```
1491 
1492This creates the beginnings of a .NET console application that can read the API key from user secrets.
1493 
1494Next, we'll setup the MCP Client:
1495 
1496```csharp theme={null}
1497var (command, arguments) = GetCommandAndArguments(args);
1498 
1499var clientTransport = new StdioClientTransport(new()
1500{
1501 Name = "Demo Server",
1502 Command = command,
1503 Arguments = arguments,
1504});
1505 
1506await using var mcpClient = await McpClient.CreateAsync(clientTransport);
1507 
1508var tools = await mcpClient.ListToolsAsync();
1509foreach (var tool in tools)
1510{
1511 Console.WriteLine($"Connected to server with tools: {tool.Name}");
1512}
1513```
1514 
1515Add this function at the end of the `Program.cs` file:
1516 
1517```csharp theme={null}
1518static (string command, string[] arguments) GetCommandAndArguments(string[] args)
1519{
1520 return args switch
15081521 {
1509 Name = "Demo Server",
1510 Command = command,
1511 Arguments = arguments,
1512 });
1513 
1514 await using var mcpClient = await McpClient.CreateAsync(clientTransport);
1515 
1516 var tools = await mcpClient.ListToolsAsync();
1517 foreach (var tool in tools)
1522 [var script] when script.EndsWith(".py") => ("python", args),
1523 [var script] when script.EndsWith(".js") => ("node", args),
1524 [var script] when Directory.Exists(script) || (File.Exists(script) && script.EndsWith(".csproj")) => ("dotnet", ["run", "--project", script, "--no-build"]),
1525 _ => throw new NotSupportedException("An unsupported server script was provided. Supported scripts are .py, .js, or .csproj")
1526 };
1527}
1528```
1529 
1530This creates an MCP client that will connect to a server that is provided as a command line argument. It then lists the available tools from the connected server.
1531 
1532### Query processing logic
1533 
1534Now let's add the core functionality for processing queries and handling tool calls:
1535 
1536```csharp theme={null}
1537using var anthropicClient = new AnthropicClient(new APIAuthentication(builder.Configuration["ANTHROPIC_API_KEY"]))
1538 .Messages
1539 .AsBuilder()
1540 .UseFunctionInvocation()
1541 .Build();
1542 
1543var options = new ChatOptions
1544{
1545 MaxOutputTokens = 1000,
1546 ModelId = "claude-opus-5",
1547 Tools = [.. tools]
1548};
1549 
1550Console.ForegroundColor = ConsoleColor.Green;
1551Console.WriteLine("MCP Client Started!");
1552Console.ResetColor();
1553 
1554PromptForInput();
1555while(Console.ReadLine() is string query && !"exit".Equals(query, StringComparison.OrdinalIgnoreCase))
1556{
1557 if (string.IsNullOrWhiteSpace(query))
15181558 {
1519 Console.WriteLine($"Connected to server with tools: {tool.Name}");
1559 PromptForInput();
1560 continue;
15201561 }
1521 ```
1522 
1523 Add this function at the end of the `Program.cs` file:
1524 
1525 ```csharp theme={null}
1526 static (string command, string[] arguments) GetCommandAndArguments(string[] args)
1562 
1563 await foreach (var message in anthropicClient.GetStreamingResponseAsync(query, options))
15271564 {
1528 return args switch
1529 {
1530 [var script] when script.EndsWith(".py") => ("python", args),
1531 [var script] when script.EndsWith(".js") => ("node", args),
1532 [var script] when Directory.Exists(script) || (File.Exists(script) && script.EndsWith(".csproj")) => ("dotnet", ["run", "--project", script, "--no-build"]),
1533 _ => throw new NotSupportedException("An unsupported server script was provided. Supported scripts are .py, .js, or .csproj")
1534 };
1565 Console.Write(message);
15351566 }
1536 ```
1537 
1538 This creates an MCP client that will connect to a server that is provided as a command line argument. It then lists the available tools from the connected server.
1539 
1540 ### Query processing logic
1541 
1542 Now let's add the core functionality for processing queries and handling tool calls:
1543 
1544 ```csharp theme={null}
1545 using var anthropicClient = new AnthropicClient(new APIAuthentication(builder.Configuration["ANTHROPIC_API_KEY"]))
1546 .Messages
1547 .AsBuilder()
1548 .UseFunctionInvocation()
1549 .Build();
1550 
1551 var options = new ChatOptions
1552 {
1553 MaxOutputTokens = 1000,
1554 ModelId = "claude-opus-5",
1555 Tools = [.. tools]
1556 };
1557 
1558 Console.ForegroundColor = ConsoleColor.Green;
1559 Console.WriteLine("MCP Client Started!");
1567 Console.WriteLine();
1568 
1569 PromptForInput();
1570}
1571 
1572static void PromptForInput()
1573{
1574 Console.WriteLine("Enter a command (or 'exit' to quit):");
1575 Console.ForegroundColor = ConsoleColor.Cyan;
1576 Console.Write("> ");
15601577 Console.ResetColor();
1561 
1562 PromptForInput();
1563 while(Console.ReadLine() is string query && !"exit".Equals(query, StringComparison.OrdinalIgnoreCase))
1564 {
1565 if (string.IsNullOrWhiteSpace(query))
1566 {
1567 PromptForInput();
1568 continue;
1569 }
1570 
1571 await foreach (var message in anthropicClient.GetStreamingResponseAsync(query, options))
1572 {
1573 Console.Write(message);
1574 }
1575 Console.WriteLine();
1576 
1577 PromptForInput();
1578 }
1579 
1580 static void PromptForInput()
1581 {
1582 Console.WriteLine("Enter a command (or 'exit' to quit):");
1583 Console.ForegroundColor = ConsoleColor.Cyan;
1584 Console.Write("> ");
1585 Console.ResetColor();
1586 }
1587 ```
1588 
1589 ## Key Components Explained
1590 
1591 ### 1. Client Initialization
1592 
1593 * The client is initialized using `McpClient.CreateAsync()`, which sets up the transport type and command to run the server.
1594 
1595 ### 2. Server Connection
1596 
1597 * Supports Python, Node.js, and .NET servers.
1598 * The server is started using the command specified in the arguments.
1599 * Configures to use stdio for communication with the server.
1600 * Initializes the session and available tools.
1601 
1602 ### 3. Query Processing
1603 
1604 * Leverages [Microsoft.Extensions.AI](https://learn.microsoft.com/dotnet/ai/ai-extensions) for the chat client.
1605 * Configures the `IChatClient` to use automatic tool (function) invocation.
1606 * The client reads user input and sends it to the server.
1607 * The server processes the query and returns a response.
1608 * The response is displayed to the user.
1609 
1610 ## Running the Client
1611 
1612 To run your client with any MCP server:
1613 
1614 ```bash theme={null}
1615 dotnet run -- path/to/server.csproj # dotnet server
1616 dotnet run -- path/to/server.py # python server
1617 dotnet run -- path/to/server.js # node server
1618 ```
1619 
1620 <Note>
1621 If you're continuing the weather tutorial from the server quickstart, your command might look something like this: `dotnet run -- path/to/QuickstartWeatherServer`.
1622 </Note>
1623 
1624 The client will:
1625 
1626 1. Connect to the specified server
1627 2. List available tools
1628 3. Start an interactive chat session where you can:
1629 * Enter queries
1630 * See tool executions
1631 * Get responses from Claude
1632 4. Exit the session when done
1633 
1634 Here's an example of what it should look like if connected to the weather server quickstart:
1635 
1636 <Frame>
1637 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-dotnet-client.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=fcf28dde150d6db879402ad8150c6b23" width="1115" height="666" data-path="images/quickstart-dotnet-client.png" />
1638 </Frame>
1639 </Tab>
1640 
1641 <Tab title="Ruby">
1642 [You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-ruby)
1643 
1644 ## System Requirements
1645 
1646 Before starting, ensure your system meets these requirements:
1647 
1648 * Mac or Windows computer
1649 * Ruby 3.2.0 or higher installed (required by the [Anthropic SDK](https://github.com/anthropics/anthropic-sdk-ruby))
1650 * Anthropic API key (Claude)
1651 
1652 ## Setting Up Your Environment
1653 
1654 First, create a new Ruby project:
1655 
1656 <CodeGroup>
1657 ```bash macOS/Linux theme={null}
1658 # Create project directory
1659 mkdir mcp-client
1660 cd mcp-client
1661 
1662 # Create a Gemfile
1663 bundle init
1664 
1665 # Add required dependencies
1666 bundle add anthropic dotenv mcp
1667 
1668 # Create our main file
1669 touch client.rb
1670 ```
1671 
1672 ```powershell Windows theme={null}
1673 # Create project directory
1674 mkdir mcp-client
1675 cd mcp-client
1676 
1677 # Create a Gemfile
1678 bundle init
1679 
1680 # Add required dependencies
1681 bundle add anthropic base64 dotenv mcp
1682 
1683 # Create our main file
1684 new-item client.rb
1685 ```
1686 </CodeGroup>
1687 
1688 ## Setting Up Your API Key
1689 
1690 You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
1691 
1692 Create a `.env` file to store it:
1693 
1694 ```bash theme={null}
1695 echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
1696 ```
1697 
1698 Add `.env` to your `.gitignore`:
1699 
1700 ```bash theme={null}
1701 echo ".env" >> .gitignore
1702 ```
1703 
1704 <Warning>
1705 Make sure you keep your `ANTHROPIC_API_KEY` secure!
1706 </Warning>
1707 
1708 ## Creating the Client
1709 
1710 ### Basic Client Structure
1711 
1712 First, let's set up our requires and create the basic client class:
1713 
1714 ```ruby theme={null}
1715 require "anthropic"
1716 require "dotenv/load"
1717 require "json"
1718 require "mcp"
1719 
1720 class MCPClient
1721 ANTHROPIC_MODEL = "claude-opus-5"
1722 
1723 def initialize
1724 @mcp_client = nil
1725 @transport = nil
1726 @anthropic_client = nil
1727 end
1728 
1729 # methods will go here
1730 end
1731 ```
1732 
1733 ### Server Connection Management
1734 
1735 Next, we'll implement the method to connect to an MCP server:
1736 
1737 ```ruby theme={null}
1738 def connect_to_server(server_script_path)
1739 command = case File.extname(server_script_path)
1740 when ".rb"
1741 "ruby"
1742 when ".py"
1743 "python3"
1744 when ".js"
1745 "node"
1746 else
1747 raise ArgumentError, "Server script must be a .rb, .py, or .js file."
1748 end
1749 
1750 @transport = MCP::Client::Stdio.new(command: command, args: [server_script_path])
1751 @mcp_client = MCP::Client.new(transport: @transport)
1752 @mcp_client.connect
1753 
1754 tool_names = @mcp_client.tools.map(&:name)
1755 puts "\nConnected to server with tools: #{tool_names}"
1756 end
1757 ```
1758 
1759 ### Query Processing Logic
1760 
1761 Now let's add the core functionality for processing queries and handling tool calls:
1762 
1763 ```ruby theme={null}
1764 private
1765 
1766 def process_query(query)
1767 messages = [{ role: "user", content: query }]
1768 
1769 available_tools = @mcp_client.tools.map do |tool|
1770 { name: tool.name, description: tool.description, input_schema: tool.input_schema }
1771 end
1772 
1773 # Initial Claude API call.
1774 response = chat(messages, tools: available_tools)
1775 
1776 # Process response and handle tool calls.
1777 if response.content.any?(Anthropic::Models::ToolUseBlock)
1778 assistant_content = response.content.filter_map do |content_block|
1779 case content_block
1780 when Anthropic::Models::TextBlock
1781 { type: "text", text: content_block.text }
1782 when Anthropic::Models::ToolUseBlock
1783 { type: "tool_use", id: content_block.id, name: content_block.name, input: content_block.input }
1784 end
1785 end
1786 messages << { role: "assistant", content: assistant_content }
1787 end
1788 
1789 response.content.each_with_object([]) do |content, response_parts|
1790 case content
1791 when Anthropic::Models::TextBlock
1792 response_parts << content.text
1793 when Anthropic::Models::ToolUseBlock
1794 # Execute tool call via MCP.
1795 result = @mcp_client.call_tool(name: content.name, arguments: content.input)
1796 response_parts << "[Calling tool #{content.name} with args #{content.input.to_json}]"
1797 
1798 tool_result_content = result.dig("result", "content")
1799 result_text = if tool_result_content.is_a?(Array)
1800 tool_result_content.filter_map { |content_item| content_item["text"] }.join("\n")
1801 else
1802 tool_result_content.to_s
1803 end
1804 
1805 messages << {
1806 role: "user",
1807 content: [{
1808 type: "tool_result",
1809 tool_use_id: content.id,
1810 content: result_text
1811 }]
1812 }
1813 
1814 # Get next response from Claude.
1815 response = chat(messages)
1816 
1817 response.content.each do |content_block|
1818 response_parts << content_block.text if content_block.is_a?(Anthropic::Models::TextBlock)
1819 end
1820 end
1821 end.join("\n")
1822 end
1823 
1824 def chat(messages, tools: nil)
1825 params = { model: ANTHROPIC_MODEL, max_tokens: 1000, messages: messages }
1826 params[:tools] = tools if tools
1827 
1828 anthropic_client.messages.create(**params)
1829 end
1830 
1831 def anthropic_client
1832 @anthropic_client ||= Anthropic::Client.new(api_key: ENV["ANTHROPIC_API_KEY"])
1833 end
1834 ```
1835 
1836 ### Interactive Chat Interface
1837 
1838 Now we'll add the chat loop and cleanup functionality:
1839 
1840 ```ruby theme={null}
1841 def chat_loop
1842 puts <<~MESSAGE
1843 MCP Client Started!
1844 Type your queries or 'quit' to exit.
1845 MESSAGE
1846 
1847 loop do
1848 print "\nQuery: "
1849 line = $stdin.gets
1850 break if line.nil?
1851 
1852 query = line.chomp.strip
1853 break if query.downcase == "quit"
1854 next if query.empty?
1855 
1856 begin
1857 response = process_query(query)
1858 puts "\n#{response}"
1859 rescue => e
1860 puts "\nError: #{e.message}"
1861 end
1578}
1579```
1580 
1581## Key Components Explained
1582 
1583### 1. Client Initialization
1584 
1585* The client is initialized using `McpClient.CreateAsync()`, which sets up the transport type and command to run the server.
1586 
1587### 2. Server Connection
1588 
1589* Supports Python, Node.js, and .NET servers.
1590* The server is started using the command specified in the arguments.
1591* Configures to use stdio for communication with the server.
1592* Initializes the session and available tools.
1593 
1594### 3. Query Processing
1595 
1596* Leverages [Microsoft.Extensions.AI](https://learn.microsoft.com/dotnet/ai/ai-extensions) for the chat client.
1597* Configures the `IChatClient` to use automatic tool (function) invocation.
1598* The client reads user input and sends it to the server.
1599* The server processes the query and returns a response.
1600* The response is displayed to the user.
1601 
1602## Running the Client
1603 
1604To run your client with any MCP server:
1605 
1606```bash theme={null}
1607dotnet run -- path/to/server.csproj # dotnet server
1608dotnet run -- path/to/server.py # python server
1609dotnet run -- path/to/server.js # node server
1610```
1611 
1612> [!NOTE]
1613>
1614> If you're continuing the weather tutorial from the server quickstart, your command might look something like this: `dotnet run -- path/to/QuickstartWeatherServer`.
1615 
1616The client will:
1617 
16181. Connect to the specified server
16192. List available tools
16203. Start an interactive chat session where you can:
1621 * Enter queries
1622 * See tool executions
1623 * Get responses from Claude
16244. Exit the session when done
1625 
1626Here's an example of what it should look like if connected to the weather server quickstart:
1627 
1628![](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-dotnet-client.png?fit=max\&auto=format\&n=4ZXF1PrDkEaJvXpn\&q=85\&s=fcf28dde150d6db879402ad8150c6b23)
1629 
1630**Ruby**
1631 
1632[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-ruby)
1633 
1634## System Requirements
1635 
1636Before starting, ensure your system meets these requirements:
1637 
1638* Mac or Windows computer
1639* Ruby 3.2.0 or higher installed (required by the [Anthropic SDK](https://github.com/anthropics/anthropic-sdk-ruby))
1640* Anthropic API key (Claude)
1641 
1642## Setting Up Your Environment
1643 
1644First, create a new Ruby project:
1645 
1646```bash macOS/Linux theme={null}
1647# Create project directory
1648mkdir mcp-client
1649cd mcp-client
1650 
1651# Create a Gemfile
1652bundle init
1653 
1654# Add required dependencies
1655bundle add anthropic dotenv mcp
1656 
1657# Create our main file
1658touch client.rb
1659```
1660 
1661```powershell Windows theme={null}
1662# Create project directory
1663mkdir mcp-client
1664cd mcp-client
1665 
1666# Create a Gemfile
1667bundle init
1668 
1669# Add required dependencies
1670bundle add anthropic base64 dotenv mcp
1671 
1672# Create our main file
1673new-item client.rb
1674```
1675 
1676## Setting Up Your API Key
1677 
1678You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
1679 
1680Create a `.env` file to store it:
1681 
1682```bash theme={null}
1683echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
1684```
1685 
1686Add `.env` to your `.gitignore`:
1687 
1688```bash theme={null}
1689echo ".env" >> .gitignore
1690```
1691 
1692> [!WARNING]
1693>
1694> Make sure you keep your `ANTHROPIC_API_KEY` secure!
1695 
1696## Creating the Client
1697 
1698### Basic Client Structure
1699 
1700First, let's set up our requires and create the basic client class:
1701 
1702```ruby theme={null}
1703require "anthropic"
1704require "dotenv/load"
1705require "json"
1706require "mcp"
1707 
1708class MCPClient
1709 ANTHROPIC_MODEL = "claude-opus-5"
1710 
1711 def initialize
1712 @mcp_client = nil
1713 @transport = nil
1714 @anthropic_client = nil
1715 end
1716 
1717 # methods will go here
1718end
1719```
1720 
1721### Server Connection Management
1722 
1723Next, we'll implement the method to connect to an MCP server:
1724 
1725```ruby theme={null}
1726def connect_to_server(server_script_path)
1727 command = case File.extname(server_script_path)
1728 when ".rb"
1729 "ruby"
1730 when ".py"
1731 "python3"
1732 when ".js"
1733 "node"
1734 else
1735 raise ArgumentError, "Server script must be a .rb, .py, or .js file."
1736 end
1737 
1738 @transport = MCP::Client::Stdio.new(command: command, args: [server_script_path])
1739 @mcp_client = MCP::Client.new(transport: @transport)
1740 @mcp_client.connect
1741 
1742 tool_names = @mcp_client.tools.map(&:name)
1743 puts "\nConnected to server with tools: #{tool_names}"
1744end
1745```
1746 
1747### Query Processing Logic
1748 
1749Now let's add the core functionality for processing queries and handling tool calls:
1750 
1751```ruby theme={null}
1752private
1753 
1754def process_query(query)
1755 messages = [{ role: "user", content: query }]
1756 
1757 available_tools = @mcp_client.tools.map do |tool|
1758 { name: tool.name, description: tool.description, input_schema: tool.input_schema }
1759 end
1760 
1761 # Initial Claude API call.
1762 response = chat(messages, tools: available_tools)
1763 
1764 # Process response and handle tool calls.
1765 if response.content.any?(Anthropic::Models::ToolUseBlock)
1766 assistant_content = response.content.filter_map do |content_block|
1767 case content_block
1768 when Anthropic::Models::TextBlock
1769 { type: "text", text: content_block.text }
1770 when Anthropic::Models::ToolUseBlock
1771 { type: "tool_use", id: content_block.id, name: content_block.name, input: content_block.input }
18621772 end
18631773 end
1864 
1865 def cleanup
1866 @transport&.close
1774 messages << { role: "assistant", content: assistant_content }
1775 end
1776 
1777 response.content.each_with_object([]) do |content, response_parts|
1778 case content
1779 when Anthropic::Models::TextBlock
1780 response_parts << content.text
1781 when Anthropic::Models::ToolUseBlock
1782 # Execute tool call via MCP.
1783 result = @mcp_client.call_tool(name: content.name, arguments: content.input)
1784 response_parts << "[Calling tool #{content.name} with args #{content.input.to_json}]"
1785 
1786 tool_result_content = result.dig("result", "content")
1787 result_text = if tool_result_content.is_a?(Array)
1788 tool_result_content.filter_map { |content_item| content_item["text"] }.join("\n")
1789 else
1790 tool_result_content.to_s
1791 end
1792 
1793 messages << {
1794 role: "user",
1795 content: [{
1796 type: "tool_result",
1797 tool_use_id: content.id,
1798 content: result_text
1799 }]
1800 }
1801 
1802 # Get next response from Claude.
1803 response = chat(messages)
1804 
1805 response.content.each do |content_block|
1806 response_parts << content_block.text if content_block.is_a?(Anthropic::Models::TextBlock)
1807 end
18671808 end
1868 ```
1869 
1870 ### Main Entry Point
1871 
1872 Finally, we'll add the main execution logic:
1873 
1874 ```ruby theme={null}
1875 if ARGV.empty?
1876 puts "Usage: ruby client.rb <path_to_server_script>"
1877 exit 1
1809 end.join("\n")
1810end
1811 
1812def chat(messages, tools: nil)
1813 params = { model: ANTHROPIC_MODEL, max_tokens: 1000, messages: messages }
1814 params[:tools] = tools if tools
1815 
1816 anthropic_client.messages.create(**params)
1817end
1818 
1819def anthropic_client
1820 @anthropic_client ||= Anthropic::Client.new(api_key: ENV["ANTHROPIC_API_KEY"])
1821end
1822```
1823 
1824### Interactive Chat Interface
1825 
1826Now we'll add the chat loop and cleanup functionality:
1827 
1828```ruby theme={null}
1829def chat_loop
1830 puts <<~MESSAGE
1831 MCP Client Started!
1832 Type your queries or 'quit' to exit.
1833 MESSAGE
1834 
1835 loop do
1836 print "\nQuery: "
1837 line = $stdin.gets
1838 break if line.nil?
1839 
1840 query = line.chomp.strip
1841 break if query.downcase == "quit"
1842 next if query.empty?
1843 
1844 begin
1845 response = process_query(query)
1846 puts "\n#{response}"
1847 rescue => e
1848 puts "\nError: #{e.message}"
18781849 end
1879 
1880 client = MCPClient.new
1881 
1882 begin
1883 client.connect_to_server(ARGV[0])
1884 
1885 api_key = ENV["ANTHROPIC_API_KEY"]
1886 if api_key.nil? || api_key.empty?
1887 puts <<~MESSAGE
1888 No ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:
1889 export ANTHROPIC_API_KEY=your-api-key-here
1890 MESSAGE
1891 exit
1892 end
1893 
1894 client.chat_loop
1895 rescue => e
1896 puts "Error: #{e.message}"
1897 exit 1
1898 ensure
1899 client.cleanup
1900 end
1901 ```
1902 
1903 You can find the complete `client.rb` file [here](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-ruby/client.rb).
1904 
1905 ## Key Components Explained
1906 
1907 ### 1. Client Initialization
1908 
1909 * The `MCPClient` class initializes with nil references for lazy setup
1910 * The Anthropic client is lazily initialized via the `anthropic_client` method
1911 * Uses `dotenv` to load environment variables from `.env`
1912 
1913 ### 2. Server Connection
1914 
1915 * Supports Ruby, Python, and Node.js servers
1916 * Uses `File.extname` to determine the server script type
1917 * Uses `MCP::Client::Stdio` for stdio transport
1918 * Initializes the MCP client and lists available tools
1919 
1920 ### 3. Query Processing
1921 
1922 * Maps MCP tools to Anthropic tool format (`name`, `description`, `input_schema`)
1923 * Uses `Anthropic::Models::TextBlock` and `Anthropic::Models::ToolUseBlock` for pattern matching
1924 * Builds assistant content once before iterating tool calls
1925 * Executes tool calls via `@mcp_client.call_tool`
1926 * Uses `chat` helper method to wrap Anthropic API calls
1927 * Extracts tool result content with `result.dig("result", "content")`
1928 * Passes tool results back to Claude for a final response
1929 
1930 ### 4. Interactive Interface
1931 
1932 * Provides a simple command-line interface
1933 * Handles user input and displays responses
1934 * Skips empty queries
1935 * Includes basic error handling
1936 
1937 ### 5. Resource Management
1938 
1939 * Proper cleanup of the transport via `begin`...`ensure`
1940 * Top-level `rescue` for error handling
1941 * API key validation after server connection
1942 
1943 ## Running the Client
1944 
1945 To run your client with any MCP server:
1946 
1947 ```bash theme={null}
1948 bundle exec ruby client.rb path/to/server.rb # ruby server
1949 bundle exec ruby client.rb path/to/server.py # python server
1950 bundle exec ruby client.rb path/to/build/index.js # node server
1951 ```
1952 
1953 <Note>
1954 If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-ruby), your command might look something like this: `bundle exec ruby client.rb /path/to/weather-server-ruby/weather.rb`
1955 </Note>
1956 
1957 The client will:
1958 
1959 1. Connect to the specified server
1960 2. List available tools
1961 3. Start an interactive chat session where you can:
1962 * Enter queries
1963 * See tool executions
1964 * Get responses from Claude
1965 
1966 ## How It Works
1967 
1968 When you submit a query:
1969 
1970 1. The client gets the list of available tools from the server
1971 2. Your query is sent to Claude along with tool descriptions
1972 3. Claude decides which tools (if any) to use
1973 4. The client executes any requested tool calls through the server
1974 5. Results are sent back to Claude
1975 6. Claude provides a natural language response
1976 7. The response is displayed to you
1977 
1978 ## Best practices
1979 
1980 1. **Error Handling**
1981 * Wrap tool calls in `begin`...`rescue` blocks
1982 * Provide meaningful error messages
1983 * Gracefully handle connection issues
1984 
1985 2. **Resource Management**
1986 * Always close the transport when done
1987 * Use `begin`...`ensure` for proper cleanup
1988 * Handle server disconnections
1989 
1990 3. **Security**
1991 * Store API keys securely in `.env`
1992 * Validate server responses
1993 * Be cautious with tool permissions
1994 
1995 4. **Tool Names**
1996 * Tool names can be validated according to the format specified [here](/specification/draft/server/tools#tool-names)
1997 * If a tool name conforms to the specified format, it should not fail validation by an MCP client
1998 
1999 ## Troubleshooting
2000 
2001 ### Server Path Issues
2002 
2003 * Double-check the path to your server script is correct
2004 * Use the absolute path if the relative path isn't working
2005 * For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
2006 * Verify the server file has the correct extension (.py for Python, .js for Node.js, or .rb for Ruby)
2007 
2008 Example of correct path usage:
2009 
2010 ```bash theme={null}
2011 # Relative path
2012 bundle exec ruby client.rb ./server/weather.rb
2013 
2014 # Absolute path
2015 bundle exec ruby client.rb /Users/username/projects/mcp-server/weather.rb
2016 
2017 # Windows path (either format works)
2018 bundle exec ruby client.rb C:/projects/mcp-server/weather.rb
2019 bundle exec ruby client.rb C:\\projects\\mcp-server\\weather.rb
2020 ```
2021 
2022 ### Response Timing
2023 
2024 * The first response might take up to 30 seconds to return
2025 * This is normal and happens while:
2026 * The server initializes
2027 * Claude processes the query
2028 * Tools are being executed
2029 * Subsequent responses are typically faster
2030 * Don't interrupt the process during this initial waiting period
2031 
2032 ### Common Error Messages
2033 
2034 If you see:
2035 
2036 * `Errno::ENOENT`: Check your server path and ensure the command (`ruby`, `python3`, `node`) is available
2037 * `Connection refused`: Ensure the server is running and the path is correct
2038 * `Tool execution failed`: Verify the tool's required environment variables are set
2039 * `Anthropic::Errors::AuthenticationError`: Check your `.env` file has a valid `ANTHROPIC_API_KEY`
2040 </Tab>
2041 
2042 <Tab title="Rust">
2043 [You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-rust)
2044 
2045 ## System Requirements
2046 
2047 Before starting, ensure your Linux system meets these requirements:
2048 
2049 * Latest stable version of [Rust and Cargo](https://www.rust-lang.org/tools/install)
2050 * Anthropic API key (Claude)
2051 * A Python, Node.js, or executable MCP server to connect to
2052 
2053 ## Setting Up Your Environment
2054 
2055 First, create a new Rust project:
2056 
2057 ```bash theme={null}
2058 cargo new mcp-client-rust
2059 cd mcp-client-rust
2060 ```
2061 
2062 Replace the contents of `Cargo.toml` with the following:
2063 
2064 ```toml Cargo.toml theme={null}
2065 [package]
2066 name = "mcp-client-rust"
2067 version = "0.1.0"
2068 edition = "2024"
2069 
2070 [dependencies]
2071 anyhow = "1.0.100"
2072 genai = "0.4.2"
2073 rmcp = { version = "0.8.0", features = ["server", "client", "transport-io", "transport-child-process"] }
2074 tokio = { version = "1.47.1", features = ["full"] }
2075 tracing = "0.1.41"
2076 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
2077 serde_json = "1.0.128"
2078 dotenvy = "0.15.7"
2079 reqwest = "0.12.23"
2080 ```
2081 
2082 The [`rmcp`](https://github.com/modelcontextprotocol/rust-sdk) crate provides the Rust MCP SDK and child-process transport. This example uses the [`genai`](https://github.com/jeremychone/rust-genai) crate to send requests to Claude and represent tools in the model request.
2083 
2084 ## Setting Up Your API Key
2085 
2086 You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
2087 
2088 Create a `.env` file to store it:
2089 
2090 ```bash theme={null}
2091 echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
2092 ```
2093 
2094 Add `.env` to your `.gitignore`:
2095 
2096 ```bash theme={null}
2097 echo ".env" >> .gitignore
2098 ```
2099 
2100 <Warning>
2101 Make sure you keep your `ANTHROPIC_API_KEY` secure!
2102 </Warning>
2103 
2104 ## Creating the Client
2105 
2106 Open `src/main.rs` and replace its contents as you work through the following sections.
2107 
2108 ### Imports and Client Structure
2109 
2110 First, add the imports, model constant, and basic client structure:
2111 
2112 ```rust theme={null}
2113 use anyhow::{Context, Result, bail};
2114 use genai::Client;
2115 use genai::chat::{
2116 ChatMessage, ChatRequest, ChatResponse, ContentPart, Tool as GenaiTool, ToolResponse,
2117 };
2118 use rmcp::model::{CallToolRequestParam, Tool as McpTool};
2119 use rmcp::service::{RoleClient, RunningService, ServiceExt};
2120 use rmcp::transport::TokioChildProcess;
2121 use serde_json::Value;
2122 use tokio::io::{self, AsyncBufReadExt, BufReader};
2123 use tokio::process::Command;
2124 
2125 const MODEL_ANTHROPIC: &str = "claude-opus-5";
2126 
2127 struct MCPClient {
2128 anthropic: Client,
2129 session: Option<RunningService<RoleClient, ()>>,
2130 tools: Vec<GenaiTool>,
1850 end
1851end
1852 
1853def cleanup
1854 @transport&.close
1855end
1856```
1857 
1858### Main Entry Point
1859 
1860Finally, we'll add the main execution logic:
1861 
1862```ruby theme={null}
1863if ARGV.empty?
1864 puts "Usage: ruby client.rb <path_to_server_script>"
1865 exit 1
1866end
1867 
1868client = MCPClient.new
1869 
1870begin
1871 client.connect_to_server(ARGV[0])
1872 
1873 api_key = ENV["ANTHROPIC_API_KEY"]
1874 if api_key.nil? || api_key.empty?
1875 puts <<~MESSAGE
1876 No ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:
1877 export ANTHROPIC_API_KEY=your-api-key-here
1878 MESSAGE
1879 exit
1880 end
1881 
1882 client.chat_loop
1883rescue => e
1884 puts "Error: #{e.message}"
1885 exit 1
1886ensure
1887 client.cleanup
1888end
1889```
1890 
1891You can find the complete `client.rb` file [here](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-ruby/client.rb).
1892 
1893## Key Components Explained
1894 
1895### 1. Client Initialization
1896 
1897* The `MCPClient` class initializes with nil references for lazy setup
1898* The Anthropic client is lazily initialized via the `anthropic_client` method
1899* Uses `dotenv` to load environment variables from `.env`
1900 
1901### 2. Server Connection
1902 
1903* Supports Ruby, Python, and Node.js servers
1904* Uses `File.extname` to determine the server script type
1905* Uses `MCP::Client::Stdio` for stdio transport
1906* Initializes the MCP client and lists available tools
1907 
1908### 3. Query Processing
1909 
1910* Maps MCP tools to Anthropic tool format (`name`, `description`, `input_schema`)
1911* Uses `Anthropic::Models::TextBlock` and `Anthropic::Models::ToolUseBlock` for pattern matching
1912* Builds assistant content once before iterating tool calls
1913* Executes tool calls via `@mcp_client.call_tool`
1914* Uses `chat` helper method to wrap Anthropic API calls
1915* Extracts tool result content with `result.dig("result", "content")`
1916* Passes tool results back to Claude for a final response
1917 
1918### 4. Interactive Interface
1919 
1920* Provides a simple command-line interface
1921* Handles user input and displays responses
1922* Skips empty queries
1923* Includes basic error handling
1924 
1925### 5. Resource Management
1926 
1927* Proper cleanup of the transport via `begin`...`ensure`
1928* Top-level `rescue` for error handling
1929* API key validation after server connection
1930 
1931## Running the Client
1932 
1933To run your client with any MCP server:
1934 
1935```bash theme={null}
1936bundle exec ruby client.rb path/to/server.rb # ruby server
1937bundle exec ruby client.rb path/to/server.py # python server
1938bundle exec ruby client.rb path/to/build/index.js # node server
1939```
1940 
1941> [!NOTE]
1942>
1943> If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-ruby), your command might look something like this: `bundle exec ruby client.rb /path/to/weather-server-ruby/weather.rb`
1944 
1945The client will:
1946 
19471. Connect to the specified server
19482. List available tools
19493. Start an interactive chat session where you can:
1950 * Enter queries
1951 * See tool executions
1952 * Get responses from Claude
1953 
1954## How It Works
1955 
1956When you submit a query:
1957 
19581. The client gets the list of available tools from the server
19592. Your query is sent to Claude along with tool descriptions
19603. Claude decides which tools (if any) to use
19614. The client executes any requested tool calls through the server
19625. Results are sent back to Claude
19636. Claude provides a natural language response
19647. The response is displayed to you
1965 
1966## Best practices
1967 
19681. **Error Handling**
1969 * Wrap tool calls in `begin`...`rescue` blocks
1970 * Provide meaningful error messages
1971 * Gracefully handle connection issues
1972 
19732. **Resource Management**
1974 * Always close the transport when done
1975 * Use `begin`...`ensure` for proper cleanup
1976 * Handle server disconnections
1977 
19783. **Security**
1979 * Store API keys securely in `.env`
1980 * Validate server responses
1981 * Be cautious with tool permissions
1982 
19834. **Tool Names**
1984 * Tool names can be validated according to the format specified [here](/specification/draft/server/tools#tool-names)
1985 * If a tool name conforms to the specified format, it should not fail validation by an MCP client
1986 
1987## Troubleshooting
1988 
1989### Server Path Issues
1990 
1991* Double-check the path to your server script is correct
1992* Use the absolute path if the relative path isn't working
1993* For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
1994* Verify the server file has the correct extension (.py for Python, .js for Node.js, or .rb for Ruby)
1995 
1996Example of correct path usage:
1997 
1998```bash theme={null}
1999# Relative path
2000bundle exec ruby client.rb ./server/weather.rb
2001 
2002# Absolute path
2003bundle exec ruby client.rb /Users/username/projects/mcp-server/weather.rb
2004 
2005# Windows path (either format works)
2006bundle exec ruby client.rb C:/projects/mcp-server/weather.rb
2007bundle exec ruby client.rb C:\\projects\\mcp-server\\weather.rb
2008```
2009 
2010### Response Timing
2011 
2012* The first response might take up to 30 seconds to return
2013* This is normal and happens while:
2014 * The server initializes
2015 * Claude processes the query
2016 * Tools are being executed
2017* Subsequent responses are typically faster
2018* Don't interrupt the process during this initial waiting period
2019 
2020### Common Error Messages
2021 
2022If you see:
2023 
2024* `Errno::ENOENT`: Check your server path and ensure the command (`ruby`, `python3`, `node`) is available
2025* `Connection refused`: Ensure the server is running and the path is correct
2026* `Tool execution failed`: Verify the tool's required environment variables are set
2027* `Anthropic::Errors::AuthenticationError`: Check your `.env` file has a valid `ANTHROPIC_API_KEY`
2028 
2029**Rust**
2030 
2031[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-rust)
2032 
2033## System Requirements
2034 
2035Before starting, ensure your Linux system meets these requirements:
2036 
2037* Latest stable version of [Rust and Cargo](https://www.rust-lang.org/tools/install)
2038* Anthropic API key (Claude)
2039* A Python, Node.js, or executable MCP server to connect to
2040 
2041## Setting Up Your Environment
2042 
2043First, create a new Rust project:
2044 
2045```bash theme={null}
2046cargo new mcp-client-rust
2047cd mcp-client-rust
2048```
2049 
2050Replace the contents of `Cargo.toml` with the following:
2051 
2052```toml Cargo.toml theme={null}
2053[package]
2054name = "mcp-client-rust"
2055version = "0.1.0"
2056edition = "2024"
2057 
2058[dependencies]
2059anyhow = "1.0.100"
2060genai = "0.4.2"
2061rmcp = { version = "0.8.0", features = ["server", "client", "transport-io", "transport-child-process"] }
2062tokio = { version = "1.47.1", features = ["full"] }
2063tracing = "0.1.41"
2064tracing-subscriber = { version = "0.3", features = ["env-filter"] }
2065serde_json = "1.0.128"
2066dotenvy = "0.15.7"
2067reqwest = "0.12.23"
2068```
2069 
2070The [`rmcp`](https://github.com/modelcontextprotocol/rust-sdk) crate provides the Rust MCP SDK and child-process transport. This example uses the [`genai`](https://github.com/jeremychone/rust-genai) crate to send requests to Claude and represent tools in the model request.
2071 
2072## Setting Up Your API Key
2073 
2074You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
2075 
2076Create a `.env` file to store it:
2077 
2078```bash theme={null}
2079echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
2080```
2081 
2082Add `.env` to your `.gitignore`:
2083 
2084```bash theme={null}
2085echo ".env" >> .gitignore
2086```
2087 
2088> [!WARNING]
2089>
2090> Make sure you keep your `ANTHROPIC_API_KEY` secure!
2091 
2092## Creating the Client
2093 
2094Open `src/main.rs` and replace its contents as you work through the following sections.
2095 
2096### Imports and Client Structure
2097 
2098First, add the imports, model constant, and basic client structure:
2099 
2100```rust theme={null}
2101use anyhow::{Context, Result, bail};
2102use genai::Client;
2103use genai::chat::{
2104 ChatMessage, ChatRequest, ChatResponse, ContentPart, Tool as GenaiTool, ToolResponse,
2105};
2106use rmcp::model::{CallToolRequestParam, Tool as McpTool};
2107use rmcp::service::{RoleClient, RunningService, ServiceExt};
2108use rmcp::transport::TokioChildProcess;
2109use serde_json::Value;
2110use tokio::io::{self, AsyncBufReadExt, BufReader};
2111use tokio::process::Command;
2112 
2113const MODEL_ANTHROPIC: &str = "claude-opus-5";
2114 
2115struct MCPClient {
2116 anthropic: Client,
2117 session: Option<RunningService<RoleClient, ()>>,
2118 tools: Vec<GenaiTool>,
2119}
2120```
2121 
2122The client keeps the model API client, the active MCP session, and the tools advertised by the connected server.
2123 
2124### Client Initialization
2125 
2126Next, initialize the model client and start without an MCP session or tools:
2127 
2128```rust theme={null}
2129impl MCPClient {
2130 fn new() -> Result<Self> {
2131 Ok(MCPClient {
2132 anthropic: Client::default(),
2133 session: None,
2134 tools: Vec::new(),
2135 })
21312136 }
2132 ```
2133 
2134 The client keeps the model API client, the active MCP session, and the tools advertised by the connected server.
2135 
2136 ### Client Initialization
2137 
2138 Next, initialize the model client and start without an MCP session or tools:
2139 
2140 ```rust theme={null}
2141 impl MCPClient {
2142 fn new() -> Result<Self> {
2143 Ok(MCPClient {
2144 anthropic: Client::default(),
2145 session: None,
2146 tools: Vec::new(),
2147 })
2137 
2138 // Additional methods will go here.
2139}
2140```
2141 
2142`genai::Client::default()` reads the `ANTHROPIC_API_KEY` environment variable when it sends a request.
2143 
2144### Server Connection Management
2145 
2146Add this method inside the `impl MCPClient` block:
2147 
2148```rust theme={null}
2149async fn connect_to_server(&mut self, server_args: &[String]) -> Result<()> {
2150 if self.session.is_some() {
2151 bail!("Client is already connected to a server");
2152 }
2153 
2154 let mut command = Command::new(&server_args[0]);
2155 command.args(&server_args[1..]);
2156 
2157 let process = TokioChildProcess::new(command)
2158 .with_context(|| format!("Failed to spawn server process for {:?}", server_args))?;
2159 
2160 let session = ().serve(process).await?;
2161 
2162 let rmcp_tools = session
2163 .list_all_tools()
2164 .await
2165 .context("Unable to list tools from server")?;
2166 
2167 let tool_names: Vec<String> = rmcp_tools
2168 .iter()
2169 .map(|tool| tool.name.to_string())
2170 .collect();
2171 
2172 println!("Connected to server with tools: {tool_names:?}");
2173 
2174 self.tools = convert_tools(&rmcp_tools);
2175 self.session = Some(session);
2176 Ok(())
2177}
2178```
2179 
2180This method:
2181 
21821. Starts the server as a child process using the command and arguments supplied on the command line
21832. Establishes an MCP session over stdio
21843. Lists all tools advertised by the server
21854. Converts those tools into the format used in model requests
2186 
2187### Converting MCP Tools
2188 
2189Add this function outside the `impl MCPClient` block:
2190 
2191```rust theme={null}
2192fn convert_tools(tools: &[McpTool]) -> Vec<GenaiTool> {
2193 tools
2194 .iter()
2195 .map(|tool| GenaiTool {
2196 name: tool.name.to_string(),
2197 description: tool.description.as_deref().map(str::to_string),
2198 schema: Some(Value::Object(tool.input_schema.as_ref().clone())),
2199 config: None,
2200 })
2201 .collect()
2202}
2203```
2204 
2205MCP and model APIs describe tools with similar information but different Rust types. `convert_tools` maps each MCP tool's name, description, and input schema into a `genai` tool definition.
2206 
2207### Sending Model Requests
2208 
2209Add this helper method inside `impl MCPClient`:
2210 
2211```rust theme={null}
2212async fn request_model(&self, chat_req: &ChatRequest) -> Result<ChatResponse> {
2213 let response = self
2214 .anthropic
2215 .exec_chat(MODEL_ANTHROPIC, chat_req.clone(), None)
2216 .await
2217 .context("Anthropic chat request failed")?;
2218 
2219 Ok(response)
2220}
2221```
2222 
2223This keeps model request handling in one place and adds useful context if the API request fails.
2224 
2225### Query Processing Logic
2226 
2227Now add the core query-processing method inside `impl MCPClient`:
2228 
2229```rust theme={null}
2230async fn process_query(&mut self, query: &str) -> Result<String> {
2231 let session = self
2232 .session
2233 .as_ref()
2234 .context("Client is not connected to any server")?;
2235 
2236 let mut messages = vec![ChatMessage::user(query)];
2237 let mut final_text = Vec::new();
2238 
2239 // Initial Claude API call with tools
2240 let mut chat_req = ChatRequest::new(messages.clone()).with_tools(self.tools.clone());
2241 let mut chat_rsp = self.request_model(&chat_req).await?;
2242 
2243 // Process response content - collect text and handle tool calls
2244 for text in chat_rsp.texts() {
2245 final_text.push(text.to_string());
2246 }
2247 
2248 let tool_calls = chat_rsp.tool_calls();
2249 if !tool_calls.is_empty() {
2250 // Append assistant's response to message history
2251 messages.push(ChatMessage::assistant(chat_rsp.content.clone()));
2252 
2253 // Execute each tool call and collect responses
2254 let mut tool_results = Vec::new();
2255 for tool_call in tool_calls {
2256 // Add information about the tool call to final text
2257 let tool_args_str = serde_json::to_string(&tool_call.fn_arguments)
2258 .unwrap_or_else(|_| "{}".to_string());
2259 
2260 final_text.push(format!(
2261 "[Calling tool {} with args {}]",
2262 tool_call.fn_name, tool_args_str
2263 ));
2264 
2265 // Query the MCP server
2266 let tool_result = session
2267 .call_tool(CallToolRequestParam {
2268 name: tool_call.fn_name.clone().into(),
2269 arguments: tool_call.fn_arguments.as_object().cloned(),
2270 })
2271 .await
2272 .with_context(|| format!("Tool call {} failed", tool_call.fn_name))?;
2273 
2274 let payload = serde_json::to_string(&tool_result)
2275 .context("Failed to serialize tool result")?;
2276 
2277 tool_results.push(ContentPart::ToolResponse(ToolResponse::new(
2278 tool_call.call_id.clone(),
2279 payload,
2280 )));
21482281 }
21492282 
2150 // Additional methods will go here.
2151 }
2152 ```
2153 
2154 `genai::Client::default()` reads the `ANTHROPIC_API_KEY` environment variable when it sends a request.
2155 
2156 ### Server Connection Management
2157 
2158 Add this method inside the `impl MCPClient` block:
2159 
2160 ```rust theme={null}
2161 async fn connect_to_server(&mut self, server_args: &[String]) -> Result<()> {
2162 if self.session.is_some() {
2163 bail!("Client is already connected to a server");
2164 }
2165 
2166 let mut command = Command::new(&server_args[0]);
2167 command.args(&server_args[1..]);
2168 
2169 let process = TokioChildProcess::new(command)
2170 .with_context(|| format!("Failed to spawn server process for {:?}", server_args))?;
2171 
2172 let session = ().serve(process).await?;
2173 
2174 let rmcp_tools = session
2175 .list_all_tools()
2176 .await
2177 .context("Unable to list tools from server")?;
2178 
2179 let tool_names: Vec<String> = rmcp_tools
2180 .iter()
2181 .map(|tool| tool.name.to_string())
2182 .collect();
2183 
2184 println!("Connected to server with tools: {tool_names:?}");
2185 
2186 self.tools = convert_tools(&rmcp_tools);
2187 self.session = Some(session);
2188 Ok(())
2189 }
2190 ```
2191 
2192 This method:
2193 
2194 1. Starts the server as a child process using the command and arguments supplied on the command line
2195 2. Establishes an MCP session over stdio
2196 3. Lists all tools advertised by the server
2197 4. Converts those tools into the format used in model requests
2198 
2199 ### Converting MCP Tools
2200 
2201 Add this function outside the `impl MCPClient` block:
2202 
2203 ```rust theme={null}
2204 fn convert_tools(tools: &[McpTool]) -> Vec<GenaiTool> {
2205 tools
2206 .iter()
2207 .map(|tool| GenaiTool {
2208 name: tool.name.to_string(),
2209 description: tool.description.as_deref().map(str::to_string),
2210 schema: Some(Value::Object(tool.input_schema.as_ref().clone())),
2211 config: None,
2212 })
2213 .collect()
2214 }
2215 ```
2216 
2217 MCP and model APIs describe tools with similar information but different Rust types. `convert_tools` maps each MCP tool's name, description, and input schema into a `genai` tool definition.
2218 
2219 ### Sending Model Requests
2220 
2221 Add this helper method inside `impl MCPClient`:
2222 
2223 ```rust theme={null}
2224 async fn request_model(&self, chat_req: &ChatRequest) -> Result<ChatResponse> {
2225 let response = self
2226 .anthropic
2227 .exec_chat(MODEL_ANTHROPIC, chat_req.clone(), None)
2228 .await
2229 .context("Anthropic chat request failed")?;
2230 
2231 Ok(response)
2232 }
2233 ```
2234 
2235 This keeps model request handling in one place and adds useful context if the API request fails.
2236 
2237 ### Query Processing Logic
2238 
2239 Now add the core query-processing method inside `impl MCPClient`:
2240 
2241 ```rust theme={null}
2242 async fn process_query(&mut self, query: &str) -> Result<String> {
2243 let session = self
2244 .session
2245 .as_ref()
2246 .context("Client is not connected to any server")?;
2247 
2248 let mut messages = vec![ChatMessage::user(query)];
2249 let mut final_text = Vec::new();
2250 
2251 // Initial Claude API call with tools
2252 let mut chat_req = ChatRequest::new(messages.clone()).with_tools(self.tools.clone());
2253 let mut chat_rsp = self.request_model(&chat_req).await?;
2254 
2255 // Process response content - collect text and handle tool calls
2283 // Append tool responses to message history
2284 messages.push(ChatMessage::user(tool_results));
2285 
2286 // Build the next request and query model
2287 chat_req = ChatRequest::new(messages.clone());
2288 chat_rsp = self.request_model(&chat_req).await?;
2289 
2290 // Collect text from response
22562291 for text in chat_rsp.texts() {
22572292 final_text.push(text.to_string());
22582293 }
2259 
2260 let tool_calls = chat_rsp.tool_calls();
2261 if !tool_calls.is_empty() {
2262 // Append assistant's response to message history
2263 messages.push(ChatMessage::assistant(chat_rsp.content.clone()));
2264 
2265 // Execute each tool call and collect responses
2266 let mut tool_results = Vec::new();
2267 for tool_call in tool_calls {
2268 // Add information about the tool call to final text
2269 let tool_args_str = serde_json::to_string(&tool_call.fn_arguments)
2270 .unwrap_or_else(|_| "{}".to_string());
2271 
2272 final_text.push(format!(
2273 "[Calling tool {} with args {}]",
2274 tool_call.fn_name, tool_args_str
2275 ));
2276 
2277 // Query the MCP server
2278 let tool_result = session
2279 .call_tool(CallToolRequestParam {
2280 name: tool_call.fn_name.clone().into(),
2281 arguments: tool_call.fn_arguments.as_object().cloned(),
2282 })
2283 .await
2284 .with_context(|| format!("Tool call {} failed", tool_call.fn_name))?;
2285 
2286 let payload = serde_json::to_string(&tool_result)
2287 .context("Failed to serialize tool result")?;
2288 
2289 tool_results.push(ContentPart::ToolResponse(ToolResponse::new(
2290 tool_call.call_id.clone(),
2291 payload,
2292 )));
2293 }
2294 
2295 // Append tool responses to message history
2296 messages.push(ChatMessage::user(tool_results));
2297 
2298 // Build the next request and query model
2299 chat_req = ChatRequest::new(messages.clone());
2300 chat_rsp = self.request_model(&chat_req).await?;
2301 
2302 // Collect text from response
2303 for text in chat_rsp.texts() {
2304 final_text.push(text.to_string());
2305 }
2294 }
2295 
2296 Ok(final_text.join("\n"))
2297}
2298```
2299 
2300The method first sends the user's query and available tools to Claude. When Claude requests tools, the client executes each request through the MCP session, sends the results back to Claude, and collects the final text response.
2301 
2302### Interactive Chat Interface
2303 
2304Add the interactive terminal loop inside `impl MCPClient`:
2305 
2306```rust theme={null}
2307async fn chat_loop(&mut self) -> Result<()> {
2308 println!("\nMCP Client Started!");
2309 println!("Type your queries or 'quit' to exit.");
2310 
2311 let mut stdin = BufReader::new(io::stdin());
2312 let mut input = String::new();
2313 
2314 loop {
2315 print!("\nQuery: ");
2316 std::io::Write::flush(&mut std::io::stdout())?;
2317 
2318 input.clear();
2319 if stdin.read_line(&mut input).await? == 0 {
2320 break; // EOF
23062321 }
23072322 
2308 Ok(final_text.join("\n"))
2323 let query = input.trim();
2324 if query.eq_ignore_ascii_case("quit") {
2325 break;
2326 }
2327 if query.is_empty() {
2328 continue;
2329 }
2330 
2331 match self.process_query(query).await {
2332 Ok(response) => println!("\n{}", response),
2333 Err(err) => println!("\nError: {}", err),
2334 }
23092335 }
2310 ```
2311 
2312 The method first sends the user's query and available tools to Claude. When Claude requests tools, the client executes each request through the MCP session, sends the results back to Claude, and collects the final text response.
2313 
2314 ### Interactive Chat Interface
2315 
2316 Add the interactive terminal loop inside `impl MCPClient`:
2317 
2318 ```rust theme={null}
2319 async fn chat_loop(&mut self) -> Result<()> {
2320 println!("\nMCP Client Started!");
2321 println!("Type your queries or 'quit' to exit.");
2322 
2323 let mut stdin = BufReader::new(io::stdin());
2324 let mut input = String::new();
2325 
2326 loop {
2327 print!("\nQuery: ");
2328 std::io::Write::flush(&mut std::io::stdout())?;
2329 
2330 input.clear();
2331 if stdin.read_line(&mut input).await? == 0 {
2332 break; // EOF
2333 }
2334 
2335 let query = input.trim();
2336 if query.eq_ignore_ascii_case("quit") {
2337 break;
2338 }
2339 if query.is_empty() {
2340 continue;
2341 }
2342 
2343 match self.process_query(query).await {
2344 Ok(response) => println!("\n{}", response),
2345 Err(err) => println!("\nError: {}", err),
2346 }
2347 }
2348 
2349 Ok(())
2336 
2337 Ok(())
2338}
2339```
2340 
2341The loop accepts queries until the user types `quit` or closes standard input. Query errors are printed without terminating the client.
2342 
2343### Cleanup
2344 
2345Add this method inside `impl MCPClient` to stop the MCP session and child process:
2346 
2347```rust theme={null}
2348async fn cleanup(&mut self) -> Result<()> {
2349 if let Some(session) = self.session.take() {
2350 let _ = session.cancel().await;
23502351 }
2351 ```
2352 
2353 The loop accepts queries until the user types `quit` or closes standard input. Query errors are printed without terminating the client.
2354 
2355 ### Cleanup
2356 
2357 Add this method inside `impl MCPClient` to stop the MCP session and child process:
2358 
2359 ```rust theme={null}
2360 async fn cleanup(&mut self) -> Result<()> {
2361 if let Some(session) = self.session.take() {
2362 let _ = session.cancel().await;
2363 }
2364 Ok(())
2352 Ok(())
2353}
2354```
2355 
2356### Main Entry Point
2357 
2358Finally, add the asynchronous entry point outside the `impl MCPClient` block:
2359 
2360```rust theme={null}
2361#[tokio::main]
2362async fn main() -> Result<()> {
2363 dotenvy::dotenv().context("Failed to load env file")?;
2364 
2365 let mut args = std::env::args();
2366 let _ = args.next();
2367 let server_args: Vec<String> = args.collect();
2368 
2369 if server_args.is_empty() {
2370 eprintln!("Usage: cargo run -- <server_script_or_binary> [args...]");
2371 std::process::exit(1);
23652372 }
2366 ```
2367 
2368 ### Main Entry Point
2369 
2370 Finally, add the asynchronous entry point outside the `impl MCPClient` block:
2371 
2372 ```rust theme={null}
2373 #[tokio::main]
2374 async fn main() -> Result<()> {
2375 dotenvy::dotenv().context("Failed to load env file")?;
2376 
2377 let mut args = std::env::args();
2378 let _ = args.next();
2379 let server_args: Vec<String> = args.collect();
2380 
2381 if server_args.is_empty() {
2382 eprintln!("Usage: cargo run -- <server_script_or_binary> [args...]");
2383 std::process::exit(1);
2384 }
2385 
2386 let mut client = MCPClient::new()?;
2387 
2388 let result = async {
2389 client.connect_to_server(&server_args).await?;
2390 client.chat_loop().await
2391 }
2392 .await;
2393 
2394 let cleanup_result = client.cleanup().await;
2395 
2396 result?;
2397 cleanup_result?;
2398 
2399 Ok(())
2373 
2374 let mut client = MCPClient::new()?;
2375 
2376 let result = async {
2377 client.connect_to_server(&server_args).await?;
2378 client.chat_loop().await
24002379 }
2401 ```
2402 
2403 The entry point loads `.env`, treats all remaining command-line arguments as the server command, connects the client, starts the chat loop, and ensures cleanup runs before exiting.
2404 
2405 ### Verify the Complete File
2406 
2407 Before running the client, confirm the items in `src/main.rs` are placed at the correct scope:
2408 
2409 * `new`, `connect_to_server`, `process_query`, `request_model`, `chat_loop`, and `cleanup` are methods inside the single `impl MCPClient` block.
2410 * `main` and `convert_tools` are functions outside the `impl MCPClient` block.
2411 
2412 Rust does not require these items to appear in a particular order, but methods and free functions must be placed in the correct scope. Compare your file with the [complete `src/main.rs` example](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-rust/src/main.rs), then check that it compiles:
2413 
2414 ```bash theme={null}
2415 cargo fmt --check
2416 cargo check
2417 ```
2418 
2419 ## Running the Client
2420 
2421 Use `cargo run --` followed by the command you would normally use to start the MCP server:
2422 
2423 ```bash theme={null}
2424 # Python server
2425 cargo run -- python path/to/server.py
2426 
2427 # Node.js server
2428 cargo run -- node path/to/build/index.js
2429 
2430 # Executable server
2431 cargo run -- path/to/server-binary
2432 ```
2433 
2434 Running bare `cargo run` without a server command prints the usage message and exits.
2435 
2436 <Note>
2437 If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-rust), build the server first and then run a command similar to: `cargo run -- ../weather-server-rust/target/debug/weather`
2438 </Note>
2439 
2440 The client will:
2441 
2442 1. Start and connect to the specified MCP server
2443 2. List the tools available from that server
2444 3. Start an interactive chat session where you can:
2445 * Enter queries
2446 * See tool executions
2447 * Get responses from Claude
2448 
2449 ## How It Works
2450 
2451 When you submit a query:
2452 
2453 1. The client sends your query and the server's available tools to Claude
2454 2. Claude decides which tools, if any, to use
2455 3. The client executes requested tools through the MCP session
2456 4. Tool results are sent back to Claude
2457 5. Claude provides a natural language response
2458 6. The response is displayed in the terminal
2459 
2460 ## Best Practices
2461 
2462 1. **Error Handling**
2463 * Add context to errors at process, MCP, model API, and serialization boundaries
2464 * Report individual query errors without terminating the interactive session
2465 * Validate server commands before running them
2466 
2467 2. **Resource Management**
2468 * Always cancel the MCP session during cleanup
2469 * Ensure cleanup runs even when connection or chat-loop operations fail
2470 * Avoid starting a second server while a session is active
2471 
2472 3. **Security**
2473 * Store API keys securely in `.env`
2474 * Review the tools exposed by a server before allowing model-driven calls
2475 * Connect only to servers and executable commands you trust
2476 
2477 ## Troubleshooting
2478 
2479 ### Server Command Issues
2480 
2481 The arguments after `cargo run --` must form a complete command. Interpreted server scripts need their runtime:
2482 
2483 ```bash theme={null}
2484 # Correct
2485 cargo run -- python ./server/weather.py
2486 cargo run -- node ./server/build/index.js
2487 
2488 # Incorrect: a Python script is not necessarily executable by itself
2489 cargo run -- ./server/weather.py
2490 ```
2491 
2492 If a command cannot be found, use its absolute path or verify it is available in your `PATH`.
2493 
2494 ### Environment File Issues
2495 
2496 If you see `Failed to load env file`, ensure `.env` exists in the directory where you run the client.
2497 
2498 If the model request reports a missing API key, confirm that `.env` contains:
2499 
2500 ```text theme={null}
2501 ANTHROPIC_API_KEY=your-api-key-goes-here
2502 ```
2503 
2504 ### Tool and Response Errors
2505 
2506 * `Unable to list tools from server`: Verify the server starts successfully and communicates over stdio
2507 * `Tool call ... failed`: Verify the server tool's required arguments and environment variables
2508 * `Failed to serialize tool result`: Inspect the server's response for unsupported or malformed content
2509 </Tab>
2510</Tabs>
2380 .await;
2381 
2382 let cleanup_result = client.cleanup().await;
2383 
2384 result?;
2385 cleanup_result?;
2386 
2387 Ok(())
2388}
2389```
2390 
2391The entry point loads `.env`, treats all remaining command-line arguments as the server command, connects the client, starts the chat loop, and ensures cleanup runs before exiting.
2392 
2393### Verify the Complete File
2394 
2395Before running the client, confirm the items in `src/main.rs` are placed at the correct scope:
2396 
2397* `new`, `connect_to_server`, `process_query`, `request_model`, `chat_loop`, and `cleanup` are methods inside the single `impl MCPClient` block.
2398* `main` and `convert_tools` are functions outside the `impl MCPClient` block.
2399 
2400Rust does not require these items to appear in a particular order, but methods and free functions must be placed in the correct scope. Compare your file with the [complete `src/main.rs` example](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-rust/src/main.rs), then check that it compiles:
2401 
2402```bash theme={null}
2403cargo fmt --check
2404cargo check
2405```
2406 
2407## Running the Client
2408 
2409Use `cargo run --` followed by the command you would normally use to start the MCP server:
2410 
2411```bash theme={null}
2412# Python server
2413cargo run -- python path/to/server.py
2414 
2415# Node.js server
2416cargo run -- node path/to/build/index.js
2417 
2418# Executable server
2419cargo run -- path/to/server-binary
2420```
2421 
2422Running bare `cargo run` without a server command prints the usage message and exits.
2423 
2424> [!NOTE]
2425>
2426> If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-rust), build the server first and then run a command similar to: `cargo run -- ../weather-server-rust/target/debug/weather`
2427 
2428The client will:
2429 
24301. Start and connect to the specified MCP server
24312. List the tools available from that server
24323. Start an interactive chat session where you can:
2433 * Enter queries
2434 * See tool executions
2435 * Get responses from Claude
2436 
2437## How It Works
2438 
2439When you submit a query:
2440 
24411. The client sends your query and the server's available tools to Claude
24422. Claude decides which tools, if any, to use
24433. The client executes requested tools through the MCP session
24444. Tool results are sent back to Claude
24455. Claude provides a natural language response
24466. The response is displayed in the terminal
2447 
2448## Best Practices
2449 
24501. **Error Handling**
2451 * Add context to errors at process, MCP, model API, and serialization boundaries
2452 * Report individual query errors without terminating the interactive session
2453 * Validate server commands before running them
2454 
24552. **Resource Management**
2456 * Always cancel the MCP session during cleanup
2457 * Ensure cleanup runs even when connection or chat-loop operations fail
2458 * Avoid starting a second server while a session is active
2459 
24603. **Security**
2461 * Store API keys securely in `.env`
2462 * Review the tools exposed by a server before allowing model-driven calls
2463 * Connect only to servers and executable commands you trust
2464 
2465## Troubleshooting
2466 
2467### Server Command Issues
2468 
2469The arguments after `cargo run --` must form a complete command. Interpreted server scripts need their runtime:
2470 
2471```bash theme={null}
2472# Correct
2473cargo run -- python ./server/weather.py
2474cargo run -- node ./server/build/index.js
2475 
2476# Incorrect: a Python script is not necessarily executable by itself
2477cargo run -- ./server/weather.py
2478```
2479 
2480If a command cannot be found, use its absolute path or verify it is available in your `PATH`.
2481 
2482### Environment File Issues
2483 
2484If you see `Failed to load env file`, ensure `.env` exists in the directory where you run the client.
2485 
2486If the model request reports a missing API key, confirm that `.env` contains:
2487 
2488```text theme={null}
2489ANTHROPIC_API_KEY=your-api-key-goes-here
2490```
2491 
2492### Tool and Response Errors
2493 
2494* `Unable to list tools from server`: Verify the server starts successfully and communicates over stdio
2495* `Tool call ... failed`: Verify the server tool's required arguments and environment variables
2496* `Failed to serialize tool result`: Inspect the server's response for unsupported or malformed content
25112497 
25122498## Next steps
25132499 
2514<CardGroup cols={2}>
2515 <Card title="Example servers" icon="grid" href="/examples">
2516 Check out our gallery of official MCP servers and implementations
2517 </Card>
2518</CardGroup>
2500> **[Example servers](/examples)**
2501>
2502> Check out our gallery of official MCP servers and implementations
25192503 

docs/draft/develop/build-server Changed · +2868 / -2938 lines

### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and setting up the instance ### Helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and setting up constants ### Helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Logging in MCP Servers ### Best Practices ### System requirements ### Set up your environment ## Building your server ### Weather Service ### Create your Boot Application ### Running the server ## Testing your server with Claude for Desktop ## Testing your server with Java client ### Create an MCP Client manually ### Use MCP Client Boot Starter ## More Java MCP Server examples ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### System requirements ### Set up your environment ## Building your server ### Setting up the instance ### Weather API helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### System requirements ### Set up your environment ## Building your server ### Weather API helper functions ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and setting up constants ### Helper methods ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and constants ### Data structures ### Helper functions ### Implementing the Weather server and tools ### Implementing the ServerHandler ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and constants ### Data structures ### Helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 8
88 
99We'll build a server that exposes two tools: `get_alerts` and `get_forecast`. Then we'll connect the server to an MCP host (in this case, Claude for Desktop):
1010 
11<Frame>
12 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/current-weather.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=dce7b2f8a06c20ba358e4bd2e75fa4c7" width="2780" height="1849" data-path="images/current-weather.png" />
13</Frame>
14 
15<Note>
16 Servers can connect to any client. We've chosen Claude for Desktop here for simplicity, but we also have a guide on [building your own client](/docs/draft/develop/build-client).
17</Note>
11![](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/current-weather.png?fit=max\&auto=format\&n=4ZXF1PrDkEaJvXpn\&q=85\&s=dce7b2f8a06c20ba358e4bd2e75fa4c7)
12 
13> [!NOTE]
14>
15> Servers can connect to any client. We've chosen Claude for Desktop here for simplicity, but we also have a guide on [building your own client](/docs/draft/develop/build-client).
1816 
1917### Core MCP Concepts
2018 
from line 24
2624 
2725This tutorial will primarily focus on tools.
2826 
29<Tabs>
30 <Tab title="Python">
31 Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python)
32 
33 ### Prerequisite knowledge
34 
35 This quickstart assumes you have familiarity with:
36 
37 * Python
38 * LLMs like Claude
39 
40 ### Logging in MCP Servers
41 
42 When implementing MCP servers, be careful about how you handle logging:
43 
44 **For STDIO-based servers:** Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server. The `print()` function writes to stdout by default, so keep it out of a STDIO server entirely.
45 
46 **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
47 
48 ### Best Practices
49 
50 * Use the standard library `logging` module, which writes to stderr.
51 * Create one logger per module with `logging.getLogger(__name__)` and call it from your tools.
52 
53 ### Quick Examples
54 
55 ```python theme={null}
56 import logging
57 
58 logger = logging.getLogger(__name__)
59 
60 # ❌ Bad (STDIO)
61 print("Processing request")
62 
63 # ✅ Good (STDIO)
64 logger.info("Processing request") # writes to stderr
65 ```
66 
67 ### System requirements
68 
69 * Python 3.10 or higher installed.
70 * You must use the Python MCP SDK 2.0.0 or higher.
71 
72 ### Set up your environment
73 
74 First, let's install `uv` and set up our Python project and environment:
75 
76 <CodeGroup>
77 ```bash macOS/Linux theme={null}
78 curl -LsSf https://astral.sh/uv/install.sh | sh
79 ```
80 
81 ```powershell Windows theme={null}
82 powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
83 ```
84 </CodeGroup>
85 
86 Make sure to restart your terminal afterwards to ensure that the `uv` command gets picked up.
87 
88 Now, let's create and set up our project:
89 
90 <CodeGroup>
91 ```bash macOS/Linux theme={null}
92 # Create a new directory for our project
93 uv init weather
94 cd weather
95 
96 # Create virtual environment and activate it
97 uv venv
98 source .venv/bin/activate
99 
100 # Install dependencies
101 uv add "mcp[cli]"
102 
103 # Create our server file
104 touch weather.py
105 ```
106 
107 ```powershell Windows theme={null}
108 # Create a new directory for our project
109 uv init weather
110 cd weather
111 
112 # Create virtual environment and activate it
113 uv venv
114 .venv\Scripts\activate
115 
116 # Install dependencies
117 uv add mcp[cli]
118 
119 # Create our server file
120 new-item weather.py
121 ```
122 </CodeGroup>
123 
124 Now let's dive into building your server.
125 
126 ## Building your server
127 
128 ### Importing packages and setting up the instance
129 
130 Add these to the top of your `weather.py`:
131 
132 ```python theme={null}
133 from typing import Any
134 
135 import httpx2
136 from mcp.server import MCPServer
137 
138 # Initialize MCPServer
139 mcp = MCPServer("weather")
140 
141 # Constants
142 NWS_API_BASE = "https://api.weather.gov"
143 USER_AGENT = "weather-app/1.0"
144 ```
145 
146 `httpx2` is the HTTP client the SDK itself depends on, so installing `mcp` already brought it in.
147 
148 The MCPServer class uses Python type hints and docstrings to automatically generate tool definitions, making it easy to create and maintain MCP tools.
149 
150 ### Helper functions
151 
152 Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
153 
154 ```python theme={null}
155 async def make_nws_request(url: str) -> dict[str, Any] | None:
156 """Make a request to the NWS API with proper error handling."""
157 headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
158 async with httpx2.AsyncClient() as client:
159 try:
160 response = await client.get(url, headers=headers, timeout=30.0)
161 response.raise_for_status()
162 return response.json()
163 except Exception:
164 return None
165 
166 
167 def format_alert(feature: dict) -> str:
168 """Format an alert feature into a readable string."""
169 props = feature["properties"]
170 return f"""
171 Event: {props.get("event", "Unknown")}
172 Area: {props.get("areaDesc", "Unknown")}
173 Severity: {props.get("severity", "Unknown")}
174 Description: {props.get("description", "No description available")}
175 Instructions: {props.get("instruction", "No specific instructions provided")}
27**Python**
28 
29Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python)
30 
31### Prerequisite knowledge
32 
33This quickstart assumes you have familiarity with:
34 
35* Python
36* LLMs like Claude
37 
38### Logging in MCP Servers
39 
40When implementing MCP servers, be careful about how you handle logging:
41 
42**For STDIO-based servers:** Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server. The `print()` function writes to stdout by default, so keep it out of a STDIO server entirely.
43 
44**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
45 
46### Best Practices
47 
48* Use the standard library `logging` module, which writes to stderr.
49* Create one logger per module with `logging.getLogger(__name__)` and call it from your tools.
50 
51### Quick Examples
52 
53```python theme={null}
54import logging
55 
56logger = logging.getLogger(__name__)
57 
58# ❌ Bad (STDIO)
59print("Processing request")
60 
61# ✅ Good (STDIO)
62logger.info("Processing request") # writes to stderr
63```
64 
65### System requirements
66 
67* Python 3.10 or higher installed.
68* You must use the Python MCP SDK 2.0.0 or higher.
69 
70### Set up your environment
71 
72First, let's install `uv` and set up our Python project and environment:
73 
74```bash macOS/Linux theme={null}
75curl -LsSf https://astral.sh/uv/install.sh | sh
76```
77 
78```powershell Windows theme={null}
79powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
80```
81 
82Make sure to restart your terminal afterwards to ensure that the `uv` command gets picked up.
83 
84Now, let's create and set up our project:
85 
86```bash macOS/Linux theme={null}
87# Create a new directory for our project
88uv init weather
89cd weather
90 
91# Create virtual environment and activate it
92uv venv
93source .venv/bin/activate
94 
95# Install dependencies
96uv add "mcp[cli]"
97 
98# Create our server file
99touch weather.py
100```
101 
102```powershell Windows theme={null}
103# Create a new directory for our project
104uv init weather
105cd weather
106 
107# Create virtual environment and activate it
108uv venv
109.venv\Scripts\activate
110 
111# Install dependencies
112uv add mcp[cli]
113 
114# Create our server file
115new-item weather.py
116```
117 
118Now let's dive into building your server.
119 
120## Building your server
121 
122### Importing packages and setting up the instance
123 
124Add these to the top of your `weather.py`:
125 
126```python theme={null}
127from typing import Any
128 
129import httpx2
130from mcp.server import MCPServer
131 
132# Initialize MCPServer
133mcp = MCPServer("weather")
134 
135# Constants
136NWS_API_BASE = "https://api.weather.gov"
137USER_AGENT = "weather-app/1.0"
138```
139 
140`httpx2` is the HTTP client the SDK itself depends on, so installing `mcp` already brought it in.
141 
142The MCPServer class uses Python type hints and docstrings to automatically generate tool definitions, making it easy to create and maintain MCP tools.
143 
144### Helper functions
145 
146Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
147 
148```python theme={null}
149async def make_nws_request(url: str) -> dict[str, Any] | None:
150 """Make a request to the NWS API with proper error handling."""
151 headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
152 async with httpx2.AsyncClient() as client:
153 try:
154 response = await client.get(url, headers=headers, timeout=30.0)
155 response.raise_for_status()
156 return response.json()
157 except Exception:
158 return None
159 
160 
161def format_alert(feature: dict) -> str:
162 """Format an alert feature into a readable string."""
163 props = feature["properties"]
164 return f"""
165Event: {props.get("event", "Unknown")}
166Area: {props.get("areaDesc", "Unknown")}
167Severity: {props.get("severity", "Unknown")}
168Description: {props.get("description", "No description available")}
169Instructions: {props.get("instruction", "No specific instructions provided")}
170"""
171```
172 
173### Implementing tool execution
174 
175The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
176 
177```python theme={null}
178@mcp.tool()
179async def get_alerts(state: str) -> str:
180 """Get weather alerts for a US state.
181 
182 Args:
183 state: Two-letter US state code (e.g. CA, NY)
176184 """
177 ```
178 
179 ### Implementing tool execution
180 
181 The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
182 
183 ```python theme={null}
184 @mcp.tool()
185 async def get_alerts(state: str) -> str:
186 """Get weather alerts for a US state.
187 
188 Args:
189 state: Two-letter US state code (e.g. CA, NY)
185 url = f"{NWS_API_BASE}/alerts/active/area/{state}"
186 data = await make_nws_request(url)
187 
188 if not data or "features" not in data:
189 return "Unable to fetch alerts or no alerts found."
190 
191 if not data["features"]:
192 return "No active alerts for this state."
193 
194 alerts = [format_alert(feature) for feature in data["features"]]
195 return "\n---\n".join(alerts)
196 
197 
198@mcp.tool()
199async def get_forecast(latitude: float, longitude: float) -> str:
200 """Get weather forecast for a location.
201 
202 Args:
203 latitude: Latitude of the location
204 longitude: Longitude of the location
205 """
206 # First get the forecast grid endpoint
207 points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
208 points_data = await make_nws_request(points_url)
209 
210 if not points_data:
211 return "Unable to fetch forecast data for this location."
212 
213 # Get the forecast URL from the points response
214 forecast_url = points_data["properties"]["forecast"]
215 forecast_data = await make_nws_request(forecast_url)
216 
217 if not forecast_data:
218 return "Unable to fetch detailed forecast."
219 
220 # Format the periods into a readable forecast
221 periods = forecast_data["properties"]["periods"]
222 forecasts = []
223 for period in periods[:5]: # Only show next 5 periods
224 forecast = f"""
225{period["name"]}:
226Temperature: {period["temperature"]}°{period["temperatureUnit"]}
227Wind: {period["windSpeed"]} {period["windDirection"]}
228Forecast: {period["detailedForecast"]}
229"""
230 forecasts.append(forecast)
231 
232 return "\n---\n".join(forecasts)
233```
234 
235### Running the server
236 
237Finally, let's initialize and run the server:
238 
239```python theme={null}
240if __name__ == "__main__":
241 mcp.run(transport="stdio")
242```
243 
244Your server is complete! Run `uv run weather.py` to start the MCP server, which will listen for messages from MCP hosts.
245 
246Let's now test your server from an existing MCP host, Claude for Desktop.
247 
248## Testing your server with Claude for Desktop
249 
250First, make sure you have Claude for Desktop installed. [You can install the latest version
251here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
252 
253We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
254 
255For example, if you have [VS Code](https://code.visualstudio.com/) installed:
256 
257```bash Linux theme={null}
258code ~/.config/Claude/claude_desktop_config.json
259```
260 
261```bash macOS theme={null}
262code ~/Library/Application\ Support/Claude/claude_desktop_config.json
263```
264 
265```powershell Windows theme={null}
266code $env:AppData\Claude\claude_desktop_config.json
267```
268 
269You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
270 
271In this case, we'll add our single weather server like so:
272 
273```json macOS/Linux theme={null}
274{
275 "mcpServers": {
276 "weather": {
277 "command": "uv",
278 "args": [
279 "--directory",
280 "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
281 "run",
282 "weather.py"
283 ]
284 }
285 }
286}
287```
288 
289```json Windows theme={null}
290{
291 "mcpServers": {
292 "weather": {
293 "command": "uv",
294 "args": [
295 "--directory",
296 "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather",
297 "run",
298 "weather.py"
299 ]
300 }
301 }
302}
303```
304 
305> [!WARNING]
306>
307> You may need to put the full path to the `uv` executable in the `command` field. You can get this by running `which uv` on macOS/Linux or `where uv` on Windows.
308 
309> [!NOTE]
310>
311> Make sure you pass in the absolute path to your server. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
312 
313This tells Claude for Desktop:
314 
3151. There's an MCP server named "weather"
3162. To launch it by running `uv --directory /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather run weather.py`
317 
318Save the file, and restart **Claude for Desktop**.
319 
320**TypeScript**
321 
322Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-typescript)
323 
324### Prerequisite knowledge
325 
326This quickstart assumes you have familiarity with:
327 
328* TypeScript
329* LLMs like Claude
330 
331### Logging in MCP Servers
332 
333When implementing MCP servers, be careful about how you handle logging:
334 
335**For STDIO-based servers:** Never use `console.log()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
336 
337**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
338 
339### Best Practices
340 
341* Use `console.error()` which writes to stderr, or use a logging library that writes to stderr or files.
342 
343### Quick Examples
344 
345```javascript theme={null}
346// ❌ Bad (STDIO)
347console.log("Server started");
348 
349// ✅ Good (STDIO)
350console.error("Server started"); // stderr is safe
351```
352 
353### System requirements
354 
355For TypeScript, make sure you have the latest version of Node installed.
356 
357### Set up your environment
358 
359First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/).
360Verify your Node.js installation:
361 
362```bash theme={null}
363node --version
364npm --version
365```
366 
367For this tutorial, you'll need Node.js version 20 or higher.
368 
369Now, let's create and set up our project:
370 
371```bash macOS/Linux theme={null}
372# Create a new directory for our project
373mkdir weather
374cd weather
375 
376# Initialize a new npm project
377npm init -y
378 
379# Install dependencies
380npm install @modelcontextprotocol/server zod
381npm install -D @types/node typescript
382 
383# Create our files
384mkdir src
385touch src/index.ts
386```
387 
388```powershell Windows theme={null}
389# Create a new directory for our project
390md weather
391cd weather
392 
393# Initialize a new npm project
394npm init -y
395 
396# Install dependencies
397npm install @modelcontextprotocol/server zod
398npm install -D @types/node typescript
399 
400# Create our files
401md src
402new-item src\index.ts
403```
404 
405Update your package.json to add type: "module" and a build script:
406 
407```json package.json theme={null}
408{
409 "type": "module",
410 "bin": {
411 "weather": "./build/index.js"
412 },
413 "scripts": {
414 "build": "tsc && chmod 755 build/index.js"
415 },
416 "files": ["build"]
417}
418```
419 
420Create a `tsconfig.json` in the root of your project:
421 
422```json tsconfig.json theme={null}
423{
424 "compilerOptions": {
425 "target": "ES2022",
426 "module": "Node16",
427 "moduleResolution": "Node16",
428 "types": ["node"],
429 "outDir": "./build",
430 "rootDir": "./src",
431 "strict": true,
432 "esModuleInterop": true,
433 "skipLibCheck": true,
434 "forceConsistentCasingInFileNames": true
435 },
436 "include": ["src/**/*"],
437 "exclude": ["node_modules"]
438}
439```
440 
441Now let's dive into building your server.
442 
443## Building your server
444 
445### Importing packages and setting up constants
446 
447Add these to the top of your `src/index.ts`:
448 
449```typescript theme={null}
450import { McpServer } from "@modelcontextprotocol/server";
451import { serveStdio } from "@modelcontextprotocol/server/stdio";
452import { z } from "zod";
453 
454const NWS_API_BASE = "https://api.weather.gov";
455const USER_AGENT = "weather-app/1.0";
456```
457 
458### Helper functions
459 
460Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
461 
462```typescript theme={null}
463// Helper function for making NWS API requests
464async function makeNWSRequest<T>(url: string): Promise<T | null> {
465 const headers = {
466 "User-Agent": USER_AGENT,
467 Accept: "application/geo+json",
468 };
469 
470 try {
471 const response = await fetch(url, { headers });
472 if (!response.ok) {
473 throw new Error(`HTTP error! status: ${response.status}`);
474 }
475 return (await response.json()) as T;
476 } catch (error) {
477 console.error("Error making NWS request:", error);
478 return null;
479 }
480}
481 
482interface AlertFeature {
483 properties: {
484 event?: string;
485 areaDesc?: string;
486 severity?: string;
487 status?: string;
488 headline?: string;
489 };
490}
491 
492// Format alert data
493function formatAlert(feature: AlertFeature): string {
494 const props = feature.properties;
495 return [
496 `Event: ${props.event || "Unknown"}`,
497 `Area: ${props.areaDesc || "Unknown"}`,
498 `Severity: ${props.severity || "Unknown"}`,
499 `Status: ${props.status || "Unknown"}`,
500 `Headline: ${props.headline || "No headline"}`,
501 "---",
502 ].join("\n");
503}
504 
505interface ForecastPeriod {
506 name?: string;
507 temperature?: number;
508 temperatureUnit?: string;
509 windSpeed?: string;
510 windDirection?: string;
511 shortForecast?: string;
512}
513 
514interface AlertsResponse {
515 features: AlertFeature[];
516}
517 
518interface PointsResponse {
519 properties: {
520 forecast?: string;
521 };
522}
523 
524interface ForecastResponse {
525 properties: {
526 periods: ForecastPeriod[];
527 };
528}
529```
530 
531### Implementing tool execution
532 
533The tool execution handler is responsible for actually executing the logic of each tool. Let's add it, inside a `createServer()` function that builds the server instance and registers both tools on it:
534 
535```typescript theme={null}
536// Build a server instance and register its tools. serveStdio calls this
537// whenever it needs an instance, so every call must return a fresh one.
538function createServer() {
539 const server = new McpServer({
540 name: "weather",
541 version: "1.0.0",
542 });
543 
544 server.registerTool(
545 "get_alerts",
546 {
547 description: "Get weather alerts for a state",
548 inputSchema: z.object({
549 state: z
550 .string()
551 .length(2)
552 .describe("Two-letter state code (e.g. CA, NY)"),
553 }),
554 },
555 async ({ state }) => {
556 const stateCode = state.toUpperCase();
557 const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
558 const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
559 
560 if (!alertsData) {
561 return {
562 content: [
563 {
564 type: "text",
565 text: "Failed to retrieve alerts data",
566 },
567 ],
568 };
569 }
570 
571 const features = alertsData.features || [];
572 if (!features.length) {
573 return {
574 content: [
575 {
576 type: "text",
577 text: `No active alerts for ${stateCode}`,
578 },
579 ],
580 };
581 }
582 
583 const formattedAlerts = features.map(formatAlert);
584 const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;
585 
586 return {
587 content: [
588 {
589 type: "text",
590 text: alertsText,
591 },
592 ],
593 };
594 },
595 );
596 
597 server.registerTool(
598 "get_forecast",
599 {
600 description: "Get weather forecast for a location",
601 inputSchema: z.object({
602 latitude: z
603 .number()
604 .min(-90)
605 .max(90)
606 .describe("Latitude of the location"),
607 longitude: z
608 .number()
609 .min(-180)
610 .max(180)
611 .describe("Longitude of the location"),
612 }),
613 },
614 async ({ latitude, longitude }) => {
615 // Get grid point data
616 const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
617 const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
618 
619 if (!pointsData) {
620 return {
621 content: [
622 {
623 type: "text",
624 text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
625 },
626 ],
627 };
628 }
629 
630 const forecastUrl = pointsData.properties?.forecast;
631 if (!forecastUrl) {
632 return {
633 content: [
634 {
635 type: "text",
636 text: "Failed to get forecast URL from grid point data",
637 },
638 ],
639 };
640 }
641 
642 // Get forecast data
643 const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
644 if (!forecastData) {
645 return {
646 content: [
647 {
648 type: "text",
649 text: "Failed to retrieve forecast data",
650 },
651 ],
652 };
653 }
654 
655 const periods = forecastData.properties?.periods || [];
656 if (periods.length === 0) {
657 return {
658 content: [
659 {
660 type: "text",
661 text: "No forecast periods available",
662 },
663 ],
664 };
665 }
666 
667 // Format forecast periods
668 const formattedForecast = periods.map((period: ForecastPeriod) =>
669 [
670 `${period.name || "Unknown"}:`,
671 `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
672 `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
673 `${period.shortForecast || "No forecast available"}`,
674 "---",
675 ].join("\n"),
676 );
677 
678 const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;
679 
680 return {
681 content: [
682 {
683 type: "text",
684 text: forecastText,
685 },
686 ],
687 };
688 },
689 );
690 
691 return server;
692}
693```
694 
695### Running the server
696 
697Finally, serve the server over stdio:
698 
699```typescript theme={null}
700serveStdio(createServer, {
701 onerror: (error) => console.error("Weather MCP Server error:", error),
702});
703console.error("Weather MCP Server running on stdio");
704```
705 
706`serveStdio` negotiates the protocol revision with each client, so the same server answers both 2026-07-28 clients and clients that still open with the 2025-era `initialize` handshake. It calls `createServer()` whenever it needs an instance, including once for a `server/discover` probe that is discarded if the client falls back to `initialize`, which is why the factory returns a new `McpServer` on every call. Wiring the server by hand with `server.connect(new StdioServerTransport())` still compiles, but a server started that way only serves 2025-era clients.
707 
708Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect.
709 
710To check that the server answers 2026-07-28 clients, list its tools with the [Inspector CLI](/docs/draft/tools/inspector/cli) in the `modern` [protocol era](/docs/draft/tools/inspector/protocol-eras):
711 
712```bash theme={null}
713npx @modelcontextprotocol/inspector --cli node build/index.js --protocol-era modern --method tools/list
714```
715 
716The Inspector defaults to the `legacy` era, which only exercises the 2025-era handshake, so run the same command once more with `--protocol-era legacy`. Both runs list `get_alerts` and `get_forecast`.
717 
718Let's now test your server from an existing MCP host, Claude for Desktop.
719 
720## Testing your server with Claude for Desktop
721 
722First, make sure you have Claude for Desktop installed. [You can install the latest version
723here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
724 
725We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
726 
727For example, if you have [VS Code](https://code.visualstudio.com/) installed:
728 
729```bash Linux theme={null}
730code ~/.config/Claude/claude_desktop_config.json
731```
732 
733```bash macOS theme={null}
734code ~/Library/Application\ Support/Claude/claude_desktop_config.json
735```
736 
737```powershell Windows theme={null}
738code $env:AppData\Claude\claude_desktop_config.json
739```
740 
741You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
742 
743In this case, we'll add our single weather server like so:
744 
745```json macOS/Linux theme={null}
746{
747 "mcpServers": {
748 "weather": {
749 "command": "node",
750 "args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js"]
751 }
752 }
753}
754```
755 
756```json Windows theme={null}
757{
758 "mcpServers": {
759 "weather": {
760 "command": "node",
761 "args": ["C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\index.js"]
762 }
763 }
764}
765```
766 
767This tells Claude for Desktop:
768 
7691. There's an MCP server named "weather"
7702. Launch it by running `node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js`
771 
772Save the file, and restart **Claude for Desktop**.
773 
774**Java**
775 
776> [!NOTE]
777>
778> This is a quickstart demo based on Spring AI MCP auto-configuration and boot starters.
779> To learn how to create sync and async MCP Servers, manually, consult the [Java SDK Server](https://java.sdk.modelcontextprotocol.io/) documentation.
780 
781Let's get started with building our weather server!
782[You can find the complete code for what we'll be building here.](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-stdio-server)
783 
784For more information, see the [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) reference documentation.
785For manual MCP Server implementation, refer to the [MCP Server Java SDK documentation](https://java.sdk.modelcontextprotocol.io/).
786 
787### Logging in MCP Servers
788 
789When implementing MCP servers, be careful about how you handle logging:
790 
791**For STDIO-based servers:** Never use `System.out.println()` or `System.out.print()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
792 
793**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
794 
795### Best Practices
796 
797* Use a logging library that writes to stderr or files.
798* Ensure any configured logging library will not write to stdout.
799 
800### System requirements
801 
802* Java 17 or higher installed.
803* [Spring Boot 3.3.x](https://docs.spring.io/spring-boot/installing.html) or higher
804 
805### Set up your environment
806 
807Use the [Spring Initializer](https://start.spring.io/) to bootstrap the project.
808 
809You will need to add the following dependencies:
810 
811```xml Maven theme={null}
812<dependencies>
813 <dependency>
814 <groupId>org.springframework.ai</groupId>
815 <artifactId>spring-ai-starter-mcp-server</artifactId>
816 </dependency>
817 
818 <dependency>
819 <groupId>org.springframework</groupId>
820 <artifactId>spring-web</artifactId>
821 </dependency>
822</dependencies>
823```
824 
825```groovy Gradle theme={null}
826dependencies {
827 implementation platform("org.springframework.ai:spring-ai-starter-mcp-server")
828 implementation platform("org.springframework:spring-web")
829}
830```
831 
832Then configure your application by setting the application properties:
833 
834```bash application.properties theme={null}
835spring.main.bannerMode=off
836logging.pattern.console=
837```
838 
839```yaml application.yml theme={null}
840logging:
841 pattern:
842 console:
843spring:
844 main:
845 banner-mode: off
846```
847 
848The [Server Configuration Properties](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html#_configuration_properties) documents all available properties.
849 
850Now let's dive into building your server.
851 
852## Building your server
853 
854### Weather Service
855 
856Let's implement a [WeatherService.java](https://github.com/spring-projects/spring-ai-examples/blob/main/model-context-protocol/weather/starter-stdio-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java) that uses a REST client to query the data from the National Weather Service API:
857 
858```java theme={null}
859@Service
860public class WeatherService {
861 
862 private final RestClient restClient;
863 
864 public WeatherService() {
865 this.restClient = RestClient.builder()
866 .baseUrl("https://api.weather.gov")
867 .defaultHeader("Accept", "application/geo+json")
868 .defaultHeader("User-Agent", "WeatherApiClient/1.0 ([email protected])")
869 .build();
870 }
871 
872 @Tool(description = "Get weather forecast for a specific latitude/longitude")
873 public String getWeatherForecastByLocation(
874 double latitude, // Latitude coordinate
875 double longitude // Longitude coordinate
876 ) {
877 // Returns detailed forecast including:
878 // - Temperature and unit
879 // - Wind speed and direction
880 // - Detailed forecast description
881 }
882 
883 @Tool(description = "Get weather alerts for a US state")
884 public String getAlerts(
885 @ToolParam(description = "Two-letter US state code (e.g. CA, NY)") String state
886 ) {
887 // Returns active alerts including:
888 // - Event type
889 // - Affected area
890 // - Severity
891 // - Description
892 // - Safety instructions
893 }
894 
895 // ......
896}
897```
898 
899The `@Service` annotation will auto-register the service in your application context.
900The Spring AI `@Tool` annotation makes it easy to create and maintain MCP tools.
901 
902The auto-configuration will automatically register these tools with the MCP server.
903 
904### Create your Boot Application
905 
906```java theme={null}
907@SpringBootApplication
908public class McpServerApplication {
909 
910 public static void main(String[] args) {
911 SpringApplication.run(McpServerApplication.class, args);
912 }
913 
914 @Bean
915 public ToolCallbackProvider weatherTools(WeatherService weatherService) {
916 return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
917 }
918}
919```
920 
921Uses the `MethodToolCallbackProvider` utils to convert the `@Tools` into actionable callbacks used by the MCP server.
922 
923### Running the server
924 
925Finally, let's build the server:
926 
927```bash theme={null}
928./mvnw clean install
929```
930 
931This will generate an `mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar` file within the `target` folder.
932 
933Let's now test your server from an existing MCP host, Claude for Desktop.
934 
935## Testing your server with Claude for Desktop
936 
937First, make sure you have Claude for Desktop installed.
938[You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
939 
940We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
941To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
942Make sure to create the file if it doesn't exist.
943 
944For example, if you have [VS Code](https://code.visualstudio.com/) installed:
945 
946```bash Linux theme={null}
947code ~/.config/Claude/claude_desktop_config.json
948```
949 
950```bash macOS theme={null}
951code ~/Library/Application\ Support/Claude/claude_desktop_config.json
952```
953 
954```powershell Windows theme={null}
955code $env:AppData\Claude\claude_desktop_config.json
956```
957 
958You'll then add your servers in the `mcpServers` key.
959The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
960 
961In this case, we'll add our single weather server like so:
962 
963```json macOS/Linux theme={null}
964{
965 "mcpServers": {
966 "spring-ai-mcp-weather": {
967 "command": "java",
968 "args": [
969 "-Dspring.ai.mcp.server.stdio=true",
970 "-jar",
971 "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
972 ]
973 }
974 }
975}
976```
977 
978```json Windows theme={null}
979{
980 "mcpServers": {
981 "spring-ai-mcp-weather": {
982 "command": "java",
983 "args": [
984 "-Dspring.ai.mcp.server.transport=STDIO",
985 "-jar",
986 "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
987 ]
988 }
989 }
990}
991```
992 
993> [!NOTE]
994>
995> Make sure you pass in the absolute path to your server.
996 
997This tells Claude for Desktop:
998 
9991. There's an MCP server named "my-weather-server"
10002. To launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar`
1001 
1002Save the file, and restart **Claude for Desktop**.
1003 
1004## Testing your server with Java client
1005 
1006### Create an MCP Client manually
1007 
1008Use the `McpClient` to connect to the server:
1009 
1010```java theme={null}
1011var stdioParams = ServerParameters.builder("java")
1012 .args("-jar", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar")
1013 .build();
1014 
1015var stdioTransport = new StdioClientTransport(stdioParams);
1016 
1017var mcpClient = McpClient.sync(stdioTransport).build();
1018 
1019mcpClient.initialize();
1020 
1021ListToolsResult toolsList = mcpClient.listTools();
1022 
1023CallToolResult weather = mcpClient.callTool(
1024 new CallToolRequest("getWeatherForecastByLocation",
1025 Map.of("latitude", "47.6062", "longitude", "-122.3321")));
1026 
1027CallToolResult alert = mcpClient.callTool(
1028 new CallToolRequest("getAlerts", Map.of("state", "NY")));
1029 
1030mcpClient.closeGracefully();
1031```
1032 
1033### Use MCP Client Boot Starter
1034 
1035Create a new boot starter application using the `spring-ai-starter-mcp-client` dependency:
1036 
1037```xml theme={null}
1038<dependency>
1039 <groupId>org.springframework.ai</groupId>
1040 <artifactId>spring-ai-starter-mcp-client</artifactId>
1041</dependency>
1042```
1043 
1044and set the `spring.ai.mcp.client.stdio.servers-configuration` property to point to your `claude_desktop_config.json`.
1045You can reuse the existing Anthropic Desktop configuration:
1046 
1047```properties theme={null}
1048spring.ai.mcp.client.stdio.servers-configuration=file:PATH/TO/claude_desktop_config.json
1049```
1050 
1051When you start your client application, the auto-configuration will automatically create MCP clients from the claude\_desktop\_config.json.
1052 
1053For more information, see the [MCP Client Boot Starters](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) reference documentation.
1054 
1055## More Java MCP Server examples
1056 
1057The [starter-webflux-server](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-webflux-server) demonstrates how to create an HTTP-based MCP server with the WebFlux starter.
1058Set the `spring.ai.mcp.server.protocol=STREAMABLE` property to serve it over Streamable HTTP.
1059It showcases how to define and register MCP Tools, Resources, and Prompts, using the Spring Boot's auto-configuration capabilities.
1060 
1061**Kotlin**
1062 
1063Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/samples/weather-stdio-server)
1064 
1065### Prerequisite knowledge
1066 
1067This quickstart assumes you have familiarity with:
1068 
1069* Kotlin
1070* LLMs like Claude
1071 
1072### Logging in MCP Servers
1073 
1074When implementing MCP servers, be careful about how you handle logging:
1075 
1076**For STDIO-based servers:** Never use `println()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1077 
1078**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1079 
1080### Best Practices
1081 
1082* Use a logging library that writes to stderr or files.
1083 
1084### System requirements
1085 
1086* JDK 11 or higher installed.
1087 
1088### Set up your environment
1089 
1090First, let's install `java` and `gradle` if you haven't already.
1091You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
1092Verify your `java` installation:
1093 
1094```bash theme={null}
1095java --version
1096```
1097 
1098Now, let's create and set up your project:
1099 
1100```bash macOS/Linux theme={null}
1101# Create a new directory for our project
1102mkdir weather
1103cd weather
1104 
1105# Initialize a new kotlin project
1106gradle init
1107```
1108 
1109```powershell Windows theme={null}
1110# Create a new directory for our project
1111md weather
1112cd weather
1113 
1114# Initialize a new kotlin project
1115gradle init
1116```
1117 
1118After running `gradle init`, select **Application** as the project type, **Kotlin** as the programming language.
1119 
1120Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
1121 
1122After creating the project, replace the contents of your `build.gradle.kts` with:
1123 
1124```kotlin build.gradle.kts theme={null}
1125// Check latest versions at https://github.com/modelcontextprotocol/kotlin-sdk/releases
1126val mcpVersion = "0.9.0"
1127val ktorVersion = "3.2.3"
1128val slf4jVersion = "2.0.17"
1129 
1130plugins {
1131 kotlin("jvm") version "2.3.20"
1132 kotlin("plugin.serialization") version "2.3.20"
1133 id("com.gradleup.shadow") version "8.3.9"
1134 application
1135}
1136 
1137application {
1138 mainClass.set("MainKt")
1139}
1140 
1141dependencies {
1142 implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
1143 implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
1144 implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
1145 implementation("io.ktor:ktor-client-cio:$ktorVersion")
1146 implementation("org.slf4j:slf4j-simple:$slf4jVersion")
1147}
1148```
1149 
1150Verify that everything is set up correctly:
1151 
1152```bash theme={null}
1153./gradlew build
1154```
1155 
1156Now let's dive into building your server.
1157 
1158## Building your server
1159 
1160### Setting up the instance
1161 
1162Add a server initialization function:
1163 
1164```kotlin theme={null}
1165fun runMcpServer() {
1166 val server = Server(
1167 Implementation(
1168 name = "weather",
1169 version = "1.0.0",
1170 ),
1171 ServerOptions(
1172 capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)),
1173 ),
1174 )
1175 
1176 // register tools on server here
1177 
1178 val transport = StdioServerTransport(
1179 System.`in`.asInput(),
1180 System.out.asSink().buffered(),
1181 )
1182 
1183 runBlocking {
1184 val session = server.createSession(transport)
1185 val done = Job()
1186 session.onClose {
1187 done.complete()
1188 }
1189 done.join()
1190 }
1191}
1192```
1193 
1194### Weather API helper functions
1195 
1196Next, let's add functions and data classes for querying and converting responses from the National Weather Service API:
1197 
1198```kotlin theme={null}
1199val httpClient = HttpClient(CIO) {
1200 defaultRequest {
1201 url("https://api.weather.gov")
1202 headers {
1203 append("Accept", "application/geo+json")
1204 append("User-Agent", "WeatherApiClient/1.0")
1205 }
1206 contentType(ContentType.Application.Json)
1207 }
1208 install(ContentNegotiation) {
1209 json(Json { ignoreUnknownKeys = true })
1210 }
1211}
1212 
1213// Extension function to fetch weather alerts for a given state
1214suspend fun HttpClient.getAlerts(state: String): List<String> {
1215 val alerts = this.get("/alerts/active/area/$state").body<AlertsResponse>()
1216 return alerts.features.map { feature ->
1901217 """
191 url = f"{NWS_API_BASE}/alerts/active/area/{state}"
192 data = await make_nws_request(url)
193 
194 if not data or "features" not in data:
195 return "Unable to fetch alerts or no alerts found."
196 
197 if not data["features"]:
198 return "No active alerts for this state."
199 
200 alerts = [format_alert(feature) for feature in data["features"]]
201 return "\n---\n".join(alerts)
202 
203 
204 @mcp.tool()
205 async def get_forecast(latitude: float, longitude: float) -> str:
206 """Get weather forecast for a location.
207 
208 Args:
209 latitude: Latitude of the location
210 longitude: Longitude of the location
1218 Event: ${feature.properties.event}
1219 Area: ${feature.properties.areaDesc}
1220 Severity: ${feature.properties.severity}
1221 Status: ${feature.properties.status}
1222 Headline: ${feature.properties.headline}
1223 """.trimIndent()
1224 }
1225}
1226 
1227// Extension function to fetch forecast information for given latitude and longitude
1228suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List<String> {
1229 val points = this.get("/points/$latitude,$longitude").body<PointsResponse>()
1230 val forecastUrl = points.properties.forecast ?: error("No forecast URL available")
1231 val forecast = this.get(forecastUrl).body<ForecastResponse>()
1232 return forecast.properties.periods.map { period ->
2111233 """
212 # First get the forecast grid endpoint
213 points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
214 points_data = await make_nws_request(points_url)
215 
216 if not points_data:
217 return "Unable to fetch forecast data for this location."
218 
219 # Get the forecast URL from the points response
220 forecast_url = points_data["properties"]["forecast"]
221 forecast_data = await make_nws_request(forecast_url)
222 
223 if not forecast_data:
224 return "Unable to fetch detailed forecast."
225 
226 # Format the periods into a readable forecast
227 periods = forecast_data["properties"]["periods"]
228 forecasts = []
229 for period in periods[:5]: # Only show next 5 periods
230 forecast = f"""
231 {period["name"]}:
232 Temperature: {period["temperature"]}°{period["temperatureUnit"]}
233 Wind: {period["windSpeed"]} {period["windDirection"]}
234 Forecast: {period["detailedForecast"]}
235 """
236 forecasts.append(forecast)
237 
238 return "\n---\n".join(forecasts)
239 ```
240 
241 ### Running the server
242 
243 Finally, let's initialize and run the server:
244 
245 ```python theme={null}
246 if __name__ == "__main__":
247 mcp.run(transport="stdio")
248 ```
249 
250 Your server is complete! Run `uv run weather.py` to start the MCP server, which will listen for messages from MCP hosts.
251 
252 Let's now test your server from an existing MCP host, Claude for Desktop.
253 
254 ## Testing your server with Claude for Desktop
255 
256 First, make sure you have Claude for Desktop installed. [You can install the latest version
257 here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
258 
259 We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
260 
261 For example, if you have [VS Code](https://code.visualstudio.com/) installed:
262 
263 <CodeGroup>
264 ```bash Linux theme={null}
265 code ~/.config/Claude/claude_desktop_config.json
266 ```
267 
268 ```bash macOS theme={null}
269 code ~/Library/Application\ Support/Claude/claude_desktop_config.json
270 ```
271 
272 ```powershell Windows theme={null}
273 code $env:AppData\Claude\claude_desktop_config.json
274 ```
275 </CodeGroup>
276 
277 You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
278 
279 In this case, we'll add our single weather server like so:
280 
281 <CodeGroup>
282 ```json macOS/Linux theme={null}
283 {
284 "mcpServers": {
285 "weather": {
286 "command": "uv",
287 "args": [
288 "--directory",
289 "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
290 "run",
291 "weather.py"
292 ]
293 }
1234 ${period.name}:
1235 Temperature: ${period.temperature}°${period.temperatureUnit}
1236 Wind: ${period.windSpeed} ${period.windDirection}
1237 ${period.shortForecast}
1238 """.trimIndent()
1239 }
1240}
1241 
1242@Serializable
1243data class PointsResponse(val properties: PointsProperties)
1244 
1245@Serializable
1246data class PointsProperties(val forecast: String? = null)
1247 
1248@Serializable
1249data class ForecastResponse(val properties: ForecastProperties)
1250 
1251@Serializable
1252data class ForecastProperties(val periods: List<ForecastPeriod> = emptyList())
1253 
1254@Serializable
1255data class ForecastPeriod(
1256 val name: String? = null,
1257 val temperature: Int? = null,
1258 val temperatureUnit: String? = null,
1259 val windSpeed: String? = null,
1260 val windDirection: String? = null,
1261 val shortForecast: String? = null,
1262)
1263 
1264@Serializable
1265data class AlertsResponse(val features: List<AlertFeature> = emptyList())
1266 
1267@Serializable
1268data class AlertFeature(val properties: AlertProperties)
1269 
1270@Serializable
1271data class AlertProperties(
1272 val event: String? = null,
1273 val areaDesc: String? = null,
1274 val severity: String? = null,
1275 val status: String? = null,
1276 val headline: String? = null,
1277)
1278```
1279 
1280### Implementing tool execution
1281 
1282The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
1283 
1284```kotlin theme={null}
1285// Register weather tools
1286 
1287server.addTool(
1288 name = "get_alerts",
1289 description = "Get weather alerts for a US state. Input is a two-letter US state code (e.g. CA, NY)",
1290 inputSchema = ToolSchema(
1291 properties = buildJsonObject {
1292 putJsonObject("state") {
1293 put("type", "string")
1294 put("description", "Two-letter US state code (e.g. CA, NY)")
1295 }
1296 },
1297 required = listOf("state"),
1298 ),
1299) { request ->
1300 val state = request.arguments?.get("state")?.jsonPrimitive?.content
1301 ?: return@addTool CallToolResult(
1302 content = listOf(TextContent("The 'state' parameter is required.")),
1303 )
1304 
1305 val alerts = httpClient.getAlerts(state)
1306 CallToolResult(content = alerts.map { TextContent(it) })
1307}
1308 
1309server.addTool(
1310 name = "get_forecast",
1311 description = "Get weather forecast for a location. Note: only US locations are supported by the NWS API.",
1312 inputSchema = ToolSchema(
1313 properties = buildJsonObject {
1314 putJsonObject("latitude") {
1315 put("type", "number")
1316 put("description", "Latitude of the location")
1317 }
1318 putJsonObject("longitude") {
1319 put("type", "number")
1320 put("description", "Longitude of the location")
1321 }
1322 },
1323 required = listOf("latitude", "longitude"),
1324 ),
1325) { request ->
1326 val latitude = request.arguments?.get("latitude")?.jsonPrimitive?.doubleOrNull
1327 val longitude = request.arguments?.get("longitude")?.jsonPrimitive?.doubleOrNull
1328 if (latitude == null || longitude == null) {
1329 return@addTool CallToolResult(
1330 content = listOf(TextContent("The 'latitude' and 'longitude' parameters are required.")),
1331 )
1332 }
1333 
1334 val forecast = httpClient.getForecast(latitude, longitude)
1335 CallToolResult(content = forecast.map { TextContent(it) })
1336}
1337```
1338 
1339### Running the server
1340 
1341Finally, implement the main function to run the server:
1342 
1343```kotlin theme={null}
1344fun main() = runMcpServer()
1345```
1346 
1347You can run the server directly during development:
1348 
1349```bash theme={null}
1350./gradlew run
1351```
1352 
1353For production use, build the shadow JAR:
1354 
1355```bash theme={null}
1356./gradlew build
1357java -jar build/libs/weather-0.1.0-all.jar
1358```
1359 
1360Let's now test your server from an existing MCP host, Claude for Desktop.
1361 
1362## Testing your server with Claude for Desktop
1363 
1364First, make sure you have Claude for Desktop installed. [You can install the latest version
1365here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1366 
1367We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
1368To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
1369Make sure to create the file if it doesn't exist.
1370 
1371For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1372 
1373```bash Linux theme={null}
1374code ~/.config/Claude/claude_desktop_config.json
1375```
1376 
1377```bash macOS theme={null}
1378code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1379```
1380 
1381```powershell Windows theme={null}
1382code $env:AppData\Claude\claude_desktop_config.json
1383```
1384 
1385You'll then add your servers in the `mcpServers` key.
1386The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1387 
1388In this case, we'll add our single weather server like so:
1389 
1390```json macOS/Linux theme={null}
1391{
1392 "mcpServers": {
1393 "weather": {
1394 "command": "java",
1395 "args": [
1396 "-jar",
1397 "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar"
1398 ]
1399 }
1400 }
1401}
1402```
1403 
1404```json Windows theme={null}
1405{
1406 "mcpServers": {
1407 "weather": {
1408 "command": "java",
1409 "args": [
1410 "-jar",
1411 "C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\libs\\weather-0.1.0-all.jar"
1412 ]
1413 }
1414 }
1415}
1416```
1417 
1418This tells Claude for Desktop:
1419 
14201. There's an MCP server named "weather"
14212. Launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar`
1422 
1423Save the file, and restart **Claude for Desktop**.
1424 
1425**C#**
1426 
1427Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/QuickstartWeatherServer)
1428 
1429### Prerequisite knowledge
1430 
1431This quickstart assumes you have familiarity with:
1432 
1433* C#
1434* LLMs like Claude
1435* .NET 8 or higher
1436 
1437### Logging in MCP Servers
1438 
1439When implementing MCP servers, be careful about how you handle logging:
1440 
1441**For STDIO-based servers:** Never use `Console.WriteLine()` or `Console.Write()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
1442 
1443**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1444 
1445### Best Practices
1446 
1447* Use a logging library that writes to stderr or files.
1448 
1449### System requirements
1450 
1451* [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or higher installed.
1452 
1453### Set up your environment
1454 
1455First, let's install `dotnet` if you haven't already. You can download `dotnet` from [official Microsoft .NET website](https://dotnet.microsoft.com/download/). Verify your `dotnet` installation:
1456 
1457```bash theme={null}
1458dotnet --version
1459```
1460 
1461Now, let's create and set up your project:
1462 
1463```bash macOS/Linux theme={null}
1464# Create a new directory for our project
1465mkdir weather
1466cd weather
1467# Initialize a new C# project
1468dotnet new console
1469```
1470 
1471```powershell Windows theme={null}
1472# Create a new directory for our project
1473mkdir weather
1474cd weather
1475# Initialize a new C# project
1476dotnet new console
1477```
1478 
1479After running `dotnet new console`, you will be presented with a new C# project.
1480You can open the project in your favorite IDE, such as [Visual Studio](https://visualstudio.microsoft.com/) or [Rider](https://www.jetbrains.com/rider/).
1481Alternatively, you can create a C# application using the [Visual Studio project wizard](https://learn.microsoft.com/en-us/visualstudio/get-started/csharp/tutorial-console?view=vs-2022).
1482After creating the project, add NuGet package for the Model Context Protocol SDK and hosting:
1483 
1484```bash theme={null}
1485# Add the Model Context Protocol SDK NuGet package
1486dotnet add package ModelContextProtocol --prerelease
1487# Add the .NET Hosting NuGet package
1488dotnet add package Microsoft.Extensions.Hosting
1489```
1490 
1491Now let’s dive into building your server.
1492 
1493## Building your server
1494 
1495Open the `Program.cs` file in your project and replace its contents with the following code:
1496 
1497```csharp theme={null}
1498using Microsoft.Extensions.DependencyInjection;
1499using Microsoft.Extensions.Hosting;
1500using ModelContextProtocol;
1501using System.Net.Http.Headers;
1502 
1503var builder = Host.CreateEmptyApplicationBuilder(settings: null);
1504 
1505builder.Services.AddMcpServer()
1506 .WithStdioServerTransport()
1507 .WithToolsFromAssembly();
1508 
1509builder.Services.AddSingleton(_ =>
1510{
1511 var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") };
1512 client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
1513 return client;
1514});
1515 
1516var app = builder.Build();
1517 
1518await app.RunAsync();
1519```
1520 
1521> [!NOTE]
1522>
1523> When creating the `ApplicationHostBuilder`, ensure you use `CreateEmptyApplicationBuilder` instead of `CreateDefaultBuilder`. This ensures that the server does not write any additional messages to the console. This is only necessary for servers using STDIO transport.
1524 
1525This code sets up a basic console application that uses the Model Context Protocol SDK to create an MCP server with standard I/O transport.
1526 
1527### Weather API helper functions
1528 
1529Create an extension class for `HttpClient` which helps simplify JSON request handling:
1530 
1531```csharp theme={null}
1532using System.Text.Json;
1533 
1534internal static class HttpClientExt
1535{
1536 public static async Task<JsonDocument> ReadJsonDocumentAsync(this HttpClient client, string requestUri)
1537 {
1538 using var response = await client.GetAsync(requestUri);
1539 response.EnsureSuccessStatusCode();
1540 return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
1541 }
1542}
1543```
1544 
1545Next, define a class with the tool execution handlers for querying and converting responses from the National Weather Service API:
1546 
1547```csharp theme={null}
1548using ModelContextProtocol.Server;
1549using System.ComponentModel;
1550using System.Globalization;
1551using System.Text.Json;
1552 
1553namespace QuickstartWeatherServer.Tools;
1554 
1555[McpServerToolType]
1556public static class WeatherTools
1557{
1558 [McpServerTool, Description("Get weather alerts for a US state code.")]
1559 public static async Task<string> GetAlerts(
1560 HttpClient client,
1561 [Description("The US state code to get alerts for.")] string state)
1562 {
1563 using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}");
1564 var jsonElement = jsonDocument.RootElement;
1565 var alerts = jsonElement.GetProperty("features").EnumerateArray();
1566 
1567 if (!alerts.Any())
1568 {
1569 return "No active alerts for this state.";
2941570 }
1571 
1572 return string.Join("\n--\n", alerts.Select(alert =>
1573 {
1574 JsonElement properties = alert.GetProperty("properties");
1575 return $"""
1576 Event: {properties.GetProperty("event").GetString()}
1577 Area: {properties.GetProperty("areaDesc").GetString()}
1578 Severity: {properties.GetProperty("severity").GetString()}
1579 Description: {properties.GetProperty("description").GetString()}
1580 Instruction: {properties.GetProperty("instruction").GetString()}
1581 """;
1582 }));
1583 }
1584 
1585 [McpServerTool, Description("Get weather forecast for a location.")]
1586 public static async Task<string> GetForecast(
1587 HttpClient client,
1588 [Description("Latitude of the location.")] double latitude,
1589 [Description("Longitude of the location.")] double longitude)
1590 {
1591 var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
1592 using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl);
1593 var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
1594 ?? throw new Exception($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");
1595 
1596 using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl);
1597 var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray();
1598 
1599 return string.Join("\n---\n", periods.Select(period => $"""
1600 {period.GetProperty("name").GetString()}
1601 Temperature: {period.GetProperty("temperature").GetInt32()}°F
1602 Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
1603 Forecast: {period.GetProperty("detailedForecast").GetString()}
1604 """));
1605 }
1606}
1607```
1608 
1609### Running the server
1610 
1611Finally, run the server using the following command:
1612 
1613```bash theme={null}
1614dotnet run
1615```
1616 
1617This will start the server and listen for incoming requests on standard input/output.
1618 
1619## Testing your server with Claude for Desktop
1620 
1621First, make sure you have Claude for Desktop installed. [You can install the latest version
1622here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1623We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1624For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1625 
1626```bash Linux theme={null}
1627code ~/.config/Claude/claude_desktop_config.json
1628```
1629 
1630```bash macOS theme={null}
1631code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1632```
1633 
1634```powershell Windows theme={null}
1635code $env:AppData\Claude\claude_desktop_config.json
1636```
1637 
1638You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1639In this case, we'll add our single weather server like so:
1640 
1641```json macOS/Linux theme={null}
1642{
1643 "mcpServers": {
1644 "weather": {
1645 "command": "dotnet",
1646 "args": ["run", "--project", "/ABSOLUTE/PATH/TO/PROJECT", "--no-build"]
1647 }
1648 }
1649}
1650```
1651 
1652```json Windows theme={null}
1653{
1654 "mcpServers": {
1655 "weather": {
1656 "command": "dotnet",
1657 "args": [
1658 "run",
1659 "--project",
1660 "C:\\ABSOLUTE\\PATH\\TO\\PROJECT",
1661 "--no-build"
1662 ]
1663 }
1664 }
1665}
1666```
1667 
1668This tells Claude for Desktop:
1669 
16701. There's an MCP server named "weather"
16712. Launch it by running `dotnet run /ABSOLUTE/PATH/TO/PROJECT`
1672 Save the file, and restart **Claude for Desktop**.
1673 
1674**Ruby**
1675 
1676Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-ruby)
1677 
1678### Prerequisite knowledge
1679 
1680This quickstart assumes you have familiarity with:
1681 
1682* Ruby
1683* LLMs like Claude
1684 
1685### Logging in MCP Servers
1686 
1687When implementing MCP servers, be careful about how you handle logging:
1688 
1689**For STDIO-based servers:** Never use `puts` or `print`, as they write to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1690 
1691**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1692 
1693### Best Practices
1694 
1695* Use a logging library that writes to stderr or files.
1696 
1697### Quick Examples
1698 
1699```ruby theme={null}
1700# ❌ Bad (STDIO)
1701puts "Processing request"
1702 
1703# ✅ Good (STDIO)
1704require "logger"
1705logger = Logger.new($stderr)
1706logger.info("Processing request")
1707```
1708 
1709### System requirements
1710 
1711* Ruby 2.7 or higher installed.
1712 
1713### Set up your environment
1714 
1715First, let's make sure you have Ruby installed. You can check by running:
1716 
1717```bash theme={null}
1718ruby --version
1719```
1720 
1721Now, let's create and set up our project:
1722 
1723```bash macOS/Linux theme={null}
1724# Create a new directory for our project
1725mkdir weather
1726cd weather
1727 
1728# Create a Gemfile
1729bundle init
1730 
1731# Add the MCP SDK dependency
1732bundle add mcp
1733 
1734# Create our server file
1735touch weather.rb
1736```
1737 
1738```powershell Windows theme={null}
1739# Create a new directory for our project
1740mkdir weather
1741cd weather
1742 
1743# Create a Gemfile
1744bundle init
1745 
1746# Add the MCP SDK dependency
1747bundle add mcp
1748 
1749# Create our server file
1750new-item weather.rb
1751```
1752 
1753Now let's dive into building your server.
1754 
1755## Building your server
1756 
1757### Importing packages and setting up constants
1758 
1759Open `weather.rb` and add these requires and constants at the top:
1760 
1761```ruby theme={null}
1762require "json"
1763require "mcp"
1764require "net/http"
1765require "uri"
1766 
1767NWS_API_BASE = "https://api.weather.gov"
1768USER_AGENT = "weather-app/1.0"
1769```
1770 
1771The `mcp` gem provides the Model Context Protocol SDK for Ruby, with classes for server implementation and stdio transport.
1772 
1773### Helper methods
1774 
1775Next, let's add helper methods for querying and formatting data from the National Weather Service API:
1776 
1777```ruby theme={null}
1778module HelperMethods
1779 def make_nws_request(url)
1780 uri = URI(url)
1781 request = Net::HTTP::Get.new(uri)
1782 request["User-Agent"] = USER_AGENT
1783 request["Accept"] = "application/geo+json"
1784 
1785 response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
1786 http.request(request)
1787 end
1788 
1789 raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
1790 
1791 JSON.parse(response.body)
1792 end
1793 
1794 def format_alert(feature)
1795 properties = feature["properties"]
1796 
1797 <<~ALERT
1798 Event: #{properties["event"] || "Unknown"}
1799 Area: #{properties["areaDesc"] || "Unknown"}
1800 Severity: #{properties["severity"] || "Unknown"}
1801 Description: #{properties["description"] || "No description available"}
1802 Instructions: #{properties["instruction"] || "No specific instructions provided"}
1803 ALERT
1804 end
1805end
1806```
1807 
1808### Implementing tool execution
1809 
1810Now let's define our tool classes. Each tool subclasses `MCP::Tool` and implements the tool logic:
1811 
1812```ruby theme={null}
1813class GetAlerts < MCP::Tool
1814 extend HelperMethods
1815 
1816 tool_name "get_alerts"
1817 description "Get weather alerts for a US state"
1818 input_schema(
1819 properties: {
1820 state: {
1821 type: "string",
1822 description: "Two-letter US state code (e.g. CA, NY)"
2951823 }
296 ```
297 
298 ```json Windows theme={null}
299 {
300 "mcpServers": {
301 "weather": {
302 "command": "uv",
303 "args": [
304 "--directory",
305 "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather",
306 "run",
307 "weather.py"
308 ]
309 }
310 }
1824 },
1825 required: ["state"]
1826 )
1827 
1828 def self.call(state:)
1829 url = "#{NWS_API_BASE}/alerts/active/area/#{state.upcase}"
1830 data = make_nws_request(url)
1831 
1832 if data["features"].empty?
1833 return MCP::Tool::Response.new([{
1834 type: "text",
1835 text: "No active alerts for this state."
1836 }])
1837 end
1838 
1839 alerts = data["features"].map { |feature| format_alert(feature) }
1840 MCP::Tool::Response.new([{
1841 type: "text",
1842 text: alerts.join("\n---\n")
1843 }])
1844 end
1845end
1846 
1847class GetForecast < MCP::Tool
1848 extend HelperMethods
1849 
1850 tool_name "get_forecast"
1851 description "Get weather forecast for a location"
1852 input_schema(
1853 properties: {
1854 latitude: {
1855 type: "number",
1856 description: "Latitude of the location"
1857 },
1858 longitude: {
1859 type: "number",
1860 description: "Longitude of the location"
3111861 }
312 ```
313 </CodeGroup>
314 
315 <Warning>
316 You may need to put the full path to the `uv` executable in the `command` field. You can get this by running `which uv` on macOS/Linux or `where uv` on Windows.
317 </Warning>
318 
319 <Note>
320 Make sure you pass in the absolute path to your server. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
321 </Note>
322 
323 This tells Claude for Desktop:
324 
325 1. There's an MCP server named "weather"
326 2. To launch it by running `uv --directory /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather run weather.py`
327 
328 Save the file, and restart **Claude for Desktop**.
329 </Tab>
330 
331 <Tab title="TypeScript">
332 Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-typescript)
333 
334 ### Prerequisite knowledge
335 
336 This quickstart assumes you have familiarity with:
337 
338 * TypeScript
339 * LLMs like Claude
340 
341 ### Logging in MCP Servers
342 
343 When implementing MCP servers, be careful about how you handle logging:
344 
345 **For STDIO-based servers:** Never use `console.log()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
346 
347 **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
348 
349 ### Best Practices
350 
351 * Use `console.error()` which writes to stderr, or use a logging library that writes to stderr or files.
352 
353 ### Quick Examples
354 
355 ```javascript theme={null}
356 // ❌ Bad (STDIO)
357 console.log("Server started");
358 
359 // ✅ Good (STDIO)
360 console.error("Server started"); // stderr is safe
361 ```
362 
363 ### System requirements
364 
365 For TypeScript, make sure you have the latest version of Node installed.
366 
367 ### Set up your environment
368 
369 First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/).
370 Verify your Node.js installation:
371 
372 ```bash theme={null}
373 node --version
374 npm --version
375 ```
376 
377 For this tutorial, you'll need Node.js version 20 or higher.
378 
379 Now, let's create and set up our project:
380 
381 <CodeGroup>
382 ```bash macOS/Linux theme={null}
383 # Create a new directory for our project
384 mkdir weather
385 cd weather
386 
387 # Initialize a new npm project
388 npm init -y
389 
390 # Install dependencies
391 npm install @modelcontextprotocol/server zod
392 npm install -D @types/node typescript
393 
394 # Create our files
395 mkdir src
396 touch src/index.ts
397 ```
398 
399 ```powershell Windows theme={null}
400 # Create a new directory for our project
401 md weather
402 cd weather
403 
404 # Initialize a new npm project
405 npm init -y
406 
407 # Install dependencies
408 npm install @modelcontextprotocol/server zod
409 npm install -D @types/node typescript
410 
411 # Create our files
412 md src
413 new-item src\index.ts
414 ```
415 </CodeGroup>
416 
417 Update your package.json to add type: "module" and a build script:
418 
419 ```json package.json theme={null}
420 {
421 "type": "module",
422 "bin": {
423 "weather": "./build/index.js"
424 },
425 "scripts": {
426 "build": "tsc && chmod 755 build/index.js"
427 },
428 "files": ["build"]
1862 },
1863 required: ["latitude", "longitude"]
1864 )
1865 
1866 def self.call(latitude:, longitude:)
1867 # First get the forecast grid endpoint.
1868 points_url = "#{NWS_API_BASE}/points/#{latitude},#{longitude}"
1869 points_data = make_nws_request(points_url)
1870 
1871 # Get the forecast URL from the points response.
1872 forecast_url = points_data["properties"]["forecast"]
1873 forecast_data = make_nws_request(forecast_url)
1874 
1875 # Format the periods into a readable forecast.
1876 periods = forecast_data["properties"]["periods"]
1877 forecasts = periods.first(5).map do |period|
1878 <<~FORECAST
1879 #{period["name"]}:
1880 Temperature: #{period["temperature"]}°#{period["temperatureUnit"]}
1881 Wind: #{period["windSpeed"]} #{period["windDirection"]}
1882 Forecast: #{period["detailedForecast"]}
1883 FORECAST
1884 end
1885 
1886 MCP::Tool::Response.new([{
1887 type: "text",
1888 text: forecasts.join("\n---\n")
1889 }])
1890 end
1891end
1892```
1893 
1894### Running the server
1895 
1896Finally, initialize and run the server:
1897 
1898```ruby theme={null}
1899server = MCP::Server.new(
1900 name: "weather",
1901 version: "1.0.0",
1902 tools: [GetAlerts, GetForecast]
1903)
1904 
1905transport = MCP::Server::Transports::StdioTransport.new(server)
1906transport.open
1907```
1908 
1909Your server is complete! Run `bundle exec ruby weather.rb` to start the MCP server, which will listen for messages from MCP hosts.
1910 
1911Let's now test your server from an existing MCP host, Claude for Desktop.
1912 
1913## Testing your server with Claude for Desktop
1914 
1915First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1916 
1917We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1918 
1919For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1920 
1921```bash Linux theme={null}
1922code ~/.config/Claude/claude_desktop_config.json
1923```
1924 
1925```bash macOS theme={null}
1926code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1927```
1928 
1929```powershell Windows theme={null}
1930code $env:AppData\Claude\claude_desktop_config.json
1931```
1932 
1933You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1934 
1935In this case, we'll add our single weather server like so:
1936 
1937```json macOS/Linux theme={null}
1938{
1939 "mcpServers": {
1940 "weather": {
1941 "command": "bundle",
1942 "args": ["exec", "ruby", "weather.rb"],
1943 "cwd": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather"
4291944 }
430 ```
431 
432 Create a `tsconfig.json` in the root of your project:
433 
434 ```json tsconfig.json theme={null}
435 {
436 "compilerOptions": {
437 "target": "ES2022",
438 "module": "Node16",
439 "moduleResolution": "Node16",
440 "types": ["node"],
441 "outDir": "./build",
442 "rootDir": "./src",
443 "strict": true,
444 "esModuleInterop": true,
445 "skipLibCheck": true,
446 "forceConsistentCasingInFileNames": true
447 },
448 "include": ["src/**/*"],
449 "exclude": ["node_modules"]
1945 }
1946}
1947```
1948 
1949```json Windows theme={null}
1950{
1951 "mcpServers": {
1952 "weather": {
1953 "command": "bundle",
1954 "args": ["exec", "ruby", "weather.rb"],
1955 "cwd": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather"
4501956 }
451 ```
452 
453 Now let's dive into building your server.
454 
455 ## Building your server
456 
457 ### Importing packages and setting up constants
458 
459 Add these to the top of your `src/index.ts`:
460 
461 ```typescript theme={null}
462 import { McpServer } from "@modelcontextprotocol/server";
463 import { serveStdio } from "@modelcontextprotocol/server/stdio";
464 import { z } from "zod";
465 
466 const NWS_API_BASE = "https://api.weather.gov";
467 const USER_AGENT = "weather-app/1.0";
468 ```
469 
470 ### Helper functions
471 
472 Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
473 
474 ```typescript theme={null}
475 // Helper function for making NWS API requests
476 async function makeNWSRequest<T>(url: string): Promise<T | null> {
477 const headers = {
478 "User-Agent": USER_AGENT,
479 Accept: "application/geo+json",
480 };
481 
482 try {
483 const response = await fetch(url, { headers });
484 if (!response.ok) {
485 throw new Error(`HTTP error! status: ${response.status}`);
486 }
487 return (await response.json()) as T;
488 } catch (error) {
489 console.error("Error making NWS request:", error);
490 return null;
491 }
492 }
493 
494 interface AlertFeature {
495 properties: {
496 event?: string;
497 areaDesc?: string;
498 severity?: string;
499 status?: string;
500 headline?: string;
501 };
502 }
503 
504 // Format alert data
505 function formatAlert(feature: AlertFeature): string {
506 const props = feature.properties;
507 return [
508 `Event: ${props.event || "Unknown"}`,
509 `Area: ${props.areaDesc || "Unknown"}`,
510 `Severity: ${props.severity || "Unknown"}`,
511 `Status: ${props.status || "Unknown"}`,
512 `Headline: ${props.headline || "No headline"}`,
513 "---",
514 ].join("\n");
515 }
516 
517 interface ForecastPeriod {
518 name?: string;
519 temperature?: number;
520 temperatureUnit?: string;
521 windSpeed?: string;
522 windDirection?: string;
523 shortForecast?: string;
524 }
525 
526 interface AlertsResponse {
527 features: AlertFeature[];
528 }
529 
530 interface PointsResponse {
531 properties: {
532 forecast?: string;
533 };
534 }
535 
536 interface ForecastResponse {
537 properties: {
538 periods: ForecastPeriod[];
539 };
540 }
541 ```
542 
543 ### Implementing tool execution
544 
545 The tool execution handler is responsible for actually executing the logic of each tool. Let's add it, inside a `createServer()` function that builds the server instance and registers both tools on it:
546 
547 ```typescript theme={null}
548 // Build a server instance and register its tools. serveStdio calls this
549 // whenever it needs an instance, so every call must return a fresh one.
550 function createServer() {
551 const server = new McpServer({
552 name: "weather",
553 version: "1.0.0",
554 });
555 
556 server.registerTool(
557 "get_alerts",
558 {
559 description: "Get weather alerts for a state",
560 inputSchema: z.object({
561 state: z
562 .string()
563 .length(2)
564 .describe("Two-letter state code (e.g. CA, NY)"),
565 }),
566 },
567 async ({ state }) => {
568 const stateCode = state.toUpperCase();
569 const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
570 const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
571 
572 if (!alertsData) {
573 return {
574 content: [
575 {
576 type: "text",
577 text: "Failed to retrieve alerts data",
578 },
579 ],
580 };
581 }
582 
583 const features = alertsData.features || [];
584 if (!features.length) {
585 return {
586 content: [
587 {
588 type: "text",
589 text: `No active alerts for ${stateCode}`,
590 },
591 ],
592 };
593 }
594 
595 const formattedAlerts = features.map(formatAlert);
596 const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;
597 
598 return {
599 content: [
600 {
601 type: "text",
602 text: alertsText,
603 },
604 ],
605 };
606 },
607 );
608 
609 server.registerTool(
610 "get_forecast",
611 {
612 description: "Get weather forecast for a location",
613 inputSchema: z.object({
614 latitude: z
615 .number()
616 .min(-90)
617 .max(90)
618 .describe("Latitude of the location"),
619 longitude: z
620 .number()
621 .min(-180)
622 .max(180)
623 .describe("Longitude of the location"),
624 }),
625 },
626 async ({ latitude, longitude }) => {
627 // Get grid point data
628 const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
629 const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
630 
631 if (!pointsData) {
632 return {
633 content: [
634 {
635 type: "text",
636 text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
637 },
638 ],
639 };
640 }
641 
642 const forecastUrl = pointsData.properties?.forecast;
643 if (!forecastUrl) {
644 return {
645 content: [
646 {
647 type: "text",
648 text: "Failed to get forecast URL from grid point data",
649 },
650 ],
651 };
652 }
653 
654 // Get forecast data
655 const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
656 if (!forecastData) {
657 return {
658 content: [
659 {
660 type: "text",
661 text: "Failed to retrieve forecast data",
662 },
663 ],
664 };
665 }
666 
667 const periods = forecastData.properties?.periods || [];
668 if (periods.length === 0) {
669 return {
670 content: [
671 {
672 type: "text",
673 text: "No forecast periods available",
674 },
675 ],
676 };
677 }
678 
679 // Format forecast periods
680 const formattedForecast = periods.map((period: ForecastPeriod) =>
681 [
682 `${period.name || "Unknown"}:`,
683 `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
684 `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
685 `${period.shortForecast || "No forecast available"}`,
686 "---",
687 ].join("\n"),
688 );
689 
690 const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;
691 
692 return {
693 content: [
694 {
695 type: "text",
696 text: forecastText,
697 },
698 ],
699 };
700 },
701 );
702 
703 return server;
704 }
705 ```
706 
707 ### Running the server
708 
709 Finally, serve the server over stdio:
710 
711 ```typescript theme={null}
712 serveStdio(createServer, {
713 onerror: (error) => console.error("Weather MCP Server error:", error),
714 });
715 console.error("Weather MCP Server running on stdio");
716 ```
717 
718 `serveStdio` negotiates the protocol revision with each client, so the same server answers both 2026-07-28 clients and clients that still open with the 2025-era `initialize` handshake. It calls `createServer()` whenever it needs an instance, including once for a `server/discover` probe that is discarded if the client falls back to `initialize`, which is why the factory returns a new `McpServer` on every call. Wiring the server by hand with `server.connect(new StdioServerTransport())` still compiles, but a server started that way only serves 2025-era clients.
719 
720 Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect.
721 
722 To check that the server answers 2026-07-28 clients, list its tools with the [Inspector CLI](/docs/draft/tools/inspector/cli) in the `modern` [protocol era](/docs/draft/tools/inspector/protocol-eras):
723 
724 ```bash theme={null}
725 npx @modelcontextprotocol/inspector --cli node build/index.js --protocol-era modern --method tools/list
726 ```
727 
728 The Inspector defaults to the `legacy` era, which only exercises the 2025-era handshake, so run the same command once more with `--protocol-era legacy`. Both runs list `get_alerts` and `get_forecast`.
729 
730 Let's now test your server from an existing MCP host, Claude for Desktop.
731 
732 ## Testing your server with Claude for Desktop
733 
734 First, make sure you have Claude for Desktop installed. [You can install the latest version
735 here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
736 
737 We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
738 
739 For example, if you have [VS Code](https://code.visualstudio.com/) installed:
740 
741 <CodeGroup>
742 ```bash Linux theme={null}
743 code ~/.config/Claude/claude_desktop_config.json
744 ```
745 
746 ```bash macOS theme={null}
747 code ~/Library/Application\ Support/Claude/claude_desktop_config.json
748 ```
749 
750 ```powershell Windows theme={null}
751 code $env:AppData\Claude\claude_desktop_config.json
752 ```
753 </CodeGroup>
754 
755 You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
756 
757 In this case, we'll add our single weather server like so:
758 
759 <CodeGroup>
760 ```json macOS/Linux theme={null}
761 {
762 "mcpServers": {
763 "weather": {
764 "command": "node",
765 "args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js"]
766 }
767 }
768 }
769 ```
770 
771 ```json Windows theme={null}
772 {
773 "mcpServers": {
774 "weather": {
775 "command": "node",
776 "args": ["C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\index.js"]
777 }
778 }
779 }
780 ```
781 </CodeGroup>
782 
783 This tells Claude for Desktop:
784 
785 1. There's an MCP server named "weather"
786 2. Launch it by running `node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js`
787 
788 Save the file, and restart **Claude for Desktop**.
789 </Tab>
790 
791 <Tab title="Java">
792 <Note>
793 This is a quickstart demo based on Spring AI MCP auto-configuration and boot starters.
794 To learn how to create sync and async MCP Servers, manually, consult the [Java SDK Server](https://java.sdk.modelcontextprotocol.io/) documentation.
795 </Note>
796 
797 Let's get started with building our weather server!
798 [You can find the complete code for what we'll be building here.](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-stdio-server)
799 
800 For more information, see the [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) reference documentation.
801 For manual MCP Server implementation, refer to the [MCP Server Java SDK documentation](https://java.sdk.modelcontextprotocol.io/).
802 
803 ### Logging in MCP Servers
804 
805 When implementing MCP servers, be careful about how you handle logging:
806 
807 **For STDIO-based servers:** Never use `System.out.println()` or `System.out.print()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
808 
809 **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
810 
811 ### Best Practices
812 
813 * Use a logging library that writes to stderr or files.
814 * Ensure any configured logging library will not write to stdout.
815 
816 ### System requirements
817 
818 * Java 17 or higher installed.
819 * [Spring Boot 3.3.x](https://docs.spring.io/spring-boot/installing.html) or higher
820 
821 ### Set up your environment
822 
823 Use the [Spring Initializer](https://start.spring.io/) to bootstrap the project.
824 
825 You will need to add the following dependencies:
826 
827 <CodeGroup>
828 ```xml Maven theme={null}
829 <dependencies>
830 <dependency>
831 <groupId>org.springframework.ai</groupId>
832 <artifactId>spring-ai-starter-mcp-server</artifactId>
833 </dependency>
834 
835 <dependency>
836 <groupId>org.springframework</groupId>
837 <artifactId>spring-web</artifactId>
838 </dependency>
839 </dependencies>
840 ```
841 
842 ```groovy Gradle theme={null}
843 dependencies {
844 implementation platform("org.springframework.ai:spring-ai-starter-mcp-server")
845 implementation platform("org.springframework:spring-web")
846 }
847 ```
848 </CodeGroup>
849 
850 Then configure your application by setting the application properties:
851 
852 <CodeGroup>
853 ```bash application.properties theme={null}
854 spring.main.bannerMode=off
855 logging.pattern.console=
856 ```
857 
858 ```yaml application.yml theme={null}
859 logging:
860 pattern:
861 console:
862 spring:
863 main:
864 banner-mode: off
865 ```
866 </CodeGroup>
867 
868 The [Server Configuration Properties](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html#_configuration_properties) documents all available properties.
869 
870 Now let's dive into building your server.
871 
872 ## Building your server
873 
874 ### Weather Service
875 
876 Let's implement a [WeatherService.java](https://github.com/spring-projects/spring-ai-examples/blob/main/model-context-protocol/weather/starter-stdio-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java) that uses a REST client to query the data from the National Weather Service API:
877 
878 ```java theme={null}
879 @Service
880 public class WeatherService {
881 
882 private final RestClient restClient;
883 
884 public WeatherService() {
885 this.restClient = RestClient.builder()
886 .baseUrl("https://api.weather.gov")
887 .defaultHeader("Accept", "application/geo+json")
888 .defaultHeader("User-Agent", "WeatherApiClient/1.0 ([email protected])")
889 .build();
890 }
891 
892 @Tool(description = "Get weather forecast for a specific latitude/longitude")
893 public String getWeatherForecastByLocation(
894 double latitude, // Latitude coordinate
895 double longitude // Longitude coordinate
896 ) {
897 // Returns detailed forecast including:
898 // - Temperature and unit
899 // - Wind speed and direction
900 // - Detailed forecast description
901 }
902 
903 @Tool(description = "Get weather alerts for a US state")
904 public String getAlerts(
905 @ToolParam(description = "Two-letter US state code (e.g. CA, NY)") String state
906 ) {
907 // Returns active alerts including:
908 // - Event type
909 // - Affected area
910 // - Severity
911 // - Description
912 // - Safety instructions
913 }
914 
915 // ......
916 }
917 ```
918 
919 The `@Service` annotation will auto-register the service in your application context.
920 The Spring AI `@Tool` annotation makes it easy to create and maintain MCP tools.
921 
922 The auto-configuration will automatically register these tools with the MCP server.
923 
924 ### Create your Boot Application
925 
926 ```java theme={null}
927 @SpringBootApplication
928 public class McpServerApplication {
929 
930 public static void main(String[] args) {
931 SpringApplication.run(McpServerApplication.class, args);
932 }
933 
934 @Bean
935 public ToolCallbackProvider weatherTools(WeatherService weatherService) {
936 return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
937 }
938 }
939 ```
940 
941 Uses the `MethodToolCallbackProvider` utils to convert the `@Tools` into actionable callbacks used by the MCP server.
942 
943 ### Running the server
944 
945 Finally, let's build the server:
946 
947 ```bash theme={null}
948 ./mvnw clean install
949 ```
950 
951 This will generate an `mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar` file within the `target` folder.
952 
953 Let's now test your server from an existing MCP host, Claude for Desktop.
954 
955 ## Testing your server with Claude for Desktop
956 
957 First, make sure you have Claude for Desktop installed.
958 [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
959 
960 We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
961 To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
962 Make sure to create the file if it doesn't exist.
963 
964 For example, if you have [VS Code](https://code.visualstudio.com/) installed:
965 
966 <CodeGroup>
967 ```bash Linux theme={null}
968 code ~/.config/Claude/claude_desktop_config.json
969 ```
970 
971 ```bash macOS theme={null}
972 code ~/Library/Application\ Support/Claude/claude_desktop_config.json
973 ```
974 
975 ```powershell Windows theme={null}
976 code $env:AppData\Claude\claude_desktop_config.json
977 ```
978 </CodeGroup>
979 
980 You'll then add your servers in the `mcpServers` key.
981 The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
982 
983 In this case, we'll add our single weather server like so:
984 
985 <CodeGroup>
986 ```json macOS/Linux theme={null}
987 {
988 "mcpServers": {
989 "spring-ai-mcp-weather": {
990 "command": "java",
991 "args": [
992 "-Dspring.ai.mcp.server.stdio=true",
993 "-jar",
994 "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
995 ]
996 }
997 }
998 }
999 ```
1000 
1001 ```json Windows theme={null}
1002 {
1003 "mcpServers": {
1004 "spring-ai-mcp-weather": {
1005 "command": "java",
1006 "args": [
1007 "-Dspring.ai.mcp.server.transport=STDIO",
1008 "-jar",
1009 "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
1010 ]
1011 }
1012 }
1013 }
1014 ```
1015 </CodeGroup>
1016 
1017 <Note>
1018 Make sure you pass in the absolute path to your server.
1019 </Note>
1020 
1021 This tells Claude for Desktop:
1022 
1023 1. There's an MCP server named "my-weather-server"
1024 2. To launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar`
1025 
1026 Save the file, and restart **Claude for Desktop**.
1027 
1028 ## Testing your server with Java client
1029 
1030 ### Create an MCP Client manually
1031 
1032 Use the `McpClient` to connect to the server:
1033 
1034 ```java theme={null}
1035 var stdioParams = ServerParameters.builder("java")
1036 .args("-jar", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar")
1037 .build();
1038 
1039 var stdioTransport = new StdioClientTransport(stdioParams);
1040 
1041 var mcpClient = McpClient.sync(stdioTransport).build();
1042 
1043 mcpClient.initialize();
1044 
1045 ListToolsResult toolsList = mcpClient.listTools();
1046 
1047 CallToolResult weather = mcpClient.callTool(
1048 new CallToolRequest("getWeatherForecastByLocation",
1049 Map.of("latitude", "47.6062", "longitude", "-122.3321")));
1050 
1051 CallToolResult alert = mcpClient.callTool(
1052 new CallToolRequest("getAlerts", Map.of("state", "NY")));
1053 
1054 mcpClient.closeGracefully();
1055 ```
1056 
1057 ### Use MCP Client Boot Starter
1058 
1059 Create a new boot starter application using the `spring-ai-starter-mcp-client` dependency:
1060 
1061 ```xml theme={null}
1062 <dependency>
1063 <groupId>org.springframework.ai</groupId>
1064 <artifactId>spring-ai-starter-mcp-client</artifactId>
1065 </dependency>
1066 ```
1067 
1068 and set the `spring.ai.mcp.client.stdio.servers-configuration` property to point to your `claude_desktop_config.json`.
1069 You can reuse the existing Anthropic Desktop configuration:
1070 
1071 ```properties theme={null}
1072 spring.ai.mcp.client.stdio.servers-configuration=file:PATH/TO/claude_desktop_config.json
1073 ```
1074 
1075 When you start your client application, the auto-configuration will automatically create MCP clients from the claude\_desktop\_config.json.
1076 
1077 For more information, see the [MCP Client Boot Starters](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) reference documentation.
1078 
1079 ## More Java MCP Server examples
1080 
1081 The [starter-webflux-server](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-webflux-server) demonstrates how to create an HTTP-based MCP server with the WebFlux starter.
1082 Set the `spring.ai.mcp.server.protocol=STREAMABLE` property to serve it over Streamable HTTP.
1083 It showcases how to define and register MCP Tools, Resources, and Prompts, using the Spring Boot's auto-configuration capabilities.
1084 </Tab>
1085 
1086 <Tab title="Kotlin">
1087 Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/samples/weather-stdio-server)
1088 
1089 ### Prerequisite knowledge
1090 
1091 This quickstart assumes you have familiarity with:
1092 
1093 * Kotlin
1094 * LLMs like Claude
1095 
1096 ### Logging in MCP Servers
1097 
1098 When implementing MCP servers, be careful about how you handle logging:
1099 
1100 **For STDIO-based servers:** Never use `println()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1101 
1102 **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1103 
1104 ### Best Practices
1105 
1106 * Use a logging library that writes to stderr or files.
1107 
1108 ### System requirements
1109 
1110 * JDK 11 or higher installed.
1111 
1112 ### Set up your environment
1113 
1114 First, let's install `java` and `gradle` if you haven't already.
1115 You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
1116 Verify your `java` installation:
1117 
1118 ```bash theme={null}
1119 java --version
1120 ```
1121 
1122 Now, let's create and set up your project:
1123 
1124 <CodeGroup>
1125 ```bash macOS/Linux theme={null}
1126 # Create a new directory for our project
1127 mkdir weather
1128 cd weather
1129 
1130 # Initialize a new kotlin project
1131 gradle init
1132 ```
1133 
1134 ```powershell Windows theme={null}
1135 # Create a new directory for our project
1136 md weather
1137 cd weather
1138 
1139 # Initialize a new kotlin project
1140 gradle init
1141 ```
1142 </CodeGroup>
1143 
1144 After running `gradle init`, select **Application** as the project type, **Kotlin** as the programming language.
1145 
1146 Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
1147 
1148 After creating the project, replace the contents of your `build.gradle.kts` with:
1149 
1150 ```kotlin build.gradle.kts theme={null}
1151 // Check latest versions at https://github.com/modelcontextprotocol/kotlin-sdk/releases
1152 val mcpVersion = "0.9.0"
1153 val ktorVersion = "3.2.3"
1154 val slf4jVersion = "2.0.17"
1155 
1156 plugins {
1157 kotlin("jvm") version "2.3.20"
1158 kotlin("plugin.serialization") version "2.3.20"
1159 id("com.gradleup.shadow") version "8.3.9"
1160 application
1161 }
1162 
1163 application {
1164 mainClass.set("MainKt")
1165 }
1166 
1167 dependencies {
1168 implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
1169 implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
1170 implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
1171 implementation("io.ktor:ktor-client-cio:$ktorVersion")
1172 implementation("org.slf4j:slf4j-simple:$slf4jVersion")
1173 }
1174 ```
1175 
1176 Verify that everything is set up correctly:
1177 
1178 ```bash theme={null}
1179 ./gradlew build
1180 ```
1181 
1182 Now let's dive into building your server.
1183 
1184 ## Building your server
1185 
1186 ### Setting up the instance
1187 
1188 Add a server initialization function:
1189 
1190 ```kotlin theme={null}
1191 fun runMcpServer() {
1192 val server = Server(
1193 Implementation(
1194 name = "weather",
1195 version = "1.0.0",
1196 ),
1197 ServerOptions(
1198 capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)),
1199 ),
1200 )
1201 
1202 // register tools on server here
1203 
1204 val transport = StdioServerTransport(
1205 System.`in`.asInput(),
1206 System.out.asSink().buffered(),
1207 )
1208 
1209 runBlocking {
1210 val session = server.createSession(transport)
1211 val done = Job()
1212 session.onClose {
1213 done.complete()
1214 }
1215 done.join()
1957 }
1958}
1959```
1960 
1961> [!NOTE]
1962>
1963> Make sure you pass in the absolute path to your project directory in the `cwd` field. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
1964 
1965This tells Claude for Desktop:
1966 
19671. There's an MCP server named "weather"
19682. Launch it by running `bundle exec ruby weather.rb` in the specified directory
1969 
1970Save the file, and restart **Claude for Desktop**.
1971 
1972**Rust**
1973 
1974Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-rust)
1975 
1976### Prerequisite knowledge
1977 
1978This quickstart assumes you have familiarity with:
1979 
1980* Rust programming language
1981* Async/await in Rust
1982* LLMs like Claude
1983 
1984### Logging in MCP Servers
1985 
1986When implementing MCP servers, be careful about how you handle logging:
1987 
1988**For STDIO-based servers:** Never use `println!()` or `print!()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
1989 
1990**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1991 
1992### Best Practices
1993 
1994* Use a logging library that writes to stderr or files, such as `tracing` or `log` in Rust.
1995* Configure your logging framework to avoid stdout output.
1996 
1997### Quick Examples
1998 
1999```rust theme={null}
2000// ❌ Bad (STDIO)
2001println!("Processing request");
2002 
2003// ✅ Good (STDIO)
2004eprintln!("Processing request"); // writes to stderr
2005```
2006 
2007### System requirements
2008 
2009* Rust 1.70 or higher installed.
2010* Cargo (comes with Rust installation).
2011 
2012### Set up your environment
2013 
2014First, let's install Rust if you haven't already. You can install Rust from [rust-lang.org](https://www.rust-lang.org/tools/install):
2015 
2016```bash macOS/Linux theme={null}
2017curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
2018```
2019 
2020```powershell Windows theme={null}
2021# Download and run rustup-init.exe from https://rustup.rs/
2022```
2023 
2024Verify your Rust installation:
2025 
2026```bash theme={null}
2027rustc --version
2028cargo --version
2029```
2030 
2031Now, let's create and set up our project:
2032 
2033```bash macOS/Linux theme={null}
2034# Create a new Rust project
2035cargo new weather
2036cd weather
2037```
2038 
2039```powershell Windows theme={null}
2040# Create a new Rust project
2041cargo new weather
2042cd weather
2043```
2044 
2045Update your `Cargo.toml` to add the required dependencies:
2046 
2047```toml Cargo.toml theme={null}
2048[package]
2049name = "weather"
2050version = "0.1.0"
2051edition = "2024"
2052 
2053[dependencies]
2054rmcp = { version = "0.3", features = ["server", "macros", "transport-io"] }
2055tokio = { version = "1.46", features = ["full"] }
2056reqwest = { version = "0.12", features = ["json"] }
2057serde = { version = "1.0", features = ["derive"] }
2058serde_json = "1.0"
2059anyhow = "1.0"
2060tracing = "0.1"
2061tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] }
2062```
2063 
2064Now let's dive into building your server.
2065 
2066## Building your server
2067 
2068### Importing packages and constants
2069 
2070Open `src/main.rs` and add these imports and constants at the top:
2071 
2072```rust theme={null}
2073use anyhow::Result;
2074use rmcp::{
2075 ServerHandler, ServiceExt,
2076 handler::server::{router::tool::ToolRouter, tool::Parameters},
2077 model::*,
2078 schemars, tool, tool_handler, tool_router,
2079};
2080use serde::Deserialize;
2081use serde::de::DeserializeOwned;
2082 
2083const NWS_API_BASE: &str = "https://api.weather.gov";
2084const USER_AGENT: &str = "weather-app/1.0";
2085```
2086 
2087The `rmcp` crate provides the Model Context Protocol SDK for Rust, with features for server implementation, procedural macros, and stdio transport.
2088 
2089### Data structures
2090 
2091Next, let's define the data structures for deserializing responses from the National Weather Service API:
2092 
2093```rust theme={null}
2094#[derive(Debug, Deserialize)]
2095struct AlertsResponse {
2096 features: Vec<AlertFeature>,
2097}
2098 
2099#[derive(Debug, Deserialize)]
2100struct AlertFeature {
2101 properties: AlertProperties,
2102}
2103 
2104#[derive(Debug, Deserialize)]
2105struct AlertProperties {
2106 event: Option<String>,
2107 #[serde(rename = "areaDesc")]
2108 area_desc: Option<String>,
2109 severity: Option<String>,
2110 description: Option<String>,
2111 instruction: Option<String>,
2112}
2113 
2114#[derive(Debug, Deserialize)]
2115struct PointsResponse {
2116 properties: PointsProperties,
2117}
2118 
2119#[derive(Debug, Deserialize)]
2120struct PointsProperties {
2121 forecast: String,
2122}
2123 
2124#[derive(Debug, Deserialize)]
2125struct ForecastResponse {
2126 properties: ForecastProperties,
2127}
2128 
2129#[derive(Debug, Deserialize)]
2130struct ForecastProperties {
2131 periods: Vec<ForecastPeriod>,
2132}
2133 
2134#[derive(Debug, Deserialize)]
2135struct ForecastPeriod {
2136 name: String,
2137 temperature: i32,
2138 #[serde(rename = "temperatureUnit")]
2139 temperature_unit: String,
2140 #[serde(rename = "windSpeed")]
2141 wind_speed: String,
2142 #[serde(rename = "windDirection")]
2143 wind_direction: String,
2144 #[serde(rename = "detailedForecast")]
2145 detailed_forecast: String,
2146}
2147```
2148 
2149Now define the request types that MCP clients will send:
2150 
2151```rust theme={null}
2152#[derive(serde::Deserialize, schemars::JsonSchema)]
2153pub struct MCPForecastRequest {
2154 latitude: f32,
2155 longitude: f32,
2156}
2157 
2158#[derive(serde::Deserialize, schemars::JsonSchema)]
2159pub struct MCPAlertRequest {
2160 state: String,
2161}
2162```
2163 
2164### Helper functions
2165 
2166Add helper functions for making API requests and formatting responses:
2167 
2168```rust theme={null}
2169async fn make_nws_request<T: DeserializeOwned>(url: &str) -> Result<T> {
2170 let client = reqwest::Client::new();
2171 let rsp = client
2172 .get(url)
2173 .header(reqwest::header::USER_AGENT, USER_AGENT)
2174 .header(reqwest::header::ACCEPT, "application/geo+json")
2175 .send()
2176 .await?
2177 .error_for_status()?;
2178 Ok(rsp.json::<T>().await?)
2179}
2180 
2181fn format_alert(feature: &AlertFeature) -> String {
2182 let props = &feature.properties;
2183 format!(
2184 "Event: {}\nArea: {}\nSeverity: {}\nDescription: {}\nInstructions: {}",
2185 props.event.as_deref().unwrap_or("Unknown"),
2186 props.area_desc.as_deref().unwrap_or("Unknown"),
2187 props.severity.as_deref().unwrap_or("Unknown"),
2188 props
2189 .description
2190 .as_deref()
2191 .unwrap_or("No description available"),
2192 props
2193 .instruction
2194 .as_deref()
2195 .unwrap_or("No specific instructions provided")
2196 )
2197}
2198 
2199fn format_period(period: &ForecastPeriod) -> String {
2200 format!(
2201 "{}:\nTemperature: {}°{}\nWind: {} {}\nForecast: {}",
2202 period.name,
2203 period.temperature,
2204 period.temperature_unit,
2205 period.wind_speed,
2206 period.wind_direction,
2207 period.detailed_forecast
2208 )
2209}
2210```
2211 
2212### Implementing the Weather server and tools
2213 
2214Now let's implement the main Weather server struct with the tool handlers:
2215 
2216```rust theme={null}
2217pub struct Weather {
2218 tool_router: ToolRouter<Weather>,
2219}
2220 
2221#[tool_router]
2222impl Weather {
2223 fn new() -> Self {
2224 Self {
2225 tool_router: Self::tool_router(),
12162226 }
12172227 }
1218 ```
1219 
1220 ### Weather API helper functions
1221 
1222 Next, let's add functions and data classes for querying and converting responses from the National Weather Service API:
1223 
1224 ```kotlin theme={null}
1225 val httpClient = HttpClient(CIO) {
1226 defaultRequest {
1227 url("https://api.weather.gov")
1228 headers {
1229 append("Accept", "application/geo+json")
1230 append("User-Agent", "WeatherApiClient/1.0")
2228 
2229 #[tool(description = "Get weather alerts for a US state.")]
2230 async fn get_alerts(
2231 &self,
2232 Parameters(MCPAlertRequest { state }): Parameters<MCPAlertRequest>,
2233 ) -> String {
2234 let url = format!(
2235 "{}/alerts/active/area/{}",
2236 NWS_API_BASE,
2237 state.to_uppercase()
2238 );
2239 
2240 match make_nws_request::<AlertsResponse>(&url).await {
2241 Ok(data) => {
2242 if data.features.is_empty() {
2243 "No active alerts for this state.".to_string()
2244 } else {
2245 data.features
2246 .iter()
2247 .map(format_alert)
2248 .collect::<Vec<_>>()
2249 .join("\n---\n")
2250 }
12312251 }
1232 contentType(ContentType.Application.Json)
1233 }
1234 install(ContentNegotiation) {
1235 json(Json { ignoreUnknownKeys = true })
2252 Err(_) => "Unable to fetch alerts or no alerts found.".to_string(),
12362253 }
12372254 }
12382255 
1239 // Extension function to fetch weather alerts for a given state
1240 suspend fun HttpClient.getAlerts(state: String): List<String> {
1241 val alerts = this.get("/alerts/active/area/$state").body<AlertsResponse>()
1242 return alerts.features.map { feature ->
1243 """
1244 Event: ${feature.properties.event}
1245 Area: ${feature.properties.areaDesc}
1246 Severity: ${feature.properties.severity}
1247 Status: ${feature.properties.status}
1248 Headline: ${feature.properties.headline}
1249 """.trimIndent()
2256 #[tool(description = "Get weather forecast for a location.")]
2257 async fn get_forecast(
2258 &self,
2259 Parameters(MCPForecastRequest {
2260 latitude,
2261 longitude,
2262 }): Parameters<MCPForecastRequest>,
2263 ) -> String {
2264 let points_url = format!("{NWS_API_BASE}/points/{latitude},{longitude}");
2265 let Ok(points_data) = make_nws_request::<PointsResponse>(&points_url).await else {
2266 return "Unable to fetch forecast data for this location.".to_string();
2267 };
2268 
2269 let forecast_url = points_data.properties.forecast;
2270 
2271 let Ok(forecast_data) = make_nws_request::<ForecastResponse>(&forecast_url).await else {
2272 return "Unable to fetch forecast data for this location.".to_string();
2273 };
2274 
2275 let periods = &forecast_data.properties.periods;
2276 let forecast_summary: String = periods
2277 .iter()
2278 .take(5) // Next 5 periods only
2279 .map(format_period)
2280 .collect::<Vec<String>>()
2281 .join("\n---\n");
2282 forecast_summary
2283 }
2284}
2285```
2286 
2287The `#[tool_router]` macro automatically generates the routing logic, and the `#[tool]` attribute marks methods as MCP tools.
2288 
2289### Implementing the ServerHandler
2290 
2291Implement the `ServerHandler` trait to define server capabilities:
2292 
2293```rust theme={null}
2294#[tool_handler]
2295impl ServerHandler for Weather {
2296 fn get_info(&self) -> ServerInfo {
2297 ServerInfo {
2298 capabilities: ServerCapabilities::builder().enable_tools().build(),
2299 ..Default::default()
12502300 }
12512301 }
1252 
1253 // Extension function to fetch forecast information for given latitude and longitude
1254 suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List<String> {
1255 val points = this.get("/points/$latitude,$longitude").body<PointsResponse>()
1256 val forecastUrl = points.properties.forecast ?: error("No forecast URL available")
1257 val forecast = this.get(forecastUrl).body<ForecastResponse>()
1258 return forecast.properties.periods.map { period ->
1259 """
1260 ${period.name}:
1261 Temperature: ${period.temperature}°${period.temperatureUnit}
1262 Wind: ${period.windSpeed} ${period.windDirection}
1263 ${period.shortForecast}
1264 """.trimIndent()
1265 }
2302}
2303```
2304 
2305### Running the server
2306 
2307Finally, implement the main function to run the server with stdio transport:
2308 
2309```rust theme={null}
2310#[tokio::main]
2311async fn main() -> Result<()> {
2312 let transport = (tokio::io::stdin(), tokio::io::stdout());
2313 let service = Weather::new().serve(transport).await?;
2314 service.waiting().await?;
2315 Ok(())
2316}
2317```
2318 
2319Build your server with:
2320 
2321```bash theme={null}
2322cargo build --release
2323```
2324 
2325The compiled binary will be in `target/release/weather`.
2326 
2327Let's now test your server from an existing MCP host, Claude for Desktop.
2328 
2329## Testing your server with Claude for Desktop
2330 
2331First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2332 
2333We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2334 
2335For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2336 
2337```bash Linux theme={null}
2338code ~/.config/Claude/claude_desktop_config.json
2339```
2340 
2341```bash macOS theme={null}
2342code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2343```
2344 
2345```powershell Windows theme={null}
2346code $env:AppData\Claude\claude_desktop_config.json
2347```
2348 
2349You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2350 
2351In this case, we'll add our single weather server like so:
2352 
2353```json macOS/Linux theme={null}
2354{
2355 "mcpServers": {
2356 "weather": {
2357 "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/target/release/weather"
12662358 }
1267 
1268 @Serializable
1269 data class PointsResponse(val properties: PointsProperties)
1270 
1271 @Serializable
1272 data class PointsProperties(val forecast: String? = null)
1273 
1274 @Serializable
1275 data class ForecastResponse(val properties: ForecastProperties)
1276 
1277 @Serializable
1278 data class ForecastProperties(val periods: List<ForecastPeriod> = emptyList())
1279 
1280 @Serializable
1281 data class ForecastPeriod(
1282 val name: String? = null,
1283 val temperature: Int? = null,
1284 val temperatureUnit: String? = null,
1285 val windSpeed: String? = null,
1286 val windDirection: String? = null,
1287 val shortForecast: String? = null,
1288 )
1289 
1290 @Serializable
1291 data class AlertsResponse(val features: List<AlertFeature> = emptyList())
1292 
1293 @Serializable
1294 data class AlertFeature(val properties: AlertProperties)
1295 
1296 @Serializable
1297 data class AlertProperties(
1298 val event: String? = null,
1299 val areaDesc: String? = null,
1300 val severity: String? = null,
1301 val status: String? = null,
1302 val headline: String? = null,
1303 )
1304 ```
1305 
1306 ### Implementing tool execution
1307 
1308 The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
1309 
1310 ```kotlin theme={null}
1311 // Register weather tools
1312 
1313 server.addTool(
1314 name = "get_alerts",
1315 description = "Get weather alerts for a US state. Input is a two-letter US state code (e.g. CA, NY)",
1316 inputSchema = ToolSchema(
1317 properties = buildJsonObject {
1318 putJsonObject("state") {
1319 put("type", "string")
1320 put("description", "Two-letter US state code (e.g. CA, NY)")
1321 }
1322 },
1323 required = listOf("state"),
1324 ),
1325 ) { request ->
1326 val state = request.arguments?.get("state")?.jsonPrimitive?.content
1327 ?: return@addTool CallToolResult(
1328 content = listOf(TextContent("The 'state' parameter is required.")),
1329 )
1330 
1331 val alerts = httpClient.getAlerts(state)
1332 CallToolResult(content = alerts.map { TextContent(it) })
2359 }
2360}
2361```
2362 
2363```json Windows theme={null}
2364{
2365 "mcpServers": {
2366 "weather": {
2367 "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\target\\release\\weather.exe"
13332368 }
1334 
1335 server.addTool(
1336 name = "get_forecast",
1337 description = "Get weather forecast for a location. Note: only US locations are supported by the NWS API.",
1338 inputSchema = ToolSchema(
1339 properties = buildJsonObject {
1340 putJsonObject("latitude") {
1341 put("type", "number")
1342 put("description", "Latitude of the location")
1343 }
1344 putJsonObject("longitude") {
1345 put("type", "number")
1346 put("description", "Longitude of the location")
1347 }
1348 },
1349 required = listOf("latitude", "longitude"),
1350 ),
1351 ) { request ->
1352 val latitude = request.arguments?.get("latitude")?.jsonPrimitive?.doubleOrNull
1353 val longitude = request.arguments?.get("longitude")?.jsonPrimitive?.doubleOrNull
1354 if (latitude == null || longitude == null) {
1355 return@addTool CallToolResult(
1356 content = listOf(TextContent("The 'latitude' and 'longitude' parameters are required.")),
1357 )
1358 }
1359 
1360 val forecast = httpClient.getForecast(latitude, longitude)
1361 CallToolResult(content = forecast.map { TextContent(it) })
2369 }
2370}
2371```
2372 
2373> [!NOTE]
2374>
2375> Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2376 
2377This tells Claude for Desktop:
2378 
23791. There's an MCP server named "weather"
23802. Launch it by running the compiled binary at the specified path
2381 
2382Save the file, and restart **Claude for Desktop**.
2383 
2384**Go**
2385 
2386Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-go)
2387 
2388### Prerequisite knowledge
2389 
2390This quickstart assumes you have familiarity with:
2391 
2392* Go
2393* LLMs like Claude
2394 
2395### Logging in MCP Servers
2396 
2397When implementing MCP servers, be careful about how you handle logging:
2398 
2399**For STDIO-based servers:** Never use `fmt.Println()` or `fmt.Printf()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
2400 
2401**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
2402 
2403### Best Practices
2404 
2405* Use `log.Println()` (which defaults to stderr) or a logging library that writes to stderr or files.
2406* Use `fmt.Fprintf(os.Stderr, ...)` to write to stderr explicitly.
2407 
2408### Quick Examples
2409 
2410```go theme={null}
2411// ❌ Bad (STDIO)
2412fmt.Println("Processing request")
2413 
2414// ✅ Good (STDIO)
2415log.Println("Processing request") // defaults to stderr
2416 
2417// ✅ Good (STDIO)
2418fmt.Fprintln(os.Stderr, "Processing request")
2419```
2420 
2421### System requirements
2422 
2423* Go 1.24 or higher installed.
2424 
2425### Set up your environment
2426 
2427First, let's install Go if you haven't already. You can download and install Go from [go.dev](https://go.dev/dl/).
2428 
2429Verify your Go installation:
2430 
2431```bash theme={null}
2432go version
2433```
2434 
2435Now, let's create and set up our project:
2436 
2437```bash macOS/Linux theme={null}
2438# Create a new directory for our project
2439mkdir weather
2440cd weather
2441 
2442# Initialize Go module
2443go mod init weather
2444 
2445# Install dependencies
2446go get github.com/modelcontextprotocol/go-sdk/mcp
2447 
2448# Create our server file
2449touch main.go
2450```
2451 
2452```powershell Windows theme={null}
2453# Create a new directory for our project
2454md weather
2455cd weather
2456 
2457# Initialize Go module
2458go mod init weather
2459 
2460# Install dependencies
2461go get github.com/modelcontextprotocol/go-sdk/mcp
2462 
2463# Create our server file
2464new-item main.go
2465```
2466 
2467Now let's dive into building your server.
2468 
2469## Building your server
2470 
2471### Importing packages and constants
2472 
2473Add these to the top of your `main.go`:
2474 
2475```go theme={null}
2476package main
2477 
2478import (
2479 "cmp"
2480 "context"
2481 "encoding/json"
2482 "fmt"
2483 "io"
2484 "log"
2485 "net/http"
2486 "strings"
2487 
2488 "github.com/modelcontextprotocol/go-sdk/mcp"
2489)
2490 
2491const (
2492 NWSAPIBase = "https://api.weather.gov"
2493 UserAgent = "weather-app/1.0"
2494)
2495```
2496 
2497### Data structures
2498 
2499Next, let's define the data structures used by our tools:
2500 
2501```go theme={null}
2502type PointsResponse struct {
2503 Properties struct {
2504 Forecast string `json:"forecast"`
2505 } `json:"properties"`
2506}
2507 
2508type ForecastResponse struct {
2509 Properties struct {
2510 Periods []ForecastPeriod `json:"periods"`
2511 } `json:"properties"`
2512}
2513 
2514type ForecastPeriod struct {
2515 Name string `json:"name"`
2516 Temperature int `json:"temperature"`
2517 TemperatureUnit string `json:"temperatureUnit"`
2518 WindSpeed string `json:"windSpeed"`
2519 WindDirection string `json:"windDirection"`
2520 DetailedForecast string `json:"detailedForecast"`
2521}
2522 
2523type AlertsResponse struct {
2524 Features []AlertFeature `json:"features"`
2525}
2526 
2527type AlertFeature struct {
2528 Properties AlertProperties `json:"properties"`
2529}
2530 
2531type AlertProperties struct {
2532 Event string `json:"event"`
2533 AreaDesc string `json:"areaDesc"`
2534 Severity string `json:"severity"`
2535 Description string `json:"description"`
2536 Instruction string `json:"instruction"`
2537}
2538 
2539type ForecastInput struct {
2540 Latitude float64 `json:"latitude" jsonschema:"Latitude of the location"`
2541 Longitude float64 `json:"longitude" jsonschema:"Longitude of the location"`
2542}
2543 
2544type AlertsInput struct {
2545 State string `json:"state" jsonschema:"Two-letter US state code (e.g. CA, NY)"`
2546}
2547```
2548 
2549### Helper functions
2550 
2551Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
2552 
2553```go theme={null}
2554func makeNWSRequest[T any](ctx context.Context, url string) (*T, error) {
2555 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
2556 if err != nil {
2557 return nil, fmt.Errorf("failed to create request: %w", err)
2558 }
2559 
2560 req.Header.Set("User-Agent", UserAgent)
2561 req.Header.Set("Accept", "application/geo+json")
2562 
2563 client := http.DefaultClient
2564 resp, err := client.Do(req)
2565 if err != nil {
2566 return nil, fmt.Errorf("failed to make request to %s: %w", url, err)
2567 }
2568 defer resp.Body.Close()
2569 
2570 if resp.StatusCode != http.StatusOK {
2571 body, _ := io.ReadAll(resp.Body)
2572 return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
2573 }
2574 
2575 var result T
2576 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
2577 return nil, fmt.Errorf("failed to decode response: %w", err)
2578 }
2579 
2580 return &result, nil
2581}
2582 
2583func formatAlert(alert AlertFeature) string {
2584 props := alert.Properties
2585 event := cmp.Or(props.Event, "Unknown")
2586 areaDesc := cmp.Or(props.AreaDesc, "Unknown")
2587 severity := cmp.Or(props.Severity, "Unknown")
2588 description := cmp.Or(props.Description, "No description available")
2589 instruction := cmp.Or(props.Instruction, "No specific instructions provided")
2590 
2591 return fmt.Sprintf(`
2592Event: %s
2593Area: %s
2594Severity: %s
2595Description: %s
2596Instructions: %s
2597`, event, areaDesc, severity, description, instruction)
2598}
2599 
2600func formatPeriod(period ForecastPeriod) string {
2601 return fmt.Sprintf(`
2602%s:
2603Temperature: %d°%s
2604Wind: %s %s
2605Forecast: %s
2606`, period.Name, period.Temperature, period.TemperatureUnit,
2607 period.WindSpeed, period.WindDirection, period.DetailedForecast)
2608}
2609```
2610 
2611### Implementing tool execution
2612 
2613The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
2614 
2615```go theme={null}
2616func getForecast(ctx context.Context, req *mcp.CallToolRequest, input ForecastInput) (
2617 *mcp.CallToolResult, any, error,
2618) {
2619 // Get points data
2620 pointsURL := fmt.Sprintf("%s/points/%f,%f", NWSAPIBase, input.Latitude, input.Longitude)
2621 pointsData, err := makeNWSRequest[PointsResponse](ctx, pointsURL)
2622 if err != nil {
2623 return &mcp.CallToolResult{
2624 Content: []mcp.Content{
2625 &mcp.TextContent{Text: "Unable to fetch forecast data for this location."},
2626 },
2627 }, nil, nil
2628 }
2629 
2630 // Get forecast data
2631 forecastURL := pointsData.Properties.Forecast
2632 if forecastURL == "" {
2633 return &mcp.CallToolResult{
2634 Content: []mcp.Content{
2635 &mcp.TextContent{Text: "Unable to fetch forecast URL."},
2636 },
2637 }, nil, nil
2638 }
2639 
2640 forecastData, err := makeNWSRequest[ForecastResponse](ctx, forecastURL)
2641 if err != nil {
2642 return &mcp.CallToolResult{
2643 Content: []mcp.Content{
2644 &mcp.TextContent{Text: "Unable to fetch detailed forecast."},
2645 },
2646 }, nil, nil
2647 }
2648 
2649 // Format the periods
2650 periods := forecastData.Properties.Periods
2651 if len(periods) == 0 {
2652 return &mcp.CallToolResult{
2653 Content: []mcp.Content{
2654 &mcp.TextContent{Text: "No forecast periods available."},
2655 },
2656 }, nil, nil
2657 }
2658 
2659 // Show next 5 periods
2660 var forecasts []string
2661 for i := range min(5, len(periods)) {
2662 forecasts = append(forecasts, formatPeriod(periods[i]))
2663 }
2664 
2665 result := strings.Join(forecasts, "\n---\n")
2666 
2667 return &mcp.CallToolResult{
2668 Content: []mcp.Content{
2669 &mcp.TextContent{Text: result},
2670 },
2671 }, nil, nil
2672}
2673 
2674func getAlerts(ctx context.Context, req *mcp.CallToolRequest, input AlertsInput) (
2675 *mcp.CallToolResult, any, error,
2676) {
2677 // Build alerts URL
2678 stateCode := strings.ToUpper(input.State)
2679 alertsURL := fmt.Sprintf("%s/alerts/active/area/%s", NWSAPIBase, stateCode)
2680 
2681 alertsData, err := makeNWSRequest[AlertsResponse](ctx, alertsURL)
2682 if err != nil {
2683 return &mcp.CallToolResult{
2684 Content: []mcp.Content{
2685 &mcp.TextContent{Text: "Unable to fetch alerts or no alerts found."},
2686 },
2687 }, nil, nil
2688 }
2689 
2690 // Check if there are any alerts
2691 if len(alertsData.Features) == 0 {
2692 return &mcp.CallToolResult{
2693 Content: []mcp.Content{
2694 &mcp.TextContent{Text: "No active alerts for this state."},
2695 },
2696 }, nil, nil
2697 }
2698 
2699 // Format alerts
2700 var alerts []string
2701 for _, feature := range alertsData.Features {
2702 alerts = append(alerts, formatAlert(feature))
2703 }
2704 
2705 result := strings.Join(alerts, "\n---\n")
2706 
2707 return &mcp.CallToolResult{
2708 Content: []mcp.Content{
2709 &mcp.TextContent{Text: result},
2710 },
2711 }, nil, nil
2712}
2713```
2714 
2715### Running the server
2716 
2717Finally, implement the main function to run the server:
2718 
2719```go theme={null}
2720func main() {
2721 // Create MCP server
2722 server := mcp.NewServer(&mcp.Implementation{
2723 Name: "weather",
2724 Version: "1.0.0",
2725 }, nil)
2726 
2727 // Add get_forecast tool
2728 mcp.AddTool(server, &mcp.Tool{
2729 Name: "get_forecast",
2730 Description: "Get weather forecast for a location",
2731 }, getForecast)
2732 
2733 // Add get_alerts tool
2734 mcp.AddTool(server, &mcp.Tool{
2735 Name: "get_alerts",
2736 Description: "Get weather alerts for a US state",
2737 }, getAlerts)
2738 
2739 // Run server on stdio transport
2740 if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
2741 log.Fatal(err)
2742 }
2743}
2744```
2745 
2746Build your server with:
2747 
2748```bash theme={null}
2749go build -o weather .
2750```
2751 
2752The compiled binary will be in `./weather`.
2753 
2754Let's now test your server from an existing MCP host, Claude for Desktop.
2755 
2756## Testing your server with Claude for Desktop
2757 
2758First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2759 
2760We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2761 
2762For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2763 
2764```bash Linux theme={null}
2765code ~/.config/Claude/claude_desktop_config.json
2766```
2767 
2768```bash macOS theme={null}
2769code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2770```
2771 
2772```powershell Windows theme={null}
2773code $env:AppData\Claude\claude_desktop_config.json
2774```
2775 
2776You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2777 
2778In this case, we'll add our single weather server like so:
2779 
2780```json macOS/Linux theme={null}
2781{
2782 "mcpServers": {
2783 "weather": {
2784 "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/weather"
13622785 }
1363 ```
1364 
1365 ### Running the server
1366 
1367 Finally, implement the main function to run the server:
1368 
1369 ```kotlin theme={null}
1370 fun main() = runMcpServer()
1371 ```
1372 
1373 You can run the server directly during development:
1374 
1375 ```bash theme={null}
1376 ./gradlew run
1377 ```
1378 
1379 For production use, build the shadow JAR:
1380 
1381 ```bash theme={null}
1382 ./gradlew build
1383 java -jar build/libs/weather-0.1.0-all.jar
1384 ```
1385 
1386 Let's now test your server from an existing MCP host, Claude for Desktop.
1387 
1388 ## Testing your server with Claude for Desktop
1389 
1390 First, make sure you have Claude for Desktop installed. [You can install the latest version
1391 here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1392 
1393 We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
1394 To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
1395 Make sure to create the file if it doesn't exist.
1396 
1397 For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1398 
1399 <CodeGroup>
1400 ```bash Linux theme={null}
1401 code ~/.config/Claude/claude_desktop_config.json
1402 ```
1403 
1404 ```bash macOS theme={null}
1405 code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1406 ```
1407 
1408 ```powershell Windows theme={null}
1409 code $env:AppData\Claude\claude_desktop_config.json
1410 ```
1411 </CodeGroup>
1412 
1413 You'll then add your servers in the `mcpServers` key.
1414 The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1415 
1416 In this case, we'll add our single weather server like so:
1417 
1418 <CodeGroup>
1419 ```json macOS/Linux theme={null}
1420 {
1421 "mcpServers": {
1422 "weather": {
1423 "command": "java",
1424 "args": [
1425 "-jar",
1426 "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar"
1427 ]
1428 }
1429 }
1430 }
1431 ```
1432 
1433 ```json Windows theme={null}
1434 {
1435 "mcpServers": {
1436 "weather": {
1437 "command": "java",
1438 "args": [
1439 "-jar",
1440 "C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\libs\\weather-0.1.0-all.jar"
1441 ]
1442 }
1443 }
1444 }
1445 ```
1446 </CodeGroup>
1447 
1448 This tells Claude for Desktop:
1449 
1450 1. There's an MCP server named "weather"
1451 2. Launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar`
1452 
1453 Save the file, and restart **Claude for Desktop**.
1454 </Tab>
1455 
1456 <Tab title="C#">
1457 Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/QuickstartWeatherServer)
1458 
1459 ### Prerequisite knowledge
1460 
1461 This quickstart assumes you have familiarity with:
1462 
1463 * C#
1464 * LLMs like Claude
1465 * .NET 8 or higher
1466 
1467 ### Logging in MCP Servers
1468 
1469 When implementing MCP servers, be careful about how you handle logging:
1470 
1471 **For STDIO-based servers:** Never use `Console.WriteLine()` or `Console.Write()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
1472 
1473 **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1474 
1475 ### Best Practices
1476 
1477 * Use a logging library that writes to stderr or files.
1478 
1479 ### System requirements
1480 
1481 * [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or higher installed.
1482 
1483 ### Set up your environment
1484 
1485 First, let's install `dotnet` if you haven't already. You can download `dotnet` from [official Microsoft .NET website](https://dotnet.microsoft.com/download/). Verify your `dotnet` installation:
1486 
1487 ```bash theme={null}
1488 dotnet --version
1489 ```
1490 
1491 Now, let's create and set up your project:
1492 
1493 <CodeGroup>
1494 ```bash macOS/Linux theme={null}
1495 # Create a new directory for our project
1496 mkdir weather
1497 cd weather
1498 # Initialize a new C# project
1499 dotnet new console
1500 ```
1501 
1502 ```powershell Windows theme={null}
1503 # Create a new directory for our project
1504 mkdir weather
1505 cd weather
1506 # Initialize a new C# project
1507 dotnet new console
1508 ```
1509 </CodeGroup>
1510 
1511 After running `dotnet new console`, you will be presented with a new C# project.
1512 You can open the project in your favorite IDE, such as [Visual Studio](https://visualstudio.microsoft.com/) or [Rider](https://www.jetbrains.com/rider/).
1513 Alternatively, you can create a C# application using the [Visual Studio project wizard](https://learn.microsoft.com/en-us/visualstudio/get-started/csharp/tutorial-console?view=vs-2022).
1514 After creating the project, add NuGet package for the Model Context Protocol SDK and hosting:
1515 
1516 ```bash theme={null}
1517 # Add the Model Context Protocol SDK NuGet package
1518 dotnet add package ModelContextProtocol --prerelease
1519 # Add the .NET Hosting NuGet package
1520 dotnet add package Microsoft.Extensions.Hosting
1521 ```
1522 
1523 Now let’s dive into building your server.
1524 
1525 ## Building your server
1526 
1527 Open the `Program.cs` file in your project and replace its contents with the following code:
1528 
1529 ```csharp theme={null}
1530 using Microsoft.Extensions.DependencyInjection;
1531 using Microsoft.Extensions.Hosting;
1532 using ModelContextProtocol;
1533 using System.Net.Http.Headers;
1534 
1535 var builder = Host.CreateEmptyApplicationBuilder(settings: null);
1536 
1537 builder.Services.AddMcpServer()
1538 .WithStdioServerTransport()
1539 .WithToolsFromAssembly();
1540 
1541 builder.Services.AddSingleton(_ =>
1542 {
1543 var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") };
1544 client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
1545 return client;
1546 });
1547 
1548 var app = builder.Build();
1549 
1550 await app.RunAsync();
1551 ```
1552 
1553 <Note>
1554 When creating the `ApplicationHostBuilder`, ensure you use `CreateEmptyApplicationBuilder` instead of `CreateDefaultBuilder`. This ensures that the server does not write any additional messages to the console. This is only necessary for servers using STDIO transport.
1555 </Note>
1556 
1557 This code sets up a basic console application that uses the Model Context Protocol SDK to create an MCP server with standard I/O transport.
1558 
1559 ### Weather API helper functions
1560 
1561 Create an extension class for `HttpClient` which helps simplify JSON request handling:
1562 
1563 ```csharp theme={null}
1564 using System.Text.Json;
1565 
1566 internal static class HttpClientExt
1567 {
1568 public static async Task<JsonDocument> ReadJsonDocumentAsync(this HttpClient client, string requestUri)
1569 {
1570 using var response = await client.GetAsync(requestUri);
1571 response.EnsureSuccessStatusCode();
1572 return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
1573 }
2786 }
2787}
2788```
2789 
2790```json Windows theme={null}
2791{
2792 "mcpServers": {
2793 "weather": {
2794 "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\weather.exe"
15742795 }
1575 ```
1576 
1577 Next, define a class with the tool execution handlers for querying and converting responses from the National Weather Service API:
1578 
1579 ```csharp theme={null}
1580 using ModelContextProtocol.Server;
1581 using System.ComponentModel;
1582 using System.Globalization;
1583 using System.Text.Json;
1584 
1585 namespace QuickstartWeatherServer.Tools;
1586 
1587 [McpServerToolType]
1588 public static class WeatherTools
1589 {
1590 [McpServerTool, Description("Get weather alerts for a US state code.")]
1591 public static async Task<string> GetAlerts(
1592 HttpClient client,
1593 [Description("The US state code to get alerts for.")] string state)
1594 {
1595 using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}");
1596 var jsonElement = jsonDocument.RootElement;
1597 var alerts = jsonElement.GetProperty("features").EnumerateArray();
1598 
1599 if (!alerts.Any())
1600 {
1601 return "No active alerts for this state.";
1602 }
1603 
1604 return string.Join("\n--\n", alerts.Select(alert =>
1605 {
1606 JsonElement properties = alert.GetProperty("properties");
1607 return $"""
1608 Event: {properties.GetProperty("event").GetString()}
1609 Area: {properties.GetProperty("areaDesc").GetString()}
1610 Severity: {properties.GetProperty("severity").GetString()}
1611 Description: {properties.GetProperty("description").GetString()}
1612 Instruction: {properties.GetProperty("instruction").GetString()}
1613 """;
1614 }));
1615 }
1616 
1617 [McpServerTool, Description("Get weather forecast for a location.")]
1618 public static async Task<string> GetForecast(
1619 HttpClient client,
1620 [Description("Latitude of the location.")] double latitude,
1621 [Description("Longitude of the location.")] double longitude)
1622 {
1623 var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
1624 using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl);
1625 var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
1626 ?? throw new Exception($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");
1627 
1628 using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl);
1629 var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray();
1630 
1631 return string.Join("\n---\n", periods.Select(period => $"""
1632 {period.GetProperty("name").GetString()}
1633 Temperature: {period.GetProperty("temperature").GetInt32()}°F
1634 Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
1635 Forecast: {period.GetProperty("detailedForecast").GetString()}
1636 """));
1637 }
1638 }
1639 ```
1640 
1641 ### Running the server
1642 
1643 Finally, run the server using the following command:
1644 
1645 ```bash theme={null}
1646 dotnet run
1647 ```
1648 
1649 This will start the server and listen for incoming requests on standard input/output.
1650 
1651 ## Testing your server with Claude for Desktop
1652 
1653 First, make sure you have Claude for Desktop installed. [You can install the latest version
1654 here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1655 We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1656 For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1657 
1658 <CodeGroup>
1659 ```bash Linux theme={null}
1660 code ~/.config/Claude/claude_desktop_config.json
1661 ```
1662 
1663 ```bash macOS theme={null}
1664 code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1665 ```
1666 
1667 ```powershell Windows theme={null}
1668 code $env:AppData\Claude\claude_desktop_config.json
1669 ```
1670 </CodeGroup>
1671 
1672 You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1673 In this case, we'll add our single weather server like so:
1674 
1675 <CodeGroup>
1676 ```json macOS/Linux theme={null}
1677 {
1678 "mcpServers": {
1679 "weather": {
1680 "command": "dotnet",
1681 "args": ["run", "--project", "/ABSOLUTE/PATH/TO/PROJECT", "--no-build"]
1682 }
1683 }
1684 }
1685 ```
1686 
1687 ```json Windows theme={null}
1688 {
1689 "mcpServers": {
1690 "weather": {
1691 "command": "dotnet",
1692 "args": [
1693 "run",
1694 "--project",
1695 "C:\\ABSOLUTE\\PATH\\TO\\PROJECT",
1696 "--no-build"
1697 ]
1698 }
1699 }
1700 }
1701 ```
1702 </CodeGroup>
1703 
1704 This tells Claude for Desktop:
1705 
1706 1. There's an MCP server named "weather"
1707 2. Launch it by running `dotnet run /ABSOLUTE/PATH/TO/PROJECT`
1708 Save the file, and restart **Claude for Desktop**.
1709 </Tab>
1710 
1711 <Tab title="Ruby">
1712 Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-ruby)
1713 
1714 ### Prerequisite knowledge
1715 
1716 This quickstart assumes you have familiarity with:
1717 
1718 * Ruby
1719 * LLMs like Claude
1720 
1721 ### Logging in MCP Servers
1722 
1723 When implementing MCP servers, be careful about how you handle logging:
1724 
1725 **For STDIO-based servers:** Never use `puts` or `print`, as they write to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1726 
1727 **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1728 
1729 ### Best Practices
1730 
1731 * Use a logging library that writes to stderr or files.
1732 
1733 ### Quick Examples
1734 
1735 ```ruby theme={null}
1736 # ❌ Bad (STDIO)
1737 puts "Processing request"
1738 
1739 # ✅ Good (STDIO)
1740 require "logger"
1741 logger = Logger.new($stderr)
1742 logger.info("Processing request")
1743 ```
1744 
1745 ### System requirements
1746 
1747 * Ruby 2.7 or higher installed.
1748 
1749 ### Set up your environment
1750 
1751 First, let's make sure you have Ruby installed. You can check by running:
1752 
1753 ```bash theme={null}
1754 ruby --version
1755 ```
1756 
1757 Now, let's create and set up our project:
1758 
1759 <CodeGroup>
1760 ```bash macOS/Linux theme={null}
1761 # Create a new directory for our project
1762 mkdir weather
1763 cd weather
1764 
1765 # Create a Gemfile
1766 bundle init
1767 
1768 # Add the MCP SDK dependency
1769 bundle add mcp
1770 
1771 # Create our server file
1772 touch weather.rb
1773 ```
1774 
1775 ```powershell Windows theme={null}
1776 # Create a new directory for our project
1777 mkdir weather
1778 cd weather
1779 
1780 # Create a Gemfile
1781 bundle init
1782 
1783 # Add the MCP SDK dependency
1784 bundle add mcp
1785 
1786 # Create our server file
1787 new-item weather.rb
1788 ```
1789 </CodeGroup>
1790 
1791 Now let's dive into building your server.
1792 
1793 ## Building your server
1794 
1795 ### Importing packages and setting up constants
1796 
1797 Open `weather.rb` and add these requires and constants at the top:
1798 
1799 ```ruby theme={null}
1800 require "json"
1801 require "mcp"
1802 require "net/http"
1803 require "uri"
1804 
1805 NWS_API_BASE = "https://api.weather.gov"
1806 USER_AGENT = "weather-app/1.0"
1807 ```
1808 
1809 The `mcp` gem provides the Model Context Protocol SDK for Ruby, with classes for server implementation and stdio transport.
1810 
1811 ### Helper methods
1812 
1813 Next, let's add helper methods for querying and formatting data from the National Weather Service API:
1814 
1815 ```ruby theme={null}
1816 module HelperMethods
1817 def make_nws_request(url)
1818 uri = URI(url)
1819 request = Net::HTTP::Get.new(uri)
1820 request["User-Agent"] = USER_AGENT
1821 request["Accept"] = "application/geo+json"
1822 
1823 response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
1824 http.request(request)
1825 end
1826 
1827 raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
1828 
1829 JSON.parse(response.body)
1830 end
1831 
1832 def format_alert(feature)
1833 properties = feature["properties"]
1834 
1835 <<~ALERT
1836 Event: #{properties["event"] || "Unknown"}
1837 Area: #{properties["areaDesc"] || "Unknown"}
1838 Severity: #{properties["severity"] || "Unknown"}
1839 Description: #{properties["description"] || "No description available"}
1840 Instructions: #{properties["instruction"] || "No specific instructions provided"}
1841 ALERT
1842 end
1843 end
1844 ```
1845 
1846 ### Implementing tool execution
1847 
1848 Now let's define our tool classes. Each tool subclasses `MCP::Tool` and implements the tool logic:
1849 
1850 ```ruby theme={null}
1851 class GetAlerts < MCP::Tool
1852 extend HelperMethods
1853 
1854 tool_name "get_alerts"
1855 description "Get weather alerts for a US state"
1856 input_schema(
1857 properties: {
1858 state: {
1859 type: "string",
1860 description: "Two-letter US state code (e.g. CA, NY)"
1861 }
1862 },
1863 required: ["state"]
1864 )
1865 
1866 def self.call(state:)
1867 url = "#{NWS_API_BASE}/alerts/active/area/#{state.upcase}"
1868 data = make_nws_request(url)
1869 
1870 if data["features"].empty?
1871 return MCP::Tool::Response.new([{
1872 type: "text",
1873 text: "No active alerts for this state."
1874 }])
1875 end
1876 
1877 alerts = data["features"].map { |feature| format_alert(feature) }
1878 MCP::Tool::Response.new([{
1879 type: "text",
1880 text: alerts.join("\n---\n")
1881 }])
1882 end
1883 end
1884 
1885 class GetForecast < MCP::Tool
1886 extend HelperMethods
1887 
1888 tool_name "get_forecast"
1889 description "Get weather forecast for a location"
1890 input_schema(
1891 properties: {
1892 latitude: {
1893 type: "number",
1894 description: "Latitude of the location"
1895 },
1896 longitude: {
1897 type: "number",
1898 description: "Longitude of the location"
1899 }
1900 },
1901 required: ["latitude", "longitude"]
1902 )
1903 
1904 def self.call(latitude:, longitude:)
1905 # First get the forecast grid endpoint.
1906 points_url = "#{NWS_API_BASE}/points/#{latitude},#{longitude}"
1907 points_data = make_nws_request(points_url)
1908 
1909 # Get the forecast URL from the points response.
1910 forecast_url = points_data["properties"]["forecast"]
1911 forecast_data = make_nws_request(forecast_url)
1912 
1913 # Format the periods into a readable forecast.
1914 periods = forecast_data["properties"]["periods"]
1915 forecasts = periods.first(5).map do |period|
1916 <<~FORECAST
1917 #{period["name"]}:
1918 Temperature: #{period["temperature"]}°#{period["temperatureUnit"]}
1919 Wind: #{period["windSpeed"]} #{period["windDirection"]}
1920 Forecast: #{period["detailedForecast"]}
1921 FORECAST
1922 end
1923 
1924 MCP::Tool::Response.new([{
1925 type: "text",
1926 text: forecasts.join("\n---\n")
1927 }])
1928 end
1929 end
1930 ```
1931 
1932 ### Running the server
1933 
1934 Finally, initialize and run the server:
1935 
1936 ```ruby theme={null}
1937 server = MCP::Server.new(
1938 name: "weather",
1939 version: "1.0.0",
1940 tools: [GetAlerts, GetForecast]
1941 )
1942 
1943 transport = MCP::Server::Transports::StdioTransport.new(server)
1944 transport.open
1945 ```
1946 
1947 Your server is complete! Run `bundle exec ruby weather.rb` to start the MCP server, which will listen for messages from MCP hosts.
1948 
1949 Let's now test your server from an existing MCP host, Claude for Desktop.
1950 
1951 ## Testing your server with Claude for Desktop
1952 
1953 First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1954 
1955 We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1956 
1957 For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1958 
1959 <CodeGroup>
1960 ```bash Linux theme={null}
1961 code ~/.config/Claude/claude_desktop_config.json
1962 ```
1963 
1964 ```bash macOS theme={null}
1965 code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1966 ```
1967 
1968 ```powershell Windows theme={null}
1969 code $env:AppData\Claude\claude_desktop_config.json
1970 ```
1971 </CodeGroup>
1972 
1973 You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1974 
1975 In this case, we'll add our single weather server like so:
1976 
1977 <CodeGroup>
1978 ```json macOS/Linux theme={null}
1979 {
1980 "mcpServers": {
1981 "weather": {
1982 "command": "bundle",
1983 "args": ["exec", "ruby", "weather.rb"],
1984 "cwd": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather"
1985 }
1986 }
1987 }
1988 ```
1989 
1990 ```json Windows theme={null}
1991 {
1992 "mcpServers": {
1993 "weather": {
1994 "command": "bundle",
1995 "args": ["exec", "ruby", "weather.rb"],
1996 "cwd": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather"
1997 }
1998 }
1999 }
2000 ```
2001 </CodeGroup>
2002 
2003 <Note>
2004 Make sure you pass in the absolute path to your project directory in the `cwd` field. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
2005 </Note>
2006 
2007 This tells Claude for Desktop:
2008 
2009 1. There's an MCP server named "weather"
2010 2. Launch it by running `bundle exec ruby weather.rb` in the specified directory
2011 
2012 Save the file, and restart **Claude for Desktop**.
2013 </Tab>
2014 
2015 <Tab title="Rust">
2016 Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-rust)
2017 
2018 ### Prerequisite knowledge
2019 
2020 This quickstart assumes you have familiarity with:
2021 
2022 * Rust programming language
2023 * Async/await in Rust
2024 * LLMs like Claude
2025 
2026 ### Logging in MCP Servers
2027 
2028 When implementing MCP servers, be careful about how you handle logging:
2029 
2030 **For STDIO-based servers:** Never use `println!()` or `print!()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
2031 
2032 **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
2033 
2034 ### Best Practices
2035 
2036 * Use a logging library that writes to stderr or files, such as `tracing` or `log` in Rust.
2037 * Configure your logging framework to avoid stdout output.
2038 
2039 ### Quick Examples
2040 
2041 ```rust theme={null}
2042 // ❌ Bad (STDIO)
2043 println!("Processing request");
2044 
2045 // ✅ Good (STDIO)
2046 eprintln!("Processing request"); // writes to stderr
2047 ```
2048 
2049 ### System requirements
2050 
2051 * Rust 1.70 or higher installed.
2052 * Cargo (comes with Rust installation).
2053 
2054 ### Set up your environment
2055 
2056 First, let's install Rust if you haven't already. You can install Rust from [rust-lang.org](https://www.rust-lang.org/tools/install):
2057 
2058 <CodeGroup>
2059 ```bash macOS/Linux theme={null}
2060 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
2061 ```
2062 
2063 ```powershell Windows theme={null}
2064 # Download and run rustup-init.exe from https://rustup.rs/
2065 ```
2066 </CodeGroup>
2067 
2068 Verify your Rust installation:
2069 
2070 ```bash theme={null}
2071 rustc --version
2072 cargo --version
2073 ```
2074 
2075 Now, let's create and set up our project:
2076 
2077 <CodeGroup>
2078 ```bash macOS/Linux theme={null}
2079 # Create a new Rust project
2080 cargo new weather
2081 cd weather
2082 ```
2083 
2084 ```powershell Windows theme={null}
2085 # Create a new Rust project
2086 cargo new weather
2087 cd weather
2088 ```
2089 </CodeGroup>
2090 
2091 Update your `Cargo.toml` to add the required dependencies:
2092 
2093 ```toml Cargo.toml theme={null}
2094 [package]
2095 name = "weather"
2096 version = "0.1.0"
2097 edition = "2024"
2098 
2099 [dependencies]
2100 rmcp = { version = "0.3", features = ["server", "macros", "transport-io"] }
2101 tokio = { version = "1.46", features = ["full"] }
2102 reqwest = { version = "0.12", features = ["json"] }
2103 serde = { version = "1.0", features = ["derive"] }
2104 serde_json = "1.0"
2105 anyhow = "1.0"
2106 tracing = "0.1"
2107 tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] }
2108 ```
2109 
2110 Now let's dive into building your server.
2111 
2112 ## Building your server
2113 
2114 ### Importing packages and constants
2115 
2116 Open `src/main.rs` and add these imports and constants at the top:
2117 
2118 ```rust theme={null}
2119 use anyhow::Result;
2120 use rmcp::{
2121 ServerHandler, ServiceExt,
2122 handler::server::{router::tool::ToolRouter, tool::Parameters},
2123 model::*,
2124 schemars, tool, tool_handler, tool_router,
2125 };
2126 use serde::Deserialize;
2127 use serde::de::DeserializeOwned;
2128 
2129 const NWS_API_BASE: &str = "https://api.weather.gov";
2130 const USER_AGENT: &str = "weather-app/1.0";
2131 ```
2132 
2133 The `rmcp` crate provides the Model Context Protocol SDK for Rust, with features for server implementation, procedural macros, and stdio transport.
2134 
2135 ### Data structures
2136 
2137 Next, let's define the data structures for deserializing responses from the National Weather Service API:
2138 
2139 ```rust theme={null}
2140 #[derive(Debug, Deserialize)]
2141 struct AlertsResponse {
2142 features: Vec<AlertFeature>,
2143 }
2144 
2145 #[derive(Debug, Deserialize)]
2146 struct AlertFeature {
2147 properties: AlertProperties,
2148 }
2149 
2150 #[derive(Debug, Deserialize)]
2151 struct AlertProperties {
2152 event: Option<String>,
2153 #[serde(rename = "areaDesc")]
2154 area_desc: Option<String>,
2155 severity: Option<String>,
2156 description: Option<String>,
2157 instruction: Option<String>,
2158 }
2159 
2160 #[derive(Debug, Deserialize)]
2161 struct PointsResponse {
2162 properties: PointsProperties,
2163 }
2164 
2165 #[derive(Debug, Deserialize)]
2166 struct PointsProperties {
2167 forecast: String,
2168 }
2169 
2170 #[derive(Debug, Deserialize)]
2171 struct ForecastResponse {
2172 properties: ForecastProperties,
2173 }
2174 
2175 #[derive(Debug, Deserialize)]
2176 struct ForecastProperties {
2177 periods: Vec<ForecastPeriod>,
2178 }
2179 
2180 #[derive(Debug, Deserialize)]
2181 struct ForecastPeriod {
2182 name: String,
2183 temperature: i32,
2184 #[serde(rename = "temperatureUnit")]
2185 temperature_unit: String,
2186 #[serde(rename = "windSpeed")]
2187 wind_speed: String,
2188 #[serde(rename = "windDirection")]
2189 wind_direction: String,
2190 #[serde(rename = "detailedForecast")]
2191 detailed_forecast: String,
2192 }
2193 ```
2194 
2195 Now define the request types that MCP clients will send:
2196 
2197 ```rust theme={null}
2198 #[derive(serde::Deserialize, schemars::JsonSchema)]
2199 pub struct MCPForecastRequest {
2200 latitude: f32,
2201 longitude: f32,
2202 }
2203 
2204 #[derive(serde::Deserialize, schemars::JsonSchema)]
2205 pub struct MCPAlertRequest {
2206 state: String,
2207 }
2208 ```
2209 
2210 ### Helper functions
2211 
2212 Add helper functions for making API requests and formatting responses:
2213 
2214 ```rust theme={null}
2215 async fn make_nws_request<T: DeserializeOwned>(url: &str) -> Result<T> {
2216 let client = reqwest::Client::new();
2217 let rsp = client
2218 .get(url)
2219 .header(reqwest::header::USER_AGENT, USER_AGENT)
2220 .header(reqwest::header::ACCEPT, "application/geo+json")
2221 .send()
2222 .await?
2223 .error_for_status()?;
2224 Ok(rsp.json::<T>().await?)
2225 }
2226 
2227 fn format_alert(feature: &AlertFeature) -> String {
2228 let props = &feature.properties;
2229 format!(
2230 "Event: {}\nArea: {}\nSeverity: {}\nDescription: {}\nInstructions: {}",
2231 props.event.as_deref().unwrap_or("Unknown"),
2232 props.area_desc.as_deref().unwrap_or("Unknown"),
2233 props.severity.as_deref().unwrap_or("Unknown"),
2234 props
2235 .description
2236 .as_deref()
2237 .unwrap_or("No description available"),
2238 props
2239 .instruction
2240 .as_deref()
2241 .unwrap_or("No specific instructions provided")
2242 )
2243 }
2244 
2245 fn format_period(period: &ForecastPeriod) -> String {
2246 format!(
2247 "{}:\nTemperature: {}°{}\nWind: {} {}\nForecast: {}",
2248 period.name,
2249 period.temperature,
2250 period.temperature_unit,
2251 period.wind_speed,
2252 period.wind_direction,
2253 period.detailed_forecast
2254 )
2255 }
2256 ```
2257 
2258 ### Implementing the Weather server and tools
2259 
2260 Now let's implement the main Weather server struct with the tool handlers:
2261 
2262 ```rust theme={null}
2263 pub struct Weather {
2264 tool_router: ToolRouter<Weather>,
2265 }
2266 
2267 #[tool_router]
2268 impl Weather {
2269 fn new() -> Self {
2270 Self {
2271 tool_router: Self::tool_router(),
2272 }
2273 }
2274 
2275 #[tool(description = "Get weather alerts for a US state.")]
2276 async fn get_alerts(
2277 &self,
2278 Parameters(MCPAlertRequest { state }): Parameters<MCPAlertRequest>,
2279 ) -> String {
2280 let url = format!(
2281 "{}/alerts/active/area/{}",
2282 NWS_API_BASE,
2283 state.to_uppercase()
2284 );
2285 
2286 match make_nws_request::<AlertsResponse>(&url).await {
2287 Ok(data) => {
2288 if data.features.is_empty() {
2289 "No active alerts for this state.".to_string()
2290 } else {
2291 data.features
2292 .iter()
2293 .map(format_alert)
2294 .collect::<Vec<_>>()
2295 .join("\n---\n")
2296 }
2297 }
2298 Err(_) => "Unable to fetch alerts or no alerts found.".to_string(),
2299 }
2300 }
2301 
2302 #[tool(description = "Get weather forecast for a location.")]
2303 async fn get_forecast(
2304 &self,
2305 Parameters(MCPForecastRequest {
2306 latitude,
2307 longitude,
2308 }): Parameters<MCPForecastRequest>,
2309 ) -> String {
2310 let points_url = format!("{NWS_API_BASE}/points/{latitude},{longitude}");
2311 let Ok(points_data) = make_nws_request::<PointsResponse>(&points_url).await else {
2312 return "Unable to fetch forecast data for this location.".to_string();
2313 };
2314 
2315 let forecast_url = points_data.properties.forecast;
2316 
2317 let Ok(forecast_data) = make_nws_request::<ForecastResponse>(&forecast_url).await else {
2318 return "Unable to fetch forecast data for this location.".to_string();
2319 };
2320 
2321 let periods = &forecast_data.properties.periods;
2322 let forecast_summary: String = periods
2323 .iter()
2324 .take(5) // Next 5 periods only
2325 .map(format_period)
2326 .collect::<Vec<String>>()
2327 .join("\n---\n");
2328 forecast_summary
2329 }
2330 }
2331 ```
2332 
2333 The `#[tool_router]` macro automatically generates the routing logic, and the `#[tool]` attribute marks methods as MCP tools.
2334 
2335 ### Implementing the ServerHandler
2336 
2337 Implement the `ServerHandler` trait to define server capabilities:
2338 
2339 ```rust theme={null}
2340 #[tool_handler]
2341 impl ServerHandler for Weather {
2342 fn get_info(&self) -> ServerInfo {
2343 ServerInfo {
2344 capabilities: ServerCapabilities::builder().enable_tools().build(),
2345 ..Default::default()
2346 }
2347 }
2348 }
2349 ```
2350 
2351 ### Running the server
2352 
2353 Finally, implement the main function to run the server with stdio transport:
2354 
2355 ```rust theme={null}
2356 #[tokio::main]
2357 async fn main() -> Result<()> {
2358 let transport = (tokio::io::stdin(), tokio::io::stdout());
2359 let service = Weather::new().serve(transport).await?;
2360 service.waiting().await?;
2361 Ok(())
2362 }
2363 ```
2364 
2365 Build your server with:
2366 
2367 ```bash theme={null}
2368 cargo build --release
2369 ```
2370 
2371 The compiled binary will be in `target/release/weather`.
2372 
2373 Let's now test your server from an existing MCP host, Claude for Desktop.
2374 
2375 ## Testing your server with Claude for Desktop
2376 
2377 First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2378 
2379 We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2380 
2381 For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2382 
2383 <CodeGroup>
2384 ```bash Linux theme={null}
2385 code ~/.config/Claude/claude_desktop_config.json
2386 ```
2387 
2388 ```bash macOS theme={null}
2389 code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2390 ```
2391 
2392 ```powershell Windows theme={null}
2393 code $env:AppData\Claude\claude_desktop_config.json
2394 ```
2395 </CodeGroup>
2396 
2397 You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2398 
2399 In this case, we'll add our single weather server like so:
2400 
2401 <CodeGroup>
2402 ```json macOS/Linux theme={null}
2403 {
2404 "mcpServers": {
2405 "weather": {
2406 "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/target/release/weather"
2407 }
2408 }
2409 }
2410 ```
2411 
2412 ```json Windows theme={null}
2413 {
2414 "mcpServers": {
2415 "weather": {
2416 "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\target\\release\\weather.exe"
2417 }
2418 }
2419 }
2420 ```
2421 </CodeGroup>
2422 
2423 <Note>
2424 Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2425 </Note>
2426 
2427 This tells Claude for Desktop:
2428 
2429 1. There's an MCP server named "weather"
2430 2. Launch it by running the compiled binary at the specified path
2431 
2432 Save the file, and restart **Claude for Desktop**.
2433 </Tab>
2434 
2435 <Tab title="Go">
2436 Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-go)
2437 
2438 ### Prerequisite knowledge
2439 
2440 This quickstart assumes you have familiarity with:
2441 
2442 * Go
2443 * LLMs like Claude
2444 
2445 ### Logging in MCP Servers
2446 
2447 When implementing MCP servers, be careful about how you handle logging:
2448 
2449 **For STDIO-based servers:** Never use `fmt.Println()` or `fmt.Printf()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
2450 
2451 **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
2452 
2453 ### Best Practices
2454 
2455 * Use `log.Println()` (which defaults to stderr) or a logging library that writes to stderr or files.
2456 * Use `fmt.Fprintf(os.Stderr, ...)` to write to stderr explicitly.
2457 
2458 ### Quick Examples
2459 
2460 ```go theme={null}
2461 // ❌ Bad (STDIO)
2462 fmt.Println("Processing request")
2463 
2464 // ✅ Good (STDIO)
2465 log.Println("Processing request") // defaults to stderr
2466 
2467 // ✅ Good (STDIO)
2468 fmt.Fprintln(os.Stderr, "Processing request")
2469 ```
2470 
2471 ### System requirements
2472 
2473 * Go 1.24 or higher installed.
2474 
2475 ### Set up your environment
2476 
2477 First, let's install Go if you haven't already. You can download and install Go from [go.dev](https://go.dev/dl/).
2478 
2479 Verify your Go installation:
2480 
2481 ```bash theme={null}
2482 go version
2483 ```
2484 
2485 Now, let's create and set up our project:
2486 
2487 <CodeGroup>
2488 ```bash macOS/Linux theme={null}
2489 # Create a new directory for our project
2490 mkdir weather
2491 cd weather
2492 
2493 # Initialize Go module
2494 go mod init weather
2495 
2496 # Install dependencies
2497 go get github.com/modelcontextprotocol/go-sdk/mcp
2498 
2499 # Create our server file
2500 touch main.go
2501 ```
2502 
2503 ```powershell Windows theme={null}
2504 # Create a new directory for our project
2505 md weather
2506 cd weather
2507 
2508 # Initialize Go module
2509 go mod init weather
2510 
2511 # Install dependencies
2512 go get github.com/modelcontextprotocol/go-sdk/mcp
2513 
2514 # Create our server file
2515 new-item main.go
2516 ```
2517 </CodeGroup>
2518 
2519 Now let's dive into building your server.
2520 
2521 ## Building your server
2522 
2523 ### Importing packages and constants
2524 
2525 Add these to the top of your `main.go`:
2526 
2527 ```go theme={null}
2528 package main
2529 
2530 import (
2531 "cmp"
2532 "context"
2533 "encoding/json"
2534 "fmt"
2535 "io"
2536 "log"
2537 "net/http"
2538 "strings"
2539 
2540 "github.com/modelcontextprotocol/go-sdk/mcp"
2541 )
2542 
2543 const (
2544 NWSAPIBase = "https://api.weather.gov"
2545 UserAgent = "weather-app/1.0"
2546 )
2547 ```
2548 
2549 ### Data structures
2550 
2551 Next, let's define the data structures used by our tools:
2552 
2553 ```go theme={null}
2554 type PointsResponse struct {
2555 Properties struct {
2556 Forecast string `json:"forecast"`
2557 } `json:"properties"`
2558 }
2559 
2560 type ForecastResponse struct {
2561 Properties struct {
2562 Periods []ForecastPeriod `json:"periods"`
2563 } `json:"properties"`
2564 }
2565 
2566 type ForecastPeriod struct {
2567 Name string `json:"name"`
2568 Temperature int `json:"temperature"`
2569 TemperatureUnit string `json:"temperatureUnit"`
2570 WindSpeed string `json:"windSpeed"`
2571 WindDirection string `json:"windDirection"`
2572 DetailedForecast string `json:"detailedForecast"`
2573 }
2574 
2575 type AlertsResponse struct {
2576 Features []AlertFeature `json:"features"`
2577 }
2578 
2579 type AlertFeature struct {
2580 Properties AlertProperties `json:"properties"`
2581 }
2582 
2583 type AlertProperties struct {
2584 Event string `json:"event"`
2585 AreaDesc string `json:"areaDesc"`
2586 Severity string `json:"severity"`
2587 Description string `json:"description"`
2588 Instruction string `json:"instruction"`
2589 }
2590 
2591 type ForecastInput struct {
2592 Latitude float64 `json:"latitude" jsonschema:"Latitude of the location"`
2593 Longitude float64 `json:"longitude" jsonschema:"Longitude of the location"`
2594 }
2595 
2596 type AlertsInput struct {
2597 State string `json:"state" jsonschema:"Two-letter US state code (e.g. CA, NY)"`
2598 }
2599 ```
2600 
2601 ### Helper functions
2602 
2603 Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
2604 
2605 ```go theme={null}
2606 func makeNWSRequest[T any](ctx context.Context, url string) (*T, error) {
2607 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
2608 if err != nil {
2609 return nil, fmt.Errorf("failed to create request: %w", err)
2610 }
2611 
2612 req.Header.Set("User-Agent", UserAgent)
2613 req.Header.Set("Accept", "application/geo+json")
2614 
2615 client := http.DefaultClient
2616 resp, err := client.Do(req)
2617 if err != nil {
2618 return nil, fmt.Errorf("failed to make request to %s: %w", url, err)
2619 }
2620 defer resp.Body.Close()
2621 
2622 if resp.StatusCode != http.StatusOK {
2623 body, _ := io.ReadAll(resp.Body)
2624 return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
2625 }
2626 
2627 var result T
2628 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
2629 return nil, fmt.Errorf("failed to decode response: %w", err)
2630 }
2631 
2632 return &result, nil
2633 }
2634 
2635 func formatAlert(alert AlertFeature) string {
2636 props := alert.Properties
2637 event := cmp.Or(props.Event, "Unknown")
2638 areaDesc := cmp.Or(props.AreaDesc, "Unknown")
2639 severity := cmp.Or(props.Severity, "Unknown")
2640 description := cmp.Or(props.Description, "No description available")
2641 instruction := cmp.Or(props.Instruction, "No specific instructions provided")
2642 
2643 return fmt.Sprintf(`
2644 Event: %s
2645 Area: %s
2646 Severity: %s
2647 Description: %s
2648 Instructions: %s
2649 `, event, areaDesc, severity, description, instruction)
2650 }
2651 
2652 func formatPeriod(period ForecastPeriod) string {
2653 return fmt.Sprintf(`
2654 %s:
2655 Temperature: %d°%s
2656 Wind: %s %s
2657 Forecast: %s
2658 `, period.Name, period.Temperature, period.TemperatureUnit,
2659 period.WindSpeed, period.WindDirection, period.DetailedForecast)
2660 }
2661 ```
2662 
2663 ### Implementing tool execution
2664 
2665 The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
2666 
2667 ```go theme={null}
2668 func getForecast(ctx context.Context, req *mcp.CallToolRequest, input ForecastInput) (
2669 *mcp.CallToolResult, any, error,
2670 ) {
2671 // Get points data
2672 pointsURL := fmt.Sprintf("%s/points/%f,%f", NWSAPIBase, input.Latitude, input.Longitude)
2673 pointsData, err := makeNWSRequest[PointsResponse](ctx, pointsURL)
2674 if err != nil {
2675 return &mcp.CallToolResult{
2676 Content: []mcp.Content{
2677 &mcp.TextContent{Text: "Unable to fetch forecast data for this location."},
2678 },
2679 }, nil, nil
2680 }
2681 
2682 // Get forecast data
2683 forecastURL := pointsData.Properties.Forecast
2684 if forecastURL == "" {
2685 return &mcp.CallToolResult{
2686 Content: []mcp.Content{
2687 &mcp.TextContent{Text: "Unable to fetch forecast URL."},
2688 },
2689 }, nil, nil
2690 }
2691 
2692 forecastData, err := makeNWSRequest[ForecastResponse](ctx, forecastURL)
2693 if err != nil {
2694 return &mcp.CallToolResult{
2695 Content: []mcp.Content{
2696 &mcp.TextContent{Text: "Unable to fetch detailed forecast."},
2697 },
2698 }, nil, nil
2699 }
2700 
2701 // Format the periods
2702 periods := forecastData.Properties.Periods
2703 if len(periods) == 0 {
2704 return &mcp.CallToolResult{
2705 Content: []mcp.Content{
2706 &mcp.TextContent{Text: "No forecast periods available."},
2707 },
2708 }, nil, nil
2709 }
2710 
2711 // Show next 5 periods
2712 var forecasts []string
2713 for i := range min(5, len(periods)) {
2714 forecasts = append(forecasts, formatPeriod(periods[i]))
2715 }
2716 
2717 result := strings.Join(forecasts, "\n---\n")
2718 
2719 return &mcp.CallToolResult{
2720 Content: []mcp.Content{
2721 &mcp.TextContent{Text: result},
2722 },
2723 }, nil, nil
2724 }
2725 
2726 func getAlerts(ctx context.Context, req *mcp.CallToolRequest, input AlertsInput) (
2727 *mcp.CallToolResult, any, error,
2728 ) {
2729 // Build alerts URL
2730 stateCode := strings.ToUpper(input.State)
2731 alertsURL := fmt.Sprintf("%s/alerts/active/area/%s", NWSAPIBase, stateCode)
2732 
2733 alertsData, err := makeNWSRequest[AlertsResponse](ctx, alertsURL)
2734 if err != nil {
2735 return &mcp.CallToolResult{
2736 Content: []mcp.Content{
2737 &mcp.TextContent{Text: "Unable to fetch alerts or no alerts found."},
2738 },
2739 }, nil, nil
2740 }
2741 
2742 // Check if there are any alerts
2743 if len(alertsData.Features) == 0 {
2744 return &mcp.CallToolResult{
2745 Content: []mcp.Content{
2746 &mcp.TextContent{Text: "No active alerts for this state."},
2747 },
2748 }, nil, nil
2749 }
2750 
2751 // Format alerts
2752 var alerts []string
2753 for _, feature := range alertsData.Features {
2754 alerts = append(alerts, formatAlert(feature))
2755 }
2756 
2757 result := strings.Join(alerts, "\n---\n")
2758 
2759 return &mcp.CallToolResult{
2760 Content: []mcp.Content{
2761 &mcp.TextContent{Text: result},
2762 },
2763 }, nil, nil
2764 }
2765 ```
2766 
2767 ### Running the server
2768 
2769 Finally, implement the main function to run the server:
2770 
2771 ```go theme={null}
2772 func main() {
2773 // Create MCP server
2774 server := mcp.NewServer(&mcp.Implementation{
2775 Name: "weather",
2776 Version: "1.0.0",
2777 }, nil)
2778 
2779 // Add get_forecast tool
2780 mcp.AddTool(server, &mcp.Tool{
2781 Name: "get_forecast",
2782 Description: "Get weather forecast for a location",
2783 }, getForecast)
2784 
2785 // Add get_alerts tool
2786 mcp.AddTool(server, &mcp.Tool{
2787 Name: "get_alerts",
2788 Description: "Get weather alerts for a US state",
2789 }, getAlerts)
2790 
2791 // Run server on stdio transport
2792 if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
2793 log.Fatal(err)
2794 }
2795 }
2796 ```
2797 
2798 Build your server with:
2799 
2800 ```bash theme={null}
2801 go build -o weather .
2802 ```
2803 
2804 The compiled binary will be in `./weather`.
2805 
2806 Let's now test your server from an existing MCP host, Claude for Desktop.
2807 
2808 ## Testing your server with Claude for Desktop
2809 
2810 First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2811 
2812 We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2813 
2814 For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2815 
2816 <CodeGroup>
2817 ```bash Linux theme={null}
2818 code ~/.config/Claude/claude_desktop_config.json
2819 ```
2820 
2821 ```bash macOS theme={null}
2822 code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2823 ```
2824 
2825 ```powershell Windows theme={null}
2826 code $env:AppData\Claude\claude_desktop_config.json
2827 ```
2828 </CodeGroup>
2829 
2830 You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2831 
2832 In this case, we'll add our single weather server like so:
2833 
2834 <CodeGroup>
2835 ```json macOS/Linux theme={null}
2836 {
2837 "mcpServers": {
2838 "weather": {
2839 "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/weather"
2840 }
2841 }
2842 }
2843 ```
2844 
2845 ```json Windows theme={null}
2846 {
2847 "mcpServers": {
2848 "weather": {
2849 "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\weather.exe"
2850 }
2851 }
2852 }
2853 ```
2854 </CodeGroup>
2855 
2856 <Note>
2857 Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2858 </Note>
2859 
2860 This tells Claude for Desktop:
2861 
2862 1. There's an MCP server named "weather"
2863 2. Launch it by running the compiled binary at the specified path
2864 
2865 Save the file, and restart **Claude for Desktop**.
2866 </Tab>
2867</Tabs>
2796 }
2797}
2798```
2799 
2800> [!NOTE]
2801>
2802> Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2803 
2804This tells Claude for Desktop:
2805 
28061. There's an MCP server named "weather"
28072. Launch it by running the compiled binary at the specified path
2808 
2809Save the file, and restart **Claude for Desktop**.
28682810 
28692811### Test with commands
28702812 
2871Let's make sure Claude for Desktop is picking up the two tools we've exposed in our `weather` server. You can do this by looking for the "Add files, connectors, and more /" <img src="https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/claude-add-files-connectors-and-more.png?fit=max&auto=format&n=cpXzQjUOzyH0mCNH&q=85&s=53acf21f6807dd5323b70b84b5d98d8a" style={{display: 'inline', margin: 0, height: '1.3em', width: 'auto'}} width="33" height="33" data-path="images/claude-add-files-connectors-and-more.png" /> icon:
2872 
2873<Frame>
2874 <img src="https://mintcdn.com/mcp/zNouQwo2h8cbxlDS/images/visual-indicator-mcp-tools.png?fit=max&auto=format&n=zNouQwo2h8cbxlDS&q=85&s=1bf23a2cfc5f6dd3dac1c7574cceebc9" width="684" height="133" data-path="images/visual-indicator-mcp-tools.png" />
2875</Frame>
2813Let's make sure Claude for Desktop is picking up the two tools we've exposed in our `weather` server. You can do this by looking for the "Add files, connectors, and more /" ![](https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/claude-add-files-connectors-and-more.png?fit=max\&auto=format\&n=cpXzQjUOzyH0mCNH\&q=85\&s=53acf21f6807dd5323b70b84b5d98d8a) icon:
2814 
2815![](https://mintcdn.com/mcp/zNouQwo2h8cbxlDS/images/visual-indicator-mcp-tools.png?fit=max\&auto=format\&n=zNouQwo2h8cbxlDS\&q=85\&s=1bf23a2cfc5f6dd3dac1c7574cceebc9)
28762816 
28772817After clicking on the plus icon, hover over the "Connectors" menu. You should see the `weather` servers listed:
28782818 
2879<Frame>
2880 <img src="https://mintcdn.com/mcp/zNouQwo2h8cbxlDS/images/available-mcp-tools.png?fit=max&auto=format&n=zNouQwo2h8cbxlDS&q=85&s=e2ace1ac88895a5fe30ebd8d01456bc3" width="437" height="244" data-path="images/available-mcp-tools.png" />
2881</Frame>
2819![](https://mintcdn.com/mcp/zNouQwo2h8cbxlDS/images/available-mcp-tools.png?fit=max\&auto=format\&n=zNouQwo2h8cbxlDS\&q=85\&s=e2ace1ac88895a5fe30ebd8d01456bc3)
28822820 
28832821If your server isn't being picked up by Claude for Desktop, proceed to the [Troubleshooting](#troubleshooting) section for debugging tips.
28842822 
from line 2825
28872825* What's the weather in Sacramento?
28882826* What are the active weather alerts in Texas?
28892827 
2890<Frame>
2891 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/current-weather.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=dce7b2f8a06c20ba358e4bd2e75fa4c7" width="2780" height="1849" data-path="images/current-weather.png" />
2892</Frame>
2893 
2894<Frame>
2895 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/weather-alerts.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=c4762bf2bd84a8781846d2965af3e4a4" width="2809" height="1850" data-path="images/weather-alerts.png" />
2896</Frame>
2897 
2898<Note>
2899 Since this is the US National Weather service, the queries will only work for US locations.
2900</Note>
2828![](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/current-weather.png?fit=max\&auto=format\&n=4ZXF1PrDkEaJvXpn\&q=85\&s=dce7b2f8a06c20ba358e4bd2e75fa4c7)
2829 
2830![](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/weather-alerts.png?fit=max\&auto=format\&n=4ZXF1PrDkEaJvXpn\&q=85\&s=c4762bf2bd84a8781846d2965af3e4a4)
2831 
2832> [!NOTE]
2833>
2834> Since this is the US National Weather service, the queries will only work for US locations.
29012835 
29022836## What's happening under the hood
29032837 
from line 2846
29122846 
29132847## Troubleshooting
29142848 
2915<AccordionGroup>
2916 <Accordion title="Claude for Desktop Integration Issues">
2917 **Getting logs from Claude for Desktop**
2918 
2919 Claude.app logging related to MCP is written to log files in `~/Library/Logs/Claude` (macOS) or `~/.config/Claude/logs/` (Linux):
2920 
2921 * `mcp.log` will contain general logging about MCP connections and connection failures.
2922 * Files named `mcp-server-SERVERNAME.log` will contain the stderr output from the named server. Stdio servers may use stderr for all their logging, so these files are not limited to errors.
2923 
2924 You can run the following command to list recent logs and follow along with any new ones:
2925 
2926 <CodeGroup>
2927 ```bash macOS theme={null}
2928 # Check Claude's logs for errors
2929 tail -n 20 -f ~/Library/Logs/Claude/mcp*.log
2930 ```
2931 
2932 ```bash Linux theme={null}
2933 # Check Claude's logs for errors
2934 tail -n 20 -f ~/.config/Claude/logs/mcp*.log
2935 ```
2936 </CodeGroup>
2937 
2938 **Server not showing up in Claude**
2939 
2940 1. Check your `claude_desktop_config.json` file syntax
2941 2. Make sure the path to your project is absolute and not relative
2942 3. Restart Claude for Desktop completely
2943 
2944 <Warning>
2945 To properly restart Claude for Desktop, you must fully quit the application:
2946 
2947 * **Windows**: Right-click the Claude icon in the system tray (which may be hidden in the "hidden icons" menu) and select "Quit" or "Exit".
2948 * **macOS**: Use Cmd+Q or select "Quit Claude" from the menu bar.
2949 * **Linux**: Right-click the Claude icon in the system tray and select "Quit", or run `pkill -f claude-desktop` from a terminal.
2950 
2951 Simply closing the window does not fully quit the application, and your MCP server configuration changes will not take effect.
2952 </Warning>
2953 
2954 **Tool calls failing silently**
2955 
2956 If Claude attempts to use the tools but they fail:
2957 
2958 1. Check Claude's logs for errors
2959 2. Verify your server builds and runs without errors
2960 3. Try restarting Claude for Desktop
2961 
2962 **None of this is working. What do I do?**
2963 
2964 Please refer to our [debugging guide](/docs/draft/tools/debugging) for better debugging tools and more detailed guidance.
2965 </Accordion>
2966 
2967 <Accordion title="Weather API Issues">
2968 **Error: Failed to retrieve grid point data**
2969 
2970 This usually means either:
2971 
2972 1. The coordinates are outside the US
2973 2. The NWS API is having issues
2974 3. You're being rate limited
2975 
2976 Fix:
2977 
2978 * Verify you're using US coordinates
2979 * Add a small delay between requests
2980 * Check the NWS API status page
2981 
2982 **Error: No active alerts for \[STATE]**
2983 
2984 This isn't an error - it just means there are no current weather alerts for that state. Try a different state or check during severe weather.
2985 </Accordion>
2986</AccordionGroup>
2987 
2988<Note>
2989 For more advanced troubleshooting, check out our guide on [Debugging MCP](/docs/draft/tools/debugging)
2990</Note>
2849> **Claude for Desktop Integration Issues**
2850>
2851> **Getting logs from Claude for Desktop**
2852>
2853> Claude.app logging related to MCP is written to log files in `~/Library/Logs/Claude` (macOS) or `~/.config/Claude/logs/` (Linux):
2854>
2855> * `mcp.log` will contain general logging about MCP connections and connection failures.
2856> * Files named `mcp-server-SERVERNAME.log` will contain the stderr output from the named server. Stdio servers may use stderr for all their logging, so these files are not limited to errors.
2857>
2858> You can run the following command to list recent logs and follow along with any new ones:
2859>
2860> ```bash macOS theme={null}
2861> # Check Claude's logs for errors
2862> tail -n 20 -f ~/Library/Logs/Claude/mcp*.log
2863> ```
2864>
2865> ```bash Linux theme={null}
2866> # Check Claude's logs for errors
2867> tail -n 20 -f ~/.config/Claude/logs/mcp*.log
2868> ```
2869>
2870> **Server not showing up in Claude**
2871>
2872> 1. Check your `claude_desktop_config.json` file syntax
2873>
2874> 2. Make sure the path to your project is absolute and not relative
2875>
2876> 3. Restart Claude for Desktop completely
2877>
2878> > [!WARNING]
2879> >
2880> > To properly restart Claude for Desktop, you must fully quit the application:
2881> >
2882> > * **Windows**: Right-click the Claude icon in the system tray (which may be hidden in the "hidden icons" menu) and select "Quit" or "Exit".
2883> > * **macOS**: Use Cmd+Q or select "Quit Claude" from the menu bar.
2884> > * **Linux**: Right-click the Claude icon in the system tray and select "Quit", or run `pkill -f claude-desktop` from a terminal.
2885> >
2886> > Simply closing the window does not fully quit the application, and your MCP server configuration changes will not take effect.
2887>
2888> **Tool calls failing silently**
2889>
2890> If Claude attempts to use the tools but they fail:
2891>
2892> 1. Check Claude's logs for errors
2893> 2. Verify your server builds and runs without errors
2894> 3. Try restarting Claude for Desktop
2895>
2896> **None of this is working. What do I do?**
2897>
2898> Please refer to our [debugging guide](/docs/draft/tools/debugging) for better debugging tools and more detailed guidance.
2899 
2900> **Weather API Issues**
2901>
2902> **Error: Failed to retrieve grid point data**
2903>
2904> This usually means either:
2905>
2906> 1. The coordinates are outside the US
2907> 2. The NWS API is having issues
2908> 3. You're being rate limited
2909>
2910> Fix:
2911>
2912> * Verify you're using US coordinates
2913> * Add a small delay between requests
2914> * Check the NWS API status page
2915>
2916> **Error: No active alerts for \[STATE]**
2917>
2918> This isn't an error - it just means there are no current weather alerts for that state. Try a different state or check during severe weather.
2919 
2920> [!NOTE]
2921>
2922> For more advanced troubleshooting, check out our guide on [Debugging MCP](/docs/draft/tools/debugging)
29912923 
29922924## Next steps
29932925 
2994<CardGroup cols={2}>
2995 <Card title="Building a client" icon="outlet" href="/docs/draft/develop/build-client">
2996 Learn how to build your own MCP client that can connect to your server
2997 </Card>
2998 
2999 <Card title="Example servers" icon="grid" href="/examples">
3000 Check out our gallery of official MCP servers and implementations
3001 </Card>
3002 
3003 <Card title="Debugging Guide" icon="bug" href="/docs/draft/tools/debugging">
3004 Learn how to effectively debug MCP servers and integrations
3005 </Card>
3006 
3007 <Card title="Build with Agent Skills" icon="comments" href="/docs/draft/develop/build-with-agent-skills">
3008 Use agent skills to guide AI coding assistants through server design
3009 </Card>
3010</CardGroup>
2926> **[Building a client](/docs/draft/develop/build-client)**
2927>
2928> Learn how to build your own MCP client that can connect to your server
2929 
2930> **[Example servers](/examples)**
2931>
2932> Check out our gallery of official MCP servers and implementations
2933 
2934> **[Debugging Guide](/docs/draft/tools/debugging)**
2935>
2936> Learn how to effectively debug MCP servers and integrations
2937 
2938> **[Build with Agent Skills](/docs/draft/develop/build-with-agent-skills)**
2939>
2940> Use agent skills to guide AI coding assistants through server design
30112941 

docs/draft/develop/build-with-agent-skills Changed · +9 / -11 lines

from line 85
8585Once your agent scaffolds the server, iterate on tool descriptions and error
8686handling, then test and ship:
8787 
88<CardGroup cols={2}>
89 <Card title="MCP Inspector" icon="magnifying-glass" href="/docs/draft/tools/inspector">
90 Test your server's tools, resources, and prompts interactively
91 </Card>
88> **[MCP Inspector](/docs/draft/tools/inspector)**
89>
90> Test your server's tools, resources, and prompts interactively
9291 
93 <Card title="Connect to a client" icon="plug" href="/docs/draft/develop/connect-local-servers">
94 Wire your server into an MCP client via local or remote configuration
95 </Card>
92> **[Connect to a client](/docs/draft/develop/connect-local-servers)**
93>
94> Wire your server into an MCP client via local or remote configuration
9695 
97 <Card title="Publish to the Registry" icon="box" href="/registry/quickstart">
98 Make your server discoverable in the MCP Registry
99 </Card>
100</CardGroup>
96> **[Publish to the Registry](/registry/quickstart)**
97>
98> Make your server discoverable in the MCP Registry
10199 

docs/draft/develop/connect-local-servers Changed · +188 / -212 lines

from line 6
66 
77This guide demonstrates how to connect to local MCP servers using Claude Desktop as an example, one of the many clients that support MCP. While we focus on Claude Desktop's implementation, the concepts apply broadly to other MCP-compatible clients. By the end of this tutorial, Claude will be able to interact with files on your computer, create new documents, organize folders, and search through your file system—all with your explicit permission for each action.
88 
9<Frame>
10 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-filesystem.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=629d7e754dc358d71a408d6ce970c1b1" alt="Claude Desktop with filesystem integration showing file management capabilities" width="1732" height="2060" data-path="images/quickstart-filesystem.png" />
11</Frame>
9![Claude Desktop with filesystem integration showing file management capabilities](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-filesystem.png?fit=max\&auto=format\&n=4ZXF1PrDkEaJvXpn\&q=85\&s=629d7e754dc358d71a408d6ce970c1b1)
1210 
1311## Prerequisites
1412 
from line 43
4543 
4644The process involves configuring Claude Desktop to automatically start the Filesystem Server whenever you launch the application. This configuration is done through a JSON file that tells Claude Desktop which servers to run and how to connect to them.
4745 
48<Steps>
49 <Step title="Open Claude Desktop Settings">
50 Start by accessing the Claude Desktop settings. Click on the Claude menu in your system's menu bar (not the settings within the Claude window itself) and select "Settings..."
46**1. Open Claude Desktop Settings**
5147 
52 On macOS, this appears in the top menu bar:
48Start by accessing the Claude Desktop settings. Click on the Claude menu in your system's menu bar (not the settings within the Claude window itself) and select "Settings..."
5349 
54 <Frame style={{ textAlign: "center" }}>
55 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-menu.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=0c8b57e0e17af3624b6762a3ea944c8e" width="400" alt="Claude Desktop menu showing Settings option" data-path="images/quickstart-menu.png" />
56 </Frame>
50On macOS, this appears in the top menu bar:
5751 
58 This opens the Claude Desktop configuration window, which is separate from your Claude account settings.
59 </Step>
52![Claude Desktop menu showing Settings option](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-menu.png?fit=max\&auto=format\&n=4ZXF1PrDkEaJvXpn\&q=85\&s=0c8b57e0e17af3624b6762a3ea944c8e)
6053 
61 <Step title="Access Developer Settings">
62 In the Settings window, navigate to the "Developer" tab in the left sidebar. This section contains options for configuring MCP servers and other developer features.
54This opens the Claude Desktop configuration window, which is separate from your Claude account settings.
6355 
64 Click the "Edit Config" button to open the configuration file:
56**2. Access Developer Settings**
6557 
66 <Frame>
67 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-developer.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=0fb595490a2f9e15c0301e771a57446c" alt="Developer settings showing Edit Config button" width="1688" height="534" data-path="images/quickstart-developer.png" />
68 </Frame>
58In the Settings window, navigate to the "Developer" tab in the left sidebar. This section contains options for configuring MCP servers and other developer features.
6959 
70 This action creates a new configuration file if one doesn't exist, or opens your existing configuration. The file is located at:
60Click the "Edit Config" button to open the configuration file:
7161 
72 * **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
73 * **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
74 </Step>
62![Developer settings showing Edit Config button](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-developer.png?fit=max\&auto=format\&n=4ZXF1PrDkEaJvXpn\&q=85\&s=0fb595490a2f9e15c0301e771a57446c)
7563 
76 <Step title="Configure the Filesystem Server">
77 Replace the contents of the configuration file with the following JSON structure. This configuration tells Claude Desktop to start the Filesystem Server with access to specific directories:
64This action creates a new configuration file if one doesn't exist, or opens your existing configuration. The file is located at:
7865 
79 <CodeGroup>
80 ```json macOS theme={null}
81 {
82 "mcpServers": {
83 "filesystem": {
84 "command": "npx",
85 "args": [
86 "-y",
87 "@modelcontextprotocol/server-filesystem",
88 "/Users/username/Desktop",
89 "/Users/username/Downloads"
90 ]
91 }
92 }
93 }
94 ```
66* **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
67* **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
9568 
96 ```json Windows theme={null}
97 {
98 "mcpServers": {
99 "filesystem": {
100 "command": "npx",
101 "args": [
102 "-y",
103 "@modelcontextprotocol/server-filesystem",
104 "C:\\Users\\username\\Desktop",
105 "C:\\Users\\username\\Downloads"
106 ]
107 }
108 }
109 }
110 ```
111 </CodeGroup>
69**3. Configure the Filesystem Server**
11270 
113 Replace `username` with your actual computer username. The paths listed in the `args` array specify which directories the Filesystem Server can access. You can modify these paths or add additional directories as needed.
71Replace the contents of the configuration file with the following JSON structure. This configuration tells Claude Desktop to start the Filesystem Server with access to specific directories:
11472 
115 <Tip>
116 **Understanding the Configuration**
73```json macOS theme={null}
74{
75 "mcpServers": {
76 "filesystem": {
77 "command": "npx",
78 "args": [
79 "-y",
80 "@modelcontextprotocol/server-filesystem",
81 "/Users/username/Desktop",
82 "/Users/username/Downloads"
83 ]
84 }
85 }
86}
87```
11788 
118 * `"filesystem"`: A friendly name for the server that appears in Claude Desktop
119 * `"command": "npx"`: Uses Node.js's npx tool to run the server
120 * `"-y"`: Automatically confirms the installation of the server package
121 * `"@modelcontextprotocol/server-filesystem"`: The package name of the Filesystem Server
122 * The remaining arguments: Directories the server is allowed to access
123 </Tip>
89```json Windows theme={null}
90{
91 "mcpServers": {
92 "filesystem": {
93 "command": "npx",
94 "args": [
95 "-y",
96 "@modelcontextprotocol/server-filesystem",
97 "C:\\Users\\username\\Desktop",
98 "C:\\Users\\username\\Downloads"
99 ]
100 }
101 }
102}
103```
124104 
125 <Warning>
126 **Security Consideration**
105Replace `username` with your actual computer username. The paths listed in the `args` array specify which directories the Filesystem Server can access. You can modify these paths or add additional directories as needed.
127106 
128 Only grant access to directories you're comfortable with Claude reading and modifying. The server runs with your user account permissions, so it can perform any file operations you can perform manually.
129 </Warning>
130 </Step>
107> [!TIP]
108>
109> **Understanding the Configuration**
110>
111> * `"filesystem"`: A friendly name for the server that appears in Claude Desktop
112> * `"command": "npx"`: Uses Node.js's npx tool to run the server
113> * `"-y"`: Automatically confirms the installation of the server package
114> * `"@modelcontextprotocol/server-filesystem"`: The package name of the Filesystem Server
115> * The remaining arguments: Directories the server is allowed to access
131116 
132 <Step title="Restart Claude Desktop">
133 After saving the configuration file, completely quit Claude Desktop and restart it. The application needs to restart to load the new configuration and start the MCP server.
117> [!WARNING]
118>
119> **Security Consideration**
120>
121> Only grant access to directories you're comfortable with Claude reading and modifying. The server runs with your user account permissions, so it can perform any file operations you can perform manually.
134122 
135 Upon successful restart, click the "Add files, connectors, and more /" indicator <img src="https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/claude-add-files-connectors-and-more.png?fit=max&auto=format&n=cpXzQjUOzyH0mCNH&q=85&s=53acf21f6807dd5323b70b84b5d98d8a" style={{display: 'inline', margin: 0, height: '1.3em', width: 'auto'}} width="33" height="33" data-path="images/claude-add-files-connectors-and-more.png" /> in the bottom-left corner of the conversation input box:
123**4. Restart Claude Desktop**
136124 
137 <Frame>
138 <img src="https://mintcdn.com/mcp/akpggzunDlIcY2im/images/quickstart-slider.png?fit=max&auto=format&n=akpggzunDlIcY2im&q=85&s=a1ebd4259cff2a7472171885f2edc035" alt="Claude Desktop interface showing MCP server indicator" width="1414" height="410" data-path="images/quickstart-slider.png" />
139 </Frame>
125After saving the configuration file, completely quit Claude Desktop and restart it. The application needs to restart to load the new configuration and start the MCP server.
140126 
141 Click on this indicator, then move the mouse over "Connectors" and click "Manage connectors". Select "filesystem" from the connector list to view the Filesystem Server's available tools:
127Upon successful restart, click the "Add files, connectors, and more /" indicator ![](https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/claude-add-files-connectors-and-more.png?fit=max\&auto=format\&n=cpXzQjUOzyH0mCNH\&q=85\&s=53acf21f6807dd5323b70b84b5d98d8a) in the bottom-left corner of the conversation input box:
142128 
143 <Frame style={{ textAlign: "center" }}>
144 <img src="https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/quickstart-tools.png?fit=max&auto=format&n=cpXzQjUOzyH0mCNH&q=85&s=212a63d76daba170d52db0d2f6f582be" width="400" alt="Available filesystem tools in Claude Desktop" data-path="images/quickstart-tools.png" />
145 </Frame>
129![Claude Desktop interface showing MCP server indicator](https://mintcdn.com/mcp/akpggzunDlIcY2im/images/quickstart-slider.png?fit=max\&auto=format\&n=akpggzunDlIcY2im\&q=85\&s=a1ebd4259cff2a7472171885f2edc035)
146130 
147 If the Filesystem Server doesn't connect, refer to the [Troubleshooting](#troubleshooting) section for debugging steps.
148 </Step>
149</Steps>
131Click on this indicator, then move the mouse over "Connectors" and click "Manage connectors". Select "filesystem" from the connector list to view the Filesystem Server's available tools:
150132 
133![Available filesystem tools in Claude Desktop](https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/quickstart-tools.png?fit=max\&auto=format\&n=cpXzQjUOzyH0mCNH\&q=85\&s=212a63d76daba170d52db0d2f6f582be)
134 
135If the Filesystem Server doesn't connect, refer to the [Troubleshooting](#troubleshooting) section for debugging steps.
136 
151137## Using the Filesystem Server
152138 
153139With the Filesystem Server connected, Claude can now interact with your file system. Try these example requests to explore the capabilities:
from line 148
162148 
163149Before executing any file system operation, Claude will request your approval. This ensures you maintain control over all actions:
164150 
165<Frame style={{ textAlign: "center" }}>
166 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-approve.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=98cc6e9dfe885fbd6e9bfae40601e494" width="500" alt="Claude requesting approval to perform a file operation" data-path="images/quickstart-approve.png" />
167</Frame>
151![Claude requesting approval to perform a file operation](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-approve.png?fit=max\&auto=format\&n=4ZXF1PrDkEaJvXpn\&q=85\&s=98cc6e9dfe885fbd6e9bfae40601e494)
168152 
169153Review each request carefully before approving. You can always deny a request if you're not comfortable with the proposed action.
170154 
from line 156
172156 
173157If you encounter issues setting up or using the Filesystem Server, these solutions address common problems:
174158 
175<AccordionGroup>
176 <Accordion title="Server not showing up in Claude / hammer icon missing">
177 1. Restart Claude Desktop completely
178 2. Check your `claude_desktop_config.json` file syntax
179 3. Make sure the file paths included in `claude_desktop_config.json` are valid and that they are absolute and not relative
180 4. Look at [logs](#getting-logs-from-claude-for-desktop) to see why the server is not connecting
181 5. In your command line, try manually running the server (replacing `username` as you did in `claude_desktop_config.json`) to see if you get any errors:
159> **Server not showing up in Claude / hammer icon missing**
160>
161> 1. Restart Claude Desktop completely
162> 2. Check your `claude_desktop_config.json` file syntax
163> 3. Make sure the file paths included in `claude_desktop_config.json` are valid and that they are absolute and not relative
164> 4. Look at [logs](#getting-logs-from-claude-for-desktop) to see why the server is not connecting
165> 5. In your command line, try manually running the server (replacing `username` as you did in `claude_desktop_config.json`) to see if you get any errors:
166>
167> ```bash macOS/Linux theme={null}
168> npx -y @modelcontextprotocol/server-filesystem /Users/username/Desktop /Users/username/Downloads
169> ```
170>
171> ```powershell Windows theme={null}
172> npx -y @modelcontextprotocol/server-filesystem C:\Users\username\Desktop C:\Users\username\Downloads
173> ```
182174 
183 <CodeGroup>
184 ```bash macOS/Linux theme={null}
185 npx -y @modelcontextprotocol/server-filesystem /Users/username/Desktop /Users/username/Downloads
186 ```
175> **Getting logs from Claude Desktop**
176>
177> Claude.app logging related to MCP is written to log files in:
178>
179> * macOS: `~/Library/Logs/Claude`
180>
181> * Windows: `%APPDATA%\Claude\logs`
182>
183> * `mcp.log` will contain general logging about MCP connections and connection failures.
184>
185> * Files named `mcp-server-SERVERNAME.log` will contain the stderr output from the named server. Stdio servers may use stderr for all their logging, so these files are not limited to errors.
186>
187> You can run the following command to list recent logs and follow along with any new ones (on Windows, it will only show recent logs):
188>
189> ```bash macOS/Linux theme={null}
190> tail -n 20 -f ~/Library/Logs/Claude/mcp*.log
191> ```
192>
193> ```powershell Windows theme={null}
194> type "%APPDATA%\Claude\logs\mcp*.log"
195> ```
187196 
188 ```powershell Windows theme={null}
189 npx -y @modelcontextprotocol/server-filesystem C:\Users\username\Desktop C:\Users\username\Downloads
190 ```
191 </CodeGroup>
192 </Accordion>
197> **Tool calls failing silently**
198>
199> If Claude attempts to use the tools but they fail:
200>
201> 1. Check Claude's logs for errors
202> 2. Verify your server builds and runs without errors
203> 3. Try restarting Claude Desktop
193204 
194 <Accordion title="Getting logs from Claude Desktop">
195 Claude.app logging related to MCP is written to log files in:
205> **None of this is working. What do I do?**
206>
207> Please refer to our [debugging guide](/docs/draft/tools/debugging) for better debugging tools and more detailed guidance.
196208 
197 * macOS: `~/Library/Logs/Claude`
209> **ENOENT error and `$\{APPDATA\}` in paths on Windows**
210>
211> If your configured server fails to load, and you see within its logs an error referring to `${APPDATA}` within a path, you may need to add the expanded value of `%APPDATA%` to your `env` key in `claude_desktop_config.json`:
212>
213> ```json theme={null}
214> {
215> "brave-search": {
216> "command": "npx",
217> "args": ["-y", "@modelcontextprotocol/server-brave-search"],
218> "env": {
219> "APPDATA": "C:\\Users\\user\\AppData\\Roaming\\",
220> "BRAVE_API_KEY": "..."
221> }
222> }
223> }
224> ```
225>
226> With this change in place, launch Claude Desktop once again.
227>
228> > [!WARNING]
229> >
230> > **npm should be installed globally**
231> >
232> > The `npx` command may continue to fail if you have not installed npm globally. If npm is already installed globally, you will find `%APPDATA%\npm` exists on your system. If not, you can install npm globally by running the following command:
233> >
234> > ```bash theme={null}
235> > npm install -g npm
236> > ```
198237 
199 * Windows: `%APPDATA%\Claude\logs`
200 
201 * `mcp.log` will contain general logging about MCP connections and connection failures.
202 
203 * Files named `mcp-server-SERVERNAME.log` will contain the stderr output from the named server. Stdio servers may use stderr for all their logging, so these files are not limited to errors.
204 
205 You can run the following command to list recent logs and follow along with any new ones (on Windows, it will only show recent logs):
206 
207 <CodeGroup>
208 ```bash macOS/Linux theme={null}
209 tail -n 20 -f ~/Library/Logs/Claude/mcp*.log
210 ```
211 
212 ```powershell Windows theme={null}
213 type "%APPDATA%\Claude\logs\mcp*.log"
214 ```
215 </CodeGroup>
216 </Accordion>
217 
218 <Accordion title="Tool calls failing silently">
219 If Claude attempts to use the tools but they fail:
220 
221 1. Check Claude's logs for errors
222 2. Verify your server builds and runs without errors
223 3. Try restarting Claude Desktop
224 </Accordion>
225 
226 <Accordion title="None of this is working. What do I do?">
227 Please refer to our [debugging guide](/docs/draft/tools/debugging) for better debugging tools and more detailed guidance.
228 </Accordion>
229 
230 <Accordion title="ENOENT error and `${APPDATA}` in paths on Windows">
231 If your configured server fails to load, and you see within its logs an error referring to `${APPDATA}` within a path, you may need to add the expanded value of `%APPDATA%` to your `env` key in `claude_desktop_config.json`:
232 
233 ```json theme={null}
234 {
235 "brave-search": {
236 "command": "npx",
237 "args": ["-y", "@modelcontextprotocol/server-brave-search"],
238 "env": {
239 "APPDATA": "C:\\Users\\user\\AppData\\Roaming\\",
240 "BRAVE_API_KEY": "..."
241 }
242 }
243 }
244 ```
245 
246 With this change in place, launch Claude Desktop once again.
247 
248 <Warning>
249 **npm should be installed globally**
250 
251 The `npx` command may continue to fail if you have not installed npm globally. If npm is already installed globally, you will find `%APPDATA%\npm` exists on your system. If not, you can install npm globally by running the following command:
252 
253 ```bash theme={null}
254 npm install -g npm
255 ```
256 </Warning>
257 </Accordion>
258</AccordionGroup>
259 
260238## Next Steps
261239 
262240Now that you've successfully connected Claude Desktop to a local MCP server, explore these options to expand your setup:
263241 
264<CardGroup cols={2}>
265 <Card title="Explore other servers" icon="grid" href="https://github.com/modelcontextprotocol/servers">
266 Browse our collection of official and community-created MCP servers for
267 additional capabilities
268 </Card>
242> **[Explore other servers](https://github.com/modelcontextprotocol/servers)**
243>
244> Browse our collection of official and community-created MCP servers for
245> additional capabilities
269246 
270 <Card title="Build your own server" icon="code" href="/docs/draft/develop/build-server">
271 Create custom MCP servers tailored to your specific workflows and
272 integrations
273 </Card>
247> **[Build your own server](/docs/draft/develop/build-server)**
248>
249> Create custom MCP servers tailored to your specific workflows and
250> integrations
274251 
275 <Card title="Connect to remote servers" icon="cloud" href="/docs/draft/develop/connect-remote-servers">
276 Learn how to connect Claude to remote MCP servers for cloud-based tools and
277 services
278 </Card>
252> **[Connect to remote servers](/docs/draft/develop/connect-remote-servers)**
253>
254> Learn how to connect Claude to remote MCP servers for cloud-based tools and
255> services
279256 
280 <Card title="Understand the protocol" icon="book" href="/docs/draft/learn/architecture">
281 Dive deeper into how MCP works and its architecture
282 </Card>
283</CardGroup>
257> **[Understand the protocol](/docs/draft/learn/architecture)**
258>
259> Dive deeper into how MCP works and its architecture
284260 

docs/draft/develop/connect-remote-servers Changed · +46 / -62 lines

from line 25
2525 
2626The process of connecting Claude to a remote MCP server involves adding a Custom Connector through the [Claude interface](https://claude.ai/). This establishes a secure connection between Claude and your chosen remote server.
2727 
28<Steps>
29 <Step title="Navigate to Connector Settings">
30 Open Claude Desktop or Claude in your browser, then navigate to the settings page:
28**1. Navigate to Connector Settings**
3129 
32 * **Desktop**: Either use the keyboard shortcut `Ctrl+Comma` or click the top-left menu icon <img src="https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/claude-desktop-hamburger-menu-icon.png?fit=max&auto=format&n=cpXzQjUOzyH0mCNH&q=85&s=66e498f668362c79f829a07b5ce863c8" style={{display: 'inline', margin: 0, height: '1.3em', width: 'auto'}} width="33" height="33" data-path="images/claude-desktop-hamburger-menu-icon.png" />, hover over "File", and select "Settings"
33 * **Browser**: Either use the keyboard shortcut `⌘⇧,` (*macOS*) or click on your profile icon, and select "Settings" from the menu
30Open Claude Desktop or Claude in your browser, then navigate to the settings page:
3431 
35 Once you're in the settings page, click "Connectors" in the sidebar. This displays your currently configured connectors and provides options for adding new ones.
36 </Step>
32* **Desktop**: Either use the keyboard shortcut `Ctrl+Comma` or click the top-left menu icon ![](https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/claude-desktop-hamburger-menu-icon.png?fit=max\&auto=format\&n=cpXzQjUOzyH0mCNH\&q=85\&s=66e498f668362c79f829a07b5ce863c8), hover over "File", and select "Settings"
33* **Browser**: Either use the keyboard shortcut `⌘⇧,` (*macOS*) or click on your profile icon, and select "Settings" from the menu
3734 
38 <Step title="Add a Custom Connector">
39 In the Connectors section, click the "Add" button at the top-right of the window, then select "Add custom connector" from the dropdown. This begins the connection process. To follow along, copy/paste the URL below:
35Once you're in the settings page, click "Connectors" in the sidebar. This displays your currently configured connectors and provides options for adding new ones.
4036 
41 ```text Example Remote Server theme={null}
42 https://example-server.modelcontextprotocol.io/mcp
43 ```
37**2. Add a Custom Connector**
4438 
45 <Frame>
46 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-remote/1-add-connector.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=b5ae9b23164875bbaa3aff4c178cdc64" alt="Add custom connector button in Claude settings" width="1038" height="809" data-path="images/quickstart-remote/1-add-connector.png" />
47 </Frame>
39In the Connectors section, click the "Add" button at the top-right of the window, then select "Add custom connector" from the dropdown. This begins the connection process. To follow along, copy/paste the URL below:
4840 
49 A dialog will appear prompting you to enter the remote MCP server URL. This URL should be provided by the server developer or administrator. Enter the complete URL, ensuring it includes the proper protocol (https\://) and any necessary path components.
41```text Example Remote Server theme={null}
42https://example-server.modelcontextprotocol.io/mcp
43```
5044 
51 <Frame>
52 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-remote/2-connect.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=0934f16d8e016cade8e560c8f89d011b" alt="Dialog for entering remote MCP server URL" width="1616" height="282" data-path="images/quickstart-remote/2-connect.png" />
53 </Frame>
45![Add custom connector button in Claude settings](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-remote/1-add-connector.png?fit=max\&auto=format\&n=4ZXF1PrDkEaJvXpn\&q=85\&s=b5ae9b23164875bbaa3aff4c178cdc64)
5446 
55 After entering the URL, click "Add" to proceed with the connection.
56 </Step>
47A dialog will appear prompting you to enter the remote MCP server URL. This URL should be provided by the server developer or administrator. Enter the complete URL, ensuring it includes the proper protocol (https\://) and any necessary path components.
5748 
58 <Step title="Complete Authentication">
59 Most remote MCP servers require authentication to ensure secure access to their resources. The authentication process varies depending on the server implementation but commonly involves OAuth, API keys, or username/password combinations.
49![Dialog for entering remote MCP server URL](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-remote/2-connect.png?fit=max\&auto=format\&n=4ZXF1PrDkEaJvXpn\&q=85\&s=0934f16d8e016cade8e560c8f89d011b)
6050 
61 <Frame>
62 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-remote/3-auth.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=89af6e1b85718637231388697cc7b015" alt="Authentication screen for remote MCP server" width="490" height="806" data-path="images/quickstart-remote/3-auth.png" />
63 </Frame>
51After entering the URL, click "Add" to proceed with the connection.
6452 
65 Follow the authentication prompts provided by the server. This may redirect you to a third-party authentication provider or display a form within Claude. Once authentication is complete, Claude will establish a secure connection to the remote server.
66 </Step>
53**3. Complete Authentication**
6754 
68 <Step title="Access Resources and Prompts">
69 After successful connection, the remote server’s resources and prompts become available in your Claude conversations. You can access these by clicking the "Add files, connectors, and more /" indicator <img src="https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/claude-add-files-connectors-and-more.png?fit=max&auto=format&n=cpXzQjUOzyH0mCNH&q=85&s=53acf21f6807dd5323b70b84b5d98d8a" style={{display: 'inline', margin: 0, height: '1.3em', width: 'auto'}} width="33" height="33" data-path="images/claude-add-files-connectors-and-more.png" /> in the bottom-left corner of the message input area. Then hover over "Connectors", move the cursor over "Add to Example Remote Server", where hovering displays the attachment menu.
55Most remote MCP servers require authentication to ensure secure access to their resources. The authentication process varies depending on the server implementation but commonly involves OAuth, API keys, or username/password combinations.
7056 
71 <Frame>
72 <img src="https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/quickstart-remote/4-select-resources-menu.png?fit=max&auto=format&n=cpXzQjUOzyH0mCNH&q=85&s=e5fa85174f8acbadbd709bac66f42d5c" alt="Attachment menu showing available resources" width="735" height="378" data-path="images/quickstart-remote/4-select-resources-menu.png" />
73 </Frame>
57![Authentication screen for remote MCP server](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-remote/3-auth.png?fit=max\&auto=format\&n=4ZXF1PrDkEaJvXpn\&q=85\&s=89af6e1b85718637231388697cc7b015)
7458 
75 The menu displays all available resources and prompts from your connected server. Select the items you want to include in your conversation. These resources provide Claude with context and information from your external tools.
59Follow the authentication prompts provided by the server. This may redirect you to a third-party authentication provider or display a form within Claude. Once authentication is complete, Claude will establish a secure connection to the remote server.
7660 
77 <Frame>
78 <img src="https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/quickstart-remote/5-select-prompts-resources.png?fit=max&auto=format&n=cpXzQjUOzyH0mCNH&q=85&s=68722669d9e18252756885c703e4f221" alt="Selecting specific resources and prompts from the menu" width="648" height="529" data-path="images/quickstart-remote/5-select-prompts-resources.png" />
79 </Frame>
80 </Step>
61**4. Access Resources and Prompts**
8162 
82 <Step title="Configure Tool Permissions">
83 Remote MCP servers often expose multiple tools with varying capabilities. You can control which tools Claude is allowed to use by configuring permissions in the connector settings. This ensures Claude only performs actions you've explicitly authorized.
63After successful connection, the remote server’s resources and prompts become available in your Claude conversations. You can access these by clicking the "Add files, connectors, and more /" indicator ![](https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/claude-add-files-connectors-and-more.png?fit=max\&auto=format\&n=cpXzQjUOzyH0mCNH\&q=85\&s=53acf21f6807dd5323b70b84b5d98d8a) in the bottom-left corner of the message input area. Then hover over "Connectors", move the cursor over "Add to Example Remote Server", where hovering displays the attachment menu.
8464 
85 <Frame>
86 <img src="https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/quickstart-remote/6-configure-tools.png?fit=max&auto=format&n=cpXzQjUOzyH0mCNH&q=85&s=5cfd8b2c5d06e7e3699eac24c68d090e" alt="Tool permission configuration interface" width="604" height="745" data-path="images/quickstart-remote/6-configure-tools.png" />
87 </Frame>
65![Attachment menu showing available resources](https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/quickstart-remote/4-select-resources-menu.png?fit=max\&auto=format\&n=cpXzQjUOzyH0mCNH\&q=85\&s=e5fa85174f8acbadbd709bac66f42d5c)
8866 
89 Navigate back to the Connectors settings and click on your connected server. Here you can enable or disable specific tools, set usage limits, and configure other security parameters according to your needs.
90 </Step>
91</Steps>
67The menu displays all available resources and prompts from your connected server. Select the items you want to include in your conversation. These resources provide Claude with context and information from your external tools.
9268 
69![Selecting specific resources and prompts from the menu](https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/quickstart-remote/5-select-prompts-resources.png?fit=max\&auto=format\&n=cpXzQjUOzyH0mCNH\&q=85\&s=68722669d9e18252756885c703e4f221)
70 
71**5. Configure Tool Permissions**
72 
73Remote MCP servers often expose multiple tools with varying capabilities. You can control which tools Claude is allowed to use by configuring permissions in the connector settings. This ensures Claude only performs actions you've explicitly authorized.
74 
75![Tool permission configuration interface](https://mintcdn.com/mcp/cpXzQjUOzyH0mCNH/images/quickstart-remote/6-configure-tools.png?fit=max\&auto=format\&n=cpXzQjUOzyH0mCNH\&q=85\&s=5cfd8b2c5d06e7e3699eac24c68d090e)
76 
77Navigate back to the Connectors settings and click on your connected server. Here you can enable or disable specific tools, set usage limits, and configure other security parameters according to your needs.
78 
9379## Best Practices for Using Remote MCP Servers
9480 
9581When working with remote MCP servers, consider these recommendations to ensure a secure and efficient experience:
from line 88
10288 
10389Now that you've connected Claude to a remote MCP server, you can explore its capabilities in your conversations. Try using the connected tools to automate tasks, access external data, or integrate with your existing workflows.
10490 
105<CardGroup cols={2}>
106 <Card title="Build your own remote server" icon="cloud" href="/docs/draft/develop/build-server">
107 Create custom remote MCP servers to integrate with proprietary tools and
108 services
109 </Card>
91> **[Build your own remote server](/docs/draft/develop/build-server)**
92>
93> Create custom remote MCP servers to integrate with proprietary tools and
94> services
11095 
111 <Card title="Explore available servers" icon="grid" href="https://github.com/modelcontextprotocol/servers">
112 Browse our collection of official and community-created MCP servers
113 </Card>
96> **[Explore available servers](https://github.com/modelcontextprotocol/servers)**
97>
98> Browse our collection of official and community-created MCP servers
11499 
115 <Card title="Connect local servers" icon="computer" href="/docs/draft/develop/connect-local-servers">
116 Learn how to connect Claude Desktop to local MCP servers for direct system
117 access
118 </Card>
100> **[Connect local servers](/docs/draft/develop/connect-local-servers)**
101>
102> Learn how to connect Claude Desktop to local MCP servers for direct system
103> access
119104 
120 <Card title="Understand the architecture" icon="book" href="/docs/draft/learn/architecture">
121 Dive deeper into how MCP works and its architecture
122 </Card>
123</CardGroup>
105> **[Understand the architecture](/docs/draft/learn/architecture)**
106>
107> Dive deeper into how MCP works and its architecture
124108 
125109Remote MCP servers unlock powerful possibilities for extending Claude's capabilities. As you become familiar with these integrations, you'll discover new ways to streamline your workflows and accomplish complex tasks more efficiently.
126110 

docs/draft/getting-started/intro Changed · +19 / -27 lines

from line 6
66 
77Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect electronic devices, MCP provides a standardized way to connect AI applications to external systems.
88 
9<Frame>
10 <img src="https://mintcdn.com/mcp/bEUxYpZqie0DsluH/images/mcp-simple-diagram.png?fit=max&auto=format&n=bEUxYpZqie0DsluH&q=85&s=35268aa0ad50b8c385913810e7604550" width="3840" height="1500" data-path="images/mcp-simple-diagram.png" />
11</Frame>
9![](https://mintcdn.com/mcp/bEUxYpZqie0DsluH/images/mcp-simple-diagram.png?fit=max\&auto=format\&n=bEUxYpZqie0DsluH\&q=85\&s=35268aa0ad50b8c385913810e7604550)
1210 
1311## What can MCP enable?
1412 
from line 29
3129 
3230## Start Building
3331 
34<CardGroup cols={2}>
35 <Card title="Build servers" icon="server" href="/docs/draft/develop/build-server">
36 Create MCP servers to expose your data and tools
37 </Card>
32> **[Build servers](/docs/draft/develop/build-server)**
33>
34> Create MCP servers to expose your data and tools
3835 
39 <Card title="Build clients" icon="computer" href="/docs/draft/develop/build-client">
40 Develop applications that connect to MCP servers
41 </Card>
36> **[Build clients](/docs/draft/develop/build-client)**
37>
38> Develop applications that connect to MCP servers
4239 
43 <Card title="Build MCP Apps" icon="puzzle-piece" href="/extensions/apps/overview">
44 Build interactive apps that run inside AI clients
45 </Card>
46</CardGroup>
40> **[Build MCP Apps](/extensions/apps/overview)**
41>
42> Build interactive apps that run inside AI clients
4743 
4844## Learn more
4945 
50<CardGroup cols={2}>
51 <Card title="Architecture" icon="book" href="/docs/draft/learn/architecture">
52 Learn the core concepts and architecture of MCP
53 </Card>
46> **[Architecture](/docs/draft/learn/architecture)**
47>
48> Learn the core concepts and architecture of MCP
5449 
55 <Card title="Security" icon="lock" href="/docs/draft/tutorials/security/security_best_practices">
56 Understand the security considerations and best practices for MCP
57 </Card>
58</CardGroup>
50> **[Security](/docs/draft/tutorials/security/security_best_practices)**
51>
52> Understand the security considerations and best practices for MCP
5953 
6054## Community
6155 
62<CardGroup cols={2}>
63 <Card title="Contributing" icon="users" href="/community/contributing">
64 Learn how to get involved and contribute to MCP
65 </Card>
66</CardGroup>
56> **[Contributing](/community/contributing)**
57>
58> Learn how to get involved and contribute to MCP
6759 
Feedback