tested on mac for both python and node

This commit is contained in:
Zachary Lee 2024-12-10 12:58:14 -05:00
commit de7df3fd78
No known key found for this signature in database
7 changed files with 703 additions and 85 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
images/current-weather.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 756 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
images/weather-alerts.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View file

@ -1,11 +1,9 @@
---
title: "Quickstart"
description: "Get started with building your first MCP server and connecting it to an MCP host"
description: "Get started with building your first MCP server and connecting it to a host"
---
In this tutorial, we'll build a simple MCP server with a `get_weather` tool and connect it to a popular MCP host, Claude for Desktop. We'll start with a basic setup, and then progress to more complex use cases.
You can think of MCP as a bridge between your LLMs and your data and tools. Model Context Protocol was developed by Anthropic (the makers of [Claude](https://claude.ai)), but MCP can work with any Large Language Model provider that supports tool calling.
In this tutorial, we'll build a simple MCP weather server and connect it to a host, Claude for Desktop. We'll start with a basic setup, and then progress to more complex use cases.
### What is MCP?
@ -18,13 +16,20 @@ MCP is an open protocol that standardizes how applications provide context to LL
### What we'll be building
Many LLMs (including Claude) do not currently have the ability to fetch the current temperature and weather conditions. Let's use MCP to solve that!
Many LLMs (including Claude) do not currently have the ability to fetch the forecast and severe weather alerts. Let's use MCP to solve that!
We'll build a server that exposes two tools: `get-alerts` and `get-forecast`. Then we'll connect the server to an MCP host (in this case, Claude Desktop):
<Frame>
<img src="/images/quickstart-weather.png" />
<img src="/images/weather-alerts.png" />
</Frame>
<Frame>
<img src="/images/current-weather.png" />
</Frame>
Servers can connect to any host. We'll walk through using Claude Desktop as the host and eventually show you how to build your own.
<Note>
Servers can connect to any host. We've chosen Claude desktop here for simplicity, but we also have guides on [building your own client](/tutorials/building-a-client)
</Note>
<Accordion title="Why Claude Desktop and not Claude.ai?">
Because servers are locally run, MCP currently only supports desktop hosts. Remote hosts are in active development.
@ -50,7 +55,7 @@ It also helps to be familiar with [tool use (aka function calling)](https://docs
### System requirements
For Python, make sure you have the latest version of Python installed. For TypeScript, make sure you have the latest version of Node installed.
For Python, make sure you have Python 3.9 or higher installed. For TypeScript, make sure you have the latest version of Node installed.
### Set up your environment
@ -64,11 +69,14 @@ First, let's install `uv` and set up our Python project and environment:
curl -LsSf https://astral.sh/uv/install.sh | sh
```
```bash Windows
```powershell Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```
</CodeGroup>
Make sure to restart your terminal afterwards to ensure that the `uv` command gets picked up.
</Tab>
<Tab title="Node">
First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/).
@ -98,17 +106,16 @@ source .venv/bin/activate
# Install dependencies
uv pip install mcp httpx
# Clean up boilerplate code
rm -rf hello.py
# Remove template file
rm hello.py
# Create our files
mkdir src
mkdir src/weather
mkdir -p src/weather
touch src/weather/__init__.py
touch src/weather/server.py
```
```bash Windows
```powershell Windows
# Create a new directory for our project
uv init mcp-quickstart
cd mcp-quickstart
@ -121,13 +128,13 @@ uv venv
uv pip install mcp httpx
# Clean up boilerplate code
del /f /q hello.py
rm hello.py
# Create our files
md src
md src\weather
echo.> src\__init__.py
echo.> src\server.py
new-item src\weather\__init__.py
new-item src\weather\server.py
```
</CodeGroup>
@ -160,7 +167,9 @@ __all__ = ['main', 'server']
</Tab>
<Tab title="Node">
```bash
<CodeGroup>
```bash MacOS/Linux
# Create a new directory for our project
mkdir mcp-quickstart
cd mcp-quickstart
@ -169,23 +178,39 @@ cd mcp-quickstart
npm init -y
# Install dependencies
npm install @modelcontextprotocol/sdk node-fetch
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript
```
Then create a new file called `src/index.ts` in your project directory:
```bash
# Create our files
mkdir src
touch src/index.ts
```
```powershell Windows
# Create a new directory for our project
md mcp-quickstart
cd mcp-quickstart
# Initialize a new npm project
npm init -y
# Install dependencies
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript
# Create our files
md src
new-item src\index.ts
```
</CodeGroup>
Update your package.json to add type: "module" and a build script:
```json package.json
{
"type": "module",
"bin": {
"mcp-quickstart": "./build/index.js"
"weather": "./build/index.js"
},
"scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
@ -196,7 +221,7 @@ Update your package.json to add type: "module" and a build script:
}
```
Create a tsconfig.json:
Create a `tsconfig.json` in the root of your project:
```json tsconfig.json
{
@ -224,113 +249,563 @@ Now let's dive into building your server.
### Importing packages
Add these to your server.py:
<Tabs>
<Tab title="Python">
Add these to the top of your `server.py`:
```python
from typing import Any
import httpx
from mcp.server.models import InitializationOptions
import mcp.types as types
from mcp.server import NotificationOptions, Server
import mcp.server.stdio
```
</Tab>
<Tab title="Node">
Add these to the top of your `src/index.ts`:
```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
```
</Tab>
</Tabs>
### Setting up the instance
Then add the cache, server instance and the base URL for the NWS API:
<Tabs>
<Tab title="Python">
Then initialize the server instance and the base URL for the NWS API:
```python
# Cache weather data
weather_cache: dict[str, Any] = {}
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"
server = Server("weather")
```
</Tab>
<Tab title="Node">
Then initialize the NWS API base URL, validation schemas, and server instance:
```typescript
const NWS_API_BASE = "https://api.weather.gov";
const USER_AGENT = "weather-app/1.0";
// Define Zod schemas for validation
const AlertsArgumentsSchema = z.object({
state: z.string().length(2),
});
const ForecastArgumentsSchema = z.object({
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180),
});
// Create server instance
const server = new Server(
{
name: "weather",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
```
</Tab>
</Tabs>
### Implementing tool listing
<Tabs>
<Tab title="Python">
We need to tell clients what tools are available. The `list_tools()` decorator registers this handler:
```python
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""
List available tools.
Each tool specifies its arguments using JSON Schema validation.
"""
return [
types.Tool(
name="get-weather",
description="Get weather data for a location",
name="get-alerts",
description="Get weather alerts for a state",
inputSchema={
"type": "object",
"properties": {
"latitude": {"type": "number"},
"longitude": {"type": "number"},
"state": {
"type": "string",
"description": "Two-letter state code (e.g. CA, NY)",
},
},
"required": ["state"],
},
),
types.Tool(
name="get-forecast",
description="Get weather forecast for a location",
inputSchema={
"type": "object",
"properties": {
"latitude": {
"type": "number",
"description": "Latitude of the location",
},
"longitude": {
"type": "number",
"description": "Longitude of the location",
},
},
"required": ["latitude", "longitude"],
},
)
),
]
```
This defines a single tool that requires latitude and longitude parameters.
```
</Tab>
<Tab title="Node">
We need to tell clients what tools are available. This `server.setRequestHandler` call will register this list for us:
```typescript
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get-alerts",
description: "Get weather alerts for a state",
inputSchema: {
type: "object",
properties: {
state: {
type: "string",
description: "Two-letter state code (e.g. CA, NY)",
},
},
required: ["state"],
},
},
{
name: "get-forecast",
description: "Get weather forecast for a location",
inputSchema: {
type: "object",
properties: {
latitude: {
type: "number",
description: "Latitude of the location",
},
longitude: {
type: "number",
description: "Longitude of the location",
},
},
required: ["latitude", "longitude"],
},
},
],
};
});
```
</Tab>
</Tabs>
This defines our two tools: `get-alerts` and `get-forecast`.
### Helper functions
Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
<Tabs>
<Tab title="Python">
```python
async def make_nws_request(client: httpx.AsyncClient, url: str) -> dict[str, Any] | None:
"""Make a request to the NWS API with proper error handling."""
headers = {
"User-Agent": USER_AGENT,
"Accept": "application/geo+json"
}
try:
response = await client.get(url, headers=headers, timeout=30.0)
response.raise_for_status()
return response.json()
except Exception:
return None
def format_alert(feature: dict) -> str:
"""Format an alert feature into a concise string."""
props = feature["properties"]
return (
f"Event: {props.get('event', 'Unknown')}\n"
f"Area: {props.get('areaDesc', 'Unknown')}\n"
f"Severity: {props.get('severity', 'Unknown')}\n"
f"Status: {props.get('status', 'Unknown')}\n"
f"Headline: {props.get('headline', 'No headline')}\n"
"---"
)
```
</Tab>
<Tab title="Node">
```typescript
// Helper function for making NWS API requests
async function makeNWSRequest<T>(url: string): Promise<T | null> {
const headers = {
"User-Agent": USER_AGENT,
Accept: "application/geo+json",
};
try {
const response = await fetch(url, { headers });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return (await response.json()) as T;
} catch (error) {
console.error("Error making NWS request:", error);
return null;
}
}
interface AlertFeature {
properties: {
event?: string;
areaDesc?: string;
severity?: string;
status?: string;
headline?: string;
};
}
// Format alert data
function formatAlert(feature: AlertFeature): string {
const props = feature.properties;
return [
`Event: ${props.event || "Unknown"}`,
`Area: ${props.areaDesc || "Unknown"}`,
`Severity: ${props.severity || "Unknown"}`,
`Status: ${props.status || "Unknown"}`,
`Headline: ${props.headline || "No headline"}`,
"---",
].join("\n");
}
interface ForecastPeriod {
name?: string;
temperature?: number;
temperatureUnit?: string;
windSpeed?: string;
windDirection?: string;
shortForecast?: string;
}
interface AlertsResponse {
features: AlertFeature[];
}
interface PointsResponse {
properties: {
forecast?: string;
};
}
interface ForecastResponse {
properties: {
periods: ForecastPeriod[];
};
}
```
</Tab>
</Tabs>
### Implementing tool execution
The tool execution handler processes requests to get weather data:
The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
<Tabs>
<Tab title="Python">
```python
@server.call_tool()
async def handle_call_tool(
name: str,
arguments: dict | None
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
# Validate tool name and arguments
if name != "get-weather":
raise ValueError(f"Unknown tool: {name}")
"""
Handle tool execution requests.
Tools can fetch weather data and notify clients of changes.
"""
if not arguments:
raise ValueError("Missing arguments")
lat = arguments.get("latitude")
lon = arguments.get("longitude")
if lat is None or lon is None:
raise ValueError("Missing latitude or longitude")
# Fetch weather data
async with httpx.AsyncClient() as client:
# Get grid point
points_url = f"{NWS_API_BASE}/points/{lat},{lon}"
points_response = await client.get(
points_url,
headers={"Accept": "application/json"}
)
points_data = points_response.json()
# Get forecast
forecast_url = points_data["properties"]["forecast"]
forecast_response = await client.get(
forecast_url,
headers={"Accept": "application/json"}
)
forecast_data = forecast_response.json()
if name == "get-alerts":
state = arguments.get("state")
if not state:
raise ValueError("Missing state parameter")
# Cache the forecast data
location_key = f"{lat},{lon}"
weather_cache[location_key] = forecast_data
# Notify clients of resource changes
await server.request_context.session.send_resource_list_changed()
return [
types.TextContent(
# Convert state to uppercase to ensure consistent format
state = state.upper()
if len(state) != 2:
raise ValueError("State must be a two-letter code (e.g. CA, NY)")
async with httpx.AsyncClient() as client:
alerts_url = f"{NWS_API_BASE}/alerts?area={state}"
alerts_data = await make_nws_request(client, alerts_url)
if not alerts_data:
return [types.TextContent(type="text", text="Failed to retrieve alerts data")]
features = alerts_data.get("features", [])
if not features:
return [types.TextContent(type="text", text=f"No active alerts for {state}")]
# Format each alert into a concise string
formatted_alerts = [format_alert(feature) for feature in features]
alerts_text = f"Active alerts for {state}:\n\n" + "\n".join(formatted_alerts)
return [
types.TextContent(
type="text",
text=alerts_text
)
]
elif name == "get-forecast":
try:
latitude = float(arguments.get("latitude"))
longitude = float(arguments.get("longitude"))
except (TypeError, ValueError):
return [types.TextContent(
type="text",
text=f"Retrieved weather data for location ({lat}, {lon})",
)
]
text="Invalid coordinates. Please provide valid numbers for latitude and longitude."
)]
# Basic coordinate validation
if not (-90 <= latitude <= 90) or not (-180 <= longitude <= 180):
return [types.TextContent(
type="text",
text="Invalid coordinates. Latitude must be between -90 and 90, longitude between -180 and 180."
)]
async with httpx.AsyncClient() as client:
# First get the grid point
lat_str = f"{latitude:.4f}"
lon_str = f"{longitude:.4f}"
points_url = f"{NWS_API_BASE}/points/{lat_str},{lon_str}"
points_data = await make_nws_request(client, points_url)
if not points_data:
return [types.TextContent(type="text", text=f"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).")]
# Extract forecast URL from the response
properties = points_data.get("properties", {})
forecast_url = properties.get("forecast")
if not forecast_url:
return [types.TextContent(type="text", text="Failed to get forecast URL from grid point data")]
# Get the forecast
forecast_data = await make_nws_request(client, forecast_url)
if not forecast_data:
return [types.TextContent(type="text", text="Failed to retrieve forecast data")]
# Format the forecast periods
periods = forecast_data.get("properties", {}).get("periods", [])
if not periods:
return [types.TextContent(type="text", text="No forecast periods available")]
# Format each period into a concise string
formatted_forecast = []
for period in periods:
forecast_text = (
f"{period.get('name', 'Unknown')}:\n"
f"Temperature: {period.get('temperature', 'Unknown')}°{period.get('temperatureUnit', 'F')}\n"
f"Wind: {period.get('windSpeed', 'Unknown')} {period.get('windDirection', '')}\n"
f"{period.get('shortForecast', 'No forecast available')}\n"
"---"
)
formatted_forecast.append(forecast_text)
forecast_text = f"Forecast for {latitude}, {longitude}:\n\n" + "\n".join(formatted_forecast)
return [types.TextContent(
type="text",
text=forecast_text
)]
else:
raise ValueError(f"Unknown tool: {name}")
```
</Tab>
<Tab title="Node">
```typescript
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === "get-alerts") {
const { state } = AlertsArgumentsSchema.parse(args);
const stateCode = state.toUpperCase();
const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
if (!alertsData) {
return {
content: [
{
type: "text",
text: "Failed to retrieve alerts data",
},
],
};
}
const features = alertsData.features || [];
if (features.length === 0) {
return {
content: [
{
type: "text",
text: `No active alerts for ${stateCode}`,
},
],
};
}
const formattedAlerts = features.map(formatAlert);
const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join(
"\n"
)}`;
return {
content: [
{
type: "text",
text: alertsText,
},
],
};
} else if (name === "get-forecast") {
const { latitude, longitude } = ForecastArgumentsSchema.parse(args);
// Get grid point data
const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(
4
)},${longitude.toFixed(4)}`;
const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
if (!pointsData) {
return {
content: [
{
type: "text",
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).`,
},
],
};
}
const forecastUrl = pointsData.properties?.forecast;
if (!forecastUrl) {
return {
content: [
{
type: "text",
text: "Failed to get forecast URL from grid point data",
},
],
};
}
// Get forecast data
const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
if (!forecastData) {
return {
content: [
{
type: "text",
text: "Failed to retrieve forecast data",
},
],
};
}
const periods = forecastData.properties?.periods || [];
if (periods.length === 0) {
return {
content: [
{
type: "text",
text: "No forecast periods available",
},
],
};
}
// Format forecast periods
const formattedForecast = periods.map((period: ForecastPeriod) =>
[
`${period.name || "Unknown"}:`,
`Temperature: ${period.temperature || "Unknown"}°${
period.temperatureUnit || "F"
}`,
`Wind: ${period.windSpeed || "Unknown"} ${
period.windDirection || ""
}`,
`${period.shortForecast || "No forecast available"}`,
"---",
].join("\n")
);
const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join(
"\n"
)}`;
return {
content: [
{
type: "text",
text: forecastText,
},
],
};
} else {
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(
`Invalid arguments: ${error.errors
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join(", ")}`
);
}
throw error;
}
});
```
</Tab>
</Tabs>
### Running the server
Finally, implement the main function to run the server:
<Tabs>
<Tab title="Python">
```python
async def main():
# Run the server using stdin/stdout streams
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
@ -345,6 +820,25 @@ async def main():
),
)
```
</Tab>
<Tab title="Node">
```typescript
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});
```
Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect.
</Tab>
</Tabs>
Your server is complete! Let's now test your server from an existing MCP host, Claude for Desktop.
@ -356,7 +850,7 @@ Unfortunately, Claude for Desktop is not yet available on Linux. Linux users can
First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download)
Open your Claude Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
Next, open your Claude Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
For example, if you have [VS Code](https://code.visualstudio.com/) installed:
@ -378,7 +872,8 @@ Add this configuration (replace the parent folder path):
<Tabs>
<Tab title="MacOS/Linux">
```json
<CodeGroup>
```json Python
{
"mcpServers": {
"weather": {
@ -393,9 +888,25 @@ Add this configuration (replace the parent folder path):
}
}
```
```json Node
{
"mcpServers": {
"weather": {
"command": "node",
"args": [
"/PATH/TO/PARENT/FOLDER/mcp-quickstart/build/index.js",
]
}
}
}
```
</CodeGroup>
</Tab>
<Tab title="Windows">
```json
<CodeGroup>
```json Python
{
"mcpServers": {
"weather": {
@ -410,24 +921,56 @@ Add this configuration (replace the parent folder path):
}
}
```
```json Node
{
"mcpServers": {
"weather": {
"command": "node",
"args": [
"C:\\PATH\TO\PARENT\FOLDER\mcp-quickstart\build\index.ts",
]
}
}
}
```
</CodeGroup>
</Tab>
</Tabs>
This tells Claude Desktop:
1. There's an MCP server named "weather"
2. Launch it by running `uv --directory /PATH/TO/PARENT/FOLDER/mcp-quickstart run weather`
2. Launch it by running `uv --directory /PATH/TO/PARENT/FOLDER/mcp-quickstart run weather` if using Python or `node /PATH/TO/PARENT/FOLDER/mcp-quickstart/build/index.js` if using Node
Save the file, and restart **Claude Desktop**.
### Test with commands
You can now test your server by running the following commands in Claude Desktop:
- What's the weather in Brooklyn?
- What's the weather in San Francisco?
First, make sure Claude Desktop is picking up the two tools we've exposed in our `weather` server. You can do this by looking for the hammer <img src="/images/claude-desktop-mcp-hammer-icon.svg" style={{display: 'inline', margin: 0, height: '1.3em'}} /> icon:
<Frame>
<img src="/images/quickstart-weather.png" />
<img src="/images/visual-indicator-mcp-tools.png" />
</Frame>
After clicking on the hammer icon, you should see two tools listed:
<Frame>
<img src="/images/available-mcp-tools.png" />
</Frame>
If your server isn't being picked up by Claude Desktop, proceed to the **Troubleshooting** section for debugging advice.
You can now test your server by running the following commands in Claude Desktop:
- What's the weather in Sacramento?
- What are the active weather alerts in Texas?
<Frame>
<img src="/images/current-weather.png" />
</Frame>
<Frame>
<img src="/images/weather-alerts.png" />
</Frame>
<Note>
@ -445,4 +988,79 @@ When you ask a question:
5. Claude formulates a natural language response
6. The response is displayed to you!
## Next steps
## Troubleshooting
<AccordionGroup>
<Accordion title="Weather API Issues">
**Error: Failed to retrieve grid point data**
This usually means either:
1. The coordinates are outside the US
2. The NWS API is having issues
3. You're being rate limited
Fix:
- Verify you're using US coordinates
- Add a small delay between requests
- Check the NWS API status page
**Error: No active alerts for [STATE]**
This isn't an error - it just means there are no current weather alerts for that state. Try a different state or check during severe weather.
</Accordion>
<Accordion title="Claude Desktop Integration Issues">
**Server not showing up in Claude**
1. Check your configuration file syntax
2. Make sure the path to your project is correct
3. Restart Claude Desktop completely
You can also check Claude's logs for errors like so:
```bash
# Check Claude's logs for errors
tail -f ~/Library/Application\ Support/Claude/Claude.log
```
**Tool calls failing silently**
If Claude attempts to use the tools but they fail:
1. Check Claude's logs for errors
2. Verify your server runs without errors
3. Try restarting Claude Desktop
</Accordion>
</AccordionGroup>
## Next steps
<CardGroup cols={2}>
<Card
title="Building a client"
icon="outlet"
href="/tutorials/building-a-client"
>
Learn how to build your first MCP client
</Card>
<Card
title="Building a local fileserver"
icon="files"
href="/examples"
>
Build an intermediate MCP server that communicates with your local filesystem
</Card>
<Card
title="Example servers"
icon="grid"
href="/examples"
>
Check out our gallery of official MCP servers and implementations
</Card>
<Card
title="Core architecture"
icon="sitemap"
href="/docs/concepts/architecture"
>
Understand how MCP connects clients, servers, and LLMs
</Card>
</CardGroup>