plotext-plus 1.0.9__py3-none-any.whl → 1.0.10__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.
@@ -1,5 +1,4 @@
1
1
  # /usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
 
4
3
  """
5
4
  Plotext Plus MCP Server - Model Context Protocol Integration
@@ -12,35 +11,221 @@ The server uses chuk-mcp-server for zero-configuration MCP functionality.
12
11
  """
13
12
 
14
13
  try:
15
- from chuk_mcp_server import tool, resource, prompt, run
16
- except ImportError:
14
+ from chuk_mcp_server import ChukMCPServer
15
+ from chuk_mcp_server import prompt
16
+ from chuk_mcp_server import resource
17
+ from chuk_mcp_server import tool
18
+ except ImportError as e:
17
19
  raise ImportError(
18
20
  "chuk-mcp-server is required for MCP functionality. "
19
21
  "Install it with: uv add --optional mcp plotext_plus"
20
- )
22
+ ) from e
21
23
 
22
- import asyncio
23
- from typing import List, Optional, Union, Dict, Any
24
24
  import json
25
- import base64
26
- from io import StringIO
25
+ import logging
27
26
  import sys
27
+ from datetime import datetime
28
+ from io import StringIO
29
+ from typing import Any
28
30
 
29
31
  # Import public plotext_plus APIs
30
- from . import plotting
32
+ from . import _core
31
33
  from . import charts
32
- from . import themes
34
+ from . import plotting
33
35
  from . import utilities
34
36
 
35
37
  # Keep track of the current plot state
36
38
  _current_plot_buffer = StringIO()
37
39
 
40
+ # Set up logging
41
+ _logger = logging.getLogger("plotext_plus_mcp")
42
+ _logger.setLevel(logging.INFO)
43
+
44
+ # Create console handler
45
+ _console_handler = logging.StreamHandler(sys.stderr)
46
+ _console_handler.setLevel(logging.INFO)
47
+
48
+ # Create formatter
49
+ _formatter = logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s")
50
+ _console_handler.setFormatter(_formatter)
51
+
52
+ # Add handler to logger
53
+ _logger.addHandler(_console_handler)
54
+
55
+
56
+ # ============================================================================
57
+ # Custom MCP Logging Handler (based on chuk-mcp-server example)
58
+ # ============================================================================
59
+
60
+
61
+ class MCPLoggingHandler(logging.Handler):
62
+ """
63
+ Custom logging handler that sends log messages to MCP clients via notifications.
64
+
65
+ This handler converts Python log records into MCP logging notifications
66
+ and sends them to connected clients.
67
+ """
68
+
69
+ def __init__(self, mcp_server: ChukMCPServer):
70
+ super().__init__()
71
+ self.mcp_server = mcp_server
72
+ self.notification_queue: list[dict[str, Any]] = []
73
+ self.clients: dict[str, Any] = {} # Track connected clients
74
+
75
+ # Set up formatting
76
+ formatter = logging.Formatter(
77
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
78
+ )
79
+ self.setFormatter(formatter)
80
+
81
+ def emit(self, record: logging.LogRecord) -> None:
82
+ """Emit a log record as an MCP notification."""
83
+ try:
84
+ # Format the log message
85
+ formatted_message = self.format(record)
86
+
87
+ # Create MCP logging notification
88
+ notification = {
89
+ "jsonrpc": "2.0",
90
+ "method": "notifications/message",
91
+ "params": {
92
+ "level": self._map_log_level(record.levelno),
93
+ "logger": record.name,
94
+ "data": {
95
+ "message": record.getMessage(),
96
+ "timestamp": datetime.fromtimestamp(record.created).isoformat(),
97
+ "module": record.module,
98
+ "function": record.funcName,
99
+ "line": record.lineno,
100
+ "formatted": formatted_message,
101
+ },
102
+ },
103
+ }
104
+
105
+ # Add exception info if present
106
+ if record.exc_info:
107
+ notification["params"]["data"]["exception"] = self.formatException(
108
+ record.exc_info
109
+ )
110
+
111
+ # Queue notification for sending to clients
112
+ self.notification_queue.append(notification)
113
+
114
+ # In a real implementation, you would send this to connected clients
115
+ # For this example, we'll just print to stderr for demonstration
116
+ print(f"[MCP LOG NOTIFICATION] {json.dumps(notification)}", file=sys.stderr)
117
+
118
+ except Exception:
119
+ # Don't raise exceptions in logging handler
120
+ self.handleError(record)
121
+
122
+ def _map_log_level(self, python_level: int) -> str:
123
+ """Map Python logging levels to MCP logging levels."""
124
+ if python_level >= logging.CRITICAL:
125
+ return "error" # MCP doesn't have CRITICAL, map to error
126
+ elif python_level >= logging.ERROR:
127
+ return "error"
128
+ elif python_level >= logging.WARNING:
129
+ return "warning"
130
+ elif python_level >= logging.INFO:
131
+ return "info"
132
+ else:
133
+ return "debug"
134
+
135
+
136
+ # ============================================================================
137
+ # Enhanced ChukMCPServer with Logging Support
138
+ # ============================================================================
139
+
140
+
141
+ class PlotextPlusMCPServer(ChukMCPServer):
142
+ """
143
+ Extended ChukMCPServer with integrated MCP logging support.
144
+
145
+ This class adds MCP logging capability and automatically sets up
146
+ a custom logging handler to send log messages to MCP clients.
147
+ Also implements the logging/setLevel MCP method.
148
+ """
149
+
150
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
151
+ # Enable logging capability - now properly supported in chuk-mcp-server
152
+ # Fixed empty capability object issue in chuk-mcp-server
153
+ kwargs.setdefault("logging", True)
154
+
155
+ super().__init__(*args, **kwargs)
156
+
157
+ # Track server events
158
+ self.server_events: list[dict[str, Any]] = []
159
+
160
+ # Server logger
161
+ self.server_logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
162
+
163
+ # Set up custom MCP logging handler
164
+ self.mcp_logging_handler = MCPLoggingHandler(self)
165
+ self.setup_logging()
166
+
167
+ # Current logging level for MCP clients
168
+ self.mcp_logging_level = "INFO"
169
+
170
+ def setup_logging(self) -> None:
171
+ """Set up the MCP logging system."""
172
+ # Get the root logger for the chuk_mcp_server package
173
+ mcp_logger = logging.getLogger("chuk_mcp_server")
174
+
175
+ # Add our custom handler
176
+ mcp_logger.addHandler(self.mcp_logging_handler)
38
177
 
39
- def _capture_plot_output(func, *args, **kwargs):
178
+ # Also add to the plotext_plus logger
179
+ plotext_logger = logging.getLogger(__name__)
180
+ plotext_logger.addHandler(self.mcp_logging_handler)
181
+
182
+ # Add to our global logger
183
+ _logger.addHandler(self.mcp_logging_handler)
184
+
185
+ self.server_logger.info("🔊 MCP Logging system initialized")
186
+
187
+ def log_server_event(
188
+ self, event_type: str, message: str, data: dict[str, Any] | None = None
189
+ ) -> None:
190
+ """Log a server event that will be sent to MCP clients."""
191
+ from datetime import datetime
192
+
193
+ event = {
194
+ "type": event_type,
195
+ "message": message,
196
+ "timestamp": datetime.now().isoformat(),
197
+ "data": data or {},
198
+ }
199
+
200
+ self.server_events.append(event)
201
+
202
+ # Log through Python logging system (will trigger MCP notification)
203
+ self.server_logger.info(
204
+ f"[{event_type.upper()}] {message}", extra={"mcp_data": data}
205
+ )
206
+
207
+ def get_log_notifications(self) -> list[dict[str, Any]]:
208
+ """Get queued log notifications (for testing/debugging)."""
209
+ return self.mcp_logging_handler.notification_queue.copy()
210
+
211
+ def run_stdio(self, debug: bool | None = None) -> Any:
212
+ """Override run_stdio to start the server with logging support."""
213
+ # logging/setLevel is now natively supported in chuk-mcp-server
214
+ result = super().run_stdio(debug)
215
+ return result
216
+
217
+ def run(self, host: str | None = None, port: int | None = None, debug: bool | None = None) -> Any:
218
+ """Override run to start the server with logging support."""
219
+ # logging/setLevel is now natively supported in chuk-mcp-server
220
+ result = super().run(host, port, debug)
221
+ return result
222
+
223
+
224
+ def _capture_plot_output(func: Any, *args: Any, **kwargs: Any) -> tuple[Any, str]:
40
225
  """Capture plot output and return as string"""
