# Server-side logging
logger = logging.getLogger(__name__)

mcp = FastMCP("MCPServer")

@mcp.tool
async def search_data(query: str, ctx: Context):
    logger.info("Tool called: search_data", extra={"query": query})

    try:
        results = perform_search(query)
        logger.info("Search completed", extra={"result_count": len(results)})

        return results
    except Exception as e:
        logger.error("Search failed", exc_info=True)
        raise
```

You can then send these logs to Elasticsearch, CloudWatch, Datadog, or your preferred aggregation platform. Using structured logging (using the `extra` parameter) enables you to search and filter your logs.

#### Server-level monitoring with Sentry

Server-level monitoring tracks your MCP server's overall health, recording error rates, uptime, and resource usage. Sentry captures errors with full context, showing you what was happening when failures occurred. You can instrument MCP tool calls for distributed tracing using `@sentry/node`:

```ts
import * as Sentry from "@sentry/node";

const registerTool = (server, name, schema, handler) => {
  server.tool(name, schema, async (args) => {
    return await Sentry.startNewTrace(async () => {
      return await Sentry.startSpan(
        {
          name: `mcp.tool/${name}`,
          attributes: {
            "tool.name": name,
            // Avoid logging raw tool parameters; sanitize or omit sensitive input
            "tool.params_present": args != null,
          },
        },
        async (span) => {
          try {
            const result = await handler(args);
            span.setAttribute("tool.status", "success");
            return result;
          } catch (err) {
            span.setAttribute("tool.status", "error");
            Sentry.captureException(err);
            throw err;
          }
        },
      );
    });
  });
};

registerTool(
  server,
  "search_data",
  { query: z.string() },
  async ({ query }) => {
    // Your tool implementation
    const results = await searchDatabase(query);
    return results;
  },
);
```

This manual instrumentation captures:

- Tool call latency from invocation to completion
- Input parameters passed to each tool
- Success or failure statuses
- Full error stack traces when tools fail
- Distributed traces showing downstream API calls

Each tool invocation creates a trace you can inspect in Sentry's dashboard. When a tool fails, Sentry shows the exact parameters that caused the failure, the backend API calls that were made, and where the error originated.

For complete implementation, including error handling, user context binding, and Cloudflare Workers setup, see the Sentry guide to [monitoring MCP servers](https://blog.sentry.io/monitoring-mcp-server-sentry/).

### Packaged servers (distributed)

When you distribute your MCP server as an npm package, as a GitHub repository for cloning, or as an MCPB file, users run it on their own machines. You have no access to their infrastructure, so monitoring requires opt-in telemetry.

#### OpenTelemetry for distributed servers

OpenTelemetry provides vendor-neutral telemetry collection. Users must explicitly enable it through environment variables.

```python
import os
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter