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