41
226
  # Save current stdout
42
227
  old_stdout = sys.stdout
43
-
228
+
44
229
  try:
45
230
  # Redirect stdout to capture plot output
46
231
  sys.stdout = _current_plot_buffer
@@ -48,6 +233,20 @@ def _capture_plot_output(func, *args, **kwargs):
48
233
  plot_output = _current_plot_buffer.getvalue()
49
234
  _current_plot_buffer.truncate(0)
50
235
  _current_plot_buffer.seek(0)
236
+
237
+ # Fix: Add zero-width space character to preserve formatting in MCP CLI
238
+ lines = plot_output.split("\n")
239
+ fixed_lines = []
240
+ for line in lines:
241
+ if line.strip(): # Only process non-empty lines
242
+ # Add a zero-width space (\u200b) at the end to prevent trimming
243
+ fixed_line = line + "\u200b"
244
+ fixed_lines.append(fixed_line)
245
+ else:
246
+ fixed_lines.append(line)
247
+
248
+ plot_output = "\n".join(fixed_lines)
249
+
51
250
  return result, plot_output
52
251
  finally:
53
252
  # Restore stdout
@@ -56,140 +255,273 @@ def _capture_plot_output(func, *args, **kwargs):
56
255
 
57
256
  # Core Plotting Tools
58
257
  @tool
59
- async def scatter_plot(x: List[Union[int, float]], y: List[Union[int, float]],
60
- marker: Optional[str] = None, color: Optional[str] = None,
61
- title: Optional[str] = None) -> str:
258
+ async def scatter_plot(
259
+ x: list[int | float | str],
260
+ y: list[int | float | str],
261
+ marker: str | None = None,
262
+ color: str | None = None,
263
+ title: str | None = None,
264
+ theme_name: str | None = None,
265
+ ) -> str:
62
266
  """Create a scatter plot with given x and y data points.
63
-
267
+
64
268
  Args:
65
- x: List of x-coordinates
66
- y: List of y-coordinates
269
+ x: List of x-coordinates (numbers, dates, or strings)
270
+ y: List of y-coordinates (numbers or strings)
67
271
  marker: Marker style (optional)
68
272
  color: Plot color (optional)
69
273
  title: Plot title (optional)
70
-
274
+ theme_name: Theme to apply (optional)
275
+
71
276
  Returns:
72
277
  The rendered plot as text
73
278
  """
74
- # Convert string inputs to float
75
- x_numeric = [float(val) if isinstance(val, str) else val for val in x]
76
- y_numeric = [float(val) if isinstance(val, str) else val for val in y]
77
-
279
+ # Process x-coordinates - handle dates, numbers, and strings (same logic as line_plot)
280
+ try:
281
+ x_processed = []
282
+ for i, val in enumerate(x):
283
+ if isinstance(val, str):
284
+ # Check if it's a date string
285
+ if "-" in val and len(val) >= 8: # Basic date format check
286
+ # For date strings, use index position as numeric value
287
+ x_processed.append(i)
288
+ else:
289
+ # Try to convert to float, fallback to index
290
+ try:
291
+ x_processed.append(float(val))
292
+ except ValueError:
293
+ x_processed.append(i)
294
+ else:
295
+ x_processed.append(float(val))
296
+
297
+ # Convert y values to numeric
298
+ y_numeric = [float(val) if isinstance(val, str) else val for val in y]
299
+
300
+ _logger.debug(
301
+ f"Processed scatter data: x_range=[{min(x_processed):.2f}..{max(x_processed):.2f}], y_range=[{min(y_numeric):.2f}..{max(y_numeric):.2f}]"
302
+ )
303
+ except Exception as e:
304
+ _logger.error(f"Error processing scatter plot inputs: {e}")
305
+ raise
306
+
78
307
  plotting.clear_figure()
308
+
309
+ # Apply theme if specified
310
+ if theme_name and theme_name != "default":
311
+ _core.theme(theme_name)
312
+ _logger.debug(f"Applied theme: {theme_name}")
313
+
79
314
  if title:
80
315
  plotting.title(title)
81
-
82
- _, output = _capture_plot_output(plotting.scatter, x_numeric, y_numeric, marker=marker, color=color)
316
+
317
+ # Set custom x-axis labels for dates if needed (same logic as line_plot)
318
+ if any(isinstance(val, str) and "-" in val and len(val) >= 8 for val in x):
319
+ # We have date strings, set them as x-axis labels
320
+ date_labels = [
321
+ str(val) if isinstance(val, str) and "-" in val else str(val) for val in x
322
+ ]
323
+ _core.xticks(x_processed, date_labels)
324
+
325
+ _, output = _capture_plot_output(
326
+ plotting.scatter, x_processed, y_numeric, marker=marker, color=color
327
+ )
83
328
  _, show_output = _capture_plot_output(plotting.show)
84
-
329
+
85
330
  return output + show_output
86
331
 
87
332
 
88
333
  @tool
89
- async def line_plot(x: List[Union[int, float]], y: List[Union[int, float]],
90
- color: Optional[str] = None, title: Optional[str] = None) -> str:
334
+ async def line_plot(
335
+ x: list[int | float | str],
336
+ y: list[int | float | str],
337
+ color: str | None = None,
338
+ title: str | None = None,
339
+ theme_name: str | None = None,
340
+ ) -> str:
91
341
  """Create a line plot with given x and y data points.
92
-
342
+
93
343
  Args:
94
- x: List of x-coordinates
95
- y: List of y-coordinates
344
+ x: List of x-coordinates (numbers, dates, or strings)
345
+ y: List of y-coordinates (numbers or strings)
96
346
  color: Line color (optional)
97
347
  title: Plot title (optional)
98
-
348
+ theme_name: Theme to apply (optional)
349
+
99
350
  Returns:
100
351
  The rendered plot as text
101
352
  """
102
- import sys
103
- print(f"DEBUG: line_plot called with x={x}, y={y}, title={title}", file=sys.stderr)
104
-
105
- # Convert string inputs to float
353
+ _logger.info(
354
+ f"Creating line plot with {len(x)} data points, title='{title}', color='{color}'"
355
+ )
356
+
357
+ # Process x-coordinates - handle dates, numbers, and strings
106
358
  try:
107
- x_numeric = [float(val) if isinstance(val, str) else val for val in x]
359
+ x_processed = []
360
+ for i, val in enumerate(x):
361
+ if isinstance(val, str):
362
+ # Check if it's a date string
363
+ if "-" in val and len(val) >= 8: # Basic date format check
364
+ # For date strings, use index position as numeric value
365
+ x_processed.append(i)
366
+ else:
367
+ # Try to convert to float, fallback to index
368
+ try:
369
+ x_processed.append(float(val))
370
+ except ValueError:
371
+ x_processed.append(i)
372
+ else:
373
+ x_processed.append(float(val))
374
+
375
+ # Convert y values to numeric
108
376
  y_numeric = [float(val) if isinstance(val, str) else val for val in y]
109
- print(f"DEBUG: Converted to x_numeric={x_numeric}, y_numeric={y_numeric}", file=sys.stderr)
377
+
378
+ _logger.debug(
379
+ f"Processed data: x_range=[{min(x_processed):.2f}..{max(x_processed):.2f}], y_range=[{min(y_numeric):.2f}..{max(y_numeric):.2f}]"
380
+ )
110
381
  except Exception as e:
