plotext-plus 1.0.8__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]] = []
38
159
 
39
- def _capture_plot_output(func, *args, **kwargs):
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)
177
+
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,106 +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
  """
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
+
74
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
+
75
314
  if title:
76
315
  plotting.title(title)
77
-
78
- _, output = _capture_plot_output(plotting.scatter, x, y, 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
+ )
79
328
  _, show_output = _capture_plot_output(plotting.show)
80
-
329
+
81
330
  return output + show_output
82
331
 
83
332
 
84
333
  @tool
85
- async def line_plot(x: List[Union[int, float]], y: List[Union[int, float]],
86
- 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:
87
341
  """Create a line plot with given x and y data points.
88
-
342
+
89
343
  Args:
90
- x: List of x-coordinates
91
- y: List of y-coordinates
344
+ x: List of x-coordinates (numbers, dates, or strings)
345
+ y: List of y-coordinates (numbers or strings)
92
346
  color: Line color (optional)
93
347
  title: Plot title (optional)
94
-
348
+ theme_name: Theme to apply (optional)
349
+
95
350
  Returns:
96
351
  The rendered plot as text
97
352
  """
98
- plotting.clear_figure()
99
- if title:
100
- plotting.title(title)
101
-
102
- _, output = _capture_plot_output(plotting.plot, x, y, color=color)
103
- _, show_output = _capture_plot_output(plotting.show)
104
-
105
- return output + show_output
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
358
+ try:
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
376
+ y_numeric = [float(val) if isinstance(val, str) else val for val in y]
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
+ )
381
+ except Exception as e:
382
+ _logger.error(f"Error processing inputs: {e}")
383
+ raise
384
+
385
+ try:
386
+ plotting.clear_figure()
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
+
394
+ if title:
395
+ plotting.title(title)
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
+
412
+ _, show_output = _capture_plot_output(plotting.show)
413
+ _logger.debug(f"Generated show output ({len(show_output)} characters)")
414
+
415
+ result = output + show_output
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
+
435
+ return result
436
+
437
+ except Exception as e:
438
+ _logger.error(f"Error during line plot creation: {e}")
439
+ import traceback
440
+
441
+ _logger.debug(f"Stack trace: {traceback.format_exc()}")
442
+ raise
106
443
 
107
444
 
108
445
  @tool
109
- async def bar_chart(labels: List[str], values: List[Union[int, float]],
110
- 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:
111
453
  """Create a bar chart with given labels and values.
112
-
454
+
113
455
  Args:
114
456
  labels: List of bar labels
115
457
  values: List of bar values
116
458
  color: Bar color (optional)
117
459
  title: Plot title (optional)
118
-
460
+ theme_name: Theme to apply (optional)
461
+
119
462
  Returns:
120
463
  The rendered plot as text
121
464
  """
465
+ # Convert string inputs to float
466
+ values_numeric = [float(val) if isinstance(val, str) else val for val in values]
467
+
122
468
  plotting.clear_figure()
469
+
470
+ # Apply theme if specified
471
+ if theme_name and theme_name != "default":
472
+ plotting.theme(theme_name)
473
+
123
474
  if title:
124
475
  plotting.title(title)
125
-
126
- _, output = _capture_plot_output(plotting.bar, labels, values, color=color)
476
+
477
+ _, output = _capture_plot_output(plotting.bar, labels, values_numeric, color=color)
127
478
  _, show_output = _capture_plot_output(plotting.show)
128
-
479
+
129
480
  return output + show_output
130
481
 
131
482
 
132
483
  @tool
133
- 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:
134
489
  """Create a matrix/heatmap plot from 2D data.
135
-
490
+
136
491
  Args:
137
492
  data: 2D list representing matrix data
138
493
  title: Plot title (optional)
139
-
494
+ theme_name: Theme to apply (optional)
495
+
140
496
  Returns:
141
497
  The rendered plot as text
