Skip to main content
mcp-agent ships with a structured logger that captures context-rich events from apps, workflows, agents, and Temporal workers. Logs are automatically correlated with traces and forwarded to MCP clients using the Model Context Protocol logging utility.

Logger entry points

  • app.logger – application-scoped logger, ideal for tools and startup messages.
  • context.logger – request-specific logger with access to the active session, token counter, and upstream MCP connection.
  • agent.logger – automatically bound when you call async with agent:; perfect for per-agent instrumentation.
from mcp_agent.app import MCPApp

app = MCPApp(name="logging_example")

@app.async_tool
async def summarize(text: str) -> str:
    logger = app.logger
    logger.info("summarize.start", data={"characters": len(text)})

    async with app.run() as running_app:
        context_logger = running_app.context.logger
        context_logger.debug("summarize.inflight", data={"preview": text[:30]})
        # ...
        result = text.upper()

    logger.info("summarize.done", data={"preview": result[:30]})
    return result
Each call emits an Event that can be routed to multiple transports. Span IDs and workflow IDs are injected automatically when tracing is enabled.

Configure transports and levels

The logger section in mcp_agent.config.yaml controls transports, batching, and formatting:
logger:
  transports: [console, file]   # also supports http, none
  level: info                   # debug | info | warning | error
  progress_display: true
  path: "logs/mcp-agent.jsonl"
  path_settings:
    path_pattern: "logs/mcp-agent-{session_id}.jsonl"
    unique_id: "session_id"     # or "timestamp"
  batch_size: 100
  flush_interval: 2.0

  # HTTP transport (optional)
  http_endpoint: "https://logging.example.com/events"
  http_headers:
    Authorization: "Bearer ${LOGGING_TOKEN}"
  http_timeout: 5.0
TransportDescription
consoleRich-formatted output to stdout (colours, nested JSON blocks).
fileJSON Lines file writer with optional timestamp/session-based file rotation.
httpBatch POST events to an HTTP endpoint (Elasticsearch, Datadog intake, etc.).
noneDisable external transports (events still flow through the async event bus).

Structured events

Logs accept a message plus an optional data payload. The payload is serialised as JSON and preserved end-to-end:
logger.info(
    "plan.generated",
    data={
        "steps": len(plan.steps),
        "agents": [task.agent for step in plan.steps for task in step.tasks],
    },
)
Sample JSON from the file transport:
{
  "level": "INFO",
  "timestamp": "2025-01-18T02:41:09Z",
  "namespace": "logging_example.plan.generated",
  "message": "plan.generated",
  "data": {
    "steps": 3,
    "agents": ["finder", "proofreader", "editor"]
  },
  "trace": {
    "trace_id": "5f26e2c4f29be3280c7b52fb93db8550",
    "span_id": "84647445abd6213e"
  }
}
Because trace IDs are present, you can pivot between logs and OpenTelemetry spans in Jaeger/Tempo with a single click.

MCP logging to upstream clients

When your app runs as an MCP server, the logger automatically forwards events to connected clients using the MCP logging channel. MCP-compatible tools (Claude Desktop, Cursor, etc.) will display your messages in their native consoles.
# Inside a workflow or tool
context_logger.info(
    "human.approval.requested",
    data={"workflow_id": self.id, "run_id": self.run_id},
)
For long-running Temporal workflows, the logger falls back to a special activity (mcp_forward_log) so events appear in the client even while the workflow is suspended.

Tips for production setups

  • Pair logging with tracing (otel.enabled: true) so every event carries span metadata.
  • Use progress_display: true when running CLI tools to get live status bars for long flows.
  • Tune batch_size/flush_interval for high-volume agents; the defaults (100 events / 2 seconds) work well for most workloads.
  • HTTP transports can carry filters—attach an EventFilter if you only want to forward warnings and errors.

Reference implementations

I