Skip to content

feat: log MCP tool calls and agent messages to per-run JSONL#1499

Merged
spomichter merged 2 commits intodevfrom
feat/agent-skill-logging
Mar 10, 2026
Merged

feat: log MCP tool calls and agent messages to per-run JSONL#1499
spomichter merged 2 commits intodevfrom
feat/agent-skill-logging

Conversation

@spomichter
Copy link
Contributor

Problem

MCP tool calls and agent messages are invisible in dimos log:

  • MCP server: Only errors were logged. Successful tool calls — zero logging.
  • Agent: pretty_print_langchain_message writes to stdout only. Nothing in per-run JSONL.

So when an agent calls a skill, there's no persistent record of what happened.

Solution

MCP server (mcp_server.py)

Log every tool call with name, args, response (truncated 200 chars), duration:

15:36:31 [inf] mcp_server.py     MCP tool call   tool=move args={"forward": 10}
15:36:31 [inf] mcp_server.py     MCP tool done   tool=move duration=0.3s response=...
15:36:31 [war] mcp_server.py     MCP tool not found  tool=nonexistent
15:36:31 [err] mcp_server.py     MCP tool error  tool=broken duration=0.1s

Agent (utils.py)

pretty_print_langchain_message now also writes to structlog:

15:36:32 [inf] utils.py          Agent message   msg_type=human content='move forward'
15:36:33 [inf] utils.py          Agent message   msg_type=ai tool_calls=[{"name": "move", ...}]
15:36:33 [inf] utils.py          Agent message   msg_type=tool content='{"status": "ok"}'

Both now visible via dimos log.

Changes

  • dimos/agents/mcp/mcp_server.py: +12 lines (4 log calls in _handle_tools_call)
  • dimos/agents/utils.py: +19 lines (structlog logger + _log_message helper)

Contributor License Agreement

  • I have read and approved the CLA

MCP server: log every tool call (name, args), response (truncated to
200 chars), duration, and errors. Previously only errors were logged.

Agent: pretty_print_langchain_message now also writes to structlog so
agent conversation (human, AI, tool calls, tool responses) appears in
per-run JSONL logs alongside module-level events.

Both are now visible via dimos log.
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR adds structured per-run JSONL logging to two previously silent code paths: MCP tool call dispatch in mcp_server.py and LangChain agent message pretty-printing in utils.py. The changes are additive and low-risk, making tool calls and agent messages visible via dimos log.

Key changes:

  • mcp_server.py: Wraps the four possible outcomes of _handle_tools_call (not-found, error, async/None result, normal result) with logger.info/logger.warning/logger.exception calls, adding tool, duration, and truncated response fields.
  • utils.py: Introduces a module-level logger and a _log_message helper called at the end of pretty_print_langchain_message, mirroring the console output to structlog.

Issue found:

  • _log_message receives raw content without passing it through _try_to_remove_url_data first, meaning base64 image data can appear in the JSONL log—inconsistent with the filtering already applied for console output.

Confidence Score: 3/5

  • Safe to merge after applying the URL data filtering fix to prevent base64 image data leaking into JSONL logs.
  • The changes are purely additive logging with no impact on control flow or return values. However, there is one concrete issue: raw content (potentially containing base64 image URLs) bypasses the existing _try_to_remove_url_data filter when written to JSONL, creating a privacy/data-leak risk that should be addressed before merge. This is a simple one-line fix but blocks the confidence score until resolved.
  • dimos/agents/utils.py — Apply _try_to_remove_url_data to content before passing to _log_message

Sequence Diagram

sequenceDiagram
    participant Client as MCP Client
    participant Server as mcp_server.py
    participant Logger as structlog (JSONL)
    participant Tool as RPC Tool

    Client->>Server: POST /mcp tools/call {name, args}
    alt Tool not found
        Server->>Logger: warning("MCP tool not found", tool=name)
        Server-->>Client: Tool not found error
    else Tool found
        Server->>Logger: info("MCP tool call", tool=name, args=args)
        Server->>Tool: rpc_call(**args)
        alt Exception raised
            Tool-->>Server: raises Exception
            Server->>Logger: exception("MCP tool error", tool=name, duration=...)
            Server-->>Client: Error response
        else result is None (async)
            Tool-->>Server: None
            Server->>Logger: info("MCP tool done (async)", tool=name, duration=...)
            Server-->>Client: "It has started..."
        else result has agent_encode
            Tool-->>Server: result
            Server->>Logger: info("MCP tool done", tool=name, duration=..., response=str(result)[:200])
            Server-->>Client: result.agent_encode()
        else result is text
            Tool-->>Server: result
            Server->>Logger: info("MCP tool done", tool=name, duration=..., response=str(result)[:200])
            Server-->>Client: str(result)
        end
    end

    Note over Server,Logger: Agent message flow (utils.py)
    participant Agent as LangChain Agent
    participant Utils as utils.py
    Agent->>Utils: pretty_print_langchain_message(msg)
    Utils->>Utils: _try_to_remove_url_data(content) [console only]
    Utils->>Logger: info("Agent message", msg_type=..., content=..., tool_calls=...)
Loading

Last reviewed commit: ca796e3

print(f"{time_str} {type_str}")

# Also log to structlog so agent messages appear in per-run JSONL logs.
_log_message(msg_type, content, tool_calls)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raw content bypasses URL filtering before JSONL logging

The _log_message call at line 86 passes raw content without filtering through _try_to_remove_url_data. However, the console output (line 70) explicitly filters via _try_to_remove_url_data to strip base64 image URLs. For multimodal messages like [{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}}], this means up to 500 characters of base64 image data will be written to the JSONL log file—inconsistent with the filtering applied to console output.

Pass the filtered content to _log_message instead:

Suggested change
_log_message(msg_type, content, tool_calls)
_log_message(msg_type, _try_to_remove_url_data(content), tool_calls)

@spomichter spomichter force-pushed the feat/agent-skill-logging branch from 496451f to fd30ed2 Compare March 10, 2026 08:36
- Filter base64 image data once at content extraction instead of at
  each usage site (console output and JSONL logging).
- Simplify _log_message: replace if/elif/else branches with a single
  logger.info call using conditional kwargs dict.
@spomichter spomichter force-pushed the feat/agent-skill-logging branch from fd30ed2 to b5e5e84 Compare March 10, 2026 08:46
@spomichter spomichter merged commit 8ebbb6c into dev Mar 10, 2026
12 checks passed
@spomichter spomichter deleted the feat/agent-skill-logging branch March 10, 2026 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants