Beginner's Guide to MCP (Model Context Protocol)


Anthropic’s MCP is the missing bridge between LLM’s and unstructured data. This blog explains what it is and how can we build on top of it. MCP aims to enhance AI responses by providing access to contextual information from systems such as content repositories, business tools, and local files. This connectivity is crucial for scaling AI applications without the need for custom integrations for each data source. MCP's architecture is client-server based, with MCP hosts (e.g., Claude Desktop, IDEs) connecting to MCP servers that expose specific capabilities. This standardization, built on JSON-RPC, facilitates secure, two-way communication, as detailed in MCP Introduction.
For this blog, we will build an MCP server that can list and read your local filesystem. This will allow an AI assistant to list files and read their contents within a specified directory, ensuring security by restricting access to prevent unauthorized file operations.
To build this MCP server, we will use the MCP Python SDK. The server must handle operations like listing files, reading and writing content, creating directories, deleting paths, and moving files, while restricting access to designated subdirectories (e.g., pictures, documents, downloads, developer).
We are going to start by creating a uv
project. UV is probably the best way to manage your Python project.
uv init mcp_example
We will then add add the MCP Python SDK as a dependency.
uv add "mcp[cli]"
Optionally, I like ruff as my python formatter.
uv add --dev ruff
We will also add python-dotenv
to read the .env
file. This environment file will have the location of our root directory and list of folders relative to root the LLM can access through MCP.
uv add python-dotenv
Now, lets get on the real stuff. Inside your project root, create a src
folder and then create a server.py
inside of src
. This server.py
will have all the code related to out MCP server. We will first load all the environment variables and then setup an MCP server using FastMCP
provided by the mcp
package.
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
# Load environment variables and set up the MCP server
load_dotenv()
mcp = FastMCP("MCP Example Server")
After this, we will setup, two variables, ROOT_DIR
and ALLOWED_SUBDIRS
that gets it values from .env
.
# Set up directory configuration
ROOT_DIR = Path(os.path.expanduser(os.getenv("MCP_ROOT_DIR", "~")))
ALLOWED_SUBDIRS = set(filter(None, os.getenv("MCP_ALLOWED_SUBDIRS", "").split(",")))
Now, let’s create a function that will check if a path is inside of ALLOWED_SUBDIRS
. This will help us to prevent an LLM to access files outside of any allowed sub-directories.
def is_allowed_path(path: Path) -> bool:
"""Check if a path is within allowed subdirectories."""
if not path.is_relative_to(ROOT_DIR):
return False
return any(path.is_relative_to(ROOT_DIR / subdir) for subdir in ALLOWED_SUBDIRS)
Now, we will create “MCP Tools”. These tools will have a single function that the LLM will use through out MCP server to perform those functions. Our first tool will be to list all the files present in a sub-directory.
@mcp.tool()
def list_files(directory: str) -> list[str]:
"""List files in a directory within allowed subdirectories."""
if directory.startswith("/"):
raise ValueError("Path cannot be absolute")
full_path = ROOT_DIR / directory
if not full_path.is_dir():
raise FileNotFoundError(f"Path {directory} is not a directory")
if not is_allowed_path(full_path):
raise ValueError(f"Path {directory} is not allowed")
return [f.name for f in full_path.iterdir() if f.is_file()]
Similarly, we will create tools to do other functions, like reading, writing, deleting, moving and creating directories.
@mcp.tool()
def read_file(file_path: str) -> str:
"""Read the content of a file within allowed subdirectories."""
if file_path.startswith("/"):
raise ValueError("Path cannot be absolute")
full_path = ROOT_DIR / file_path
if not full_path.is_file():
raise FileNotFoundError(f"File {file_path} does not exist")
if not is_allowed_path(full_path):
raise ValueError(f"Path {file_path} is not allowed")
with open(full_path, "r") as f:
return f.read()
@mcp.tool()
def write_file(file_path: str, content: str) -> None:
"""Write content to a file within allowed subdirectories."""
if file_path.startswith("/"):
raise ValueError("Path cannot be absolute")
full_path = ROOT_DIR / file_path
if not is_allowed_path(full_path):
raise ValueError(f"Path {file_path} is not allowed")
# Create parent directory if needed
dir_path = full_path.parent
if not dir_path.exists():
try:
dir_path.mkdir(parents=True, exist_ok=True)
except (PermissionError, OSError) as e:
raise FileNotFoundError(f"Directory {dir_path} could not be created: {e}")
try:
with open(full_path, "w") as f:
f.write(content)
except (PermissionError, OSError) as e:
raise ValueError(f"Could not write to file {file_path}: {e}")
@mcp.tool()
def create_directory(directory: str) -> None:
"""Create a new directory within allowed subdirectories."""
if directory.startswith("/"):
raise ValueError("Path cannot be absolute")
full_path = ROOT_DIR / directory
if full_path.exists():
raise FileExistsError(f"Directory {directory} already exists")
if not is_allowed_path(full_path):
raise ValueError(f"Path {directory} is not allowed")
full_path.mkdir(parents=True, exist_ok=False)
@mcp.tool()
def delete_path(path_str: str) -> None:
"""Delete a file or directory within allowed subdirectories."""
if path_str.startswith("/"):
raise ValueError("Path cannot be absolute")
full_path = ROOT_DIR / path_str
if not is_allowed_path(full_path):
raise ValueError(f"Path {path_str} is not allowed")
if full_path == ROOT_DIR or full_path in [
ROOT_DIR / subdir for subdir in ALLOWED_SUBDIRS
]:
raise ValueError("Cannot delete root directory or allowed subdirectories")
if full_path.is_dir():
shutil.rmtree(str(full_path))
elif full_path.is_file():
full_path.unlink()
else:
raise ValueError(f"Path {path_str} does not exist")
@mcp.tool()
def move_file(source_path: str, destination_path: str) -> None:
"""Move or rename a file or directory within allowed subdirectories."""
if source_path.startswith("/") or destination_path.startswith("/"):
raise ValueError("Paths cannot be absolute")
full_source = ROOT_DIR / source_path
full_destination = ROOT_DIR / destination_path
if not is_allowed_path(full_source) or not is_allowed_path(full_destination):
raise ValueError("Both source and destination paths must be allowed")
if not full_source.exists():
raise FileNotFoundError(f"Source path {source_path} does not exist")
shutil.move(str(full_source), str(full_destination))
Now, it’s time to test our MCP server. Fortunately, we have MCP Inspector, to quickly test and debug our MCP server in the browser without the need to use an LLM.
To run our server in test mode,
mcp dev src/server.py
You will get something like
Starting MCP inspector...
Proxy server listening on port 3000
🔍 MCP Inspector is up and running at http://localhost:5173 🚀
Go to localhost:5173 and click on connect. This will establish a connection between MCP Inspector and our server.
After this, go to Tools and Click on List Tools. This will list all the tools that we created before in server.py
Try out each tools, by clicking on them, providing the inputs and clicking on “Run Tool”.
Ensuring everything is running fine, we can now use LLMs like Claude to interact with our MCP Server. Install the Anthropic Desktop App and then run
mcp install src/server.py -f .env
to install the MCP server into the Anthropic Desktop App. You can now see that our MCP tools were installed into the Claude Desktop App.
Subscribe to my newsletter
Read articles from Chandram Dutta directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Chandram Dutta
Chandram Dutta
swe intern. swift , flutter 💙, svelte 🧡, rust 🦀. he/him.