Zum Hauptinhalt springen
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
Windows Tipps & SecurityGrafikkarte vor Überhitzung schützen: So geht’s(25.09.2026 um 08:00 Uhr)
••••••••••
Windows Tipps & SecurityGrafikkarte vor Überhitzung schützen: So geht’s(25.09.2026 um 08:00 Uhr)
••••••••••
Intelligence View
⚡ tsecurity.de Intelligence

I Built an MCP Server in 40 Minutes. Here's Exactly What Happened.

I kept seeing MCP mentioned everywhere — in Claude Code docs, in Cursor settings, in every "AI engineering stack" post on LinkedIn. I kept nodding like I understood it. I didn't. So last week I sat down and built one from scratch. A real …

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

I kept seeing MCP mentioned everywhere — in Claude Code docs, in Cursor settings, in every "AI engineering stack" post on LinkedIn. I kept nodding like I understood it.



I didn't.



So last week I sat down and built one from scratch. A real MCP server, connected to a real Postgres database, that Claude can actually query. No toy examples. No hand-waving over the hard parts.



This is the full account of what happened — including the two hours I lost to a configuration mistake that had nothing to do with code, and the exact moment the whole thing clicked.









First: what MCP actually is, in one paragraph



Before MCP existed, connecting an AI to your database meant writing a custom integration for every AI tool separately. One integration for Claude, another for Cursor, another for GitHub Copilot. Three tools, three integrations, all slightly different.



MCP is a standardised protocol — think of it as USB for AI tools. You write one MCP server that connects to your Postgres database, and every MCP-compatible client (Claude Desktop, Claude Code, Cursor, Windsurf, VS Code Copilot) can use it immediately. The official server registry crossed 2,000 community implementations by Q1 2026, and SDK downloads jumped roughly 970x in 18 months. It became the standard faster than almost any protocol I can remember.



That's the pitch. Here's the build.









What we're building



A Python MCP server with two tools:




  1. list_tables — shows Claude what tables exist in your database


  2. query_database — runs a read-only SQL query and returns results




By the end, you'll open Claude Desktop, type "what tables are in my database?" and get a real answer from your actual Postgres instance.



Prerequisites:




  • Python 3.10 or higher


  • A Postgres database (local or cloud — Neon has a free tier that works perfectly)


  • Claude Desktop installed (download here)


  • Basic familiarity with Python async/await










Step 1: Set up the project



uv is now the standard for MCP Python projects. It's faster than pip and handles virtual environments automatically. If you don't have it:




# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh

# Create the project
uv init zyvop-mcp-server
cd zyvop-mcp-server

# Install dependencies
uv add "mcp[cli]" fastmcp asyncpg python-dotenv






If you prefer pip:




python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install "mcp[cli]" fastmcp asyncpg python-dotenv






Create a .env file for your database credentials:




DATABASE_URL=postgresql://user:password@localhost:5432/yourdb






Never hardcode credentials. Never commit .env. Add it to .gitignore right now before you forget.









Step 2: Write the server



Create server.py. This is the full working server — nothing left out:




import asyncio
import asyncpg
from fastmcp import FastMCP
from dotenv import load_dotenv
import os

load_dotenv()

# FastMCP does the heavy lifting — one line to create the server
mcp = FastMCP(
name="postgres-assistant",
instructions="You are a database assistant. Use list_tables first to understand
the schema, then query_database to answer questions about the data.
"
)

DATABASE_URL = os.getenv("DATABASE_URL")

async def get_connection():
"""Create a database connection."""
return await asyncpg.connect(DATABASE_URL)

@mcp.tool()
async def list_tables() -> str:
"""
List all tables in the database with their column names and types.
Always call this before querying to understand the schema.
"""
conn = await get_connection()
try:
rows = await conn.fetch("""
SELECT
table_name,
column_name,
data_type
FROM information_schema.columns
WHERE table_schema =
'public'
ORDER BY table_name, ordinal_position
""")

if not rows:
return "No tables found in the public schema."

# Group by table name for readable output
tables = {}
for row in rows:
table = row['table_name']
if table not in tables:
tables[table] = []
tables[table].append(f" {row['column_name']} ({row['data_type']})")

result = []
for table_name, columns in tables.items():
result.append(f"Table: {table_name}")
result.extend(columns)
result.append("")

return "\n".join(result)
finally:
await conn.close()

@mcp.tool()
async def query_database(sql: str) -> str:
"""
Run a read-only SQL SELECT query against the database.
Only SELECT statements are allowed — no INSERT, UPDATE, DELETE, or DROP.
Returns results as a formatted table.

Args:
sql: A valid SQL SELECT statement
"""
# Safety check — only allow SELECT
sql_clean = sql.strip().upper()
if not sql_clean.startswith("SELECT"):
return "Error: Only SELECT queries are allowed. Received: " + sql[:50]

