onetool-mcp 1.0.0b1__py3-none-any.whl → 1.0.0rc2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. onetool/cli.py +63 -4
  2. onetool_mcp-1.0.0rc2.dist-info/METADATA +266 -0
  3. onetool_mcp-1.0.0rc2.dist-info/RECORD +129 -0
  4. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/LICENSE.txt +1 -1
  5. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/NOTICE.txt +54 -64
  6. ot/__main__.py +6 -6
  7. ot/config/__init__.py +48 -46
  8. ot/config/global_templates/__init__.py +2 -2
  9. ot/config/{defaults → global_templates}/diagram-templates/api-flow.mmd +33 -33
  10. ot/config/{defaults → global_templates}/diagram-templates/c4-context.puml +30 -30
  11. ot/config/{defaults → global_templates}/diagram-templates/class-diagram.mmd +87 -87
  12. ot/config/{defaults → global_templates}/diagram-templates/feature-mindmap.mmd +70 -70
  13. ot/config/{defaults → global_templates}/diagram-templates/microservices.d2 +81 -81
  14. ot/config/{defaults → global_templates}/diagram-templates/project-gantt.mmd +37 -37
  15. ot/config/{defaults → global_templates}/diagram-templates/state-machine.mmd +42 -42
  16. ot/config/global_templates/diagram.yaml +167 -0
  17. ot/config/global_templates/onetool.yaml +3 -1
  18. ot/config/{defaults → global_templates}/prompts.yaml +102 -97
  19. ot/config/global_templates/security.yaml +31 -0
  20. ot/config/global_templates/servers.yaml +93 -12
  21. ot/config/global_templates/snippets.yaml +5 -26
  22. ot/config/{defaults → global_templates}/tool_templates/__init__.py +7 -7
  23. ot/config/loader.py +221 -105
  24. ot/config/mcp.py +5 -1
  25. ot/config/secrets.py +192 -190
  26. ot/decorators.py +116 -116
  27. ot/executor/__init__.py +35 -35
  28. ot/executor/base.py +16 -16
  29. ot/executor/fence_processor.py +83 -83
  30. ot/executor/linter.py +142 -142
  31. ot/executor/pep723.py +288 -288
  32. ot/executor/runner.py +20 -6
  33. ot/executor/simple.py +163 -163
  34. ot/executor/validator.py +603 -164
  35. ot/http_client.py +145 -145
  36. ot/logging/__init__.py +37 -37
  37. ot/logging/entry.py +213 -213
  38. ot/logging/format.py +191 -188
  39. ot/logging/span.py +349 -349
  40. ot/meta.py +236 -14
  41. ot/paths.py +32 -49
  42. ot/prompts.py +218 -218
  43. ot/proxy/manager.py +14 -2
  44. ot/registry/__init__.py +189 -189
  45. ot/registry/parser.py +269 -269
  46. ot/server.py +330 -315
  47. ot/shortcuts/__init__.py +15 -15
  48. ot/shortcuts/aliases.py +87 -87
  49. ot/shortcuts/snippets.py +258 -258
  50. ot/stats/__init__.py +35 -35
  51. ot/stats/html.py +2 -2
  52. ot/stats/reader.py +354 -354
  53. ot/stats/timing.py +57 -57
  54. ot/support.py +63 -63
  55. ot/tools.py +1 -1
  56. ot/utils/batch.py +161 -161
  57. ot/utils/cache.py +120 -120
  58. ot/utils/exceptions.py +23 -23
  59. ot/utils/factory.py +178 -179
  60. ot/utils/format.py +65 -65
  61. ot/utils/http.py +202 -202
  62. ot/utils/platform.py +45 -45
  63. ot/utils/truncate.py +69 -69
  64. ot_tools/__init__.py +4 -4
  65. ot_tools/_convert/__init__.py +12 -12
  66. ot_tools/_convert/pdf.py +254 -254
  67. ot_tools/diagram.yaml +167 -167
  68. ot_tools/scaffold.py +2 -2
  69. ot_tools/transform.py +124 -19
  70. ot_tools/web_fetch.py +94 -43
  71. onetool_mcp-1.0.0b1.dist-info/METADATA +0 -163
  72. onetool_mcp-1.0.0b1.dist-info/RECORD +0 -132
  73. ot/config/defaults/bench.yaml +0 -4
  74. ot/config/defaults/onetool.yaml +0 -25
  75. ot/config/defaults/servers.yaml +0 -7
  76. ot/config/defaults/snippets.yaml +0 -4
  77. ot_tools/firecrawl.py +0 -732
  78. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/WHEEL +0 -0
  79. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/entry_points.txt +0 -0
  80. /ot/config/{defaults → global_templates}/tool_templates/extension.py +0 -0
  81. /ot/config/{defaults → global_templates}/tool_templates/isolated.py +0 -0