111
- print(f"DEBUG: Error converting inputs: {e}", file=sys.stderr)
382
+ _logger.error(f"Error processing inputs: {e}")
112
383
  raise
113
-
384
+
114
385
  try:
115
386
  plotting.clear_figure()
116
- print("DEBUG: Cleared figure", file=sys.stderr)
117
-
387
+ _logger.debug("Cleared figure")
388
+
389
+ # Apply theme if specified
390
+ if theme_name and theme_name != "default":
391
+ _core.theme(theme_name)
392
+ _logger.debug(f"Applied theme: {theme_name}")
393
+
118
394
  if title:
119
395
  plotting.title(title)
120
- print(f"DEBUG: Set title: {title}", file=sys.stderr)
121
-
122
- _, output = _capture_plot_output(plotting.plot, x_numeric, y_numeric, color=color)
123
- print(f"DEBUG: Generated plot output, length: {len(output)}", file=sys.stderr)
124
-
396
+ _logger.debug(f"Set plot title: {title}")
397
+
398
+ # Set custom x-axis labels for dates if needed
399
+ if any(isinstance(val, str) and "-" in val and len(val) >= 8 for val in x):
400
+ # We have date strings, set them as x-axis labels
401
+ date_labels = [
402
+ str(val) if isinstance(val, str) and "-" in val else str(val)
403
+ for val in x
404
+ ]
405
+ _core.xticks(x_processed, date_labels)
406
+
407
+ _, output = _capture_plot_output(
408
+ plotting.plot, x_processed, y_numeric, color=color
409
+ )
410
+ _logger.debug(f"Generated plot output ({len(output)} characters)")
411
+
125
412
  _, show_output = _capture_plot_output(plotting.show)
126
- print(f"DEBUG: Generated show output, length: {len(show_output)}", file=sys.stderr)
127
-
413
+ _logger.debug(f"Generated show output ({len(show_output)} characters)")
414
+
128
415
  result = output + show_output
129
- print(f"DEBUG: Returning result, total length: {len(result)}", file=sys.stderr)
416
+
417
+ # Fix: Add zero-width space character to preserve formatting in MCP CLI
418
+ # This prevents MCP CLI from stripping trailing spaces/borders
419
+ lines = result.split("\n")
420
+ fixed_lines = []
421
+ for line in lines:
422
+ if line.strip(): # Only process non-empty lines
423
+ # Add a zero-width space (\u200b) at the end to prevent trimming
424
+ fixed_line = line + "\u200b"
425
+ fixed_lines.append(fixed_line)
426
+ else:
427
+ fixed_lines.append(line)
428
+
429
+ result = "\n".join(fixed_lines)
430
+ _logger.debug(
431
+ f"Applied formatting fix, final result ({len(result)} characters)"
432
+ )
433
+ _logger.info("Line plot created successfully")
434
+
130
435
  return result
131
-
436
+
132
437
  except Exception as e:
133
- print(f"DEBUG: Error during plotting: {e}", file=sys.stderr)
438
+ _logger.error(f"Error during line plot creation: {e}")
134
439
  import traceback
135
- traceback.print_exc(file=sys.stderr)
440
+
441
+ _logger.debug(f"Stack trace: {traceback.format_exc()}")
136
442
  raise
137
443
 
138
444
 
139
445
  @tool
140
- async def bar_chart(labels: List[str], values: List[Union[int, float]],
141
- color: Optional[str] = None, title: Optional[str] = None) -> str:
446
+ async def bar_chart(
447
+ labels: list[str],
448
+ values: list[int | float],
449
+ color: str | None = None,
450
+ title: str | None = None,
451
+ theme_name: str | None = None,
452
+ ) -> str:
142
453
  """Create a bar chart with given labels and values.
143
-
454
+
144
455
  Args:
145
456
  labels: List of bar labels
146
457
  values: List of bar values
147
458
  color: Bar color (optional)
148
459
  title: Plot title (optional)
149
-
460
+ theme_name: Theme to apply (optional)
461
+
150
462
  Returns:
151
463
  The rendered plot as text
152
464
  """
153
465
  # Convert string inputs to float
154
466
  values_numeric = [float(val) if isinstance(val, str) else val for val in values]
155
-
467
+
156
468
  plotting.clear_figure()
469
+
470
+ # Apply theme if specified
471
+ if theme_name and theme_name != "default":
472
+ plotting.theme(theme_name)
473
+
157
474
  if title:
158
475
  plotting.title(title)
159
-
476
+
160
477
  _, output = _capture_plot_output(plotting.bar, labels, values_numeric, color=color)
161
478
  _, show_output = _capture_plot_output(plotting.show)
162
-
479
+
163
480
  return output + show_output
164
481
 
165
482
 
166
483
  @tool
167
- async def matrix_plot(data: List[List[Union[int, float]]], title: Optional[str] = None) -> str:
484
+ async def matrix_plot(
485
+ data: list[list[int | float]],
486
+ title: str | None = None,
487
+ theme_name: str | None = None,
488
+ ) -> str:
168
489
  """Create a matrix/heatmap plot from 2D data.
169
-
490
+
170
491
  Args:
171
492
  data: 2D list representing matrix data
172
493
  title: Plot title (optional)
173
-
494
+ theme_name: Theme to apply (optional)
495
+
174
496
  Returns:
175
497
  The rendered plot as text
176
498
  """
177
499
  plotting.clear_figure()
500
+
501
+ # Apply theme if specified
502
+ if theme_name and theme_name != "default":
503
+ plotting.theme(theme_name)
504
+
178
505
  if title:
179
506
  plotting.title(title)
180
-
507
+
181
508
  _, output = _capture_plot_output(plotting.matrix_plot, data)
182
509
  _, show_output = _capture_plot_output(plotting.show)
183
-
510
+
184
511
  return output + show_output
185
512
 
186
513
 
187
514
  @tool
188
- async def image_plot(image_path: str, title: Optional[str] = None,
189
- marker: Optional[str] = None, style: Optional[str] = None,
190
- fast: bool = False, grayscale: bool = False) -> str:
515
+ async def image_plot(
516
+ image_path: str,
517
+ title: str | None = None,
518
+ marker: str | None = None,
519
+ style: str | None = None,
520
+ fast: bool = False,
521
+ grayscale: bool = False,
522
+ ) -> str:
191
523
  """Display an image in the terminal using ASCII art.
192
-
524
+
193
525
  Args:
194
526
  image_path: Path to the image file to display
195
527
  title: Plot title (optional)
@@ -197,114 +529,234 @@ async def image_plot(image_path: str, title: Optional[str] = None,
197
529
  style: Style for image rendering (optional)
198
530
  fast: Enable fast rendering mode for better performance (optional)
199
531
  grayscale: Render image in grayscale (optional)
200
-
532
+
201
533
  Returns:
202
534
  The rendered image plot as text
203
535
  """
204
536
  plotting.clear_figure()
205
537
  if title:
206
538
  plotting.title(title)
207
-
208
- _, output = _capture_plot_output(plotting.image_plot, image_path,
209
- marker=marker, style=style,
210
- fast=fast, grayscale=grayscale)
539
+
540
+ _, output = _capture_plot_output(
541
+ plotting.image_plot,
542
+ image_path,
543
+ marker=marker,
544
+ style=style,
545
+ fast=fast,
546
+ grayscale=grayscale,
547
+ )
211
548
  _, show_output = _capture_plot_output(plotting.show)
212
-
549
+
213
550
  return output + show_output
214
551
 
215
552
 
216
553
  @tool
217
554
  async def play_gif(gif_path: str) -> str:
218
555
  """Play a GIF animation in the terminal.
219
-
556
+
220
557
  Args:
221
558
  gif_path: Path to the GIF file to play
222
-
559
+
223
560
  Returns:
224
561
  Confirmation message (GIF plays automatically)
225
562
  """
226
563
  plotting.clear_figure()
227
-
564
+
228
565
  # play_gif handles its own output and doesn't need show()
229
566
  plotting.play_gif(gif_path)
