The picture of this article is the output of Claude, using a local MCP server that gives it the output of the `ls -l` command on a given path. Notice how the LLM likes to praise me, exaggerating a bit, In my opinion this is just a way to keep your users hooked on their product. Who doesn't like to be praised and approved with everything they say, right?

Hello everyone,
I wanted to write an article on MCP servers since I just written one in Python last week.
Theory
MCP stands for Model Context Protocol, and it is a standardized protocol designed to connect LLMs with tools, resources and prompt templates. Tools are functions that get executed by LLMs on demand.
This is quite nice because once you have an MCP server set up you can use it with multiple LLMs, imagine the model calls your server's functions and you only need to tell the model which parameters the functions take and what data does it return. And of course some hints for the LLM to know how to use your tools.
Servers supports two transport layers, the STDIO mode and HTTP Streaming.
- STDIO uses standard IO and is used for local processes. I believe that you just call the server like you call a CLI tool and then you capture it's output, please correct me if I'm wrong.
- HTTP Streaming servers uses Server-Sent Events with JSON-RPC, which is just JSON like:
JavaScript:
--> [
{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
{"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
{"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
{"foo": "boo"},
{"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
{"jsonrpc": "2.0", "method": "get_data", "id": "9"}
]
<-- [
{"jsonrpc": "2.0", "result": 7, "id": "1"},
{"jsonrpc": "2.0", "result": 19, "id": "2"},
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"},
{"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
]
You can read more about MCP's specification here: Specification - Model Context Protocol.
Use cases
I can think about various use-cases on how I would use custom MCP servers, as a homelab user, hobbies, not for business. Some use cases imply online LLMs (OpenAI, Mistral, Claude) when I don't care about privacy and some use cases with offline LLMs in order to preserve my privacy.
- Email prioritization, Most email provides offer you access to read emails using IMAP[1] and POP protocols. You could write an MCP server that exposes some tools for reading emails and let the LLM analyze them.
- Endpoint security. Write a local MCP server that provides information about files on the system, permissions and use an LLM to analyze them
- Weather information: Write a MCP server that exposes tools for reading data from various sources i.e sensors, weather APIs and a tool for alerting when values are out of range or an unwanted event may occur, such as thunderstorms.
- News summarization. You can write an MCP server that scrapes your favorite news sites and provides structured output to an LLM for summarizing them.
Claude also uses MCP servers in the form of Connectors.
[1] - imaplib — IMAP4 protocol client
A Python MCP Server
To write MCP server I recommend that you use the official SDKs. At the current time there are SDKs for Python, Java, JavaScript, TypeScript, Rust, Ruby, Go, Kotlin, Swift and C#.To follow along you will need UV installed
Bash:
curl -LsSf https://astral.sh/uv/install.sh | sh
uv init nuculabs-mcp
cd nuculabs-mcp
uv add mcp[cli]
The asyncio Python-based MCP HTTP Streaming server can look like this:
Python:
import asyncio
import logging
import subprocess
from mcp.server.fastmcp import FastMCP
from mcp.types import ToolAnnotations
from pydantic import BaseModel, Field
class LsOutput(BaseModel):
"""
LsOutput is a model class that represents the output of the LS command, this simple example for educational
purposes only shows a simple text field. You can parse the ls output it to return more meaningful fields
like the file type, name, date, permissions and so on.
"""
output: str = Field(..., title="The output of the `ls` command.")
class SystemAdminTools:
def __init__(self):
self._logger = logging.getLogger("dev.nuculabs.mcp.SystemAdminTools")
self._logger.info("hello world")
async def get_ls_output(self, directory: str) -> LsOutput:
"""
Returns the output of the `ls` command for the specified directory.
:param directory: - The directory path
:return: The LSOutput.
"""
self._logger.info(f"get_ls_output tool called with: {directory}")
# This is a dummy example for educational purposes, but you can write a tool that parses the output and returns a meaningful model
# instead of a simple text file. You could also extend it to remote machines.
# Security Warning: when taking user input such as the 'directory' variable here, always sanitize and validate it,
# using this code as it is malicious output can be used to highjack the target system.
result = subprocess.run(['ls', "-l", directory], capture_output=True, text=True)
output = result.stdout
return LsOutput(output=output)
async def main():
logging.basicConfig(level="INFO")
# Create an MCP server
mcp = FastMCP(
"NucuLabs System Files MCP",
instructions="You're an expert T-shaped system administrator assistant.",
host="0.0.0.0", # open to the world :-)
)
tools = SystemAdminTools()
mcp.add_tool(
tools.get_ls_output,
"get_ls_output",
title="Tool that returns the output from the list files command.",
description="This tool executes ls on the given directory path and returns all the information from the command.",
annotations=ToolAnnotations(
readOnlyHint=True,
destructiveHint=False,
idempotentHint=True,
openWorldHint=False,
),
structured_output=True,
)
await mcp.run_streamable_http_async()
if __name__ == "__main__":
asyncio.run(main())
Testing the Server
To validate and test the MCP server quickly and without using an LLM you can use an MCP server testing tool such as MCP Inspector or Postman. To use MCP Inspector you only need to have NodeJS >= 22 installed on your system and then run.
JavaScript:
npx @modelcontextprotocol/inspector
The command will open up a browser with the MCP Inspector server. I haven't managed to test the server using the aforementioned tool.
I had a positive experience with Postman's MCP client:
Testing with an LLM
To test the server with an LLM you can use the agno-framework or your client SDK if you already have one. Agno is pretty easy to use.
Python:
import asyncio
from agno.agent import Agent
from agno.models.anthropic import Claude
from agno.tools.mcp import MCPTools
async def main():
async with MCPTools(url="http://localhost:8000/mcp", transport="streamable-http") as mcp_tools:
agent = Agent(
# set api key in ANTHROPIC_API_KEY environment variable or pass it as a parameter
model=Claude(),
tools=[
mcp_tools
],
instructions="Use tools to analyze systems.",
markdown=False,
)
await agent.aprint_response(
"Describe my personality based on the contents of the following directory: /home/dnutiu",
stream=True,
show_full_reasoning=True,
stream_intermediate_steps=True,
)
if __name__ == '__main__':
asyncio.run(main())
After running the snippet I get the following output from Claude:
Code:
┏━ Message ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ Describe my personality based on the contents of the following directory: ┃
┃ /home/dnutiu ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Tool Calls ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ • get_ls_output(directory=/home/dnutiu) ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Response (9.2s) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ Let me check the contents of that directory for you.Based on the contents of ┃
┃ your home directory, I can tell that you're a software developer with ┃
┃ diverse interests in programming and technology. Here's what I can deduce ┃
┃ about your personality: ┃
┃ ┃
┃ 1. You're versatile and multi-skilled: You have multiple IDE project ┃
┃ directories (CLion, PyCharm, PhpStorm, IntelliJ IDEA, Rustrover) which shows ┃
┃ you work with various programming languages including Python, PHP, Java, ┃
┃ C/C++, and Rust. ┃
┃ ┃
┃ 2. You're organized: Your directory structure is well-maintained with clear ┃
┃ separations for different types of content (Documents, Downloads, Pictures, ┃
┃ etc.). ┃
┃ ┃
┃ 3. You're technically inclined: The presence of DevOps directory and Postman ┃
┃ suggests you're involved in backend development and API testing. ┃
┃ ┃
┃ 4. You're likely a professional developer: The presence of multiple ┃
┃ development tools and project directories suggests this is more than just a ┃
┃ hobby. ┃
┃ ┃
┃ 5. You use Linux (KDE specifically): This shows you're comfortable with ┃
┃ technical tools and prefer having control over your computing environment. ┃
┃ ┃
┃ 6. You might have some personal projects: The "nuculabs" directory might be ┃
┃ a personal project or endeavor. ┃
┃ ┃
┃ 7. You keep up with modern development tools: You have the JetBrains toolbox ┃
┃ installed, showing you use current development tools. ┃
┃ ┃
┃ 8. You're interested in multiple areas of software development: From web ┃
┃ development (PHP) to systems programming (Rust, C++) to scripting (Python), ┃
┃ showing a broad range of interests in programming. ┃
┃ ┃
┃ Your directory structure reveals someone who is methodical, technically ┃
┃ sophisticated, and has a strong interest in various aspects of software ┃
┃ development and technology. ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
If you have subscription and a supported os you can also test it with Claude Desktop. On Linux Ollama has some models which support MCP tools.
I also wanted to test the prompt "Analyze critical directories of my Linux system and tell me if I'm exposed to security threats based on incorrect file permissions." using Anthropic flagship model claude-opus-4-20250514 but it gave me a rate limit error and it billed me half my credits 3.5$ yeap, this tech is currently very expensive. The fact that it wasted my credits returning an error is kinda dumb in my opinion.
Code:
┏━ Message ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ Analyze critical directories of my Linux system and tell me if I'm exposed ┃
┃ to security threats based on incorrect file permissions. ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Tool Calls ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ • get_ls_output(directory=/etc) ┃
┃ • get_ls_output(directory=/home) ┃
┃ • get_ls_output(directory=/tmp) ┃
┃ • get_ls_output(directory=/root) ┃
┃ • get_ls_output(directory=/var/log) ┃
┃ • get_ls_output(directory=/etc/passwd) ┃
┃ • get_ls_output(directory=/etc/shadow) ┃
┃ • get_ls_output(directory=/etc/sudoers.d) ┃
┃ • get_ls_output(directory=/etc/ssh) ┃
┃ • get_ls_output(directory=/var/www) ┃
┃ • get_ls_output(directory=/usr/bin) ┃
┃ • get_ls_output(directory=/etc/passwd) ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
So I tried again with the following prompt:
"Analyze a maximum of 3 critical directories of my Linux system and tell me if I'm exposed to security threats based on incorrect file permissions." and downgrading the model to claude-sonnet-4-20250514.
Code:
┏━ Message ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ Analyze a maximum of 3 critical directories of my Linux system and tell me ┃
┃ if I'm exposed to security threats based on incorrect file permissions. ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Tool Calls ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ • get_ls_output(directory=/etc) ┃
┃ • get_ls_output(directory=/usr/bin) ┃
┃ • get_ls_output(directory=/home) ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Response (18.2s) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ I'll analyze 3 critical directories on your Linux system to check for ┃
┃ potential security threats related to file permissions. Let me examine ┃
┃ `/etc`, `/usr/bin`, and `/home` directories:Based on my analysis of the ┃
┃ three critical directories, here's my security assessment: ┃
┃ ┃
┃ ## Security Analysis Results ┃
┃ ┃
┃ ### ✅ `/etc` Directory - **SECURE** ┃
┃ The `/etc` directory has proper permissions: ┃
┃ - **Ownership**: All files owned by `root:root` (correct) ┃
┃ - **Critical files have appropriate restrictions**: ┃
┃ - `/etc/shadow`: `600` permissions - only root can read/write ┃
┃ - `/etc/passwd`: `644` permissions - world readable but only root can ┃
┃ write ┃
┃ - Cron files have proper restrictive permissions (`600`) ┃
┃ - SSH configs and keys have appropriate permissions ┃
┃ ┃
┃ ### ✅ `/usr/bin` Directory - **SECURE** ┃
┃ The system binaries are properly configured: ┃
┃ - **All binaries owned by root** (correct) ┃
┃ - **Proper SUID permissions** on critical system utilities: ┃
┃ - `su`, `sudo`, `passwd`, `chfn`, `chsh` have SUID bit set appropriately ┃
┃ - No suspicious SUID binaries found ┃
┃ - **Standard executable permissions** (`755`) for most binaries ┃
┃ ┃
┃ ### ✅ `/home` Directory - **SECURE** ┃
┃ Home directory structure is properly secured: ┃
┃ - **User home directory**: `drwx------` (700 permissions) - only the user ┃
┃ `dnutiu` can access ┃
┃ - **Proper ownership**: `dnutiu:dnutiu` ┃
┃ - No world-readable home directories that could expose sensitive data ┃
┃ ┃
┃ ## Summary ┃
┃ **Your system appears to be SECURE** regarding file permissions in these ┃
┃ critical directories. The key security controls are in place: ┃
┃ ┃
┃ 1. **No world-writable directories or files** in critical locations ┃
┃ 2. **Proper SUID bit management** - only necessary system utilities have ┃
┃ elevated privileges ┃
┃ 3. **Restrictive home directory permissions** preventing unauthorized access ┃
┃ 4. **Critical system files properly protected** (shadow, passwd, etc.) ┃
┃ ┃
┃ ## Recommendations for Ongoing Security: ┃
┃ - Regularly audit SUID/SGID files: `find / -perm /4000 -o -perm /2000 ┃
┃ 2>/dev/null` ┃
┃ - Monitor for any world-writable files: `find / -perm -002 2>/dev/null` ┃
┃ - Keep the system updated to ensure security patches are applied ┃
┃ ┃
┃ Your Linux system shows good security hygiene with no obvious ┃
┃ permission-based vulnerabilities in these critical directories. ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
And finally, this is the reponse of Mistral's mistral-large model, unlike Claude this API is free for experimentation. You can create an API key for Mistral vising La Plateforme | Mistral AI
Code:
┏━ Message ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ Analyze a maximum of 3 critical directories of my Linux system and tell me ┃
┃ if I'm exposed to security threats based on incorrect file permissions. ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Tool Calls ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ • get_ls_output(directory=/etc) ┃
┃ • get_ls_output(directory=/bin) ┃
┃ • get_ls_output(directory=/var/log) ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Response (20.7s) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ The analysis of the critical directories on your Linux system has been ┃
┃ completed. Here are the findings and recommendations based on the file ┃
┃ permissions: ┃
┃ ┃
┃ 1. **/etc Directory:** ┃
┃ - **Findings:** ┃
┃ - Most of the files and directories belong to the `root` user and ┃
┃ `root` group, which is expected. ┃
┃ - There are specific files and directories with restricted permissions, ┃
┃ such as `shadow`, `gshadow`, and `sudoers.d`, which are correctly set to be ┃
┃ readable and writable only by the `root` user. ┃
┃ - Some files like `chrony.conf`, `mysqlaccess.conf`, and `shadow` have ┃
┃ restricted permissions (`-rw-r-----`), allowing only the `root` user and ┃
┃ specific groups to read/write. ┃
┃ - **Recommendations:** ┃
┃ - Ensure that sensitive files like `shadow` and `gshadow` maintain ┃
┃ their restricted permissions (`-rw-r-----`). ┃
┃ - Regularly review and update permissions for configuration files to ┃
┃ ensure only necessary users and groups have access. ┃
┃ ┃
┃ 2. **/bin Directory:** ┃
┃ - **Findings:** ┃
┃ - The `/bin` directory is a symbolic link to `usr/bin`, which is a ┃
┃ common setup in modern Linux distributions. ┃
┃ - **Recommendations:** ┃
┃ - Ensure that the symbolic link points to the correct directory and ┃
┃ that the target directory (`usr/bin`) has appropriate permissions. ┃
┃ ┃
┃ 3. **/var/log Directory:** ┃
┃ - **Findings:** ┃
┃ - Log files are owned by `root` and have varied permissions, with some ┃
┃ being world-readable and others restricted to specific users or groups. ┃
┃ - Sensitive log files like `boot.log` and `plymouth-debug.log` are ┃
┃ correctly set to be readable only by the `root` user. ┃
┃ - **Recommendations:** ┃
┃ - Regularly review and rotate log files to prevent unauthorized access ┃
┃ to sensitive information. ┃
┃ - Ensure that log files containing sensitive data have restricted ┃
┃ permissions (`-rw-------`). ┃
┃ ┃
┃ **General Recommendations:** ┃
┃ - Regularly audit file permissions and ownership to ensure they align with ┃
┃ the principle of least privilege. ┃
┃ - Use tools like `auditd` or `aide` to monitor and alert on changes to ┃
┃ critical files and directories. ┃
┃ - Implement a robust backup and recovery plan to protect against data loss ┃
┃ or corruption. ┃
┃ ┃
┃ By following these recommendations, you can enhance the security of your ┃
┃ Linux system and reduce the risk of unauthorized access or data breaches. ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
That's about it! I hope you've enjoyed this article!
Last edited: