惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

美团技术团队
J
Java Code Geeks
有赞技术团队
有赞技术团队
GbyAI
GbyAI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
Microsoft Security Blog
Microsoft Security Blog
IT之家
IT之家
G
Google Developers Blog
月光博客
月光博客
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
S
SegmentFault 最新的问题
博客园 - 三生石上(FineUI控件)
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - Franky
腾讯CDC
V
Visual Studio Blog
博客园 - 【当耐特】
D
Docker
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Engineering at Meta
Engineering at Meta
L
LangChain Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
5 MCP Server Mistakes That Waste Your AI Agent's Time (An...
Nebula · 2026-05-02 · via DEV Community

I've reviewed dozens of custom MCP servers built by developers connecting AI assistants to their internal tools. The build tutorials are everywhere — the mistake patterns aren't.

Here are the five most common mistakes that make MCP servers unreliable, slow, or silently broken.

TL;DR

# Mistake Impact Fix
1 Printing to stdout Server disconnects immediately Route all diagnostics to stderr
2 Vague tool descriptions AI calls wrong tools or hallucinates params Write descriptions the AI reads at call time
3 Synchronous blocking I/O One slow tool freezes all others Use async def and connection pooling
4 No input validation Garbage inputs crash the server Use Pydantic models for every tool schema
5 No tool-level error handling AI gets raw stack traces Wrap tools, return structured errors

1. The stdout Trap — Printing Diagnostics That Kill Your Server

This is the single most common reason MCP servers "just disconnect" with no useful error message.

When you run an MCP server over stdio transport (the default for Claude Desktop, Cursor, and local agents), stdout is the protocol channel. Every byte you write to stdout must be valid JSON-RPC. A stray print() statement corrupts the entire stream.

The mistake:

@mcp.tool()
def query_data(filters: dict) -> list:
    print(f"Querying with filters: {filters}")  # BOOM
    results = db.query(filters)
    return results

Enter fullscreen mode Exit fullscreen mode

That print() works fine when you test locally. The moment Claude Desktop connects, it receives your debug line instead of a JSON-RPC message and drops the connection with a generic "MCP server disconnected" error.

The fix:

import logging
import sys

logging.basicConfig(
    level=logging.INFO,
    stream=sys.stderr,  # ALWAYS stderr for MCP servers
    format="%(asctime)s %(levelname)s %(message)s"
)
logger = logging.getLogger("mcp-server")

@mcp.tool()
def query_data(filters: dict) -> list:
    logger.info(f"Querying with filters: {filters}")  # Safe
    results = db.query(filters)
    return results

Enter fullscreen mode Exit fullscreen mode

Rule of thumb: if it would appear in a terminal, it goes to stderr. If it's data for the AI client, it goes in the return value.

2. Vague Tool Descriptions — The AI Doesn't Know When to Call Your Tool

Your tool's docstring is the only context the AI has when deciding whether to call your function. It's not documentation for humans — it's a prompt for the model's tool router.

The mistake:

@mcp.tool()
def get_data(query: str) -> dict:
    """Get data."""
    return database.search(query)

Enter fullscreen mode Exit fullscreen mode

"Get data" tells the AI nothing. It now has to guess: what kind of data? When should I call this vs. another tool? What does query mean — a SQL string? A search term? An ID?

The fix:

@mcp.tool()
def get_data(
    search_term: Annotated[
        str,
        Field(description="Free-text product name or SKU, e.g. 'headphones' or 'SKU-001'")
    ]
) -> list[Product]:
    """Search the product catalog by name or SKU. Call when the user asks about inventory, stock levels, or specific products."""
    return database.search(search_term)

Enter fullscreen mode Exit fullscreen mode

The docstring now specifies when to call, and the Field(description=...) specifies what to pass. Together they eliminate the AI's need to guess.

Test your descriptions by asking: if the AI read ONLY this docstring (no function name, no code), would it know exactly when to call this tool?

3. Synchronous Blocking I/O — One Slow Tool Freezes Everything

MCP servers serving stdio or SSE handle one request at a time in sync mode. If one tool makes a slow HTTP call or database query with a 30-second timeout, every other tool waits.

The mistake:

import psycopg2

@mcp.tool()
def run_report(date_range: str) -> dict:
    conn = psycopg2.connect(DSN)  # Blocks, 3-5s
    cursor = conn.cursor()
    cursor.execute("SELECT ...")  # Blocks, 15-30s for large reports
    return cursor.fetchall()