# Block dangerous keywords even inside SELECT
dangerous = ["DROP", "DELETE", "INSERT", "UPDATE", "TRUNCATE", "ALTER", "CREATE"]
for keyword in dangerous:
if keyword in sql_clean:
return f"Error: Query contains forbidden keyword: {keyword}"

conn = await get_connection()
try:
rows = await conn.fetch(sql)

if not rows:
return "Query returned no results."

# Format as readable table
headers = list(rows[0].keys())
header_row = " | ".join(headers)
separator = "-" * len(header_row)

result_rows = [header_row, separator]
for row in rows[:50]: # Limit to 50 rows to avoid token bloat
result_rows.append(" | ".join(str(val) for val in row.values()))

if len(rows) > 50:
result_rows.append(f"\n... and {len(rows) - 50} more rows (query returned {len(rows)} total)")

return "\n".join(result_rows)

except asyncpg.PostgresError as e:
return f"Database error: {str(e)}"
finally:
await conn.close()

if __name__ == "__main__":
mcp.run()






Three things worth noting in this code:



The docstrings are not optional. Without a docstring on your tool function, the AI model has no idea what the tool does and will either refuse to use it or use it wrong. The docstring is literally what Claude reads to decide whether to call your tool.



The 50-row limit on results is deliberate. Token bloat is the number one production pain point — developers report tool schemas and responses consuming 15,000+ tokens before the agent even starts reasoning. Return the minimum useful data. Claude can always ask for more.



The safety checks are not paranoia. A SQL injection via your MCP server would be Claude running DROP TABLE users on your behalf. The keyword check is basic but catches the obvious cases.









Step 3: Test locally with the MCP Inspector



Before connecting to Claude, test with the official inspector:




fastmcp dev server.py






This opens a browser UI at http://localhost:6274 where you can call your tools directly, see the raw JSON-RPC messages, and verify everything works before involving Claude.



Call list_tables first. If it returns your schema, you're in good shape. If it returns an error, it's almost certainly the DATABASE_URL — double-check the connection string format.









Step 4: Connect to Claude Desktop



Find your Claude Desktop config file:




  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json


  • Windows: %APPDATA%\Claude\claude_desktop_config.json




Add your server:




{
"mcpServers": {
"postgres-assistant": {
"command": "uv",
"args": [
"run",
"--project",
"/absolute/path/to/zyvop-mcp-server",
"python",
"server.py"
],
"env": {
"DATABASE_URL": "postgresql://user:password@localhost:5432/yourdb"
}
}
}
}






Use the absolute path. This is where I lost two hours. A relative path like ./zyvop-mcp-server fails silently on some systems — Claude Desktop starts, shows no error, but the server never loads. Absolute path, always.



Restart Claude Desktop completely. Look for a hammer icon in the bottom-right of the chat input — that's your MCP tools indicator. If it's there, your server connected.









The moment it clicked



I typed: "What tables do I have in my database?"



Claude called list_tables. Returned my schema. Then I asked: "How many users signed up last week?"



It wrote a SQL query, called query_database, got the result, and answered me in plain English.



No code. No terminal. Just a question and an answer, backed by my actual data.



That was the moment I understood why MCP had taken off. It's not impressive as a demo. It's impressive as a workflow — the feeling of having a tool that can navigate your actual systems rather than giving you generic advice about systems it imagines.









What breaks in production (and how to fix it)



Connection pool exhaustion. The server above creates a new connection per query. For low-traffic personal tools that's fine. For anything handling multiple simultaneous requests, create a connection pool at startup:




pool = None

@mcp.lifespan
async def lifespan():
global pool
pool = await asyncpg.create_pool(DATABASE_URL, min_size=2, max_size=10)
yield
await pool.close()

# Then in your tools:
async with pool.acquire() as conn:
rows = await conn.fetch(sql)






The "dumps 43 tools into context" problem. One developer reported that GitHub's own MCP server "dumps 43 tools into the context window" before doing anything, destroying agent performance. Keep your tool count small — 5 to 8 focused tools outperforms 40 broad ones. Every tool adds tokens to every conversation whether Claude uses it or not.



Responses that aren't strings. A tool that returned a raw Python dict rendered fine in the MCP Inspector but came back truncated inside Claude Desktop. Wrapping the return value as a typed text string fixed it instantly. Always return strings from your tools, not dicts or lists.









What to build next



Once your Postgres server works, the same pattern extends to anything:




  • A GitHub MCP server that Claude can use to read your issues and PRs


  • A filesystem server that lets Claude navigate your project directory


  • An API wrapper that connects Claude to any internal service your company uses




The official MCP server repository has reference implementations for Slack, Google Drive, GitHub, and more — good reading for patterns to follow.









The honest take



Building this took 40 minutes of actual coding and 2 hours of debugging a config file path.



