-
Notifications
You must be signed in to change notification settings - Fork 3
Add logging infrastructure with MCPD_LOG_LEVEL support #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughAdds an internal logging subsystem (LogLevel, Logger protocol, create_logger), exposes Logger/LogLevel from the package, integrates an optional logger into McpdClient (constructor accepts logger and uses create_logger), replaces TODO warnings with real logging, updates README, and adds unit tests for logging behaviour. No public runtime API changes beyond new exports and optional logger parameter. Changes
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: ASSERTIVE Plan: Pro 📒 Files selected for processing (2)
🔇 Additional comments (11)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
README.md (1)
155-162: Clarify logger parameter usage in initialisation snippetYou mention
loggerin the comment but the example call does not show it. Consider either addinglogger=to the example (e.g.logger=CustomLogger()) or adding a second snippet demonstrating passing a logger so readers can immediately see how to wire it in.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (5)
README.md(2 hunks)src/mcpd/__init__.py(2 hunks)src/mcpd/_logger.py(1 hunks)src/mcpd/mcpd_client.py(6 hunks)tests/unit/test_mcpd_client.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/mcpd/_logger.py (1)
tests/unit/test_mcpd_client.py (20)
trace(987-988)trace(1015-1016)trace(1056-1057)trace(1100-1101)debug(990-991)debug(1018-1019)debug(1059-1060)debug(1103-1104)info(993-994)info(1021-1022)info(1062-1063)info(1106-1107)warn(996-997)warn(1024-1025)warn(1065-1066)warn(1109-1110)error(999-1000)error(1027-1028)error(1068-1069)error(1112-1113)
src/mcpd/mcpd_client.py (2)
src/mcpd/_logger.py (5)
Logger(31-56)create_logger(213-264)warn(50-52)warn(143-148)warn(198-203)src/mcpd/exceptions.py (4)
TimeoutError(275-305)AuthenticationError(83-102)ServerNotFoundError(105-132)McpdError(8-59)
tests/unit/test_mcpd_client.py (2)
tests/conftest.py (3)
client(52-53)tools_side_effect(62-83)side_effect(78-79)src/mcpd/_logger.py (9)
trace(38-40)trace(122-127)trace(177-182)warn(50-52)warn(143-148)warn(198-203)error(54-56)error(150-155)error(205-210)
src/mcpd/__init__.py (1)
src/mcpd/_logger.py (2)
Logger(31-56)LogLevel(17-28)
🪛 Ruff (0.14.5)
src/mcpd/_logger.py
167-167: Missing return type annotation for special method __init__
Add return type annotation: None
(ANN204)
src/mcpd/mcpd_client.py
113-113: Missing return type annotation for special method __init__
Add return type annotation: None
(ANN204)
539-539: Logging statement uses warn instead of warning
Convert to warning
(G010)
570-570: Logging statement uses warn instead of warning
Convert to warning
(G010)
575-575: Logging statement uses warn instead of warning
Convert to warning
(G010)
tests/unit/test_mcpd_client.py
983-983: Missing return type annotation for special method __init__
Add return type annotation: None
(ANN204)
1012-1012: Missing return type annotation for special method __init__
Add return type annotation: None
(ANN204)
1053-1053: Missing return type annotation for special method __init__
Add return type annotation: None
(ANN204)
1097-1097: Missing return type annotation for special method __init__
Add return type annotation: None
(ANN204)
1125-1125: Missing return type annotation for private function tools_side_effect
(ANN202)
1129-1129: Avoid specifying long messages outside the exception class
(TRY003)
🔇 Additional comments (6)
README.md (1)
186-272: Logging documentation matches implementation and is clearThe description of
MCPD_LOG_LEVEL, available levels, default “off” behaviour, stderr-only output, and the custom/partial logger examples all line up with the new_loggermodule and McpdClient wiring. The warning about not enabling logging in MCP server contexts is especially important and well‑placed.src/mcpd/__init__.py (1)
15-33: Public export of Logger and LogLevel looks appropriateExposing
LoggerandLogLevelvia__all__matches the README examples (from mcpd import Logger) and keeps the logging API discoverable from the package root. No behavioural risks here.src/mcpd/mcpd_client.py (2)
527-546: Warning log on tool fetch failures is appropriate; silence Ruff’s false positiveCatching
ConnectionError,TimeoutError,AuthenticationError,ServerNotFoundError, andMcpdErrorhere and logging a warning while continuing ensures one misbehaving server does not prevent tools from healthy servers being exposed – that matches the docstring comment about ignoring servers whose schemas cannot be retrieved.Ruff’s G010 warning about
.warnis a false positive in this context becauseself._loggeris your ownLoggerprotocol, notlogging.Logger. If Ruff is enforced as an error, consider explicitly suppressing it:- self._logger.warn("Server '%s' became unavailable or unhealthy during tool fetch: %s", server_name, e) + self._logger.warn( # noqa: G010 - custom Logger.warn, not logging.Logger.warn + "Server '%s' became unavailable or unhealthy during tool fetch: %s", + server_name, + e, + )
564-580: Health filtering logs are correct; consider linter suppression for.warnThe new warnings for non‑existent servers and unhealthy servers in
_get_healthy_serversare consistent with the “silent ignore” contract when logging is disabled (default), while providing useful diagnostics whenMCPD_LOG_LEVELenables logging.As with the other call site, Ruff’s G010 about
.warnis not applicable to your customLoggerprotocol. If needed, you can silence it similarly:- if not health: - self._logger.warn("Skipping non-existent server '%s'", name) + if not health: + self._logger.warn( # noqa: G010 - custom Logger.warn, not logging.Logger.warn + "Skipping non-existent server '%s'", + name, + ) return False status = health.get("status") if not HealthStatus.is_healthy(status): - self._logger.warn("Skipping unhealthy server '%s' with status '%s'", name, status) + self._logger.warn( # noqa: G010 - custom Logger.warn, not logging.Logger.warn + "Skipping unhealthy server '%s' with status '%s'", + name, + status, + ) return Falsesrc/mcpd/_logger.py (2)
17-57: LogLevel and Logger protocol are well‑designed and match docsThe
LogLevelenum values (trace,debug,info,warn,error,off) and theLoggerprotocol’s methods align with the README and the wayMcpdClientcalls into the logger. Usingstr, Enummakes env/value comparisons straightforward and avoids accidental mismatches.
59-157: Default logger correctly honours MCPD_LOG_LEVEL and is safe by defaultThe combination of
_RANKS,_resolve_log_level,_get_level, and_DefaultLoggergives you:
- Logging fully disabled when
MCPD_LOG_LEVELis unset/invalid (off).- Lazy evaluation of the env var on every call, so tests or callers can change the level at runtime.
- Proper mapping to Python’s logging levels (including a custom
TRACElevel and alias support forwarning).The use of a single module‑level logger with a stderr
StreamHandlerandpropagate = Falseensures you don’t accidentally inherit application‑level logging configuration, matching the “don’t contaminate stdout/stderr” requirement.
96df4c4 to
eda8234
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (5)
README.md(2 hunks)src/mcpd/__init__.py(2 hunks)src/mcpd/_logger.py(1 hunks)src/mcpd/mcpd_client.py(6 hunks)tests/unit/test_mcpd_client.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/mcpd/__init__.py (1)
src/mcpd/_logger.py (2)
Logger(31-56)LogLevel(17-28)
tests/unit/test_mcpd_client.py (2)
src/mcpd/mcpd_client.py (1)
McpdClient(71-1021)src/mcpd/_logger.py (15)
trace(38-40)trace(122-127)trace(177-182)debug(42-44)debug(129-134)debug(184-189)info(46-48)info(136-141)info(191-196)warn(50-52)warn(143-148)warn(198-203)error(54-56)error(150-155)error(205-210)
src/mcpd/mcpd_client.py (1)
src/mcpd/_logger.py (5)
Logger(31-56)create_logger(213-264)warn(50-52)warn(143-148)warn(198-203)
🪛 Ruff (0.14.5)
tests/unit/test_mcpd_client.py
983-983: Missing return type annotation for special method __init__
Add return type annotation: None
(ANN204)
1012-1012: Missing return type annotation for special method __init__
Add return type annotation: None
(ANN204)
1053-1053: Missing return type annotation for special method __init__
Add return type annotation: None
(ANN204)
1097-1097: Missing return type annotation for special method __init__
Add return type annotation: None
(ANN204)
1125-1125: Missing return type annotation for private function tools_side_effect
(ANN202)
1129-1129: Avoid specifying long messages outside the exception class
(TRY003)
src/mcpd/mcpd_client.py
539-539: Logging statement uses warn instead of warning
Convert to warning
(G010)
570-570: Logging statement uses warn instead of warning
Convert to warning
(G010)
575-575: Logging statement uses warn instead of warning
Convert to warning
(G010)
🔇 Additional comments (10)
src/mcpd/__init__.py (1)
15-15: LGTM! Logger and LogLevel properly exported.The imports and public API exports align correctly with the new logging subsystem.
Also applies to: 32-33
README.md (1)
186-272: Excellent documentation of logging features.The Logging section is comprehensive and well-structured:
- Clear warning about stdout contamination in MCP server contexts
- Complete list of available log levels
- Practical examples for default, custom, and partial loggers
- Aligns well with the implementation
src/mcpd/mcpd_client.py (4)
539-539: Static analysis hint aboutwarnvswarningis a false positive.Ruff suggests using
warninginstead ofwarnfor consistency with Python's standardloggingmodule. However, theLoggerprotocol (defined insrc/mcpd/_logger.py) intentionally useswarnto align with the mcpd server binary's log levels for consistency across the mcpd ecosystem, as documented in theLogLevelenum.This design choice is correct given the project's requirements.
Based on relevant code snippets showing the Logger protocol defines
warn, notwarning.Also applies to: 570-570, 575-575
113-119: Logger integration is well-implemented.The optional
loggerparameter with lazy evaluation viacreate_logger(logger)cleanly supports default, full custom, and partial custom loggers without changing existing behaviour whenloggeris omitted. The return type annotation-> Noneis already present, satisfying type checking requirements.Also applies to: 152-152
537-540: Logging for tool fetch failures is appropriate.The warning log when a previously healthy server becomes unavailable during tool fetch provides valuable diagnostic information without breaking the agent tools flow.
566-578: Health filtering with logging is well-designed.The
_get_healthy_serversmethod correctly logs warnings for non-existent and unhealthy servers whilst filtering them out. The early return pattern is clean and the logging provides useful diagnostic information.src/mcpd/_logger.py (4)
17-56: LogLevel enum and Logger protocol are well-defined.The
LogLevelenum values align with the mcpd server binary log levels for ecosystem consistency. TheLoggerprotocol provides a clear interface that supports both custom implementations and standard logging semantics.
59-71: Custom TRACE level correctly implemented.The custom
TRACElevel positioned at 5 (belowDEBUG=10) and registered vialogging.addLevelName()is correct. The_RANKSdictionary provides a clean mapping for level comparisons.
102-157: Default logger implementation with lazy evaluation is sound.The lazy evaluation pattern (
_get_level()called on each log statement) enables dynamic level changes without module reloading, which is useful for testing. The handler is created once with proper configuration (stderr output, no propagation), and each log method correctly checks the level before logging.
160-264: Partial logger wrapper and factory function are well-designed.The
_PartialLoggerWrappercorrectly routes each log method to the custom implementation when present, otherwise falling back to the default logger. Thecreate_loggerfactory's detection of full implementations viarequired_methodsusingLogLevelenum values keeps method names synchronised and the logic is robust. The__init__return type annotation is already present.
Implements SDK logging supporting MCPD_LOG_LEVEL environment variable to help users diagnose issues with server health and tool availability. - Add LogLevel enum and Logger protocol in _logger.py - Integrate logger into McpdClient with lazy evaluation pattern - Log warnings for non-existent servers, unhealthy servers, and tool fetch failures - Export Logger and LogLevel in public API - Add comprehensive test coverage for logging functionality - Document logging configuration and custom logger usage in README Logging is disabled by default to avoid contaminating stdout/stderr, particularly important for MCP server contexts where stdout is used for JSON-RPC communication.
eda8234 to
3afb591
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (1)
tests/unit/test_mcpd_client.py (1)
968-1149: Logging tests are thorough; type‑hint nits are optionalThe
TestLoggersuite gives good coverage of default/custom logger initialisation and the three key warning paths (_get_healthy_serversnon‑existent/unhealthy servers and_agent_toolstool‑fetch failures). This should catch regressions in the new logging behaviour.If you want to satisfy Ruff’s ANN20x hints, you can optionally add explicit
-> Noneto the innerCustomLogger.__init__methods and a return type ontools_side_effect, e.g.:- class CustomLogger: - def __init__(self): + class CustomLogger: + def __init__(self) -> None: self.warnings = [] self.errors = [] ... - def tools_side_effect(server_name=None): + def tools_side_effect(server_name: str | None = None) -> list[dict]: ...Purely cosmetic; behaviour stays the same.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (5)
README.md(2 hunks)src/mcpd/__init__.py(2 hunks)src/mcpd/_logger.py(1 hunks)src/mcpd/mcpd_client.py(6 hunks)tests/unit/test_mcpd_client.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/mcpd/__init__.py (1)
src/mcpd/_logger.py (2)
Logger(31-56)LogLevel(17-28)
src/mcpd/mcpd_client.py (2)
src/mcpd/_logger.py (5)
Logger(31-56)create_logger(213-264)warn(50-52)warn(143-148)warn(198-203)src/mcpd/exceptions.py (4)
TimeoutError(275-305)AuthenticationError(83-102)ServerNotFoundError(105-132)McpdError(8-59)
🪛 Ruff (0.14.5)
src/mcpd/mcpd_client.py
539-539: Logging statement uses warn instead of warning
Convert to warning
(G010)
570-570: Logging statement uses warn instead of warning
Convert to warning
(G010)
575-575: Logging statement uses warn instead of warning
Convert to warning
(G010)
tests/unit/test_mcpd_client.py
983-983: Missing return type annotation for special method __init__
Add return type annotation: None
(ANN204)
1012-1012: Missing return type annotation for special method __init__
Add return type annotation: None
(ANN204)
1053-1053: Missing return type annotation for special method __init__
Add return type annotation: None
(ANN204)
1097-1097: Missing return type annotation for special method __init__
Add return type annotation: None
(ANN204)
1125-1125: Missing return type annotation for private function tools_side_effect
(ANN202)
1129-1129: Avoid specifying long messages outside the exception class
(TRY003)
🔇 Additional comments (5)
src/mcpd/__init__.py (1)
15-15: Public Logger/LogLevel exports look correctImporting
LoggerandLogLevelfrom._loggerand adding them to__all__keeps the public API aligned with the new logging subsystem and README usage examples.Also applies to: 29-43
src/mcpd/mcpd_client.py (3)
21-21: Logger wiring inMcpdClientis sound and non‑intrusiveImporting
Logger/create_logger, adding thelogger: Logger | None = Noneparameter, and initialisingself._logger = create_logger(logger)cleanly support default, full custom, and partial custom loggers without changing behaviour when the parameter is omitted. Keeping logging behind this indirection and defaulting MCPD_LOG_LEVEL tooffpreserves current clients by default.Also applies to: 113-120, 151-166
506-547: Warning on tool‑fetch failure for previously healthy servers is well‑scopedCatching
ConnectionError,TimeoutError,AuthenticationError,ServerNotFoundError, andMcpdErroraroundself.tools(server_name=...)and emittingself._logger.warn(...)before skipping that server matches the requirement to warn when assumed‑healthy servers fail during schema retrieval, while still allowing other healthy servers’ tools to be built. This is a sensible trade‑off between robustness and observability.
548-581: Health‑filter warnings behave as described in the docstring
_get_healthy_serversnow logs a warning when:
- A requested server name is absent from
health_map(treated as non‑existent); and- A server’s
statusis not healthy, using the detailed status string.Using
HealthStatus.is_healthyhere keeps the logic in sync with the enum, and centralising the health map fetch inserver_health()avoids redundant calls.src/mcpd/_logger.py (1)
1-71: Logging shim design matches requirements and is safely default‑off
LogLevel, theLoggerprotocol, the default_DefaultLogger, and_PartialLoggerWrappertogether provide:
- A consistent level vocabulary (
trace,debug,info,warn,error,off) matching the README.- A shared module‑level default logger that is disabled unless
MCPD_LOG_LEVELis set.- Lazy level evaluation on each log call via
_get_level, so config changes and tests can adjust behaviour at runtime.- Partial custom logger support, with only overridden methods bypassing the default logger.
Using a dedicated logger with a single
StreamHandlerto stderr andpropagate = Falseis a good fit for avoiding accidental contamination of application‑level logging.Also applies to: 102-211, 213-268
Implements SDK logging supporting
MCPD_LOG_LEVELenvironment variable to help users diagnose issues with server health and tool availability.LogLevelenum and Logger protocol in_logger.pyMcpdClientwith lazy evaluation patternLogLevelin public APILogging is disabled by default to avoid contaminating stdout/stderr, particularly important for MCP server contexts where stdout is used for JSON-RPC communication.
Closes: #35
Refs: mozilla-ai/mcpd-sdk-javascript#18
Summary by CodeRabbit
New Features
Documentation
Tests
✏️ Tip: You can customize this high-level summary in your review settings.