Enter fullscreen mode Exit fullscreen mode

While run_report executes, list_products, ping, and every other tool is blocked. The AI client times out.

The fix: Use async I/O with connection pooling:

import asyncpg
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("async-server")

_pool = None

async def get_pool():
    global _pool
    if _pool is None:
        _pool = await asyncpg.create_pool(DSN, min_size=2, max_size=10)
    return _pool

@mcp.tool()
async def run_report(date_range: str) -> list[dict]:
    pool = await get_pool()
    async with pool.acquire() as conn:
        rows = await conn.fetch("SELECT ... WHERE ...", date_range)
    return [dict(r) for r in rows]

Enter fullscreen mode Exit fullscreen mode

Now the server can handle multiple tool calls concurrently. If the AI calls run_report and list_products in parallel, both execute simultaneously instead of queueing.

4. No Input Validation — Garbage Inputs Crash Your Server

Without explicit schemas, the AI may pass None, empty strings, or completely wrong types. An unhandled TypeError in your tool handler propagates as a cryptic server error to the client.

The mistake:

@mcp.tool()
def update_stock(sku: str, quantity: int) -> str:
    database.update(sku, quantity)
    return f"Updated {sku}"

Enter fullscreen mode Exit fullscreen mode

If the AI passes quantity=None or sku=42 (int instead of str), you get a crash — or worse, silent corruption.

The fix: Use Pydantic models with constraints:

from pydantic import BaseModel, Field

class StockUpdate(BaseModel):
    sku: str = Field(pattern=r"^SKU-\d{3,}$", description="SKU format: SKU-001")
    quantity: int = Field(ge=0, le=99999, description="Must be non-negative")

@mcp.tool()
def update_stock(update: StockUpdate) -> str:
    database.update(update.sku, update.quantity)
    return f"Updated {update.sku}: new stock = {update.quantity}"

Enter fullscreen mode Exit fullscreen mode

Pydantic validates before your function runs. Invalid inputs get a structured error back to the AI instead of an unhandled exception. The AI can then retry with corrected parameters.

5. Bare Exceptions — Raw Stack Traces Leak to the AI Client

When a database connection fails, catching nothing means the AI client sees a 50-line Python traceback. Not only does this waste context tokens — it also leaks internal file paths, library versions, and connection strings.

The mistake:

@mcp.tool()
def get_product(sku: str) -> dict:
    conn = psycopg2.connect("postgresql://admin:secret@db.internal:5432/prod")
    return conn.execute("SELECT * FROM products WHERE sku = %s", (sku,)).fetchone()

Enter fullscreen mode Exit fullscreen mode

If the database is down, every token in that traceback is a token stolen from the AI's reasoning budget, plus you just leaked your connection string.

The fix: Wrap, sanitize, return structured errors:

@mcp.tool()
def get_product(sku: str) -> dict:
    try:
        with get_db() as conn:
            row = conn.execute(
                "SELECT id, sku, name, stock FROM products WHERE sku = %s",
                (sku,)
            ).fetchone()
            if row is None:
                return {"error": "not_found", "sku": sku}
            return dict(row)
    except psycopg2.OperationalError:
        return {"error": "database_unavailable", "retry_after": 30}
    except Exception as e:
        return {"error": "internal_error", "detail": str(e)[:100]}

Enter fullscreen mode Exit fullscreen mode

The AI now gets a structured response it can reason about: "database_unavailable" tells it to wait and retry. "not_found" tells it to suggest alternatives. A raw traceback tells it nothing.

Quick Checklist Before You Ship

Run through this before connecting any MCP server to production:

  • [ ] All print() statements replaced with logger.info() to stderr
  • [ ] Every tool docstring answers: when to call, what it returns
  • [ ] Every Field() has a description= the AI will see
  • [ ] Async I/O for any tool hitting a database, API, or filesystem
  • [ ] Pydantic models with constraints on every tool that takes parameters
  • [ ] Every tool wrapped in try/except, returning structured {error: ...} dicts
  • [ ] Test with mcp dev before connecting to Claude Desktop or Cursor

A well-built MCP server feels invisible to the AI — it just works. These five patterns are the the difference between "the agent keeps using my tools" and "the agent gave up and asked the user instead."