@mseep/csv-editor 1.0.0
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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +53 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +38 -0
- package/.github/workflows/deploy-docs.yml +62 -0
- package/.github/workflows/publish-github.yml +52 -0
- package/.github/workflows/publish.yml +44 -0
- package/.github/workflows/test.yml +32 -0
- package/.pre-commit-config.yaml +157 -0
- package/ALTERNATIVE_PUBLISHING.md +175 -0
- package/ARCHITECTURE.md +1011 -0
- package/CHANGELOG.md +99 -0
- package/CODE_OF_CONDUCT.md +41 -0
- package/CONTRIBUTING.md +427 -0
- package/Dockerfile +22 -0
- package/LICENSE +21 -0
- package/MCP_CONFIG.md +505 -0
- package/PUBLISHING.md +210 -0
- package/README.md +400 -0
- package/SECURITY.md +61 -0
- package/docs/README.md +41 -0
- package/docs/blog/2019-05-28-first-blog-post.md +12 -0
- package/docs/blog/2019-05-29-long-blog-post.md +44 -0
- package/docs/blog/2021-08-01-mdx-blog-post.mdx +24 -0
- package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
- package/docs/blog/2021-08-26-welcome/index.md +29 -0
- package/docs/blog/authors.yml +25 -0
- package/docs/blog/tags.yml +19 -0
- package/docs/docs/api/overview.md +183 -0
- package/docs/docs/installation.md +252 -0
- package/docs/docs/intro.md +87 -0
- package/docs/docs/tutorial-basics/_category_.json +8 -0
- package/docs/docs/tutorial-basics/congratulations.md +23 -0
- package/docs/docs/tutorial-basics/create-a-blog-post.md +34 -0
- package/docs/docs/tutorial-basics/create-a-document.md +57 -0
- package/docs/docs/tutorial-basics/create-a-page.md +43 -0
- package/docs/docs/tutorial-basics/deploy-your-site.md +31 -0
- package/docs/docs/tutorial-basics/markdown-features.mdx +152 -0
- package/docs/docs/tutorial-extras/_category_.json +7 -0
- package/docs/docs/tutorial-extras/img/docsVersionDropdown.png +0 -0
- package/docs/docs/tutorial-extras/img/localeDropdown.png +0 -0
- package/docs/docs/tutorial-extras/manage-docs-versions.md +55 -0
- package/docs/docs/tutorial-extras/translate-your-site.md +88 -0
- package/docs/docs/tutorials/quickstart.md +365 -0
- package/docs/docusaurus.config.ts +163 -0
- package/docs/package-lock.json +17493 -0
- package/docs/package.json +48 -0
- package/docs/sidebars.ts +33 -0
- package/docs/src/components/HomepageFeatures/index.tsx +71 -0
- package/docs/src/components/HomepageFeatures/styles.module.css +11 -0
- package/docs/src/css/custom.css +30 -0
- package/docs/src/pages/index.module.css +23 -0
- package/docs/src/pages/index.tsx +44 -0
- package/docs/src/pages/markdown-page.md +7 -0
- package/docs/static/.nojekyll +0 -0
- package/docs/static/img/docusaurus-social-card.jpg +0 -0
- package/docs/static/img/docusaurus.png +0 -0
- package/docs/static/img/favicon.ico +0 -0
- package/docs/static/img/logo.svg +1 -0
- package/docs/static/img/undraw_docusaurus_mountain.svg +171 -0
- package/docs/static/img/undraw_docusaurus_react.svg +170 -0
- package/docs/static/img/undraw_docusaurus_tree.svg +40 -0
- package/docs/tsconfig.json +8 -0
- package/examples/README.md +48 -0
- package/examples/auto_save_demo.py +206 -0
- package/examples/auto_save_overwrite.py +201 -0
- package/examples/basic_usage.py +135 -0
- package/examples/demo.py +139 -0
- package/examples/history_demo.py +317 -0
- package/examples/test_default_autosave.py +124 -0
- package/examples/update_consignee_example.py +179 -0
- package/package.json +51 -0
- package/plans/2026-04-19-fastmcp3-migration-plan.md +1045 -0
- package/pyproject.toml +331 -0
- package/requirements-dev.txt +30 -0
- package/requirements.txt +22 -0
- package/scripts/publish.py +67 -0
- package/smithery.yaml +15 -0
- package/specs/2026-04-19-fastmcp3-migration-design.md +243 -0
- package/src/csv_editor/__init__.py +8 -0
- package/src/csv_editor/models/__init__.py +39 -0
- package/src/csv_editor/models/auto_save.py +246 -0
- package/src/csv_editor/models/csv_session.py +468 -0
- package/src/csv_editor/models/data_models.py +244 -0
- package/src/csv_editor/models/history_manager.py +456 -0
- package/src/csv_editor/prompts/__init__.py +0 -0
- package/src/csv_editor/prompts/data_prompts.py +13 -0
- package/src/csv_editor/resources/__init__.py +0 -0
- package/src/csv_editor/resources/csv_resources.py +22 -0
- package/src/csv_editor/server.py +640 -0
- package/src/csv_editor/tools/__init__.py +5 -0
- package/src/csv_editor/tools/analytics.py +700 -0
- package/src/csv_editor/tools/auto_save_operations.py +235 -0
- package/src/csv_editor/tools/data_operations.py +3 -0
- package/src/csv_editor/tools/history_operations.py +315 -0
- package/src/csv_editor/tools/io_operations.py +431 -0
- package/src/csv_editor/tools/transformations.py +663 -0
- package/src/csv_editor/tools/validation.py +822 -0
- package/src/csv_editor/utils/__init__.py +0 -0
- package/src/csv_editor/utils/validators.py +205 -0
- package/tests/README.md +65 -0
- package/tests/__init__.py +7 -0
- package/tests/conftest.py +50 -0
- package/tests/test_auto_save.py +378 -0
- package/tests/test_basic.py +103 -0
- package/tests/test_integration.py +356 -0
- package/tests/test_server_boot.py +50 -0
- package/tests/test_settings.py +184 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""I/O operations tools for CSV Editor MCP Server."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from fastmcp import Context
|
|
10
|
+
|
|
11
|
+
from ..models import ExportFormat, OperationResult, OperationType, get_session_manager
|
|
12
|
+
from ..utils.validators import validate_file_path, validate_url
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def load_csv(
|
|
16
|
+
file_path: str,
|
|
17
|
+
encoding: str = "utf-8",
|
|
18
|
+
delimiter: str = ",",
|
|
19
|
+
session_id: str | None = None,
|
|
20
|
+
header: int | None = 0,
|
|
21
|
+
na_values: list[str] | None = None,
|
|
22
|
+
parse_dates: list[str] | None = None,
|
|
23
|
+
ctx: Context = None,
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
"""Load a CSV file into a session.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
file_path: Path to the CSV file
|
|
29
|
+
encoding: File encoding (default: utf-8)
|
|
30
|
+
delimiter: Column delimiter (default: comma)
|
|
31
|
+
session_id: Optional existing session ID to use
|
|
32
|
+
header: Row number to use as header (default: 0)
|
|
33
|
+
na_values: Additional strings to recognize as NA/NaN
|
|
34
|
+
parse_dates: Columns to parse as dates
|
|
35
|
+
ctx: FastMCP context
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Operation result with session ID and data info
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
# Validate file path
|
|
42
|
+
is_valid, validated_path = validate_file_path(file_path)
|
|
43
|
+
if not is_valid:
|
|
44
|
+
return OperationResult(
|
|
45
|
+
success=False, message=f"Invalid file path: {validated_path}", error=validated_path
|
|
46
|
+
).model_dump()
|
|
47
|
+
|
|
48
|
+
if ctx:
|
|
49
|
+
await ctx.info(f"Loading CSV file: {validated_path}")
|
|
50
|
+
await ctx.report_progress(0.1, "Validating file...")
|
|
51
|
+
|
|
52
|
+
# Get or create session
|
|
53
|
+
session_manager = get_session_manager()
|
|
54
|
+
session = session_manager.get_or_create_session(session_id)
|
|
55
|
+
|
|
56
|
+
if ctx:
|
|
57
|
+
await ctx.report_progress(0.3, "Reading file...")
|
|
58
|
+
|
|
59
|
+
# Read CSV with pandas
|
|
60
|
+
read_params = {
|
|
61
|
+
"filepath_or_buffer": validated_path,
|
|
62
|
+
"encoding": encoding,
|
|
63
|
+
"delimiter": delimiter,
|
|
64
|
+
"header": header,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if na_values:
|
|
68
|
+
read_params["na_values"] = na_values
|
|
69
|
+
if parse_dates:
|
|
70
|
+
read_params["parse_dates"] = parse_dates
|
|
71
|
+
|
|
72
|
+
df = pd.read_csv(**read_params)
|
|
73
|
+
|
|
74
|
+
if ctx:
|
|
75
|
+
await ctx.report_progress(0.8, "Processing data...")
|
|
76
|
+
|
|
77
|
+
# Load into session
|
|
78
|
+
session.load_data(df, validated_path)
|
|
79
|
+
|
|
80
|
+
if ctx:
|
|
81
|
+
await ctx.report_progress(1.0, "Complete!")
|
|
82
|
+
await ctx.info(f"Loaded {len(df)} rows and {len(df.columns)} columns")
|
|
83
|
+
|
|
84
|
+
return OperationResult(
|
|
85
|
+
success=True,
|
|
86
|
+
message="Successfully loaded CSV file",
|
|
87
|
+
session_id=session.session_id,
|
|
88
|
+
rows_affected=len(df),
|
|
89
|
+
columns_affected=df.columns.tolist(),
|
|
90
|
+
data={
|
|
91
|
+
"shape": df.shape,
|
|
92
|
+
"dtypes": {col: str(dtype) for col, dtype in df.dtypes.items()},
|
|
93
|
+
"memory_usage_mb": df.memory_usage(deep=True).sum() / (1024 * 1024),
|
|
94
|
+
"preview": df.to_dict("records"),
|
|
95
|
+
},
|
|
96
|
+
).model_dump()
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
if ctx:
|
|
100
|
+
await ctx.error(f"Failed to load CSV: {e!s}")
|
|
101
|
+
return OperationResult(
|
|
102
|
+
success=False, message="Failed to load CSV file", error=str(e)
|
|
103
|
+
).model_dump()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def load_csv_from_url(
|
|
107
|
+
url: str,
|
|
108
|
+
encoding: str = "utf-8",
|
|
109
|
+
delimiter: str = ",",
|
|
110
|
+
session_id: str | None = None,
|
|
111
|
+
ctx: Context = None,
|
|
112
|
+
) -> dict[str, Any]:
|
|
113
|
+
"""Load a CSV file from a URL.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
url: URL of the CSV file
|
|
117
|
+
encoding: File encoding
|
|
118
|
+
delimiter: Column delimiter
|
|
119
|
+
session_id: Optional existing session ID
|
|
120
|
+
ctx: FastMCP context
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Operation result with session ID and data info
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
# Validate URL
|
|
127
|
+
is_valid, validated_url = validate_url(url)
|
|
128
|
+
if not is_valid:
|
|
129
|
+
return OperationResult(
|
|
130
|
+
success=False, message=f"Invalid URL: {validated_url}", error=validated_url
|
|
131
|
+
).model_dump()
|
|
132
|
+
|
|
133
|
+
if ctx:
|
|
134
|
+
await ctx.info(f"Loading CSV from URL: {url}")
|
|
135
|
+
await ctx.report_progress(0.1, "Downloading file...")
|
|
136
|
+
|
|
137
|
+
# Download CSV using pandas (it handles URLs directly)
|
|
138
|
+
df = pd.read_csv(url, encoding=encoding, delimiter=delimiter)
|
|
139
|
+
|
|
140
|
+
if ctx:
|
|
141
|
+
await ctx.report_progress(0.8, "Processing data...")
|
|
142
|
+
|
|
143
|
+
# Get or create session
|
|
144
|
+
session_manager = get_session_manager()
|
|
145
|
+
session = session_manager.get_or_create_session(session_id)
|
|
146
|
+
session.load_data(df, url)
|
|
147
|
+
|
|
148
|
+
if ctx:
|
|
149
|
+
await ctx.report_progress(1.0, "Complete!")
|
|
150
|
+
await ctx.info(f"Loaded {len(df)} rows and {len(df.columns)} columns")
|
|
151
|
+
|
|
152
|
+
return OperationResult(
|
|
153
|
+
success=True,
|
|
154
|
+
message="Successfully loaded CSV from URL",
|
|
155
|
+
session_id=session.session_id,
|
|
156
|
+
rows_affected=len(df),
|
|
157
|
+
columns_affected=df.columns.tolist(),
|
|
158
|
+
data={"shape": df.shape, "source_url": url, "preview": df.head(5).to_dict("records")},
|
|
159
|
+
).model_dump()
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
if ctx:
|
|
163
|
+
await ctx.error(f"Failed to load CSV from URL: {e!s}")
|
|
164
|
+
return OperationResult(
|
|
165
|
+
success=False, message="Failed to load CSV from URL", error=str(e)
|
|
166
|
+
).model_dump()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def load_csv_from_content(
|
|
170
|
+
content: str,
|
|
171
|
+
delimiter: str = ",",
|
|
172
|
+
session_id: str | None = None,
|
|
173
|
+
has_header: bool = True,
|
|
174
|
+
ctx: Context = None,
|
|
175
|
+
) -> dict[str, Any]:
|
|
176
|
+
"""Load CSV data from a string content.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
content: CSV content as string
|
|
180
|
+
delimiter: Column delimiter
|
|
181
|
+
session_id: Optional existing session ID
|
|
182
|
+
has_header: Whether first row is header
|
|
183
|
+
ctx: FastMCP context
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Operation result with session ID and data info
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
if ctx:
|
|
190
|
+
await ctx.info("Loading CSV from content string")
|
|
191
|
+
|
|
192
|
+
# Parse CSV from string
|
|
193
|
+
from io import StringIO
|
|
194
|
+
|
|
195
|
+
df = pd.read_csv(StringIO(content), delimiter=delimiter, header=0 if has_header else None)
|
|
196
|
+
|
|
197
|
+
# Get or create session
|
|
198
|
+
session_manager = get_session_manager()
|
|
199
|
+
session = session_manager.get_or_create_session(session_id)
|
|
200
|
+
session.load_data(df, None)
|
|
201
|
+
|
|
202
|
+
if ctx:
|
|
203
|
+
await ctx.info(f"Loaded {len(df)} rows and {len(df.columns)} columns")
|
|
204
|
+
|
|
205
|
+
return OperationResult(
|
|
206
|
+
success=True,
|
|
207
|
+
message="Successfully loaded CSV from content",
|
|
208
|
+
session_id=session.session_id,
|
|
209
|
+
rows_affected=len(df),
|
|
210
|
+
columns_affected=df.columns.tolist(),
|
|
211
|
+
data={"shape": df.shape, "preview": df.head(5).to_dict("records")},
|
|
212
|
+
).model_dump()
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
if ctx:
|
|
216
|
+
await ctx.error(f"Failed to parse CSV content: {e!s}")
|
|
217
|
+
return OperationResult(
|
|
218
|
+
success=False, message="Failed to parse CSV content", error=str(e)
|
|
219
|
+
).model_dump()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
async def export_csv(
|
|
223
|
+
session_id: str,
|
|
224
|
+
file_path: str | None = None,
|
|
225
|
+
format: ExportFormat = ExportFormat.CSV,
|
|
226
|
+
encoding: str = "utf-8",
|
|
227
|
+
index: bool = False,
|
|
228
|
+
ctx: Context = None,
|
|
229
|
+
) -> dict[str, Any]:
|
|
230
|
+
"""Export session data to various formats.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
session_id: Session ID to export
|
|
234
|
+
file_path: Optional output file path (auto-generated if not provided)
|
|
235
|
+
format: Export format (csv, tsv, json, excel, parquet, html, markdown)
|
|
236
|
+
encoding: Output encoding
|
|
237
|
+
index: Whether to include index in output
|
|
238
|
+
ctx: FastMCP context
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Operation result with file path
|
|
242
|
+
"""
|
|
243
|
+
try:
|
|
244
|
+
# Get session
|
|
245
|
+
session_manager = get_session_manager()
|
|
246
|
+
session = session_manager.get_session(session_id)
|
|
247
|
+
|
|
248
|
+
if not session or session.df is None:
|
|
249
|
+
return OperationResult(
|
|
250
|
+
success=False,
|
|
251
|
+
message="Session not found or no data loaded",
|
|
252
|
+
error="Invalid session ID",
|
|
253
|
+
).model_dump()
|
|
254
|
+
|
|
255
|
+
if ctx:
|
|
256
|
+
await ctx.info(f"Exporting data in {format.value} format")
|
|
257
|
+
await ctx.report_progress(0.1, "Preparing export...")
|
|
258
|
+
|
|
259
|
+
# Generate file path if not provided
|
|
260
|
+
if not file_path:
|
|
261
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
262
|
+
filename = f"export_{session_id[:8]}_{timestamp}"
|
|
263
|
+
|
|
264
|
+
# Determine extension based on format
|
|
265
|
+
extensions = {
|
|
266
|
+
ExportFormat.CSV: ".csv",
|
|
267
|
+
ExportFormat.TSV: ".tsv",
|
|
268
|
+
ExportFormat.JSON: ".json",
|
|
269
|
+
ExportFormat.EXCEL: ".xlsx",
|
|
270
|
+
ExportFormat.PARQUET: ".parquet",
|
|
271
|
+
ExportFormat.HTML: ".html",
|
|
272
|
+
ExportFormat.MARKDOWN: ".md",
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
file_path = tempfile.gettempdir() + "/" + filename + extensions[format]
|
|
276
|
+
|
|
277
|
+
file_path = Path(file_path)
|
|
278
|
+
df = session.df
|
|
279
|
+
|
|
280
|
+
if ctx:
|
|
281
|
+
await ctx.report_progress(0.5, f"Writing {format.value} file...")
|
|
282
|
+
|
|
283
|
+
# Export based on format
|
|
284
|
+
if format == ExportFormat.CSV:
|
|
285
|
+
df.to_csv(file_path, encoding=encoding, index=index)
|
|
286
|
+
elif format == ExportFormat.TSV:
|
|
287
|
+
df.to_csv(file_path, sep="\t", encoding=encoding, index=index)
|
|
288
|
+
elif format == ExportFormat.JSON:
|
|
289
|
+
df.to_json(file_path, orient="records", indent=2)
|
|
290
|
+
elif format == ExportFormat.EXCEL:
|
|
291
|
+
df.to_excel(file_path, index=index, engine="openpyxl")
|
|
292
|
+
elif format == ExportFormat.PARQUET:
|
|
293
|
+
df.to_parquet(file_path, index=index)
|
|
294
|
+
elif format == ExportFormat.HTML:
|
|
295
|
+
df.to_html(file_path, index=index)
|
|
296
|
+
elif format == ExportFormat.MARKDOWN:
|
|
297
|
+
df.to_markdown(file_path, index=index)
|
|
298
|
+
else:
|
|
299
|
+
return OperationResult(
|
|
300
|
+
success=False,
|
|
301
|
+
message=f"Unsupported format: {format}",
|
|
302
|
+
error="Invalid export format",
|
|
303
|
+
).model_dump()
|
|
304
|
+
|
|
305
|
+
# Record operation
|
|
306
|
+
session.record_operation(
|
|
307
|
+
OperationType.EXPORT, {"format": format.value, "file_path": str(file_path)}
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if ctx:
|
|
311
|
+
await ctx.report_progress(1.0, "Export complete!")
|
|
312
|
+
await ctx.info(f"Exported to {file_path}")
|
|
313
|
+
|
|
314
|
+
return OperationResult(
|
|
315
|
+
success=True,
|
|
316
|
+
message=f"Successfully exported data to {format.value}",
|
|
317
|
+
session_id=session_id,
|
|
318
|
+
data={
|
|
319
|
+
"file_path": str(file_path),
|
|
320
|
+
"format": format.value,
|
|
321
|
+
"rows_exported": len(df),
|
|
322
|
+
"file_size_bytes": file_path.stat().st_size,
|
|
323
|
+
},
|
|
324
|
+
).model_dump()
|
|
325
|
+
|
|
326
|
+
except Exception as e:
|
|
327
|
+
if ctx:
|
|
328
|
+
await ctx.error(f"Failed to export data: {e!s}")
|
|
329
|
+
return OperationResult(
|
|
330
|
+
success=False, message="Failed to export data", error=str(e)
|
|
331
|
+
).model_dump()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
async def get_session_info(session_id: str, ctx: Context = None) -> dict[str, Any]:
|
|
335
|
+
"""Get information about a specific session.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
session_id: Session ID
|
|
339
|
+
ctx: FastMCP context
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Session information
|
|
343
|
+
"""
|
|
344
|
+
try:
|
|
345
|
+
session_manager = get_session_manager()
|
|
346
|
+
session = session_manager.get_session(session_id)
|
|
347
|
+
|
|
348
|
+
if not session:
|
|
349
|
+
return OperationResult(
|
|
350
|
+
success=False, message="Session not found", error="Invalid session ID"
|
|
351
|
+
).model_dump()
|
|
352
|
+
|
|
353
|
+
if ctx:
|
|
354
|
+
await ctx.info(f"Retrieved info for session {session_id}")
|
|
355
|
+
|
|
356
|
+
info = session.get_info()
|
|
357
|
+
return OperationResult(
|
|
358
|
+
success=True,
|
|
359
|
+
message="Session info retrieved",
|
|
360
|
+
session_id=session_id,
|
|
361
|
+
data=info.model_dump(),
|
|
362
|
+
).model_dump()
|
|
363
|
+
|
|
364
|
+
except Exception as e:
|
|
365
|
+
if ctx:
|
|
366
|
+
await ctx.error(f"Failed to get session info: {e!s}")
|
|
367
|
+
return OperationResult(
|
|
368
|
+
success=False, message="Failed to get session info", error=str(e)
|
|
369
|
+
).model_dump()
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
async def list_sessions(ctx: Context = None) -> dict[str, Any]:
|
|
373
|
+
"""List all active sessions.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
ctx: FastMCP context
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
List of active sessions
|
|
380
|
+
"""
|
|
381
|
+
try:
|
|
382
|
+
session_manager = get_session_manager()
|
|
383
|
+
sessions = session_manager.list_sessions()
|
|
384
|
+
|
|
385
|
+
if ctx:
|
|
386
|
+
await ctx.info(f"Found {len(sessions)} active sessions")
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
"success": True,
|
|
390
|
+
"message": f"Found {len(sessions)} active sessions",
|
|
391
|
+
"sessions": [s.model_dump() for s in sessions],
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
except Exception as e:
|
|
395
|
+
if ctx:
|
|
396
|
+
await ctx.error(f"Failed to list sessions: {e!s}")
|
|
397
|
+
return {"success": False, "message": "Failed to list sessions", "error": str(e)}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
async def close_session(session_id: str, ctx: Context = None) -> dict[str, Any]:
|
|
401
|
+
"""Close and clean up a session.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
session_id: Session ID to close
|
|
405
|
+
ctx: FastMCP context
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
Operation result
|
|
409
|
+
"""
|
|
410
|
+
try:
|
|
411
|
+
session_manager = get_session_manager()
|
|
412
|
+
removed = await session_manager.remove_session(session_id)
|
|
413
|
+
|
|
414
|
+
if not removed:
|
|
415
|
+
return OperationResult(
|
|
416
|
+
success=False, message="Session not found", error="Invalid session ID"
|
|
417
|
+
).model_dump()
|
|
418
|
+
|
|
419
|
+
if ctx:
|
|
420
|
+
await ctx.info(f"Closed session {session_id}")
|
|
421
|
+
|
|
422
|
+
return OperationResult(
|
|
423
|
+
success=True, message=f"Session {session_id} closed successfully", session_id=session_id
|
|
424
|
+
).model_dump()
|
|
425
|
+
|
|
426
|
+
except Exception as e:
|
|
427
|
+
if ctx:
|
|
428
|
+
await ctx.error(f"Failed to close session: {e!s}")
|
|
429
|
+
return OperationResult(
|
|
430
|
+
success=False, message="Failed to close session", error=str(e)
|
|
431
|
+
).model_dump()
|