230
-
567
+
231
568
  return f"Playing GIF: {gif_path}"
232
569
 
233
570
 
234
571
  # Chart Class Tools
235
572
  @tool
236
- async def quick_scatter(x: List[Union[int, float]], y: List[Union[int, float]],
237
- title: Optional[str] = None, theme_name: Optional[str] = None) -> str:
573
+ async def quick_scatter(
574
+ x: list[int | float | str],
575
+ y: list[int | float | str],
576
+ title: str | None = None,
577
+ theme_name: str | None = None,
578
+ ) -> str:
238
579
  """Create a quick scatter chart using the chart classes API.
239
-
580
+
240
581
  Args:
241
- x: List of x-coordinates
242
- y: List of y-coordinates
582
+ x: List of x-coordinates (numbers, dates, or strings)
583
+ y: List of y-coordinates (numbers or strings)
243
584
  title: Chart title (optional)
244
585
  theme_name: Theme to apply (optional)
245
-
586
+
246
587
  Returns:
247
588
  The rendered chart as text
248
589
  """
249
- # Convert string inputs to float
250
- x_numeric = [float(val) if isinstance(val, str) else val for val in x]
251
- y_numeric = [float(val) if isinstance(val, str) else val for val in y]
252
-
253
- _, output = _capture_plot_output(charts.quick_scatter, x_numeric, y_numeric, title=title, theme=theme_name)
254
- return output
590
+ # Process x-coordinates - handle dates, numbers, and strings (same logic as line_plot)
591
+ try:
592
+ x_processed = []
593
+ for i, val in enumerate(x):
594
+ if isinstance(val, str):
595
+ # Check if it's a date string
596
+ if "-" in val and len(val) >= 8: # Basic date format check
597
+ # For date strings, use index position as numeric value
598
+ x_processed.append(i)
599
+ else:
600
+ # Try to convert to float, fallback to index
601
+ try:
602
+ x_processed.append(float(val))
603
+ except ValueError:
604
+ x_processed.append(i)
605
+ else:
606
+ x_processed.append(float(val))
607
+
608
+ # Convert y values to numeric
609
+ y_processed = []
610
+ for val in y:
611
+ if isinstance(val, str):
612
+ try:
613
+ y_processed.append(float(val))
614
+ except ValueError as e:
615
+ # If string can't be converted to number, skip this point
616
+ raise ValueError(
617
+ f"Y-coordinate '{val}' cannot be converted to a number"
618
+ ) from e
619
+ else:
620
+ y_processed.append(float(val))
621
+
622
+ _, output = _capture_plot_output(
623
+ charts.quick_scatter,
624
+ x_processed,
625
+ y_processed,
626
+ title=title,
627
+ theme_name=theme_name,
628
+ )
629
+ return output
630
+
631
+ except Exception as e:
632
+ _logger.error(f"Error during quick_scatter creation: {e}")
633
+ import traceback
634
+
635
+ _logger.debug(f"Stack trace: {traceback.format_exc()}")
636
+ raise
255
637
 
256
638
 
257
639
  @tool
258
- async def quick_line(x: List[Union[int, float]], y: List[Union[int, float]],
259
- title: Optional[str] = None, theme_name: Optional[str] = None) -> str:
640
+ async def quick_line(
641
+ x: list[int | float | str],
642
+ y: list[int | float | str],
643
+ title: str | None = None,
644
+ theme_name: str | None = None,
645
+ ) -> str:
260
646
  """Create a quick line chart using the chart classes API.
261
-
647
+
262
648
  Args:
263
- x: List of x-coordinates
264
- y: List of y-coordinates
649
+ x: List of x-coordinates (numbers, dates, or strings)
650
+ y: List of y-coordinates (numbers or strings)
265
651
  title: Chart title (optional)
266
652
  theme_name: Theme to apply (optional)
267
-
653
+
268
654
  Returns:
269
655
  The rendered chart as text
270
656
  """
271
- # Convert string inputs to float
272
- x_numeric = [float(val) if isinstance(val, str) else val for val in x]
273
- y_numeric = [float(val) if isinstance(val, str) else val for val in y]
274
-
275
- _, output = _capture_plot_output(charts.quick_line, x_numeric, y_numeric, title=title, theme=theme_name)
657
+ # Process x-coordinates - handle dates, numbers, and strings (same logic as line_plot)
658
+ try:
659
+ x_processed = []
660
+ for i, val in enumerate(x):
661
+ if isinstance(val, str):
662
+ # Check if it's a date string
663
+ if "-" in val and len(val) >= 8: # Basic date format check
664
+ # For date strings, use index position as numeric value
665
+ x_processed.append(i)
666
+ else:
667
+ # Try to convert to float, fallback to index
668
+ try:
669
+ x_processed.append(float(val))
670
+ except ValueError:
671
+ x_processed.append(i)
672
+ else:
673
+ x_processed.append(float(val))
674
+
675
+ # Convert y values to numeric
676
+ y_numeric = [float(val) if isinstance(val, str) else val for val in y]
677
+
678
+ _logger.debug(
679
+ f"Processed quick_line data: x_range=[{min(x_processed):.2f}..{max(x_processed):.2f}], y_range=[{min(y_numeric):.2f}..{max(y_numeric):.2f}]"
680
+ )
681
+ except Exception as e:
682
+ _logger.error(f"Error processing quick_line inputs: {e}")
683
+ raise
684
+
685
+ _, output = _capture_plot_output(
686
+ charts.quick_line, x_processed, y_numeric, title=title, theme_name=theme_name
687
+ )
276
688
  return output
277
689
 
278
690
 
279
691
  @tool
280
- async def quick_bar(labels: List[str], values: List[Union[int, float]],
281
- title: Optional[str] = None, theme_name: Optional[str] = None) -> str:
692
+ async def quick_bar(
693
+ labels: list[str],
694
+ values: list[int | float],
695
+ title: str | None = None,
696
+ horizontal: bool = False,
697
+ theme_name: str | None = None,
698
+ ) -> str:
282
699
  """Create a quick bar chart using the chart classes API.
283
-
700
+
284
701
  Args:
285
702
  labels: List of bar labels
286
703
  values: List of bar values
287
704
  title: Chart title (optional)
705
+ horizontal: Create horizontal bars if True (optional, default False)
288
706
  theme_name: Theme to apply (optional)
289
-
707
+
290
708
  Returns:
291
709
  The rendered chart as text
292
710
  """
711
+ _logger.info(
712
+ f"Creating quick bar chart with {len(labels)} labels, title='{title}', horizontal={horizontal}"
713
+ )
714
+
293
715
  # Convert string inputs to float
294
- values_numeric = [float(val) if isinstance(val, str) else val for val in values]
295
-
296
- _, output = _capture_plot_output(charts.quick_bar, labels, values_numeric, title=title, theme=theme_name)
297
- return output
716
+ try:
717
+ values_numeric = [float(val) if isinstance(val, str) else val for val in values]
718
+ _logger.debug(
719
+ f"Converted values: {len(values_numeric)} numeric values, range=[{min(values_numeric):.2f}..{max(values_numeric):.2f}]"
720
+ )
721
+ except Exception as e:
722
+ _logger.error(f"Error converting values to numeric: {e}")
723
+ raise
724
+
725
+ try:
726
+ _, output = _capture_plot_output(
727
+ charts.quick_bar,
728
+ labels,
729
+ values_numeric,
730
+ title=title,
731
+ horizontal=horizontal,
732
+ theme_name=theme_name,
733
+ )
734
+ _logger.debug(f"Generated quick bar chart output ({len(output)} characters)")
735
+ _logger.info("Quick bar chart created successfully")
736
+ return output
737
+ except Exception as e:
738
+ _logger.error(f"Error during quick bar chart creation: {e}")
739
+ import traceback
740
+
741
+ _logger.debug(f"Stack trace: {traceback.format_exc()}")
742
+ raise
298
743
 
299
744
 
300
745
  @tool