ot/logging/span.py CHANGED
@@ -1,349 +1,349 @@
1
- """LogSpan context manager for auto-logging operations.
2
-
3
- Wraps LogEntry and auto-logs on context exit with duration and status.
4
- Supports FastMCP Context for async MCP tool execution.
5
-
6
- Example (sync):
7
- with LogSpan(span="tool.execute", tool="search") as s:
8
- result = execute_tool()
9
- s.add("resultCount", len(result))
10
- # Auto-logs with duration, status=SUCCESS at INFO level
11
-
12
- Example (async with FastMCP Context):
13
- async with LogSpan.async_span(ctx, span="tool.execute", tool="search") as s:
14
- result = await execute_tool()
15
- await s.log_info("Tool completed", resultCount=len(result))
16
- # Logs via FastMCP Context if available, falls back to loguru
17
- """
18
-
19
- from __future__ import annotations
20
-
21
- import json
22
- from contextlib import asynccontextmanager
23
- from typing import TYPE_CHECKING, Any
24
-
25
- from loguru import logger
26
-
27
- from ot.logging.entry import LogEntry
28
- from ot.logging.format import format_log_entry
29
-
30
- if TYPE_CHECKING:
31
- from collections.abc import AsyncIterator
32
- from types import TracebackType
33
-
34
- # FastMCP Context is Any since it's an optional dependency with dynamic methods
35
- Context = Any # FastMCP context with log_info, log_error, etc.
36
-
37
-
38
- def _format_for_output(entry: LogEntry) -> str:
39
- """Format a LogEntry for log output with truncation and sanitisation.
40
-
41
- Args:
42
- entry: LogEntry to format
43
-
44
- Returns:
45
- JSON string with formatted values
46
- """
47
- # Import here to avoid circular dependency
48
- from ot.config import is_log_verbose
49
-
50
- formatted = format_log_entry(entry.to_dict(), verbose=is_log_verbose())
51
- return json.dumps(formatted, separators=(",", ":"), default=str)
52
-
53
-
54
- class LogSpan:
55
- """Context manager that wraps LogEntry and auto-logs on exit.
56
-
57
- On successful exit, logs at INFO level with status=SUCCESS.
58
- On exception, logs at ERROR level with status=FAILED, errorType, errorMessage.
59
-
60
- Supports optional FastMCP Context for async logging in MCP tool execution.
61
- """
62
-
63
- def __init__(
64
- self,
65
- level: str = "INFO",
66
- ctx: Context | None = None,
67
- **initial_fields: Any,
68
- ) -> None:
69
- """Initialize a log span.
70
-
71
- Args:
72
- level: Default log level for successful completion (default: INFO)
73
- ctx: Optional FastMCP Context for async logging
74
- **initial_fields: Initial fields for the underlying LogEntry
75
- """
76
- self._level = level.upper()
77
- self._entry = LogEntry(**initial_fields)
78
- self._ctx = ctx
79
-
80
- def add(self, key: str | None = None, value: Any = None, **kwargs: Any) -> LogSpan:
81
- """Add one or more fields to the span.
82
-
83
- Delegates to the underlying LogEntry.
84
-
85
- Args:
86
- key: Field name (optional if using kwargs)
87
- value: Field value (required if key is provided)
88
- **kwargs: Bulk field additions
89
-
90
- Returns:
91
- Self for method chaining
92
- """
93
- self._entry.add(key, value, **kwargs)
94
- return self
95
-
96
- def __setitem__(self, key: str, value: Any) -> None:
97
- """Set a field using dict-style access.
98
-
99
- Args:
100
- key: Field name
101
- value: Field value
102
- """
103
- self._entry[key] = value
104
-
105
- def __getitem__(self, key: str) -> Any:
106
- """Get a field using dict-style access.
107
-
108
- Args:
109
- key: Field name
110
-
111
- Returns:
112
- Field value
113
- """
114
- return self._entry[key]
115
-
116
- @property
117
- def entry(self) -> LogEntry:
118
- """Return the underlying LogEntry for direct access.
119
-
120
- Returns:
121
- The wrapped LogEntry instance
122
- """
123
- return self._entry
124
-
125
- @property
126
- def duration(self) -> float:
127
- """Return current duration since span creation.
128
-
129
- Returns:
130
- Duration in seconds
131
- """
132
- return self._entry.duration
133
-
134
- @property
135
- def context(self) -> Any:
136
- """Return the FastMCP Context if available.
137
-
138
- Returns:
139
- FastMCP Context or None
140
- """
141
- return self._ctx
142
-
143
- def to_dict(self) -> dict[str, Any]:
144
- """Return all fields with duration for output.
145
-
146
- Returns:
147
- Dict with all fields, duration, and status info
148
- """
149
- return self._entry.to_dict()
150
-
151
- # -------------------------------------------------------------------------
152
- # Sync context manager
153
- # -------------------------------------------------------------------------
154
-
155
- def __enter__(self) -> LogSpan:
156
- """Enter the span context.
157
-
158
- Returns:
159
- Self for use in with statement
160
- """
161
- return self
162
-
163
- def __exit__(
164
- self,
165
- exc_type: type[BaseException] | None,
166
- exc_val: BaseException | None,
167
- exc_tb: TracebackType | None,
168
- ) -> None:
169
- """Exit the span context and auto-log.
170
-
171
- On success (no exception), logs at the configured level with status=SUCCESS.
172
- On exception, logs at ERROR level with status=FAILED and error details.
173
-
174
- Args:
175
- exc_type: Exception type if an exception was raised
176
- exc_val: Exception value if an exception was raised
177
- exc_tb: Exception traceback if an exception was raised
178
- """
179
- if exc_val is not None:
180
- # Exception occurred - log as FAILED at ERROR level
181
- if isinstance(exc_val, Exception):
182
- self._entry.failure(error=exc_val)
183
- else:
184
- self._entry.failure(
185
- error_type=type(exc_val).__name__, error_message=str(exc_val)
186
- )
187
- # depth=1 makes loguru report the caller's location, not span.py
188
- logger.opt(depth=1).error(_format_for_output(self._entry))
189
- else:
190
- # Success - log at configured level
191
- self._entry.success()
192
- # depth=1 makes loguru report the caller's location, not span.py
193
- logger.opt(depth=1).log(self._level, _format_for_output(self._entry))
194
-
195
- # -------------------------------------------------------------------------
196
- # Async logging methods
197
- # -------------------------------------------------------------------------
198
-
199
- async def log_debug(self, message: str, **kwargs: Any) -> None:
200
- """Log a debug message.
201
-
202
- Dispatches to FastMCP Context if available, otherwise uses loguru.
203
-
204
- Args:
205
- message: Log message
206
- **kwargs: Additional fields to include
207
- """
208
- await self._log_async("DEBUG", message, **kwargs)
209
-
210
- async def log_info(self, message: str, **kwargs: Any) -> None:
211
- """Log an info message.
212
-
213
- Dispatches to FastMCP Context if available, otherwise uses loguru.
214
-
215
- Args:
216
- message: Log message
217
- **kwargs: Additional fields to include
218
- """
219
- await self._log_async("INFO", message, **kwargs)
220
-
221
- async def log_warning(self, message: str, **kwargs: Any) -> None:
222
- """Log a warning message.
223
-
224
- Dispatches to FastMCP Context if available, otherwise uses loguru.
225
-
226
- Args:
227
- message: Log message
228
- **kwargs: Additional fields to include
229
- """
230
- await self._log_async("WARNING", message, **kwargs)
231
-
232
- async def log_error(self, message: str, **kwargs: Any) -> None:
233
- """Log an error message.
234
-
235
- Dispatches to FastMCP Context if available, otherwise uses loguru.
236
-
237
- Args:
238
- message: Log message
239
- **kwargs: Additional fields to include
240
- """
241
- await self._log_async("ERROR", message, **kwargs)
242
-
243
- async def _log_async(self, level: str, message: str, **kwargs: Any) -> None:
244
- """Internal async logging dispatcher.
245
-
246
- Args:
247
- level: Log level (DEBUG, INFO, WARNING, ERROR)
248
- message: Log message
249
- **kwargs: Additional fields
250
- """
251
- # Add fields to entry
252
- for key, value in kwargs.items():
253
- self._entry.add(key, value)
254
-
255
- # Try to use FastMCP Context if available
256
- if self._ctx is not None:
257
- try:
258
- # FastMCP Context has log_* methods
259
- log_method = getattr(self._ctx, f"log_{level.lower()}", None)
260
- if log_method is not None and callable(log_method):
261
- await log_method(message)
262
- return
263
- except Exception:
264
- pass # Fall through to loguru
265
-
266
- # Fallback to loguru
267
- log_message = f"{message} - {self._entry}"
268
- logger.opt(depth=2).log(level.upper(), log_message)
269
-
270
- # -------------------------------------------------------------------------
271
- # Async context manager
272
- # -------------------------------------------------------------------------
273
-
274
- @classmethod
275
- @asynccontextmanager
276
- async def async_span(
277
- cls,
278
- ctx: Context | None = None,
279
- level: str = "INFO",
280
- **initial_fields: Any,
281
- ) -> AsyncIterator[LogSpan]:
282
- """Create an async context manager span.
283
-
284
- Use this for async code that needs to log via FastMCP Context.
285
-
286
- Args:
287
- ctx: Optional FastMCP Context for async logging
288
- level: Default log level for successful completion
289
- **initial_fields: Initial fields for the span
290
-
291
- Yields:
292
- LogSpan instance
293
-
294
- Example:
295
- async with LogSpan.async_span(ctx, span="tool.run") as span:
296
- result = await run_tool()
297
- span.add("result", result)
298
- """
299
- span = cls(level=level, ctx=ctx, **initial_fields)
300
- exc_info: tuple[type[BaseException] | None, BaseException | None, Any] = (
301
- None,
302
- None,
303
- None,
304
- )
305
-
306
- try:
307
- yield span
308
- except BaseException as e:
309
- exc_info = (type(e), e, e.__traceback__)
310
- raise
311
- finally:
312
- _exc_type, exc_val, _exc_tb = exc_info
313
-
314
- if exc_val is not None:
315
- # Exception occurred
316
- if isinstance(exc_val, Exception):
317
- span._entry.failure(error=exc_val)
318
- else:
319
- span._entry.failure(
320
- error_type=type(exc_val).__name__,
321
- error_message=str(exc_val),
322
- )
323
-
324
- # Log via Context or loguru
325
- formatted = _format_for_output(span._entry)
326
- if ctx is not None:
327
- try:
328
- await ctx.log_error(formatted)
329
- except Exception:
330
- logger.error(formatted)
331
- else:
332
- logger.error(formatted)
333
- else:
334
- # Success
335
- span._entry.success()
336
-
337
- # Log via Context or loguru
338
- formatted = _format_for_output(span._entry)
339
- if ctx is not None:
340
- try:
341
- log_method = getattr(ctx, f"log_{span._level.lower()}", None)
342
- if log_method is not None and callable(log_method):
343
- await log_method(formatted)
344
- else:
345
- await ctx.log_info(formatted)
346
- except Exception:
347
- logger.log(span._level, formatted)
348
- else:
349
- logger.log(span._level, formatted)
1
+ """LogSpan context manager for auto-logging operations.
2
+
3
+ Wraps LogEntry and auto-logs on context exit with duration and status.
4
+ Supports FastMCP Context for async MCP tool execution.
5
+
6
+ Example (sync):
7
+ with LogSpan(span="tool.execute", tool="search") as s:
8
+ result = execute_tool()
9
+ s.add("resultCount", len(result))
10
+ # Auto-logs with duration, status=SUCCESS at INFO level
11
+
12
+ Example (async with FastMCP Context):
13
+ async with LogSpan.async_span(ctx, span="tool.execute", tool="search") as s:
14
+ result = await execute_tool()
15
+ await s.log_info("Tool completed", resultCount=len(result))
16
+ # Logs via FastMCP Context if available, falls back to loguru
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ from contextlib import asynccontextmanager
23
+ from typing import TYPE_CHECKING, Any
24
+
25
+ from loguru import logger
26
+
27
+ from ot.logging.entry import LogEntry
28
+ from ot.logging.format import format_log_entry
29
+
30
+ if TYPE_CHECKING:
31
+ from collections.abc import AsyncIterator
32
+ from types import TracebackType
33
+
34
+ # FastMCP Context is Any since it's an optional dependency with dynamic methods
35
+ Context = Any # FastMCP context with log_info, log_error, etc.
36
+
37
+
38
+ def _format_for_output(entry: LogEntry) -> str:
39
+ """Format a LogEntry for log output with truncation and sanitisation.
40
+
41
+ Args:
42
+ entry: LogEntry to format
43
+
44
+ Returns:
45
+ JSON string with formatted values
46
+ """
47
+ # Import here to avoid circular dependency
48
+ from ot.config import is_log_verbose
49
+
50
+ formatted = format_log_entry(entry.to_dict(), verbose=is_log_verbose())
51
+ return json.dumps(formatted, separators=(",", ":"), default=str)
52
+
53
+
54
+ class LogSpan:
55
+ """Context manager that wraps LogEntry and auto-logs on exit.
56
+
57
+ On successful exit, logs at INFO level with status=SUCCESS.
58
+ On exception, logs at ERROR level with status=FAILED, errorType, errorMessage.
59
+
60
+ Supports optional FastMCP Context for async logging in MCP tool execution.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ level: str = "INFO",
66
+ ctx: Context | None = None,
67
+ **initial_fields: Any,
68
+ ) -> None:
69
+ """Initialize a log span.
70
+
71
+ Args:
72
+ level: Default log level for successful completion (default: INFO)
73
+ ctx: Optional FastMCP Context for async logging
74
+ **initial_fields: Initial fields for the underlying LogEntry
75
+ """
76
+ self._level = level.upper()
77
+ self._entry = LogEntry(**initial_fields)
78
+ self._ctx = ctx
79
+
80
+ def add(self, key: str | None = None, value: Any = None, **kwargs: Any) -> LogSpan:
81
+ """Add one or more fields to the span.
82
+
83
+ Delegates to the underlying LogEntry.
84
+
85
+ Args:
86
+ key: Field name (optional if using kwargs)
87
+ value: Field value (required if key is provided)
88
+ **kwargs: Bulk field additions
89
+
90
+ Returns:
91
+ Self for method chaining
92
+ """
93
+ self._entry.add(key, value, **kwargs)
94
+ return self
95
+
96
+ def __setitem__(self, key: str, value: Any) -> None:
97
+ """Set a field using dict-style access.
98
+
99
+ Args:
100
+ key: Field name
101
+ value: Field value
102
+ """
103
+ self._entry[key] = value
104
+
105
+ def __getitem__(self, key: str) -> Any:
106
+ """Get a field using dict-style access.
107
+
108
+ Args:
109
+ key: Field name
110
+
111
+ Returns:
112
+ Field value
113
+ """
114
+ return self._entry[key]
115
+
116
+ @property
117
+ def entry(self) -> LogEntry:
118
+ """Return the underlying LogEntry for direct access.
119
+
120
+ Returns:
121
+ The wrapped LogEntry instance
122
+ """
123
+ return self._entry
124
+
125
+ @property
126
+ def duration(self) -> float:
127
+ """Return current duration since span creation.
128
+
129
+ Returns:
130
+ Duration in seconds
131
+ """
132
+ return self._entry.duration
133
+
134
+ @property
135
+ def context(self) -> Any:
136
+ """Return the FastMCP Context if available.
137
+
138
+ Returns:
139
+ FastMCP Context or None
140
+ """
141
+ return self._ctx
142
+
143
+ def to_dict(self) -> dict[str, Any]:
144
+ """Return all fields with duration for output.
145
+
146
+ Returns:
147
+ Dict with all fields, duration, and status info
148
+ """
149
+ return self._entry.to_dict()
150
+
151
+ # -------------------------------------------------------------------------
152
+ # Sync context manager
153
+ # -------------------------------------------------------------------------
154
+
155
+ def __enter__(self) -> LogSpan:
156
+ """Enter the span context.
157
+
158
+ Returns:
159
+ Self for use in with statement
160
+ """
161
+ return self
162
+
163
+ def __exit__(
164
+ self,
165
+ exc_type: type[BaseException] | None,
166
+ exc_val: BaseException | None,
167
+ exc_tb: TracebackType | None,
168
+ ) -> None:
169
+ """Exit the span context and auto-log.
170
+
171
+ On success (no exception), logs at the configured level with status=SUCCESS.
172
+ On exception, logs at ERROR level with status=FAILED and error details.
173
+
174
+ Args:
175
+ exc_type: Exception type if an exception was raised
176
+ exc_val: Exception value if an exception was raised
177
+ exc_tb: Exception traceback if an exception was raised
178
+ """
179
+ if exc_val is not None:
180
+ # Exception occurred - log as FAILED at ERROR level
181
+ if isinstance(exc_val, Exception):
182
+ self._entry.failure(error=exc_val)
183
+ else:
184
+ self._entry.failure(
185
+ error_type=type(exc_val).__name__, error_message=str(exc_val)
186
+ )
187
+ # depth=1 makes loguru report the caller's location, not span.py
188
+ logger.opt(depth=1).error(_format_for_output(self._entry))
189
+ else:
190
+ # Success - log at configured level
191
+ self._entry.success()
192
+ # depth=1 makes loguru report the caller's location, not span.py
193
+ logger.opt(depth=1).log(self._level, _format_for_output(self._entry))
194
+
195
+ # -------------------------------------------------------------------------
196
+ # Async logging methods
197
+ # -------------------------------------------------------------------------
198
+
199
+ async def log_debug(self, message: str, **kwargs: Any) -> None:
200
+ """Log a debug message.
201
+
202
+ Dispatches to FastMCP Context if available, otherwise uses loguru.
203
+
204
+ Args:
205
+ message: Log message
206
+ **kwargs: Additional fields to include
207
+ """
208
+ await self._log_async("DEBUG", message, **kwargs)
209
+
210
+ async def log_info(self, message: str, **kwargs: Any) -> None:
211
+ """Log an info message.
212
+
213
+ Dispatches to FastMCP Context if available, otherwise uses loguru.
214
+
215
+ Args:
216
+ message: Log message
217
+ **kwargs: Additional fields to include
218
+ """
219
+ await self._log_async("INFO", message, **kwargs)
220
+
221
+ async def log_warning(self, message: str, **kwargs: Any) -> None:
222
+ """Log a warning message.
223
+
224
+ Dispatches to FastMCP Context if available, otherwise uses loguru.
225
+
226
+ Args:
227
+ message: Log message
228
+ **kwargs: Additional fields to include
229
+ """
230
+ await self._log_async("WARNING", message, **kwargs)
231
+
232
+ async def log_error(self, message: str, **kwargs: Any) -> None:
233
+ """Log an error message.
234
+
235
+ Dispatches to FastMCP Context if available, otherwise uses loguru.
236
+
237
+ Args:
238
+ message: Log message
239
+ **kwargs: Additional fields to include
240
+ """
241
+ await self._log_async("ERROR", message, **kwargs)
242
+
243
+ async def _log_async(self, level: str, message: str, **kwargs: Any) -> None:
244
+ """Internal async logging dispatcher.
245
+
246
+ Args:
247
+ level: Log level (DEBUG, INFO, WARNING, ERROR)
248
+ message: Log message
249
+ **kwargs: Additional fields
250
+ """
251
+ # Add fields to entry
252
+ for key, value in kwargs.items():
253
+ self._entry.add(key, value)
254
+
255
+ # Try to use FastMCP Context if available
256
+ if self._ctx is not None:
257
+ try:
258
+ # FastMCP Context has log_* methods
259
+ log_method = getattr(self._ctx, f"log_{level.lower()}", None)
260
+ if log_method is not None and callable(log_method):
261
+ await log_method(message)
262
+ return
263
+ except Exception:
264
+ pass # Fall through to loguru
265
+
266
+ # Fallback to loguru
267
+ log_message = f"{message} - {self._entry}"
268
+ logger.opt(depth=2).log(level.upper(), log_message)
269
+
270
+ # -------------------------------------------------------------------------
271
+ # Async context manager
272
+ # -------------------------------------------------------------------------
273
+
274
+ @classmethod
275
+ @asynccontextmanager
276
+ async def async_span(
277
+ cls,
278
+ ctx: Context | None = None,
279
+ level: str = "INFO",
280
+ **initial_fields: Any,
281
+ ) -> AsyncIterator[LogSpan]:
282
+ """Create an async context manager span.
283
+
284
+ Use this for async code that needs to log via FastMCP Context.
285
+
286
+ Args:
287
+ ctx: Optional FastMCP Context for async logging
288
+ level: Default log level for successful completion
289
+ **initial_fields: Initial fields for the span
290
+
291
+ Yields:
292
+ LogSpan instance
293
+
294
+ Example:
295
+ async with LogSpan.async_span(ctx, span="tool.run") as span:
296
+ result = await run_tool()
297
+ span.add("result", result)
298
+ """
299
+ span = cls(level=level, ctx=ctx, **initial_fields)
300
+ exc_info: tuple[type[BaseException] | None, BaseException | None, Any] = (
301
+ None,
302
+ None,
303
+ None,
304
+ )
305
+
306
+ try:
307
+ yield span
308
+ except BaseException as e:
309
+ exc_info = (type(e), e, e.__traceback__)
310
+ raise
311
+ finally:
312
+ _exc_type, exc_val, _exc_tb = exc_info
313
+
314
+ if exc_val is not None:
315
+ # Exception occurred
316
+ if isinstance(exc_val, Exception):
317
+ span._entry.failure(error=exc_val)
318
+ else:
319
+ span._entry.failure(
320
+ error_type=type(exc_val).__name__,
321
+ error_message=str(exc_val),
322
+ )
323
+
324
+ # Log via Context or loguru
325
+ formatted = _format_for_output(span._entry)
326
+ if ctx is not None:
327
+ try:
328
+ await ctx.log_error(formatted)
329
+ except Exception:
330
+ logger.error(formatted)
331
+ else:
332
+ logger.error(formatted)
333
+ else:
334
+ # Success
335
+ span._entry.success()
336
+
337
+ # Log via Context or loguru
338
+ formatted = _format_for_output(span._entry)
339
+ if ctx is not None:
340
+ try:
341
+ log_method = getattr(ctx, f"log_{span._level.lower()}", None)
342
+ if log_method is not None and callable(log_method):
343
+ await log_method(formatted)
344
+ else:
345
+ await ctx.log_info(formatted)
346
+ except Exception:
347
+ logger.log(span._level, formatted)
348
+ else:
349
+ logger.log(span._level, formatted)