Follow Discord
Sweep 30 Oct 2026 · 21:36Z Build v2.1.315 533 read Stable v2.1.308 Latest v2.1.314 Next v2.1.315 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One capture · mcp

One read of Model Context Protocolmcp-20261030T183718Z

91 pages moved out of 359 read.

Pages moved 91 significant first
Pages read 359 in this capture
Captured 18:37 UTC
Corpus hash 972f3bd44853 corpus-hash

What this read moved

26-50 of 91, page 2 of 4

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

docs/2025-06-18/tools/debugging Changed · +3 / -3 lines

from line 327
327327 
328328## Next steps
329329 
330> **[MCP Inspector](https://modelcontextprotocol.io/docs/2025-06-18/tools/inspector.md)**
330> [card] [MCP Inspector](https://modelcontextprotocol.io/docs/2025-06-18/tools/inspector.md)
331331>
332332> Learn to use the MCP Inspector
333333 
334> **[Build an MCP server](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-server.md)**
334> [card] [Build an MCP server](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-server.md)
335335>
336336> Walk through building a server from scratch
337337 
338> **[Connect local servers](https://modelcontextprotocol.io/docs/2025-06-18/develop/connect-local-servers.md)**
338> [card] [Connect local servers](https://modelcontextprotocol.io/docs/2025-06-18/develop/connect-local-servers.md)
339339>
340340> Full claude\_desktop\_config.json reference and troubleshooting
341341 

docs/2025-06-18/tools/inspector Changed · +31 / -31 lines

from line 22
2222 
2323A common way to start server packages from [npm](https://npmjs.com) or [PyPI](https://pypi.org).
2424 
25**npm package**
25> [tab] npm package
26>
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> ```
2632 
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```
33> [tab] PyPI package
34>
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> ```
3240 
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 
4141#### Inspecting locally developed servers
4242 
4343To inspect servers locally developed or downloaded as a repository, the most common
4444way is:
4545 
46**TypeScript**
46> [tab] TypeScript
47>
48> ```bash theme={null}
49> npx @modelcontextprotocol/inspector node path/to/server/index.js args...
50> ```
4751 
48```bash theme={null}
49npx @modelcontextprotocol/inspector node path/to/server/index.js args...
50```
52> [tab] Python
53>
54> ```bash theme={null}
55> npx @modelcontextprotocol/inspector \
56> uv \
57> --directory path/to/server \
58> run \
59> package-name \
60> args...
61> ```
5162 
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 
6363Please carefully read any attached README for the most accurate instructions.
6464 
6565## Feature overview
from line 125
125125 
126126## Next steps
127127 
128> **[Inspector Repository](https://github.com/modelcontextprotocol/inspector)**
128> [card] [Inspector Repository](https://github.com/modelcontextprotocol/inspector)
129129>
130130> Check out the MCP Inspector source code
131131 
132> **[Debugging Guide](https://modelcontextprotocol.io/docs/2025-06-18/tools/debugging.md)**
132> [card] [Debugging Guide](https://modelcontextprotocol.io/docs/2025-06-18/tools/debugging.md)
133133>
134134> Learn about broader debugging strategies
135135 

docs/2025-06-18/tutorials/security/authorization Changed · +727 / -727 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 246
246246 
247247For 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.
248248 
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`,
330 );
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");
370 }
371 
372 const params = new URLSearchParams({
373 token: token,
374 client_id: CONFIG.auth.clientId,
375 });
376 
377 if (CONFIG.auth.clientSecret) {
378 params.set("client_secret", CONFIG.auth.clientSecret);
379 }
380 
381 let response: Response;
382 try {
383 response = await fetch(endpoint, {
384 method: "POST",
385 headers: {
386 "Content-Type": "application/x-www-form-urlencoded",
387 },
388 body: params.toString(),
389 });
390 } catch (e) {
391 console.error("[auth] introspection fetch threw", e);
392 throw e;
393 }
394 
395 if (!response.ok) {
396 const txt = await response.text();
397 console.error("[auth] introspection non-OK", { status: response.status });
398 
399 try {
400 const obj = JSON.parse(txt);
401 console.log(JSON.stringify(obj, null, 2));
402 } catch {
403 console.error(txt);
404 }
405 throw new Error(`Invalid or expired token: ${txt}`);
406 }
407 
408 let data: any;
409 try {
410 data = await response.json();
411 } catch (e) {
412 const txt = await response.text();
413 console.error("[auth] failed to parse introspection JSON", {
414 error: String(e),
415 body: txt,
416 });
417 throw e;
418 }
419 
420 if (data.active === false) {
421 throw new Error("Inactive token");
422 }
423 
424 if (!data.aud) {
425 throw new Error("Resource indicator (aud) missing");
426 }
427 
428 const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
429 const allowed = audiences.some((a) =>
430 checkResourceAllowed({
431 requestedResource: a,
432 configuredResource: mcpServerUrl,
433 }),
434 );
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,
446 };
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()
708 }
709 
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()
727 }
728 
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;
920 }
921 };
922 
923 options.RequireHttpsMetadata = false; // Set to true in production
924 
925 options.Events = new JwtBearerEvents
926 {
927 OnTokenValidated = context =>
928 {
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;
933 },
934 OnAuthenticationFailed = context =>
935 {
936 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
937 return Task.CompletedTask;
938 },
939 };
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"]
949 };
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).
249> [tab] TypeScript
250>
251> You can see the complete TypeScript project in the [sample repository](https://github.com/localden/min-ts-mcp-auth).
252>
253> Prior 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
257> HOST=localhost
258> PORT=3000
259>
260> # Auth server location
261> AUTH_HOST=localhost
262> AUTH_PORT=8080
263> AUTH_REALM=master
264>
265> # Keycloak OAuth client credentials
266> OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
267> OAUTH_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>
272> 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.
273>
274> ```typescript theme={null}
275> import "dotenv/config";
276> import express from "express";
277> import { randomUUID } from "node:crypto";
278> import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
279> import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
280> import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
281> import { z } from "zod";
282> import cors from "cors";
283> import {
284> mcpAuthMetadataRouter,
285> getOAuthProtectedResourceMetadataUrl,
286> } from "@modelcontextprotocol/sdk/server/auth/router.js";
287> import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
288> import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
289> import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
290> const 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>
302> function 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>
323> function 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`,
330> );
331> });
332> next();
333> };
334> }
335>
336> const app = express();
337>
338> app.use(
339> express.json({
340> verify: (req: any, _res, buf) => {
341> req.rawBody = buf?.toString() ?? "";
342> },
343> }),
344> );
345>
346> app.use(
347> cors({
348> origin: "*",
349> exposedHeaders: ["Mcp-Session-Id"],
350> }),
351> );
352>
353> app.use(createRequestLogger());
354>
355> const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
356> const oauthUrls = createOAuthUrls();
357>
358> const oauthMetadata: OAuthMetadata = {
359> ...oauthUrls,
360> response_types_supported: ["code"],
361> };
362>
363> const tokenVerifier = {
364> verifyAccessToken: async (token: string) => {
365> const endpoint = oauthMetadata.introspection_endpoint;
366>
367> if (!endpoint) {
368> console.error("[auth] no introspection endpoint in metadata");
369> throw new Error("No token verification endpoint available in metadata");
370> }
371>
372> const params = new URLSearchParams({
373> token: token,
374> client_id: CONFIG.auth.clientId,
375> });
376>
377> if (CONFIG.auth.clientSecret) {
378> params.set("client_secret", CONFIG.auth.clientSecret);
379> }
380>
381> let response: Response;
382> try {
383> response = await fetch(endpoint, {
384> method: "POST",
385> headers: {
386> "Content-Type": "application/x-www-form-urlencoded",
387> },
388> body: params.toString(),
389> });
390> } catch (e) {
391> console.error("[auth] introspection fetch threw", e);
392> throw e;
393> }
394>
395> if (!response.ok) {
396> const txt = await response.text();
397> console.error("[auth] introspection non-OK", { status: response.status });
398>
399> try {
400> const obj = JSON.parse(txt);
401> console.log(JSON.stringify(obj, null, 2));
402> } catch {
403> console.error(txt);
404> }
405> throw new Error(`Invalid or expired token: ${txt}`);
406> }
407>
408> let data: any;
409> try {
410> data = await response.json();
411> } catch (e) {
412> const txt = await response.text();
413> console.error("[auth] failed to parse introspection JSON", {
414> error: String(e),
415> body: txt,
416> });
417> throw e;
418> }
419>
420> if (data.active === false) {
421> throw new Error("Inactive token");
422> }
423>
424> if (!data.aud) {
425> throw new Error("Resource indicator (aud) missing");
426> }
427>
428> const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
429> const allowed = audiences.some((a) =>
430> checkResourceAllowed({
431> requestedResource: a,
432> configuredResource: mcpServerUrl,
433> }),
434> );
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,
446> };
447> },
448> };
449> app.use(
450> mcpAuthMetadataRouter({
451> oauthMetadata,
452> resourceServerUrl: mcpServerUrl,
453> scopesSupported: ["mcp:tools"],
454> resourceName: "MCP Demo Server",
455> }),
456> );
457>
458> const authMiddleware = requireBearerAuth({
459> verifier: tokenVerifier,
460> requiredScopes: [],
461> resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
462> });
463>
464> const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
465>
466> function 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>
505> const 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>
542> const 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>
556> app.post("/", authMiddleware, mcpPostHandler);
557> app.get("/", authMiddleware, handleSessionRequest);
558> app.delete("/", authMiddleware, handleSessionRequest);
559>
560> app.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>
569> When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
570>
571> For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
572 
573> [tab] Python
574>
575> You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
576>
577> 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.
578>
579> 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:
580>
581> ```python theme={null}
582> """Configuration settings for the MCP auth server."""
583>
584> import os
585> from typing import Optional
586>
587>
588> class 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
626> config = Config()
627>
628> ```
629>
630> The server implementation is as follows:
631>
632> ```python theme={null}
633> import datetime
634> import logging
635> from typing import Any
636>
637> from pydantic import AnyHttpUrl
638>
639> from mcp.server.auth.settings import AuthSettings
640> from mcp.server.fastmcp.server import FastMCP
641>
642> from .config import config
643> from .token_verifier import IntrospectionTokenVerifier
644>
645> logger = logging.getLogger(__name__)
646>
647>
648> def 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>
662> def 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()
708> }
709>
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()
727> }
728>
729> return app
730>
731>
732> def 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>
768> if __name__ == "__main__":
769> exit(main())
770> ```
771>
772> 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
773>
774> ```python theme={null}
775> """Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
776>
777> import logging
778> from typing import Any
779>
780> from mcp.server.auth.provider import AccessToken, TokenVerifier
781> from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
782>
783> logger = logging.getLogger(__name__)
784>
785>
786> class 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>
876> For more details, see the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
877 
878> [tab] C#
879>
880> You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
881>
882> 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.
883>
884> ```csharp theme={null}
885> using Microsoft.AspNetCore.Authentication.JwtBearer;
886> using Microsoft.IdentityModel.Tokens;
887> using ModelContextProtocol.AspNetCore.Authentication;
888> using ProtectedMcpServer.Tools;
889> using System.Security.Claims;
890>
891> var builder = WebApplication.CreateBuilder(args);
892>
893> var serverUrl = "http://localhost:3000/";
894> var authorizationServerUrl = "http://localhost:8080/realms/master/";
895>
896> builder.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;
920> }
921> };
922>
923> options.RequireHttpsMetadata = false; // Set to true in production
924>
925> options.Events = new JwtBearerEvents
926> {
927> OnTokenValidated = context =>
928> {
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;
933> },
934> OnAuthenticationFailed = context =>
935> {
936> Console.WriteLine($"Authentication failed: {context.Exception.Message}");
937> return Task.CompletedTask;
938> },
939> };
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"]
949> };
950> });
951>
952> builder.Services.AddAuthorization();
953>
954> builder.Services.AddHttpContextAccessor();
955> builder.Services.AddMcpServer()
956> .WithTools<MathTools>()
957> .WithHttpTransport();
958>
959> var app = builder.Build();
960>
961> app.UseAuthentication();
962> app.UseAuthorization();
963>
964> app.MapMcp().RequireAuthorization();
965>
966> Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
967> Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
968> Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
969> Console.WriteLine("Exposed Math tools: Add, Multiply");
970> Console.WriteLine("Press Ctrl+C to stop the server");
971>
972> app.Run(serverUrl);
973> ```
974>
975> For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
976976 
977977## Testing the MCP Server
978978 
979979 

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

docs/2025-11-25/develop/build-server Changed · +2784 / -2784 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 24
2424 
2525This tutorial will primarily focus on tools.
2626 
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)
183 """
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](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md) 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](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md) 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 ->
1212 """
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 ->
1228 """
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](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md) 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.";
1565 }
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](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md) 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)"
1818 }
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](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md) 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(),
2221 }
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(),
2248 }
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()
2295 }
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](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md) 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](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md) 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**.
27> [tab] Python
28>
29> 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)
30>
31> ### Prerequisite knowledge
32>
33> This quickstart assumes you have familiarity with:
34>
35> * Python
36> * LLMs like Claude
37>
38> ### Logging in MCP Servers
39>
40> When 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}
53> import sys
54> import logging
55>
56> # ❌ Bad (STDIO)
57> print("Processing request")
58>
59> # ✅ Good (STDIO)
60> print("Processing request", file=sys.stderr)
61>
62> # ✅ Good (STDIO)
63> logging.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>
73> First, let's install `uv` and set up our Python project and environment:
74>
75> ```bash macOS/Linux theme={null}
76> curl -LsSf https://astral.sh/uv/install.sh | sh
77> ```
78>
79> ```powershell Windows theme={null}
80> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
81> ```
82>
83> Make sure to restart your terminal afterwards to ensure that the `uv` command gets picked up.
84>
85> Now, let's create and set up our project:
86>
87> ```bash macOS/Linux theme={null}
88> # Create a new directory for our project
89> uv init weather
90> cd weather
91>
92> # Create virtual environment and activate it
93> uv venv
94> source .venv/bin/activate
95>
96> # Install dependencies
97> uv add "mcp[cli]" httpx
98>
99> # Create our server file
100> touch weather.py
101> ```
102>
103> ```powershell Windows theme={null}
104> # Create a new directory for our project
105> uv init weather
106> cd weather
107>
108> # Create virtual environment and activate it
109> uv venv
110> .venv\Scripts\activate
111>
112> # Install dependencies
113> uv add mcp[cli] httpx
114>
115> # Create our server file
116> new-item weather.py
117> ```
118>
119> Now let's dive into building your server.
120>
121> ## Building your server
122>
123> ### Importing packages and setting up the instance
124>
125> Add these to the top of your `weather.py`:
126>
127> ```python theme={null}
128> from typing import Any
129>
130> import httpx
131> from mcp.server.fastmcp import FastMCP
132>
133> # Initialize FastMCP server
134> mcp = FastMCP("weather")
135>
136> # Constants
137> NWS_API_BASE = "https://api.weather.gov"
138> USER_AGENT = "weather-app/1.0"
139> ```
140>
141> The 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>
145> Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
146>
147> ```python theme={null}
148> async 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>
160> def format_alert(feature: dict) -> str:
161> """Format an alert feature into a readable string."""
162> props = feature["properties"]
163> return f"""
164> Event: {props.get("event", "Unknown")}
165> Area: {props.get("areaDesc", "Unknown")}
166> Severity: {props.get("severity", "Unknown")}
167> Description: {props.get("description", "No description available")}
168> Instructions: {props.get("instruction", "No specific instructions provided")}
169> """
170> ```
171>
172> ### Implementing tool execution
173>
174> The 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()
178> async 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)
183> """
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()
198> async 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"]}:
225> Temperature: {period["temperature"]}°{period["temperatureUnit"]}
226> Wind: {period["windSpeed"]} {period["windDirection"]}
227> Forecast: {period["detailedForecast"]}
228> """
229> forecasts.append(forecast)
230>
231> return "\n---\n".join(forecasts)
232> ```
233>
234> ### Running the server
235>
236> Finally, let's initialize and run the server:
237>
238> ```python theme={null}
239> def main():
240> # Initialize and run the server
241> mcp.run(transport="stdio")
242>
243>
244> if __name__ == "__main__":
245> main()
246> ```
247>
248> Your server is complete! Run `uv run weather.py` to start the MCP server, which will listen for messages from MCP hosts.
249>
250> Let'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](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
257>
258> First, make sure you have Claude for Desktop installed. [You can install the latest version
259> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
260>
261> 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.
262>
263> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
264>
265> ```bash macOS/Linux theme={null}
266> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
267> ```
268>
269> ```powershell Windows theme={null}
270> code $env:AppData\Claude\claude_desktop_config.json
271> ```
272>
273> 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.
274>
275> In 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>
317> This tells Claude for Desktop:
318>
319> 1. There's an MCP server named "weather"
320> 2. To launch it by running `uv --directory /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather run weather.py`
321>
322> Save the file, and restart **Claude for Desktop**.
323 
324> [tab] TypeScript
325>
326> 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)
327>
328> ### Prerequisite knowledge
329>
330> This quickstart assumes you have familiarity with:
331>
332> * TypeScript
333> * LLMs like Claude
334>
335> ### Logging in MCP Servers
336>
337> When 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)
351> console.log("Server started");
352>
353> // ✅ Good (STDIO)
354> console.error("Server started"); // stderr is safe
355> ```
356>
357> ### System requirements
358>
359> For TypeScript, make sure you have the latest version of Node installed.
360>
361> ### Set up your environment
362>
363> First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/).
364> Verify your Node.js installation:
365>
366> ```bash theme={null}
367> node --version
368> npm --version
369> ```
370>
371> For this tutorial, you'll need Node.js version 16 or higher.
372>
373> Now, let's create and set up our project:
374>
375> ```bash macOS/Linux theme={null}
376> # Create a new directory for our project
377> mkdir weather
378> cd weather
379>
380> # Initialize a new npm project
381> npm init -y
382>
383> # Install dependencies
384> npm install @modelcontextprotocol/sdk zod@3
385> npm install -D @types/node typescript
386>
387> # Create our files
388> mkdir src
389> touch src/index.ts
390> ```
391>
392> ```powershell Windows theme={null}
393> # Create a new directory for our project
394> md weather
395> cd weather
396>
397> # Initialize a new npm project
398> npm init -y
399>
400> # Install dependencies
401> npm install @modelcontextprotocol/sdk zod@3
402> npm install -D @types/node typescript
403>
404> # Create our files
405> md src
406> new-item src\index.ts
407> ```
408>
409> Update 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>
424> Create 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>
444> Now let's dive into building your server.
445>
446> ## Building your server
447>
448> ### Importing packages and setting up the instance
449>
450> Add these to the top of your `src/index.ts`:
451>
452> ```typescript theme={null}
453> import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
454> import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
455> import { z } from "zod";
456>
457> const NWS_API_BASE = "https://api.weather.gov";
458> const USER_AGENT = "weather-app/1.0";
459>
460> // Create server instance
461> const server = new McpServer({
462> name: "weather",
463> version: "1.0.0",
464> });
465> ```
466>
467> ### Helper functions
468>
469> Next, 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
473> async 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>
491> interface 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
502> function 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>
514> interface ForecastPeriod {
515> name?: string;
516> temperature?: number;
517> temperatureUnit?: string;
518> windSpeed?: string;
519> windDirection?: string;
520> shortForecast?: string;
521> }
522>
523> interface AlertsResponse {
524> features: AlertFeature[];
525> }
526>
527> interface PointsResponse {
528> properties: {
529> forecast?: string;
530> };
531> }
532>
533> interface ForecastResponse {
534> properties: {
535> periods: ForecastPeriod[];
536> };
537> }
538> ```
539>
540> ### Implementing tool execution
541>
542> The 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>
547> server.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>
600> server.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>
697> Finally, implement the main function to run the server:
698>
699> ```typescript theme={null}
700> async function main() {
701> const transport = new StdioServerTransport();
702> await server.connect(transport);
703> console.error("Weather MCP Server running on stdio");
704> }
705>
706> main().catch((error) => {
707> console.error("Fatal error in main():", error);
708> process.exit(1);
709> });
710> ```
711>
712> Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect.
713>
714> Let'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](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
721>
722> First, make sure you have Claude for Desktop installed. [You can install the latest version
723> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
724>
725> 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.
726>
727> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
728>
729> ```bash macOS/Linux theme={null}
730> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
731> ```
732>
733> ```powershell Windows theme={null}
734> code $env:AppData\Claude\claude_desktop_config.json
735> ```
736>
737> 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.
738>
739> In 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>
763> This tells Claude for Desktop:
764>
765> 1. There's an MCP server named "weather"
766> 2. Launch it by running `node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js`
767>
768> Save the file, and restart **Claude for Desktop**.
769 
770> [tab] 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>
777> Let'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>
780> 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.
781> For manual MCP Server implementation, refer to the [MCP Server Java SDK documentation](https://java.sdk.modelcontextprotocol.io/).
782>
783> ### Logging in MCP Servers
784>
785> When 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>
803> Use the [Spring Initializer](https://start.spring.io/) to bootstrap the project.
804>
805> You 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}
822> dependencies {
823> implementation platform("org.springframework.ai:spring-ai-starter-mcp-server")
824> implementation platform("org.springframework:spring-web")
825> }
826> ```
827>
828> Then configure your application by setting the application properties:
829>
830> ```bash application.properties theme={null}
831> spring.main.bannerMode=off
832> logging.pattern.console=
833> ```
834>
835> ```yaml application.yml theme={null}
836> logging:
837> pattern:
838> console:
839> spring:
840> main:
841> banner-mode: off
842> ```
843>
844> 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.
845>
846> Now let's dive into building your server.
847>
848> ## Building your server
849>
850> ### Weather Service
851>
852> 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:
853>
854> ```java theme={null}
855> @Service
856> public 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>
895> The `@Service` annotation will auto-register the service in your application context.
896> The Spring AI `@Tool` annotation makes it easy to create and maintain MCP tools.
897>
898> The auto-configuration will automatically register these tools with the MCP server.
899>
900> ### Create your Boot Application
901>
902> ```java theme={null}
903> @SpringBootApplication
904> public 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>
917> Uses the `MethodToolCallbackProvider` utils to convert the `@Tools` into actionable callbacks used by the MCP server.
918>
919> ### Running the server
920>
921> Finally, let's build the server:
922>
923> ```bash theme={null}
924> ./mvnw clean install
925> ```
926>
927> This will generate an `mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar` file within the `target` folder.
928>
929> Let'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>
937> First, 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>
940> We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
941> To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
942> Make sure to create the file if it doesn't exist.
943>
944> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
945>
946> ```bash macOS/Linux theme={null}
947> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
948> ```
949>
950> ```powershell Windows theme={null}
951> code $env:AppData\Claude\claude_desktop_config.json
952> ```
953>
954> You'll then add your servers in the `mcpServers` key.
955> The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
956>
957> In 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>
993> This tells Claude for Desktop:
994>
995> 1. There's an MCP server named "my-weather-server"
996> 2. To launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar`
997>
998> Save the file, and restart **Claude for Desktop**.
999>
1000> ## Testing your server with Java client
1001>
1002> ### Create an MCP Client manually
1003>
1004> Use the `McpClient` to connect to the server:
1005>
1006> ```java theme={null}
1007> var stdioParams = ServerParameters.builder("java")
1008> .args("-jar", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar")
1009> .build();
1010>
1011> var stdioTransport = new StdioClientTransport(stdioParams);
1012>
1013> var mcpClient = McpClient.sync(stdioTransport).build();
1014>
1015> mcpClient.initialize();
1016>
1017> ListToolsResult toolsList = mcpClient.listTools();
1018>
1019> CallToolResult weather = mcpClient.callTool(
1020> new CallToolRequest("getWeatherForecastByLocation",
1021> Map.of("latitude", "47.6062", "longitude", "-122.3321")));
1022>
1023> CallToolResult alert = mcpClient.callTool(
1024> new CallToolRequest("getAlerts", Map.of("state", "NY")));
1025>
1026> mcpClient.closeGracefully();
1027> ```
1028>
1029> ### Use MCP Client Boot Starter
1030>
1031> Create 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>
1040> and set the `spring.ai.mcp.client.stdio.servers-configuration` property to point to your `claude_desktop_config.json`.
1041> You can reuse the existing Anthropic Desktop configuration:
1042>
1043> ```properties theme={null}
1044> spring.ai.mcp.client.stdio.servers-configuration=file:PATH/TO/claude_desktop_config.json
1045> ```
1046>
1047> When you start your client application, the auto-configuration will automatically create MCP clients from the claude\_desktop\_config.json.
1048>
1049> 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.
1050>
1051> ## More Java MCP Server examples
1052>
1053> 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.
1054> It showcases how to define and register MCP Tools, Resources, and Prompts, using the Spring Boot's auto-configuration capabilities.
1055 
1056> [tab] Kotlin
1057>
1058> 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)
1059>
1060> ### Prerequisite knowledge
1061>
1062> This quickstart assumes you have familiarity with:
1063>
1064> * Kotlin
1065> * LLMs like Claude
1066>
1067> ### Logging in MCP Servers
1068>
1069> When 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>
1085> First, let's install `java` and `gradle` if you haven't already.
1086> You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
1087> Verify your `java` installation:
1088>
1089> ```bash theme={null}
1090> java --version
1091> ```
1092>
1093> Now, let's create and set up your project:
1094>
1095> ```bash macOS/Linux theme={null}
1096> # Create a new directory for our project
1097> mkdir weather
1098> cd weather
1099>
1100> # Initialize a new kotlin project
1101> gradle init
1102> ```
1103>
1104> ```powershell Windows theme={null}
1105> # Create a new directory for our project
1106> md weather
1107> cd weather
1108>
1109> # Initialize a new kotlin project
1110> gradle init
1111> ```
1112>
1113> After running `gradle init`, select **Application** as the project type, **Kotlin** as the programming language.
1114>
1115> Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
1116>
1117> After 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
1121> val mcpVersion = "0.9.0"
1122> val ktorVersion = "3.2.3"
1123> val slf4jVersion = "2.0.17"
1124>
1125> plugins {
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>
1132> application {
1133> mainClass.set("MainKt")
1134> }
1135>
1136> dependencies {
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>
1145> Verify that everything is set up correctly:
1146>
1147> ```bash theme={null}
1148> ./gradlew build
1149> ```
1150>
1151> Now let's dive into building your server.
1152>
1153> ## Building your server
1154>
1155> ### Setting up the instance
1156>
1157> Add a server initialization function:
1158>
1159> ```kotlin theme={null}
1160> fun 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>
1191> Next, let's add functions and data classes for querying and converting responses from the National Weather Service API:
1192>
1193> ```kotlin theme={null}
1194> val 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
1209> suspend fun HttpClient.getAlerts(state: String): List<String> {
1210> val alerts = this.get("/alerts/active/area/$state").body<AlertsResponse>()
1211> return alerts.features.map { feature ->
1212> """
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
1223> suspend 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 ->
1228> """
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
1238> data class PointsResponse(val properties: PointsProperties)
1239>
1240> @Serializable
1241> data class PointsProperties(val forecast: String? = null)
1242>
1243> @Serializable
1244> data class ForecastResponse(val properties: ForecastProperties)
1245>
1246> @Serializable
1247> data class ForecastProperties(val periods: List<ForecastPeriod> = emptyList())
1248>
1249> @Serializable
1250> data 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
1260> data class AlertsResponse(val features: List<AlertFeature> = emptyList())
1261>
1262> @Serializable
1263> data class AlertFeature(val properties: AlertProperties)
1264>
1265> @Serializable
1266> data 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>
1277> The 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>
1282> server.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>
1304> server.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>
1336> Finally, implement the main function to run the server:
1337>
1338> ```kotlin theme={null}
1339> fun main() = runMcpServer()
1340> ```
1341>
1342> You can run the server directly during development:
1343>
1344> ```bash theme={null}
1345> ./gradlew run
1346> ```
1347>
1348> For production use, build the shadow JAR:
1349>
1350> ```bash theme={null}
1351> ./gradlew build
1352> java -jar build/libs/weather-0.1.0-all.jar
1353> ```
1354>
1355> Let'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](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1362>
1363> First, make sure you have Claude for Desktop installed. [You can install the latest version
1364> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1365>
1366> We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
1367> To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
1368> Make sure to create the file if it doesn't exist.
1369>
1370> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1371>
1372> ```bash macOS/Linux theme={null}
1373> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1374> ```
1375>
1376> ```powershell Windows theme={null}
1377> code $env:AppData\Claude\claude_desktop_config.json
1378> ```
1379>
1380> You'll then add your servers in the `mcpServers` key.
1381> The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1382>
1383> In 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>
1413> This tells Claude for Desktop:
1414>
1415> 1. There's an MCP server named "weather"
1416> 2. Launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar`
1417>
1418> Save the file, and restart **Claude for Desktop**.
1419 
1420> [tab] C#
1421>
1422> 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)
1423>
1424> ### Prerequisite knowledge
1425>
1426> This 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>
1434> When 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>
1450> 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:
1451>
1452> ```bash theme={null}
1453> dotnet --version
1454> ```
1455>
1456> Now, let's create and set up your project:
1457>
1458> ```bash macOS/Linux theme={null}
1459> # Create a new directory for our project
1460> mkdir weather
1461> cd weather
1462> # Initialize a new C# project
1463> dotnet new console
1464> ```
1465>
1466> ```powershell Windows theme={null}
1467> # Create a new directory for our project
1468> mkdir weather
1469> cd weather
1470> # Initialize a new C# project
1471> dotnet new console
1472> ```
1473>
1474> After running `dotnet new console`, you will be presented with a new C# project.
1475> You can open the project in your favorite IDE, such as [Visual Studio](https://visualstudio.microsoft.com/) or [Rider](https://www.jetbrains.com/rider/).
1476> 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).
1477> After 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
1481> dotnet add package ModelContextProtocol --prerelease
1482> # Add the .NET Hosting NuGet package
1483> dotnet add package Microsoft.Extensions.Hosting
1484> ```
1485>
1486> Now let’s dive into building your server.
1487>
1488> ## Building your server
1489>
1490> Open the `Program.cs` file in your project and replace its contents with the following code:
1491>
1492> ```csharp theme={null}
1493> using Microsoft.Extensions.DependencyInjection;
1494> using Microsoft.Extensions.Hosting;
1495> using ModelContextProtocol;
1496> using System.Net.Http.Headers;
1497>
1498> var builder = Host.CreateEmptyApplicationBuilder(settings: null);
1499>
1500> builder.Services.AddMcpServer()
1501> .WithStdioServerTransport()
1502> .WithToolsFromAssembly();
1503>
1504> builder.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>
1511> var app = builder.Build();
1512>
1513> await 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>
1520> 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.
1521>
1522> ### Weather API helper functions
1523>
1524> Create an extension class for `HttpClient` which helps simplify JSON request handling:
1525>
1526> ```csharp theme={null}
1527> using System.Text.Json;
1528>
1529> internal 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>
1540> Next, define a class with the tool execution handlers for querying and converting responses from the National Weather Service API:
1541>
1542> ```csharp theme={null}
1543> using ModelContextProtocol.Server;
1544> using System.ComponentModel;
1545> using System.Globalization;
1546> using System.Text.Json;
1547>
1548> namespace QuickstartWeatherServer.Tools;
1549>
1550> [McpServerToolType]
1551> public 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.";
1565> }
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>
1606> Finally, run the server using the following command:
1607>
1608> ```bash theme={null}
1609> dotnet run
1610> ```
1611>
1612> This 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](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1619>
1620> First, make sure you have Claude for Desktop installed. [You can install the latest version
1621> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1622> 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.
1623> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1624>
1625> ```bash macOS/Linux theme={null}
1626> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1627> ```
1628>
1629> ```powershell Windows theme={null}
1630> code $env:AppData\Claude\claude_desktop_config.json
1631> ```
1632>
1633> 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.
1634> In 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>
1663> This tells Claude for Desktop:
1664>
1665> 1. There's an MCP server named "weather"
1666> 2. Launch it by running `dotnet run /ABSOLUTE/PATH/TO/PROJECT`
1667> Save the file, and restart **Claude for Desktop**.
1668 
1669> [tab] Ruby
1670>
1671> 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)
1672>
1673> ### Prerequisite knowledge
1674>
1675> This quickstart assumes you have familiarity with:
1676>
1677> * Ruby
1678> * LLMs like Claude
1679>
1680> ### Logging in MCP Servers
1681>
1682> When 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)
1696> puts "Processing request"
1697>
1698> # ✅ Good (STDIO)
1699> require "logger"
1700> logger = Logger.new($stderr)
1701> logger.info("Processing request")
1702> ```
1703>
1704> ### System requirements
1705>
1706> * Ruby 2.7 or higher installed.
1707>
1708> ### Set up your environment
1709>
1710> First, let's make sure you have Ruby installed. You can check by running:
1711>
1712> ```bash theme={null}
1713> ruby --version
1714> ```
1715>
1716> Now, let's create and set up our project:
1717>
1718> ```bash macOS/Linux theme={null}
1719> # Create a new directory for our project
1720> mkdir weather
1721> cd weather
1722>
1723> # Create a Gemfile
1724> bundle init
1725>
1726> # Add the MCP SDK dependency
1727> bundle add mcp
1728>
1729> # Create our server file
1730> touch weather.rb
1731> ```
1732>
1733> ```powershell Windows theme={null}
1734> # Create a new directory for our project
1735> mkdir weather
1736> cd weather
1737>
1738> # Create a Gemfile
1739> bundle init
1740>
1741> # Add the MCP SDK dependency
1742> bundle add mcp
1743>
1744> # Create our server file
1745> new-item weather.rb
1746> ```
1747>
1748> Now let's dive into building your server.
1749>
1750> ## Building your server
1751>
1752> ### Importing packages and setting up constants
1753>
1754> Open `weather.rb` and add these requires and constants at the top:
1755>
1756> ```ruby theme={null}
1757> require "json"
1758> require "mcp"
1759> require "net/http"
1760> require "uri"
1761>
1762> NWS_API_BASE = "https://api.weather.gov"
1763> USER_AGENT = "weather-app/1.0"
1764> ```
1765>
1766> The `mcp` gem provides the Model Context Protocol SDK for Ruby, with classes for server implementation and stdio transport.
1767>
1768> ### Helper methods
1769>
1770> Next, let's add helper methods for querying and formatting data from the National Weather Service API:
1771>
1772> ```ruby theme={null}
1773> module 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
1800> end
1801> ```
1802>
1803> ### Implementing tool execution
1804>
1805> Now let's define our tool classes. Each tool subclasses `MCP::Tool` and implements the tool logic:
1806>
1807> ```ruby theme={null}
1808> class 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)"
1818> }
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
1840> end
1841>
1842> class 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
1886> end
1887> ```
1888>
1889> ### Running the server
1890>
1891> Finally, initialize and run the server:
1892>
1893> ```ruby theme={null}
1894> server = MCP::Server.new(
1895> name: "weather",
1896> version: "1.0.0",
1897> tools: [GetAlerts, GetForecast]
1898> )
1899>
1900> transport = MCP::Server::Transports::StdioTransport.new(server)
1901> transport.open
1902> ```
1903>
1904> Your server is complete! Run `bundle exec ruby weather.rb` to start the MCP server, which will listen for messages from MCP hosts.
1905>
1906> Let'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](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1913>
1914> 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.**
1915>
1916> 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.
1917>
1918> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1919>
1920> ```bash macOS/Linux theme={null}
1921> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1922> ```
1923>
1924> ```powershell Windows theme={null}
1925> code $env:AppData\Claude\claude_desktop_config.json
1926> ```
1927>
1928> 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.
1929>
1930> In 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>
1960> This tells Claude for Desktop:
1961>
1962> 1. There's an MCP server named "weather"
1963> 2. Launch it by running `bundle exec ruby weather.rb` in the specified directory
1964>
1965> Save the file, and restart **Claude for Desktop**.
1966 
1967> [tab] Rust
1968>
1969> 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)
1970>
1971> ### Prerequisite knowledge
1972>
1973> This 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>
1981> When 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)
1996> println!("Processing request");
1997>
1998> // ✅ Good (STDIO)
1999> eprintln!("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>
2009> 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):
2010>
2011> ```bash macOS/Linux theme={null}
2012> curl --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>
2019> Verify your Rust installation:
2020>
2021> ```bash theme={null}
2022> rustc --version
2023> cargo --version
2024> ```
2025>
2026> Now, let's create and set up our project:
2027>
2028> ```bash macOS/Linux theme={null}
2029> # Create a new Rust project
2030> cargo new weather
2031> cd weather
2032> ```
2033>
2034> ```powershell Windows theme={null}
2035> # Create a new Rust project
2036> cargo new weather
2037> cd weather
2038> ```
2039>
2040> Update your `Cargo.toml` to add the required dependencies:
2041>
2042> ```toml Cargo.toml theme={null}
2043> [package]
2044> name = "weather"
2045> version = "0.1.0"
2046> edition = "2024"
2047>
2048> [dependencies]
2049> rmcp = { version = "0.3", features = ["server", "macros", "transport-io"] }
2050> tokio = { version = "1.46", features = ["full"] }
2051> reqwest = { version = "0.12", features = ["json"] }
2052> serde = { version = "1.0", features = ["derive"] }
2053> serde_json = "1.0"
2054> anyhow = "1.0"
2055> tracing = "0.1"
2056> tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] }
2057> ```
2058>
2059> Now let's dive into building your server.
2060>
2061> ## Building your server
2062>
2063> ### Importing packages and constants
2064>
2065> Open `src/main.rs` and add these imports and constants at the top:
2066>
2067> ```rust theme={null}
2068> use anyhow::Result;
2069> use rmcp::{
2070> ServerHandler, ServiceExt,
2071> handler::server::{router::tool::ToolRouter, tool::Parameters},
2072> model::*,
2073> schemars, tool, tool_handler, tool_router,
2074> };
2075> use serde::Deserialize;
2076> use serde::de::DeserializeOwned;
2077>
2078> const NWS_API_BASE: &str = "https://api.weather.gov";
2079> const USER_AGENT: &str = "weather-app/1.0";
2080> ```
2081>
2082> The `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>
2086> Next, let's define the data structures for deserializing responses from the National Weather Service API:
2087>
2088> ```rust theme={null}
2089> #[derive(Debug, Deserialize)]
2090> struct AlertsResponse {
2091> features: Vec<AlertFeature>,
2092> }
2093>
2094> #[derive(Debug, Deserialize)]
2095> struct AlertFeature {
2096> properties: AlertProperties,
2097> }
2098>
2099> #[derive(Debug, Deserialize)]
2100> struct 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)]
2110> struct PointsResponse {
2111> properties: PointsProperties,
2112> }
2113>
2114> #[derive(Debug, Deserialize)]
2115> struct PointsProperties {
2116> forecast: String,
2117> }
2118>
2119> #[derive(Debug, Deserialize)]
2120> struct ForecastResponse {
2121> properties: ForecastProperties,
2122> }
2123>
2124> #[derive(Debug, Deserialize)]
2125> struct ForecastProperties {
2126> periods: Vec<ForecastPeriod>,
2127> }
2128>
2129> #[derive(Debug, Deserialize)]
2130> struct 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>
2144> Now define the request types that MCP clients will send:
2145>
2146> ```rust theme={null}
2147> #[derive(serde::Deserialize, schemars::JsonSchema)]
2148> pub struct MCPForecastRequest {
2149> latitude: f32,
2150> longitude: f32,
2151> }
2152>
2153> #[derive(serde::Deserialize, schemars::JsonSchema)]
2154> pub struct MCPAlertRequest {
2155> state: String,
2156> }
2157> ```
2158>
2159> ### Helper functions
2160>
2161> Add helper functions for making API requests and formatting responses:
2162>
2163> ```rust theme={null}
2164> async 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>
2176> fn 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>
2194> fn 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>
2209> Now let's implement the main Weather server struct with the tool handlers:
2210>
2211> ```rust theme={null}
2212> pub struct Weather {
2213> tool_router: ToolRouter<Weather>,
2214> }
2215>
2216> #[tool_router]
2217> impl Weather {
2218> fn new() -> Self {
2219> Self {
2220> tool_router: Self::tool_router(),
2221> }
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(),
2248> }
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>
2282> The `#[tool_router]` macro automatically generates the routing logic, and the `#[tool]` attribute marks methods as MCP tools.
2283>
2284> ### Implementing the ServerHandler
2285>
2286> Implement the `ServerHandler` trait to define server capabilities:
2287>
2288> ```rust theme={null}
2289> #[tool_handler]
2290> impl ServerHandler for Weather {
2291> fn get_info(&self) -> ServerInfo {
2292> ServerInfo {
2293> capabilities: ServerCapabilities::builder().enable_tools().build(),
2294> ..Default::default()
2295> }
2296> }
2297> }
2298> ```
2299>
2300> ### Running the server
2301>
2302> Finally, implement the main function to run the server with stdio transport:
2303>
2304> ```rust theme={null}
2305> #[tokio::main]
2306> async 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>
2314> Build your server with:
2315>
2316> ```bash theme={null}
2317> cargo build --release
2318> ```
2319>
2320> The compiled binary will be in `target/release/weather`.
2321>
2322> Let'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](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
2329>
2330> 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.**
2331>
2332> 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.
2333>
2334> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2335>
2336> ```bash macOS/Linux theme={null}
2337> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2338> ```
2339>
2340> ```powershell Windows theme={null}
2341> code $env:AppData\Claude\claude_desktop_config.json
2342> ```
2343>
2344> 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.
2345>
2346> In 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>
2372> This tells Claude for Desktop:
2373>
2374> 1. There's an MCP server named "weather"
2375> 2. Launch it by running the compiled binary at the specified path
2376>
2377> Save the file, and restart **Claude for Desktop**.
2378 
2379> [tab] Go
2380>
2381> 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)
2382>
2383> ### Prerequisite knowledge
2384>
2385> This quickstart assumes you have familiarity with:
2386>
2387> * Go
2388> * LLMs like Claude
2389>
2390> ### Logging in MCP Servers
2391>
2392> When 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)
2407> fmt.Println("Processing request")
2408>
2409> // ✅ Good (STDIO)
2410> log.Println("Processing request") // defaults to stderr
2411>
2412> // ✅ Good (STDIO)
2413> fmt.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>
2422> First, let's install Go if you haven't already. You can download and install Go from [go.dev](https://go.dev/dl/).
2423>
2424> Verify your Go installation:
2425>
2426> ```bash theme={null}
2427> go version
2428> ```
2429>
2430> Now, let's create and set up our project:
2431>
2432> ```bash macOS/Linux theme={null}
2433> # Create a new directory for our project
2434> mkdir weather
2435> cd weather
2436>
2437> # Initialize Go module
2438> go mod init weather
2439>
2440> # Install dependencies
2441> go get github.com/modelcontextprotocol/go-sdk/mcp
2442>
2443> # Create our server file
2444> touch main.go
2445> ```
2446>
2447> ```powershell Windows theme={null}
2448> # Create a new directory for our project
2449> md weather
2450> cd weather
2451>
2452> # Initialize Go module
2453> go mod init weather
2454>
2455> # Install dependencies
2456> go get github.com/modelcontextprotocol/go-sdk/mcp
2457>
2458> # Create our server file
2459> new-item main.go
2460> ```
2461>
2462> Now let's dive into building your server.
2463>
2464> ## Building your server
2465>
2466> ### Importing packages and constants
2467>
2468> Add these to the top of your `main.go`:
2469>
2470> ```go theme={null}
2471> package main
2472>
2473> import (
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>
2486> const (
2487> NWSAPIBase = "https://api.weather.gov"
2488> UserAgent = "weather-app/1.0"
2489> )
2490> ```
2491>
2492> ### Data structures
2493>
2494> Next, let's define the data structures used by our tools:
2495>
2496> ```go theme={null}
2497> type PointsResponse struct {
2498> Properties struct {
2499> Forecast string `json:"forecast"`
2500> } `json:"properties"`
2501> }
2502>
2503> type ForecastResponse struct {
2504> Properties struct {
2505> Periods []ForecastPeriod `json:"periods"`
2506> } `json:"properties"`
2507> }
2508>
2509> type 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>
2518> type AlertsResponse struct {
2519> Features []AlertFeature `json:"features"`
2520> }
2521>
2522> type AlertFeature struct {
2523> Properties AlertProperties `json:"properties"`
2524> }
2525>
2526> type 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>
2534> type ForecastInput struct {
2535> Latitude float64 `json:"latitude" jsonschema:"Latitude of the location"`
2536> Longitude float64 `json:"longitude" jsonschema:"Longitude of the location"`
2537> }
2538>
2539> type AlertsInput struct {
2540> State string `json:"state" jsonschema:"Two-letter US state code (e.g. CA, NY)"`
2541> }
2542> ```
2543>
2544> ### Helper functions
2545>
2546> Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
2547>
2548> ```go theme={null}
2549> func 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>
2578> func 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(`
2587> Event: %s
2588> Area: %s
2589> Severity: %s
2590> Description: %s
2591> Instructions: %s
2592> `, event, areaDesc, severity, description, instruction)
2593> }
2594>
2595> func formatPeriod(period ForecastPeriod) string {
2596> return fmt.Sprintf(`
2597> %s:
2598> Temperature: %d°%s
2599> Wind: %s %s
2600> Forecast: %s
2601> `, period.Name, period.Temperature, period.TemperatureUnit,
2602> period.WindSpeed, period.WindDirection, period.DetailedForecast)
2603> }
2604> ```
2605>
2606> ### Implementing tool execution
2607>
2608> The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
2609>
2610> ```go theme={null}
2611> func 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>
2669> func 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>
2712> Finally, implement the main function to run the server:
2713>
2714> ```go theme={null}
2715> func 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>
2741> Build your server with:
2742>
2743> ```bash theme={null}
2744> go build -o weather .
2745> ```
2746>
2747> The compiled binary will be in `./weather`.
2748>
2749> Let'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](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
2756>
2757> 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.**
2758>
2759> 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.
2760>
2761> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2762>
2763> ```bash macOS/Linux theme={null}
2764> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2765> ```
2766>
2767> ```powershell Windows theme={null}
2768> code $env:AppData\Claude\claude_desktop_config.json
2769> ```
2770>
2771> 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.
2772>
2773> In 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>
2799> This tells Claude for Desktop:
2800>
2801> 1. There's an MCP server named "weather"
2802> 2. Launch it by running the compiled binary at the specified path
2803>
2804> Save the file, and restart **Claude for Desktop**.
28052805 
28062806### Test with commands
28072807 
from line 2841
28412841 
28422842## Troubleshooting
28432843 
2844> **Claude for Desktop Integration Issues**
2844> [accordion] Claude for Desktop Integration Issues
28452845>
28462846> **Getting logs from Claude for Desktop**
28472847>
from line 2886
28862886>
28872887> Please refer to our [debugging guide](https://modelcontextprotocol.io/docs/2025-11-25/tools/debugging.md) for better debugging tools and more detailed guidance.
28882888 
2889> **Weather API Issues**
2889> [accordion] Weather API Issues
28902890>
28912891> **Error: Failed to retrieve grid point data**
28922892>
from line 2912
29122912 
29132913## Next steps
29142914 
2915> **[Building a client](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md)**
2915> [card] [Building a client](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md)
29162916>
29172917> Learn how to build your own MCP client that can connect to your server
29182918 
2919> **[Example servers](https://modelcontextprotocol.io/examples.md)**
2919> [card] [Example servers](https://modelcontextprotocol.io/examples.md)
29202920>
29212921> Check out our gallery of official MCP servers and implementations
29222922 
2923> **[Debugging Guide](https://modelcontextprotocol.io/docs/2025-11-25/tools/debugging.md)**
2923> [card] [Debugging Guide](https://modelcontextprotocol.io/docs/2025-11-25/tools/debugging.md)
29242924>
29252925> Learn how to effectively debug MCP servers and integrations
29262926 
2927> **[Build with Agent Skills](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-with-agent-skills.md)**
2927> [card] [Build with Agent Skills](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-with-agent-skills.md)
29282928>
29292929> Use agent skills to guide AI coding assistants through server design
29302930 

docs/2025-11-25/develop/build-with-agent-skills Changed · +3 / -3 lines

from line 85
8585Once your agent scaffolds the server, iterate on tool descriptions and error
8686handling, then test and ship:
8787 
88> **[MCP Inspector](https://modelcontextprotocol.io/docs/2025-11-25/tools/inspector.md)**
88> [card] [MCP Inspector](https://modelcontextprotocol.io/docs/2025-11-25/tools/inspector.md)
8989>
9090> Test your server's tools, resources, and prompts interactively
9191 
92> **[Connect to a client](https://modelcontextprotocol.io/docs/2025-11-25/develop/connect-local-servers.md)**
92> [card] [Connect to a client](https://modelcontextprotocol.io/docs/2025-11-25/develop/connect-local-servers.md)
9393>
9494> Wire your server into an MCP client via local or remote configuration
9595 
96> **[Publish to the Registry](https://modelcontextprotocol.io/registry/quickstart.md)**
96> [card] [Publish to the Registry](https://modelcontextprotocol.io/registry/quickstart.md)
9797>
9898> Make your server discoverable in the MCP Registry
9999 

docs/2025-11-25/develop/connect-local-servers Changed · +9 / -9 lines

from line 156
156156 
157157If you encounter issues setting up or using the Filesystem Server, these solutions address common problems:
158158 
159> **Server not showing up in Claude / hammer icon missing**
159> [accordion] Server not showing up in Claude / hammer icon missing
160160>
161161> 1. Restart Claude Desktop completely
162162> 2. Check your `claude_desktop_config.json` file syntax
from line 172
172172> npx -y @modelcontextprotocol/server-filesystem C:\Users\username\Desktop C:\Users\username\Downloads
173173> ```
174174 
175> **Getting logs from Claude Desktop**
175> [accordion] Getting logs from Claude Desktop
176176>
177177> Claude.app logging related to MCP is written to log files in:
178178>
from line 194
194194> type "%APPDATA%\Claude\logs\mcp*.log"
195195> ```
196196 
197> **Tool calls failing silently**
197> [accordion] Tool calls failing silently
198198>
199199> If Claude attempts to use the tools but they fail:
200200>
from line 202
202202> 2. Verify your server builds and runs without errors
203203> 3. Try restarting Claude Desktop
204204 
205> **None of this is working. What do I do?**
205> [accordion] None of this is working. What do I do?
206206>
207207> Please refer to our [debugging guide](https://modelcontextprotocol.io/docs/2025-11-25/tools/debugging.md) for better debugging tools and more detailed guidance.
208208 
209> **ENOENT error and `$\{APPDATA\}` in paths on Windows**
209> [accordion] ENOENT error and `$\{APPDATA\}` in paths on Windows
210210>
211211> 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`:
212212>
from line 239
239239 
240240Now that you've successfully connected Claude Desktop to a local MCP server, explore these options to expand your setup:
241241 
242> **[Explore other servers](https://github.com/modelcontextprotocol/servers)**
242> [card] [Explore other servers](https://github.com/modelcontextprotocol/servers)
243243>
244244> Browse our collection of official and community-created MCP servers for
245245> additional capabilities
246246 
247> **[Build your own server](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-server.md)**
247> [card] [Build your own server](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-server.md)
248248>
249249> Create custom MCP servers tailored to your specific workflows and
250250> integrations
251251 
252> **[Connect to remote servers](https://modelcontextprotocol.io/docs/2025-11-25/develop/connect-remote-servers.md)**
252> [card] [Connect to remote servers](https://modelcontextprotocol.io/docs/2025-11-25/develop/connect-remote-servers.md)
253253>
254254> Learn how to connect Claude to remote MCP servers for cloud-based tools and
255255> services
256256 
257> **[Understand the protocol](https://modelcontextprotocol.io/docs/2025-11-25/learn/architecture.md)**
257> [card] [Understand the protocol](https://modelcontextprotocol.io/docs/2025-11-25/learn/architecture.md)
258258>
259259> Dive deeper into how MCP works and its architecture
260260 

docs/2025-11-25/develop/connect-remote-servers Changed · +4 / -4 lines

from line 81
8181 
8282Now 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.
8383 
84> **[Build your own remote server](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-server.md)**
84> [card] [Build your own remote server](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-server.md)
8585>
8686> Create custom remote MCP servers to integrate with proprietary tools and
8787> services
8888 
89> **[Explore available servers](https://github.com/modelcontextprotocol/servers)**
89> [card] [Explore available servers](https://github.com/modelcontextprotocol/servers)
9090>
9191> Browse our collection of official and community-created MCP servers
9292 
93> **[Connect local servers](https://modelcontextprotocol.io/docs/2025-11-25/develop/connect-local-servers.md)**
93> [card] [Connect local servers](https://modelcontextprotocol.io/docs/2025-11-25/develop/connect-local-servers.md)
9494>
9595> Learn how to connect Claude Desktop to local MCP servers for direct system
9696> access
9797 
98> **[Understand the architecture](https://modelcontextprotocol.io/docs/2025-11-25/learn/architecture.md)**
98> [card] [Understand the architecture](https://modelcontextprotocol.io/docs/2025-11-25/learn/architecture.md)
9999>
100100> Dive deeper into how MCP works and its architecture
101101 

docs/2025-11-25/getting-started/intro Changed · +6 / -6 lines

from line 29
2929 
3030## Start Building
3131 
32> **[Build servers](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-server.md)**
32> [card] [Build servers](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-server.md)
3333>
3434> Create MCP servers to expose your data and tools
3535 
36> **[Build clients](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md)**
36> [card] [Build clients](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-client.md)
3737>
3838> Develop applications that connect to MCP servers
3939 
40> **[Build MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview.md)**
40> [card] [Build MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview.md)
4141>
4242> Build interactive apps that run inside AI clients
4343 
4444## Learn more
4545 
46> **[Architecture](https://modelcontextprotocol.io/docs/2025-11-25/learn/architecture.md)**
46> [card] [Architecture](https://modelcontextprotocol.io/docs/2025-11-25/learn/architecture.md)
4747>
4848> Learn the core concepts and architecture of MCP
4949 
50> **[Security](https://modelcontextprotocol.io/docs/2025-11-25/tutorials/security/security_best_practices.md)**
50> [card] [Security](https://modelcontextprotocol.io/docs/2025-11-25/tutorials/security/security_best_practices.md)
5151>
5252> Understand the security considerations and best practices for MCP
5353 
5454## Community
5555 
56> **[Contributing](https://modelcontextprotocol.io/community/contributing.md)**
56> [card] [Contributing](https://modelcontextprotocol.io/community/contributing.md)
5757>
5858> Learn how to get involved and contribute to MCP
5959 

docs/2025-11-25/tools/debugging Changed · +3 / -3 lines

from line 327
327327 
328328## Next steps
329329 
330> **[MCP Inspector](https://modelcontextprotocol.io/docs/2025-11-25/tools/inspector.md)**
330> [card] [MCP Inspector](https://modelcontextprotocol.io/docs/2025-11-25/tools/inspector.md)
331331>
332332> Learn to use the MCP Inspector
333333 
334> **[Build an MCP server](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-server.md)**
334> [card] [Build an MCP server](https://modelcontextprotocol.io/docs/2025-11-25/develop/build-server.md)
335335>
336336> Walk through building a server from scratch
337337 
338> **[Connect local servers](https://modelcontextprotocol.io/docs/2025-11-25/develop/connect-local-servers.md)**
338> [card] [Connect local servers](https://modelcontextprotocol.io/docs/2025-11-25/develop/connect-local-servers.md)
339339>
340340> Full claude\_desktop\_config.json reference and troubleshooting
341341 

docs/2025-11-25/tools/inspector Changed · +31 / -31 lines

from line 22
2222 
2323A common way to start server packages from [npm](https://npmjs.com) or [PyPI](https://pypi.org).
2424 
25**npm package**
25> [tab] npm package
26>
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> ```
2632 
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```
33> [tab] PyPI package
34>
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> ```
3240 
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 
4141#### Inspecting locally developed servers
4242 
4343To inspect servers locally developed or downloaded as a repository, the most common
4444way is:
4545 
46**TypeScript**
46> [tab] TypeScript
47>
48> ```bash theme={null}
49> npx @modelcontextprotocol/inspector node path/to/server/index.js args...
50> ```
4751 
48```bash theme={null}
49npx @modelcontextprotocol/inspector node path/to/server/index.js args...
50```
52> [tab] Python
53>
54> ```bash theme={null}
55> npx @modelcontextprotocol/inspector \
56> uv \
57> --directory path/to/server \
58> run \
59> package-name \
60> args...
61> ```
5162 
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 
6363Please carefully read any attached README for the most accurate instructions.
6464 
6565## Feature overview
from line 125
125125 
126126## Next steps
127127 
128> **[Inspector Repository](https://github.com/modelcontextprotocol/inspector)**
128> [card] [Inspector Repository](https://github.com/modelcontextprotocol/inspector)
129129>
130130> Check out the MCP Inspector source code
131131 
132> **[Debugging Guide](https://modelcontextprotocol.io/docs/2025-11-25/tools/debugging.md)**
132> [card] [Debugging Guide](https://modelcontextprotocol.io/docs/2025-11-25/tools/debugging.md)
133133>
134134> Learn about broader debugging strategies
135135 

docs/2025-11-25/tutorials/security/authorization Changed · +727 / -727 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 246
246246 
247247For 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.
248248 
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`,
330 );
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");
370 }
371 
372 const params = new URLSearchParams({
373 token: token,
374 client_id: CONFIG.auth.clientId,
375 });
376 
377 if (CONFIG.auth.clientSecret) {
378 params.set("client_secret", CONFIG.auth.clientSecret);
379 }
380 
381 let response: Response;
382 try {
383 response = await fetch(endpoint, {
384 method: "POST",
385 headers: {
386 "Content-Type": "application/x-www-form-urlencoded",
387 },
388 body: params.toString(),
389 });
390 } catch (e) {
391 console.error("[auth] introspection fetch threw", e);
392 throw e;
393 }
394 
395 if (!response.ok) {
396 const txt = await response.text();
397 console.error("[auth] introspection non-OK", { status: response.status });
398 
399 try {
400 const obj = JSON.parse(txt);
401 console.log(JSON.stringify(obj, null, 2));
402 } catch {
403 console.error(txt);
404 }
405 throw new Error(`Invalid or expired token: ${txt}`);
406 }
407 
408 let data: any;
409 try {
410 data = await response.json();
411 } catch (e) {
412 const txt = await response.text();
413 console.error("[auth] failed to parse introspection JSON", {
414 error: String(e),
415 body: txt,
416 });
417 throw e;
418 }
419 
420 if (data.active === false) {
421 throw new Error("Inactive token");
422 }
423 
424 if (!data.aud) {
425 throw new Error("Resource indicator (aud) missing");
426 }
427 
428 const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
429 const allowed = audiences.some((a) =>
430 checkResourceAllowed({
431 requestedResource: a,
432 configuredResource: mcpServerUrl,
433 }),
434 );
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,
446 };
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()
708 }
709 
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()
727 }
728 
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;
920 }
921 };
922 
923 options.RequireHttpsMetadata = false; // Set to true in production
924 
925 options.Events = new JwtBearerEvents
926 {
927 OnTokenValidated = context =>
928 {
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;
933 },
934 OnAuthenticationFailed = context =>
935 {
936 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
937 return Task.CompletedTask;
938 },
939 };
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"]
949 };
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).
249> [tab] TypeScript
250>
251> You can see the complete TypeScript project in the [sample repository](https://github.com/localden/min-ts-mcp-auth).
252>
253> Prior 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
257> HOST=localhost
258> PORT=3000
259>
260> # Auth server location
261> AUTH_HOST=localhost
262> AUTH_PORT=8080
263> AUTH_REALM=master
264>
265> # Keycloak OAuth client credentials
266> OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
267> OAUTH_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>
272> 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.
273>
274> ```typescript theme={null}
275> import "dotenv/config";
276> import express from "express";
277> import { randomUUID } from "node:crypto";
278> import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
279> import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
280> import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
281> import { z } from "zod";
282> import cors from "cors";
283> import {
284> mcpAuthMetadataRouter,
285> getOAuthProtectedResourceMetadataUrl,
286> } from "@modelcontextprotocol/sdk/server/auth/router.js";
287> import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
288> import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
289> import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
290> const 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>
302> function 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>
323> function 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`,
330> );
331> });
332> next();
333> };
334> }
335>
336> const app = express();
337>
338> app.use(
339> express.json({
340> verify: (req: any, _res, buf) => {
341> req.rawBody = buf?.toString() ?? "";
342> },
343> }),
344> );
345>
346> app.use(
347> cors({
348> origin: "*",
349> exposedHeaders: ["Mcp-Session-Id"],
350> }),
351> );
352>
353> app.use(createRequestLogger());
354>
355> const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
356> const oauthUrls = createOAuthUrls();
357>
358> const oauthMetadata: OAuthMetadata = {
359> ...oauthUrls,
360> response_types_supported: ["code"],
361> };
362>
363> const tokenVerifier = {
364> verifyAccessToken: async (token: string) => {
365> const endpoint = oauthMetadata.introspection_endpoint;
366>
367> if (!endpoint) {
368> console.error("[auth] no introspection endpoint in metadata");
369> throw new Error("No token verification endpoint available in metadata");
370> }
371>
372> const params = new URLSearchParams({
373> token: token,
374> client_id: CONFIG.auth.clientId,
375> });
376>
377> if (CONFIG.auth.clientSecret) {
378> params.set("client_secret", CONFIG.auth.clientSecret);
379> }
380>
381> let response: Response;
382> try {
383> response = await fetch(endpoint, {
384> method: "POST",
385> headers: {
386> "Content-Type": "application/x-www-form-urlencoded",
387> },
388> body: params.toString(),
389> });
390> } catch (e) {
391> console.error("[auth] introspection fetch threw", e);
392> throw e;
393> }
394>
395> if (!response.ok) {
396> const txt = await response.text();
397> console.error("[auth] introspection non-OK", { status: response.status });
398>
399> try {
400> const obj = JSON.parse(txt);
401> console.log(JSON.stringify(obj, null, 2));
402> } catch {
403> console.error(txt);
404> }
405> throw new Error(`Invalid or expired token: ${txt}`);
406> }
407>
408> let data: any;
409> try {
410> data = await response.json();
411> } catch (e) {
412> const txt = await response.text();
413> console.error("[auth] failed to parse introspection JSON", {
414> error: String(e),
415> body: txt,
416> });
417> throw e;
418> }
419>
420> if (data.active === false) {
421> throw new Error("Inactive token");
422> }
423>
424> if (!data.aud) {
425> throw new Error("Resource indicator (aud) missing");
426> }
427>
428> const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
429> const allowed = audiences.some((a) =>
430> checkResourceAllowed({
431> requestedResource: a,
432> configuredResource: mcpServerUrl,
433> }),
434> );
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,
446> };
447> },
448> };
449> app.use(
450> mcpAuthMetadataRouter({
451> oauthMetadata,
452> resourceServerUrl: mcpServerUrl,
453> scopesSupported: ["mcp:tools"],
454> resourceName: "MCP Demo Server",
455> }),
456> );
457>
458> const authMiddleware = requireBearerAuth({
459> verifier: tokenVerifier,
460> requiredScopes: [],
461> resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
462> });
463>
464> const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
465>
466> function 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>
505> const 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>
542> const 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>
556> app.post("/", authMiddleware, mcpPostHandler);
557> app.get("/", authMiddleware, handleSessionRequest);
558> app.delete("/", authMiddleware, handleSessionRequest);
559>
560> app.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>
569> When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
570>
571> For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
572 
573> [tab] Python
574>
575> You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
576>
577> 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.
578>
579> 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:
580>
581> ```python theme={null}
582> """Configuration settings for the MCP auth server."""
583>
584> import os
585> from typing import Optional
586>
587>
588> class 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
626> config = Config()
627>
628> ```
629>
630> The server implementation is as follows:
631>
632> ```python theme={null}
633> import datetime
634> import logging
635> from typing import Any
636>
637> from pydantic import AnyHttpUrl
638>
639> from mcp.server.auth.settings import AuthSettings
640> from mcp.server.fastmcp.server import FastMCP
641>
642> from .config import config
643> from .token_verifier import IntrospectionTokenVerifier
644>
645> logger = logging.getLogger(__name__)
646>
647>
648> def 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>
662> def 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()
708> }
709>
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()
727> }
728>
729> return app
730>
731>
732> def 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>
768> if __name__ == "__main__":
769> exit(main())
770> ```
771>
772> 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
773>
774> ```python theme={null}
775> """Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
776>
777> import logging
778> from typing import Any
779>
780> from mcp.server.auth.provider import AccessToken, TokenVerifier
781> from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
782>
783> logger = logging.getLogger(__name__)
784>
785>
786> class 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>
876> For more details, see the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
877 
878> [tab] C#
879>
880> You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
881>
882> 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.
883>
884> ```csharp theme={null}
885> using Microsoft.AspNetCore.Authentication.JwtBearer;
886> using Microsoft.IdentityModel.Tokens;
887> using ModelContextProtocol.AspNetCore.Authentication;
888> using ProtectedMcpServer.Tools;
889> using System.Security.Claims;
890>
891> var builder = WebApplication.CreateBuilder(args);
892>
893> var serverUrl = "http://localhost:3000/";
894> var authorizationServerUrl = "http://localhost:8080/realms/master/";
895>
896> builder.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;
920> }
921> };
922>
923> options.RequireHttpsMetadata = false; // Set to true in production
924>
925> options.Events = new JwtBearerEvents
926> {
927> OnTokenValidated = context =>
928> {
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;
933> },
934> OnAuthenticationFailed = context =>
935> {
936> Console.WriteLine($"Authentication failed: {context.Exception.Message}");
937> return Task.CompletedTask;
938> },
939> };
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"]
949> };
950> });
951>
952> builder.Services.AddAuthorization();
953>
954> builder.Services.AddHttpContextAccessor();
955> builder.Services.AddMcpServer()
956> .WithTools<MathTools>()
957> .WithHttpTransport();
958>
959> var app = builder.Build();
960>
961> app.UseAuthentication();
962> app.UseAuthorization();
963>
964> app.MapMcp().RequireAuthorization();
965>
966> Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
967> Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
968> Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
969> Console.WriteLine("Exposed Math tools: Add, Multiply");
970> Console.WriteLine("Press Ctrl+C to stop the server");
971>
972> app.Run(serverUrl);
973> ```
974>
975> For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
976976 
977977## Testing the MCP Server
978978 
979979 

docs/2026-07-28/develop/build-client Changed · +2475 / -2483 lines

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

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

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

docs/2026-07-28/develop/build-server Changed · +2789 / -2789 lines

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

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

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

docs/2026-07-28/develop/build-with-agent-skills Changed · +3 / -3 lines

from line 85
8585Once your agent scaffolds the server, iterate on tool descriptions and error
8686handling, then test and ship:
8787 
88> **[MCP Inspector](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector.md)**
88> [card] [MCP Inspector](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector.md)
8989>
9090> Test your server's tools, resources, and prompts interactively
9191 
92> **[Connect to a client](https://modelcontextprotocol.io/docs/2026-07-28/develop/connect-local-servers.md)**
92> [card] [Connect to a client](https://modelcontextprotocol.io/docs/2026-07-28/develop/connect-local-servers.md)
9393>
9494> Wire your server into an MCP client via local or remote configuration
9595 
96> **[Publish to the Registry](https://modelcontextprotocol.io/registry/quickstart.md)**
96> [card] [Publish to the Registry](https://modelcontextprotocol.io/registry/quickstart.md)
9797>
9898> Make your server discoverable in the MCP Registry
9999 

docs/2026-07-28/develop/connect-local-servers Changed · +18 / -11 lines

from line 65
6565 
6666* **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
6767* **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
68* **Linux**: `~/.config/Claude/claude_desktop_config.json`
6869 
6970**3. Configure the Filesystem Server**
7071 
from line 157
156157 
157158If you encounter issues setting up or using the Filesystem Server, these solutions address common problems:
158159 
159> **Server not showing up in Claude / hammer icon missing**
160> [accordion] Server not showing up in Claude / hammer icon missing
160161>
161162> 1. Restart Claude Desktop completely
162163> 2. Check your `claude_desktop_config.json` file syntax
163164> 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> 4. Look at [logs](#getting-logs-from-claude-desktop) to see why the server is not connecting
165166> 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:
166167>
167168> ```bash macOS/Linux theme={null}
from line 173
172173> npx -y @modelcontextprotocol/server-filesystem C:\Users\username\Desktop C:\Users\username\Downloads
173174> ```
174175 
175> **Getting logs from Claude Desktop**
176> [accordion] Getting logs from Claude Desktop
176177>
177178> Claude.app logging related to MCP is written to log files in:
178179>
from line 181
180181>
181182> * Windows: `%APPDATA%\Claude\logs`
182183>
184> * Linux: `~/.config/Claude/logs`
185>
183186> * `mcp.log` will contain general logging about MCP connections and connection failures.
184187>
185188> * Files named `mcp-server-SERVERNAME.log` will contain the stderr output from the named server. Stdio servers may use stderr for all their logging, so these files are not limited to errors.
from line 189
186189>
187190> 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):
188191>
189> ```bash macOS/Linux theme={null}
192> ```bash macOS theme={null}
190193> tail -n 20 -f ~/Library/Logs/Claude/mcp*.log
191194> ```
192195>
196> ```bash Linux theme={null}
197> tail -n 20 -f ~/.config/Claude/logs/mcp*.log
198> ```
199>
193200> ```powershell Windows theme={null}
194201> type "%APPDATA%\Claude\logs\mcp*.log"
195202> ```
196203 
197> **Tool calls failing silently**
204> [accordion] Tool calls failing silently
198205>
199206> If Claude attempts to use the tools but they fail:
200207>
from line 209
202209> 2. Verify your server builds and runs without errors
203210> 3. Try restarting Claude Desktop
204211 
205> **None of this is working. What do I do?**
212> [accordion] None of this is working. What do I do?
206213>
207214> Please refer to our [debugging guide](https://modelcontextprotocol.io/docs/2026-07-28/tools/debugging.md) for better debugging tools and more detailed guidance.
208215 
209> **ENOENT error and `$\{APPDATA\}` in paths on Windows**
216> [accordion] ENOENT error and `$\{APPDATA\}` in paths on Windows
210217>
211218> 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`:
212219>
from line 246
239246 
240247Now that you've successfully connected Claude Desktop to a local MCP server, explore these options to expand your setup:
241248 
242> **[Explore other servers](https://github.com/modelcontextprotocol/servers)**
249> [card] [Explore other servers](https://github.com/modelcontextprotocol/servers)
243250>
244251> Browse our collection of official and community-created MCP servers for
245252> additional capabilities
246253 
247> **[Build your own server](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-server.md)**
254> [card] [Build your own server](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-server.md)
248255>
249256> Create custom MCP servers tailored to your specific workflows and
250257> integrations
251258 
252> **[Connect to remote servers](https://modelcontextprotocol.io/docs/2026-07-28/develop/connect-remote-servers.md)**
259> [card] [Connect to remote servers](https://modelcontextprotocol.io/docs/2026-07-28/develop/connect-remote-servers.md)
253260>
254261> Learn how to connect Claude to remote MCP servers for cloud-based tools and
255262> services
256263 
257> **[Understand the protocol](https://modelcontextprotocol.io/docs/2026-07-28/learn/architecture.md)**
264> [card] [Understand the protocol](https://modelcontextprotocol.io/docs/2026-07-28/learn/architecture.md)
258265>
259266> Dive deeper into how MCP works and its architecture
260267 

docs/2026-07-28/develop/connect-remote-servers Changed · +4 / -4 lines

from line 88
8888 
8989Now 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.
9090 
91> **[Build your own remote server](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-server.md)**
91> [card] [Build your own remote server](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-server.md)
9292>
9393> Create custom remote MCP servers to integrate with proprietary tools and
9494> services
9595 
96> **[Explore available servers](https://github.com/modelcontextprotocol/servers)**
96> [card] [Explore available servers](https://github.com/modelcontextprotocol/servers)
9797>
9898> Browse our collection of official and community-created MCP servers
9999 
100> **[Connect local servers](https://modelcontextprotocol.io/docs/2026-07-28/develop/connect-local-servers.md)**
100> [card] [Connect local servers](https://modelcontextprotocol.io/docs/2026-07-28/develop/connect-local-servers.md)
101101>
102102> Learn how to connect Claude Desktop to local MCP servers for direct system
103103> access
104104 
105> **[Understand the architecture](https://modelcontextprotocol.io/docs/2026-07-28/learn/architecture.md)**
105> [card] [Understand the architecture](https://modelcontextprotocol.io/docs/2026-07-28/learn/architecture.md)
106106>
107107> Dive deeper into how MCP works and its architecture
108108 

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

from line 29
2929 
3030## Start Building
3131 
32> **[Build servers](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-server.md)**
32> [card] [Build servers](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-server.md)
3333>
3434> Create MCP servers to expose your data and tools
3535 
36> **[Build clients](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-client.md)**
36> [card] [Build clients](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-client.md)
3737>
3838> Develop applications that connect to MCP servers
3939 
40> **[Build MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview.md)**
40> [card] [Build MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview.md)
4141>
4242> Build interactive apps that run inside AI clients
4343 
4444## Learn more
4545 
46> **[Architecture](https://modelcontextprotocol.io/docs/2026-07-28/learn/architecture.md)**
46> [card] [Architecture](https://modelcontextprotocol.io/docs/2026-07-28/learn/architecture.md)
4747>
4848> Learn the core concepts and architecture of MCP
4949 
50> **[Security](https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices.md)**
50> [card] [Security](https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices.md)
5151>
5252> Understand the security considerations and best practices for MCP
5353 
5454## Community
5555 
56> **[Contributing](https://modelcontextprotocol.io/community/contributing.md)**
56> [card] [Contributing](https://modelcontextprotocol.io/community/contributing.md)
5757>
5858> Learn how to get involved and contribute to MCP
5959 

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

from line 347
347347 
348348## Next steps
349349 
350> **[MCP Inspector](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector.md)**
350> [card] [MCP Inspector](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector.md)
351351>
352352> Learn to use the MCP Inspector
353353 
354> **[Build an MCP server](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-server.md)**
354> [card] [Build an MCP server](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-server.md)
355355>
356356> Walk through building a server from scratch
357357 
358> **[Connect local servers](https://modelcontextprotocol.io/docs/2026-07-28/develop/connect-local-servers.md)**
358> [card] [Connect local servers](https://modelcontextprotocol.io/docs/2026-07-28/develop/connect-local-servers.md)
359359>
360360> Full claude\_desktop\_config.json reference and troubleshooting
361361 

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

from line 22
2222 
2323The Inspector requires **Node 22.19.0 or newer** and runs directly through `npx`. No installation is required:
2424 
25**Web**
25> [tab] Web
26>
27> ```bash theme={null}
28> # Launch the web UI and connect to a local stdio server
29> npx @modelcontextprotocol/inspector node path/to/server/index.js
30>
31> # Or launch with no target and add servers from the UI
32> npx @modelcontextprotocol/inspector
33> ```
34>
35> The command prints a URL carrying a per-launch API token and opens it in your browser (set `MCP_AUTO_OPEN_ENABLED=false` to stop it opening). See [Web client](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/web.md).
2636 
27```bash theme={null}
28# Launch the web UI and connect to a local stdio server
29npx @modelcontextprotocol/inspector node path/to/server/index.js
37> [tab] CLI
38>
39> ```bash theme={null}
40> # List a server's tools and exit
41> npx @modelcontextprotocol/inspector --cli node path/to/server/index.js --method tools/list
42>
43> # Call a tool and pipe the result into jq
44> npx @modelcontextprotocol/inspector --cli https://api.example.com/mcp --transport http \
45> --method tools/call --tool-name get_weather --tool-arg city=Boston --format json | jq .result
46> ```
47>
48> See [CLI client](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/cli.md).
3049 
31# Or launch with no target and add servers from the UI
32npx @modelcontextprotocol/inspector
33```
50> [tab] TUI
51>
52> ```bash theme={null}
53> npx @modelcontextprotocol/inspector --tui node path/to/server/index.js
54> ```
55>
56> See [TUI client](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/tui.md).
3457 
35The command prints a URL carrying a per-launch API token and opens it in your browser (set `MCP_AUTO_OPEN_ENABLED=false` to stop it opening). See [Web client](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/web.md).
36 
37**CLI**
38 
39```bash theme={null}
40# List a server's tools and exit
41npx @modelcontextprotocol/inspector --cli node path/to/server/index.js --method tools/list
42 
43# Call a tool and pipe the result into jq
44npx @modelcontextprotocol/inspector --cli https://api.example.com/mcp --transport http \
45 --method tools/call --tool-name get_weather --tool-arg city=Boston --format json | jq .result
46```
47 
48See [CLI client](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/cli.md).
49 
50**TUI**
51 
52```bash theme={null}
53npx @modelcontextprotocol/inspector --tui node path/to/server/index.js
54```
55 
56See [TUI client](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/tui.md).
57 
5858### Inspecting published servers
5959 
6060Pass the command that launches the server as the Inspector's arguments, or point it at a remote server with `--server-url`:
6161 
62**npm package**
62> [tab] npm package
63>
64> ```bash theme={null}
65> npx -y @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem ~/Desktop
66> ```
6367 
64```bash theme={null}
65npx -y @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem ~/Desktop
66```
68> [tab] PyPI package
69>
70> ```bash theme={null}
71> npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git
72> ```
6773 
68**PyPI package**
74> [tab] Remote HTTP server
75>
76> ```bash theme={null}
77> npx @modelcontextprotocol/inspector --server-url https://api.example.com/mcp --transport http
78> ```
6979 
70```bash theme={null}
71npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git
72```
73 
74**Remote HTTP server**
75 
76```bash theme={null}
77npx @modelcontextprotocol/inspector --server-url https://api.example.com/mcp --transport http
78```
79 
8080Always read a server's own README first, since every server requires different commands and arguments.
8181 
8282> [!WARNING]
from line 117
117117 
118118## Where to go next
119119 
120> **[Web client](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/web.md)**
120> [card] [Web client](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/web.md)
121121>
122122> A tab-by-tab walkthrough of the graphical inspector.
123123 
124> **[CLI client](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/cli.md)**
124> [card] [CLI client](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/cli.md)
125125>
126126> Method reference, output formats, exit codes, and CI recipes.
127127 
128> **[TUI client](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/tui.md)**
128> [card] [TUI client](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/tui.md)
129129>
130130> Terminal navigation and keyboard reference.
131131 
132> **[Configuration and flags](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/configuration.md)**
132> [card] [Configuration and flags](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/configuration.md)
133133>
134134> Catalog vs. config files, the full per-client flag reference, and
135135> environment variables.
136136 
137> **[Authorization](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/authorization.md)**
137> [card] [Authorization](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/authorization.md)
138138>
139139> The OAuth flow end to end, mid-session re-authorization, and loopback
140140> callbacks.
141141 
142> **[Protocol eras](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/protocol-eras.md)**
142> [card] [Protocol eras](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/protocol-eras.md)
143143>
144144> Legacy vs. modern (2026-07-28) operation, and how every tab changes between
145145> protocol eras.
146146 
147> **[mcpdo connection client](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/mcpdo.md)**
147> [card] [mcpdo connection client](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/mcpdo.md)
148148>
149149> Connect once, then run many commands against a named connection.
150150 
151> **[Security](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/security.md)**
151> [card] [Security](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/security.md)
152152>
153153> The threat model: the web backend's token, Docker, secret storage, stdio
154154> servers, and the mcpdo daemon.
155155 
156> **[Recipes](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/recipes.md)**
156> [card] [Recipes](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/recipes.md)
157157>
158158> Importing client configs, reviewing MCP Apps, Docker, and network hosting.
159159 
160> **[Debugging guide](https://modelcontextprotocol.io/docs/2026-07-28/tools/debugging.md)**
160> [card] [Debugging guide](https://modelcontextprotocol.io/docs/2026-07-28/tools/debugging.md)
161161>
162162> Broader debugging strategies beyond the Inspector.
163163 

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

from line 51
5151 
5252## Logging
5353 
54**Legacy**
54> [tab] Legacy
55>
56> Logging is **session-scoped**. The client sends `logging/setLevel` once, and the server emits `notifications/message` at or above that level for the rest of the session.
57>
58> The **Logs** tab shows a **Set Active Level** selector plus a **Set** button. Choose a level, click Set, and subsequent server logs stream into the panel.
59>
60> Reproduce with `test-servers/configs/logging-legacy-http.json`.
5561 
56Logging is **session-scoped**. The client sends `logging/setLevel` once, and the server emits `notifications/message` at or above that level for the rest of the session.
62> [tab] Modern
63>
64> `logging/setLevel` is **gone**. Instead the client opts in **per request**, by stamping `_meta["io.modelcontextprotocol/logLevel"]` on each outgoing request. A server MUST NOT emit `notifications/message` for a request that did not opt in.
65>
66> The **Logs** tab therefore shows a **Log Level per Request** control instead. Pick a level and every subsequent request carries the stamp, visible in the Network tab's request body. Logs emitted while handling a request ride that request's SSE response stream.
67>
68> Set the control to **Off** and the `logLevel` key is omitted entirely, so the same tool call produces no logs at all. That silence is correct behavior, not a bug.
69>
70> The per-server default is `debug` (opted in at the most verbose level, since the Inspector is a debugging tool); set `modernLogLevel: "off"` on a server to opt back out by default.
71>
72> Reproduce with `test-servers/configs/logging-modern-http.json`.
5773 
58The **Logs** tab shows a **Set Active Level** selector plus a **Set** button. Choose a level, click Set, and subsequent server logs stream into the panel.
59 
60Reproduce with `test-servers/configs/logging-legacy-http.json`.
61 
62**Modern**
63 
64`logging/setLevel` is **gone**. Instead the client opts in **per request**, by stamping `_meta["io.modelcontextprotocol/logLevel"]` on each outgoing request. A server MUST NOT emit `notifications/message` for a request that did not opt in.
65 
66The **Logs** tab therefore shows a **Log Level per Request** control instead. Pick a level and every subsequent request carries the stamp, visible in the Network tab's request body. Logs emitted while handling a request ride that request's SSE response stream.
67 
68Set the control to **Off** and the `logLevel` key is omitted entirely, so the same tool call produces no logs at all. That silence is correct behavior, not a bug.
69 
70The per-server default is `debug` (opted in at the most verbose level, since the Inspector is a debugging tool); set `modernLogLevel: "off"` on a server to opt back out by default.
71 
72Reproduce with `test-servers/configs/logging-modern-http.json`.
73 
7474![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/logs-legacy.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=ec5fddd86d1fe7ecc1ef83549798f33e)
7575 
7676Legacy: the Logs tab offers a session-scoped Set Active Level control, and a log arrives after calling send\_notification.
from line 83
8383 
8484## Resource subscriptions
8585 
86**Legacy**
86> [tab] Legacy
87>
88> Clicking **Subscribe** on a resource sends `resources/subscribe`. The Subscriptions section lists the URI with no stream chrome. When the resource changes, the server emits `notifications/resources/updated` and the subscribed tile's last-updated time is stamped.
89>
90> Reproduce with `test-servers/configs/subscriptions-legacy-http.json`, which also serves an `update_resource` tool so you can drive the notification round-trip yourself.
8791 
88Clicking **Subscribe** on a resource sends `resources/subscribe`. The Subscriptions section lists the URI with no stream chrome. When the resource changes, the server emits `notifications/resources/updated` and the subscribed tile's last-updated time is stamped.
92> [tab] Modern
93>
94> The same **Subscribe** button instead sends **`subscriptions/listen`**, with a filter carrying `resourceSubscriptions` plus whichever `*ListChanged` opt-ins apply (such as `resourcesListChanged`). The subscription is confirmed when the server sends `notifications/subscriptions/acknowledged`.
95>
96> Because the subscription is now a long-lived stream rather than a session flag, the Subscriptions section grows a **stream-status badge** in its header that moves from `Connecting...` to `Listening`. If the stream drops, the badge shows `Reconnecting...` while the Inspector re-sends `subscriptions/listen`. It shows `Stream ended` once the stream closes for good. It shows `Not acknowledged` when the server answers the listen with a plain result and never sends the acknowledgement; the Inspector does not retry in that case.
97>
98> Reproduce with `test-servers/configs/subscriptions-modern-http.json`. To see the `Not acknowledged` state, use `test-servers/configs/subscriptions-never-acknowledged-http.json`. That server acknowledges your first subscription, then refuses every later listen, so subscribing to a second resource trips the badge.
8999 
90Reproduce with `test-servers/configs/subscriptions-legacy-http.json`, which also serves an `update_resource` tool so you can drive the notification round-trip yourself.
91 
92**Modern**
93 
94The same **Subscribe** button instead sends **`subscriptions/listen`**, with a filter carrying `resourceSubscriptions` plus whichever `*ListChanged` opt-ins apply (such as `resourcesListChanged`). The subscription is confirmed when the server sends `notifications/subscriptions/acknowledged`.
95 
96Because the subscription is now a long-lived stream rather than a session flag, the Subscriptions section grows a **stream-status badge** in its header that moves from `Connecting...` to `Listening`. If the stream drops, the badge shows `Reconnecting...` while the Inspector re-sends `subscriptions/listen`. It shows `Stream ended` once the stream closes for good. It shows `Not acknowledged` when the server answers the listen with a plain result and never sends the acknowledgement; the Inspector does not retry in that case.
97 
98Reproduce with `test-servers/configs/subscriptions-modern-http.json`. To see the `Not acknowledged` state, use `test-servers/configs/subscriptions-never-acknowledged-http.json`. That server acknowledges your first subscription, then refuses every later listen, so subscribing to a second resource trips the badge.
99 
100100![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/resources-subscriptions-modern.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=5ca4c64c6783eb210fe54248f03de144)
101101 
102102A modern subscription: the Subscriptions section carries a LISTENING stream-status badge that a legacy subscription has no need for.
from line 107
107107 
108108Tasks change the most between protocol eras, including *how the Inspector UI tab is gated*.
109109 
110**Legacy**
110> [tab] Legacy
111>
112> The **Tasks** tab appears when the server advertises `capabilities.tasks`. Run a tool with **Run as task** enabled and the tab lists it, populated by `tasks/list` and polled with `tasks/get`. The completed payload is fetched with a **blocking `tasks/result`**, and **Cancel** sends `tasks/cancel`.
113>
114> Reproduce with `test-servers/configs/tasks-legacy-http.json`.
111115 
112The **Tasks** tab appears when the server advertises `capabilities.tasks`. Run a tool with **Run as task** enabled and the tab lists it, populated by `tasks/list` and polled with `tasks/get`. The completed payload is fetched with a **blocking `tasks/result`**, and **Cancel** sends `tasks/cancel`.
113 
114Reproduce with `test-servers/configs/tasks-legacy-http.json`.
115 
116**Modern**
117 
118Tasks are an **extension** (`io.modelcontextprotocol/tasks`, [SEP-2663](https://modelcontextprotocol.io/seps/2663-tasks-extension.md)), so the tab is gated on the server *advertising that extension* rather than on `capabilities.tasks`.
119 
120Run a tool as a task and `tools/call` returns a `CreateTaskResult` (`resultType: "task"`, visible in the Protocol and Network tabs). The Inspector polls **`tasks/get`** only; there is no `tasks/list`, so **Refresh** re-polls the handles the client already knows about. A completed task **inlines its result**, with no blocking `tasks/result` call.
121 
122A task that needs more information moves to `input_required` and surfaces an embedded [elicitation](https://modelcontextprotocol.io/specification/draft/client/elicitation.md) in the pending-request modal (the dialog the web client opens whenever a request is waiting on you). Answering it sends **`tasks/update`** carrying the `inputResponses`, and the next poll completes.
123 
124Reproduce with `test-servers/configs/tasks-modern-http.json` (tools `modern_task` and `modern_input_task`).
116> [tab] Modern
117>
118> Tasks are an **extension** (`io.modelcontextprotocol/tasks`, [SEP-2663](https://modelcontextprotocol.io/seps/2663-tasks-extension.md)), so the tab is gated on the server *advertising that extension* rather than on `capabilities.tasks`.
119>
120> Run a tool as a task and `tools/call` returns a `CreateTaskResult` (`resultType: "task"`, visible in the Protocol and Network tabs). The Inspector polls **`tasks/get`** only; there is no `tasks/list`, so **Refresh** re-polls the handles the client already knows about. A completed task **inlines its result**, with no blocking `tasks/result` call.
121>
122> A task that needs more information moves to `input_required` and surfaces an embedded [elicitation](https://modelcontextprotocol.io/specification/draft/client/elicitation.md) in the pending-request modal (the dialog the web client opens whenever a request is waiting on you). Answering it sends **`tasks/update`** carrying the `inputResponses`, and the next poll completes.
123>
124> Reproduce with `test-servers/configs/tasks-modern-http.json` (tools `modern_task` and `modern_input_task`).
125125 
126126![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tasks-legacy.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=777729bfc1b58b359808c8120d9597d3)
127127 

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

from line 183
183183 
184184Hand the key to the container **as a file** with `MCP_INSPECTOR_SECRET_KEY_FILE`, not as an environment variable. A key passed with `-e MCP_INSPECTOR_SECRET_KEY=…` is readable by anyone who can run `docker inspect` or `docker exec`.
185185 
186**docker run**
186> [tab] docker run
187>
188> ```bash theme={null}
189> docker run --rm -p 127.0.0.1:6274:6274 \
190> -v mcp-inspector-data:/home/node/.mcp-inspector \
191> -v "$HOME/.config/mcp-inspector/secret-key:/run/secrets/mcp_inspector_secret_key:ro" \
192> -e MCP_INSPECTOR_SECRET_KEY_FILE=/run/secrets/mcp_inspector_secret_key \
193> ghcr.io/modelcontextprotocol/inspector
194> ```
187195 
188```bash theme={null}
189docker run --rm -p 127.0.0.1:6274:6274 \
190 -v mcp-inspector-data:/home/node/.mcp-inspector \
191 -v "$HOME/.config/mcp-inspector/secret-key:/run/secrets/mcp_inspector_secret_key:ro" \
192 -e MCP_INSPECTOR_SECRET_KEY_FILE=/run/secrets/mcp_inspector_secret_key \
193 ghcr.io/modelcontextprotocol/inspector
194```
195 
196**Compose secrets**
197 
198```yaml theme={null}
199services:
200 inspector:
201 image: ghcr.io/modelcontextprotocol/inspector
202 ports: ["127.0.0.1:6274:6274"]
203 volumes: ["mcp-inspector-data:/home/node/.mcp-inspector"]
204 environment:
205 MCP_INSPECTOR_SECRET_KEY_FILE: /run/secrets/mcp_inspector_secret_key
206 secrets: [mcp_inspector_secret_key]
207secrets:
208 mcp_inspector_secret_key:
209 file: ${HOME}/.config/mcp-inspector/secret-key
210volumes:
211 mcp-inspector-data:
212```
196> [tab] Compose secrets
197>
198> ```yaml theme={null}
199> services:
200> inspector:
201> image: ghcr.io/modelcontextprotocol/inspector
202> ports: ["127.0.0.1:6274:6274"]
203> volumes: ["mcp-inspector-data:/home/node/.mcp-inspector"]
204> environment:
205> MCP_INSPECTOR_SECRET_KEY_FILE: /run/secrets/mcp_inspector_secret_key
206> secrets: [mcp_inspector_secret_key]
207> secrets:
208> mcp_inspector_secret_key:
209> file: ${HOME}/.config/mcp-inspector/secret-key
210> volumes:
211> mcp-inspector-data:
212> ```
213213 
214214Without Swarm, Compose secrets are bind mounts that keep the host file's owner and mode, so the `0600` key file must be owned by uid `1000`. On a Linux host where your uid is different, run `sudo chown 1000 ~/.config/mcp-inspector/secret-key` rather than loosening its mode. Supply the **same** key on every run.
215215 

docs/2026-07-28/tutorials/security/authorization Changed · +827 / -827 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 246
246246 
247247For 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.
248248 
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`,
330 );
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");
370 }
371 
372 const params = new URLSearchParams({
373 token: token,
374 client_id: CONFIG.auth.clientId,
375 });
376 
377 if (CONFIG.auth.clientSecret) {
378 params.set("client_secret", CONFIG.auth.clientSecret);
379 }
380 
381 let response: Response;
382 try {
383 response = await fetch(endpoint, {
384 method: "POST",
385 headers: {
386 "Content-Type": "application/x-www-form-urlencoded",
387 },
388 body: params.toString(),
389 });
390 } catch (e) {
391 console.error("[auth] introspection fetch threw", e);
392 throw e;
393 }
394 
395 if (!response.ok) {
396 const txt = await response.text();
397 console.error("[auth] introspection non-OK", { status: response.status });
398 
399 try {
400 const obj = JSON.parse(txt);
401 console.log(JSON.stringify(obj, null, 2));
402 } catch {
403 console.error(txt);
404 }
405 throw new Error(`Invalid or expired token: ${txt}`);
406 }
407 
408 let data: any;
409 try {
410 data = await response.json();
411 } catch (e) {
412 const txt = await response.text();
413 console.error("[auth] failed to parse introspection JSON", {
414 error: String(e),
415 body: txt,
416 });
417 throw e;
418 }
419 
420 if (data.active === false) {
421 throw new Error("Inactive token");
422 }
423 
424 if (!data.aud) {
425 throw new Error("Resource indicator (aud) missing");
426 }
427 
428 const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
429 const allowed = audiences.some((a) => {
430 try {
431 return checkResourceAllowed({
432 requestedResource: a,
433 configuredResource: mcpServerUrl,
434 });
435 } catch {
436 // Keycloak tokens include non-URL audiences (e.g. "account", "test-client").
437 // Those are never our resource, so treat them as "no match" instead of crashing.
438 return false;
439 }
440 });
441 if (!allowed) {
442 throw new Error(
443 `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
444 );
445 }
446 
447 return {
448 token,
449 clientId: data.client_id,
450 scopes: data.scope ? data.scope.split(" ") : [],
451 expiresAt: data.exp,
452 };
453 },
454};
455app.use(
456 mcpAuthMetadataRouter({
457 oauthMetadata,
458 resourceServerUrl: mcpServerUrl,
459 scopesSupported: ["mcp:tools"],
460 resourceName: "MCP Demo Server",
461 }),
462);
463 
464const authMiddleware = requireBearerAuth({
465 verifier: tokenVerifier,
466 requiredScopes: [],
467 resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
468});
469 
470const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
471 
472function createMcpServer() {
473 const server = new McpServer({
474 name: "example-server",
475 version: "1.0.0",
476 });
477 
478 server.registerTool(
479 "add",
480 {
481 title: "Addition Tool",
482 description: "Add two numbers together",
483 inputSchema: {
484 a: z.number().describe("First number to add"),
485 b: z.number().describe("Second number to add"),
486 },
487 },
488 async ({ a, b }) => ({
489 content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
490 }),
491 );
492 
493 server.registerTool(
494 "multiply",
495 {
496 title: "Multiplication Tool",
497 description: "Multiply two numbers together",
498 inputSchema: {
499 x: z.number().describe("First number to multiply"),
500 y: z.number().describe("Second number to multiply"),
501 },
502 },
503 async ({ x, y }) => ({
504 content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
505 }),
506 );
507 
508 return server;
509}
510 
511const mcpPostHandler = async (req: express.Request, res: express.Response) => {
512 const sessionId = req.headers["mcp-session-id"] as string | undefined;
513 let transport: StreamableHTTPServerTransport;
514 
515 if (sessionId && transports[sessionId]) {
516 transport = transports[sessionId];
517 } else if (!sessionId && isInitializeRequest(req.body)) {
518 transport = new StreamableHTTPServerTransport({
519 sessionIdGenerator: () => randomUUID(),
520 onsessioninitialized: (sessionId) => {
521 transports[sessionId] = transport;
522 },
523 });
524 
525 transport.onclose = () => {
526 if (transport.sessionId) {
527 delete transports[transport.sessionId];
528 }
529 };
530 
531 const server = createMcpServer();
532 await server.connect(transport);
533 } else {
534 res.status(400).json({
535 jsonrpc: "2.0",
536 error: {
537 code: -32000,
538 message: "Bad Request: No valid session ID provided",
539 },
540 id: null,
541 });
542 return;
543 }
544 
545 await transport.handleRequest(req, res, req.body);
546};
547 
548const handleSessionRequest = async (
549 req: express.Request,
550 res: express.Response,
551) => {
552 const sessionId = req.headers["mcp-session-id"] as string | undefined;
553 if (!sessionId || !transports[sessionId]) {
554 res.status(400).send("Invalid or missing session ID");
555 return;
556 }
557 
558 const transport = transports[sessionId];
559 await transport.handleRequest(req, res);
560};
561 
562app.post("/", authMiddleware, mcpPostHandler);
563app.get("/", authMiddleware, handleSessionRequest);
564app.delete("/", authMiddleware, handleSessionRequest);
565 
566app.listen(CONFIG.port, CONFIG.host, () => {
567 console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
568 console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
569 console.log(
570 `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
571 );
572});
573```
574 
575When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
576 
577For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
578 
579**Python**
580 
581You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
582 
583To simplify our authorization interaction, in Python scenarios we rely on the `MCPServer` class from the [Python SDK](https://py.sdk.modelcontextprotocol.io/v2/run/authorization/). It publishes the Protected Resource Metadata document, answers unauthenticated requests with a `401` whose `WWW-Authenticate` header points back at that document, and hands every bearer token to a verifier that we supply. Many of the conventions around authorization, like the endpoints and token validation logic, are consistent across languages, but some offer simpler ways of integrating them in production scenarios.
584 
585Prior to writing the actual server, we need to set up our configuration in `config.py` - the contents are entirely based on your local server setup:
586 
587```python theme={null}
588"""Configuration settings for the MCP auth server."""
589 
590import os
591 
592 
593class Config:
594 """Configuration class that loads from environment variables with sensible defaults."""
595 
596 # Server settings
597 HOST: str = os.getenv("HOST", "localhost")
598 PORT: int = int(os.getenv("PORT", "3000"))
599 
600 # Auth server settings
601 AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
602 AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
603 AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
604 
605 # OAuth client settings
606 OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "test-client")
607 OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "")
608 
609 # Scope required on every token
610 MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
611 
612 @property
613 def server_url(self) -> str:
614 """Build the server URL."""
615 return f"http://{self.HOST}:{self.PORT}"
616 
617 @property
618 def auth_base_url(self) -> str:
619 """Build the auth server base URL."""
620 return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
621 
622 
623# Global configuration instance
624config = Config()
625```
626 
627`OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier. Set them in your environment before starting the server.
628 
629The server implementation is as follows:
630 
631```python theme={null}
632import datetime
633import logging
634from typing import Any
635from urllib.parse import urljoin
636 
637from pydantic import AnyHttpUrl
638 
639from mcp.server import MCPServer
640from mcp.server.auth.settings import AuthSettings
641 
642from .config import config
643from .token_verifier import IntrospectionTokenVerifier
644 
645logger = logging.getLogger(__name__)
646 
647 
648def create_oauth_urls() -> dict[str, str]:
649 """Create OAuth URLs based on configuration (Keycloak-style)."""
650 auth_base_url = config.auth_base_url
651 
652 return {
653 "issuer": auth_base_url,
654 "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
655 "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
656 "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
657 }
658 
659 
660def create_server() -> MCPServer:
661 """Create and configure the MCP server."""
662 
663 oauth_urls = create_oauth_urls()
664 
665 token_verifier = IntrospectionTokenVerifier(
666 introspection_endpoint=oauth_urls["introspection_endpoint"],
667 server_url=config.server_url,
668 client_id=config.OAUTH_CLIENT_ID,
669 client_secret=config.OAUTH_CLIENT_SECRET,
670 )
671 
672 app = MCPServer(
673 name="MCP Resource Server",
674 instructions="Resource Server that validates tokens via Authorization Server introspection",
675 debug=True,
676 token_verifier=token_verifier,
677 auth=AuthSettings(
678 issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
679 required_scopes=[config.MCP_SCOPE],
680 resource_server_url=AnyHttpUrl(config.server_url),
681 ),
682 )
683 
684 @app.tool()
685 async def add_numbers(a: float, b: float) -> dict[str, Any]:
686 """
687 Add two numbers together.
688 This tool demonstrates basic arithmetic operations with OAuth authentication.
689 
690 Args:
691 a: The first number to add
692 b: The second number to add
693 """
694 result = a + b
695 return {
696 "operation": "addition",
697 "operand_a": a,
698 "operand_b": b,
699 "result": result,
700 "timestamp": datetime.datetime.now().isoformat(),
701 }
702 
703 @app.tool()
704 async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
705 """
706 Multiply two numbers together.
707 This tool demonstrates basic arithmetic operations with OAuth authentication.
708 
709 Args:
710 x: The first number to multiply
711 y: The second number to multiply
712 """
713 result = x * y
714 return {
715 "operation": "multiplication",
716 "operand_x": x,
717 "operand_y": y,
718 "result": result,
719 "timestamp": datetime.datetime.now().isoformat(),
720 }
721 
722 return app
723 
724 
725def main() -> int:
726 """
727 Run the MCP Resource Server.
728 
729 This server:
730 - Provides RFC 9728 Protected Resource Metadata
731 - Validates tokens via Authorization Server introspection
732 - Serves MCP tools requiring authentication
733 
734 Configuration is loaded from config.py and environment variables.
735 """
736 logging.basicConfig(level=logging.INFO)
737 
738 oauth_urls = create_oauth_urls()
739 
740 try:
741 mcp_server = create_server()
742 
743 logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
744 logger.info("Authorization Server: %s", oauth_urls["issuer"])
745 
746 mcp_server.run(
747 transport="streamable-http",
748 host=config.HOST,
749 port=config.PORT,
750 streamable_http_path="/",
751 )
752 return 0
753 
754 except Exception:
755 logger.exception("Server error")
756 return 1
757 
758 
759if __name__ == "__main__":
760 exit(main())
761```
762 
763Lastly, the token verification logic is delegated entirely to `token_verifier.py`, ensuring that we can use the Keycloak introspection endpoint to verify the validity of any credential artifacts.
764 
765```python theme={null}
766"""Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
767 
768import logging
769from typing import Any
770 
771import httpx2
772 
773from mcp.server.auth.provider import AccessToken, TokenVerifier
774from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
775 
776logger = logging.getLogger(__name__)
777 
778 
779class IntrospectionTokenVerifier(TokenVerifier):
780 """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662)."""
781 
782 def __init__(
783 self,
784 introspection_endpoint: str,
785 server_url: str,
786 client_id: str,
787 client_secret: str,
788 ):
789 self.introspection_endpoint = introspection_endpoint
790 self.server_url = server_url
791 self.client_id = client_id
792 self.client_secret = client_secret
793 self.resource_url = resource_url_from_server_url(server_url)
794 
795 async def verify_token(self, token: str) -> AccessToken | None:
796 """Verify token via introspection endpoint."""
797 if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
798 return None
799 
800 timeout = httpx2.Timeout(10.0, connect=5.0)
801 limits = httpx2.Limits(max_connections=10, max_keepalive_connections=5)
802 
803 async with httpx2.AsyncClient(
804 timeout=timeout,
805 limits=limits,
806 verify=True,
807 ) as client:
808 try:
809 form_data = {
810 "token": token,
811 "client_id": self.client_id,
812 }
813 # Only send client_secret when one is configured
814 # Public clients authenticate with client_id alone.
815 if self.client_secret:
816 form_data["client_secret"] = self.client_secret
817 headers = {"Content-Type": "application/x-www-form-urlencoded"}
818 
819 response = await client.post(
820 self.introspection_endpoint,
821 data=form_data,
822 headers=headers,
823 )
824 
825 if response.status_code != 200:
826 return None
827 
828 data = response.json()
829 if not data.get("active", False):
830 return None
831 
832 if not self._validate_resource(data):
833 return None
834 
835 return AccessToken(
836 token=token,
837 client_id=data.get("client_id", "unknown"),
838 scopes=data.get("scope", "").split() if data.get("scope") else [],
839 expires_at=data.get("exp"),
840 # AccessToken.resource is `str | None`. Keycloak returns `aud`
841 # as a *list* here (e.g. ["test-client", "http://localhost:3000",
842 # "account"]); passing that list straight in raises a pydantic
843 # ValidationError that the broad `except` below turns into a
844 # silent 401. We already confirmed this server's resource is a
845 # valid audience in `_validate_resource`, so record that.
846 resource=self.resource_url,
847 subject=data.get("sub"), # RFC 7662 subject (resource owner)
848 claims=data,
849 )
850 
851 except Exception:
852 logger.exception("Token introspection failed")
853 return None
854 
855 def _validate_resource(self, token_data: dict[str, Any]) -> bool:
856 """Validate token was issued for this resource server.
857 
858 Rules:
859 - Reject if 'aud' missing.
860 - Accept if any audience entry matches the derived resource URL.
861 - Supports string or list forms per JWT spec.
862 """
863 if not self.server_url or not self.resource_url:
864 return False
865 
866 aud: list[str] | str | None = token_data.get("aud")
867 if isinstance(aud, list):
868 return any(self._is_valid_resource(a) for a in aud)
869 if isinstance(aud, str):
870 return self._is_valid_resource(aud)
871 return False
872 
873 def _is_valid_resource(self, resource: str) -> bool:
874 """Check if the given resource matches our server."""
875 return check_resource_allowed(requested_resource=self.resource_url, configured_resource=resource)
876```
877 
878For more details, see below or the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
879 
880**Python MCP Server**
881 
882In the server's root have a `pyproject.toml` file and a `mcp_server` folder. Put all the Python files in the `mcp_server` folder, and fill the `pyproject.toml` file like:
883 
884```toml theme={null}
885[project]
886name = "mcp-simple-auth"
887version = "0.1.0"
888description = "A simple MCP server demonstrating OAuth authentication"
889requires-python = ">=3.10"
890authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
891license = { text = "MIT" }
892dependencies = [
893 "httpx2>=2.5.0",
894 "mcp>=2.0.0rc1",
895 "pydantic>=2.0",
896]
897 
898[project.scripts]
899mcp-simple-auth-rs = "mcp_server.server:main"
900 
901[build-system]
902requires = ["hatchling"]
903build-backend = "hatchling.build"
904 
905[tool.hatch.build.targets.wheel]
906packages = ["mcp_server"]
907 
908[dependency-groups]
909dev = ["pyright>=1.1.391", "pytest>=8.3.4", "ruff>=0.8.5"]
910```
911 
912Then run the commands below to start the server.
913 
914```bash theme={null}
915uv sync
916uv run mcp-simple-auth-rs
917```
918 
919**C#**
920 
921You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
922 
923To set up authorization in your MCP server using the MCP C# SDK, you can lean on the standard ASP.NET Core builder pattern. Instead of using the introspection endpoint provided by Keycloak, we will use built-in ASP.NET Core capabilities for token validation.
924 
925In the root of your server folder, create two files, `Program.cs` and `ProtectedMcpServer.csproj`, and a `Tools` folder. Fill `Program.cs` with:
926 
927```csharp theme={null}
928using Microsoft.AspNetCore.Authentication.JwtBearer;
929using Microsoft.IdentityModel.Tokens;
930using ModelContextProtocol.AspNetCore.Authentication;
931using ProtectedMcpServer.Tools;
932using System.Security.Claims;
933 
934var builder = WebApplication.CreateBuilder(args);
935 
936var serverUrl = "http://localhost:3000/";
937var authorizationServerUrl = "http://localhost:8080/realms/master/";
938 
939builder.Services.AddAuthentication(options =>
940{
941 options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
942 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
943})
944.AddJwtBearer(options =>
945{
946 options.Authority = authorizationServerUrl;
947 var normalizedServerAudience = serverUrl.TrimEnd('/');
948 options.TokenValidationParameters = new TokenValidationParameters
949 {
950 ValidIssuer = authorizationServerUrl,
951 ValidAudiences = new[] { normalizedServerAudience, serverUrl },
952 AudienceValidator = (audiences, securityToken, validationParameters) =>
953 {
954 if (audiences == null) return false;
955 foreach (var aud in audiences)
956 {
957 if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
958 {
959 return true;
960 }
961 }
962 return false;
963 }
964 };
965 
966 options.RequireHttpsMetadata = false; // Set to true in production
967 
968 options.Events = new JwtBearerEvents
969 {
970 OnTokenValidated = context =>
971 {
972 var name = context.Principal?.Identity?.Name ?? "unknown";
973 var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
974 Console.WriteLine($"Token validated for: {name} ({email})");
975 return Task.CompletedTask;
976 },
977 OnAuthenticationFailed = context =>
978 {
979 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
980 return Task.CompletedTask;
981 },
982 };
983})
984.AddMcp(options =>
985{
986 options.ResourceMetadata = new()
987 {
988 Resource = serverUrl,
989 ResourceDocumentation = "https://docs.example.com/api/math",
990 AuthorizationServers = { authorizationServerUrl },
991 ScopesSupported = ["mcp:tools"]
992 };
993});
994 
995builder.Services.AddAuthorization();
996 
997builder.Services.AddHttpContextAccessor();
998builder.Services.AddMcpServer()
999 .WithTools<MathTools>()
1000 .WithHttpTransport();
1001 
1002var app = builder.Build();
1003 
1004app.UseAuthentication();
1005app.UseAuthorization();
1006 
1007app.MapMcp().RequireAuthorization();
1008 
1009Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
1010Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
1011Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
1012Console.WriteLine("Exposed Math tools: Add, Multiply");
1013Console.WriteLine("Press Ctrl+C to stop the server");
1014 
1015app.Run(serverUrl);
1016```
1017 
1018Fill `ProtectedMcpServer.csproj` with:
1019 
1020```xml theme={null}
1021<Project Sdk="Microsoft.NET.Sdk.Web">
1022 
1023 <PropertyGroup>
1024 <TargetFramework>net9.0</TargetFramework>
1025 <Nullable>enable</Nullable>
1026 <ImplicitUsings>enable</ImplicitUsings>
1027 <!-- Identifier for the local secret store, not a secret itself. -->
1028 <UserSecretsId>local-authorization-mcp-server</UserSecretsId>
1029 </PropertyGroup>
1030 
1031 <ItemGroup>
1032 <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.18" />
1033 <PackageReference Include="ModelContextProtocol" Version="2.0.0" />
1034 <PackageReference Include="ModelContextProtocol.AspNetCore" Version="2.0.0" />
1035 </ItemGroup>
1036 
1037</Project>
1038```
1039 
1040In the `Tools` folder, create `MathTools.cs` and fill it with:
1041 
1042```csharp theme={null}
1043using System.ComponentModel;
1044using ModelContextProtocol.Server;
1045 
1046namespace ProtectedMcpServer.Tools;
1047 
1048[McpServerToolType]
1049public sealed class MathTools
1050{
1051 [McpServerTool, Description("Add two numbers together.")]
1052 public Task<double> Add(
1053 [Description("First operand")] double a,
1054 [Description("Second operand")] double b)
1055 {
1056 return Task.FromResult(a + b);
1057 }
1058 
1059 [McpServerTool, Description("Multiply two numbers together.")]
1060 public Task<double> Multiply(
1061 [Description("First operand")] double a,
1062 [Description("Second operand")] double b)
1063 {
1064 return Task.FromResult(a * b);
1065 }
1066}
1067```
1068 
1069Then from the server's root, run:
1070 
1071```bash theme={null}
1072dotnet run
1073```
1074 
1075For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
249> [tab] TypeScript
250>
251> You can see the complete TypeScript project in the [sample repository](https://github.com/localden/min-ts-mcp-auth).
252>
253> Prior 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
257> HOST=localhost
258> PORT=3000
259>
260> # Auth server location
261> AUTH_HOST=localhost
262> AUTH_PORT=8080
263> AUTH_REALM=master
264>
265> # Keycloak OAuth client credentials
266> OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
267> OAUTH_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>
272> 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.
273>
274> ```typescript theme={null}
275> import "dotenv/config";
276> import express from "express";
277> import { randomUUID } from "node:crypto";
278> import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
279> import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
280> import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
281> import { z } from "zod";
282> import cors from "cors";
283> import {
284> mcpAuthMetadataRouter,
285> getOAuthProtectedResourceMetadataUrl,
286> } from "@modelcontextprotocol/sdk/server/auth/router.js";
287> import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
288> import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
289> import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
290> const 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>
302> function 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>
323> function 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`,
330> );
331> });
332> next();
333> };
334> }
335>
336> const app = express();
337>
338> app.use(
339> express.json({
340> verify: (req: any, _res, buf) => {
341> req.rawBody = buf?.toString() ?? "";
342> },
343> }),
344> );
345>
346> app.use(
347> cors({
348> origin: "*",
349> exposedHeaders: ["Mcp-Session-Id"],
350> }),
351> );
352>
353> app.use(createRequestLogger());
354>
355> const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
356> const oauthUrls = createOAuthUrls();
357>
358> const oauthMetadata: OAuthMetadata = {
359> ...oauthUrls,
360> response_types_supported: ["code"],
361> };
362>
363> const tokenVerifier = {
364> verifyAccessToken: async (token: string) => {
365> const endpoint = oauthMetadata.introspection_endpoint;
366>
367> if (!endpoint) {
368> console.error("[auth] no introspection endpoint in metadata");
369> throw new Error("No token verification endpoint available in metadata");
370> }
371>
372> const params = new URLSearchParams({
373> token: token,
374> client_id: CONFIG.auth.clientId,
375> });
376>
377> if (CONFIG.auth.clientSecret) {
378> params.set("client_secret", CONFIG.auth.clientSecret);
379> }
380>
381> let response: Response;
382> try {
383> response = await fetch(endpoint, {
384> method: "POST",
385> headers: {
386> "Content-Type": "application/x-www-form-urlencoded",
387> },
388> body: params.toString(),
389> });
390> } catch (e) {
391> console.error("[auth] introspection fetch threw", e);
392> throw e;
393> }
394>
395> if (!response.ok) {
396> const txt = await response.text();
397> console.error("[auth] introspection non-OK", { status: response.status });
398>
399> try {
400> const obj = JSON.parse(txt);
401> console.log(JSON.stringify(obj, null, 2));
402> } catch {
403> console.error(txt);
404> }
405> throw new Error(`Invalid or expired token: ${txt}`);
406> }
407>
408> let data: any;
409> try {
410> data = await response.json();
411> } catch (e) {
412> const txt = await response.text();
413> console.error("[auth] failed to parse introspection JSON", {
414> error: String(e),
415> body: txt,
416> });
417> throw e;
418> }
419>
420> if (data.active === false) {
421> throw new Error("Inactive token");
422> }
423>
424> if (!data.aud) {
425> throw new Error("Resource indicator (aud) missing");
426> }
427>
428> const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
429> const allowed = audiences.some((a) => {
430> try {
431> return checkResourceAllowed({
432> requestedResource: a,
433> configuredResource: mcpServerUrl,
434> });
435> } catch {
436> // Keycloak tokens include non-URL audiences (e.g. "account", "test-client").
437> // Those are never our resource, so treat them as "no match" instead of crashing.
438> return false;
439> }
440> });
441> if (!allowed) {
442> throw new Error(
443> `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
444> );
445> }
446>
447> return {
448> token,
449> clientId: data.client_id,
450> scopes: data.scope ? data.scope.split(" ") : [],
451> expiresAt: data.exp,
452> };
453> },
454> };
455> app.use(
456> mcpAuthMetadataRouter({
457> oauthMetadata,
458> resourceServerUrl: mcpServerUrl,
459> scopesSupported: ["mcp:tools"],
460> resourceName: "MCP Demo Server",
461> }),
462> );
463>
464> const authMiddleware = requireBearerAuth({
465> verifier: tokenVerifier,
466> requiredScopes: [],
467> resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
468> });
469>
470> const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
471>
472> function createMcpServer() {
473> const server = new McpServer({
474> name: "example-server",
475> version: "1.0.0",
476> });
477>
478> server.registerTool(
479> "add",
480> {
481> title: "Addition Tool",
482> description: "Add two numbers together",
483> inputSchema: {
484> a: z.number().describe("First number to add"),
485> b: z.number().describe("Second number to add"),
486> },
487> },
488> async ({ a, b }) => ({
489> content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
490> }),
491> );
492>
493> server.registerTool(
494> "multiply",
495> {
496> title: "Multiplication Tool",
497> description: "Multiply two numbers together",
498> inputSchema: {
499> x: z.number().describe("First number to multiply"),
500> y: z.number().describe("Second number to multiply"),
501> },
502> },
503> async ({ x, y }) => ({
504> content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
505> }),
506> );
507>
508> return server;
509> }
510>
511> const mcpPostHandler = async (req: express.Request, res: express.Response) => {
512> const sessionId = req.headers["mcp-session-id"] as string | undefined;
513> let transport: StreamableHTTPServerTransport;
514>
515> if (sessionId && transports[sessionId]) {
516> transport = transports[sessionId];
517> } else if (!sessionId && isInitializeRequest(req.body)) {
518> transport = new StreamableHTTPServerTransport({
519> sessionIdGenerator: () => randomUUID(),
520> onsessioninitialized: (sessionId) => {
521> transports[sessionId] = transport;
522> },
523> });
524>
525> transport.onclose = () => {
526> if (transport.sessionId) {
527> delete transports[transport.sessionId];
528> }
529> };
530>
531> const server = createMcpServer();
532> await server.connect(transport);
533> } else {
534> res.status(400).json({
535> jsonrpc: "2.0",
536> error: {
537> code: -32000,
538> message: "Bad Request: No valid session ID provided",
539> },
540> id: null,
541> });
542> return;
543> }
544>
545> await transport.handleRequest(req, res, req.body);
546> };
547>
548> const handleSessionRequest = async (
549> req: express.Request,
550> res: express.Response,
551> ) => {
552> const sessionId = req.headers["mcp-session-id"] as string | undefined;
553> if (!sessionId || !transports[sessionId]) {
554> res.status(400).send("Invalid or missing session ID");
555> return;
556> }
557>
558> const transport = transports[sessionId];
559> await transport.handleRequest(req, res);
560> };
561>
562> app.post("/", authMiddleware, mcpPostHandler);
563> app.get("/", authMiddleware, handleSessionRequest);
564> app.delete("/", authMiddleware, handleSessionRequest);
565>
566> app.listen(CONFIG.port, CONFIG.host, () => {
567> console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
568> console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
569> console.log(
570> `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
571> );
572> });
573> ```
574>
575> When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
576>
577> For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
578 
579> [tab] Python
580>
581> You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
582>
583> To simplify our authorization interaction, in Python scenarios we rely on the `MCPServer` class from the [Python SDK](https://py.sdk.modelcontextprotocol.io/v2/run/authorization/). It publishes the Protected Resource Metadata document, answers unauthenticated requests with a `401` whose `WWW-Authenticate` header points back at that document, and hands every bearer token to a verifier that we supply. Many of the conventions around authorization, like the endpoints and token validation logic, are consistent across languages, but some offer simpler ways of integrating them in production scenarios.
584>
585> 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:
586>
587> ```python theme={null}
588> """Configuration settings for the MCP auth server."""
589>
590> import os
591>
592>
593> class Config:
594> """Configuration class that loads from environment variables with sensible defaults."""
595>
596> # Server settings
597> HOST: str = os.getenv("HOST", "localhost")
598> PORT: int = int(os.getenv("PORT", "3000"))
599>
600> # Auth server settings
601> AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
602> AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
603> AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
604>
605> # OAuth client settings
606> OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "test-client")
607> OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "")
608>
609> # Scope required on every token
610> MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
611>
612> @property
613> def server_url(self) -> str:
614> """Build the server URL."""
615> return f"http://{self.HOST}:{self.PORT}"
616>
617> @property
618> def auth_base_url(self) -> str:
619> """Build the auth server base URL."""
620> return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
621>
622>
623> # Global configuration instance
624> config = Config()
625> ```
626>
627> `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier. Set them in your environment before starting the server.
628>
629> The server implementation is as follows:
630>
631> ```python theme={null}
632> import datetime
633> import logging
634> from typing import Any
635> from urllib.parse import urljoin
636>
637> from pydantic import AnyHttpUrl
638>
639> from mcp.server import MCPServer
640> from mcp.server.auth.settings import AuthSettings
641>
642> from .config import config
643> from .token_verifier import IntrospectionTokenVerifier
644>
645> logger = logging.getLogger(__name__)
646>
647>
648> def create_oauth_urls() -> dict[str, str]:
649> """Create OAuth URLs based on configuration (Keycloak-style)."""
650> auth_base_url = config.auth_base_url
651>
652> return {
653> "issuer": auth_base_url,
654> "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
655> "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
656> "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
657> }
658>
659>
660> def create_server() -> MCPServer:
661> """Create and configure the MCP server."""
662>
663> oauth_urls = create_oauth_urls()
664>
665> token_verifier = IntrospectionTokenVerifier(
666> introspection_endpoint=oauth_urls["introspection_endpoint"],
667> server_url=config.server_url,
668> client_id=config.OAUTH_CLIENT_ID,
669> client_secret=config.OAUTH_CLIENT_SECRET,
670> )
671>
672> app = MCPServer(
673> name="MCP Resource Server",
674> instructions="Resource Server that validates tokens via Authorization Server introspection",
675> debug=True,
676> token_verifier=token_verifier,
677> auth=AuthSettings(
678> issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
679> required_scopes=[config.MCP_SCOPE],
680> resource_server_url=AnyHttpUrl(config.server_url),
681> ),
682> )
683>
684> @app.tool()
685> async def add_numbers(a: float, b: float) -> dict[str, Any]:
686> """
687> Add two numbers together.
688> This tool demonstrates basic arithmetic operations with OAuth authentication.
689>
690> Args:
691> a: The first number to add
692> b: The second number to add
693> """
694> result = a + b
695> return {
696> "operation": "addition",
697> "operand_a": a,
698> "operand_b": b,
699> "result": result,
700> "timestamp": datetime.datetime.now().isoformat(),
701> }
702>
703> @app.tool()
704> async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
705> """
706> Multiply two numbers together.
707> This tool demonstrates basic arithmetic operations with OAuth authentication.
708>
709> Args:
710> x: The first number to multiply
711> y: The second number to multiply
712> """
713> result = x * y
714> return {
715> "operation": "multiplication",
716> "operand_x": x,
717> "operand_y": y,
718> "result": result,
719> "timestamp": datetime.datetime.now().isoformat(),
720> }
721>
722> return app
723>
724>
725> def main() -> int:
726> """
727> Run the MCP Resource Server.
728>
729> This server:
730> - Provides RFC 9728 Protected Resource Metadata
731> - Validates tokens via Authorization Server introspection
732> - Serves MCP tools requiring authentication
733>
734> Configuration is loaded from config.py and environment variables.
735> """
736> logging.basicConfig(level=logging.INFO)
737>
738> oauth_urls = create_oauth_urls()
739>
740> try:
741> mcp_server = create_server()
742>
743> logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
744> logger.info("Authorization Server: %s", oauth_urls["issuer"])
745>
746> mcp_server.run(
747> transport="streamable-http",
748> host=config.HOST,
749> port=config.PORT,
750> streamable_http_path="/",
751> )
752> return 0
753>
754> except Exception:
755> logger.exception("Server error")
756> return 1
757>
758>
759> if __name__ == "__main__":
760> exit(main())
761> ```
762>
763> 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.
764>
765> ```python theme={null}
766> """Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
767>
768> import logging
769> from typing import Any
770>
771> import httpx2
772>
773> from mcp.server.auth.provider import AccessToken, TokenVerifier
774> from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
775>
776> logger = logging.getLogger(__name__)
777>
778>
779> class IntrospectionTokenVerifier(TokenVerifier):
780> """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662)."""
781>
782> def __init__(
783> self,
784> introspection_endpoint: str,
785> server_url: str,
786> client_id: str,
787> client_secret: str,
788> ):
789> self.introspection_endpoint = introspection_endpoint
790> self.server_url = server_url
791> self.client_id = client_id
792> self.client_secret = client_secret
793> self.resource_url = resource_url_from_server_url(server_url)
794>
795> async def verify_token(self, token: str) -> AccessToken | None:
796> """Verify token via introspection endpoint."""
797> if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
798> return None
799>
800> timeout = httpx2.Timeout(10.0, connect=5.0)
801> limits = httpx2.Limits(max_connections=10, max_keepalive_connections=5)
802>
803> async with httpx2.AsyncClient(
804> timeout=timeout,
805> limits=limits,
806> verify=True,
807> ) as client:
808> try:
809> form_data = {
810> "token": token,
811> "client_id": self.client_id,
812> }
813> # Only send client_secret when one is configured
814> # Public clients authenticate with client_id alone.
815> if self.client_secret:
816> form_data["client_secret"] = self.client_secret
817> headers = {"Content-Type": "application/x-www-form-urlencoded"}
818>
819> response = await client.post(
820> self.introspection_endpoint,
821> data=form_data,
822> headers=headers,
823> )
824>
825> if response.status_code != 200:
826> return None
827>
828> data = response.json()
829> if not data.get("active", False):
830> return None
831>
832> if not self._validate_resource(data):
833> return None
834>
835> return AccessToken(
836> token=token,
837> client_id=data.get("client_id", "unknown"),
838> scopes=data.get("scope", "").split() if data.get("scope") else [],
839> expires_at=data.get("exp"),
840> # AccessToken.resource is `str | None`. Keycloak returns `aud`
841> # as a *list* here (e.g. ["test-client", "http://localhost:3000",
842> # "account"]); passing that list straight in raises a pydantic
843> # ValidationError that the broad `except` below turns into a
844> # silent 401. We already confirmed this server's resource is a
845> # valid audience in `_validate_resource`, so record that.
846> resource=self.resource_url,
847> subject=data.get("sub"), # RFC 7662 subject (resource owner)
848> claims=data,
849> )
850>
851> except Exception:
852> logger.exception("Token introspection failed")
853> return None
854>
855> def _validate_resource(self, token_data: dict[str, Any]) -> bool:
856> """Validate token was issued for this resource server.
857>
858> Rules:
859> - Reject if 'aud' missing.
860> - Accept if any audience entry matches the derived resource URL.
861> - Supports string or list forms per JWT spec.
862> """
863> if not self.server_url or not self.resource_url:
864> return False
865>
866> aud: list[str] | str | None = token_data.get("aud")
867> if isinstance(aud, list):
868> return any(self._is_valid_resource(a) for a in aud)
869> if isinstance(aud, str):
870> return self._is_valid_resource(aud)
871> return False
872>
873> def _is_valid_resource(self, resource: str) -> bool:
874> """Check if the given resource matches our server."""
875> return check_resource_allowed(requested_resource=self.resource_url, configured_resource=resource)
876> ```
877>
878> For more details, see below or the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
879>
880> **Python MCP Server**
881>
882> In the server's root have a `pyproject.toml` file and a `mcp_server` folder. Put all the Python files in the `mcp_server` folder, and fill the `pyproject.toml` file like:
883>
884> ```toml theme={null}
885> [project]
886> name = "mcp-simple-auth"
887> version = "0.1.0"
888> description = "A simple MCP server demonstrating OAuth authentication"
889> requires-python = ">=3.10"
890> authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
891> license = { text = "MIT" }
892> dependencies = [
893> "httpx2>=2.5.0",
894> "mcp>=2.0.0rc1",
895> "pydantic>=2.0",
896> ]
897>
898> [project.scripts]
899> mcp-simple-auth-rs = "mcp_server.server:main"
900>
901> [build-system]
902> requires = ["hatchling"]
903> build-backend = "hatchling.build"
904>
905> [tool.hatch.build.targets.wheel]
906> packages = ["mcp_server"]
907>
908> [dependency-groups]
909> dev = ["pyright>=1.1.391", "pytest>=8.3.4", "ruff>=0.8.5"]
910> ```
911>
912> Then run the commands below to start the server.
913>
914> ```bash theme={null}
915> uv sync
916> uv run mcp-simple-auth-rs
917> ```
918 
919> [tab] C#
920>
921> You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
922>
923> 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.
924>
925> In the root of your server folder, create two files, `Program.cs` and `ProtectedMcpServer.csproj`, and a `Tools` folder. Fill `Program.cs` with:
926>
927> ```csharp theme={null}
928> using Microsoft.AspNetCore.Authentication.JwtBearer;
929> using Microsoft.IdentityModel.Tokens;
930> using ModelContextProtocol.AspNetCore.Authentication;
931> using ProtectedMcpServer.Tools;
932> using System.Security.Claims;
933>
934> var builder = WebApplication.CreateBuilder(args);
935>
936> var serverUrl = "http://localhost:3000/";
937> var authorizationServerUrl = "http://localhost:8080/realms/master/";
938>
939> builder.Services.AddAuthentication(options =>
940> {
941> options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
942> options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
943> })
944> .AddJwtBearer(options =>
945> {
946> options.Authority = authorizationServerUrl;
947> var normalizedServerAudience = serverUrl.TrimEnd('/');
948> options.TokenValidationParameters = new TokenValidationParameters
949> {
950> ValidIssuer = authorizationServerUrl,
951> ValidAudiences = new[] { normalizedServerAudience, serverUrl },
952> AudienceValidator = (audiences, securityToken, validationParameters) =>
953> {
954> if (audiences == null) return false;
955> foreach (var aud in audiences)
956> {
957> if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
958> {
959> return true;
960> }
961> }
962> return false;
963> }
964> };
965>
966> options.RequireHttpsMetadata = false; // Set to true in production
967>
968> options.Events = new JwtBearerEvents
969> {
970> OnTokenValidated = context =>
971> {
972> var name = context.Principal?.Identity?.Name ?? "unknown";
973> var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
974> Console.WriteLine($"Token validated for: {name} ({email})");
975> return Task.CompletedTask;
976> },
977> OnAuthenticationFailed = context =>
978> {
979> Console.WriteLine($"Authentication failed: {context.Exception.Message}");
980> return Task.CompletedTask;
981> },
982> };
983> })
984> .AddMcp(options =>
985> {
986> options.ResourceMetadata = new()
987> {
988> Resource = serverUrl,
989> ResourceDocumentation = "https://docs.example.com/api/math",
990> AuthorizationServers = { authorizationServerUrl },
991> ScopesSupported = ["mcp:tools"]
992> };
993> });
994>
995> builder.Services.AddAuthorization();
996>
997> builder.Services.AddHttpContextAccessor();
998> builder.Services.AddMcpServer()
999> .WithTools<MathTools>()
1000> .WithHttpTransport();
1001>
1002> var app = builder.Build();
1003>
1004> app.UseAuthentication();
1005> app.UseAuthorization();
1006>
1007> app.MapMcp().RequireAuthorization();
1008>
1009> Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
1010> Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
1011> Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
1012> Console.WriteLine("Exposed Math tools: Add, Multiply");
1013> Console.WriteLine("Press Ctrl+C to stop the server");
1014>
1015> app.Run(serverUrl);
1016> ```
1017>
1018> Fill `ProtectedMcpServer.csproj` with:
1019>
1020> ```xml theme={null}
1021> <Project Sdk="Microsoft.NET.Sdk.Web">
1022>
1023> <PropertyGroup>
1024> <TargetFramework>net9.0</TargetFramework>
1025> <Nullable>enable</Nullable>
1026> <ImplicitUsings>enable</ImplicitUsings>
1027> <!-- Identifier for the local secret store, not a secret itself. -->
1028> <UserSecretsId>local-authorization-mcp-server</UserSecretsId>
1029> </PropertyGroup>
1030>
1031> <ItemGroup>
1032> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.18" />
1033> <PackageReference Include="ModelContextProtocol" Version="2.0.0" />
1034> <PackageReference Include="ModelContextProtocol.AspNetCore" Version="2.0.0" />
1035> </ItemGroup>
1036>
1037> </Project>
1038> ```
1039>
1040> In the `Tools` folder, create `MathTools.cs` and fill it with:
1041>
1042> ```csharp theme={null}
1043> using System.ComponentModel;
1044> using ModelContextProtocol.Server;
1045>
1046> namespace ProtectedMcpServer.Tools;
1047>
1048> [McpServerToolType]
1049> public sealed class MathTools
1050> {
1051> [McpServerTool, Description("Add two numbers together.")]
1052> public Task<double> Add(
1053> [Description("First operand")] double a,
1054> [Description("Second operand")] double b)
1055> {
1056> return Task.FromResult(a + b);
1057> }
1058>
1059> [McpServerTool, Description("Multiply two numbers together.")]
1060> public Task<double> Multiply(
1061> [Description("First operand")] double a,
1062> [Description("Second operand")] double b)
1063> {
1064> return Task.FromResult(a * b);
1065> }
1066> }
1067> ```
1068>
1069> Then from the server's root, run:
1070>
1071> ```bash theme={null}
1072> dotnet run
1073> ```
1074>
1075> For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
10761076 
10771077## Testing the MCP Server
10781078 
10791079 

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

from line 419
419419 
420420## Next Steps
421421 
422> **[Security Best Practices](https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices.md)**
422> [card] [Security Best Practices](https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices.md)
423423>
424424> The protocol threat model: confused deputy, token passthrough, and session
425425> hijacking
426426 
427> **[Authorization](https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/authorization.md)**
427> [card] [Authorization](https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/authorization.md)
428428>
429429> How OAuth-based authorization works for remote MCP servers
430430 
431> **[SEP-1024](https://modelcontextprotocol.io/seps/1024-mcp-client-security-requirements-for-local-server-.md)**
431> [card] [SEP-1024](https://modelcontextprotocol.io/seps/1024-mcp-client-security-requirements-for-local-server-.md)
432432>
433433> Client security requirements for local server installation consent
434434 
435> **[Connect to local servers](https://modelcontextprotocol.io/docs/2026-07-28/develop/connect-local-servers.md)**
435> [card] [Connect to local servers](https://modelcontextprotocol.io/docs/2026-07-28/develop/connect-local-servers.md)
436436>
437437> Set up local servers in your client — the workflow this guide secures
438438 

docs/draft/develop/build-client Changed · +2475 / -2483 lines

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

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

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