The coding part is genuinely simple — FastMCP eliminates the boilerplate and provides a decorator-based framework that turns a Python function into a fully typed MCP tool with one line. If you know Python async, you know enough to build this.



The debugging part is a rite of passage. The MCP Inspector is your friend. The Claude Desktop logs (at ~/Library/Logs/Claude/ on macOS) are your other friend. Between those two, every error I hit was eventually traceable.



The thing on the other side is worth it. Give it an afternoon.






Built something with MCP? Drop what you connected it to in the comments — the most interesting integrations I've seen have come from developers solving very specific, unglamorous problems.






External links:








Originally published on ZyVOP



💡 For more articles like this, subscribe to the ZyVOP newsletter!

1. Sofort-Triage & Abwehrmaßnahmen

SOC Incident Playbook: Remote Code Execution (RCE) Defense
1 Warnungen
title: Detect Exploitation - I Built an MCP Server in 40 Minutes. Here's Exactly What Happened.
id: bbb865ca-3c43-4c90-8abc-e265458e7752
status: experimental
description: Automatisch generierte SIEM-Erkennungsregel basierend auf CTI Intelligence
references:
  - https://tsecurity.de/
author: iShareStuff CTI Automated Detection Engine
date: 2026-09-26
logsource:
  category: network_connection
  product: any
detection:
  selection:
      CommandLine|contains:
        - 'exploit'
  condition: selection
falsepositives:
  - Legitime administrative Zugriffe oder Penetrationstests
level: high
tags:
  - attack.initial_access
  - attack.t1190
Syntax validiert (0 Fehler)
rule CTI_Threat_Indicator {
    meta:
        author = "iShareStuff CTI Automated Detection Engine"
        date = "2026-09-26"
        description = "YARA Signature for "
    strings:
        $str = "I Built an MCP Server in 40 Mi" ascii wide
    condition:
        any of them
}
Syntax validiert (0 Fehler)
index=security sourcetype IN ("cisco:asa", "pan:traffic", "zeek_conn", "suricata", "WinEventLog:Security")
("I Built an MCP Server in 40 Minutes Here")
| stats count earliest(_time) as first_seen latest(_time) as last_seen by src_ip, dest_ip, dest_host, signature
| eval first_seen=strftime(first_seen, "%Y-%m-%d %H:%M:%S"), last_seen=strftime(last_seen, "%Y-%m-%d %H:%M:%S")
| sort - count
Syntax validiert (0 Fehler)
message: "*I Built an MCP Server in 40 Minutes Here*"
Syntax validiert (0 Fehler)
CommonSecurityLog
| where Message has "I Built an MCP Server in 40 Minutes Here"
| summarize EventCount = count(), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated) by SourceIP, DestinationIP, DestinationPort, Activity
| extend DetectionRule = "iShareStuff-CTI-Compiled"
| sort by EventCount desc

2. Cyber Threat Intelligence & Forensik

CTI Threat Relationship Graph5 Knoten / 4 Relationen
CVE / Incident Software MITRE ATT&CK CWE Weakness IoC
🎯
MITRE ATT&CK Matrix Navigator 14 Taktiken
Identifiziert: T1190Exploit Public-Facing Application
Reconnaissance
-
Resource Development
-
Initial Access
Execution
Persistence
-
Privilege Escalation
Defense Evasion
Credential Access
-
Discovery
-
Lateral Movement
-
Collection
-
Command and Control
Exfiltration
-
Impact
tsecurity.de Cognitive Threat RAG
Fokus-Vektor:

Kognitive Analyse für identifizierte Bedrohung: Erhöhte Bedrohungslage im Bereich I Built an MCP Server in 40 Minutes. Her.... Basierend auf 368k Vektor-Korrelationen werden sofortige Isolationsmaßnahmen für betroffene Endpunkte empfohlen.

🛡️ Angriffsfläche & Exposure

Netzwerk/Remote-Zugriff ohne Vorauthentifizierung möglich.

⚡ Empfohlene Sofortmaßnahmen
  • 1. Perimeter-Inspektion: Relevante Portfreigaben und exponierte Endpunkte unverzüglich scannen.
  • 2. Patch-Applikation: Hersteller-Hotfix einspielen oder betroffene Daemons in isolierte DMZ-Segmente überführen.
  • 3. Telemetrie & EDR-Alerts: Prozessaufrufe und Child-Processes auf anomale Shell-Spawns überwachen.
🔗 Semantisch verwandte Zero-Days MariaDB 11.7 VEC
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten I Built an MCP Server in 40 Minutes. Here's Exactly What Happened.

Thematisch verwandte Begriffe: Built, Server, Minutes, Heres · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

💬 Kommentare werden geladen…
Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-100620 | Capgo CLI (npm package @capgo/cli) through 7.98.2 is affected by an ove…
Advisory →
tsecurity.de Icon
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag