mcp-server-builder — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited mcp-server-builder (Agent Skill) and scored it 91/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} is the classic direct prompt-injection phrasing. Placed in a skill body that the agent reads as trusted instructions, it tries to make the agent abandon its prior rules and follow whatever comes next — a full system-prompt override.
ignore/disregard/forget … previous instructions sentence.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
An MCP server exposes three primitives to AI clients:
| Primitive | Purpose | When to Use |
|---|---|---|
| Tools | Callable functions the agent invokes (tools/call) | Actions, API calls, computations |
| Resources | Read-only data the agent fetches (resources/read) | Files, database records, configuration |
| Prompts | Reusable prompt templates (prompts/get) | Guided workflows, structured interactions |
pip install fastmcp# server.py
from fastmcp import FastMCP
mcp = FastMCP("My Tools Server", version="1.0.0")
@mcp.tool(description="Add two numbers together")
def add(a: float, b: float) -> float:
"""Add two numbers and return the result."""
return a + b
@mcp.tool(description="Fetch the current weather for a city")
async def get_weather(city: str, unit: str = "celsius") -> dict:
"""Get current weather. unit: 'celsius' or 'fahrenheit'"""
import httpx
async with httpx.AsyncClient() as client:
resp = await client.get(f"https://wttr.in/{city}?format=j1")
resp.raise_for_status()
data = resp.json()
return {
"city": city,
"temperature": data["current_condition"][0]["temp_C" if unit == "celsius" else "temp_F"],
"condition": data["current_condition"][0]["weatherDesc"][0]["value"],
}
if __name__ == "__main__":
mcp.run() # stdio mode (default)# stdio (for Claude Code, Cursor - recommended for local tools)
python server.py
# SSE (for browser clients and remote access)
MCP_TRANSPORT=sse python server.py # http://localhost:8000/sse
# Streamable HTTP (modern, bidirectional)
MCP_TRANSPORT=streamable-http python server.py@mcp.resource("config://app-settings")
def get_app_settings() -> str:
"""Return current application settings."""
import json
return json.dumps({
"version": "1.0.0",
"features": ["auth", "analytics"],
"max_users": 1000,
})
@mcp.resource("file://{path}") # template URI with parameter
def read_file(path: str) -> str:
"""Read a file by path."""
import pathlib
# IMPORTANT: validate path to prevent traversal
base = pathlib.Path("./allowed_dir").resolve()
target = (base / path).resolve()
if base not in target.parents and base != target:
raise ValueError("Path outside allowed directory")
return target.read_text()@mcp.prompt(description="Generate a code review checklist for a PR")
def code_review_prompt(language: str, pr_description: str) -> str:
return f"""Review the following {language} pull request:
{pr_description}
Check for:
1. Correctness - does it do what it claims?
2. Security - any vulnerabilities (injection, auth issues, path traversal)?
3. Performance - any N+1 queries, unnecessary allocations?
4. Error handling - all edge cases covered?
5. Tests - adequate coverage?
Use severity tags: [CRITICAL] [HIGH] [MEDIUM] [LOW]"""npm install @modelcontextprotocol/sdk// server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
const server = new Server(
{ name: 'my-tools-server', version: '1.0.0' },
{ capabilities: { tools: {}, resources: {} } }
)
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'calculate',
description: 'Perform arithmetic calculations',
inputSchema: {
type: 'object',
properties: {
expression: { type: 'string', description: 'Math expression, e.g. "2 + 2"' },
},
required: ['expression'],
},
},
],
}))
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'calculate') {
const { expression } = request.params.arguments as { expression: string }
// Safe eval - use a proper math library in production
const result = Function(`'use strict'; return (${expression})`)()
return { content: [{ type: 'text', text: String(result) }] }
}
throw new Error(`Unknown tool: ${request.params.name}`)
})
// Start
const transport = new StdioServerTransport()
await server.connect(transport)The description is what the agent reads to decide when to call the tool:
# ❌ Too vague
@mcp.tool(description="Database lookup")
def db_lookup(query: str) -> list: ...
# ✅ Tells the agent exactly when and how to use it
@mcp.tool(description="""Search the product catalog by keyword, category, or SKU.
Returns a list of matching products with id, name, price, and stock_count.
Use when the user asks about a product, wants to find items, or asks about availability.
Returns empty list if no products match. Max 20 results.""")
def search_products(query: str, category: str = "", limit: int = 20) -> list[dict]: ...# ❌ String concatenation - hard for agent to parse
return f"User {user.name} has {len(orders)} orders totalling ${total}"
# ✅ Structured dict - agent can reason about individual fields
return {
"user": {"id": user.id, "name": user.name},
"order_count": len(orders),
"total_spent": total,
"currency": "USD",
}@mcp.tool(description="Look up a user by email address")
async def get_user(email: str) -> dict:
user = await db.find_user_by_email(email)
if not user:
# Return structured error - don't raise (let agent handle it)
return {"found": False, "error": f"No user found with email: {email}"}
return {"found": True, "user": {"id": user.id, "name": user.name, "email": user.email}}{
"mcpServers": {
"my-tools": {
"command": "python",
"args": ["server.py"],
"cwd": "/path/to/server",
"env": {
"DATABASE_URL": "postgresql://...",
"API_KEY": "sk-..."
}
}
}
}For SSE transport:
{
"mcpServers": {
"my-tools": {
"transport": "sse",
"url": "https://my-mcp-server.com/sse"
}
}
}# Test with MCP Inspector
npx @modelcontextprotocol/inspector python server.py
# Or for SSE:
npx @modelcontextprotocol/inspector sse http://localhost:8000/sseThe Inspector provides a web UI to list tools, call them, and view responses without needing an AI client.
{"found": false} response so the agent can handle it gracefullyhttpx (not requests), asyncpg (not psycopg2)~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.