301
- async def quick_pie(labels: List[str], values: List[Union[int, float]],
302
- colors: Optional[List[str]] = None, title: Optional[str] = None,
303
- show_values: bool = True, show_percentages: bool = True,
304
- show_values_on_slices: bool = False, donut: bool = False,
305
- remaining_color: Optional[str] = None) -> str:
746
+ async def quick_pie(
747
+ labels: list[str],
748
+ values: list[int | float],
749
+ colors: list[str] | None = None,
750
+ title: str | None = None,
751
+ show_values: bool = True,
752
+ show_percentages: bool = True,
753
+ show_values_on_slices: bool = False,
754
+ donut: bool = False,
755
+ remaining_color: str | None = None,
756
+ theme_name: str | None = None,
757
+ ) -> str:
306
758
  """Create a quick pie chart using the chart classes API.
307
-
759
+
308
760
  Args:
309
761
  labels: List of pie segment labels
310
762
  values: List of pie segment values
@@ -315,29 +767,44 @@ async def quick_pie(labels: List[str], values: List[Union[int, float]],
315
767
  show_values_on_slices: Show values directly on pie slices (optional, default False)
316
768
  donut: Create doughnut chart with hollow center (optional, default False)
317
769
  remaining_color: Color for remaining slice in single-value charts (optional)
318
-
770
+ theme_name: Theme to apply (optional)
771
+
319
772
  Returns:
320
773
  The rendered pie chart as text
321
774
  """
322
775
  # Convert string inputs to float
323
776
  values_numeric = [float(val) if isinstance(val, str) else val for val in values]
324
-
325
- _, output = _capture_plot_output(charts.quick_pie, labels, values_numeric, colors=colors,
326
- title=title, show_values=show_values,
327
- show_percentages=show_percentages,
328
- show_values_on_slices=show_values_on_slices,
329
- donut=donut, remaining_color=remaining_color)
777
+
778
+ _, output = _capture_plot_output(
779
+ charts.quick_pie,
780
+ labels,
781
+ values_numeric,
782
+ colors=colors,
783
+ title=title,
784
+ show_values=show_values,
785
+ show_percentages=show_percentages,
786
+ show_values_on_slices=show_values_on_slices,
787
+ donut=donut,
788
+ remaining_color=remaining_color,
789
+ theme_name=theme_name,
790
+ )
330
791
  return output
331
792
 
332
793
 
333
794
  @tool
334
- async def quick_donut(labels: List[str], values: List[Union[int, float]],
335
- colors: Optional[List[str]] = None, title: Optional[str] = None,
336
- show_values: bool = True, show_percentages: bool = True,
337
- show_values_on_slices: bool = False,
338
- remaining_color: Optional[str] = None) -> str:
795
+ async def quick_donut(
796
+ labels: list[str],
797
+ values: list[int | float],
798
+ colors: list[str] | None = None,
799
+ title: str | None = None,
800
+ show_values: bool = True,
801
+ show_percentages: bool = True,
802
+ show_values_on_slices: bool = False,
803
+ remaining_color: str | None = None,
804
+ theme_name: str | None = None,
805
+ ) -> str:
339
806
  """Create a quick doughnut chart (pie chart with hollow center) using the chart classes API.
340
-
807
+
341
808
  Args:
342
809
  labels: List of pie segment labels
343
810
  values: List of pie segment values
@@ -347,45 +814,56 @@ async def quick_donut(labels: List[str], values: List[Union[int, float]],
347
814
  show_percentages: Show percentages in legend (optional, default True)
348
815
  show_values_on_slices: Show values directly on pie slices (optional, default False)
349
816
  remaining_color: Color for remaining slice in single-value charts (optional)
350
-
817
+ theme_name: Theme to apply (optional)
818
+
351
819
  Returns:
352
820
  The rendered doughnut chart as text
353
821
  """
354
822
  # Convert string inputs to float
355
823
  values_numeric = [float(val) if isinstance(val, str) else val for val in values]
356
-
357
- _, output = _capture_plot_output(charts.quick_donut, labels, values_numeric, colors=colors,
358
- title=title, show_values=show_values,
359
- show_percentages=show_percentages,
360
- show_values_on_slices=show_values_on_slices,
361
- remaining_color=remaining_color)
824
+
825
+ _, output = _capture_plot_output(
826
+ charts.quick_donut,
827
+ labels,
828
+ values_numeric,
829
+ colors=colors,
830
+ title=title,
831
+ show_values=show_values,
832
+ show_percentages=show_percentages,
833
+ show_values_on_slices=show_values_on_slices,
834
+ remaining_color=remaining_color,
835
+ theme_name=theme_name,
836
+ )
362
837
  return output
363
838
 
364
839
 
365
840
  # Theme Tools
366
841
  @tool
367
- async def get_available_themes() -> Dict[str, Any]:
842
+ async def get_available_themes() -> dict[str, Any]:
368
843
  """Get information about available themes.
369
-
844
+
370
845
  Returns:
371
846
  Dictionary containing theme information
372
847
  """
373
848
  from .themes import get_theme_info
849
+
374
850
  return get_theme_info()
375
851
 
376
852
 
377
853
  @tool
378
854
  async def apply_plot_theme(theme_name: str) -> str:
379
855
  """Apply a theme to the current plot.
380
-
856
+
381
857
  Args:
382
858
  theme_name: Name of the theme to apply
383
-
859
+
384
860
  Returns:
385
861
  Confirmation message
386
862
  """
863
+ _logger.info(f"Applying plot theme: {theme_name}")
387
864
  plotting.clear_figure()
388
865
  plotting.theme(theme_name)
866
+ _logger.debug(f"Theme '{theme_name}' applied successfully")
389
867
  return f"Applied theme: {theme_name}"
390
868
 
391
869
 
@@ -393,7 +871,7 @@ async def apply_plot_theme(theme_name: str) -> str:
393
871
  @tool
394
872
  async def get_terminal_width() -> int:
395
873
  """Get the current terminal width.
396
-
874
+
397
875
  Returns:
398
876
  Terminal width in characters
399
877
  """
@@ -403,11 +881,11 @@ async def get_terminal_width() -> int:
403
881
  @tool
404
882
  async def colorize_text(text: str, color: str) -> str:
405
883
  """Apply color formatting to text.
406
-
884
+
407
885
  Args:
408
886
  text: Text to colorize
409
887
  color: Color name or code
410
-
888
+
411
889
  Returns:
412
890
  Colorized text
413
891
  """
@@ -417,10 +895,10 @@ async def colorize_text(text: str, color: str) -> str:
417
895
  @tool
418
896
  async def log_info(message: str) -> str:
419
897
  """Log an informational message.
420
-
898
+
421
899
  Args:
422
900
  message: Message to log
423
-
901
+
424
902
  Returns:
425
903
  Formatted log message
426
904
  """
@@ -431,10 +909,10 @@ async def log_info(message: str) -> str:
431
909
  @tool
432
910
  async def log_success(message: str) -> str:
433
911
  """Log a success message.
434
-
912
+
435
913
  Args:
436
914
  message: Message to log
437
-
915
+
438
916
  Returns:
439
917
  Formatted log message
440
918
  """
@@ -445,10 +923,10 @@ async def log_success(message: str) -> str:
445
923
  @tool
446
924
  async def log_warning(message: str) -> str:
447
925
  """Log a warning message.
448
-
926
+
449
927
  Args:
450
928
  message: Message to log
451
-
929
+
452
930
  Returns:
453
931
  Formatted log message
454
932
  """
@@ -459,10 +937,10 @@ async def log_warning(message: str) -> str:
459
937
  @tool
460
938
  async def log_error(message: str) -> str:
461
939
  """Log an error message.
462
-
940
+
463
941
  Args:
464
942
  message: Message to log
465
-
943
+
466
944
  Returns:
467
945
  Formatted log message
468
946
  """
@@ -474,40 +952,106 @@ async def log_error(message: str) -> str:
474
952
  @tool
475
953
  async def set_plot_size(width: int, height: int) -> str:
