Skip to main content

Logging

MCP defines a protocol-level logging mechanism where the server sends log messages to the client. This is separate from your application's own logging (log4cats, slf4j, etc.) — MCP logging delivers structured messages over the protocol for the client to display or act on.

See the MCP specification on logging for the full concept.

Sending Log Messages

Both ToolContext and ResourceContext provide a log method:

import cats.effect.IO
import io.circe.Json
import mcp.protocol.{Content, LoggingLevel}
import mcp.server.*

type AnalyzeInput = (query: String, verbose: Option[Boolean])
given InputDef[AnalyzeInput] = InputDef[AnalyzeInput](
query = InputField[String]("Query to analyze"),
verbose = InputField[Option[Boolean]]("Enable verbose output")
)

val analyzeTool = ToolDef.unstructured[IO, AnalyzeInput](
name = "analyze",
description = Some("Analyze a query")
) { (input, ctx) =>
for {
_ <- ctx.log(LoggingLevel.info, Json.fromString(s"Analyzing: ${input.query}"))
_ <- ctx.log(LoggingLevel.debug, Json.obj("step" -> Json.fromString("parsing")))
_ <- ctx.log(LoggingLevel.warning, Json.fromString("Large result set"), Some("performance"))
} yield List(Content.Text("Done"))
}

The log method takes:

ParameterTypeDescription
levelLoggingLevelMessage severity
dataJsonArbitrary JSON payload
loggerOption[String]Optional logger name for categorization

Log Levels

MCP defines eight severity levels (lowest to highest):

LevelUse case
debugDetailed diagnostic information
infoGeneral operational messages
noticeNormal but noteworthy events
warningPotential issues
errorError conditions
criticalCritical failures
alertAction must be taken immediately
emergencySystem is unusable

Client-Controlled Filtering

The client sets the minimum log level via logging/setLevel. The server filters messages below this threshold — they are never sent over the wire.

If the client hasn't set a level, log messages are silently dropped.

Logging in Resources

Resource handlers also have access to logging via ResourceContext:

import cats.effect.Async
import cats.syntax.all.*
import io.circe.{Codec, Json}
import mcp.protocol.LoggingLevel
import mcp.server.ResourceDef

case class Config(env: String) derives Codec.AsObject

def configResource[F[_]: Async]: ResourceDef[F, Config] =
ResourceDef[F, Config](
uri = "config://app",
name = "App Config",
handler = ctx => {
ctx.log(LoggingLevel.info, Json.fromString("Config accessed")) *>
Async[F].pure(Some(Config("production")))
}
)

MCP Logging vs Application Logging

MCP Logging (ctx.log)Application Logging (log4cats, etc.)
DestinationSent to MCP client over the protocolWritten to stderr, files, etc.
AudienceThe AI client / end userDevelopers / operators
StructuredJSON payloadFramework-dependent
Use forTool progress, status updates, diagnostics visible to the clientInternal debugging, error tracking

Both can coexist. Use MCP logging when you want the client to see the message, and application logging for operational concerns.

caution

Never include credentials, secrets, PII, or sensitive system details in MCP log messages — they are transmitted to the client.