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

26-50 of 306, page 2 of 13

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

docs/2024-11-05/tools/inspector Changed · +36 / -44 lines

from line 22
2222 
2323A common way to start server packages from [npm](https://npmjs.com) or [PyPI](https://pypi.org).
2424 
25<Tabs>
26 <Tab title="npm package">
27 ```bash theme={null}
28 npx -y @modelcontextprotocol/inspector npx <package-name> <args>
29 # For example
30 npx -y @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem /Users/username/Desktop
31 ```
32 </Tab>
25**npm package**
3326 
34 <Tab title="PyPI package">
35 ```bash theme={null}
36 npx @modelcontextprotocol/inspector uvx <package-name> <args>
37 # For example
38 npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git
39 ```
40 </Tab>
41</Tabs>
27```bash theme={null}
28npx -y @modelcontextprotocol/inspector npx <package-name> <args>
29# For example
30npx -y @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem /Users/username/Desktop
31```
4232 
33**PyPI package**
34 
35```bash theme={null}
36npx @modelcontextprotocol/inspector uvx <package-name> <args>
37# For example
38npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git
39```
40 
4341#### Inspecting locally developed servers
4442 
4543To inspect servers locally developed or downloaded as a repository, the most common
4644way is:
4745 
48<Tabs>
49 <Tab title="TypeScript">
50 ```bash theme={null}
51 npx @modelcontextprotocol/inspector node path/to/server/index.js args...
52 ```
53 </Tab>
46**TypeScript**
5447 
55 <Tab title="Python">
56 ```bash theme={null}
57 npx @modelcontextprotocol/inspector \
58 uv \
59 --directory path/to/server \
60 run \
61 package-name \
62 args...
63 ```
64 </Tab>
65</Tabs>
48```bash theme={null}
49npx @modelcontextprotocol/inspector node path/to/server/index.js args...
50```
6651 
52**Python**
53 
54```bash theme={null}
55npx @modelcontextprotocol/inspector \
56 uv \
57 --directory path/to/server \
58 run \
59 package-name \
60 args...
61```
62 
6763Please carefully read any attached README for the most accurate instructions.
6864 
6965## Feature overview
7066 
71<Frame caption="The MCP Inspector interface">
72 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/mcp-inspector.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=83b12e2a457c96ef4ad17c7357236290" width="2888" height="1761" data-path="images/mcp-inspector.png" />
73</Frame>
67![](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/mcp-inspector.png?fit=max\&auto=format\&n=4ZXF1PrDkEaJvXpn\&q=85\&s=83b12e2a457c96ef4ad17c7357236290)
7468 
7569The Inspector provides several features for interacting with your MCP server:
7670 
from line 123
129123 
130124## Next steps
131125 
132<CardGroup cols={2}>
133 <Card title="Inspector Repository" icon="github" href="https://github.com/modelcontextprotocol/inspector">
134 Check out the MCP Inspector source code
135 </Card>
126> **[Inspector Repository](https://github.com/modelcontextprotocol/inspector)**
127>
128> Check out the MCP Inspector source code
136129 
137 <Card title="Debugging Guide" icon="bug" href="/docs/2024-11-05/tools/debugging">
138 Learn about broader debugging strategies
139 </Card>
140</CardGroup>
130> **[Debugging Guide](/docs/2024-11-05/tools/debugging)**
131>
132> Learn about broader debugging strategies
141133 

docs/2024-11-05/tutorials/security/authorization Changed · +841 / -861 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/2024-11-05/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/2024-11-05/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}/`,
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;
328 console.log(
329 `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
317330 );
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 };
331 });
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");
333370 }
334371 
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 };
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);
346379 }
347380 
348 const app = express();
349 
350 app.use(
351 express.json({
352 verify: (req: any, _res, buf) => {
353 req.rawBody = buf?.toString() ?? "";
381 let response: Response;
382 try {
383 response = await fetch(endpoint, {
384 method: "POST",
385 headers: {
386 "Content-Type": "application/x-www-form-urlencoded",
354387 },
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 checkResourceAllowed({
431 requestedResource: a,
432 configuredResource: mcpServerUrl,
355433 }),
356434 );
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"],
435 if (!allowed) {
436 throw new Error(
437 `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
438 );
439 }
440 
441 return {
442 token,
443 clientId: data.client_id,
444 scopes: data.scope ? data.scope.split(" ") : [],
445 expiresAt: data.exp,
373446 };
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");
447 },
448};
449app.use(
450 mcpAuthMetadataRouter({
451 oauthMetadata,
452 resourceServerUrl: mcpServerUrl,
453 scopesSupported: ["mcp:tools"],
454 resourceName: "MCP Demo Server",
455 }),
456);
457 
458const authMiddleware = requireBearerAuth({
459 verifier: tokenVerifier,
460 requiredScopes: [],
461 resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
462});
463 
464const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
465 
466function createMcpServer() {
467 const server = new McpServer({
468 name: "example-server",
469 version: "1.0.0",
470 });
471 
472 server.registerTool(
473 "add",
474 {
475 title: "Addition Tool",
476 description: "Add two numbers together",
477 inputSchema: {
478 a: z.number().describe("First number to add"),
479 b: z.number().describe("Second number to add"),
480 },
481 },
482 async ({ a, b }) => ({
483 content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
484 }),
485 );
486 
487 server.registerTool(
488 "multiply",
489 {
490 title: "Multiplication Tool",
491 description: "Multiply two numbers together",
492 inputSchema: {
493 x: z.number().describe("First number to multiply"),
494 y: z.number().describe("Second number to multiply"),
495 },
496 },
497 async ({ x, y }) => ({
498 content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
499 }),
500 );
501 
502 return server;
503}
504 
505const mcpPostHandler = async (req: express.Request, res: express.Response) => {
506 const sessionId = req.headers["mcp-session-id"] as string | undefined;
507 let transport: StreamableHTTPServerTransport;
508 
509 if (sessionId && transports[sessionId]) {
510 transport = transports[sessionId];
511 } else if (!sessionId && isInitializeRequest(req.body)) {
512 transport = new StreamableHTTPServerTransport({
513 sessionIdGenerator: () => randomUUID(),
514 onsessioninitialized: (sessionId) => {
515 transports[sessionId] = transport;
516 },
517 });
518 
519 transport.onclose = () => {
520 if (transport.sessionId) {
521 delete transports[transport.sessionId];
522 }
523 };
524 
525 const server = createMcpServer();
526 await server.connect(transport);
527 } else {
528 res.status(400).json({
529 jsonrpc: "2.0",
530 error: {
531 code: -32000,
532 message: "Bad Request: No valid session ID provided",
533 },
534 id: null,
535 });
536 return;
537 }
538 
539 await transport.handleRequest(req, res, req.body);
540};
541 
542const handleSessionRequest = async (
543 req: express.Request,
544 res: express.Response,
545) => {
546 const sessionId = req.headers["mcp-session-id"] as string | undefined;
547 if (!sessionId || !transports[sessionId]) {
548 res.status(400).send("Invalid or missing session ID");
549 return;
550 }
551 
552 const transport = transports[sessionId];
553 await transport.handleRequest(req, res);
554};
555 
556app.post("/", authMiddleware, mcpPostHandler);
557app.get("/", authMiddleware, handleSessionRequest);
558app.delete("/", authMiddleware, handleSessionRequest);
559 
560app.listen(CONFIG.port, CONFIG.host, () => {
561 console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
562 console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
563 console.log(
564 `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
565 );
566});
567```
568 
569When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
570 
571For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
572 
573**Python**
574 
575You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
576 
577To simplify our authorization interaction, in Python scenarios we rely on [FastMCP](https://gofastmcp.com/getting-started/welcome). 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.
578 
579Prior 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:
580 
581```python theme={null}
582"""Configuration settings for the MCP auth server."""
583 
584import os
585from typing import Optional
586 
587 
588class Config:
589 """Configuration class that loads from environment variables with sensible defaults."""
590 
591 # Server settings
592 HOST: str = os.getenv("HOST", "localhost")
593 PORT: int = int(os.getenv("PORT", "3000"))
594 
595 # Auth server settings
596 AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
597 AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
598 AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
599 
600 # OAuth client settings
601 OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "mcp-server")
602 OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "UO3rmozkFFkXr0QxPTkzZ0LMXDidIikB")
603 
604 # Server settings
605 MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
606 OAUTH_STRICT: bool = os.getenv("OAUTH_STRICT", "false").lower() in ("true", "1", "yes")
607 TRANSPORT: str = os.getenv("TRANSPORT", "streamable-http")
608 
609 @property
610 def server_url(self) -> str:
611 """Build the server URL."""
612 return f"http://{self.HOST}:{self.PORT}"
613 
614 @property
615 def auth_base_url(self) -> str:
616 """Build the auth server base URL."""
617 return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
618 
619 def validate(self) -> None:
620 """Validate configuration."""
621 if self.TRANSPORT not in ["sse", "streamable-http"]:
622 raise ValueError(f"Invalid transport: {self.TRANSPORT}. Must be 'sse' or 'streamable-http'")
623 
624 
625# Global configuration instance
626config = Config()
627 
628```
629 
630The server implementation is as follows:
631 
632```python theme={null}
633import datetime
634import logging
635from typing import Any
636 
637from pydantic import AnyHttpUrl
638 
639from mcp.server.auth.settings import AuthSettings
640from mcp.server.fastmcp.server import FastMCP
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 from urllib.parse import urljoin
651 
652 auth_base_url = config.auth_base_url
653 
654 return {
655 "issuer": auth_base_url,
656 "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
657 "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
658 "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
659 }
660 
661 
662def create_server() -> FastMCP:
663 """Create and configure the FastMCP server."""
664 
665 config.validate()
666 
667 oauth_urls = create_oauth_urls()
668 
669 token_verifier = IntrospectionTokenVerifier(
670 introspection_endpoint=oauth_urls["introspection_endpoint"],
671 server_url=config.server_url,
672 client_id=config.OAUTH_CLIENT_ID,
673 client_secret=config.OAUTH_CLIENT_SECRET,
674 )
675 
676 app = FastMCP(
677 name="MCP Resource Server",
678 instructions="Resource Server that validates tokens via Authorization Server introspection",
679 host=config.HOST,
680 port=config.PORT,
681 debug=True,
682 streamable_http_path="/",
683 token_verifier=token_verifier,
684 auth=AuthSettings(
685 issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
686 required_scopes=[config.MCP_SCOPE],
687 resource_server_url=AnyHttpUrl(config.server_url),
688 ),
689 )
690 
691 @app.tool()
692 async def add_numbers(a: float, b: float) -> dict[str, Any]:
693 """
694 Add two numbers together.
695 This tool demonstrates basic arithmetic operations with OAuth authentication.
696 
697 Args:
698 a: The first number to add
699 b: The second number to add
700 """
701 result = a + b
702 return {
703 "operation": "addition",
704 "operand_a": a,
705 "operand_b": b,
706 "result": result,
707 "timestamp": datetime.datetime.now().isoformat()
382708 }
383709 
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);
710 @app.tool()
711 async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
712 """
713 Multiply two numbers together.
714 This tool demonstrates basic arithmetic operations with OAuth authentication.
715 
716 Args:
717 x: The first number to multiply
718 y: The second number to multiply
719 """
720 result = x * y
721 return {
722 "operation": "multiplication",
723 "operand_x": x,
724 "operand_y": y,
725 "result": result,
726 "timestamp": datetime.datetime.now().isoformat()
391727 }
392728 
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;
729 return app
730 
731 
732def main() -> int:
733 """
734 Run the MCP Resource Server.
735 
736 This server:
737 - Provides RFC 9728 Protected Resource Metadata
738 - Validates tokens via Authorization Server introspection
739 - Serves MCP tools requiring authentication
740 
741 Configuration is loaded from config.py and environment variables.
742 """
743 logging.basicConfig(level=logging.INFO)
744 
745 try:
746 config.validate()
747 oauth_urls = create_oauth_urls()
748 
749 except ValueError as e:
750 logger.error("Configuration error: %s", e)
751 return 1
752 
753 try:
754 mcp_server = create_server()
755 
756 logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
757 logger.info("Authorization Server: %s", oauth_urls["issuer"])
758 logger.info("Transport: %s", config.TRANSPORT)
759 
760 mcp_server.run(transport=config.TRANSPORT)
761 return 0
762 
763 except Exception:
764 logger.exception("Server error")
765 return 1
766 
767 
768if __name__ == "__main__":
769 exit(main())
770```
771 
772Lastly, 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
773 
774```python theme={null}
775"""Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
776 
777import logging
778from typing import Any
779 
780from mcp.server.auth.provider import AccessToken, TokenVerifier
781from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
782 
783logger = logging.getLogger(__name__)
784 
785 
786class IntrospectionTokenVerifier(TokenVerifier):
787 """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662).
788 """
789 
790 def __init__(
791 self,
792 introspection_endpoint: str,
793 server_url: str,
794 client_id: str,
795 client_secret: str,
796 ):
797 self.introspection_endpoint = introspection_endpoint
798 self.server_url = server_url
799 self.client_id = client_id
800 self.client_secret = client_secret
801 self.resource_url = resource_url_from_server_url(server_url)
802 
803 async def verify_token(self, token: str) -> AccessToken | None:
804 """Verify token via introspection endpoint."""
805 import httpx
806 
807 if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
808 return None
809 
810 timeout = httpx.Timeout(10.0, connect=5.0)
811 limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)
812 
813 async with httpx.AsyncClient(
814 timeout=timeout,
815 limits=limits,
816 verify=True,
817 ) as client:
818 try:
819 form_data = {
820 "token": token,
821 "client_id": self.client_id,
822 "client_secret": self.client_secret,
823 }
824 headers = {"Content-Type": "application/x-www-form-urlencoded"}
825 
826 response = await client.post(
827 self.introspection_endpoint,
828 data=form_data,
829 headers=headers,
830 )
831 
832 if response.status_code != 200:
833 return None
834 
835 data = response.json()
836 if not data.get("active", False):
837 return None
838 
839 if not self._validate_resource(data):
840 return None
841 
842 return AccessToken(
843 token=token,
844 client_id=data.get("client_id", "unknown"),
845 scopes=data.get("scope", "").split() if data.get("scope") else [],
846 expires_at=data.get("exp"),
847 resource=data.get("aud"), # Include resource in token
848 )
849 
850 except Exception as e:
851 return None
852 
853 def _validate_resource(self, token_data: dict[str, Any]) -> bool:
854 """Validate token was issued for this resource server.
855 
856 Rules:
857 - Reject if 'aud' missing.
858 - Accept if any audience entry matches the derived resource URL.
859 - Supports string or list forms per JWT spec.
860 """
861 if not self.server_url or not self.resource_url:
862 return False
863 
864 aud: list[str] | str | None = token_data.get("aud")
865 if isinstance(aud, list):
866 return any(self._is_valid_resource(a) for a in aud)
867 if isinstance(aud, str):
868 return self._is_valid_resource(aud)
869 return False
870 
871 def _is_valid_resource(self, resource: str) -> bool:
872 """Check if the given resource matches our server."""
873 return check_resource_allowed(self.resource_url, resource)
874```
875 
876For more details, see the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
877 
878**C#**
879 
880You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
881 
882To 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.
883 
884```csharp theme={null}
885using Microsoft.AspNetCore.Authentication.JwtBearer;
886using Microsoft.IdentityModel.Tokens;
887using ModelContextProtocol.AspNetCore.Authentication;
888using ProtectedMcpServer.Tools;
889using System.Security.Claims;
890 
891var builder = WebApplication.CreateBuilder(args);
892 
893var serverUrl = "http://localhost:3000/";
894var authorizationServerUrl = "http://localhost:8080/realms/master/";
895 
896builder.Services.AddAuthentication(options =>
897{
898 options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
899 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
900})
901.AddJwtBearer(options =>
902{
903 options.Authority = authorizationServerUrl;
904 var normalizedServerAudience = serverUrl.TrimEnd('/');
905 options.TokenValidationParameters = new TokenValidationParameters
906 {
907 ValidIssuer = authorizationServerUrl,
908 ValidAudiences = new[] { normalizedServerAudience, serverUrl },
909 AudienceValidator = (audiences, securityToken, validationParameters) =>
910 {
911 if (audiences == null) return false;
912 foreach (var aud in audiences)
913 {
914 if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
915 {
916 return true;
917 }
918 }
919 return false;
405920 }
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 checkResourceAllowed({
443 requestedResource: a,
444 configuredResource: mcpServerUrl,
445 }),
446 );
447 if (!allowed) {
448 throw new Error(
449 `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
450 );
451 }
452 
453 return {
454 token,
455 clientId: data.client_id,
456 scopes: data.scope ? data.scope.split(" ") : [],
457 expiresAt: data.exp,
458 };
459 },
460921 };
461 app.use(
462 mcpAuthMetadataRouter({
463 oauthMetadata,
464 resourceServerUrl: mcpServerUrl,
465 scopesSupported: ["mcp:tools"],
466 resourceName: "MCP Demo Server",
467 }),
468 );
469 
470 const authMiddleware = requireBearerAuth({
471 verifier: tokenVerifier,
472 requiredScopes: [],
473 resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
474 });
475 
476 const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
477 
478 function createMcpServer() {
479 const server = new McpServer({
480 name: "example-server",
481 version: "1.0.0",
482 });
483 
484 server.registerTool(
485 "add",
922 
923 options.RequireHttpsMetadata = false; // Set to true in production
924 
925 options.Events = new JwtBearerEvents
926 {
927 OnTokenValidated = context =>
486928 {
487 title: "Addition Tool",
488 description: "Add two numbers together",
489 inputSchema: {
490 a: z.number().describe("First number to add"),
491 b: z.number().describe("Second number to add"),
492 },
929 var name = context.Principal?.Identity?.Name ?? "unknown";
930 var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
931 Console.WriteLine($"Token validated for: {name} ({email})");
932 return Task.CompletedTask;
493933 },
494 async ({ a, b }) => ({
495 content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
496 }),
497 );
498 
499 server.registerTool(
500 "multiply",
934 OnAuthenticationFailed = context =>
501935 {
502 title: "Multiplication Tool",
503 description: "Multiply two numbers together",
504 inputSchema: {
505 x: z.number().describe("First number to multiply"),
506 y: z.number().describe("Second number to multiply"),
507 },
936 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
937 return Task.CompletedTask;
508938 },
509 async ({ x, y }) => ({
510 content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
511 }),
512 );
513 
514 return server;
515 }
516 
517 const mcpPostHandler = async (req: express.Request, res: express.Response) => {
518 const sessionId = req.headers["mcp-session-id"] as string | undefined;
519 let transport: StreamableHTTPServerTransport;
520 
521 if (sessionId && transports[sessionId]) {
522 transport = transports[sessionId];
523 } else if (!sessionId && isInitializeRequest(req.body)) {
524 transport = new StreamableHTTPServerTransport({
525 sessionIdGenerator: () => randomUUID(),
526 onsessioninitialized: (sessionId) => {
527 transports[sessionId] = transport;
528 },
529 });
530 
531 transport.onclose = () => {
532 if (transport.sessionId) {
533 delete transports[transport.sessionId];
534 }
535 };
536 
537 const server = createMcpServer();
538 await server.connect(transport);
539 } else {
540 res.status(400).json({
541 jsonrpc: "2.0",
542 error: {
543 code: -32000,
544 message: "Bad Request: No valid session ID provided",
545 },
546 id: null,
547 });
548 return;
549 }
550 
551 await transport.handleRequest(req, res, req.body);
552939 };
553 
554 const handleSessionRequest = async (
555 req: express.Request,
556 res: express.Response,
557 ) => {
558 const sessionId = req.headers["mcp-session-id"] as string | undefined;
559 if (!sessionId || !transports[sessionId]) {
560 res.status(400).send("Invalid or missing session ID");
561 return;
562 }
563 
564 const transport = transports[sessionId];
565 await transport.handleRequest(req, res);
940})
941.AddMcp(options =>
942{
943 options.ResourceMetadata = new()
944 {
945 Resource = new Uri(serverUrl),
946 ResourceDocumentation = new Uri("https://docs.example.com/api/math"),
947 AuthorizationServers = { new Uri(authorizationServerUrl) },
948 ScopesSupported = ["mcp:tools"]
566949 };
567 
568 app.post("/", authMiddleware, mcpPostHandler);
569 app.get("/", authMiddleware, handleSessionRequest);
570 app.delete("/", authMiddleware, handleSessionRequest);
571 
572 app.listen(CONFIG.port, CONFIG.host, () => {
573 console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
574 console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
575 console.log(
576 `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
577 );
578 });
579 ```
580 
581 When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
582 
583 For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
584 </Tab>
585 
586 <Tab title="Python">
587 You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
588 
589 To simplify our authorization interaction, in Python scenarios we rely on [FastMCP](https://gofastmcp.com/getting-started/welcome). 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.
590 
591 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:
592 
593 ```python theme={null}
594 """Configuration settings for the MCP auth server."""
595 
596 import os
597 from typing import Optional
598 
599 
600 class Config:
601 """Configuration class that loads from environment variables with sensible defaults."""
602 
603 # Server settings
604 HOST: str = os.getenv("HOST", "localhost")
605 PORT: int = int(os.getenv("PORT", "3000"))
606 
607 # Auth server settings
608 AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
609 AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
610 AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
611 
612 # OAuth client settings
613 OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "mcp-server")
614 OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "UO3rmozkFFkXr0QxPTkzZ0LMXDidIikB")
615 
616 # Server settings
617 MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
618 OAUTH_STRICT: bool = os.getenv("OAUTH_STRICT", "false").lower() in ("true", "1", "yes")
619 TRANSPORT: str = os.getenv("TRANSPORT", "streamable-http")
620 
621 @property
622 def server_url(self) -> str:
623 """Build the server URL."""
624 return f"http://{self.HOST}:{self.PORT}"
625 
626 @property
627 def auth_base_url(self) -> str:
628 """Build the auth server base URL."""
629 return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
630 
631 def validate(self) -> None:
632 """Validate configuration."""
633 if self.TRANSPORT not in ["sse", "streamable-http"]:
634 raise ValueError(f"Invalid transport: {self.TRANSPORT}. Must be 'sse' or 'streamable-http'")
635 
636 
637 # Global configuration instance
638 config = Config()
639 
640 ```
641 
642 The server implementation is as follows:
643 
644 ```python theme={null}
645 import datetime
646 import logging
647 from typing import Any
648 
649 from pydantic import AnyHttpUrl
650 
651 from mcp.server.auth.settings import AuthSettings
652 from mcp.server.fastmcp.server import FastMCP
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 from urllib.parse import urljoin
663 
664 auth_base_url = config.auth_base_url
665 
666 return {
667 "issuer": auth_base_url,
668 "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
669 "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
670 "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
671 }
672 
673 
674 def create_server() -> FastMCP:
675 """Create and configure the FastMCP server."""
676 
677 config.validate()
678 
679 oauth_urls = create_oauth_urls()
680 
681 token_verifier = IntrospectionTokenVerifier(
682 introspection_endpoint=oauth_urls["introspection_endpoint"],
683 server_url=config.server_url,
684 client_id=config.OAUTH_CLIENT_ID,
685 client_secret=config.OAUTH_CLIENT_SECRET,
686 )
687 
688 app = FastMCP(
689 name="MCP Resource Server",
690 instructions="Resource Server that validates tokens via Authorization Server introspection",
691 host=config.HOST,
692 port=config.PORT,
693 debug=True,
694 streamable_http_path="/",
695 token_verifier=token_verifier,
696 auth=AuthSettings(
697 issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
698 required_scopes=[config.MCP_SCOPE],
699 resource_server_url=AnyHttpUrl(config.server_url),
700 ),
701 )
702 
703 @app.tool()
704 async def add_numbers(a: float, b: float) -> dict[str, Any]:
705 """
706 Add two numbers together.
707 This tool demonstrates basic arithmetic operations with OAuth authentication.
708 
709 Args:
710 a: The first number to add
711 b: The second number to add
712 """
713 result = a + b
714 return {
715 "operation": "addition",
716 "operand_a": a,
717 "operand_b": b,
718 "result": result,
719 "timestamp": datetime.datetime.now().isoformat()
720 }
721 
722 @app.tool()
723 async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
724 """
725 Multiply two numbers together.
726 This tool demonstrates basic arithmetic operations with OAuth authentication.
727 
728 Args:
729 x: The first number to multiply
730 y: The second number to multiply
731 """
732 result = x * y
733 return {
734 "operation": "multiplication",
735 "operand_x": x,
736 "operand_y": y,
737 "result": result,
738 "timestamp": datetime.datetime.now().isoformat()
739 }
740 
741 return app
742 
743 
744 def main() -> int:
745 """
746 Run the MCP Resource Server.
747 
748 This server:
749 - Provides RFC 9728 Protected Resource Metadata
750 - Validates tokens via Authorization Server introspection
751 - Serves MCP tools requiring authentication
752 
753 Configuration is loaded from config.py and environment variables.
754 """
755 logging.basicConfig(level=logging.INFO)
756 
757 try:
758 config.validate()
759 oauth_urls = create_oauth_urls()
760 
761 except ValueError as e:
762 logger.error("Configuration error: %s", e)
763 return 1
764 
765 try:
766 mcp_server = create_server()
767 
768 logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
769 logger.info("Authorization Server: %s", oauth_urls["issuer"])
770 logger.info("Transport: %s", config.TRANSPORT)
771 
772 mcp_server.run(transport=config.TRANSPORT)
773 return 0
774 
775 except Exception:
776 logger.exception("Server error")
777 return 1
778 
779 
780 if __name__ == "__main__":
781 exit(main())
782 ```
783 
784 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
785 
786 ```python theme={null}
787 """Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
788 
789 import logging
790 from typing import Any
791 
792 from mcp.server.auth.provider import AccessToken, TokenVerifier
793 from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
794 
795 logger = logging.getLogger(__name__)
796 
797 
798 class IntrospectionTokenVerifier(TokenVerifier):
799 """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662).
800 """
801 
802 def __init__(
803 self,
804 introspection_endpoint: str,
805 server_url: str,
806 client_id: str,
807 client_secret: str,
808 ):
809 self.introspection_endpoint = introspection_endpoint
810 self.server_url = server_url
811 self.client_id = client_id
812 self.client_secret = client_secret
813 self.resource_url = resource_url_from_server_url(server_url)
814 
815 async def verify_token(self, token: str) -> AccessToken | None:
816 """Verify token via introspection endpoint."""
817 import httpx
818 
819 if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
820 return None
821 
822 timeout = httpx.Timeout(10.0, connect=5.0)
823 limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)
824 
825 async with httpx.AsyncClient(
826 timeout=timeout,
827 limits=limits,
828 verify=True,
829 ) as client:
830 try:
831 form_data = {
832 "token": token,
833 "client_id": self.client_id,
834 "client_secret": self.client_secret,
835 }
836 headers = {"Content-Type": "application/x-www-form-urlencoded"}
837 
838 response = await client.post(
839 self.introspection_endpoint,
840 data=form_data,
841 headers=headers,
842 )
843 
844 if response.status_code != 200:
845 return None
846 
847 data = response.json()
848 if not data.get("active", False):
849 return None
850 
851 if not self._validate_resource(data):
852 return None
853 
854 return AccessToken(
855 token=token,
856 client_id=data.get("client_id", "unknown"),
857 scopes=data.get("scope", "").split() if data.get("scope") else [],
858 expires_at=data.get("exp"),
859 resource=data.get("aud"), # Include resource in token
860 )
861 
862 except Exception as e:
863 return None
864 
865 def _validate_resource(self, token_data: dict[str, Any]) -> bool:
866 """Validate token was issued for this resource server.
867 
868 Rules:
869 - Reject if 'aud' missing.
870 - Accept if any audience entry matches the derived resource URL.
871 - Supports string or list forms per JWT spec.
872 """
873 if not self.server_url or not self.resource_url:
874 return False
875 
876 aud: list[str] | str | None = token_data.get("aud")
877 if isinstance(aud, list):
878 return any(self._is_valid_resource(a) for a in aud)
879 if isinstance(aud, str):
880 return self._is_valid_resource(aud)
881 return False
882 
883 def _is_valid_resource(self, resource: str) -> bool:
884 """Check if the given resource matches our server."""
885 return check_resource_allowed(self.resource_url, resource)
886 ```
887 
888 For more details, see the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
889 </Tab>
890 
891 <Tab title="C#">
892 You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
893 
894 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.
895 
896 ```csharp theme={null}
897 using Microsoft.AspNetCore.Authentication.JwtBearer;
898 using Microsoft.IdentityModel.Tokens;
899 using ModelContextProtocol.AspNetCore.Authentication;
900 using ProtectedMcpServer.Tools;
901 using System.Security.Claims;
902 
903 var builder = WebApplication.CreateBuilder(args);
904 
905 var serverUrl = "http://localhost:3000/";
906 var authorizationServerUrl = "http://localhost:8080/realms/master/";
907 
908 builder.Services.AddAuthentication(options =>
909 {
910 options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
911 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
912 })
913 .AddJwtBearer(options =>
914 {
915 options.Authority = authorizationServerUrl;
916 var normalizedServerAudience = serverUrl.TrimEnd('/');
917 options.TokenValidationParameters = new TokenValidationParameters
918 {
919 ValidIssuer = authorizationServerUrl,
920 ValidAudiences = new[] { normalizedServerAudience, serverUrl },
921 AudienceValidator = (audiences, securityToken, validationParameters) =>
922 {
923 if (audiences == null) return false;
924 foreach (var aud in audiences)
925 {
926 if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
927 {
928 return true;
929 }
930 }
931 return false;
932 }
933 };
934 
935 options.RequireHttpsMetadata = false; // Set to true in production
936 
937 options.Events = new JwtBearerEvents
938 {
939 OnTokenValidated = context =>
940 {
941 var name = context.Principal?.Identity?.Name ?? "unknown";
942 var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
943 Console.WriteLine($"Token validated for: {name} ({email})");
944 return Task.CompletedTask;
945 },
946 OnAuthenticationFailed = context =>
947 {
948 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
949 return Task.CompletedTask;
950 },
951 };
952 })
953 .AddMcp(options =>
954 {
955 options.ResourceMetadata = new()
956 {
957 Resource = new Uri(serverUrl),
958 ResourceDocumentation = new Uri("https://docs.example.com/api/math"),
959 AuthorizationServers = { new Uri(authorizationServerUrl) },
960 ScopesSupported = ["mcp:tools"]
961 };
962 });
963 
964 builder.Services.AddAuthorization();
965 
966 builder.Services.AddHttpContextAccessor();
967 builder.Services.AddMcpServer()
968 .WithTools<MathTools>()
969 .WithHttpTransport();
970 
971 var app = builder.Build();
972 
973 app.UseAuthentication();
974 app.UseAuthorization();
975 
976 app.MapMcp().RequireAuthorization();
977 
978 Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
979 Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
980 Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
981 Console.WriteLine("Exposed Math tools: Add, Multiply");
982 Console.WriteLine("Press Ctrl+C to stop the server");
983 
984 app.Run(serverUrl);
985 ```
986 
987 For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
988 </Tab>
989</Tabs>
950});
951 
952builder.Services.AddAuthorization();
953 
954builder.Services.AddHttpContextAccessor();
955builder.Services.AddMcpServer()
956 .WithTools<MathTools>()
957 .WithHttpTransport();
958 
959var app = builder.Build();
960 
961app.UseAuthentication();
962app.UseAuthorization();
963 
964app.MapMcp().RequireAuthorization();
965 
966Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
967Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
968Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
969Console.WriteLine("Exposed Math tools: Add, Multiply");
970Console.WriteLine("Press Ctrl+C to stop the server");
971 
972app.Run(serverUrl);
973```
974 
975For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
990976 
991977## Testing the MCP Server
992978 
from line 989
1003989 
1004990On 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.
1005991 
1006<Frame>
1007 <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" />
1008</Frame>
992![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)
1009993 
1010994After consenting, you will see the tools listed right above the server entry in `mcp.json`.
1011995 
1012<Frame>
1013 <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" />
1014</Frame>
996![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)
1015997 
1016998You will be able to invoke individual tools with the help of the `#` sign in the chat view.
1017999 
1018<Frame>
1019 <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" />
1020</Frame>
1000![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)
10211001 
10221002## Common Pitfalls and How to Avoid Them
10231003 
10241004 

