3 MCP Tutorials: Building MCP Servers and Clients
- 15 minutes to read
Table of Contents
What is Model Context Protocol (MCP)?
The Model Context Protocol (MCP) is an open standard that enables large language models (LLMs) to communicate with external tools, applications, databases, and services through a standardized interface. While LLMs are powerful, they are limited by the information available at the time they were trained and cannot directly interact with outside systems. MCP addresses these limitations by providing a common way for AI applications to retrieve current information and perform actions in external environments.
Instead of requiring developers to build separate integrations for every AI model and every external service, MCP defines a shared protocol that both sides can use. This allows AI applications to discover available tools, send structured requests, receive responses, and combine the results into a final answer for the user. As a result, LLMs can move beyond generating text from static knowledge and become agents that can access live data, interact with software, and complete multi-step tasks.
Tutorials included in this article:
- Building an MCP server with agent skills: Use AI coding assistant skills to choose a deployment path, scaffold an MCP server, add UI or packaging, test capabilities, and refine the tool surface.
- Building an MCP server manually: Create a Python weather MCP server with FastMCP, expose alert and forecast tools, run it over stdio, and connect it to Claude for Desktop.
- Building an MCP client: Build a Python client that connects to an MCP server, lists tools, sends queries to Claude, executes tool calls, and returns final responses.
How MCP Works
MCP uses a client-server architecture. The AI application, such as an IDE, chatbot, or agent platform, acts as the MCP host, while MCP clients inside that host connect to MCP servers. Each MCP server exposes a specific external capability, such as reading files, querying a database, searching documentation, calling an API, or interacting with a business application.
MCP servers commonly expose tools, resources, and prompts:
- Tools are actions the model can request, such as running a query or creating a ticket.
- Resources provide contextual data, such as files, schemas, records, or documents.
- Prompts define reusable instruction templates for specific workflows.
A typical flow starts with a user request, continues with the client selecting the right MCP server, and ends with the server returning data or action results for the model to use in its response.
To start working with MCP, developers should identify the external system they want the AI application to access, define the specific functions or data that should be exposed, and connect those capabilities through an MCP server. This keeps business logic inside the server while the client handles communication with the model, making integrations easier to reuse, secure, and maintain.
Tutorial #1: Building an MCP Server with Agent Skills
This and the following tutorial are adapted from the official MCP documentation. This tutorial shows how to start an MCP server project using agent skills.
Agent skills can help you design and scaffold an MCP server with less manual setup. They act as reusable instruction sets for AI coding assistants. For MCP development, they guide the assistant through choices such as deployment model, tool design, authentication, and packaging.
Step 1: Install the MCP Development Skills
First, install the MCP development skill set in your coding assistant.
For example, in Claude Code, you can install the plugin with:
/plugin marketplace add anthropics/claude-plugins-official
/plugin install mcp-server-dev
The plugin includes three related skills:
build-mcp-server # Main entry point for designing and scaffolding an MCP server
build-mcp-app # Adds interactive UI widgets inside chat
build-mcpb # Packages a local stdio server with its runtime
Each skill includes a SKILL.md file and supporting reference files. These files give the agent guidance on auth flows, tool design patterns, widget templates, and packaging formats.
Step 2: Describe What You Want to Build
After installing the skills, ask your agent to help build an MCP server.
For example:
Help me build an MCP server that connects to our internal ticketing API.
It should let users search tickets, create tickets, and update ticket status.
The API uses OAuth 2.0.
The server will be used by my team.
The agent will usually start with a discovery phase before writing code. It may ask what system the server connects to, who will use it, how many actions it should expose, whether users need interactive input, and what authentication method is required.
If your first prompt already includes those details, the agent can move directly to a recommendation.
Step 3: Choose the Right Deployment Path
The skill recommends a deployment path based on your use case.
- For cloud APIs, a remote Streamable HTTP server is usually the default. This works well when one hosted server should serve many users. It also supports OAuth flows because the server can handle redirects and store tokens.
- For applications that need richer user interaction, use an MCP app. This adds widgets such as searchable pickers, forms, charts, or dashboards that appear directly in chat.
- For local machine access, use an MCP Bundle. This is useful when the server needs to read local files, control desktop apps, or connect to localhost services. The bundle packages the server and runtime into a single .mcpb archive, so users do not need to install Node or Python separately.
- For early experiments, a local stdio server is still useful. It is simple for prototyping, and you can later package it as an MCP Bundle for distribution.
Step 4: Let the Agent Scaffold the Server
Once the deployment path is selected, the agent can generate the server structure.
A typical scaffold may include:
mcp-server/
src/
server.ts
tools/
searchTickets.ts
createTicket.ts
updateTicket.ts
package.json
README.md
For a ticketing API, the server might expose tools like:
server.tool("search_tickets", {
description: "Search tickets by keyword, status, or assignee",
inputSchema: {
query: "string",
status: "string",
assignee: "string"
}
});
The exact code depends on the deployment path, runtime, and API being wrapped. The important point is that the skill helps the agent choose a practical structure before generating code.
Step 5: Add Interaction or Packaging if Needed
If the server needs interactive UI, the main skill can route the project to build-mcp-app. This is useful when simple text responses or flat forms are not enough.
For example, a support dashboard could show:
- A searchable ticket picker
- A status filter
- A chart of open tickets by priority
- A live dashboard of recent updates
If the server must run locally on a user’s machine, the main skill can route the project to build-mcpb. This creates a distributable .mcpb package that includes the local server and its runtime.
Step 6: Test the MCP Server
After scaffolding, connect the server to an MCP-compatible host and test each exposed capability.
Test that:
- The host can discover the server
- Each tool has a clear name and input schema
- Authentication works
- Tool responses are structured and useful
- Errors are returned in a way the model can understand
This step helps confirm that the server is not just running, but usable by an AI application.
Step 7: Refine the Tool Surface
Keep the tool surface focused. A server should expose the operations the model actually needs, not every endpoint in an API.
For example, instead of wrapping an entire ticketing API, start with:
search_tickets
get_ticket_details
create_ticket
update_ticket_status
This makes the server easier to test, secure, and maintain. You can add more tools later as real use cases become clear.
Related content: Read our guide to agentic AI frameworks.
Tutorial #2: Building an MCP Server Manually
This tutorial shows how to build a simple MCP weather server in Python. The server exposes two tools:
get_alerts
get_forecast
The first tool returns active weather alerts for a US state. The second returns a forecast for a location using latitude and longitude.
Step 1: Set Up the Python Project
You need Python 3.10 or later and the Python MCP SDK version 1.2.0 or later.
Install uv first:
curl -LsSf https://astral.sh/uv/install.sh | sh
Restart your terminal, then create the project:
uv init weather
cd weather
uv venv
source .venv/bin/activate
uv add "mcp[cli]" httpx
touch weather.py
Step 2: Import Packages and Create the MCP Server
Open weather.py and add the base setup:
from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"
FastMCP uses Python type hints and docstrings to create tool definitions. This lets the MCP host understand what each tool does and what inputs it needs.
Step 3: Add Helper Functions
Add a helper function for calling the National Weather Service API:
async def make_nws_request(url: str) -> dict[str, Any] | None:
"""Make a request to the NWS API with basic error handling."""
headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
async with httpx.AsyncClient() as client:
try:
response = await client.get(url, headers=headers, timeout=30.0)
response.raise_for_status()
return response.json()
except Exception:
return None
Then add a formatter for weather alerts:
def format_alert(feature: dict) -> str:
"""Format an alert into a readable string."""
props = feature["properties"]
return f"""
Event: {props.get("event", "Unknown")}
Area: {props.get("areaDesc", "Unknown")}
Severity: {props.get("severity", "Unknown")}
Description: {props.get("description", "No description available")}
Instructions: {props.get("instruction", "No specific instructions provided")}
"""
Step 4: Create the get_alerts Tool
Add the first MCP tool:
@mcp.tool()
async def get_alerts(state: str) -> str:
"""Get weather alerts for a US state.
Args:
state: Two-letter US state code, such as CA or NY
"""
url = f"{NWS_API_BASE}/alerts/active/area/{state}"
data = await make_nws_request(url)
if not data or "features" not in data:
return "Unable to fetch alerts or no alerts found."
if not data["features"]:
return "No active alerts for this state."
alerts = [format_alert(feature) for feature in data["features"]]
return "\n---\n".join(alerts)
This tool accepts a two-letter US state code. It calls the active alerts endpoint and returns a readable list of alerts.
Step 5: Create the get_forecast Tool
Add the forecast tool:
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
"""Get weather forecast for a location.
Args:
latitude: Latitude of the location
longitude: Longitude of the location
"""
points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
points_data = await make_nws_request(points_url)
if not points_data:
return "Unable to fetch forecast data for this location."
forecast_url = points_data["properties"]["forecast"]
forecast_data = await make_nws_request(forecast_url)
if not forecast_data:
return "Unable to fetch detailed forecast."
periods = forecast_data["properties"]["periods"]
forecasts = []
for period in periods[:5]:
forecast = f"""
{period["name"]}:
Temperature: {period["temperature"]}°{period["temperatureUnit"]}
Wind: {period["windSpeed"]} {period["windDirection"]}
Forecast: {period["detailedForecast"]}
"""
forecasts.append(forecast)
return "\n---\n".join(forecasts)
This tool first converts latitude and longitude into a forecast endpoint. It then fetches the detailed forecast and returns the next five forecast periods.
Step 6: Run the MCP Server
Add the server entry point at the bottom of weather.py:
def main():
mcp.run(transport="stdio")
if __name__ == "__main__":
main()
Run the server with:
uv run weather.py
The server now listens for messages from an MCP host over stdio.
For stdio servers, do not write normal logs to stdout. MCP uses stdout for JSON-RPC messages, so extra output can break communication.
Use stderr or a logging library instead:
import sys
import logging
print("Processing request", file=sys.stderr)
logging.info("Processing request")
Step 7: Connect the Server to Claude for Desktop
Open the Claude for Desktop configuration file.
On macOS:
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
Add the weather server under mcpServers:
{
"mcpServers": {
"weather": {
"command": "uv",
"args": [
"--directory",
"/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
"run",
"weather.py"
]
}
}
}
Replace the directory value with the absolute path to your project.
You can find it with:
pwd
If Claude cannot find uv, use the full path to the executable:
which uv
Save the file and restart Claude for Desktop.
Step 8: Test the Tools
After restarting Claude for Desktop, open the connectors menu and check that the weather server appears.
Then test it with prompts such as:
What’s the weather in Sacramento?
What are the active weather alerts in Texas?
These examples use the US National Weather Service API, so they only work for US locations.
Step 9: Understand the Request Flow
When the user asks a weather question, the MCP host sends the question to the model. The model checks the available tools and decides whether to call get_alerts or get_forecast.
The client then executes the selected tool through the MCP server. The server calls the weather API and returns the result. The model uses that result to write a natural language answer for the user.
Tutorial #3: Build an MCP Client
This tutorial shows how to build a Python MCP client. The client connects to an MCP server, lists its available tools, sends user queries to Claude, executes tool calls, and returns the final response.
Step 1: Set Up the Project
Create a new Python project with uv:
uv init mcp-client
cd mcp-client
uv venv
source .venv/bin/activate
uv add mcp anthropic python-dotenv
rm main.py
touch client.py
You need a Mac or Windows computer, Python, and the latest version of uv.
Step 2: Add Your API Key
Create a .env file:
echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
Add it to .gitignore:
echo ".env" >> .gitignore
Do not commit API keys to source control.
Step 3: Create the Basic Client Class
Open client.py and add the imports and client setup:
import asyncio
from typing import Optional
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv()
class MCPClient:
def __init__(self):
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.anthropic = Anthropic()
The client stores the MCP session, manages cleanup with AsyncExitStack, and creates an Anthropic client for Claude requests.
Step 4: Connect to an MCP Server
Add a method that starts a server process and opens an MCP session:
async def connect_to_server(self, server_script_path: str):
"""Connect to an MCP server.
Args:
server_script_path: Path to a .py or .js server script
"""
is_python = server_script_path.endswith(".py")
is_js = server_script_path.endswith(".js")
if not (is_python or is_js):
raise ValueError("Server script must be a .py or .js file")
command = "python" if is_python else "node"
server_params = StdioServerParameters(
command=command,
args=[server_script_path],
env=None
)
stdio_transport = await self.exit_stack.enter_async_context(
stdio_client(server_params)
)
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(self.stdio, self.write)
)
await self.session.initialize()
response = await self.session.list_tools()
tools = response.tools
print("\nConnected to server with tools:", [tool.name for tool in tools])
This method supports Python and Node.js MCP servers. It creates the transport, initializes the session, and prints the tools exposed by the server.
Step 5: Process User Queries
Add the main query-processing method:
async def process_query(self, query: str) -> str:
"""Process a query using Claude and available MCP tools."""
messages = [
{
"role": "user",
"content": query
}
]
response = await self.session.list_tools()
available_tools = [
{
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
}
for tool in response.tools
]
response = self.anthropic.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1000,
messages=messages,
tools=available_tools
)
final_text = []
assistant_message_content = []
for content in response.content:
if content.type == "text":
final_text.append(content.text)
assistant_message_content.append(content)
elif content.type == "tool_use":
tool_name = content.name
tool_args = content.input
result = await self.session.call_tool(tool_name, tool_args)
final_text.append(
f"[Calling tool {tool_name} with args {tool_args}]"
)
assistant_message_content.append(content)
messages.append({
"role": "assistant",
"content": assistant_message_content
})
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": content.id,
"content": result.content
}
]
})
response = self.anthropic.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1000,
messages=messages,
tools=available_tools
)
final_text.append(response.content[0].text)
return "\n".join(final_text)
This method sends the user query and tool definitions to Claude. If Claude requests a tool, the client calls that tool through the MCP server, sends the result back to Claude, and returns the final answer.
Step 6: Add the Chat Loop
Add an interactive command-line loop:
async def chat_loop(self):
"""Run an interactive chat loop."""
print("\nMCP Client Started!")
print("Type your queries or 'quit' to exit.")
while True:
try:
query = input("\nQuery: ").strip()
if query.lower() == "quit":
break
response = await self.process_query(query)
print("\n" + response)
except Exception as e:
print(f"\nError: {str(e)}")
This loop reads user input, passes each query to process_query(), and prints the response.
Step 7: Clean Up Resources
Add a cleanup method:
async def cleanup(self):
"""Clean up resources."""
await self.exit_stack.aclose()
This closes the MCP session and related transport resources when the client exits.
Step 8: Add the Main Entry Point
Add the main execution logic at the bottom of client.py:
async def main():
if len(sys.argv) < 2:
print("Usage: python client.py <path_to_server_script>")
sys.exit(1)
client = MCPClient()
try:
await client.connect_to_server(sys.argv[1])
await client.chat_loop()
finally:
await client.cleanup()
if __name__ == "__main__":
import sys
asyncio.run(main())
The client expects the path to an MCP server script as a command-line argument.
Step 9: Run the Client
Run the client with a Python MCP server:
uv run client.py path/to/server.py
Or with a Node.js MCP server:
uv run client.py path/to/build/index.js
For the weather server from the previous tutorial, the command may look like this:
uv run client.py ../weather/weather.py
When the client starts, it connects to the server, lists available tools, and opens an interactive chat session.
Step 10: Test the Tool Flow
Once connected to the weather server, try a query such as:
What are the active weather alerts in Texas?
The client gets the available tools from the server and sends them to Claude with the user query. Claude decides whether to call a tool. If a tool is needed, the client executes it through the MCP server.
The tool result is then sent back to Claude. Claude uses that result to generate the final natural language response.
MCP Development Best Practices
1. Start with Read-Only MCP Use Cases
Begin MCP adoption with tools that retrieve information rather than modify systems. Read-only use cases, such as searching documents, querying internal knowledge bases, checking database records, or retrieving API data, allow teams to validate the protocol while keeping operational risk low.
This approach helps developers understand how MCP servers expose tools, how clients discover them, and how model-driven tool calls behave in real workflows. Once the organization is confident in tool design, permission handling, and user approval flows, it can gradually introduce write actions such as creating records, updating files, or triggering workflows.
2. Use Least Privilege for MCP Tools
MCP tools should only expose the minimum access required to complete a specific task. Avoid giving a server broad access to entire file systems, databases, cloud environments, or business applications when a narrower scope is enough. Each tool should have a clear purpose, well-defined inputs, and limited permissions.
For example, a file server should only access approved directories, and an API-connected server should use scoped credentials rather than full administrative access. Limiting permissions reduces the potential impact of prompt injection, accidental tool misuse, compromised credentials, or overly broad model actions.
3. Require Human Approval for High-Risk Actions
Because MCP tools can allow AI applications to perform actions in external systems, high-risk operations should require explicit user approval before execution. This is especially important for actions that modify files, send messages, update business records, delete data, trigger workflows, or access sensitive information.
Approval prompts should clearly show what tool is being called, what inputs are being used, and what action will occur. Users should be able to approve, deny, or adjust the request before the action is completed. Keeping a human in the loop helps preserve user control while reducing the chance of unintended or unsafe operations.
4. Maintain an Approved MCP Server Registry
Organizations should maintain an internal list of approved MCP servers that have been reviewed for security, functionality, ownership, and business purpose. This registry should include details such as the server name, maintainer, exposed tools, data access scope, authentication method, deployment model, and approved users or teams.
An approved registry helps prevent tool sprawl and reduces the risk of employees connecting unverified servers to sensitive systems. It also makes MCP environments easier to audit, update, and govern over time. As new servers are added, they should go through a review process before being made broadly available.
5. Monitor MCP Tool Usage with Security Analytics
MCP tool usage should be monitored as part of the organization’s broader security operations. Logs should capture which tools were invoked, which user or agent initiated the request, what external system was accessed, whether approval was granted, and whether the action succeeded or failed.
Security analytics can help detect unusual behavior, such as excessive tool calls, repeated failed requests, access to unexpected resources, or activity outside normal user patterns. Monitoring MCP activity gives security teams visibility into how AI agents interact with enterprise systems and helps identify misuse, misconfiguration, or compromised workflows early.
Monitoring MCP and OpenAI Agent Activity with Exabeam
As organizations connect OpenAI models to MCP servers, AI agents become a new class of non-human identity operating inside business systems at machine speed and scale. Exabeam delivers advanced user and entity behavior analytics (UEBA) that uses AI and automation to help security operations teams detect, investigate, and respond to threats involving users, entities, and AI agents through Agent Behavior Analytics (ABA). By baselining what normal behavior looks like and surfacing risky deviations, Exabeam gives teams the visibility needed to govern MCP-enabled OpenAI agents and the workflows they touch.
Key capabilities of Exabeam UEBA:
- AI agent behavior analytics: ABA extends UEBA to AI agents and other non-human identities, adding behavioral visibility, centralized monitoring, and investigation support so analysts can identify suspicious agent activity and respond with better context.
- Risk-based anomaly detection: New-Scale Analytics builds behavioral baselines and uses business context to refine risk scores, identifying subtle changes across users, entities, and non-human identities; when risk meets a set threshold, a case is opened for investigation.
- Entity context: Attack Surface Insights identifies entities in parsed logs and connects attributes and contextual data to build profiles that strengthen behavioral detections across on-premises and cloud environments.
- Automated investigation timelines: Automated timelines organize related detections chronologically to visualize the full scope of a threat, while activity summaries for anomalies and prebuilt playbooks reduce manual routines so teams respond faster.
- Flexible SIEM augmentation or replacement: New-Scale Analytics runs on top of an existing SIEM to add advanced behavioral analytics, while New-Scale Fusion provides a full-featured platform for a new deployment or a SIEM replacement.
Integrating ABA with Praxen and Observra
To secure AI agents, Exabeam combines runtime analytics with open-source frameworks for telemetry and verification. Integrating Agent Behavior Analytics (ABA) with Praxen and Observra creates a comprehensive security lifecycle for autonomous machine identities.
- Standardizing Agent Telemetry with Observra: Active AI agents generate complex, unstructured activity logs. Observra acts as an open-source SDK and logging library that captures runtime agent operations, including model calls, prompt details, tool usage, system costs, and errors. It normalizes this data into a structured event format, providing the consistent telemetry stream required for advanced threat detection.
- Verifying Agent Policies with Praxen: Before an agent is deployed, Praxen serves as an open-source reference implementation for Agent Behavior Verification (ABV). It compares the declared security policy of an AI agent against its actual underlying code, deployment state, and initial behavioral logs. This ensures that discrepancies, logic flaws, or unauthorized capabilities are flagged before the agent enters production.
- Continuous Behavioral Analysis with ABA: During active operations, the ABA engine ingests normalized telemetry from Observra and leverages the policy baselines established by Praxen. The engine applies machine learning models to build behavioral baselines for each agent, scoring risks and flagging real-time anomalies. If an agent drifts from its intended role, calls unapproved tools, or accesses unauthorized data, ABA immediately raises risk scores and triggers alerts.
This combined approach allows security teams to verify AI agent behavior prior to deployment and maintain continuous, baseline-driven visibility throughout active operations.
Exabeam offers a Model Context Protocol (MCP) server for developers that connects AI assistants directly to public API documentation, enabling teams to explore endpoints, analyze request and response schemas, and generate custom integration code through natural language conversation. In parallel, LogRhythm delivers a community-released SIEM MCP server that runs locally in on-premises environments to bridge security telemetry directly with chosen large language models, allowing analysts to run automated threat-hunting queries, triage active incidents, and summarize security alarms while keeping sensitive security logs inside the private.
To see how Exabeam helps security teams monitor and secure human and AI agent activity across the agentic enterprise, explore Exabeam’s UEBA capabilities. Read our MCP for Developers page to learn more on MCP as well.
Learn More About Exabeam
Learn about the Exabeam platform and expand your knowledge of information security with our collection of white papers, podcasts, webinars, and more.