476
954
  """Set the plot size.
477
-
955
+
478
956
  Args:
479
957
  width: Plot width
480
958
  height: Plot height
481
-
959
+
482
960
  Returns:
483
961
  Confirmation message
484
962
  """
485
- plotting.plotsize(width, height)
486
- return f"Plot size set to {width}x{height}"
963
+ try:
964
+ # Avoid potential logging issues during STDIO mode
965
+ if _logger.level <= logging.INFO:
966
+ _logger.info(f"Setting plot size to {width}x{height}")
967
+
968
+ # Input validation
969
+ if width <= 0 or height <= 0:
970
+ raise ValueError(
971
+ f"Width and height must be positive integers. Got width={width}, height={height}"
972
+ )
973
+
974
+ if width > 1000 or height > 1000 and _logger.level <= logging.WARNING:
975
+ _logger.warning(f"Large plot size requested: {width}x{height}")
976
+
977
+ # Check if plotting.plotsize is available
978
+ if not hasattr(plotting, "plotsize"):
979
+ raise AttributeError("plotting.plotsize function is not available")
980
+
981
+ # Call the function directly (it's fast and shouldn't cause issues)
982
+ plotting.plotsize(width, height)
983
+
984
+ if _logger.level <= logging.DEBUG:
985
+ _logger.debug(f"Plot size successfully set to {width}x{height}")
986
+
987
+ # Ensure immediate return
988
+ result = f"Plot size set to {width}x{height}"
989
+ sys.stdout.flush() # Force flush stdout
990
+ return result
991
+ except Exception as e:
992
+ if _logger.level <= logging.ERROR:
993
+ _logger.error(f"Error setting plot size: {e}")
994
+ import traceback
995
+
996
+ if _logger.level <= logging.DEBUG:
997
+ _logger.debug(f"Stack trace: {traceback.format_exc()}")
998
+ raise
487
999
 
488
1000
 
489
1001
  @tool
490
- async def enable_banner_mode(enabled: bool = True, title: Optional[str] = None,
491
- subtitle: Optional[str] = None) -> str:
1002
+ async def enable_banner_mode(
1003
+ enabled: bool = True, title: str | None = None, subtitle: str | None = None
1004
+ ) -> str:
492
1005
  """Enable or disable banner mode.
493
-
1006
+
494
1007
  Args:
495
1008
  enabled: Whether to enable banner mode
496
1009
  title: Banner title (optional)
497
- subtitle: Banner subtitle (optional)
498
-
1010
+ subtitle: Banner subtitle (optional, will be appended to title)
1011
+
499
1012
  Returns:
500
1013
  Confirmation message
501
1014
  """
502
- plotting.banner_mode(enabled, title=title, subtitle=subtitle)
503
- status = "enabled" if enabled else "disabled"
504
- return f"Banner mode {status}"
1015
+ try:
1016
+ # Avoid potential logging issues during STDIO mode
1017
+ if _logger.level <= logging.INFO:
1018
+ _logger.info(
1019
+ f"Setting banner mode: enabled={enabled}, title='{title}', subtitle='{subtitle}'"
1020
+ )
1021
+
1022
+ # Combine title and subtitle since banner_mode only accepts title parameter
1023
+ combined_title = title
1024
+ if subtitle:
1025
+ combined_title = f"{title} - {subtitle}" if title else subtitle
1026
+
1027
+ # Call the function directly (it's fast and shouldn't cause issues)
1028
+ plotting.banner_mode(enabled, title=combined_title)
1029
+
1030
+ status = "enabled" if enabled else "disabled"
1031
+ response = f"Banner mode {status}"
1032
+ if combined_title:
1033
+ response += f" with title: '{combined_title}'"
1034
+
1035
+ if _logger.level <= logging.DEBUG:
1036
+ _logger.debug(f"Banner mode successfully {status}")
1037
+
1038
+ # Ensure immediate return
1039
+ sys.stdout.flush() # Force flush stdout
1040
+ return response
1041
+ except Exception as e:
1042
+ if _logger.level <= logging.ERROR:
1043
+ _logger.error(f"Error setting banner mode: {e}")
1044
+ import traceback
1045
+
1046
+ if _logger.level <= logging.DEBUG:
1047
+ _logger.debug(f"Stack trace: {traceback.format_exc()}")
1048
+ raise
505
1049
 
506
1050
 
507
1051
  @tool
508
1052
  async def clear_plot() -> str:
509
1053
  """Clear the current plot.
510
-
1054
+
511
1055
  Returns:
512
1056
  Confirmation message
513
1057
  """
@@ -517,65 +1061,89 @@ async def clear_plot() -> str:
517
1061
 
518
1062
  # Resource for plot configuration
519
1063
  @resource("config://plotext")
520
- async def get_plot_config() -> Dict[str, Any]:
1064
+ async def get_plot_config() -> dict[str, Any]:
521
1065
  """Get current plot configuration."""
522
1066
  from .themes import get_theme_info
1067
+
523
1068
  return {
524
1069
  "terminal_width": utilities.terminal_width(),
525
1070
  "available_themes": get_theme_info(),
526
1071
  "library_version": "plotext_plus",
527
- "mcp_enabled": True
1072
+ "mcp_enabled": True,
1073
+ "logging_enabled": True,
1074
+ }
1075
+
1076
+
1077
+ # Resource for logging information
1078
+ @resource("logs://recent")
1079
+ async def get_recent_logs() -> dict[str, Any]:
1080
+ """Get recent server events and log notifications (requires custom server)."""
1081
+ # This would work if we had access to the server instance
1082
+ # For now, return basic logging info
1083
+ return {
1084
+ "logging_enabled": True,
1085
+ "log_levels": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
1086
+ "mcp_notifications": "enabled",
1087
+ "timestamp": datetime.now().isoformat(),
528
1088
  }
529
1089
 
530
1090
 
531
1091
  # Resource for tool documentation/info
532
1092
  @resource("info://plotext")
533
- async def get_tool_info() -> Dict[str, Any]:
1093
+ async def get_tool_info() -> dict[str, Any]:
534
1094
  """Get comprehensive information about all available plotting tools."""
