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

51-75 of 91, page 3 of 4

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

docs/draft/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/draft/tools/inspector/cli.md) in the `modern` [protocol era](https://modelcontextprotocol.io/docs/draft/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/draft/tools/inspector/cli.md) in the `modern` [protocol era](https://modelcontextprotocol.io/docs/draft/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/draft/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/draft/develop/build-client.md)**
2926> [card] [Building a client](https://modelcontextprotocol.io/docs/draft/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/draft/tools/debugging.md)**
2934> [card] [Debugging Guide](https://modelcontextprotocol.io/docs/draft/tools/debugging.md)
29352935>
29362936> Learn how to effectively debug MCP servers and integrations
29372937 
2938> **[Build with Agent Skills](https://modelcontextprotocol.io/docs/draft/develop/build-with-agent-skills.md)**
2938> [card] [Build with Agent Skills](https://modelcontextprotocol.io/docs/draft/develop/build-with-agent-skills.md)
29392939>
29402940> Use agent skills to guide AI coding assistants through server design
29412941 

docs/draft/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/draft/tools/inspector.md)**
88> [card] [MCP Inspector](https://modelcontextprotocol.io/docs/draft/tools/inspector.md)
8989>
9090> Test your server's tools, resources, and prompts interactively
9191 
92> **[Connect to a client](https://modelcontextprotocol.io/docs/draft/develop/connect-local-servers.md)**
92> [card] [Connect to a client](https://modelcontextprotocol.io/docs/draft/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/draft/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/draft/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/draft/develop/build-server.md)**
254> [card] [Build your own server](https://modelcontextprotocol.io/docs/draft/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/draft/develop/connect-remote-servers.md)**
259> [card] [Connect to remote servers](https://modelcontextprotocol.io/docs/draft/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/draft/learn/architecture.md)**
264> [card] [Understand the protocol](https://modelcontextprotocol.io/docs/draft/learn/architecture.md)
258265>
259266> Dive deeper into how MCP works and its architecture
260267 

docs/draft/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/draft/develop/build-server.md)**
91> [card] [Build your own remote server](https://modelcontextprotocol.io/docs/draft/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/draft/develop/connect-local-servers.md)**
100> [card] [Connect local servers](https://modelcontextprotocol.io/docs/draft/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/draft/learn/architecture.md)**
105> [card] [Understand the architecture](https://modelcontextprotocol.io/docs/draft/learn/architecture.md)
106106>
107107> Dive deeper into how MCP works and its architecture
108108 

docs/draft/getting-started/intro Changed · +6 / -6 lines

from line 29
2929 
3030## Start Building
3131 
32> **[Build servers](https://modelcontextprotocol.io/docs/draft/develop/build-server.md)**
32> [card] [Build servers](https://modelcontextprotocol.io/docs/draft/develop/build-server.md)
3333>
3434> Create MCP servers to expose your data and tools
3535 
36> **[Build clients](https://modelcontextprotocol.io/docs/draft/develop/build-client.md)**
36> [card] [Build clients](https://modelcontextprotocol.io/docs/draft/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/draft/learn/architecture.md)**
46> [card] [Architecture](https://modelcontextprotocol.io/docs/draft/learn/architecture.md)
4747>
4848> Learn the core concepts and architecture of MCP
4949 
50> **[Security](https://modelcontextprotocol.io/docs/draft/tutorials/security/security_best_practices.md)**
50> [card] [Security](https://modelcontextprotocol.io/docs/draft/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/draft/tools/debugging Changed · +3 / -3 lines

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

docs/draft/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/draft/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/draft/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/draft/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/draft/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/draft/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/draft/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/draft/tools/inspector/web.md)**
120> [card] [Web client](https://modelcontextprotocol.io/docs/draft/tools/inspector/web.md)
121121>
122122> A tab-by-tab walkthrough of the graphical inspector.
123123 
124> **[CLI client](https://modelcontextprotocol.io/docs/draft/tools/inspector/cli.md)**
124> [card] [CLI client](https://modelcontextprotocol.io/docs/draft/tools/inspector/cli.md)
125125>
126126> Method reference, output formats, exit codes, and CI recipes.
127127 
128> **[TUI client](https://modelcontextprotocol.io/docs/draft/tools/inspector/tui.md)**
128> [card] [TUI client](https://modelcontextprotocol.io/docs/draft/tools/inspector/tui.md)
129129>
130130> Terminal navigation and keyboard reference.
131131 
132> **[Configuration and flags](https://modelcontextprotocol.io/docs/draft/tools/inspector/configuration.md)**
132> [card] [Configuration and flags](https://modelcontextprotocol.io/docs/draft/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/draft/tools/inspector/authorization.md)**
137> [card] [Authorization](https://modelcontextprotocol.io/docs/draft/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/draft/tools/inspector/protocol-eras.md)**
142> [card] [Protocol eras](https://modelcontextprotocol.io/docs/draft/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/draft/tools/inspector/mcpdo.md)**
147> [card] [mcpdo connection client](https://modelcontextprotocol.io/docs/draft/tools/inspector/mcpdo.md)
148148>
149149> Connect once, then run many commands against a named connection.
150150 
151> **[Security](https://modelcontextprotocol.io/docs/draft/tools/inspector/security.md)**
151> [card] [Security](https://modelcontextprotocol.io/docs/draft/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/draft/tools/inspector/recipes.md)**
156> [card] [Recipes](https://modelcontextprotocol.io/docs/draft/tools/inspector/recipes.md)
157157>
158158> Importing client configs, reviewing MCP Apps, Docker, and network hosting.
159159 
160> **[Debugging guide](https://modelcontextprotocol.io/docs/draft/tools/debugging.md)**
160> [card] [Debugging guide](https://modelcontextprotocol.io/docs/draft/tools/debugging.md)
161161>
162162> Broader debugging strategies beyond the Inspector.
163163 

docs/draft/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/draft/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/draft/tutorials/security/authorization Changed · +768 / -768 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 
925```csharp theme={null}
926using Microsoft.AspNetCore.Authentication.JwtBearer;
927using Microsoft.IdentityModel.Tokens;
928using ModelContextProtocol.AspNetCore.Authentication;
929using ProtectedMcpServer.Tools;
930using System.Security.Claims;
931 
932var builder = WebApplication.CreateBuilder(args);
933 
934var serverUrl = "http://localhost:3000/";
935var authorizationServerUrl = "http://localhost:8080/realms/master/";
936 
937builder.Services.AddAuthentication(options =>
938{
939 options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
940 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
941})
942.AddJwtBearer(options =>
943{
944 options.Authority = authorizationServerUrl;
945 var normalizedServerAudience = serverUrl.TrimEnd('/');
946 options.TokenValidationParameters = new TokenValidationParameters
947 {
948 ValidIssuer = authorizationServerUrl,
949 ValidAudiences = new[] { normalizedServerAudience, serverUrl },
950 AudienceValidator = (audiences, securityToken, validationParameters) =>
951 {
952 if (audiences == null) return false;
953 foreach (var aud in audiences)
954 {
955 if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
956 {
957 return true;
958 }
959 }
960 return false;
961 }
962 };
963 
964 options.RequireHttpsMetadata = false; // Set to true in production
965 
966 options.Events = new JwtBearerEvents
967 {
968 OnTokenValidated = context =>
969 {
970 var name = context.Principal?.Identity?.Name ?? "unknown";
971 var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
972 Console.WriteLine($"Token validated for: {name} ({email})");
973 return Task.CompletedTask;
974 },
975 OnAuthenticationFailed = context =>
976 {
977 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
978 return Task.CompletedTask;
979 },
980 };
981})
982.AddMcp(options =>
983{
984 options.ResourceMetadata = new()
985 {
986 Resource = new Uri(serverUrl),
987 ResourceDocumentation = new Uri("https://docs.example.com/api/math"),
988 AuthorizationServers = { new Uri(authorizationServerUrl) },
989 ScopesSupported = ["mcp:tools"]
990 };
991});
992 
993builder.Services.AddAuthorization();
994 
995builder.Services.AddHttpContextAccessor();
996builder.Services.AddMcpServer()
997 .WithTools<MathTools>()
998 .WithHttpTransport();
999 
1000var app = builder.Build();
1001 
1002app.UseAuthentication();
1003app.UseAuthorization();
1004 
1005app.MapMcp().RequireAuthorization();
1006 
1007Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
1008Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
1009Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
1010Console.WriteLine("Exposed Math tools: Add, Multiply");
1011Console.WriteLine("Press Ctrl+C to stop the server");
1012 
1013app.Run(serverUrl);
1014```
1015 
1016For 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> ```csharp theme={null}
926> using Microsoft.AspNetCore.Authentication.JwtBearer;
927> using Microsoft.IdentityModel.Tokens;
928> using ModelContextProtocol.AspNetCore.Authentication;
929> using ProtectedMcpServer.Tools;
930> using System.Security.Claims;
931>
932> var builder = WebApplication.CreateBuilder(args);
933>
934> var serverUrl = "http://localhost:3000/";
935> var authorizationServerUrl = "http://localhost:8080/realms/master/";
936>
937> builder.Services.AddAuthentication(options =>
938> {
939> options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
940> options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
941> })
942> .AddJwtBearer(options =>
943> {
944> options.Authority = authorizationServerUrl;
945> var normalizedServerAudience = serverUrl.TrimEnd('/');
946> options.TokenValidationParameters = new TokenValidationParameters
947> {
948> ValidIssuer = authorizationServerUrl,
949> ValidAudiences = new[] { normalizedServerAudience, serverUrl },
950> AudienceValidator = (audiences, securityToken, validationParameters) =>
951> {
952> if (audiences == null) return false;
953> foreach (var aud in audiences)
954> {
955> if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
956> {
957> return true;
958> }
959> }
960> return false;
961> }
962> };
963>
964> options.RequireHttpsMetadata = false; // Set to true in production
965>
966> options.Events = new JwtBearerEvents
967> {
968> OnTokenValidated = context =>
969> {
970> var name = context.Principal?.Identity?.Name ?? "unknown";
971> var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
972> Console.WriteLine($"Token validated for: {name} ({email})");
973> return Task.CompletedTask;
974> },
975> OnAuthenticationFailed = context =>
976> {
977> Console.WriteLine($"Authentication failed: {context.Exception.Message}");
978> return Task.CompletedTask;
979> },
980> };
981> })
982> .AddMcp(options =>
983> {
984> options.ResourceMetadata = new()
985> {
986> Resource = new Uri(serverUrl),
987> ResourceDocumentation = new Uri("https://docs.example.com/api/math"),
988> AuthorizationServers = { new Uri(authorizationServerUrl) },
989> ScopesSupported = ["mcp:tools"]
990> };
991> });
992>
993> builder.Services.AddAuthorization();
994>
995> builder.Services.AddHttpContextAccessor();
996> builder.Services.AddMcpServer()
997> .WithTools<MathTools>()
998> .WithHttpTransport();
999>
1000> var app = builder.Build();
1001>
1002> app.UseAuthentication();
1003> app.UseAuthorization();
1004>
1005> app.MapMcp().RequireAuthorization();
1006>
1007> Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
1008> Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
1009> Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
1010> Console.WriteLine("Exposed Math tools: Add, Multiply");
1011> Console.WriteLine("Press Ctrl+C to stop the server");
1012>
1013> app.Run(serverUrl);
1014> ```
1015>
1016> For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
10171017 
10181018## Testing the MCP Server
10191019 
10201020 

docs/draft/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/draft/tutorials/security/security_best_practices.md)**
422> [card] [Security Best Practices](https://modelcontextprotocol.io/docs/draft/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/draft/tutorials/security/authorization.md)**
427> [card] [Authorization](https://modelcontextprotocol.io/docs/draft/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/draft/develop/connect-local-servers.md)**
435> [card] [Connect to local servers](https://modelcontextprotocol.io/docs/draft/develop/connect-local-servers.md)
436436>
437437> Set up local servers in your client — the workflow this guide secures
438438 

examples Changed · +2 / -15 lines

## More servers ## Official integrations ## Community implementations

from line 24
2424 
2525They are provided for historical reference only.
2626 
27## Official integrations
27## More servers
2828 
29Visit the [MCP Servers Repository (Official Integrations section)](https://github.com/modelcontextprotocol/servers?tab=readme-ov-file#%EF%B8%8F-official-integrations) for a list of MCP servers maintained by companies for their platforms.
29Visit the MCP Registry at [registry.modelcontextprotocol.io](https://registry.modelcontextprotocol.io/) for servers published by companies and community members.
3030 
31## Community implementations
32 
33Visit the [MCP Servers Repository (Community section)](https://github.com/modelcontextprotocol/servers?tab=readme-ov-file#-community-servers) for a list of MCP servers maintained by community members.
34 
3531## Getting started
3632 
3733### Using reference servers
from line 67
7167 "@modelcontextprotocol/server-filesystem",
7268 "/path/to/allowed/files"
7369 ]
74 },
75 "github": {
76 "command": "npx",
77 "args": ["-y", "@modelcontextprotocol/server-github"],
78 "env": {
79 "GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
80 }
8170 }
8271 }
8372}
from line 73
8473```
8574 
8675## Additional resources
87 
88Visit the [MCP Servers Repository (Resources section)](https://github.com/modelcontextprotocol/servers?tab=readme-ov-file#-resources) for a collection of other resources and projects related to MCP.
8976 
9077Visit our [GitHub Discussions](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions) to engage with the MCP community.
9178 

extensions/apps/build Changed · +53 / -53 lines

from line 4
44 
55## Prerequisites
66 
7You'll need [Node.js](https://nodejs.org/en/download) 18 or higher. Familiarity
7You'll need [Node.js](https://nodejs.org/en/download) 20 or higher. Familiarity
88with [MCP tools](https://modelcontextprotocol.io/specification/latest/server/tools.md) and
99[resources](https://modelcontextprotocol.io/specification/latest/server/resources.md) is recommended since MCP
1010Apps combine both primitives. Experience with the
from line 160
160160 
161161**3. Configure the project**
162162 
163**package.json**
163> [tab] package.json
164>
165> The `"type": "module"` setting enables ES module syntax. The `build` script uses the `INPUT` environment variable to tell Vite which HTML file to bundle. The `serve` script runs your server using `tsx` for TypeScript execution.
166>
167> ```json theme={null}
168> {
169> "type": "module",
170> "scripts": {
171> "build": "INPUT=mcp-app.html vite build",
172> "serve": "npx tsx server.ts"
173> }
174> }
175> ```
164176 
165The `"type": "module"` setting enables ES module syntax. The `build` script uses the `INPUT` environment variable to tell Vite which HTML file to bundle. The `serve` script runs your server using `tsx` for TypeScript execution.
177> [tab] tsconfig.json
178>
179> The TypeScript configuration targets modern JavaScript (`ES2022`) and uses ESNext modules with bundler resolution, which works well with Vite. The `include` array covers both the server code in the root and UI code in `src/`.
180>
181> ```json theme={null}
182> {
183> "compilerOptions": {
184> "target": "ES2022",
185> "module": "ESNext",
186> "moduleResolution": "bundler",
187> "strict": true,
188> "esModuleInterop": true,
189> "skipLibCheck": true,
190> "outDir": "dist"
191> },
192> "include": ["*.ts", "src/**/*.ts"]
193> }
194> ```
166195 
167```json theme={null}
168{
169 "type": "module",
170 "scripts": {
171 "build": "INPUT=mcp-app.html vite build",
172 "serve": "npx tsx server.ts"
173 }
174}
175```
196> [tab] vite.config.ts
197>
198> ```typescript theme={null}
199> import { defineConfig } from "vite";
200> import { viteSingleFile } from "vite-plugin-singlefile";
201>
202> export default defineConfig({
203> plugins: [viteSingleFile()],
204> build: {
205> outDir: "dist",
206> rollupOptions: {
207> input: process.env.INPUT,
208> },
209> },
210> });
211> ```
176212 
177**tsconfig.json**
178 
179The TypeScript configuration targets modern JavaScript (`ES2022`) and uses ESNext modules with bundler resolution, which works well with Vite. The `include` array covers both the server code in the root and UI code in `src/`.
180 
181```json theme={null}
182{
183 "compilerOptions": {
184 "target": "ES2022",
185 "module": "ESNext",
186 "moduleResolution": "bundler",
187 "strict": true,
188 "esModuleInterop": true,
189 "skipLibCheck": true,
190 "outDir": "dist"
191 },
192 "include": ["*.ts", "src/**/*.ts"]
193}
194```
195 
196**vite.config.ts**
197 
198```typescript theme={null}
199import { defineConfig } from "vite";
200import { viteSingleFile } from "vite-plugin-singlefile";
201 
202export default defineConfig({
203 plugins: [viteSingleFile()],
204 build: {
205 outDir: "dist",
206 rollupOptions: {
207 input: process.env.INPUT,
208 },
209 },
210});
211```
212 
213213**4. Build the project**
214214 
215215With the project structure and configuration in place, continue to [Building an MCP App](#building-an-mcp-app) below to implement the server and UI.
from line 472
472472 
473473## Learn more
474474 
475> **[API Documentation](https://apps.extensions.modelcontextprotocol.io/api/)**
475> [card] [API Documentation](https://apps.extensions.modelcontextprotocol.io/api/)
476476>
477477> Full SDK reference and API details
478478 
479> **[GitHub Repository](https://github.com/modelcontextprotocol/ext-apps)**
479> [card] [GitHub Repository](https://github.com/modelcontextprotocol/ext-apps)
480480>
481481> Source code, examples, and issue tracker
482482 
483> **[Specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)**
483> [card] [Specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)
484484>
485485> Technical specification for implementers
486486 

extensions/auth/enterprise-managed-authorization Changed · +5 / -5 lines

from line 4
44 
55The Enterprise-Managed Authorization extension (`io.modelcontextprotocol/enterprise-managed-authorization`) enables organizations to control MCP server access centrally through their existing identity provider (IdP). Instead of each employee authorizing each MCP server individually, the organization's IT or security team manages access policies in one place.
66 
7> **[Specification](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/enterprise-managed-authorization/2026-07-28/enterprise-managed-authorization.mdx)**
7> [card] [Specification](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/enterprise-managed-authorization/2026-07-28/enterprise-managed-authorization.mdx)
88>
99> Full technical specification for the Enterprise-Managed Authorization
1010> extension.
from line 180
180180 
181181## Related resources
182182 
183> **[ext-auth repository](https://github.com/modelcontextprotocol/ext-auth)**
183> [card] [ext-auth repository](https://github.com/modelcontextprotocol/ext-auth)
184184>
185185> Source code and reference implementations
186186 
187> **[Full specification](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/enterprise-managed-authorization/2026-07-28/enterprise-managed-authorization.mdx)**
187> [card] [Full specification](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/enterprise-managed-authorization/2026-07-28/enterprise-managed-authorization.mdx)
188188>
189189> Technical specification with normative requirements
190190 
191> **[SEP-990](https://modelcontextprotocol.io/seps/990-enable-enterprise-idp-policy-controls-during-mcp-o.md)**
191> [card] [SEP-990](https://modelcontextprotocol.io/seps/990-enable-enterprise-idp-policy-controls-during-mcp-o.md)
192192>
193193> Original proposal: Enable Enterprise IdP Policy Controls
194194 
195> **[MCP Authorization](https://modelcontextprotocol.io/specification/latest/basic/authorization.md)**
195> [card] [MCP Authorization](https://modelcontextprotocol.io/specification/latest/basic/authorization.md)
196196>
197197> Core MCP authorization specification
198198 

extensions/auth/oauth-client-credentials Changed · +220 / -214 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 4
44 
55The OAuth Client Credentials extension (`io.modelcontextprotocol/oauth-client-credentials`) adds support for the [OAuth 2.0 client credentials flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4) to MCP. This enables automated systems to connect to MCP servers without interactive user authorization.
66 
7> **[Specification](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/oauth-client-credentials/2026-07-28/oauth-client-credentials.mdx)**
7> [card] [Specification](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/oauth-client-credentials/2026-07-28/oauth-client-credentials.mdx)
88>
99> Full technical specification for the OAuth Client Credentials extension.
1010 
from line 158
158158 
159159**1. Install the SDK**
160160 
161**TypeScript**
162 
163```bash theme={null}
164npm install @modelcontextprotocol/client
165```
166 
167**Python**
168 
169```bash theme={null}
170pip install mcp
171```
161> [tab] TypeScript
162>
163> ```bash theme={null}
164> npm install @modelcontextprotocol/client
165> ```
166 
167> [tab] Python
168>
169> ```bash theme={null}
170> pip install mcp
171> ```
172172 
173173**2. Create a provider and connect**
174174 
175175Choose the credential format that matches your setup:
176176 
177Each sample pins the issuer of the authorization server that provisioned the credentials, so the client refuses to send them to any other authorization server the MCP server might advertise.
178 
177179#### Using a client secret
178180 
179**TypeScript**
180 
181```typescript theme={null}
182import {
183 Client,
184 ClientCredentialsProvider,
185 StreamableHTTPClientTransport,
186} from "@modelcontextprotocol/client";
187 
188const provider = new ClientCredentialsProvider({
189 clientId: "my-service",
190 clientSecret: "s3cr3t",
191});
192 
193const client = new Client(
194 { name: "my-service", version: "1.0.0" },
195 { capabilities: {} },
196);
197 
198const transport = new StreamableHTTPClientTransport(
199 new URL("https://mcp.example.com/mcp"),
200 { authProvider: provider },
201);
202 
203await client.connect(transport);
204 
205// Use the client
206const tools = await client.listTools();
207console.log(
208 "Available tools:",
209 tools.tools.map((t) => t.name),
210);
211 
212await transport.close();
213```
214 
215**Python**
216 
217```python theme={null}
218import asyncio
219 
220import httpx2
221 
222from mcp import Client
223from mcp.client.auth.extensions.client_credentials import (
224 ClientCredentialsOAuthProvider,
225)
226from mcp.client.streamable_http import streamable_http_client
227from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
228 
229 
230class InMemoryTokenStorage:
231 def __init__(self) -> None:
232 self.tokens: OAuthToken | None = None
233 self.client_info: OAuthClientInformationFull | None = None
234 
235 async def get_tokens(self) -> OAuthToken | None:
236 return self.tokens
237 
238 async def set_tokens(self, tokens: OAuthToken) -> None:
239 self.tokens = tokens
240 
241 async def get_client_info(self) -> OAuthClientInformationFull | None:
242 return self.client_info
243 
244 async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
245 self.client_info = client_info
246 
247 
248provider = ClientCredentialsOAuthProvider(
249 server_url="https://mcp.example.com/mcp",
250 storage=InMemoryTokenStorage(),
251 client_id="my-service",
252 client_secret="s3cr3t",
253 scopes="read write",
254)
255 
256 
257async def main() -> None:
258 async with httpx2.AsyncClient(auth=provider) as http_client:
259 transport = streamable_http_client(
260 "https://mcp.example.com/mcp",
261 http_client=http_client,
262 )
263 async with Client(transport) as client:
264 # Use the client
265 tools = await client.list_tools()
266 print("Available tools:", [t.name for t in tools.tools])
267 
268 
269if __name__ == "__main__":
270 asyncio.run(main())
271```
181> [tab] TypeScript
182>
183> ```typescript theme={null}
184> import {
185> Client,
186> ClientCredentialsProvider,
187> StreamableHTTPClientTransport,
188> } from "@modelcontextprotocol/client";
189>
190> const provider = new ClientCredentialsProvider({
191> clientId: "my-service",
192> clientSecret: "s3cr3t",
193> expectedIssuer: "https://auth.example.com",
194> });
195>
196> const client = new Client(
197> { name: "my-service", version: "1.0.0" },
198> { capabilities: {} },
199> );
200>
201> const transport = new StreamableHTTPClientTransport(
202> new URL("https://mcp.example.com/mcp"),
203> { authProvider: provider },
204> );
205>
206> await client.connect(transport);
207>
208> // Use the client
209> const tools = await client.listTools();
210> console.log(
211> "Available tools:",
212> tools.tools.map((t) => t.name),
213> );
214>
215> await transport.close();
216> ```
217 
218> [tab] Python
219>
220> ```python theme={null}
221> import asyncio
222>
223> import httpx2
224>
225> from mcp import Client
226> from mcp.client.auth.extensions.client_credentials import (
227> ClientCredentialsOAuthProvider,
228> )
229> from mcp.client.streamable_http import streamable_http_client
230> from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
231>
232>
233> class InMemoryTokenStorage:
234> def __init__(self) -> None:
235> self.tokens: OAuthToken | None = None
236> self.client_info: OAuthClientInformationFull | None = None
237>
238> async def get_tokens(self) -> OAuthToken | None:
239> return self.tokens
240>
241> async def set_tokens(self, tokens: OAuthToken) -> None:
242> self.tokens = tokens
243>
244> async def get_client_info(self) -> OAuthClientInformationFull | None:
245> return self.client_info
246>
247> async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
248> self.client_info = client_info
249>
250>
251> provider = ClientCredentialsOAuthProvider(
252> server_url="https://mcp.example.com/mcp",
253> storage=InMemoryTokenStorage(),
254> client_id="my-service",
255> client_secret="s3cr3t",
256> scope="read write",
257> issuer="https://auth.example.com",
258> )
259>
260>
261> async def main() -> None:
262> async with httpx2.AsyncClient(auth=provider) as http_client:
263> transport = streamable_http_client(
264> "https://mcp.example.com/mcp",
265> http_client=http_client,
266> )
267> async with Client(transport) as client:
268> # Use the client
269> tools = await client.list_tools()
270> print("Available tools:", [t.name for t in tools.tools])
271>
272>
273> if __name__ == "__main__":
274> asyncio.run(main())
275> ```
272276 
273277#### Using a JWT private key
274278 
275**TypeScript**
276 
277```typescript theme={null}
278import {
279 Client,
280 PrivateKeyJwtProvider,
281 StreamableHTTPClientTransport,
282} from "@modelcontextprotocol/client";
283 
284const provider = new PrivateKeyJwtProvider({
285 clientId: "my-service",
286 privateKey: process.env.CLIENT_PRIVATE_KEY_PEM,
287 algorithm: "RS256",
288});
289 
290const client = new Client(
291 { name: "my-service", version: "1.0.0" },
292 { capabilities: {} },
293);
294 
295const transport = new StreamableHTTPClientTransport(
296 new URL("https://mcp.example.com/mcp"),
297 { authProvider: provider },
298);
299 
300await client.connect(transport);
301 
302// Use the client
303const tools = await client.listTools();
304console.log(
305 "Available tools:",
306 tools.tools.map((t) => t.name),
307);
308 
309await transport.close();
310```
311 
312**Python**
313 
314```python theme={null}
315import asyncio
316from pathlib import Path
317 
318import httpx2
319 
320from mcp import Client
321from mcp.client.auth.extensions.client_credentials import (
322 PrivateKeyJWTOAuthProvider,
323 SignedJWTParameters,
324)
325from mcp.client.streamable_http import streamable_http_client
326from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
327 
328 
329class InMemoryTokenStorage:
330 def __init__(self) -> None:
331 self.tokens: OAuthToken | None = None
332 self.client_info: OAuthClientInformationFull | None = None
333 
334 async def get_tokens(self) -> OAuthToken | None:
335 return self.tokens
336 
337 async def set_tokens(self, tokens: OAuthToken) -> None:
338 self.tokens = tokens
339 
340 async def get_client_info(self) -> OAuthClientInformationFull | None:
341 return self.client_info
342 
343 async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
344 self.client_info = client_info
345 
346 
347# Create a signed JWT assertion provider from key parameters
348jwt_params = SignedJWTParameters(
349 issuer="my-service",
350 subject="my-service",
351 signing_key=Path("private_key.pem").read_text(),
352 signing_algorithm="RS256",
353 lifetime_seconds=300,
354)
355 
356provider = PrivateKeyJWTOAuthProvider(
357 server_url="https://mcp.example.com/mcp",
358 storage=InMemoryTokenStorage(),
359 client_id="my-service",
360 assertion_provider=jwt_params.create_assertion_provider(),
361 scopes="read write",
362)
363 
364 
365async def main() -> None:
366 async with httpx2.AsyncClient(auth=provider) as http_client:
367 transport = streamable_http_client(
368 "https://mcp.example.com/mcp",
369 http_client=http_client,
370 )
371 async with Client(transport) as client:
372 # Use the client
373 tools = await client.list_tools()
374 print("Available tools:", [t.name for t in tools.tools])
375 
376 
377if __name__ == "__main__":
378 asyncio.run(main())
379```
279> [tab] TypeScript
280>
281> ```typescript theme={null}
282> import {
283> Client,
284> PrivateKeyJwtProvider,
285> StreamableHTTPClientTransport,
286> } from "@modelcontextprotocol/client";
287>
288> const provider = new PrivateKeyJwtProvider({
289> clientId: "my-service",
290> privateKey: process.env.CLIENT_PRIVATE_KEY_PEM,
291> algorithm: "RS256",
292> expectedIssuer: "https://auth.example.com",
293> });
294>
295> const client = new Client(
296> { name: "my-service", version: "1.0.0" },
297> { capabilities: {} },
298> );
299>
300> const transport = new StreamableHTTPClientTransport(
301> new URL("https://mcp.example.com/mcp"),
302> { authProvider: provider },
303> );
304>
305> await client.connect(transport);
306>
307> // Use the client
308> const tools = await client.listTools();
309> console.log(
310> "Available tools:",
311> tools.tools.map((t) => t.name),
312> );
313>
314> await transport.close();
315> ```
316 
317> [tab] Python
318>
319> ```python theme={null}
320> import asyncio
321> from pathlib import Path
322>
323> import httpx2
324>
325> from mcp import Client
326> from mcp.client.auth.extensions.client_credentials import (
327> PrivateKeyJWTOAuthProvider,
328> SignedJWTParameters,
329> )
330> from mcp.client.streamable_http import streamable_http_client
331> from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
332>
333>
334> class InMemoryTokenStorage:
335> def __init__(self) -> None:
336> self.tokens: OAuthToken | None = None
337> self.client_info: OAuthClientInformationFull | None = None
338>
339> async def get_tokens(self) -> OAuthToken | None:
340> return self.tokens
341>
342> async def set_tokens(self, tokens: OAuthToken) -> None:
343> self.tokens = tokens
344>
345> async def get_client_info(self) -> OAuthClientInformationFull | None:
346> return self.client_info
347>
348> async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
349> self.client_info = client_info
350>
351>
352> # Create a signed JWT assertion provider from key parameters
353> jwt_params = SignedJWTParameters(
354> issuer="my-service",
355> subject="my-service",
356> signing_key=Path("private_key.pem").read_text(),
357> signing_algorithm="RS256",
358> lifetime_seconds=300,
359> )
360>
361> provider = PrivateKeyJWTOAuthProvider(
362> server_url="https://mcp.example.com/mcp",
363> storage=InMemoryTokenStorage(),
364> client_id="my-service",
365> assertion_provider=jwt_params.create_assertion_provider(),
366> scope="read write",
367> issuer="https://auth.example.com",
368> )
369>
370>
371> async def main() -> None:
372> async with httpx2.AsyncClient(auth=provider) as http_client:
373> transport = streamable_http_client(
374> "https://mcp.example.com/mcp",
375> http_client=http_client,
376> )
377> async with Client(transport) as client:
378> # Use the client
379> tools = await client.list_tools()
380> print("Available tools:", [t.name for t in tools.tools])
381>
382>
383> if __name__ == "__main__":
384> asyncio.run(main())
385> ```
380386 
381387## Client support
382388 
from line 394
388394 
389395## Related resources
390396 
391> **[ext-auth repository](https://github.com/modelcontextprotocol/ext-auth)**
397> [card] [ext-auth repository](https://github.com/modelcontextprotocol/ext-auth)
392398>
393399> Source code and reference implementations
394400 
395> **[Full specification](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/oauth-client-credentials/2026-07-28/oauth-client-credentials.mdx)**
401> [card] [Full specification](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/oauth-client-credentials/2026-07-28/oauth-client-credentials.mdx)
396402>
397403> Technical specification with normative requirements
398404 
399> **[RFC 6749 — Client Credentials Grant](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4)**
405> [card] [RFC 6749 — Client Credentials Grant](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4)
400406>
401407> The underlying OAuth 2.0 specification
402408 
403> **[RFC 7523 — JWT Bearer Assertions](https://datatracker.ietf.org/doc/html/rfc7523)**
409> [card] [RFC 7523 — JWT Bearer Assertions](https://datatracker.ietf.org/doc/html/rfc7523)
404410>
405411> JWT assertion format specification
406412 

extensions/auth/overview Changed · +3 / -3 lines

from line 4
44 
55The [ext-auth repository](https://github.com/modelcontextprotocol/ext-auth) contains official MCP extensions that add authorization capabilities beyond the core MCP specification. These extensions address specific real-world scenarios where the standard OAuth 2.0 authorization code flow isn't the right fit.
66 
7> **[modelcontextprotocol/ext-auth](https://github.com/modelcontextprotocol/ext-auth)**
7> [card] [modelcontextprotocol/ext-auth](https://github.com/modelcontextprotocol/ext-auth)
88>
99> Source code, specifications, and reference implementations for MCP
1010> authorization extensions.
from line 22
2222 
2323## Available extensions
2424 
25> **[OAuth Client Credentials](https://modelcontextprotocol.io/extensions/auth/oauth-client-credentials.md)**
25> [card] [OAuth Client Credentials](https://modelcontextprotocol.io/extensions/auth/oauth-client-credentials.md)
2626>
2727> Machine-to-machine authentication using the OAuth 2.0 client credentials
2828> flow. No user interaction required.
2929 
30> **[Enterprise-Managed Authorization](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization.md)**
30> [card] [Enterprise-Managed Authorization](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization.md)
3131>
3232> Centralized access control via enterprise identity providers. Employees
3333> access MCP servers through their organization's IdP.

extensions/overview Changed · +5 / -5 lines

from line 32
3232 
3333### MCP Authorization Extensions
3434 
35> **[modelcontextprotocol/ext-auth](https://github.com/modelcontextprotocol/ext-auth)**
35> [card] [modelcontextprotocol/ext-auth](https://github.com/modelcontextprotocol/ext-auth)
3636>
3737> Extensions for supplementary authorization mechanisms beyond the core
3838> specification.
from line 44
4444 
4545### MCP Apps
4646 
47> **[modelcontextprotocol/ext-apps](https://github.com/modelcontextprotocol/ext-apps)**
47> [card] [modelcontextprotocol/ext-apps](https://github.com/modelcontextprotocol/ext-apps)
4848>
4949> Extensions for interactive UI elements in conversational MCP clients.
5050 
from line 56
5656 
5757### MCP Tasks
5858 
59> **[modelcontextprotocol/ext-tasks](https://github.com/modelcontextprotocol/ext-tasks)**
59> [card] [modelcontextprotocol/ext-tasks](https://github.com/modelcontextprotocol/ext-tasks)
6060>
6161> Extensions for long-running operations.
6262 
from line 66
6666 
6767### Skills over MCP
6868 
69> **[modelcontextprotocol/ext-skills](https://github.com/modelcontextprotocol/ext-skills)**
69> [card] [modelcontextprotocol/ext-skills](https://github.com/modelcontextprotocol/ext-skills)
7070>
7171> Discover and read Agent Skills from MCP servers.
7272 
from line 79
7979 
8080### MCP Server Cards
8181 
82> **[modelcontextprotocol/ext-server-card](https://github.com/modelcontextprotocol/ext-server-card)**
82> [card] [modelcontextprotocol/ext-server-card](https://github.com/modelcontextprotocol/ext-server-card)
8383>
8484> HTTP discovery of MCP server metadata.
8585 

extensions/skills/overview Changed · +4 / -2 lines

from line 12
1212[implementations list](https://github.com/modelcontextprotocol/ext-skills/blob/main/docs/implementations.md)
1313for current coverage.
1414 
15> **[modelcontextprotocol/ext-skills](https://github.com/modelcontextprotocol/ext-skills)**
15> [card] [modelcontextprotocol/ext-skills](https://github.com/modelcontextprotocol/ext-skills)
1616>
1717> Specification and documentation for Skills over MCP.
1818 
from line 269
269269 "name": "checklist.md",
270270 "mimeType": "text/markdown"
271271 }
272 ]
272 ],
273 "ttlMs": 300000,
274 "cacheScope": "public"
273275 }
274276}
275277```

specification/2024-11-05/basic/index Changed · +6 / -6 lines

from line 35
3535 
3636See the following pages for more details on the different components:
3737 
38> **[Lifecycle](https://modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle.md)**
38> [card] [Lifecycle](https://modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle.md)
3939 
40> **[Resources](https://modelcontextprotocol.io/specification/2024-11-05/server/resources.md)**
40> [card] [Resources](https://modelcontextprotocol.io/specification/2024-11-05/server/resources.md)
4141 
42> **[Prompts](https://modelcontextprotocol.io/specification/2024-11-05/server/prompts.md)**
42> [card] [Prompts](https://modelcontextprotocol.io/specification/2024-11-05/server/prompts.md)
4343 
44> **[Tools](https://modelcontextprotocol.io/specification/2024-11-05/server/tools.md)**
44> [card] [Tools](https://modelcontextprotocol.io/specification/2024-11-05/server/tools.md)
4545 
46> **[Logging](https://modelcontextprotocol.io/specification/2024-11-05/server/utilities/logging.md)**
46> [card] [Logging](https://modelcontextprotocol.io/specification/2024-11-05/server/utilities/logging.md)
4747 
48> **[Sampling](https://modelcontextprotocol.io/specification/2024-11-05/client/sampling.md)**
48> [card] [Sampling](https://modelcontextprotocol.io/specification/2024-11-05/client/sampling.md)
4949 
5050## Auth
5151 

specification/2024-11-05/index Changed · +5 / -5 lines

from line 116
116116 
117117Explore the detailed specification for each protocol component:
118118 
119> **[Architecture](https://modelcontextprotocol.io/specification/2024-11-05/architecture.md)**
119> [card] [Architecture](https://modelcontextprotocol.io/specification/2024-11-05/architecture.md)
120120 
121> **[Base Protocol](https://modelcontextprotocol.io/specification/2024-11-05/basic.md)**
121> [card] [Base Protocol](https://modelcontextprotocol.io/specification/2024-11-05/basic.md)
122122 
123> **[Server Features](https://modelcontextprotocol.io/specification/2024-11-05/server.md)**
123> [card] [Server Features](https://modelcontextprotocol.io/specification/2024-11-05/server.md)
124124 
125> **[Client Features](https://modelcontextprotocol.io/specification/2024-11-05/client)**
125> [card] [Client Features](https://modelcontextprotocol.io/specification/2024-11-05/client)
126126 
127> **[Contributing](https://modelcontextprotocol.io/community/contributing.md)**
127> [card] [Contributing](https://modelcontextprotocol.io/community/contributing.md)
128128 

specification/2024-11-05/server/index Changed · +3 / -3 lines

from line 20
2020 
2121Explore these key primitives in more detail below:
2222 
23> **[Prompts](https://modelcontextprotocol.io/specification/2024-11-05/server/prompts.md)**
23> [card] [Prompts](https://modelcontextprotocol.io/specification/2024-11-05/server/prompts.md)
2424 
25> **[Resources](https://modelcontextprotocol.io/specification/2024-11-05/server/resources.md)**
25> [card] [Resources](https://modelcontextprotocol.io/specification/2024-11-05/server/resources.md)
2626 
27> **[Tools](https://modelcontextprotocol.io/specification/2024-11-05/server/tools.md)**
27> [card] [Tools](https://modelcontextprotocol.io/specification/2024-11-05/server/tools.md)
2828 

specification/2025-03-26/index Changed · +5 / -5 lines

from line 118
118118 
119119Explore the detailed specification for each protocol component:
120120 
121> **[Architecture](https://modelcontextprotocol.io/specification/2025-03-26/architecture.md)**
121> [card] [Architecture](https://modelcontextprotocol.io/specification/2025-03-26/architecture.md)
122122 
123> **[Base Protocol](https://modelcontextprotocol.io/specification/2025-03-26/basic.md)**
123> [card] [Base Protocol](https://modelcontextprotocol.io/specification/2025-03-26/basic.md)
124124 
125> **[Server Features](https://modelcontextprotocol.io/specification/2025-03-26/server.md)**
125> [card] [Server Features](https://modelcontextprotocol.io/specification/2025-03-26/server.md)
126126 
127> **[Client Features](https://modelcontextprotocol.io/specification/2025-03-26/client)**
127> [card] [Client Features](https://modelcontextprotocol.io/specification/2025-03-26/client)
128128 
129> **[Contributing](https://modelcontextprotocol.io/community/contributing.md)**
129> [card] [Contributing](https://modelcontextprotocol.io/community/contributing.md)
130130 

specification/2025-03-26/server/index Changed · +3 / -3 lines

from line 20
2020 
2121Explore these key primitives in more detail below:
2222 
23> **[Prompts](https://modelcontextprotocol.io/specification/2025-03-26/server/prompts.md)**
23> [card] [Prompts](https://modelcontextprotocol.io/specification/2025-03-26/server/prompts.md)
2424 
25> **[Resources](https://modelcontextprotocol.io/specification/2025-03-26/server/resources.md)**
25> [card] [Resources](https://modelcontextprotocol.io/specification/2025-03-26/server/resources.md)
2626 
27> **[Tools](https://modelcontextprotocol.io/specification/2025-03-26/server/tools.md)**
27> [card] [Tools](https://modelcontextprotocol.io/specification/2025-03-26/server/tools.md)
2828 

specification/2025-06-18/index Changed · +5 / -5 lines

from line 120
120120 
121121Explore the detailed specification for each protocol component:
122122 
123> **[Architecture](https://modelcontextprotocol.io/specification/2025-06-18/architecture.md)**
123> [card] [Architecture](https://modelcontextprotocol.io/specification/2025-06-18/architecture.md)
124124 
125> **[Base Protocol](https://modelcontextprotocol.io/specification/2025-06-18/basic.md)**
125> [card] [Base Protocol](https://modelcontextprotocol.io/specification/2025-06-18/basic.md)
126126 
127> **[Server Features](https://modelcontextprotocol.io/specification/2025-06-18/server.md)**
127> [card] [Server Features](https://modelcontextprotocol.io/specification/2025-06-18/server.md)
128128 
129> **[Client Features](https://modelcontextprotocol.io/specification/2025-06-18/client)**
129> [card] [Client Features](https://modelcontextprotocol.io/specification/2025-06-18/client)
130130 
131> **[Contributing](https://modelcontextprotocol.io/community/contributing.md)**
131> [card] [Contributing](https://modelcontextprotocol.io/community/contributing.md)
132132 

specification/2025-06-18/server/index Changed · +3 / -3 lines

from line 20
2020 
2121Explore these key primitives in more detail below:
2222 
23> **[Prompts](https://modelcontextprotocol.io/specification/2025-06-18/server/prompts.md)**
23> [card] [Prompts](https://modelcontextprotocol.io/specification/2025-06-18/server/prompts.md)
2424 
25> **[Resources](https://modelcontextprotocol.io/specification/2025-06-18/server/resources.md)**
25> [card] [Resources](https://modelcontextprotocol.io/specification/2025-06-18/server/resources.md)
2626 
27> **[Tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools.md)**
27> [card] [Tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools.md)
2828 
Feedback