142
498
  """
143
499
  plotting.clear_figure()
500
+
501
+ # Apply theme if specified
502
+ if theme_name and theme_name != "default":
503
+ plotting.theme(theme_name)
504
+
144
505
  if title:
145
506
  plotting.title(title)
146
-
507
+
147
508
  _, output = _capture_plot_output(plotting.matrix_plot, data)
148
509
  _, show_output = _capture_plot_output(plotting.show)
149
-
510
+
150
511
  return output + show_output
151
512
 
152
513
 
153
514
  @tool
154
- async def image_plot(image_path: str, title: Optional[str] = None,
155
- marker: Optional[str] = None, style: Optional[str] = None,
156
- 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:
157
523
  """Display an image in the terminal using ASCII art.
158
-
524
+
159
525
  Args:
160
526
  image_path: Path to the image file to display
161
527
  title: Plot title (optional)
@@ -163,103 +529,234 @@ async def image_plot(image_path: str, title: Optional[str] = None,
163
529
  style: Style for image rendering (optional)
164
530
  fast: Enable fast rendering mode for better performance (optional)
165
531
  grayscale: Render image in grayscale (optional)
166
-
532
+
167
533
  Returns:
168
534
  The rendered image plot as text
169
535
  """
170
536
  plotting.clear_figure()
171
537
  if title:
172
538
  plotting.title(title)
173
-
174
- _, output = _capture_plot_output(plotting.image_plot, image_path,
175
- marker=marker, style=style,
176
- 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
+ )
177
548
  _, show_output = _capture_plot_output(plotting.show)
178
-
549
+
179
550
  return output + show_output
180
551
 
181
552
 
182
553
  @tool
183
554
  async def play_gif(gif_path: str) -> str:
184
555
  """Play a GIF animation in the terminal.
185
-
556
+
186
557
  Args:
187
558
  gif_path: Path to the GIF file to play
188
-
559
+
189
560
  Returns:
190
561
  Confirmation message (GIF plays automatically)
191
562
  """
192
563
  plotting.clear_figure()
193
-
564
+
194
565
  # play_gif handles its own output and doesn't need show()
195
566
  plotting.play_gif(gif_path)
196
-
567
+
197
568
  return f"Playing GIF: {gif_path}"
198
569
 
199
570
 
200
571
  # Chart Class Tools
201
572
  @tool
202
- async def quick_scatter(x: List[Union[int, float]], y: List[Union[int, float]],
203
- 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:
204
579
  """Create a quick scatter chart using the chart classes API.
205
-
580
+
206
581
  Args:
207
- x: List of x-coordinates
208
- y: List of y-coordinates
582
+ x: List of x-coordinates (numbers, dates, or strings)
583
+ y: List of y-coordinates (numbers or strings)
209
584
  title: Chart title (optional)
210
585
  theme_name: Theme to apply (optional)
211
-
586
+
212
587
  Returns:
213
588
  The rendered chart as text
214
589
  """
215
- _, output = _capture_plot_output(charts.quick_scatter, x, y, title=title, theme=theme_name)
216
- 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
217
637
 
218
638
 
219
639
  @tool
220
- async def quick_line(x: List[Union[int, float]], y: List[Union[int, float]],
221
- 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:
222
646
  """Create a quick line chart using the chart classes API.
223
-
647
+
224
648
  Args:
225
- x: List of x-coordinates
226
- y: List of y-coordinates
649
+ x: List of x-coordinates (numbers, dates, or strings)
650
+ y: List of y-coordinates (numbers or strings)
227
651
  title: Chart title (optional)
228
652
  theme_name: Theme to apply (optional)
229
-
653
+
230
654
  Returns:
231
655
  The rendered chart as text
232
656
  """
233
- _, output = _capture_plot_output(charts.quick_line, x, y, 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
+ )
234
688
  return output
235
689
 
236
690
 
237
691
  @tool
238
- async def quick_bar(labels: List[str], values: List[Union[int, float]],
239
- 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:
240
699
  """Create a quick bar chart using the chart classes API.
241
-
700
+
242
701
  Args:
243
702
  labels: List of bar labels
244
703
  values: List of bar values
245
704
  title: Chart title (optional)
705
+ horizontal: Create horizontal bars if True (optional, default False)
246
706
  theme_name: Theme to apply (optional)
247
-
707
+
248
708
  Returns:
249
709
  The rendered chart as text
250
710
  """
251
- _, output = _capture_plot_output(charts.quick_bar, labels, values, title=title, theme=theme_name)
252
- return output
711
+ _logger.info(
712
+ f"Creating quick bar chart with {len(labels)} labels, title='{title}', horizontal={horizontal}"
713
+ )
714
+
715
+ # Convert string inputs to float
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
253
743
 
254
744
 
255
745
  @tool
256
- async def quick_pie(labels: List[str], values: List[Union[int, float]],
257
- colors: Optional[List[str]] = None, title: Optional[str] = None,
258
- show_values: bool = True, show_percentages: bool = True,
259
- show_values_on_slices: bool = False, donut: bool = False,
260
- 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:
261
758
  """Create a quick pie chart using the chart classes API.
262
-
759
+
263
760
  Args:
264
761
  labels: List of pie segment labels
265
762
  values: List of pie segment values
@@ -270,26 +767,44 @@ async def quick_pie(labels: List[str], values: List[Union[int, float]],
270
767
  show_values_on_slices: Show values directly on pie slices (optional, default False)
271
768
  donut: Create doughnut chart with hollow center (optional, default False)
272
769
  remaining_color: Color for remaining slice in single-value charts (optional)
273
-
770
+ theme_name: Theme to apply (optional)
771
+
274
772
  Returns:
275
773
  The rendered pie chart as text
276
774
  """
277
- _, output = _capture_plot_output(charts.quick_pie, labels, values, colors=colors,
278
- title=title, show_values=show_values,
279
- show_percentages=show_percentages,
280
- show_values_on_slices=show_values_on_slices,
281
- donut=donut, remaining_color=remaining_color)
775
+ # Convert string inputs to float
776
+ values_numeric = [float(val) if isinstance(val, str) else val for val in values]
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
+ )
282
791
  return output
283
792
 
284
793
 
285
794
  @tool
286
- async def quick_donut(labels: List[str], values: List[Union[int, float]],
287
- colors: Optional[List[str]] = None, title: Optional[str] = None,
288
- show_values: bool = True, show_percentages: bool = True,
289
- show_values_on_slices: bool = False,
290
- 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:
291
806
  """Create a quick doughnut chart (pie chart with hollow center) using the chart classes API.
292
-
807
+
293
808
  Args:
294
809
  labels: List of pie segment labels
295
810
  values: List of pie segment values
@@ -299,42 +814,56 @@ async def quick_donut(labels: List[str], values: List[Union[int, float]],
299
814
  show_percentages: Show percentages in legend (optional, default True)
300
815
  show_values_on_slices: Show values directly on pie slices (optional, default False)
301
816
  remaining_color: Color for remaining slice in single-value charts (optional)
302
-
817
+ theme_name: Theme to apply (optional)
818
+
303
819
  Returns:
304
820
  The rendered doughnut chart as text
305
821
  """
306
- _, output = _capture_plot_output(charts.quick_donut, labels, values, colors=colors,
307
- title=title, show_values=show_values,
308
- show_percentages=show_percentages,
309
- show_values_on_slices=show_values_on_slices,
310
- remaining_color=remaining_color)
822
+ # Convert string inputs to float
823
+ values_numeric = [float(val) if isinstance(val, str) else val for val in values]
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
+ )
311
837
  return output
312
838
 
313
839
 
314
840
  # Theme Tools
315
841
  @tool
316
- async def get_available_themes() -> Dict[str, Any]:
842
+ async def get_available_themes() -> dict[str, Any]:
317
843
  """Get information about available themes.
318
-
844
+
319
845
  Returns:
320
846
  Dictionary containing theme information
321
847
  """
322
848
  from .themes import get_theme_info
849
+
323
850
  return get_theme_info()
324
851
 
325
852
 
326
853
  @tool
327
854
  async def apply_plot_theme(theme_name: str) -> str:
328
855
  """Apply a theme to the current plot.
329
-
856
+
330
857
  Args:
331
858
  theme_name: Name of the theme to apply
332
-
859
+
333
860
  Returns:
334
861
  Confirmation message
335
862
  """
863
+ _logger.info(f"Applying plot theme: {theme_name}")
336
864
  plotting.clear_figure()
337
865
  plotting.theme(theme_name)
866
+ _logger.debug(f"Theme '{theme_name}' applied successfully")
338
867
  return f"Applied theme: {theme_name}"
339
868
 
340
869
 
@@ -342,7 +871,7 @@ async def apply_plot_theme(theme_name: str) -> str:
342
871
  @tool
343
872
  async def get_terminal_width() -> int:
344
873
  """Get the current terminal width.
345
-
874
+
346
875
  Returns:
347
876
  Terminal width in characters
348
877
  """
@@ -352,11 +881,11 @@ async def get_terminal_width() -> int:
352
881
  @tool
353
882
  async def colorize_text(text: str, color: str) -> str:
354
883
  """Apply color formatting to text.
355
-
884
+
356
885
  Args:
357
886
  text: Text to colorize
358
887
  color: Color name or code
359
-
888
+
360
889
  Returns:
361
890
  Colorized text
362
891
  """
@@ -366,10 +895,10 @@ async def colorize_text(text: str, color: str) -> str:
366
895
  @tool
367
896
  async def log_info(message: str) -> str:
368
897
  """Log an informational message.
369
-
898
+
370
899
  Args:
371
900
  message: Message to log
372
-
901
+
373
902
  Returns:
374
903
  Formatted log message
375
904
  """
@@ -380,10 +909,10 @@ async def log_info(message: str) -> str:
380
909
  @tool
381
910
  async def log_success(message: str) -> str:
382
911
  """Log a success message.
383
-
912
+
384
913
  Args:
385
914
  message: Message to log
386
-
915
+
387
916
  Returns:
388
917
  Formatted log message
389
918
  """
@@ -394,10 +923,10 @@ async def log_success(message: str) -> str:
394
923
  @tool
395
924
  async def log_warning(message: str) -> str:
396
925
  """Log a warning message.
397
-
926
+
398
927
  Args:
399
928
  message: Message to log
400
-
929
+
401
930
  Returns:
402
931
  Formatted log message
403
932
  """
@@ -408,10 +937,10 @@ async def log_warning(message: str) -> str:
408
937
  @tool
409
938
  async def log_error(message: str) -> str:
410
939
  """Log an error message.
411
-
940
+
412
941
  Args:
413
942
  message: Message to log
414
-
943
+
415
944
  Returns:
416
945
  Formatted log message
417
946
  """
@@ -423,40 +952,106 @@ async def log_error(message: str) -> str:
423
952
  @tool
424
953
  async def set_plot_size(width: int, height: int) -> str:
425
954
  """Set the plot size.
426
-
955
+
427
956
  Args:
428
957
  width: Plot width
429
958
  height: Plot height
430
-
959
+
431
960
  Returns:
432
961
  Confirmation message
433
962
  """
434
- plotting.plotsize(width, height)
435
- 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
436
999
 
437
1000
 
438
1001
  @tool
439
- async def enable_banner_mode(enabled: bool = True, title: Optional[str] = None,
440
- 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:
441
1005
  """Enable or disable banner mode.
442
-
1006
+
443
1007
  Args:
444
1008
  enabled: Whether to enable banner mode
445
1009
  title: Banner title (optional)
446
- subtitle: Banner subtitle (optional)
447
-
1010
+ subtitle: Banner subtitle (optional, will be appended to title)
1011
+
448
1012
  Returns:
449
1013
  Confirmation message
450
1014
  """
451
- plotting.banner_mode(enabled, title=title, subtitle=subtitle)
452
- status = "enabled" if enabled else "disabled"
453
- 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
454
1049
 
455
1050
 
456
1051
  @tool
457
1052
  async def clear_plot() -> str:
458
1053
  """Clear the current plot.
459
-
1054
+
460
1055
  Returns:
461
1056
  Confirmation message
462
1057
  """
@@ -466,65 +1061,89 @@ async def clear_plot() -> str:
466
1061
 
467
1062
  # Resource for plot configuration
468
1063
  @resource("config://plotext")
469
- async def get_plot_config() -> Dict[str, Any]:
1064
+ async def get_plot_config() -> dict[str, Any]:
470
1065
  """Get current plot configuration."""
471
1066
  from .themes import get_theme_info
1067
+
472
1068
  return {
473
1069
  "terminal_width": utilities.terminal_width(),
474
1070
  "available_themes": get_theme_info(),
475
1071
  "library_version": "plotext_plus",
476
- "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(),
477
1088
  }
478
1089
 
479
1090
 
480
1091
  # Resource for tool documentation/info
481
1092
  @resource("info://plotext")
482
- async def get_tool_info() -> Dict[str, Any]:
1093
+ async def get_tool_info() -> dict[str, Any]:
483
1094
  """Get comprehensive information about all available plotting tools."""
484
1095
  return {
485
1096
  "server_info": {
486
1097
  "name": "Plotext Plus MCP Server",
487
1098
  "description": "Model Context Protocol server for plotext_plus terminal plotting library",
488
1099
  "version": "1.0.0",
489
- "capabilities": ["plotting", "theming", "multimedia", "charts"]
1100
+ "capabilities": ["plotting", "theming", "multimedia", "charts"],
490
1101
  },
491
1102
  "plotting_tools": {
492
1103
  "scatter_plot": "Create scatter plots with x/y data points",
493
- "line_plot": "Create line plots for time series and continuous data",
1104
+ "line_plot": "Create line plots for time series and continuous data",
494
1105
  "bar_chart": "Create bar charts for categorical data",
495
1106
  "matrix_plot": "Create heatmaps from 2D matrix data",
496
1107
  "image_plot": "Display images in terminal using ASCII art",
497
- "play_gif": "Play animated GIFs in the terminal"
1108
+ "play_gif": "Play animated GIFs in the terminal",
498
1109
  },
499
1110
  "quick_chart_tools": {
500
1111
  "quick_scatter": "Quickly create scatter charts with theming",
501
1112
  "quick_line": "Quickly create line charts with theming",
502
- "quick_bar": "Quickly create bar charts with theming",
1113
+ "quick_bar": "Quickly create bar charts with theming",
503
1114
  "quick_pie": "Quickly create pie charts with custom colors, donut mode, and remaining_color options",
504
- "quick_donut": "Quickly create doughnut charts (hollow center pie charts)"
1115
+ "quick_donut": "Quickly create doughnut charts (hollow center pie charts)",
505
1116
  },
506
1117
  "theme_tools": {
507
1118
  "get_available_themes": "List all available color themes",
508
- "apply_plot_theme": "Apply a theme to plots"
1119
+ "apply_plot_theme": "Apply a theme to plots",
509
1120
  },
510
1121
  "utility_tools": {
511
1122
  "get_terminal_width": "Get current terminal width",
512
1123
  "colorize_text": "Apply colors to text output",
513
1124
  "log_info": "Output informational messages",
514
- "log_success": "Output success messages",
1125
+ "log_success": "Output success messages",
515
1126
  "log_warning": "Output warning messages",
516
- "log_error": "Output error messages"
1127
+ "log_error": "Output error messages",
517
1128
  },
518
1129
  "configuration_tools": {
519
1130
  "set_plot_size": "Set plot dimensions",
520
1131
  "enable_banner_mode": "Enable/disable banner mode",
521
- "clear_plot": "Clear current plot"
1132
+ "clear_plot": "Clear current plot",
522
1133
  },
523
1134
  "supported_formats": {
524
1135
  "image_formats": ["PNG", "JPG", "JPEG", "BMP", "GIF (static)"],
525
1136
  "gif_formats": ["GIF (animated)"],
526
- "chart_types": ["scatter", "line", "bar", "pie", "doughnut", "matrix/heatmap", "image"],
527
- "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",
528
1147
  },
529
1148
  "usage_tips": {
530
1149
  "pie_charts": "Best for 3-7 categories, use full terminal dimensions",
@@ -532,8 +1151,8 @@ async def get_tool_info() -> Dict[str, Any]:
532
1151
  "single_value_charts": "Perfect for progress/completion rates: ['Complete', 'Remaining'] with 'default' color",
533
1152
  "images": "Use fast=True for better performance with large images",
534
1153
  "themes": "Apply themes before creating plots for consistent styling",
535
- "banners": "Enable banner mode for professional-looking outputs"
536
- }
1154
+ "banners": "Enable banner mode for professional-looking outputs",
1155
+ },
537
1156
  }
538
1157
 
539
1158
 
@@ -609,7 +1228,7 @@ async def colorized_output_prompt() -> str:
609
1228
  @prompt("regional_sales_analysis")
610
1229
  async def regional_sales_analysis_prompt() -> str:
611
1230
  """Data analysis workflow example"""
612
- 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.
613
1232
 
614
1233
  Please:
615
1234
  1. Create individual plots for each region
@@ -622,7 +1241,7 @@ Please:
622
1241
  async def comparative_visualization_prompt() -> str:
623
1242
  """Comparative visualization example"""
624
1243
  return """Compare two datasets using multiple visualization types:
625
- - Dataset 1: [5,10,15,20,25]
1244
+ - Dataset 1: [5,10,15,20,25]
626
1245
  - Dataset 2: [3,8,18,22,28]
627
1246
  - Show both as scatter plot and line plot
628
1247
  - Use different colors and add meaningful titles"""
@@ -633,7 +1252,7 @@ async def error_handling_test_prompt() -> str:
633
1252
  """Error handling example"""
634
1253
  return """Try to create plots with various data scenarios and show how the system handles edge cases:
635
1254
  - Empty datasets
636
- - Mismatched array lengths
1255
+ - Mismatched array lengths
637
1256
  - Invalid color names
638
1257
  - Non-existent themes"""
639
1258
 
@@ -705,7 +1324,7 @@ async def multimedia_showcase_prompt() -> str:
705
1324
  async def basic_pie_chart_prompt() -> str:
706
1325
  """Basic pie chart example"""
707
1326
  return """Create a simple pie chart showing market share data:
708
- - Categories: ['iOS', 'Android', 'Windows', 'Other']
1327
+ - Categories: ['iOS', 'Android', 'Windows', 'Other']
709
1328
  - Values: [35, 45, 15, 5]
710
1329
  - Use colors: ['blue', 'green', 'orange', 'gray']
711
1330
  - Add title 'Mobile OS Market Share'
@@ -717,7 +1336,7 @@ async def pie_chart_styling_prompt() -> str:
717
1336
  """Advanced pie chart styling example"""
718
1337
  return """Create a styled pie chart with advanced features:
719
1338
  1. Use quick_pie with show_values_on_slices=True
720
- 2. Data: Budget categories ['Housing', 'Food', 'Transport', 'Entertainment']
1339
+ 2. Data: Budget categories ['Housing', 'Food', 'Transport', 'Entertainment']
721
1340
  3. Values: [1200, 400, 300, 200] (monthly budget)
722
1341
  4. Custom colors for each category
723
1342
  5. Add meaningful title and ensure full terminal usage"""
@@ -728,7 +1347,7 @@ async def pie_chart_comparison_prompt() -> str:
728
1347
  """Pie chart comparison example"""
729
1348
  return """Create multiple pie charts for comparison:
730
1349
  1. Q1 Sales: ['Product A', 'Product B', 'Product C'] = [30, 45, 25]
731
- 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]
732
1351
  3. Show both charts with different colors
733
1352
  4. Use appropriate titles ('Q1 Sales Distribution', 'Q2 Sales Distribution')
734
1353
  5. Discuss the trends visible in the comparison"""
@@ -740,7 +1359,7 @@ async def pie_chart_best_practices_prompt() -> str:
740
1359
  return """Demonstrate pie chart best practices:
741
1360
  1. Start with many categories: ['A', 'B', 'C', 'D', 'E', 'F', 'G'] = [5, 8, 12, 15, 25, 20, 15]
742
1361
  2. Show why this is problematic (too many small segments)
743
- 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]
744
1363
  4. Create the improved version with title 'Improved: Combined Small Categories'
745
1364
  5. Explain the improvement in readability"""
746
1365
 
@@ -750,7 +1369,7 @@ async def single_value_pie_chart_prompt() -> str:
750
1369
  """Single-value pie chart for progress indicators"""
751
1370
  return """Create single-value pie charts perfect for progress indicators:
752
1371
  1. Basic progress chart: ['Complete', 'Remaining'] = [75, 25], colors=['green', 'default']
753
- 2. Title: 'Project Progress: 75%'
1372
+ 2. Title: 'Project Progress: 75%'
754
1373
  3. Show only percentages (show_values=False, show_percentages=True)
755
1374
  4. Note: Remaining area appears as spaces, legend only shows 'Complete' entry
756
1375
  5. Perfect for dashboards, completion meters, utilization rates"""
@@ -772,7 +1391,7 @@ async def doughnut_chart_basic_prompt() -> str:
772
1391
  """Basic doughnut chart with hollow center"""
773
1392
  return """Create a doughnut chart with hollow center:
774
1393
  1. Data: ['Sales', 'Marketing', 'Support', 'Development'] = [40, 25, 15, 20]
775
- 2. Colors: ['blue', 'orange', 'green', 'red']
1394
+ 2. Colors: ['blue', 'orange', 'green', 'red']
776
1395
  3. Add donut=True parameter to create hollow center
777
1396
  4. Title: 'Department Budget - Doughnut Chart'
778
1397
  5. Note: Inner radius automatically set to 1/3 of outer radius, center remains empty"""
@@ -783,7 +1402,7 @@ async def doughnut_progress_indicator_prompt() -> str:
783
1402
  """Doughnut chart as progress indicator"""
784
1403
  return """Create a doughnut chart progress indicator:
785
1404
  1. Single-value data: ['Completed', 'Remaining'] = [85, 15]
786
- 2. Colors: ['cyan', 'default']
1405
+ 2. Colors: ['cyan', 'default']
787
1406
  3. Use both donut=True and show only percentages
788
1407
  4. Title: 'Project Progress - 85% Complete'
789
1408
  5. Perfect for modern dashboards - combines hollow center with progress visualization"""
@@ -801,11 +1420,68 @@ async def quick_donut_convenience_prompt() -> str:
801
1420
 
802
1421
 
803
1422
  # Main server entry point
804
- def start_server():
805
- """Start the MCP server."""
806
- print("Starting Plotext Plus MCP Server...")
807
- run()
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
+ """
1429
+ import os
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:
1436
+ is_stdio_mode = False
1437
+ elif force_stdio or stdio_mode:
1438
+ is_stdio_mode = True
1439
+ else:
1440
+ # Auto-detect based on stdin
1441
+ is_stdio_mode = not sys.stdin.isatty()
1442
+
1443
+ if is_stdio_mode:
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
+ }
1454
+ else:
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()
808
1476
 
809
1477
 
810
1478
  if __name__ == "__main__":
811
- 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)