Module 7: Advanced MCP and Tool Integration
6. The MCP Ecosystem and External Tools
Overview
In capsules 03 through 05 you built your own MCP servers and clients, and integrated them into LangGraph agents. You wrote every tool by hand — search_papers, save_finding, fetch_github_repo. That's perfect for domain-specific tools only you need. But one of MCP's biggest value propositions is that you don't have to build everything from scratch. There's already a growing ecosystem of ready-to-use MCP servers: filesystem, GitHub, Slack, PostgreSQL, web search, and dozens more. Someone already wrote the server, published it, maintains it — you just install, configure, and connect.
This capsule shifts your perspective from creator to consumer of the ecosystem. You'll learn where to find servers, how to evaluate them, how to install and configure them, and — critically — how to do it safely. Because an MCP server you connect to your agent can read files, run queries against your database, or send messages to your Slack. If you don't understand which permissions you're granting, you're delegating dangerous capabilities to code you didn't write and may not have reviewed.
The MCP ecosystem is in a phase similar to npm in 2012 or pip in 2014: explosive growth, lots of innovation, but also abandoned servers, badly documented ones, and questionable security practices. Knowing how to navigate that — telling a trustworthy server from a risky one, configuring minimal permissions, and composing multiple servers in an agent — is a skill that separates you from the developer who just copies npm install without thinking.
The MCP Server Ecosystem
The current landscape
The MCP ecosystem includes three categories of servers:
┌─────────────────────────────────────────────────────────────┐
│ MCP SERVER ECOSYSTEM │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. OFFICIAL SERVERS (Anthropic / MCP org) │
│ ├── filesystem — Read/write local files │
│ ├── github — Issues, PRs, repos, search │
│ ├── postgres — SQL queries with schema inspection │
│ ├── slack — Messages, channels, search │
│ ├── brave-search — Web search │
│ ├── memory — Persistent knowledge graph │
│ ├── sqlite — Local database │
│ ├── puppeteer — Web scraping / browser automation │
│ ├── fetch — Generic HTTP requests │
│ └── git — Git operations (log, diff, blame) │
│ │
│ 2. COMMUNITY SERVERS │
│ ├── tavily-search — AI-optimized web search │
│ ├── notion — Pages, databases, blocks │
│ ├── linear/jira — Issues, projects, boards │
│ ├── docker — Containers, images, compose │
│ ├── stripe — Payments, customers, invoices │
│ └── (hundreds more...) │
│ │
│ 3. CUSTOM SERVERS (the ones you build) │
│ └── Capsules 03-05 of this module │
│ │
└─────────────────────────────────────────────────────────────┘
What a typical server offers
Each MCP server exposes a combination of tools, resources, and prompts:
| Server | Typical tools | Main use |
|---|---|---|
| filesystem | read_file, write_file, list_directory, search_files | Local file access |
| github | create_issue, search_repos, get_file_contents, create_pull_request | Repo management |
| postgres | query (SELECT/INSERT/UPDATE) | Database access |
| slack | send_message, search_messages, list_channels | Team communication |
| brave-search | web_search, local_search | Real-time web search |
| memory | create_entities, create_relations, search_nodes | A persistent knowledge graph |
You discover each server's tool list with list_tools() — the dynamic discovery you learned in capsule 04. You don't need to memorize which tools each server has: you connect, you ask, and the protocol tells you.
Using Community Servers
Installing MCP servers
Most servers are distributed as npm (Node.js) or pip (Python) packages. Anthropic's official servers use npm:
# Official servers (npm) — npx downloads and runs without installing globally
npx -y @modelcontextprotocol/server-filesystem /allowed/path
npx -y @modelcontextprotocol/server-github
npx -y @modelcontextprotocol/server-postgres postgresql://localhost/mydb
npx -y @modelcontextprotocol/server-brave-search
# Python servers (pip)
pip install mcp-server-sqlite
pip install tavily-mcp
The arguments after the package name are the configuration — for example, the filesystem server needs to know which directories it's allowed to access.
Configuration in Claude Desktop
Claude Desktop is the most popular MCP host. You configure servers in claude_desktop_config.json:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/your-user/projects"
]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx"
}
},
"brave-search": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
"env": {
"BRAVE_API_KEY": "BSAxxxxxxxxxxxx"
}
}
}
}
Each entry defines:
command: The executable (npx,python,node)args: The command's arguments, including the package and its configurationenv: Environment variables — API keys, tokens, credentials
Claude Desktop launches each server as a stdio subprocess, discovers its tools, and presents them to the LLM as available tools. Restart Claude Desktop after modifying the configuration.
Configuration in your own Python agent
You don't depend on Claude Desktop. In your Python agent you can connect to any community server with the same pattern from capsule 04:
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def use_community_server():
server = StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem", "/tmp/workspace"],
)
async with stdio_client(server) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print(f"Tools from the filesystem server: {len(tools.tools)}")
for tool in tools.tools:
print(f" - {tool.name}: {tool.description}")
result = await session.call_tool(
"list_directory", {"path": "/tmp/workspace"}
)
print(f"\nContents: {result.content[0].text}")
asyncio.run(use_community_server())
StdioServerParameters → stdio_client → ClientSession → list_tools() / call_tool(). The server is a community one, but your client doesn't need to know that — MCP abstracts the difference away.
Multiple servers in one agent
A production agent connects to several servers at once. The server__tool namespacing avoids collisions:
SERVERS = {
"files": StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem", "/tmp/workspace"],
),
"search": StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-brave-search"],
env={"BRAVE_API_KEY": "BSAxxxx"},
),
"github": StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-github"],
env={"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxx"},
),
}
async def discover_all_tools():
all_tools = {}
for name, params in SERVERS.items():
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
for tool in (await session.list_tools()).tools:
namespaced = f"{name}__{tool.name}"
all_tools[namespaced] = {
"server": name,
"original_name": tool.name,
"description": tool.description,
}
print(f"Total tools discovered: {len(all_tools)}")
return all_tools
With 3 servers you could have 15-20 tools available. With 10 servers, 50+. In capsule 07 (production) you'll see how to handle that scale.
MCP Registries
Where to find servers
| Registry / Directory | URL | Description |
|---|---|---|
| MCP Servers (official) | github.com/modelcontextprotocol/servers | Servers maintained by Anthropic and the community |
| Smithery | smithery.ai | A registry with one-click install, reviews, categorization |
| mcp.run | mcp.run | Servers as managed cloud services |
| Awesome MCP Servers | github.com/punkpeye/awesome-mcp-servers | A curated community list with categories |
| Glama | glama.ai/mcp/servers | A directory with filters by category and popularity |
| npm / PyPI | npmjs.com / pypi.org | Direct search: mcp-server |
The official repository
The most trustworthy starting point:
github.com/modelcontextprotocol/servers
├── src/
│ ├── brave-search/ ← Official server
│ ├── filesystem/ ← Official server
│ ├── github/ ← Official server
│ ├── memory/ ← Official server
│ ├── postgres/ ← Official server
│ ├── puppeteer/ ← Official server
│ ├── slack/ ← Official server
│ └── sqlite/ ← Official server
└── README.md ← The list + community servers
Each server has a README with: a description, the tools it exposes, the configuration, and an example claude_desktop_config.json. It's the quality standard against which to measure community servers.
Smithery: MCP's npm
Smithery is the ecosystem's most mature registry. It offers search by category, guided installation (it generates the JSON configuration for Claude Desktop), metrics (downloads, last update), and community reviews.
When you evaluate a server on Smithery, look at:
- The last update — No commits in 6 months = probably broken with the latest SDK version
- The number of tools — Servers with 30+ tools are probably mixing domains
- Documentation — No detailed README = high risk
- Open issues — Look for security problems or breaking changes
mcp.run: managed servers
mcp.run runs MCP servers as cloud services. It removes the need to have Node.js/Python installed, but it adds network latency and a dependency on a third party. Useful for quick prototyping; evaluate whether it's right for production based on your latency and data privacy requirements.
Security in MCP
Why security matters here
This is the most important section of the capsule. An MCP server isn't an inert package you import — it's a program you run on your machine with access to real resources. When you connect the filesystem server, you give it access to your files. When you connect the PostgreSQL one, you give it access to your database.
⚠️ FUNDAMENTAL RULE:
An MCP server has exactly the permissions
of the process that runs it.
If you run it as your user → it accesses everything you access.
If you run it as root → it accesses the whole system.
The risk isn't theoretical:
- Data exfiltration: A malicious server exposes a "search_files" tool that secretly sends your files' contents to an external server
- Prompt injection via tools: A server returns results with hidden instructions for the LLM
- Lateral movement: A compromised server uses its filesystem access to read
~/.ssh/id_rsaor~/.aws/credentials - Resource abuse: A badly implemented server runs heavy queries that take down your database
The principle of least privilege
Always give the server the minimum permissions it needs:
✅ CORRECT: "/Users/dev/current-project" → Only the project
❌ RISKY: "/Users/dev" → All your projects
❌ DANGEROUS: "/Users/dev/.ssh" → SSH keys
❌ DISASTER: "/" → The entire filesystem
The filesystem server can only access the directories you pass it as arguments. Never pass / or ~ as an allowed path.
Sandboxing
For servers you don't know or don't fully trust, run them in an isolated environment:
# Docker with a read-only volume
docker run --rm -i \
-v /path/project:/workspace:ro \
mcp-server-image \
--allowed-dir /workspace
The :ro flag mounts the directory as read-only. The server can read the project's files but can't modify them or access anything outside /workspace. A virtualenv isolates Python dependencies but does not isolate the filesystem or the network — for real isolation, use Docker.
Credentials and authentication
Never hardcode credentials in the configuration. Use environment variables and follow these rules:
- Create tokens with minimum permissions (read-only if you only need to read)
- Use short-lived tokens whenever possible
- Rotate tokens periodically
- Never use your personal admin token — create a token specific to the agent
Rate limiting
If your agent has access to tools that run queries or make API calls, implement controls:
import time
from collections import defaultdict
class RateLimiter:
def __init__(self, max_calls: int, window_seconds: int):
self.max_calls = max_calls
self.window = window_seconds
self.calls: dict[str, list[float]] = defaultdict(list)
def allow(self, tool_name: str) -> bool:
now = time.time()
self.calls[tool_name] = [
t for t in self.calls[tool_name] if now - t < self.window
]
if len(self.calls[tool_name]) >= self.max_calls:
return False
self.calls[tool_name].append(now)
return True
limiter = RateLimiter(max_calls=10, window_seconds=60)
async def safe_call_tool(session, tool_name, args):
if not limiter.allow(tool_name):
return f"Rate limit: '{tool_name}' exceeded {limiter.max_calls} calls/{limiter.window}s"
return await session.call_tool(tool_name, args)
Without rate limiting, a faulty reasoning loop can run hundreds of queries in seconds.
A quick security checklist
Before connecting any MCP server to your agent:
□ Is it from a known source? (official repo, verified org)
□ Does it have public source code you can audit?
□ Which permissions does it need? Are they reasonable for its function?
□ Does it use the network? Where does it connect to?
□ Which data can it read? Is it limited to the minimum necessary?
□ Are there reported security issues?
□ Are you running it with the lowest possible permissions?
□ Do you have rate limiting active?
Evaluating an MCP Server
The evaluation framework
Evaluate each server across 5 dimensions:
| Dimension | Key questions | Positive signals | Negative signals |
|---|---|---|---|
| Origin | Who maintains it? | Official repo, a known org | An account with no history, an abandoned fork |
| Quality | Does it have tests? A complete README? | CI/CD, clear docs | No tests, a 3-line README |
| Maintenance | When was it last updated? | Recent commits, closed issues | No commits in 6+ months |
| Security | Which permissions does it need? | Minimal permissions, no strange dependencies | eval() in the code, asks for excessive permissions |
| Compatibility | The current SDK version? | Pinned to a recent version | Outdated dependencies |
Auditing the source code
For servers that handle sensitive data, review before using:
git clone https://github.com/org/mcp-server-example
cd mcp-server-example
# Does it make HTTP requests to external servers?
rg "https?://" --type ts --type py
# Does it use eval, exec, or subprocess?
rg "(eval|exec|subprocess)" --type ts --type py
# Does it read files outside the expected directory?
rg "(\/etc\/|\/root\/|\.ssh|\.aws|\.env)" --type ts --type py
A legitimate filesystem server accesses the local filesystem and nothing else. If you see fetch("https://telemetry.example.com/...") in a filesystem server, that's a red flag.
Example: an official server vs. an unknown one
The official GitHub server (@modelcontextprotocol/server-github):
| Dimension | Evaluation |
|---|---|
| Origin | The MCP org's official repository. Maintained by Anthropic. ✅ |
| Quality | TypeScript with strict types, a complete README. ✅ |
| Maintenance | Frequent commits, issues answered. ✅ |
| Security | Requires a PAT. Uses GitHub's official API. No requests to other domains. ✅ |
| Compatibility | Compatible with MCP SDK v1.x. ✅ |
Verdict: Trustworthy for production use.
A hypothetical awesome-ai-tools-mcp server from an individual developer:
| Dimension | Evaluation |
|---|---|
| Origin | A GitHub account with 3 repos, no verified profile. ⚠️ |
| Quality | A 10-line README, no tests, no types. ❌ |
| Maintenance | Last commit 8 months ago, issues unanswered. ❌ |
| Security | Requires AWS_ACCESS_KEY_ID but it isn't an AWS server. ❌ |
| Compatibility | Depends on mcp@0.1.0 (an outdated version). ❌ |
Verdict: Don't use it. High risk of leaked credentials.
Comparison: Popular Servers
| Server | Package | Main tools | Auth | Trust |
|---|---|---|---|---|
| Filesystem | @modelcontextprotocol/server-filesystem | read_file, write_file, search_files | No (path args) | ⭐⭐⭐⭐⭐ |
| GitHub | @modelcontextprotocol/server-github | create_issue, search_repos, create_pull_request | PAT token | ⭐⭐⭐⭐⭐ |
| PostgreSQL | @modelcontextprotocol/server-postgres | query | Conn string | ⭐⭐⭐⭐⭐ |
| Brave Search | @modelcontextprotocol/server-brave-search | web_search, local_search | API key | ⭐⭐⭐⭐⭐ |
| Slack | @modelcontextprotocol/server-slack | send_message, search_messages | Bot token | ⭐⭐⭐⭐⭐ |
| Memory | @modelcontextprotocol/server-memory | create_entities, search_nodes | No | ⭐⭐⭐⭐⭐ |
| SQLite | @modelcontextprotocol/server-sqlite | read_query, write_query | No (db path) | ⭐⭐⭐⭐⭐ |
| Git | @modelcontextprotocol/server-git | git_log, git_diff, git_status | No (repo path) | ⭐⭐⭐⭐⭐ |
| Puppeteer | @modelcontextprotocol/server-puppeteer | navigate, screenshot, click | No | ⭐⭐⭐⭐⭐ |
| Fetch | @modelcontextprotocol/server-fetch | fetch (GET/POST/PUT) | No | ⭐⭐⭐⭐⭐ |
Which server for which case
I need my agent to...
...read and write files → filesystem
...search the web → brave-search
...manage GitHub repos → github
...query a database → postgres / sqlite
...send messages to Slack → slack
...remember information → memory
...browse websites → puppeteer
...make HTTP requests → fetch
...analyze git history → git
For cases the official servers don't cover, search Smithery or Awesome MCP Servers. If you don't find a suitable one — build your own (capsules 03-04).
Connection to the Project
The module's project (capsule 08) requires an agent that integrates at least 2 MCP servers. What you learned in this capsule prepares you to:
- Choose servers from the ecosystem that are relevant to your use case
- Evaluate them with the 5-dimension framework before trusting them
- Configure them with minimal permissions and secure credentials
- Compose them into a LangGraph agent with namespacing and dynamic discovery
- Protect yourself with rate limiting and sandboxing
You could combine filesystem to persist reports, brave-search for web research, and your custom research server — standard ecosystem tools alongside domain-specific ones.
Troubleshooting
Problem 1: "The npm server won't start — npx not found"
Symptom: FileNotFoundError: [Errno 2] No such file or directory: 'npx'
Cause: Node.js isn't installed, or npx isn't in the Python process's PATH.
Solution: Check with which npx and use the full path in StdioServerParameters:
StdioServerParameters(
command="/usr/local/bin/npx",
args=["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
)
Problem 2: "list_tools() returns empty"
Symptom: The connection succeeds but 0 tools get discovered.
Cause: The server needs configuration you didn't pass it (API key, path, connection string).
Solution: Check the server's README. Most require arguments:
# ❌ No configuration
StdioServerParameters(command="npx", args=["-y", "@modelcontextprotocol/server-postgres"])
# ✅ With a connection string
StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"],
)
Problem 3: "Authentication error with the GitHub server"
Symptom: 401 Unauthorized when invoking tools.
Cause: An expired token, insufficient permissions, or a badly configured environment variable.
Solution: Verify the token exists and has the right scopes (repo, read:org):
import os
github_token = os.environ.get("GITHUB_TOKEN")
if not github_token:
raise ValueError("GITHUB_TOKEN not configured")
Problem 4: "The filesystem server rejects the path"
Symptom: Error: Path /home/user/docs not in allowed directories
Cause: The server only accesses directories passed explicitly as arguments.
Solution: Add each needed directory as a separate argument:
StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem",
"/home/user/project", "/home/user/data"],
)
Problem 5: "Timeouts connecting to npm servers"
Symptom: Connections that take >30 seconds the first time.
Cause: npx -y downloads dependencies on the first run.
Solution: Pre-install globally (npm install -g @modelcontextprotocol/server-filesystem) and add explicit timeouts:
await asyncio.wait_for(session.initialize(), timeout=30.0)
Exercises
Exercise 1: A server inventory for a use case (Easy)
Your team is building a technical support agent that needs to: (1) search the internal documentation, (2) query Jira tickets, (3) reply on Slack, and (4) query the customer database in PostgreSQL. Which MCP servers would you use? For each one, state whether it's official, community, or custom.
See solution
| Need | Server | Type | Note |
|---|---|---|---|
| Internal documentation | server-filesystem | Official | If they're local files. Custom if they're in a CMS |
| Jira tickets | A community server (Smithery) or custom | Community | There's no official one — evaluate with the 5-dimension framework |
| Reply on Slack | server-slack | Official | A mature server with send/search |
| Database | server-postgres | Official | Create a DB user with SELECT-only permissions for the agent |
Security: The PostgreSQL server with customer data requires a database user with SELECT-only permissions on the necessary tables. Never give write access to a support agent.
Exercise 2: A secure Claude Desktop configuration (Easy)
Write the claude_desktop_config.json for a developer who wants: filesystem (only their project directory), GitHub (read-only), and Brave Search. Apply the principle of least privilege.
See solution
{
"mcpServers": {
"project-files": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/developer/projects/my-project"
]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_TOKEN_READ_ONLY_SCOPE"
}
},
"web-search": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
"env": {
"BRAVE_API_KEY": "BSA_xxxxxxxxx"
}
}
}
}
Least privilege applied:
- Filesystem: Only one specific directory, not
~or/ - GitHub: A token with the
public_reposcope (read-only), withoutrepoordelete_repo - Brave Search: An API key on the free tier is enough for development
Exercise 3: Evaluate an MCP server (Medium)
Find a community MCP server on github.com/modelcontextprotocol/servers or smithery.ai. Apply the 5-dimension evaluation framework and document your analysis in a table.
See solution
An example evaluating the Notion server (community):
| Dimension | Evaluation | Score |
|---|---|---|
| Origin | Listed in the official README as a community server | ✅ |
| Quality | A README with documented tools, TypeScript with types | ✅ |
| Maintenance | Commits in the last 3 months, issues answered | ✅ |
| Security | Requires a Notion Integration Token. Only accesses Notion's API | ✅ |
| Compatibility | Compatible with the current MCP SDK | ✅ |
The process for your evaluation:
- Clone the repo and read the full README
- Check
package.jsonfor suspicious dependencies - Look for HTTP requests to unexpected domains:
rg "https?://" --type ts - Check the last commit's date:
git log -1 --format="%ci" - Review the open issues, especially the security ones
Exercise 4: Multi-server with rate limiting (Medium)
Implement safe_multi_server_discover that connects to a list of servers, discovers tools with namespacing, and applies rate limiting of 20 calls/minute per server.
See solution
import asyncio
import time
from collections import defaultdict
from dataclasses import dataclass, field
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
@dataclass
class ServerRateLimiter:
max_calls: int = 20
window_seconds: int = 60
_calls: dict[str, list[float]] = field(default_factory=lambda: defaultdict(list))
def allow(self, server_name: str) -> bool:
now = time.time()
self._calls[server_name] = [
t for t in self._calls[server_name] if now - t < self.window_seconds
]
if len(self._calls[server_name]) >= self.max_calls:
return False
self._calls[server_name].append(now)
return True
@dataclass
class DiscoveredTool:
namespaced_name: str
server: str
original_name: str
description: str
async def safe_multi_server_discover(
servers: dict[str, StdioServerParameters],
timeout: float = 15.0,
) -> tuple[list[DiscoveredTool], ServerRateLimiter]:
all_tools: list[DiscoveredTool] = []
limiter = ServerRateLimiter()
for name, params in servers.items():
try:
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await asyncio.wait_for(session.initialize(), timeout=timeout)
for tool in (await session.list_tools()).tools:
all_tools.append(DiscoveredTool(
namespaced_name=f"{name}__{tool.name}",
server=name,
original_name=tool.name,
description=tool.description or "",
))
print(f"✓ [{name}] {len(all_tools)} tools")
except asyncio.TimeoutError:
print(f"✗ [{name}] Timeout after {timeout}s")
except Exception as e:
print(f"✗ [{name}] Error: {e}")
print(f"\nTotal: {len(all_tools)} tools from {len(servers)} servers")
return all_tools, limiter
async def safe_invoke(session, tool, args, limiter):
if not limiter.allow(tool.server):
return f"Rate limited: {tool.server} exceeded {limiter.max_calls} calls/{limiter.window_seconds}s"
result = await session.call_tool(tool.original_name, args)
return result.content[0].text
safe_multi_server_discover handles timeouts and errors per server without stopping discovery of the others. The rate limiter controls calls per server — a server with 10 tools is still limited to 20 calls/min to the whole server.
Exercise 5: A security audit of a server (Hard)
Write audit_mcp_server.py that connects to an MCP server, lists all its tools, and generates a security report: the name of each tool, whether any parameter accepts paths or URLs (an access risk), and a risk score (low/medium/high).
See solution
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
HIGH_RISK = {"path", "file", "directory", "dir", "folder", "url", "uri"}
MEDIUM_RISK = {"query", "sql", "command", "cmd", "script", "code", "exec"}
WRITE_WORDS = {"write", "create", "delete", "update", "send", "remove"}
def assess_risk(name: str, desc: str, schema: dict) -> tuple[str, list[str]]:
reasons = []
# The level is decided with a flag, not by reading the text of `reasons`:
# that text is for humans and changes with the language; the logic must not depend on it.
has_high_risk = False
props = schema.get("properties", {})
for param, info in props.items():
p_lower = param.lower()
d_lower = (info.get("description", "") or "").lower()
for kw in HIGH_RISK:
if kw in p_lower or kw in d_lower:
reasons.append(f"Param '{param}' accepts a {kw} (resource access)")
has_high_risk = True
break
for kw in MEDIUM_RISK:
if kw in p_lower or kw in d_lower:
reasons.append(f"Param '{param}' accepts a {kw} (potential execution)")
break
for kw in WRITE_WORDS:
if kw in name.lower() or kw in (desc or "").lower():
reasons.append(f"Tool performs a write/modification ({kw})")
break
if has_high_risk:
return "HIGH", reasons
elif reasons:
return "MEDIUM", reasons
return "LOW", reasons
async def audit_server(params: StdioServerParameters, server_name: str):
print(f"{'=' * 50}")
print(f" MCP AUDIT: {server_name}")
print(f"{'=' * 50}\n")
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
results = []
for tool in tools.tools:
level, reasons = assess_risk(
tool.name, tool.description or "", tool.inputSchema
)
results.append((tool.name, level, reasons))
high = sum(1 for _, l, _ in results if l == "HIGH")
med = sum(1 for _, l, _ in results if l == "MEDIUM")
low = sum(1 for _, l, _ in results if l == "LOW")
print(f"Tools: {len(results)} | 🔴 High: {high} | 🟡 Medium: {med} | 🟢 Low: {low}\n")
for name, level, reasons in results:
icon = {"HIGH": "🔴", "MEDIUM": "🟡", "LOW": "🟢"}[level]
print(f"{icon} {name} [{level}]")
for r in reasons:
print(f" ⚠ {r}")
overall = "HIGH" if high else ("MEDIUM" if med else "LOW")
print(f"\n{'=' * 50}")
print(f" OVERALL RISK: {overall}")
if high:
print(f" → Review the source code. Run it in a sandbox.")
print(f"{'=' * 50}")
if __name__ == "__main__":
server = StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
)
asyncio.run(audit_server(server, "filesystem"))
The filesystem server will report HIGH risk — expected, because it accesses files by design. High risk isn't necessarily bad; what matters is that the permissions you give it (the allowed directory) limit the impact.
Summary
In this capsule you went from creator to consumer of the MCP ecosystem:
- The ecosystem is real and growing: Official servers (filesystem, GitHub, PostgreSQL, Slack, Brave Search, Memory) and hundreds of community servers already available
- Registries to discover them: The official repo, Smithery, mcp.run, and Awesome MCP Servers are your starting points
- Installation and configuration:
npx -y @modelcontextprotocol/server-<name>for npm servers. Configuration in Claude Desktop via JSON, or in your Python agent viaStdioServerParameters - Security isn't optional: An MCP server runs code on your machine with access to real resources. Least privilege, read-only tokens, restricted directories, rate limiting
- The evaluation framework: 5 dimensions — origin, quality, maintenance, security, compatibility. Apply it before trusting any community server
- Composing servers: A production agent combines multiple servers with namespacing. Dynamic discovery makes adding servers trivial — the hard part is security and evaluation
Next capsule: MCP in Production — deploying MCP servers as services, authentication between client and server, protocol-level rate limiting, monitoring, tool versioning, and testing MCP integrations.
Additional Resources
- MCP Servers — The Official Repository — The complete list of official and community servers with documentation
- Smithery — MCP Registry — A registry with search, categories, and quality metrics
- MCP Security Best Practices — The protocol's security considerations
- MCP Specification — Security — The security section of the formal specification
- Awesome MCP Servers — A curated community list organized by category
- Claude Desktop MCP Setup — The official guide to configuring servers in Claude Desktop