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.
- plotext_plus/__init__.py +20 -15
- plotext_plus/__main__.py +1 -0
- plotext_plus/_api.py +632 -371
- plotext_plus/_build.py +765 -149
- plotext_plus/_core.py +609 -164
- plotext_plus/_date.py +50 -32
- plotext_plus/_default.py +35 -28
- plotext_plus/_dict.py +807 -103
- plotext_plus/_doc.py +867 -296
- plotext_plus/_doc_utils.py +279 -245
- plotext_plus/_figure.py +1295 -303
- plotext_plus/_global.py +238 -140
- plotext_plus/_matrix.py +217 -63
- plotext_plus/_monitor.py +1036 -489
- plotext_plus/_output.py +29 -23
- plotext_plus/_shtab.py +2 -0
- plotext_plus/_themes.py +363 -247
- plotext_plus/_utility.py +581 -313
- plotext_plus/api.py +418 -332
- plotext_plus/charts.py +47 -24
- plotext_plus/core.py +570 -177
- plotext_plus/mcp_cli.py +15 -13
- plotext_plus/mcp_server.py +842 -166
- plotext_plus/plotext_cli.py +414 -275
- plotext_plus/plotting.py +86 -70
- plotext_plus/themes.py +10 -13
- plotext_plus/utilities.py +33 -33
- plotext_plus/utils.py +240 -140
- {plotext_plus-1.0.8.dist-info → plotext_plus-1.0.10.dist-info}/METADATA +7 -2
- plotext_plus-1.0.10.dist-info/RECORD +33 -0
- plotext_plus-1.0.8.dist-info/RECORD +0 -33
- {plotext_plus-1.0.8.dist-info → plotext_plus-1.0.10.dist-info}/WHEEL +0 -0
- {plotext_plus-1.0.8.dist-info → plotext_plus-1.0.10.dist-info}/entry_points.txt +0 -0
- {plotext_plus-1.0.8.dist-info → plotext_plus-1.0.10.dist-info}/licenses/LICENSE +0 -0
plotext_plus/mcp_server.py
CHANGED
|
@@ -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
|
|
16
|
-
|
|
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
|
|
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
|
|
32
|
+
from . import _core
|
|
31
33
|
from . import charts
|
|
32
|
-
from . import
|
|
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
|
-
|
|
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(
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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(
|
|
86
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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(
|
|
110
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
155
|
-
|
|
156
|
-
|
|
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(
|
|
175
|
-
|
|
176
|
-
|
|
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(
|
|
203
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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(
|
|
221
|
-
|
|
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
|
-
|
|
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(
|
|
239
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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() ->
|
|
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
|
-
|
|
435
|
-
|
|
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(
|
|
440
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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() ->
|
|
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() ->
|
|
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": [
|
|
527
|
-
|
|
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
|
-
|
|
807
|
-
|
|
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
|
-
|
|
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)
|