535
1095
  return {
536
1096
  "server_info": {
537
1097
  "name": "Plotext Plus MCP Server",
538
1098
  "description": "Model Context Protocol server for plotext_plus terminal plotting library",
539
1099
  "version": "1.0.0",
540
- "capabilities": ["plotting", "theming", "multimedia", "charts"]
1100
+ "capabilities": ["plotting", "theming", "multimedia", "charts"],
541
1101
  },
542
1102
  "plotting_tools": {
543
1103
  "scatter_plot": "Create scatter plots with x/y data points",
544
- "line_plot": "Create line plots for time series and continuous data",
1104
+ "line_plot": "Create line plots for time series and continuous data",
545
1105
  "bar_chart": "Create bar charts for categorical data",
546
1106
  "matrix_plot": "Create heatmaps from 2D matrix data",
547
1107
  "image_plot": "Display images in terminal using ASCII art",
548
- "play_gif": "Play animated GIFs in the terminal"
1108
+ "play_gif": "Play animated GIFs in the terminal",
549
1109
  },
550
1110
  "quick_chart_tools": {
551
1111
  "quick_scatter": "Quickly create scatter charts with theming",
552
1112
  "quick_line": "Quickly create line charts with theming",
553
- "quick_bar": "Quickly create bar charts with theming",
1113
+ "quick_bar": "Quickly create bar charts with theming",
554
1114
  "quick_pie": "Quickly create pie charts with custom colors, donut mode, and remaining_color options",
555
- "quick_donut": "Quickly create doughnut charts (hollow center pie charts)"
1115
+ "quick_donut": "Quickly create doughnut charts (hollow center pie charts)",
556
1116
  },
557
1117
  "theme_tools": {
558
1118
  "get_available_themes": "List all available color themes",
559
- "apply_plot_theme": "Apply a theme to plots"
1119
+ "apply_plot_theme": "Apply a theme to plots",
560
1120
  },
561
1121
  "utility_tools": {
562
1122
  "get_terminal_width": "Get current terminal width",
563
1123
  "colorize_text": "Apply colors to text output",
564
1124
  "log_info": "Output informational messages",
565
- "log_success": "Output success messages",
1125
+ "log_success": "Output success messages",
566
1126
  "log_warning": "Output warning messages",
567
- "log_error": "Output error messages"
1127
+ "log_error": "Output error messages",
568
1128
  },
569
1129
  "configuration_tools": {
570
1130
  "set_plot_size": "Set plot dimensions",
571
1131
  "enable_banner_mode": "Enable/disable banner mode",
572
- "clear_plot": "Clear current plot"
1132
+ "clear_plot": "Clear current plot",
573
1133
  },
574
1134
  "supported_formats": {
575
1135
  "image_formats": ["PNG", "JPG", "JPEG", "BMP", "GIF (static)"],
576
1136
  "gif_formats": ["GIF (animated)"],
577
- "chart_types": ["scatter", "line", "bar", "pie", "doughnut", "matrix/heatmap", "image"],
578
- "themes": "20+ built-in themes including solarized, dracula, cyberpunk"
1137
+ "chart_types": [
1138
+ "scatter",
1139
+ "line",
1140
+ "bar",
1141
+ "pie",
1142
+ "doughnut",
1143
+ "matrix/heatmap",
1144
+ "image",
1145
+ ],
1146
+ "themes": "20+ built-in themes including solarized, dracula, cyberpunk",
579
1147
  },
580
1148
  "usage_tips": {
581
1149
  "pie_charts": "Best for 3-7 categories, use full terminal dimensions",
@@ -583,8 +1151,8 @@ async def get_tool_info() -> Dict[str, Any]:
583
1151
  "single_value_charts": "Perfect for progress/completion rates: ['Complete', 'Remaining'] with 'default' color",
584
1152
  "images": "Use fast=True for better performance with large images",
585
1153
  "themes": "Apply themes before creating plots for consistent styling",
586
- "banners": "Enable banner mode for professional-looking outputs"
587
- }
1154
+ "banners": "Enable banner mode for professional-looking outputs",
1155
+ },
588
1156
  }
589
1157
 
590
1158
 
@@ -660,7 +1228,7 @@ async def colorized_output_prompt() -> str:
660
1228
  @prompt("regional_sales_analysis")
661
1229
  async def regional_sales_analysis_prompt() -> str:
662
1230
  """Data analysis workflow example"""
663
- return """I have sales data by region: East=[100,120,110], West=[80,95,105], North=[60,75,85], South=[90,100,115] over 3 quarters.
1231
+ return """I have sales data by region: East=[100,120,110], West=[80,95,105], North=[60,75,85], South=[90,100,115] over 3 quarters.
664
1232
 
665
1233
  Please:
666
1234
  1. Create individual plots for each region
@@ -673,7 +1241,7 @@ Please:
673
1241
  async def comparative_visualization_prompt() -> str:
674
1242
  """Comparative visualization example"""
675
1243
  return """Compare two datasets using multiple visualization types:
676
- - Dataset 1: [5,10,15,20,25]
1244
+ - Dataset 1: [5,10,15,20,25]
677
1245
  - Dataset 2: [3,8,18,22,28]
678
1246
  - Show both as scatter plot and line plot
679
1247
  - Use different colors and add meaningful titles"""
@@ -684,7 +1252,7 @@ async def error_handling_test_prompt() -> str:
684
1252
  """Error handling example"""
685
1253
  return """Try to create plots with various data scenarios and show how the system handles edge cases:
686
1254
  - Empty datasets
687
- - Mismatched array lengths
1255
+ - Mismatched array lengths
688
1256
  - Invalid color names
689
1257
  - Non-existent themes"""
690
1258
 
@@ -756,7 +1324,7 @@ async def multimedia_showcase_prompt() -> str:
756
1324
  async def basic_pie_chart_prompt() -> str:
757
1325
  """Basic pie chart example"""
758
1326
  return """Create a simple pie chart showing market share data:
759
- - Categories: ['iOS', 'Android', 'Windows', 'Other']
1327
+ - Categories: ['iOS', 'Android', 'Windows', 'Other']
760
1328
  - Values: [35, 45, 15, 5]
761
1329
  - Use colors: ['blue', 'green', 'orange', 'gray']
762
1330
  - Add title 'Mobile OS Market Share'
@@ -768,7 +1336,7 @@ async def pie_chart_styling_prompt() -> str:
768
1336
  """Advanced pie chart styling example"""
769
1337
  return """Create a styled pie chart with advanced features:
770
1338
  1. Use quick_pie with show_values_on_slices=True
771
- 2. Data: Budget categories ['Housing', 'Food', 'Transport', 'Entertainment']
1339
+ 2. Data: Budget categories ['Housing', 'Food', 'Transport', 'Entertainment']
772
1340
  3. Values: [1200, 400, 300, 200] (monthly budget)
773
1341
  4. Custom colors for each category
774
1342
  5. Add meaningful title and ensure full terminal usage"""
@@ -779,7 +1347,7 @@ async def pie_chart_comparison_prompt() -> str:
779
1347
  """Pie chart comparison example"""
780
1348
  return """Create multiple pie charts for comparison:
781
1349
  1. Q1 Sales: ['Product A', 'Product B', 'Product C'] = [30, 45, 25]
782
- 2. Q2 Sales: ['Product A', 'Product B', 'Product C'] = [25, 50, 25]
1350
+ 2. Q2 Sales: ['Product A', 'Product B', 'Product C'] = [25, 50, 25]
783
1351
  3. Show both charts with different colors
784
1352
  4. Use appropriate titles ('Q1 Sales Distribution', 'Q2 Sales Distribution')
785
1353
  5. Discuss the trends visible in the comparison"""
@@ -791,7 +1359,7 @@ async def pie_chart_best_practices_prompt() -> str:
791
1359
  return """Demonstrate pie chart best practices:
792
1360
  1. Start with many categories: ['A', 'B', 'C', 'D', 'E', 'F', 'G'] = [5, 8, 12, 15, 25, 20, 15]
793
1361
  2. Show why this is problematic (too many small segments)
794
- 3. Combine small categories: ['A+B+C', 'D', 'E', 'F', 'G'] = [25, 15, 25, 20, 15]
1362
+ 3. Combine small categories: ['A+B+C', 'D', 'E', 'F', 'G'] = [25, 15, 25, 20, 15]
795
1363
  4. Create the improved version with title 'Improved: Combined Small Categories'
796
1364
  5. Explain the improvement in readability"""
797
1365
 
@@ -801,7 +1369,7 @@ async def single_value_pie_chart_prompt() -> str:
801
1369
  """Single-value pie chart for progress indicators"""
802
1370
  return """Create single-value pie charts perfect for progress indicators:
803
1371
  1. Basic progress chart: ['Complete', 'Remaining'] = [75, 25], colors=['green', 'default']
804
- 2. Title: 'Project Progress: 75%'
1372
+ 2. Title: 'Project Progress: 75%'
805
1373
  3. Show only percentages (show_values=False, show_percentages=True)
806
1374
  4. Note: Remaining area appears as spaces, legend only shows 'Complete' entry
807
1375
  5. Perfect for dashboards, completion meters, utilization rates"""
@@ -823,7 +1391,7 @@ async def doughnut_chart_basic_prompt() -> str:
823
1391
  """Basic doughnut chart with hollow center"""
824
1392
  return """Create a doughnut chart with hollow center:
825
1393
  1. Data: ['Sales', 'Marketing', 'Support', 'Development'] = [40, 25, 15, 20]
826
- 2. Colors: ['blue', 'orange', 'green', 'red']
1394
+ 2. Colors: ['blue', 'orange', 'green', 'red']
827
1395
  3. Add donut=True parameter to create hollow center
828
1396
  4. Title: 'Department Budget - Doughnut Chart'
