MCP · Python · Any Stack

The pattern,
not the stack.

Pick your data sources below. You'll get copy-pasteable Claude prompts to scaffold your own observability MCP server, adapted to your tools, with placeholders for your URLs and credentials. The pattern works regardless of whether you use Elasticsearch or Loki, MongoDB or PostgreSQL, Prometheus or Datadog.

Three ideas that
make it work

MCP is just a protocol. The real work is in how you structure the server and write the tool descriptions.

01
One tool per data source
Each tool is a thin, typed wrapper around one system. app_logs queries your log store. db_slow_queries queries your database diagnostics. They don't know about each other. Claude reasons across them.
02
Descriptions are the prompt
Claude reads your tool descriptions to decide which tool to call, with what parameters. A vague description produces a vague call. Be specific: name the fields, give example values, state the defaults explicitly.
03
Always return a summary
Every tool returns a dict with a "summary" key: a one-sentence description of what was found. Claude reads this first. If the summary is good, the rest of the response is supplementary detail.

Pick your stack

Select what you use for each layer. The prompts below will update to match.

Logs
Database
Metrics
Errors
Cloud

Copy these into
Claude Code

Run these prompts in order. Fill in the [PLACEHOLDERS] with your actual values before sending.

Prompt 1 · Scaffold the server
Run this first in a new empty directory. It gives you the full folder structure with placeholder tools. Don't implement the queries yet.
Prompt 2 · Implement the tools
Send this after the scaffold is in place. Fill in your actual connection details, credentials, and field names before sending.
Prompt 3 · Write the tool descriptions
This is the most important prompt. The descriptions steer Claude's parameter inference. Be specific about your actual field names, index patterns, and data quirks.
Prompt 4 · Full spec doc (alternative to prompts 1–3)
Use this instead of prompts 1–3 if you prefer to give Claude the full picture in one shot. Works well with Claude's extended thinking or Opus.

Two gotchas that
will bite you

Both are specific to the streamable-HTTP transport. Both are invisible from error messages. Both cost hours.

stateless=True
Stateful mode expects the MCP client to GET an endpoint first to establish a session, then POST with the session ID. Claude Code's client just POSTs directly. Without stateless mode, every tool call silently fails with a session error and the message gives no hint why.
Fix: StreamableHTTPSessionManager(stateless=True)
Trailing slash on /mcp/
Starlette's Mount("/mcp", ...) redirects /mcp to /mcp/ as a GET. The client follows that redirect as a GET, not a POST, which breaks the handshake. Trivial fix, embarrassingly long to find.
Fix: always use /mcp/ (trailing slash) in the client URL

Writing tool descriptions
that actually work

This is what takes time. The prompt scaffold gets you a working server in an hour. Getting Claude to call the right tool with the right parameters from a plain English question takes weeks of tuning.

Name the fields
Don't say "the time field." Say time field (ISO string, e.g. "2024-01-15T09:30:00Z"). If different services use different field names for the same concept, say that explicitly.
Bad: "filter by timestamp" Good: "time range uses the `time` field (ISO string). Note: `@timestamp` exists but is not date-mapped, do not filter on it."
State the defaults explicitly
If your tool defaults to the last 7 days when no time range is given, say so in the description. "Check logs for the payments service" should just work without the caller specifying a window.
Bad: "start: start of time range (optional)" Good: "start: UTC ISO string. Defaults to 7 days ago if omitted."
Explain your index or table naming
If your log indices follow a pattern like app-{service}-{date}, describe it. If a service name fragment works as a wildcard, say so. This is the single most common source of bad tool calls.
Bad: "service_name: the service to query" Good: "service_name: fragment matched as *{name}* against index names. e.g. 'payments' matches 'app-payments-api-2024-01-15'"
Document the weird stuff
Production data has quirks. The message field is a string on some services and an object on others. The level field is an integer on some and a string on others. If Claude doesn't know this, it will write queries that blow up.
Bad: (nothing) Good: "message field: STRING on most services, OBJECT (request/response payload) on api services. Use lenient=true in query_string to skip object-typed fields."
Give an example query in the description
The most reliable way to steer inference is to show a worked example in the description itself. "To find all errors for the payments service yesterday, call with service_name='payments', level_min=50, date='2024-01-14'."