docs/2024-11-05/tutorials/security/security_best_practices Changed · +4 / -4 lines

from line 423
423423* Link-local: `169.254.0.0/16` (including cloud metadata endpoints)
424424* Private IPv6 ranges: `fc00::/7`, `fe80::/10`
425425 
426<Note>
427 Avoid implementing IP validation manually. Attackers exploit encoding tricks
428 (octal, hex, IPv4-mapped IPv6) that custom parsers often miss.
429</Note>
426> [!NOTE]
427>
428> Avoid implementing IP validation manually. Attackers exploit encoding tricks
429> (octal, hex, IPv4-mapped IPv6) that custom parsers often miss.
430430 
431431**Validate Redirect Targets**
432432 

docs/2025-03-26/develop/build-client Changed · +2436 / -2452 lines

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

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

docs/2025-03-26/develop/build-with-agent-skills Changed · +9 / -11 lines

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

docs/2025-03-26/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="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 scroll over "Connectors" and click "Manage connectors". Select "filesystem" from the connector list to view the 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 scroll over "Connectors" and click "Manage connectors". Select "filesystem" from the connector list to view the 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 error (stderr) logging from the named server.
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/2025-03-26/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 error (stderr) logging from the named server.
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/2025-03-26/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/2025-03-26/develop/build-server">
271 Create custom MCP servers tailored to your specific workflows and
272 integrations
273 </Card>
247> **[Build your own server](/docs/2025-03-26/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/2025-03-26/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/2025-03-26/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/2025-03-26/learn/architecture">
281 Dive deeper into how MCP works and its architecture
282 </Card>
283</CardGroup>
257> **[Understand the protocol](/docs/2025-03-26/learn/architecture)**
258>
259> Dive deeper into how MCP works and its architecture
284260 

docs/2025-03-26/develop/connect-remote-servers Changed · +41 / -57 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 in your browser and navigate to the settings page. You can access this by clicking on your profile icon and selecting "Settings" from the dropdown menu. Once in settings, locate and click on the "Connectors" section in the sidebar.
28**1. Navigate to Connector Settings**
3129 
32 This will display your currently configured connectors and provide options to add new ones.
33 </Step>
30Open Claude in your browser and navigate to the settings page. You can access this by clicking on your profile icon and selecting "Settings" from the dropdown menu. Once in settings, locate and click on the "Connectors" section in the sidebar.
3431 
35 <Step title="Add a Custom Connector">
36 In the Connectors section, scroll to the bottom where you'll find the "Add custom connector" button. Click this button to begin the connection process.
32This will display your currently configured connectors and provide options to add new ones.
3733 
38 <Frame>
39 <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" />
40 </Frame>
34**2. Add a Custom Connector**
4135 
42 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.
36In the Connectors section, scroll to the bottom where you'll find the "Add custom connector" button. Click this button to begin the connection process.
4337 
44 <Frame>
45 <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" />
46 </Frame>
38![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)
4739 
48 After entering the URL, click "Add" to proceed with the connection.
49 </Step>
40A 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.
5041 
51 <Step title="Complete Authentication">
52 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.
42![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)
5343 
54 <Frame>
55 <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" />
56 </Frame>
44After entering the URL, click "Add" to proceed with the connection.
5745 
58 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.
59 </Step>
46**3. Complete Authentication**
6047 
61 <Step title="Access Resources and Prompts">
62 After successful connection, the remote server's resources and prompts become available in your Claude conversations. You can access these by clicking the paperclip icon in the message input area, which opens the attachment menu.
48Most 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.
6349 
64 <Frame>
65 <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" />
66 </Frame>
50![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)
6751 
68 The menu displays all available resources and prompts from your connected servers. Select the items you want to include in your conversation. These resources provide Claude with context and information from your external tools.
52Follow 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.
6953 
70 <Frame>
71 <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" />
72 </Frame>
73 </Step>
54**4. Access Resources and Prompts**
7455 
75 <Step title="Configure Tool Permissions">
76 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.
56After successful connection, the remote server's resources and prompts become available in your Claude conversations. You can access these by clicking the paperclip icon in the message input area, which opens the attachment menu.
7757 
78 <Frame>
79 <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" />
80 </Frame>
58![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)
8159 
82 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.
83 </Step>
84</Steps>
60The menu displays all available resources and prompts from your connected servers. Select the items you want to include in your conversation. These resources provide Claude with context and information from your external tools.
8561 
62![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)
63 
64**5. Configure Tool Permissions**
65 
66Remote 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.
67 
68![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)
69 
70Navigate 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.
71 
8672## Best Practices for Using Remote MCP Servers
8773 
8874When working with remote MCP servers, consider these recommendations to ensure a secure and efficient experience:
from line 81
9581 
9682Now 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.
9783 
98<CardGroup cols={2}>
99 <Card title="Build your own remote server" icon="cloud" href="/docs/2025-03-26/develop/build-server">
100 Create custom remote MCP servers to integrate with proprietary tools and
101 services
102 </Card>
84> **[Build your own remote server](/docs/2025-03-26/develop/build-server)**
85>
86> Create custom remote MCP servers to integrate with proprietary tools and
87> services
10388 
104 <Card title="Explore available servers" icon="grid" href="https://github.com/modelcontextprotocol/servers">
105 Browse our collection of official and community-created MCP servers
106 </Card>
89> **[Explore available servers](https://github.com/modelcontextprotocol/servers)**
90>
91> Browse our collection of official and community-created MCP servers
10792 
108 <Card title="Connect local servers" icon="computer" href="/docs/2025-03-26/develop/connect-local-servers">
109 Learn how to connect Claude Desktop to local MCP servers for direct system
110 access
111 </Card>
93> **[Connect local servers](/docs/2025-03-26/develop/connect-local-servers)**
94>
95> Learn how to connect Claude Desktop to local MCP servers for direct system
96> access
11297 
113 <Card title="Understand the architecture" icon="book" href="/docs/2025-03-26/learn/architecture">
114 Dive deeper into how MCP works and its architecture
115 </Card>
116</CardGroup>
98> **[Understand the architecture](/docs/2025-03-26/learn/architecture)**
99>
100> Dive deeper into how MCP works and its architecture
117101 
118102Remote 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.
119103 

docs/2025-03-26/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/2025-03-26/develop/build-server">
36 Create MCP servers to expose your data and tools
37 </Card>
32> **[Build servers](/docs/2025-03-26/develop/build-server)**
33>
34> Create MCP servers to expose your data and tools
3835 
39 <Card title="Build clients" icon="computer" href="/docs/2025-03-26/develop/build-client">
40 Develop applications that connect to MCP servers
41 </Card>
36> **[Build clients](/docs/2025-03-26/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/2025-03-26/learn/architecture">
52 Learn the core concepts and architecture of MCP
53 </Card>
46> **[Architecture](/docs/2025-03-26/learn/architecture)**
47>
48> Learn the core concepts and architecture of MCP
5449 
55 <Card title="Security" icon="lock" href="/docs/2025-03-26/tutorials/security/security_best_practices">
56 Understand the security considerations and best practices for MCP
57 </Card>
58</CardGroup>
50> **[Security](/docs/2025-03-26/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/2025-03-26/learn/architecture Changed · +304 / -312 lines

#### Understanding the Initialization 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 #### 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#### Lifecycle management
106106 
107MCP is a <Tooltip tip="A subset of MCP can be made stateless using the Streamable HTTP transport">stateful protocol</Tooltip> that requires lifecycle management. The purpose of lifecycle management is to negotiate the <Tooltip tip="Features and operations that a client or server supports, such as tools, resources, or prompts">capabilities</Tooltip> that both client and server support. Detailed information can be found in the [specification](/specification/2025-03-26/basic/lifecycle), and the [example](#example) showcases the initialization sequence.
107MCP is a stateful protocol (A subset of MCP can be made stateless using the Streamable HTTP transport) that requires lifecycle management. The purpose of lifecycle management is to negotiate the capabilities (Features and operations that a client or server supports, such as tools, resources, or prompts) that both client and server support. Detailed information can be found in the [specification](/specification/2025-03-26/basic/lifecycle), and the [example](#example) showcases the initialization sequence.
108108 
109109#### Primitives
110110 
from line 144
144144 
145145This section provides a step-by-step walkthrough of an MCP client-server interaction, focusing on the data layer protocol. We'll demonstrate the lifecycle sequence, tool operations, and notifications using JSON-RPC 2.0 messages.
146146 
147<Steps>
148 <Step title="Initialization (Lifecycle Management)">
149 MCP begins with lifecycle management through a capability negotiation handshake. As described in the [lifecycle management](#lifecycle-management) section, the client sends an `initialize` request to establish the connection and negotiate supported features.
150 
151 <CodeGroup>
152 ```json Initialize Request theme={null}
147**1. Initialization (Lifecycle Management)**
148 
149MCP begins with lifecycle management through a capability negotiation handshake. As described in the [lifecycle management](#lifecycle-management) section, the client sends an `initialize` request to establish the connection and negotiate supported features.
150 
151```json Initialize Request theme={null}
152{
153 "jsonrpc": "2.0",
154 "id": 1,
155 "method": "initialize",
156 "params": {
157 "protocolVersion": "2025-03-26",
158 "capabilities": {
159 "sampling": {}
160 },
161 "clientInfo": {
162 "name": "example-client",
163 "version": "1.0.0"
164 }
165 }
166}
167```
168 
169```json Initialize Response theme={null}
170{
171 "jsonrpc": "2.0",
172 "id": 1,
173 "result": {
174 "protocolVersion": "2025-03-26",
175 "capabilities": {
176 "tools": {
177 "listChanged": true
178 },
179 "resources": {}
180 },
181 "serverInfo": {
182 "name": "example-server",
183 "version": "1.0.0"
184 }
185 }
186}
187```
188 
189#### Understanding the Initialization Exchange
190 
191The initialization process is a key part of MCP's lifecycle management and serves several critical purposes:
192 
1931. **Protocol Version Negotiation**: The `protocolVersion` field (e.g., "2025-03-26") ensures both client and server are using compatible protocol versions. This prevents communication errors that could occur when different versions attempt to interact. If a mutually compatible version is not negotiated, the connection should be terminated.
194 
1952. **Capability Discovery**: The `capabilities` object allows each party to declare what features they support, including which [primitives](#primitives) they can handle (tools, resources, prompts) and whether they support features like [notifications](#notifications). This enables efficient communication by avoiding unsupported operations.
196 
1973. **Identity Exchange**: The `clientInfo` and `serverInfo` objects provide identification and versioning information for debugging and compatibility purposes.
198 
199In this example, the capability negotiation demonstrates how MCP primitives are declared:
200 
201**Client Capabilities**:
202 
203* `"sampling": {}` - The client declares it can handle server sampling requests (can receive `sampling/createMessage` method calls)
204 
205**Server Capabilities**:
206 
207* `"tools": {"listChanged": true}` - The server supports the tools primitive AND can send `tools/list_changed` notifications when its tool list changes
208* `"resources": {}` - The server also supports the resources primitive (can handle `resources/list` and `resources/read` methods)
209 
210After successful initialization, the client sends a notification to indicate it's ready:
211 
212```json Notification theme={null}
213{
214 "jsonrpc": "2.0",
215 "method": "notifications/initialized"
216}
217```
218 
219#### How This Works in AI Applications
220 
221During initialization, the AI application's MCP client manager establishes connections to configured servers and stores their 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.
222 
223```python Pseudo-code for AI application initialization theme={null}
224# Pseudo Code
225async with stdio_client(server_config) as (read, write):
226 async with ClientSession(read, write) as session:
227 init_response = await session.initialize()
228 if init_response.capabilities.tools:
229 app.register_mcp_server(session, supports_tools=True)
230 app.set_server_ready(session)
231```
232 
233**2. Tool Discovery (Primitives)**
234 
235Now that the connection is established, 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.
236 
237```json Tools List Request theme={null}
238{
239 "jsonrpc": "2.0",
240 "id": 2,
241 "method": "tools/list"
242}
243```
244 
245```json Tools List Response theme={null}
246{
247 "jsonrpc": "2.0",
248 "id": 2,
249 "result": {
250 "tools": [
153251 {
154 "jsonrpc": "2.0",
155 "id": 1,
156 "method": "initialize",
157 "params": {
158 "protocolVersion": "2025-03-26",
159 "capabilities": {
160 "sampling": {}
252 "name": "calculator_arithmetic",
253 "title": "Calculator",
254 "description": "Perform mathematical calculations including basic arithmetic, trigonometric functions, and algebraic operations",
255 "inputSchema": {
256 "type": "object",
257 "properties": {
258 "expression": {
259 "type": "string",
260 "description": "Mathematical expression to evaluate (e.g., '2 + 3 * 4', 'sin(30)', 'sqrt(16)')"
261 }
161262 },
162 "clientInfo": {
163 "name": "example-client",
164 "version": "1.0.0"
165 }
263 "required": ["expression"]
264 }
265 },
266 {
267 "name": "weather_current",
268 "title": "Weather Information",
269 "description": "Get current weather information for any location worldwide",
270 "inputSchema": {
271 "type": "object",
272 "properties": {
273 "location": {
274 "type": "string",
275 "description": "City name, address, or coordinates (latitude,longitude)"
276 },
277 "units": {
278 "type": "string",
279 "enum": ["metric", "imperial", "kelvin"],
280 "description": "Temperature units to use in response",
281 "default": "metric"
282 }
283 },
284 "required": ["location"]
166285 }
167286 }
168 ```
169 
170 ```json Initialize Response theme={null}
287 ]
288 }
289}
290```
291 
292#### Understanding the Tool Discovery Request
293 
294The `tools/list` request is simple, containing no parameters.
295 
296#### Understanding the Tool Discovery Response
297 
298The 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.
299 
300Each tool object in the response includes several key fields:
301 
302* **`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`)
303* **`title`**: A human-readable display name for the tool that clients can show to users
304* **`description`**: Detailed explanation of what the tool does and when to use it
305* **`inputSchema`**: A JSON Schema that defines the expected input parameters, enabling type validation and providing clear documentation about required and optional parameters
306 
307#### How This Works in AI Applications
308 
309The 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.
310 
311```python Pseudo-code for AI application tool discovery theme={null}
312# Pseudo-code using MCP Python SDK patterns
313available_tools = []
314for session in app.mcp_server_sessions():
315 tools_response = await session.list_tools()
316 available_tools.extend(tools_response.tools)
317conversation.register_available_tools(available_tools)
318```
319 
320**3. Tool Execution (Primitives)**
321 
322The 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.
323 
324#### Understanding the Tool Execution Request
325 
326The `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:
327 
328```json Tool Call Request theme={null}
329{
330 "jsonrpc": "2.0",
331 "id": 3,
332 "method": "tools/call",
333 "params": {
334 "name": "weather_current",
335 "arguments": {
336 "location": "San Francisco",
337 "units": "imperial"
338 }
339 }
340}
341```
342 
343```json Tool Call Response theme={null}
344{
345 "jsonrpc": "2.0",
346 "id": 3,
347 "result": {
348 "content": [
171349 {
172 "jsonrpc": "2.0",
173 "id": 1,
174 "result": {
175 "protocolVersion": "2025-03-26",
176 "capabilities": {
177 "tools": {
178 "listChanged": true
179 },
180 "resources": {}
181 },
182 "serverInfo": {
183 "name": "example-server",
184 "version": "1.0.0"
185 }
186 }
350 "type": "text",
351 "text": "Current weather in San Francisco: 68°F, partly cloudy with light winds from the west at 8 mph. Humidity: 65%"
187352 }
188 ```
189 </CodeGroup>
190 
191 #### Understanding the Initialization Exchange
192 
193 The initialization process is a key part of MCP's lifecycle management and serves several critical purposes:
194 
195 1. **Protocol Version Negotiation**: The `protocolVersion` field (e.g., "2025-03-26") ensures both client and server are using compatible protocol versions. This prevents communication errors that could occur when different versions attempt to interact. If a mutually compatible version is not negotiated, the connection should be terminated.
196 
197 2. **Capability Discovery**: The `capabilities` object allows each party to declare what features they support, including which [primitives](#primitives) they can handle (tools, resources, prompts) and whether they support features like [notifications](#notifications). This enables efficient communication by avoiding unsupported operations.
198 
199 3. **Identity Exchange**: The `clientInfo` and `serverInfo` objects provide identification and versioning information for debugging and compatibility purposes.
200 
201 In this example, the capability negotiation demonstrates how MCP primitives are declared:
202 
203 **Client Capabilities**:
204 
205 * `"sampling": {}` - The client declares it can handle server sampling requests (can receive `sampling/createMessage` method calls)
206 
207 **Server Capabilities**:
208 
209 * `"tools": {"listChanged": true}` - The server supports the tools primitive AND can send `tools/list_changed` notifications when its tool list changes
210 * `"resources": {}` - The server also supports the resources primitive (can handle `resources/list` and `resources/read` methods)
211 
212 After successful initialization, the client sends a notification to indicate it's ready:
213 
214 ```json Notification theme={null}
215 {
216 "jsonrpc": "2.0",
217 "method": "notifications/initialized"
218 }
219 ```
220 
221 #### How This Works in AI Applications
222 
223 During initialization, the AI application's MCP client manager establishes connections to configured servers and stores their 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.
224 
225 ```python Pseudo-code for AI application initialization theme={null}
226 # Pseudo Code
227 async with stdio_client(server_config) as (read, write):
228 async with ClientSession(read, write) as session:
229 init_response = await session.initialize()
230 if init_response.capabilities.tools:
231 app.register_mcp_server(session, supports_tools=True)
232 app.set_server_ready(session)
233 ```
234 </Step>
235 
236 <Step title="Tool Discovery (Primitives)">
237 Now that the connection is established, 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.
238 
239 <CodeGroup>
240 ```json Tools List Request theme={null}
241 {
242 "jsonrpc": "2.0",
243 "id": 2,
244 "method": "tools/list"
245 }
246 ```
247 
248 ```json Tools List Response theme={null}
249 {
250 "jsonrpc": "2.0",
251 "id": 2,
252 "result": {
253 "tools": [
254 {
255 "name": "calculator_arithmetic",
256 "title": "Calculator",
257 "description": "Perform mathematical calculations including basic arithmetic, trigonometric functions, and algebraic operations",
258 "inputSchema": {
259 "type": "object",
260 "properties": {
261 "expression": {
262 "type": "string",
263 "description": "Mathematical expression to evaluate (e.g., '2 + 3 * 4', 'sin(30)', 'sqrt(16)')"
264 }
265 },
266 "required": ["expression"]
267 }
268 },
269 {
270 "name": "weather_current",
271 "title": "Weather Information",
272 "description": "Get current weather information for any location worldwide",
273 "inputSchema": {
274 "type": "object",
275 "properties": {
276 "location": {
277 "type": "string",
278 "description": "City name, address, or coordinates (latitude,longitude)"
279 },
280 "units": {
281 "type": "string",
282 "enum": ["metric", "imperial", "kelvin"],
283 "description": "Temperature units to use in response",
284 "default": "metric"
285 }
286 },
287 "required": ["location"]
288 }
289 }
290 ]
291 }
292 }
293 ```
294 </CodeGroup>
295 
296 #### Understanding the Tool Discovery Request
297 
298 The `tools/list` request is simple, containing no parameters.
299 
300 #### Understanding the Tool Discovery Response
301 
302 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.
303 
304 Each tool object in the response includes several key fields:
305 
306 * **`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`)
307 * **`title`**: A human-readable display name for the tool that clients can show to users
308 * **`description`**: Detailed explanation of what the tool does and when to use it
309 * **`inputSchema`**: A JSON Schema that defines the expected input parameters, enabling type validation and providing clear documentation about required and optional parameters
310 
311 #### How This Works in AI Applications
312 
313 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.
314 
315 ```python Pseudo-code for AI application tool discovery theme={null}
316 # Pseudo-code using MCP Python SDK patterns
317 available_tools = []
318 for session in app.mcp_server_sessions():
319 tools_response = await session.list_tools()
320 available_tools.extend(tools_response.tools)
321 conversation.register_available_tools(available_tools)
322 ```
323 </Step>
324 
325 <Step title="Tool Execution (Primitives)">
326 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.
327 
328 #### Understanding the Tool Execution Request
329 
330 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:
331 
332 <CodeGroup>
333 ```json Tool Call Request theme={null}
334 {
335 "jsonrpc": "2.0",
336 "id": 3,
337 "method": "tools/call",
338 "params": {
339 "name": "weather_current",
340 "arguments": {
341 "location": "San Francisco",
342 "units": "imperial"
343 }
344 }
345 }
346 ```
347 
348 ```json Tool Call Response theme={null}
349 {
350 "jsonrpc": "2.0",
351 "id": 3,
352 "result": {
353 "content": [
354 {
355 "type": "text",
356 "text": "Current weather in San Francisco: 68°F, partly cloudy with light winds from the west at 8 mph. Humidity: 65%"
357 }
358 ]
359 }
360 }
361 ```
362 </CodeGroup>
363 
364 #### Key Elements of Tool Execution
365 
366 The request structure includes several important components:
367 
368 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.
369 
370 2. **`arguments`**: Contains the input parameters as defined by the tool's `inputSchema`. In this example:
371 * `location`: "San Francisco" (required parameter)
372 * `units`: "imperial" (optional parameter, defaults to "metric" if not specified)
373 
374 3. **JSON-RPC Structure**: Uses standard JSON-RPC 2.0 format with unique `id` for request-response correlation.
375 
376 #### Understanding the Tool Execution Response
377 
378 The response demonstrates MCP's flexible content system:
379 
380 1. **`content` Array**: Tool responses return an array of content objects, allowing for rich, multi-format responses (text, images, resources, etc.)
381 
382 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.
383 
384 3. **Structured Output**: The response provides actionable information that the AI application can use as context for language model interactions.
385 
386 This execution pattern allows AI applications to dynamically invoke server functionality and receive structured responses that can be integrated into conversations with language models.
387 
388 #### How This Works in AI Applications
389 
390 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.
391 
392 ```python theme={null}
393 # Pseudo-code for AI application tool execution
394 async def handle_tool_call(conversation, tool_name, arguments):
395 session = app.find_mcp_session_for_tool(tool_name)
396 result = await session.call_tool(tool_name, arguments)
397 conversation.add_tool_result(result.content)
398 ```
399 </Step>
400 
401 <Step title="Real-time Updates (Notifications)">
402 MCP supports real-time notifications that enable servers to inform clients about changes without being explicitly requested. This demonstrates the notification system, a key feature that keeps MCP connections synchronized and responsive.
403 
404 #### Understanding Tool List Change Notifications
405 
406 When the server's available tools change—such as when new functionality becomes available, existing tools are modified, or tools become temporarily unavailable—the server can proactively notify connected clients:
407 
408 ```json Request theme={null}
409 {
410 "jsonrpc": "2.0",
411 "method": "notifications/tools/list_changed"
412 }
413 ```
414 
415 #### Key Features of MCP Notifications
416 
417 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.
418 
419 2. **Capability-Based**: This notification is only sent by servers that declared `"listChanged": true` in their tools capability during initialization (as shown in Step 1).
420 
421 3. **Event-Driven**: The server decides when to send notifications based on internal state changes, making MCP connections dynamic and responsive.
422 
423 #### Client Response to Notifications
424 
425 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:
426 
427 ```json Request theme={null}
428 {
429 "jsonrpc": "2.0",
430 "id": 4,
431 "method": "tools/list"
432 }
433 ```
434 
435 #### Why Notifications Matter
436 
437 This notification system is crucial for several reasons:
438 
439 1. **Dynamic Environments**: Tools may come and go based on server state, external dependencies, or user permissions
440 2. **Efficiency**: Clients don't need to poll for changes; they're notified when updates occur
441 3. **Consistency**: Ensures clients always have accurate information about available server capabilities
442 4. **Real-time Collaboration**: Enables responsive AI applications that can adapt to changing contexts
443 
444 This notification pattern extends beyond tools to other MCP primitives, enabling comprehensive real-time synchronization between clients and servers.
445 
446 #### How This Works in AI Applications
447 
448 When the AI application receives a notification about changed tools, 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.
449 
450 ```python theme={null}
451 # Pseudo-code for AI application notification handling
452 async def handle_tools_changed_notification(session):
453 tools_response = await session.list_tools()
454 app.update_available_tools(session, tools_response.tools)
455 if app.conversation.is_active():
456 app.conversation.notify_llm_of_new_capabilities()
457 ```
458 </Step>
459</Steps>
353 ]
354 }
355}
356```
357 
358#### Key Elements of Tool Execution
359 
360The request structure includes several important components:
361 
3621. **`name`**: Must match exactly the tool name from the discovery response (`weather_current`). This ensures the server can correctly identify which tool to execute.
363 
3642. **`arguments`**: Contains the input parameters as defined by the tool's `inputSchema`. In this example:
365 * `location`: "San Francisco" (required parameter)
366 * `units`: "imperial" (optional parameter, defaults to "metric" if not specified)
367 
3683. **JSON-RPC Structure**: Uses standard JSON-RPC 2.0 format with unique `id` for request-response correlation.
369 
370#### Understanding the Tool Execution Response
371 
372The response demonstrates MCP's flexible content system:
373 
3741. **`content` Array**: Tool responses return an array of content objects, allowing for rich, multi-format responses (text, images, resources, etc.)
375 
3762. **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.
377 
3783. **Structured Output**: The response provides actionable information that the AI application can use as context for language model interactions.
379 
380This execution pattern allows AI applications to dynamically invoke server functionality and receive structured responses that can be integrated into conversations with language models.
381 
382#### How This Works in AI Applications
383 
384When 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.
385 
386```python theme={null}
387# Pseudo-code for AI application tool execution
388async def handle_tool_call(conversation, tool_name, arguments):
389 session = app.find_mcp_session_for_tool(tool_name)
390 result = await session.call_tool(tool_name, arguments)
391 conversation.add_tool_result(result.content)
392```
393 
394**4. Real-time Updates (Notifications)**
395 
396MCP supports real-time notifications that enable servers to inform clients about changes without being explicitly requested. This demonstrates the notification system, a key feature that keeps MCP connections synchronized and responsive.
397 
398#### Understanding Tool List Change Notifications
399 
400When the server's available tools change—such as when new functionality becomes available, existing tools are modified, or tools become temporarily unavailable—the server can proactively notify connected clients:
401 
402```json Request theme={null}
403{
404 "jsonrpc": "2.0",
405 "method": "notifications/tools/list_changed"
406}
407```
408 
409#### Key Features of MCP Notifications
410 
4111. **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.
412 
4132. **Capability-Based**: This notification is only sent by servers that declared `"listChanged": true` in their tools capability during initialization (as shown in Step 1).
414 
4153. **Event-Driven**: The server decides when to send notifications based on internal state changes, making MCP connections dynamic and responsive.
416 
417#### Client Response to Notifications
418 
419Upon 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:
420 
421```json Request theme={null}
422{
423 "jsonrpc": "2.0",
424 "id": 4,
425 "method": "tools/list"
426}
427```
428 
429#### Why Notifications Matter
430 
431This notification system is crucial for several reasons:
432 
4331. **Dynamic Environments**: Tools may come and go based on server state, external dependencies, or user permissions
4342. **Efficiency**: Clients don't need to poll for changes; they're notified when updates occur
4353. **Consistency**: Ensures clients always have accurate information about available server capabilities
4364. **Real-time Collaboration**: Enables responsive AI applications that can adapt to changing contexts
437 
438This notification pattern extends beyond tools to other MCP primitives, enabling comprehensive real-time synchronization between clients and servers.
439 
440#### How This Works in AI Applications
441 
442When the AI application receives a notification about changed tools, 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.
443 
444```python theme={null}
445# Pseudo-code for AI application notification handling
446async def handle_tools_changed_notification(session):
447 tools_response = await session.list_tools()
448 app.update_available_tools(session, tools_response.tools)
449 if app.conversation.is_active():
450 app.conversation.notify_llm_of_new_capabilities()
451```
460452 

docs/2025-03-26/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/2025-03-26/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/2025-03-26/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="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> |
16| <Icon icon="rust" size={24} />   [Rust](https://rust.sdk.modelcontextprotocol.io) | [modelcontextprotocol/rust-sdk](https://github.com/modelcontextprotocol/rust-sdk) | <Badge color="purple" shape="pill">Tier 2</Badge> |
17| <Icon icon="swift" size={24} />   Swift | [modelcontextprotocol/swift-sdk](https://github.com/modelcontextprotocol/swift-sdk) | <Badge color="orange" shape="pill">Tier 3</Badge> |
18| <Icon icon="gem" size={24} />   [Ruby](https://ruby.sdk.modelcontextprotocol.io) | [modelcontextprotocol/ruby-sdk](https://github.com/modelcontextprotocol/ruby-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|   [Java](https://java.sdk.modelcontextprotocol.io) | [modelcontextprotocol/java-sdk](https://github.com/modelcontextprotocol/java-sdk) | Tier 2 |
16|   [Rust](https://rust.sdk.modelcontextprotocol.io) | [modelcontextprotocol/rust-sdk](https://github.com/modelcontextprotocol/rust-sdk) | Tier 2 |
17|   Swift | [modelcontextprotocol/swift-sdk](https://github.com/modelcontextprotocol/swift-sdk) | Tier 3 |
18|   [Ruby](https://ruby.sdk.modelcontextprotocol.io) | [modelcontextprotocol/ruby-sdk](https://github.com/modelcontextprotocol/ruby-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/2025-03-26/develop/build-server">
41 Learn how to create your first MCP server
42 </Card>
39> **[Build a Server](/docs/2025-03-26/develop/build-server)**
40>
41> Learn how to create your first MCP server
4342 
44 <Card title="Build a Client" icon="computer" href="/docs/2025-03-26/develop/build-client">
45 Create applications that connect to MCP servers
46 </Card>
47</CardGroup>
43> **[Build a Client](/docs/2025-03-26/develop/build-client)**
44>
45> Create applications that connect to MCP servers
4846 

docs/2025-03-26/tools/debugging Changed · +46 / -54 lines

from line 32
3232logged to stderr (standard error) will be captured by the host application
3333automatically.
3434 
35<Warning>
36 Local MCP servers should not log messages to stdout (standard out), as this
37 will interfere with protocol operation.
38</Warning>
35> [!WARNING]
36>
37> Local MCP servers should not log messages to stdout (standard out), as this
38> will interfere with protocol operation.
3939 
4040For servers using the
4141[Streamable HTTP transport](/specification/2025-03-26/basic/transports#streamable-http),
from line 48
4848For all [transports](/specification/2025-03-26/basic/transports), you can also
4949provide logging to the client by sending a log message notification:
5050 
51<CodeGroup>
52 ```python Python theme={null}
53 @server.tool()
54 async def my_tool(ctx: Context) -> str:
55 await ctx.session.send_log_message(
56 level="info",
57 data="Server started successfully",
58 )
59 return "done"
60 ```
51```python Python theme={null}
52@server.tool()
53async def my_tool(ctx: Context) -> str:
54 await ctx.session.send_log_message(
55 level="info",
56 data="Server started successfully",
57 )
58 return "done"
59```
6160 
62 ```typescript TypeScript theme={null}
63 await server.sendLoggingMessage({
64 level: "info",
65 data: "Server started successfully",
66 });
67 ```
68</CodeGroup>
61```typescript TypeScript theme={null}
62await server.sendLoggingMessage({
63 level: "info",
64 data: "Server started successfully",
65});
66```
6967 
7068MCP defines eight
7169[RFC 5424 severity levels](/specification/2025-03-26/server/utilities/logging#log-levels)
from line 188
190188hover over the **Connectors** menu to see connected servers and available
191189tools.
192190 
193<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" />
191![Available MCP tools](https://mintcdn.com/mcp/zNouQwo2h8cbxlDS/images/available-mcp-tools.png?fit=max\&auto=format\&n=zNouQwo2h8cbxlDS\&q=85\&s=e2ace1ac88895a5fe30ebd8d01456bc3)
194192 
195193### Viewing logs
196194 
from line 197
199197* macOS: `~/Library/Logs/Claude`
200198* Windows: `%APPDATA%\Claude\logs`
201199 
202<CodeGroup>
203 ```bash macOS theme={null}
204 tail -n 20 -F ~/Library/Logs/Claude/mcp*.log
205 ```
200```bash macOS theme={null}
201tail -n 20 -F ~/Library/Logs/Claude/mcp*.log
202```
206203 
207 ```powershell Windows theme={null}
208 type "$env:AppData\Claude\logs\mcp*.log"
209 ```
210</CodeGroup>
204```powershell Windows theme={null}
205type "$env:AppData\Claude\logs\mcp*.log"
206```
211207 
212208The logs capture:
213209 
from line 219
223219 
2242201. Create a `developer_settings.json` file with `allowDevTools` set to true:
225221 
226<CodeGroup>
227 ```bash macOS theme={null}
228 echo '{"allowDevTools": true}' > ~/Library/Application\ Support/Claude/developer_settings.json
229 ```
222```bash macOS theme={null}
223echo '{"allowDevTools": true}' > ~/Library/Application\ Support/Claude/developer_settings.json
224```
230225 
231 ```powershell Windows theme={null}
232 '{"allowDevTools": true}' | Set-Content "$env:AppData\Claude\developer_settings.json"
233 ```
234</CodeGroup>
226```powershell Windows theme={null}
227'{"allowDevTools": true}' | Set-Content "$env:AppData\Claude\developer_settings.json"
228```
235229 
2362302. Open DevTools: `Command-Option-I` (macOS) or `Ctrl+Alt+I` (Windows)
237231 
from line 326
332326 
333327## Next steps
334328 
335<CardGroup cols={2}>
336 <Card title="MCP Inspector" icon="magnifying-glass" href="/docs/2025-03-26/tools/inspector">
337 Learn to use the MCP Inspector
338 </Card>
329> **[MCP Inspector](/docs/2025-03-26/tools/inspector)**
330>
331> Learn to use the MCP Inspector
339332 
340 <Card title="Build an MCP server" icon="code" href="/docs/2025-03-26/develop/build-server">
341 Walk through building a server from scratch
342 </Card>
333> **[Build an MCP server](/docs/2025-03-26/develop/build-server)**
334>
335> Walk through building a server from scratch
343336 
344 <Card title="Connect local servers" icon="plug" href="/docs/2025-03-26/develop/connect-local-servers">
345 Full claude\_desktop\_config.json reference and troubleshooting
346 </Card>
347</CardGroup>
337> **[Connect local servers](/docs/2025-03-26/develop/connect-local-servers)**
338>
339> Full claude\_desktop\_config.json reference and troubleshooting
348340 

docs/2025-03-26/tools/inspector Changed · +36 / -44 lines

from line 22
2222 
2323A common way to start server packages from [npm](https://npmjs.com) or [PyPI](https://pypi.org).
2424 
25<Tabs>
26 <Tab title="npm package">
27 ```bash theme={null}
28 npx -y @modelcontextprotocol/inspector npx <package-name> <args>
29 # For example
30 npx -y @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem /Users/username/Desktop
31 ```
32 </Tab>
25**npm package**
3326 
34 <Tab title="PyPI package">
35 ```bash theme={null}
36 npx @modelcontextprotocol/inspector uvx <package-name> <args>
37 # For example
38 npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git
39 ```
40 </Tab>
41</Tabs>
27```bash theme={null}
28npx -y @modelcontextprotocol/inspector npx <package-name> <args>
29# For example
30npx -y @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem /Users/username/Desktop
31```
4232 
33**PyPI package**
34 
35```bash theme={null}
36npx @modelcontextprotocol/inspector uvx <package-name> <args>
37# For example
38npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git
39```
40 
4341#### Inspecting locally developed servers
4442 
4543To inspect servers locally developed or downloaded as a repository, the most common
4644way is:
4745 
48<Tabs>
49 <Tab title="TypeScript">
50 ```bash theme={null}
51 npx @modelcontextprotocol/inspector node path/to/server/index.js args...
52 ```
53 </Tab>
46**TypeScript**
5447 
55 <Tab title="Python">
56 ```bash theme={null}
57 npx @modelcontextprotocol/inspector \
58 uv \
59 --directory path/to/server \
60 run \
61 package-name \
62 args...
63 ```
64 </Tab>
65</Tabs>
48```bash theme={null}
49npx @modelcontextprotocol/inspector node path/to/server/index.js args...
50```
6651 
52**Python**
53 
54```bash theme={null}
55npx @modelcontextprotocol/inspector \
56 uv \
57 --directory path/to/server \
58 run \
59 package-name \
60 args...
61```
62 
6763Please carefully read any attached README for the most accurate instructions.
6864 
6965## Feature overview
7066 
71<Frame caption="The MCP Inspector interface">
72 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/mcp-inspector.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=83b12e2a457c96ef4ad17c7357236290" width="2888" height="1761" data-path="images/mcp-inspector.png" />
73</Frame>
67![](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/mcp-inspector.png?fit=max\&auto=format\&n=4ZXF1PrDkEaJvXpn\&q=85\&s=83b12e2a457c96ef4ad17c7357236290)
7468 
7569The Inspector provides several features for interacting with your MCP server:
7670 
from line 123
129123 
130124## Next steps
131125 
132<CardGroup cols={2}>
133 <Card title="Inspector Repository" icon="github" href="https://github.com/modelcontextprotocol/inspector">
134 Check out the MCP Inspector source code
135 </Card>
126> **[Inspector Repository](https://github.com/modelcontextprotocol/inspector)**
127>
128> Check out the MCP Inspector source code
136129 
137 <Card title="Debugging Guide" icon="bug" href="/docs/2025-03-26/tools/debugging">
138 Learn about broader debugging strategies
139 </Card>
140</CardGroup>
130> **[Debugging Guide](/docs/2025-03-26/tools/debugging)**
131>
132> Learn about broader debugging strategies
141133 

docs/2025-03-26/tutorials/security/authorization Changed · +841 / -861 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/2025-03-26/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/2025-03-26/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}/`,
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;
328 console.log(
329 `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
317330 );
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 };
331 });
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");
333370 }
334371 
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 };
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);
346379 }
347380 
348 const app = express();
349 
350 app.use(
351 express.json({
352 verify: (req: any, _res, buf) => {
353 req.rawBody = buf?.toString() ?? "";
381 let response: Response;
382 try {
383 response = await fetch(endpoint, {
384 method: "POST",
385 headers: {
386 "Content-Type": "application/x-www-form-urlencoded",
354387 },
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 checkResourceAllowed({
431 requestedResource: a,
432 configuredResource: mcpServerUrl,
355433 }),
356434 );
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"],
435 if (!allowed) {
436 throw new Error(
437 `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
438 );
439 }
440 
441 return {
442 token,
443 clientId: data.client_id,
444 scopes: data.scope ? data.scope.split(" ") : [],
445 expiresAt: data.exp,
373446 };
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");
447 },
448};
449app.use(
450 mcpAuthMetadataRouter({
451 oauthMetadata,
452 resourceServerUrl: mcpServerUrl,
453 scopesSupported: ["mcp:tools"],
454 resourceName: "MCP Demo Server",
455 }),
456);
457 
458const authMiddleware = requireBearerAuth({
459 verifier: tokenVerifier,
460 requiredScopes: [],
461 resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
462});
463 
464const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
465 
466function createMcpServer() {
467 const server = new McpServer({
468 name: "example-server",
469 version: "1.0.0",
470 });
471 
472 server.registerTool(
473 "add",
474 {
475 title: "Addition Tool",
476 description: "Add two numbers together",
477 inputSchema: {
478 a: z.number().describe("First number to add"),
479 b: z.number().describe("Second number to add"),
480 },
481 },
482 async ({ a, b }) => ({
483 content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
484 }),
485 );
486 
487 server.registerTool(
488 "multiply",
489 {
490 title: "Multiplication Tool",
491 description: "Multiply two numbers together",
492 inputSchema: {
493 x: z.number().describe("First number to multiply"),
494 y: z.number().describe("Second number to multiply"),
495 },
496 },
497 async ({ x, y }) => ({
498 content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
499 }),
500 );
501 
502 return server;
503}
504 
505const mcpPostHandler = async (req: express.Request, res: express.Response) => {
506 const sessionId = req.headers["mcp-session-id"] as string | undefined;
507 let transport: StreamableHTTPServerTransport;
508 
509 if (sessionId && transports[sessionId]) {
510 transport = transports[sessionId];
511 } else if (!sessionId && isInitializeRequest(req.body)) {
512 transport = new StreamableHTTPServerTransport({
513 sessionIdGenerator: () => randomUUID(),
514 onsessioninitialized: (sessionId) => {
515 transports[sessionId] = transport;
516 },
517 });
518 
519 transport.onclose = () => {
520 if (transport.sessionId) {
521 delete transports[transport.sessionId];
522 }
523 };
524 
525 const server = createMcpServer();
526 await server.connect(transport);
527 } else {
528 res.status(400).json({
529 jsonrpc: "2.0",
530 error: {
531 code: -32000,
532 message: "Bad Request: No valid session ID provided",
533 },
534 id: null,
535 });
536 return;
537 }
538 
539 await transport.handleRequest(req, res, req.body);
540};
541 
542const handleSessionRequest = async (
543 req: express.Request,
544 res: express.Response,
545) => {
546 const sessionId = req.headers["mcp-session-id"] as string | undefined;
547 if (!sessionId || !transports[sessionId]) {
548 res.status(400).send("Invalid or missing session ID");
549 return;
550 }
551 
552 const transport = transports[sessionId];
553 await transport.handleRequest(req, res);
554};
555 
556app.post("/", authMiddleware, mcpPostHandler);
557app.get("/", authMiddleware, handleSessionRequest);
558app.delete("/", authMiddleware, handleSessionRequest);
559 
560app.listen(CONFIG.port, CONFIG.host, () => {
561 console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
562 console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
563 console.log(
564 `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
565 );
566});
567```
568 
569When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
570 
571For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
572 
573**Python**
574 
575You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
576 
577To simplify our authorization interaction, in Python scenarios we rely on [FastMCP](https://gofastmcp.com/getting-started/welcome). 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.
578 
579Prior 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:
580 
581```python theme={null}
582"""Configuration settings for the MCP auth server."""
583 
584import os
585from typing import Optional
586 
587 
588class Config:
589 """Configuration class that loads from environment variables with sensible defaults."""
590 
591 # Server settings
592 HOST: str = os.getenv("HOST", "localhost")
593 PORT: int = int(os.getenv("PORT", "3000"))
594 
595 # Auth server settings
596 AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
597 AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
598 AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
599 
600 # OAuth client settings
601 OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "mcp-server")
602 OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "UO3rmozkFFkXr0QxPTkzZ0LMXDidIikB")
603 
604 # Server settings
605 MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
606 OAUTH_STRICT: bool = os.getenv("OAUTH_STRICT", "false").lower() in ("true", "1", "yes")
607 TRANSPORT: str = os.getenv("TRANSPORT", "streamable-http")
608 
609 @property
610 def server_url(self) -> str:
611 """Build the server URL."""
612 return f"http://{self.HOST}:{self.PORT}"
613 
614 @property
615 def auth_base_url(self) -> str:
616 """Build the auth server base URL."""
617 return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
618 
619 def validate(self) -> None:
620 """Validate configuration."""
621 if self.TRANSPORT not in ["sse", "streamable-http"]:
622 raise ValueError(f"Invalid transport: {self.TRANSPORT}. Must be 'sse' or 'streamable-http'")
623 
624 
625# Global configuration instance
626config = Config()
627 
628```
629 
630The server implementation is as follows:
631 
632```python theme={null}
633import datetime
634import logging
635from typing import Any
636 
637from pydantic import AnyHttpUrl
638 
639from mcp.server.auth.settings import AuthSettings
640from mcp.server.fastmcp.server import FastMCP
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 from urllib.parse import urljoin
651 
652 auth_base_url = config.auth_base_url
653 
654 return {
655 "issuer": auth_base_url,
656 "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
657 "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
658 "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
659 }
660 
661 
662def create_server() -> FastMCP:
663 """Create and configure the FastMCP server."""
664 
665 config.validate()
666 
667 oauth_urls = create_oauth_urls()
668 
669 token_verifier = IntrospectionTokenVerifier(
670 introspection_endpoint=oauth_urls["introspection_endpoint"],
671 server_url=config.server_url,
672 client_id=config.OAUTH_CLIENT_ID,
673 client_secret=config.OAUTH_CLIENT_SECRET,
674 )
675 
676 app = FastMCP(
677 name="MCP Resource Server",
678 instructions="Resource Server that validates tokens via Authorization Server introspection",
679 host=config.HOST,
680 port=config.PORT,
681 debug=True,
682 streamable_http_path="/",
683 token_verifier=token_verifier,
684 auth=AuthSettings(
685 issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
686 required_scopes=[config.MCP_SCOPE],
687 resource_server_url=AnyHttpUrl(config.server_url),
688 ),
689 )
690 
691 @app.tool()
692 async def add_numbers(a: float, b: float) -> dict[str, Any]:
693 """
694 Add two numbers together.
695 This tool demonstrates basic arithmetic operations with OAuth authentication.
696 
697 Args:
698 a: The first number to add
699 b: The second number to add
700 """
701 result = a + b
702 return {
703 "operation": "addition",
704 "operand_a": a,
705 "operand_b": b,
706 "result": result,
707 "timestamp": datetime.datetime.now().isoformat()
382708 }
383709 
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);
710 @app.tool()
711 async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
712 """
713 Multiply two numbers together.
714 This tool demonstrates basic arithmetic operations with OAuth authentication.
715 
716 Args:
717 x: The first number to multiply
718 y: The second number to multiply
719 """
720 result = x * y
721 return {
722 "operation": "multiplication",
723 "operand_x": x,
724 "operand_y": y,
725 "result": result,
726 "timestamp": datetime.datetime.now().isoformat()
391727 }
392728 
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;
729 return app
730 
731 
732def main() -> int:
733 """
734 Run the MCP Resource Server.
735 
736 This server:
737 - Provides RFC 9728 Protected Resource Metadata
738 - Validates tokens via Authorization Server introspection
739 - Serves MCP tools requiring authentication
740 
741 Configuration is loaded from config.py and environment variables.
742 """
743 logging.basicConfig(level=logging.INFO)
744 
745 try:
746 config.validate()
747 oauth_urls = create_oauth_urls()
748 
749 except ValueError as e:
750 logger.error("Configuration error: %s", e)
751 return 1
752 
753 try:
754 mcp_server = create_server()
755 
756 logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
757 logger.info("Authorization Server: %s", oauth_urls["issuer"])
758 logger.info("Transport: %s", config.TRANSPORT)
759 
760 mcp_server.run(transport=config.TRANSPORT)
761 return 0
762 
763 except Exception:
764 logger.exception("Server error")
765 return 1
766 
767 
768if __name__ == "__main__":
769 exit(main())
770```
771 
772Lastly, 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
773 
774```python theme={null}
775"""Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
776 
777import logging
778from typing import Any
779 
780from mcp.server.auth.provider import AccessToken, TokenVerifier
781from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
782 
783logger = logging.getLogger(__name__)
784 
785 
786class IntrospectionTokenVerifier(TokenVerifier):
787 """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662).
788 """
789 
790 def __init__(
791 self,
792 introspection_endpoint: str,
793 server_url: str,
794 client_id: str,
795 client_secret: str,
796 ):
797 self.introspection_endpoint = introspection_endpoint
798 self.server_url = server_url
799 self.client_id = client_id
800 self.client_secret = client_secret
801 self.resource_url = resource_url_from_server_url(server_url)
802 
803 async def verify_token(self, token: str) -> AccessToken | None:
804 """Verify token via introspection endpoint."""
805 import httpx
806 
807 if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
808 return None
809 
810 timeout = httpx.Timeout(10.0, connect=5.0)
811 limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)
812 
813 async with httpx.AsyncClient(
814 timeout=timeout,
815 limits=limits,
816 verify=True,
817 ) as client:
818 try:
819 form_data = {
820 "token": token,
821 "client_id": self.client_id,
822 "client_secret": self.client_secret,
823 }
824 headers = {"Content-Type": "application/x-www-form-urlencoded"}
825 
826 response = await client.post(
827 self.introspection_endpoint,
828 data=form_data,
829 headers=headers,
830 )
831 
832 if response.status_code != 200:
833 return None
834 
835 data = response.json()
836 if not data.get("active", False):
837 return None
838 
839 if not self._validate_resource(data):
840 return None
841 
842 return AccessToken(
843 token=token,
844 client_id=data.get("client_id", "unknown"),
845 scopes=data.get("scope", "").split() if data.get("scope") else [],
846 expires_at=data.get("exp"),
847 resource=data.get("aud"), # Include resource in token
848 )
849 
850 except Exception as e:
851 return None
852 
853 def _validate_resource(self, token_data: dict[str, Any]) -> bool:
854 """Validate token was issued for this resource server.
855 
856 Rules:
857 - Reject if 'aud' missing.
858 - Accept if any audience entry matches the derived resource URL.
859 - Supports string or list forms per JWT spec.
860 """
861 if not self.server_url or not self.resource_url:
862 return False
863 
864 aud: list[str] | str | None = token_data.get("aud")
865 if isinstance(aud, list):
866 return any(self._is_valid_resource(a) for a in aud)
867 if isinstance(aud, str):
868 return self._is_valid_resource(aud)
869 return False
870 
871 def _is_valid_resource(self, resource: str) -> bool:
872 """Check if the given resource matches our server."""
873 return check_resource_allowed(self.resource_url, resource)
874```
875 
876For more details, see the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
877 
878**C#**
879 
880You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
881 
882To 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.
883 
884```csharp theme={null}
885using Microsoft.AspNetCore.Authentication.JwtBearer;
886using Microsoft.IdentityModel.Tokens;
887using ModelContextProtocol.AspNetCore.Authentication;
888using ProtectedMcpServer.Tools;
889using System.Security.Claims;
890 
891var builder = WebApplication.CreateBuilder(args);
892 
893var serverUrl = "http://localhost:3000/";
894var authorizationServerUrl = "http://localhost:8080/realms/master/";
895 
896builder.Services.AddAuthentication(options =>
897{
898 options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
899 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
900})
901.AddJwtBearer(options =>
902{
903 options.Authority = authorizationServerUrl;
904 var normalizedServerAudience = serverUrl.TrimEnd('/');
905 options.TokenValidationParameters = new TokenValidationParameters
906 {
907 ValidIssuer = authorizationServerUrl,
908 ValidAudiences = new[] { normalizedServerAudience, serverUrl },
909 AudienceValidator = (audiences, securityToken, validationParameters) =>
910 {
911 if (audiences == null) return false;
912 foreach (var aud in audiences)
913 {
914 if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
915 {
916 return true;
917 }
918 }
919 return false;
405920 }
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 checkResourceAllowed({
443 requestedResource: a,
444 configuredResource: mcpServerUrl,
445 }),
446 );
447 if (!allowed) {
448 throw new Error(
449 `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
450 );
451 }
452 
453 return {
454 token,
455 clientId: data.client_id,
456 scopes: data.scope ? data.scope.split(" ") : [],
457 expiresAt: data.exp,
458 };
459 },
460921 };
461 app.use(
462 mcpAuthMetadataRouter({
463 oauthMetadata,
464 resourceServerUrl: mcpServerUrl,
465 scopesSupported: ["mcp:tools"],
466 resourceName: "MCP Demo Server",
467 }),
468 );
469 
470 const authMiddleware = requireBearerAuth({
471 verifier: tokenVerifier,
472 requiredScopes: [],
473 resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
474 });
475 
476 const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
477 
478 function createMcpServer() {
479 const server = new McpServer({
480 name: "example-server",
481 version: "1.0.0",
482 });
483 
484 server.registerTool(
485 "add",
922 
923 options.RequireHttpsMetadata = false; // Set to true in production
924 
925 options.Events = new JwtBearerEvents
926 {
927 OnTokenValidated = context =>
486928 {
487 title: "Addition Tool",
488 description: "Add two numbers together",
489 inputSchema: {
490 a: z.number().describe("First number to add"),
491 b: z.number().describe("Second number to add"),
492 },
929 var name = context.Principal?.Identity?.Name ?? "unknown";
930 var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
931 Console.WriteLine($"Token validated for: {name} ({email})");
932 return Task.CompletedTask;
493933 },
494 async ({ a, b }) => ({
495 content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
496 }),
497 );
498 
499 server.registerTool(
500 "multiply",
934 OnAuthenticationFailed = context =>
501935 {
502 title: "Multiplication Tool",
503 description: "Multiply two numbers together",
504 inputSchema: {
505 x: z.number().describe("First number to multiply"),
506 y: z.number().describe("Second number to multiply"),
507 },
936 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
937 return Task.CompletedTask;
508938 },
509 async ({ x, y }) => ({
510 content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
511 }),
512 );
513 
514 return server;
515 }
516 
517 const mcpPostHandler = async (req: express.Request, res: express.Response) => {
518 const sessionId = req.headers["mcp-session-id"] as string | undefined;
519 let transport: StreamableHTTPServerTransport;
520 
521 if (sessionId && transports[sessionId]) {
522 transport = transports[sessionId];
523 } else if (!sessionId && isInitializeRequest(req.body)) {
524 transport = new StreamableHTTPServerTransport({
525 sessionIdGenerator: () => randomUUID(),
526 onsessioninitialized: (sessionId) => {
527 transports[sessionId] = transport;
528 },
529 });
530 
531 transport.onclose = () => {
532 if (transport.sessionId) {
533 delete transports[transport.sessionId];
534 }
535 };
536 
537 const server = createMcpServer();
538 await server.connect(transport);
539 } else {
540 res.status(400).json({
541 jsonrpc: "2.0",
542 error: {
543 code: -32000,
544 message: "Bad Request: No valid session ID provided",
545 },
546 id: null,
547 });
548 return;
549 }
550 
551 await transport.handleRequest(req, res, req.body);
552939 };
553 
554 const handleSessionRequest = async (
555 req: express.Request,
556 res: express.Response,
557 ) => {
558 const sessionId = req.headers["mcp-session-id"] as string | undefined;
559 if (!sessionId || !transports[sessionId]) {
560 res.status(400).send("Invalid or missing session ID");
561 return;
562 }
563 
564 const transport = transports[sessionId];
565 await transport.handleRequest(req, res);
940})
941.AddMcp(options =>
942{
943 options.ResourceMetadata = new()
944 {
945 Resource = new Uri(serverUrl),
946 ResourceDocumentation = new Uri("https://docs.example.com/api/math"),
947 AuthorizationServers = { new Uri(authorizationServerUrl) },
948 ScopesSupported = ["mcp:tools"]
566949 };
567 
568 app.post("/", authMiddleware, mcpPostHandler);
569 app.get("/", authMiddleware, handleSessionRequest);
570 app.delete("/", authMiddleware, handleSessionRequest);
571 
572 app.listen(CONFIG.port, CONFIG.host, () => {
573 console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
574 console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
575 console.log(
576 `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
577 );
578 });
579 ```
580 
581 When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
582 
583 For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
584 </Tab>
585 
586 <Tab title="Python">
587 You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
588 
589 To simplify our authorization interaction, in Python scenarios we rely on [FastMCP](https://gofastmcp.com/getting-started/welcome). 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.
590 
591 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:
592 
593 ```python theme={null}
594 """Configuration settings for the MCP auth server."""
595 
596 import os
597 from typing import Optional
598 
599 
600 class Config:
601 """Configuration class that loads from environment variables with sensible defaults."""
602 
603 # Server settings
604 HOST: str = os.getenv("HOST", "localhost")
605 PORT: int = int(os.getenv("PORT", "3000"))
606 
607 # Auth server settings
608 AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
609 AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
610 AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
611 
612 # OAuth client settings
613 OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "mcp-server")
614 OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "UO3rmozkFFkXr0QxPTkzZ0LMXDidIikB")
615 
616 # Server settings
617 MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
618 OAUTH_STRICT: bool = os.getenv("OAUTH_STRICT", "false").lower() in ("true", "1", "yes")
619 TRANSPORT: str = os.getenv("TRANSPORT", "streamable-http")
620 
621 @property
622 def server_url(self) -> str:
623 """Build the server URL."""
624 return f"http://{self.HOST}:{self.PORT}"
625 
626 @property
627 def auth_base_url(self) -> str:
628 """Build the auth server base URL."""
629 return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
630 
631 def validate(self) -> None:
632 """Validate configuration."""
633 if self.TRANSPORT not in ["sse", "streamable-http"]:
634 raise ValueError(f"Invalid transport: {self.TRANSPORT}. Must be 'sse' or 'streamable-http'")
635 
636 
637 # Global configuration instance
638 config = Config()
639 
640 ```
641 
642 The server implementation is as follows:
643 
644 ```python theme={null}
645 import datetime
646 import logging
647 from typing import Any
648 
649 from pydantic import AnyHttpUrl
650 
651 from mcp.server.auth.settings import AuthSettings
652 from mcp.server.fastmcp.server import FastMCP
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 from urllib.parse import urljoin
663 
664 auth_base_url = config.auth_base_url
665 
666 return {
667 "issuer": auth_base_url,
668 "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
669 "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
670 "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
671 }
672 
673 
674 def create_server() -> FastMCP:
675 """Create and configure the FastMCP server."""
676 
677 config.validate()
678 
679 oauth_urls = create_oauth_urls()
680 
681 token_verifier = IntrospectionTokenVerifier(
682 introspection_endpoint=oauth_urls["introspection_endpoint"],
683 server_url=config.server_url,
684 client_id=config.OAUTH_CLIENT_ID,
685 client_secret=config.OAUTH_CLIENT_SECRET,
686 )
687 
688 app = FastMCP(
689 name="MCP Resource Server",
690 instructions="Resource Server that validates tokens via Authorization Server introspection",
691 host=config.HOST,
692 port=config.PORT,
693 debug=True,
694 streamable_http_path="/",
695 token_verifier=token_verifier,
696 auth=AuthSettings(
697 issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
698 required_scopes=[config.MCP_SCOPE],
699 resource_server_url=AnyHttpUrl(config.server_url),
700 ),
701 )
702 
703 @app.tool()
704 async def add_numbers(a: float, b: float) -> dict[str, Any]:
705 """
706 Add two numbers together.
707 This tool demonstrates basic arithmetic operations with OAuth authentication.
708 
709 Args:
710 a: The first number to add
711 b: The second number to add
712 """
713 result = a + b
714 return {
715 "operation": "addition",
716 "operand_a": a,
717 "operand_b": b,
718 "result": result,
719 "timestamp": datetime.datetime.now().isoformat()
720 }
721 
722 @app.tool()
723 async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
724 """
725 Multiply two numbers together.
726 This tool demonstrates basic arithmetic operations with OAuth authentication.
727 
728 Args:
729 x: The first number to multiply
730 y: The second number to multiply
731 """
732 result = x * y
733 return {
734 "operation": "multiplication",
735 "operand_x": x,
736 "operand_y": y,
737 "result": result,
738 "timestamp": datetime.datetime.now().isoformat()
739 }
740 
741 return app
742 
743 
744 def main() -> int:
745 """
746 Run the MCP Resource Server.
747 
748 This server:
749 - Provides RFC 9728 Protected Resource Metadata
750 - Validates tokens via Authorization Server introspection
751 - Serves MCP tools requiring authentication
752 
753 Configuration is loaded from config.py and environment variables.
754 """
755 logging.basicConfig(level=logging.INFO)
756 
757 try:
758 config.validate()
759 oauth_urls = create_oauth_urls()
760 
761 except ValueError as e:
762 logger.error("Configuration error: %s", e)
763 return 1
764 
765 try:
766 mcp_server = create_server()
767 
768 logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
769 logger.info("Authorization Server: %s", oauth_urls["issuer"])
770 logger.info("Transport: %s", config.TRANSPORT)
771 
772 mcp_server.run(transport=config.TRANSPORT)
773 return 0
774 
775 except Exception:
776 logger.exception("Server error")
777 return 1
778 
779 
780 if __name__ == "__main__":
781 exit(main())
782 ```
783 
784 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
785 
786 ```python theme={null}
787 """Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
788 
789 import logging
790 from typing import Any
791 
792 from mcp.server.auth.provider import AccessToken, TokenVerifier
793 from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
794 
795 logger = logging.getLogger(__name__)
796 
797 
798 class IntrospectionTokenVerifier(TokenVerifier):
799 """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662).
800 """
801 
802 def __init__(
803 self,
804 introspection_endpoint: str,
805 server_url: str,
806 client_id: str,
807 client_secret: str,
808 ):
809 self.introspection_endpoint = introspection_endpoint
810 self.server_url = server_url
811 self.client_id = client_id
812 self.client_secret = client_secret
813 self.resource_url = resource_url_from_server_url(server_url)
814 
815 async def verify_token(self, token: str) -> AccessToken | None:
816 """Verify token via introspection endpoint."""
817 import httpx
818 
819 if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
820 return None
821 
822 timeout = httpx.Timeout(10.0, connect=5.0)
823 limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)
824 
825 async with httpx.AsyncClient(
826 timeout=timeout,
827 limits=limits,
828 verify=True,
829 ) as client:
830 try:
831 form_data = {
832 "token": token,
833 "client_id": self.client_id,
834 "client_secret": self.client_secret,
835 }
836 headers = {"Content-Type": "application/x-www-form-urlencoded"}
837 
838 response = await client.post(
839 self.introspection_endpoint,
840 data=form_data,
841 headers=headers,
842 )
843 
844 if response.status_code != 200:
845 return None
846 
847 data = response.json()
848 if not data.get("active", False):
849 return None
850 
851 if not self._validate_resource(data):
852 return None
853 
854 return AccessToken(
855 token=token,
856 client_id=data.get("client_id", "unknown"),
857 scopes=data.get("scope", "").split() if data.get("scope") else [],
858 expires_at=data.get("exp"),
859 resource=data.get("aud"), # Include resource in token
860 )
861 
862 except Exception as e:
863 return None
864 
865 def _validate_resource(self, token_data: dict[str, Any]) -> bool:
866 """Validate token was issued for this resource server.
867 
868 Rules:
869 - Reject if 'aud' missing.
870 - Accept if any audience entry matches the derived resource URL.
871 - Supports string or list forms per JWT spec.
872 """
873 if not self.server_url or not self.resource_url:
874 return False
875 
876 aud: list[str] | str | None = token_data.get("aud")
877 if isinstance(aud, list):
878 return any(self._is_valid_resource(a) for a in aud)
879 if isinstance(aud, str):
880 return self._is_valid_resource(aud)
881 return False
882 
883 def _is_valid_resource(self, resource: str) -> bool:
884 """Check if the given resource matches our server."""
885 return check_resource_allowed(self.resource_url, resource)
886 ```
887 
888 For more details, see the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
889 </Tab>
890 
891 <Tab title="C#">
892 You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
893 
894 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.
895 
896 ```csharp theme={null}
897 using Microsoft.AspNetCore.Authentication.JwtBearer;
898 using Microsoft.IdentityModel.Tokens;
899 using ModelContextProtocol.AspNetCore.Authentication;
900 using ProtectedMcpServer.Tools;
901 using System.Security.Claims;
902 
903 var builder = WebApplication.CreateBuilder(args);
904 
905 var serverUrl = "http://localhost:3000/";
906 var authorizationServerUrl = "http://localhost:8080/realms/master/";
907 
908 builder.Services.AddAuthentication(options =>
909 {
910 options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
911 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
912 })
913 .AddJwtBearer(options =>
914 {
915 options.Authority = authorizationServerUrl;
916 var normalizedServerAudience = serverUrl.TrimEnd('/');
917 options.TokenValidationParameters = new TokenValidationParameters
918 {
919 ValidIssuer = authorizationServerUrl,
920 ValidAudiences = new[] { normalizedServerAudience, serverUrl },
921 AudienceValidator = (audiences, securityToken, validationParameters) =>
922 {
923 if (audiences == null) return false;
924 foreach (var aud in audiences)
925 {
926 if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
927 {
928 return true;
929 }
930 }
931 return false;
932 }
933 };
934 
935 options.RequireHttpsMetadata = false; // Set to true in production
936 
937 options.Events = new JwtBearerEvents
938 {
939 OnTokenValidated = context =>
940 {
941 var name = context.Principal?.Identity?.Name ?? "unknown";
942 var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
943 Console.WriteLine($"Token validated for: {name} ({email})");
944 return Task.CompletedTask;
945 },
946 OnAuthenticationFailed = context =>
947 {
948 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
949 return Task.CompletedTask;
950 },
951 };
952 })
953 .AddMcp(options =>
954 {
955 options.ResourceMetadata = new()
956 {
957 Resource = new Uri(serverUrl),
958 ResourceDocumentation = new Uri("https://docs.example.com/api/math"),
959 AuthorizationServers = { new Uri(authorizationServerUrl) },
960 ScopesSupported = ["mcp:tools"]
961 };
962 });
963 
964 builder.Services.AddAuthorization();
965 
966 builder.Services.AddHttpContextAccessor();
967 builder.Services.AddMcpServer()
968 .WithTools<MathTools>()
969 .WithHttpTransport();
970 
971 var app = builder.Build();
972 
973 app.UseAuthentication();
974 app.UseAuthorization();
975 
976 app.MapMcp().RequireAuthorization();
977 
978 Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
979 Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
980 Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
981 Console.WriteLine("Exposed Math tools: Add, Multiply");
982 Console.WriteLine("Press Ctrl+C to stop the server");
983 
984 app.Run(serverUrl);
985 ```
986 
987 For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
988 </Tab>
989</Tabs>
950});
951 
952builder.Services.AddAuthorization();
953 
954builder.Services.AddHttpContextAccessor();
955builder.Services.AddMcpServer()
956 .WithTools<MathTools>()
957 .WithHttpTransport();
958 
959var app = builder.Build();
960 
961app.UseAuthentication();
962app.UseAuthorization();
963 
964app.MapMcp().RequireAuthorization();
965 
966Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
967Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
968Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
969Console.WriteLine("Exposed Math tools: Add, Multiply");
970Console.WriteLine("Press Ctrl+C to stop the server");
971 
972app.Run(serverUrl);
973```
974 
975For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
990976 
991977## Testing the MCP Server
992978 
from line 989
1003989 
1004990On 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.
1005991 
1006<Frame>
1007 <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" />
1008</Frame>
992![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)
1009993 
1010994After consenting, you will see the tools listed right above the server entry in `mcp.json`.
1011995 
1012<Frame>
1013 <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" />
1014</Frame>
996![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)
1015997 
1016998You will be able to invoke individual tools with the help of the `#` sign in the chat view.
1017999 
1018<Frame>
1019 <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" />
1020</Frame>
1000![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)
10211001 
10221002## Common Pitfalls and How to Avoid Them
10231003 
10241004 

docs/2025-03-26/tutorials/security/security_best_practices Changed · +4 / -4 lines

from line 423
423423* Link-local: `169.254.0.0/16` (including cloud metadata endpoints)
424424* Private IPv6 ranges: `fc00::/7`, `fe80::/10`
425425 
426<Note>
427 Avoid implementing IP validation manually. Attackers exploit encoding tricks
428 (octal, hex, IPv4-mapped IPv6) that custom parsers often miss.
429</Note>
426> [!NOTE]
427>
428> Avoid implementing IP validation manually. Attackers exploit encoding tricks
429> (octal, hex, IPv4-mapped IPv6) that custom parsers often miss.
430430 
431431**Validate Redirect Targets**
432432 

docs/2025-06-18/develop/build-client Changed · +2436 / -2452 lines

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

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

docs/2025-06-18/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/2025-06-18/tools/inspector">
90 Test your server's tools, resources, and prompts interactively
91 </Card>
88> **[MCP Inspector](/docs/2025-06-18/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/2025-06-18/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/2025-06-18/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/2025-06-18/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="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 scroll over "Connectors" and click "Manage connectors". Select "filesystem" from the connector list to view the 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 scroll over "Connectors" and click "Manage connectors". Select "filesystem" from the connector list to view the 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 error (stderr) logging from the named server.
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/2025-06-18/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 error (stderr) logging from the named server.
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/2025-06-18/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/2025-06-18/develop/build-server">
271 Create custom MCP servers tailored to your specific workflows and
272 integrations
273 </Card>
247> **[Build your own server](/docs/2025-06-18/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/2025-06-18/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/2025-06-18/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/2025-06-18/learn/architecture">
281 Dive deeper into how MCP works and its architecture
282 </Card>
283</CardGroup>
257> **[Understand the protocol](/docs/2025-06-18/learn/architecture)**
258>
259> Dive deeper into how MCP works and its architecture
284260 

docs/2025-06-18/develop/connect-remote-servers Changed · +41 / -57 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 in your browser and navigate to the settings page. You can access this by clicking on your profile icon and selecting "Settings" from the dropdown menu. Once in settings, locate and click on the "Connectors" section in the sidebar.
28**1. Navigate to Connector Settings**
3129 
32 This will display your currently configured connectors and provide options to add new ones.
33 </Step>
30Open Claude in your browser and navigate to the settings page. You can access this by clicking on your profile icon and selecting "Settings" from the dropdown menu. Once in settings, locate and click on the "Connectors" section in the sidebar.
3431 
35 <Step title="Add a Custom Connector">
36 In the Connectors section, scroll to the bottom where you'll find the "Add custom connector" button. Click this button to begin the connection process.
32This will display your currently configured connectors and provide options to add new ones.
3733 
38 <Frame>
39 <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" />
40 </Frame>
34**2. Add a Custom Connector**
4135 
42 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.
36In the Connectors section, scroll to the bottom where you'll find the "Add custom connector" button. Click this button to begin the connection process.
4337 
44 <Frame>
45 <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" />
46 </Frame>
38![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)
4739 
48 After entering the URL, click "Add" to proceed with the connection.
49 </Step>
40A 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.
5041 
51 <Step title="Complete Authentication">
52 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.
42![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)
5343 
54 <Frame>
55 <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" />
56 </Frame>
44After entering the URL, click "Add" to proceed with the connection.
5745 
58 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.
59 </Step>
46**3. Complete Authentication**
6047 
61 <Step title="Access Resources and Prompts">
62 After successful connection, the remote server's resources and prompts become available in your Claude conversations. You can access these by clicking the paperclip icon in the message input area, which opens the attachment menu.
48Most 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.
6349 
64 <Frame>
65 <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" />
66 </Frame>
50![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)
6751 
68 The menu displays all available resources and prompts from your connected servers. Select the items you want to include in your conversation. These resources provide Claude with context and information from your external tools.
52Follow 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.
6953 
70 <Frame>
71 <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" />
72 </Frame>
73 </Step>
54**4. Access Resources and Prompts**
7455 
75 <Step title="Configure Tool Permissions">
76 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.
56After successful connection, the remote server's resources and prompts become available in your Claude conversations. You can access these by clicking the paperclip icon in the message input area, which opens the attachment menu.
7757 
78 <Frame>
79 <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" />
80 </Frame>
58![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)
8159 
82 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.
83 </Step>
84</Steps>
60The menu displays all available resources and prompts from your connected servers. Select the items you want to include in your conversation. These resources provide Claude with context and information from your external tools.
8561 
62![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)
63 
64**5. Configure Tool Permissions**
65 
66Remote 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.
67 
68![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)
69 
70Navigate 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.
71 
8672## Best Practices for Using Remote MCP Servers
8773 
8874When working with remote MCP servers, consider these recommendations to ensure a secure and efficient experience:
from line 81
9581 
9682Now 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.
9783 
98<CardGroup cols={2}>
99 <Card title="Build your own remote server" icon="cloud" href="/docs/2025-06-18/develop/build-server">
100 Create custom remote MCP servers to integrate with proprietary tools and
101 services
102 </Card>
84> **[Build your own remote server](/docs/2025-06-18/develop/build-server)**
85>
86> Create custom remote MCP servers to integrate with proprietary tools and
87> services
10388 
104 <Card title="Explore available servers" icon="grid" href="https://github.com/modelcontextprotocol/servers">
105 Browse our collection of official and community-created MCP servers
106 </Card>
89> **[Explore available servers](https://github.com/modelcontextprotocol/servers)**
90>
91> Browse our collection of official and community-created MCP servers
10792 
108 <Card title="Connect local servers" icon="computer" href="/docs/2025-06-18/develop/connect-local-servers">
109 Learn how to connect Claude Desktop to local MCP servers for direct system
110 access
111 </Card>
93> **[Connect local servers](/docs/2025-06-18/develop/connect-local-servers)**
94>
95> Learn how to connect Claude Desktop to local MCP servers for direct system
96> access
11297 
113 <Card title="Understand the architecture" icon="book" href="/docs/2025-06-18/learn/architecture">
114 Dive deeper into how MCP works and its architecture
115 </Card>
116</CardGroup>
98> **[Understand the architecture](/docs/2025-06-18/learn/architecture)**
99>
100> Dive deeper into how MCP works and its architecture
117101 
118102Remote 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.
119103 

docs/2025-06-18/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/2025-06-18/develop/build-server">
36 Create MCP servers to expose your data and tools
37 </Card>
32> **[Build servers](/docs/2025-06-18/develop/build-server)**
33>
34> Create MCP servers to expose your data and tools
3835 
39 <Card title="Build clients" icon="computer" href="/docs/2025-06-18/develop/build-client">
40 Develop applications that connect to MCP servers
41 </Card>
36> **[Build clients](/docs/2025-06-18/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/2025-06-18/learn/architecture">
52 Learn the core concepts and architecture of MCP
53 </Card>
46> **[Architecture](/docs/2025-06-18/learn/architecture)**
47>
48> Learn the core concepts and architecture of MCP
5449 
55 <Card title="Security" icon="lock" href="/docs/2025-06-18/tutorials/security/security_best_practices">
56 Understand the security considerations and best practices for MCP
57 </Card>
58</CardGroup>
50> **[Security](/docs/2025-06-18/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/2025-06-18/learn/architecture Changed · +304 / -312 lines

#### Understanding the Initialization 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 #### 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#### Lifecycle management
106106 
107MCP is a <Tooltip tip="A subset of MCP can be made stateless using the Streamable HTTP transport">stateful protocol</Tooltip> that requires lifecycle management. The purpose of lifecycle management is to negotiate the <Tooltip tip="Features and operations that a client or server supports, such as tools, resources, or prompts">capabilities</Tooltip> that both client and server support. Detailed information can be found in the [specification](/specification/2025-06-18/basic/lifecycle), and the [example](#example) showcases the initialization sequence.
107MCP is a stateful protocol (A subset of MCP can be made stateless using the Streamable HTTP transport) that requires lifecycle management. The purpose of lifecycle management is to negotiate the capabilities (Features and operations that a client or server supports, such as tools, resources, or prompts) that both client and server support. Detailed information can be found in the [specification](/specification/2025-06-18/basic/lifecycle), and the [example](#example) showcases the initialization sequence.
108108 
109109#### Primitives
110110 
from line 145
145145 
146146This section provides a step-by-step walkthrough of an MCP client-server interaction, focusing on the data layer protocol. We'll demonstrate the lifecycle sequence, tool operations, and notifications using JSON-RPC 2.0 messages.
147147 
148<Steps>
149 <Step title="Initialization (Lifecycle Management)">
150 MCP begins with lifecycle management through a capability negotiation handshake. As described in the [lifecycle management](#lifecycle-management) section, the client sends an `initialize` request to establish the connection and negotiate supported features.
151 
152 <CodeGroup>
153 ```json Initialize Request theme={null}
148**1. Initialization (Lifecycle Management)**
149 
150MCP begins with lifecycle management through a capability negotiation handshake. As described in the [lifecycle management](#lifecycle-management) section, the client sends an `initialize` request to establish the connection and negotiate supported features.
151 
152```json Initialize Request theme={null}
153{
154 "jsonrpc": "2.0",
155 "id": 1,
156 "method": "initialize",
157 "params": {
158 "protocolVersion": "2025-06-18",
159 "capabilities": {
160 "elicitation": {}
161 },
162 "clientInfo": {
163 "name": "example-client",
164 "version": "1.0.0"
165 }
166 }
167}
168```
169 
170```json Initialize Response theme={null}
171{
172 "jsonrpc": "2.0",
173 "id": 1,
174 "result": {
175 "protocolVersion": "2025-06-18",
176 "capabilities": {
177 "tools": {
178 "listChanged": true
179 },
180 "resources": {}
181 },
182 "serverInfo": {
183 "name": "example-server",
184 "version": "1.0.0"
185 }
186 }
187}
188```
189 
190#### Understanding the Initialization Exchange
191 
192The initialization process is a key part of MCP's lifecycle management and serves several critical purposes:
193 
1941. **Protocol Version Negotiation**: The `protocolVersion` field (e.g., "2025-06-18") ensures both client and server are using compatible protocol versions. This prevents communication errors that could occur when different versions attempt to interact. If a mutually compatible version is not negotiated, the connection should be terminated.
195 
1962. **Capability Discovery**: The `capabilities` object allows each party to declare what features they support, including which [primitives](#primitives) they can handle (tools, resources, prompts) and whether they support features like [notifications](#notifications). This enables efficient communication by avoiding unsupported operations.
197 
1983. **Identity Exchange**: The `clientInfo` and `serverInfo` objects provide identification and versioning information for debugging and compatibility purposes.
199 
200In this example, the capability negotiation demonstrates how MCP primitives are declared:
201 
202**Client Capabilities**:
203 
204* `"elicitation": {}` - The client declares it can work with user interaction requests (can receive `elicitation/create` method calls)
205 
206**Server Capabilities**:
207 
208* `"tools": {"listChanged": true}` - The server supports the tools primitive AND can send `tools/list_changed` notifications when its tool list changes
209* `"resources": {}` - The server also supports the resources primitive (can handle `resources/list` and `resources/read` methods)
210 
211After successful initialization, the client sends a notification to indicate it's ready:
212 
213```json Notification theme={null}
214{
215 "jsonrpc": "2.0",
216 "method": "notifications/initialized"
217}
218```
219 
220#### How This Works in AI Applications
221 
222During initialization, the AI application's MCP client manager establishes connections to configured servers and stores their 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.
223 
224```python Pseudo-code for AI application initialization theme={null}
225# Pseudo Code
226async with stdio_client(server_config) as (read, write):
227 async with ClientSession(read, write) as session:
228 init_response = await session.initialize()
229 if init_response.capabilities.tools:
230 app.register_mcp_server(session, supports_tools=True)
231 app.set_server_ready(session)
232```
233 
234**2. Tool Discovery (Primitives)**
235 
236Now that the connection is established, 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.
237 
238```json Tools List Request theme={null}
239{
240 "jsonrpc": "2.0",
241 "id": 2,
242 "method": "tools/list"
243}
244```
245 
246```json Tools List Response theme={null}
247{
248 "jsonrpc": "2.0",
249 "id": 2,
250 "result": {
251 "tools": [
154252 {
155 "jsonrpc": "2.0",
156 "id": 1,
157 "method": "initialize",
158 "params": {
159 "protocolVersion": "2025-06-18",
160 "capabilities": {
161 "elicitation": {}
253 "name": "calculator_arithmetic",
254 "title": "Calculator",
255 "description": "Perform mathematical calculations including basic arithmetic, trigonometric functions, and algebraic operations",
256 "inputSchema": {
257 "type": "object",
258 "properties": {
259 "expression": {
260 "type": "string",
261 "description": "Mathematical expression to evaluate (e.g., '2 + 3 * 4', 'sin(30)', 'sqrt(16)')"
262 }
162263 },
163 "clientInfo": {
164 "name": "example-client",
165 "version": "1.0.0"
166 }
264 "required": ["expression"]
265 }
266 },
267 {
268 "name": "weather_current",
269 "title": "Weather Information",
270 "description": "Get current weather information for any location worldwide",
271 "inputSchema": {
272 "type": "object",
273 "properties": {
274 "location": {
275 "type": "string",
276 "description": "City name, address, or coordinates (latitude,longitude)"
277 },
278 "units": {
279 "type": "string",
280 "enum": ["metric", "imperial", "kelvin"],
281 "description": "Temperature units to use in response",
282 "default": "metric"
283 }
284 },
285 "required": ["location"]
167286 }
168287 }
169 ```
170 
171 ```json Initialize Response theme={null}
288 ]
289 }
290}
291```
292 
293#### Understanding the Tool Discovery Request
294 
295The `tools/list` request is simple, containing no parameters.
296 
297#### Understanding the Tool Discovery Response
298 
299The 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.
300 
301Each tool object in the response includes several key fields:
302 
303* **`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`)
304* **`title`**: A human-readable display name for the tool that clients can show to users
305* **`description`**: Detailed explanation of what the tool does and when to use it
306* **`inputSchema`**: A JSON Schema that defines the expected input parameters, enabling type validation and providing clear documentation about required and optional parameters
307 
308#### How This Works in AI Applications
309 
310The 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.
311 
312```python Pseudo-code for AI application tool discovery theme={null}
313# Pseudo-code using MCP Python SDK patterns
314available_tools = []
315for session in app.mcp_server_sessions():
316 tools_response = await session.list_tools()
317 available_tools.extend(tools_response.tools)
318conversation.register_available_tools(available_tools)
319```
320 
321**3. Tool Execution (Primitives)**
322 
323The 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.
324 
325#### Understanding the Tool Execution Request
326 
327The `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:
328 
329```json Tool Call Request theme={null}
330{
331 "jsonrpc": "2.0",
332 "id": 3,
333 "method": "tools/call",
334 "params": {
335 "name": "weather_current",
336 "arguments": {
337 "location": "San Francisco",
338 "units": "imperial"
339 }
340 }
341}
342```
343 
344```json Tool Call Response theme={null}
345{
346 "jsonrpc": "2.0",
347 "id": 3,
348 "result": {
349 "content": [
172350 {
173 "jsonrpc": "2.0",
174 "id": 1,
175 "result": {
176 "protocolVersion": "2025-06-18",
177 "capabilities": {
178 "tools": {
179 "listChanged": true
180 },
181 "resources": {}
182 },
183 "serverInfo": {
184 "name": "example-server",
185 "version": "1.0.0"
186 }
187 }
351 "type": "text",
352 "text": "Current weather in San Francisco: 68°F, partly cloudy with light winds from the west at 8 mph. Humidity: 65%"
188353 }
189 ```
190 </CodeGroup>
191 
192 #### Understanding the Initialization Exchange
193 
194 The initialization process is a key part of MCP's lifecycle management and serves several critical purposes:
195 
196 1. **Protocol Version Negotiation**: The `protocolVersion` field (e.g., "2025-06-18") ensures both client and server are using compatible protocol versions. This prevents communication errors that could occur when different versions attempt to interact. If a mutually compatible version is not negotiated, the connection should be terminated.
197 
198 2. **Capability Discovery**: The `capabilities` object allows each party to declare what features they support, including which [primitives](#primitives) they can handle (tools, resources, prompts) and whether they support features like [notifications](#notifications). This enables efficient communication by avoiding unsupported operations.
199 
200 3. **Identity Exchange**: The `clientInfo` and `serverInfo` objects provide identification and versioning information for debugging and compatibility purposes.
201 
202 In this example, the capability negotiation demonstrates how MCP primitives are declared:
203 
204 **Client Capabilities**:
205 
206 * `"elicitation": {}` - The client declares it can work with user interaction requests (can receive `elicitation/create` method calls)
207 
208 **Server Capabilities**:
209 
210 * `"tools": {"listChanged": true}` - The server supports the tools primitive AND can send `tools/list_changed` notifications when its tool list changes
211 * `"resources": {}` - The server also supports the resources primitive (can handle `resources/list` and `resources/read` methods)
212 
213 After successful initialization, the client sends a notification to indicate it's ready:
214 
215 ```json Notification theme={null}
216 {
217 "jsonrpc": "2.0",
218 "method": "notifications/initialized"
219 }
220 ```
221 
222 #### How This Works in AI Applications
223 
224 During initialization, the AI application's MCP client manager establishes connections to configured servers and stores their 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.
225 
226 ```python Pseudo-code for AI application initialization theme={null}
227 # Pseudo Code
228 async with stdio_client(server_config) as (read, write):
229 async with ClientSession(read, write) as session:
230 init_response = await session.initialize()
231 if init_response.capabilities.tools:
232 app.register_mcp_server(session, supports_tools=True)
233 app.set_server_ready(session)
234 ```
235 </Step>
236 
237 <Step title="Tool Discovery (Primitives)">
238 Now that the connection is established, 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.
239 
240 <CodeGroup>
241 ```json Tools List Request theme={null}
242 {
243 "jsonrpc": "2.0",
244 "id": 2,
245 "method": "tools/list"
246 }
247 ```
248 
249 ```json Tools List Response theme={null}
250 {
251 "jsonrpc": "2.0",
252 "id": 2,
253 "result": {
254 "tools": [
255 {
256 "name": "calculator_arithmetic",
257 "title": "Calculator",
258 "description": "Perform mathematical calculations including basic arithmetic, trigonometric functions, and algebraic operations",
259 "inputSchema": {
260 "type": "object",
261 "properties": {
262 "expression": {
263 "type": "string",
264 "description": "Mathematical expression to evaluate (e.g., '2 + 3 * 4', 'sin(30)', 'sqrt(16)')"
265 }
266 },
267 "required": ["expression"]
268 }
269 },
270 {
271 "name": "weather_current",
272 "title": "Weather Information",
273 "description": "Get current weather information for any location worldwide",
274 "inputSchema": {
275 "type": "object",
276 "properties": {
277 "location": {
278 "type": "string",
279 "description": "City name, address, or coordinates (latitude,longitude)"
280 },
281 "units": {
282 "type": "string",
283 "enum": ["metric", "imperial", "kelvin"],
284 "description": "Temperature units to use in response",
285 "default": "metric"
286 }
287 },
288 "required": ["location"]
289 }
290 }
291 ]
292 }
293 }
294 ```
295 </CodeGroup>
296 
297 #### Understanding the Tool Discovery Request
298 
299 The `tools/list` request is simple, containing no parameters.
300 
301 #### Understanding the Tool Discovery Response
302 
303 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.
304 
305 Each tool object in the response includes several key fields:
306 
307 * **`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`)
308 * **`title`**: A human-readable display name for the tool that clients can show to users
309 * **`description`**: Detailed explanation of what the tool does and when to use it
310 * **`inputSchema`**: A JSON Schema that defines the expected input parameters, enabling type validation and providing clear documentation about required and optional parameters
311 
312 #### How This Works in AI Applications
313 
314 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.
315 
316 ```python Pseudo-code for AI application tool discovery theme={null}
317 # Pseudo-code using MCP Python SDK patterns
318 available_tools = []
319 for session in app.mcp_server_sessions():
320 tools_response = await session.list_tools()
321 available_tools.extend(tools_response.tools)
322 conversation.register_available_tools(available_tools)
323 ```
324 </Step>
325 
326 <Step title="Tool Execution (Primitives)">
327 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.
328 
329 #### Understanding the Tool Execution Request
330 
331 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:
332 
333 <CodeGroup>
334 ```json Tool Call Request theme={null}
335 {
336 "jsonrpc": "2.0",
337 "id": 3,
338 "method": "tools/call",
339 "params": {
340 "name": "weather_current",
341 "arguments": {
342 "location": "San Francisco",
343 "units": "imperial"
344 }
345 }
346 }
347 ```
348 
349 ```json Tool Call Response theme={null}
350 {
351 "jsonrpc": "2.0",
352 "id": 3,
353 "result": {
354 "content": [
355 {
356 "type": "text",
357 "text": "Current weather in San Francisco: 68°F, partly cloudy with light winds from the west at 8 mph. Humidity: 65%"
358 }
359 ]
360 }
361 }
362 ```
363 </CodeGroup>
364 
365 #### Key Elements of Tool Execution
366 
367 The request structure includes several important components:
368 
369 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.
370 
371 2. **`arguments`**: Contains the input parameters as defined by the tool's `inputSchema`. In this example:
372 * `location`: "San Francisco" (required parameter)
373 * `units`: "imperial" (optional parameter, defaults to "metric" if not specified)
374 
375 3. **JSON-RPC Structure**: Uses standard JSON-RPC 2.0 format with unique `id` for request-response correlation.
376 
377 #### Understanding the Tool Execution Response
378 
379 The response demonstrates MCP's flexible content system:
380 
381 1. **`content` Array**: Tool responses return an array of content objects, allowing for rich, multi-format responses (text, images, resources, etc.)
382 
383 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.
384 
385 3. **Structured Output**: The response provides actionable information that the AI application can use as context for language model interactions.
386 
387 This execution pattern allows AI applications to dynamically invoke server functionality and receive structured responses that can be integrated into conversations with language models.
388 
389 #### How This Works in AI Applications
390 
391 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.
392 
393 ```python theme={null}
394 # Pseudo-code for AI application tool execution
395 async def handle_tool_call(conversation, tool_name, arguments):
396 session = app.find_mcp_session_for_tool(tool_name)
397 result = await session.call_tool(tool_name, arguments)
398 conversation.add_tool_result(result.content)
399 ```
400 </Step>
401 
402 <Step title="Real-time Updates (Notifications)">
403 MCP supports real-time notifications that enable servers to inform clients about changes without being explicitly requested. This demonstrates the notification system, a key feature that keeps MCP connections synchronized and responsive.
404 
405 #### Understanding Tool List Change Notifications
406 
407 When the server's available tools change—such as when new functionality becomes available, existing tools are modified, or tools become temporarily unavailable—the server can proactively notify connected clients:
408 
409 ```json Request theme={null}
410 {
411 "jsonrpc": "2.0",
412 "method": "notifications/tools/list_changed"
413 }
414 ```
415 
416 #### Key Features of MCP Notifications
417 
418 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.
419 
420 2. **Capability-Based**: This notification is only sent by servers that declared `"listChanged": true` in their tools capability during initialization (as shown in Step 1).
421 
422 3. **Event-Driven**: The server decides when to send notifications based on internal state changes, making MCP connections dynamic and responsive.
423 
424 #### Client Response to Notifications
425 
426 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:
427 
428 ```json Request theme={null}
429 {
430 "jsonrpc": "2.0",
431 "id": 4,
432 "method": "tools/list"
433 }
434 ```
435 
436 #### Why Notifications Matter
437 
438 This notification system is crucial for several reasons:
439 
440 1. **Dynamic Environments**: Tools may come and go based on server state, external dependencies, or user permissions
441 2. **Efficiency**: Clients don't need to poll for changes; they're notified when updates occur
442 3. **Consistency**: Ensures clients always have accurate information about available server capabilities
443 4. **Real-time Collaboration**: Enables responsive AI applications that can adapt to changing contexts
444 
445 This notification pattern extends beyond tools to other MCP primitives, enabling comprehensive real-time synchronization between clients and servers.
446 
447 #### How This Works in AI Applications
448 
449 When the AI application receives a notification about changed tools, 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.
450 
451 ```python theme={null}
452 # Pseudo-code for AI application notification handling
453 async def handle_tools_changed_notification(session):
454 tools_response = await session.list_tools()
455 app.update_available_tools(session, tools_response.tools)
456 if app.conversation.is_active():
457 app.conversation.notify_llm_of_new_capabilities()
458 ```
459 </Step>
460</Steps>
354 ]
355 }
356}
357```
358 
359#### Key Elements of Tool Execution
360 
361The request structure includes several important components:
362 
3631. **`name`**: Must match exactly the tool name from the discovery response (`weather_current`). This ensures the server can correctly identify which tool to execute.
364 
3652. **`arguments`**: Contains the input parameters as defined by the tool's `inputSchema`. In this example:
366 * `location`: "San Francisco" (required parameter)
367 * `units`: "imperial" (optional parameter, defaults to "metric" if not specified)
368 
3693. **JSON-RPC Structure**: Uses standard JSON-RPC 2.0 format with unique `id` for request-response correlation.
370 
371#### Understanding the Tool Execution Response
372 
373The response demonstrates MCP's flexible content system:
374 
3751. **`content` Array**: Tool responses return an array of content objects, allowing for rich, multi-format responses (text, images, resources, etc.)
376 
3772. **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.
378 
3793. **Structured Output**: The response provides actionable information that the AI application can use as context for language model interactions.
380 
381This execution pattern allows AI applications to dynamically invoke server functionality and receive structured responses that can be integrated into conversations with language models.
382 
383#### How This Works in AI Applications
384 
385When 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.
386 
387```python theme={null}
388# Pseudo-code for AI application tool execution
389async def handle_tool_call(conversation, tool_name, arguments):
390 session = app.find_mcp_session_for_tool(tool_name)
391 result = await session.call_tool(tool_name, arguments)
392 conversation.add_tool_result(result.content)
393```
394 
395**4. Real-time Updates (Notifications)**
396 
397MCP supports real-time notifications that enable servers to inform clients about changes without being explicitly requested. This demonstrates the notification system, a key feature that keeps MCP connections synchronized and responsive.
398 
399#### Understanding Tool List Change Notifications
400 
401When the server's available tools change—such as when new functionality becomes available, existing tools are modified, or tools become temporarily unavailable—the server can proactively notify connected clients:
402 
403```json Request theme={null}
404{
405 "jsonrpc": "2.0",
406 "method": "notifications/tools/list_changed"
407}
408```
409 
410#### Key Features of MCP Notifications
411 
4121. **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.
413 
4142. **Capability-Based**: This notification is only sent by servers that declared `"listChanged": true` in their tools capability during initialization (as shown in Step 1).
415 
4163. **Event-Driven**: The server decides when to send notifications based on internal state changes, making MCP connections dynamic and responsive.
417 
418#### Client Response to Notifications
419 
420Upon 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:
421 
422```json Request theme={null}
423{
424 "jsonrpc": "2.0",
425 "id": 4,
426 "method": "tools/list"
427}
428```
429 
430#### Why Notifications Matter
431 
432This notification system is crucial for several reasons:
433 
4341. **Dynamic Environments**: Tools may come and go based on server state, external dependencies, or user permissions
4352. **Efficiency**: Clients don't need to poll for changes; they're notified when updates occur
4363. **Consistency**: Ensures clients always have accurate information about available server capabilities
4374. **Real-time Collaboration**: Enables responsive AI applications that can adapt to changing contexts
438 
439This notification pattern extends beyond tools to other MCP primitives, enabling comprehensive real-time synchronization between clients and servers.
440 
441#### How This Works in AI Applications
442 
443When the AI application receives a notification about changed tools, 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.
444 
445```python theme={null}
446# Pseudo-code for AI application notification handling
447async def handle_tools_changed_notification(session):
448 tools_response = await session.list_tools()
449 app.update_available_tools(session, tools_response.tools)
450 if app.conversation.is_active():
451 app.conversation.notify_llm_of_new_capabilities()
452```
461453 

docs/2025-06-18/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 
Feedback