829
1397
  5. Note: Inner radius automatically set to 1/3 of outer radius, center remains empty"""
@@ -834,7 +1402,7 @@ async def doughnut_progress_indicator_prompt() -> str:
834
1402
  """Doughnut chart as progress indicator"""
835
1403
  return """Create a doughnut chart progress indicator:
836
1404
  1. Single-value data: ['Completed', 'Remaining'] = [85, 15]
837
- 2. Colors: ['cyan', 'default']
1405
+ 2. Colors: ['cyan', 'default']
838
1406
  3. Use both donut=True and show only percentages
839
1407
  4. Title: 'Project Progress - 85% Complete'
840
1408
  5. Perfect for modern dashboards - combines hollow center with progress visualization"""
@@ -852,155 +1420,68 @@ async def quick_donut_convenience_prompt() -> str:
852
1420
 
853
1421
 
854
1422
  # Main server entry point
855
- def start_server():
856
- """Start the MCP server."""
857
- import sys
1423
+ def start_server(stdio_mode: bool = False) -> None:
1424
+ """Start the MCP server.
1425
+
1426
+ Args:
1427
+ stdio_mode: If True, use STDIO transport mode
1428
+ """
858
1429
  import os
859
-
860
- # Check if we're being run in STDIO mode (by MCP CLI)
861
- # Force HTTP mode if MCP_HTTP_MODE is set, otherwise detect based on stdin
862
- force_http = os.getenv('MCP_HTTP_MODE', '').lower() == 'true'
863
- force_stdio = os.getenv('MCP_STDIO_MODE', '').lower() == 'true'
864
-
865
- if force_http:
1430
+
1431
+ # Detect mode automatically if not explicitly specified
1432
+ force_http = os.getenv("MCP_HTTP_MODE", "").lower() == "true"
1433
+ force_stdio = os.getenv("MCP_STDIO_MODE", "").lower() == "true" or stdio_mode
1434
+
1435
+ if force_http and not stdio_mode:
866
1436
  is_stdio_mode = False
867
- elif force_stdio:
1437
+ elif force_stdio or stdio_mode:
868
1438
  is_stdio_mode = True
869
1439
  else:
870
- # Auto-detect: STDIO mode when stdin is not a terminal (pipes/redirects)
1440
+ # Auto-detect based on stdin
871
1441
  is_stdio_mode = not sys.stdin.isatty()
872
-
1442
+
873
1443
  if is_stdio_mode:
874
- # STDIO mode for MCP CLI - simple JSON-RPC implementation
875
- import json
876
- import asyncio
877
-
878
- async def handle_stdio():
879
- try:
880
- for line in sys.stdin:
881
- if not line.strip():
882
- continue
883
-
884
- try:
885
- request = json.loads(line)
886
- method = request.get('method')
887
- request_id = request.get('id')
888
-
889
- if method == 'initialize':
890
- response = {
891
- "jsonrpc": "2.0",
892
- "id": request_id,
893
- "result": {
894
- "protocolVersion": "2024-11-05",
895
- "capabilities": {
896
- "tools": {},
897
- "prompts": {},
898
- "resources": {}
899
- },
900
- "serverInfo": {
901
- "name": "Plotext Plus MCP Server",
902
- "version": "1.0.0"
903
- }
904
- }
905
- }
906
- elif method == 'tools/list':
907
- response = {
908
- "jsonrpc": "2.0",
909
- "id": request_id,
910
- "result": {
911
- "tools": [
912
- {
913
- "name": "line_plot",
914
- "description": "Create a line plot with given x and y data points",
915
- "inputSchema": {
916
- "type": "object",
917
- "properties": {
918
- "x": {"type": "array", "items": {"type": "number"}},
919
- "y": {"type": "array", "items": {"type": "number"}},
920
- "title": {"type": "string"}
921
- },
922
- "required": ["x", "y"]
923
- }
924
- }
925
- ]
926
- }
927
- }
928
- elif method == 'tools/call':
929
- tool_name = request['params']['name']
930
- arguments = request['params']['arguments']
931
-
932
- if tool_name == 'line_plot':
933
- try:
934
- result = await line_plot(**arguments)
935
- response = {
936
- "jsonrpc": "2.0",
937
- "id": request_id,
938
- "result": {
939
- "content": [
940
- {
941
- "type": "text",
942
- "text": result
943
- }
944
- ]
945
- }
946
- }
947
- except Exception as e:
948
- response = {
949
- "jsonrpc": "2.0",
950
- "id": request_id,
951
- "error": {
952
- "code": -32000,
953
- "message": str(e)
954
- }
955
- }
956
- else:
957
- response = {
958
- "jsonrpc": "2.0",
959
- "id": request_id,
960
- "error": {
961
- "code": -32601,
962
- "message": f"Unknown tool: {tool_name}"
963
- }
964
- }
965
- else:
966
- response = {
967
- "jsonrpc": "2.0",
968
- "id": request_id,
969
- "error": {
970
- "code": -32601,
971
- "message": f"Unknown method: {method}"
972
- }
973
- }
974
-
975
- print(json.dumps(response), flush=True)
976
-
977
- except Exception as e:
978
- error_response = {
979
- "jsonrpc": "2.0",
980
- "id": request.get('id') if 'request' in locals() else None,
981
- "error": {
982
- "code": -32700,
983
- "message": f"Parse error: {str(e)}"
984
- }
985
- }
986
- print(json.dumps(error_response), flush=True)
987
-
988
- except Exception as e:
989
- print(f"STDIO handler error: {e}", file=sys.stderr)
990
-
991
- asyncio.run(handle_stdio())
1444
+ print("Starting Plotext Plus MCP Server (STDIO mode)...", file=sys.stderr)
1445
+ sys.stderr.flush() # Ensure stderr is flushed
1446
+ _logger.info("Starting Plotext Plus MCP Server in STDIO mode")
1447
+ server_kwargs = {
1448
+ "name": "Plotext Plus MCP Server",
1449
+ "version": "1.0.0",
1450
+ "prompts": True,
1451
+ "transport": "stdio", # Use STDIO transport
1452
+ "debug": False, # Disable debug mode to prevent hanging
1453
+ }
992
1454
  else:
993
- # HTTP server mode (default)
994
- print("Starting Plotext Plus MCP Server...")
995
- from chuk_mcp_server import ChukMCPServer
996
- server = ChukMCPServer(
997
- name="Plotext Plus MCP Server",
998
- version="1.0.0",
999
- prompts=True, # Enable prompts capability
1000
- logging=True # Enable logging capability for MCP clients
1001
- )
1002
- server.run()
1455
+ print("Starting Plotext Plus MCP Server (HTTP mode)...", file=sys.stderr)
1456
+ _logger.info("Starting Plotext Plus MCP Server in HTTP mode")
1457
+ server_kwargs = {
1458
+ "name": "Plotext Plus MCP Server",
1459
+ "version": "1.0.0",
1460
+ "prompts": True, # Enable prompts capability
1461
+ }
1462
+
1463
+ # Use custom server with proper logging support
1464
+ server = PlotextPlusMCPServer(**server_kwargs)
1465
+ server.log_server_event(
1466
+ "SERVER_START",
1467
+ "Plotext Plus MCP Server starting up",
1468
+ {
1469
+ "capabilities": ["tools", "resources", "prompts", "logging"],
1470
+ "mode": "stdio" if is_stdio_mode else "http",
1471
+ "logging_methods": ["logging/setLevel"],
1472
+ "custom_features": ["mcp_notifications", "structured_logging"],
1473
+ },
1474
+ )
1475
+ server.run()
1003
1476
 
1004
1477
 
1005
1478
  if __name__ == "__main__":
1006
- start_server()
1479
+ import argparse
1480
+ import sys
1481
+
1482
+ # Parse command line arguments
1483
+ parser = argparse.ArgumentParser(description="Plotext Plus MCP Server")
1484
+ parser.add_argument("--stdio", action="store_true", help="Use STDIO transport mode")
1485
+ args = parser.parse_args()
1486
+
1487
+ start_server(stdio_mode=args.stdio)