@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,13 @@
|
|
|
1
|
+
"""Prompt templates - placeholder implementation."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def analyze_csv_prompt(session_id: str, analysis_type: str) -> str:
|
|
5
|
+
return f"Analyze CSV data in session {session_id} for {analysis_type}"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def suggest_transformations_prompt(session_id: str, goal: str) -> str:
|
|
9
|
+
return f"Suggest transformations for session {session_id} to achieve: {goal}"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def data_cleaning_prompt(session_id: str, issues: list[str]) -> str:
|
|
13
|
+
return f"Suggest cleaning for session {session_id} with issues: {', '.join(issues)}"
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Resource definitions - placeholder implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastmcp import Context
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Placeholder resources - will implement next
|
|
9
|
+
async def get_csv_data(session_id: str, ctx: Context) -> dict[str, Any]:
|
|
10
|
+
return {"session_id": session_id, "data": "Not yet implemented"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def get_csv_schema(session_id: str, ctx: Context) -> dict[str, Any]:
|
|
14
|
+
return {"session_id": session_id, "schema": "Not yet implemented"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def get_csv_preview(session_id: str, ctx: Context) -> dict[str, Any]:
|
|
18
|
+
return {"session_id": session_id, "preview": "Not yet implemented"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def list_active_sessions(ctx: Context) -> list[dict[str, Any]]:
|
|
22
|
+
return []
|
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
"""Main FastMCP server for CSV Editor."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastmcp import Context, FastMCP
|
|
8
|
+
|
|
9
|
+
# Configure logging
|
|
10
|
+
logging.basicConfig(
|
|
11
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
12
|
+
)
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Initialize FastMCP server
|
|
16
|
+
mcp = FastMCP("CSV Editor")
|
|
17
|
+
|
|
18
|
+
# Import our models
|
|
19
|
+
from .models import get_session_manager
|
|
20
|
+
|
|
21
|
+
# ============================================================================
|
|
22
|
+
# HEALTH AND INFO TOOLS
|
|
23
|
+
# ============================================================================
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@mcp.tool
|
|
27
|
+
async def health_check(ctx: Context) -> dict[str, Any]:
|
|
28
|
+
"""Check the health status of the CSV Editor."""
|
|
29
|
+
session_manager = get_session_manager()
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
active_sessions = len(session_manager.sessions)
|
|
33
|
+
|
|
34
|
+
if ctx:
|
|
35
|
+
await ctx.info("Health check performed successfully")
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
"success": True,
|
|
39
|
+
"status": "healthy",
|
|
40
|
+
"version": "2.0.0",
|
|
41
|
+
"active_sessions": active_sessions,
|
|
42
|
+
"max_sessions": session_manager.max_sessions,
|
|
43
|
+
"session_ttl_minutes": session_manager.ttl_minutes,
|
|
44
|
+
}
|
|
45
|
+
except Exception as e:
|
|
46
|
+
if ctx:
|
|
47
|
+
await ctx.error(f"Health check failed: {e!s}")
|
|
48
|
+
return {"success": False, "status": "error", "error": str(e)}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@mcp.tool
|
|
52
|
+
async def get_server_info(ctx: Context) -> dict[str, Any]:
|
|
53
|
+
"""Get information about the CSV Editor capabilities."""
|
|
54
|
+
if ctx:
|
|
55
|
+
await ctx.info("Server information requested")
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
"name": "CSV Editor",
|
|
59
|
+
"version": "2.0.0",
|
|
60
|
+
"description": "A comprehensive MCP server for CSV file operations and data analysis",
|
|
61
|
+
"capabilities": {
|
|
62
|
+
"data_io": [
|
|
63
|
+
"load_csv",
|
|
64
|
+
"load_csv_from_url",
|
|
65
|
+
"load_csv_from_content",
|
|
66
|
+
"export_csv",
|
|
67
|
+
"multiple_export_formats",
|
|
68
|
+
],
|
|
69
|
+
"data_manipulation": [
|
|
70
|
+
"filter_rows",
|
|
71
|
+
"sort_data",
|
|
72
|
+
"select_columns",
|
|
73
|
+
"rename_columns",
|
|
74
|
+
"add_column",
|
|
75
|
+
"remove_columns",
|
|
76
|
+
"change_column_type",
|
|
77
|
+
"fill_missing_values",
|
|
78
|
+
"remove_duplicates",
|
|
79
|
+
],
|
|
80
|
+
"data_analysis": [
|
|
81
|
+
"get_statistics",
|
|
82
|
+
"correlation_matrix",
|
|
83
|
+
"group_by_aggregate",
|
|
84
|
+
"value_counts",
|
|
85
|
+
"detect_outliers",
|
|
86
|
+
"profile_data",
|
|
87
|
+
],
|
|
88
|
+
"data_validation": ["validate_schema", "check_data_quality", "find_anomalies"],
|
|
89
|
+
"session_management": ["multi_session_support", "session_isolation", "auto_cleanup"],
|
|
90
|
+
},
|
|
91
|
+
"supported_formats": ["csv", "tsv", "json", "excel", "parquet", "html", "markdown"],
|
|
92
|
+
"max_file_size_mb": int(os.getenv("CSV_MAX_FILE_SIZE", "1024")),
|
|
93
|
+
"session_timeout_minutes": int(os.getenv("CSV_SESSION_TIMEOUT", "60")),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ============================================================================
|
|
98
|
+
# DATA I/O TOOLS
|
|
99
|
+
# ============================================================================
|
|
100
|
+
|
|
101
|
+
from .tools.io_operations import close_session as _close_session
|
|
102
|
+
from .tools.io_operations import export_csv as _export_csv
|
|
103
|
+
from .tools.io_operations import get_session_info as _get_session_info
|
|
104
|
+
from .tools.io_operations import list_sessions as _list_sessions
|
|
105
|
+
from .tools.io_operations import load_csv as _load_csv
|
|
106
|
+
from .tools.io_operations import load_csv_from_content as _load_csv_from_content
|
|
107
|
+
from .tools.io_operations import load_csv_from_url as _load_csv_from_url
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Register I/O tools with decorators
|
|
111
|
+
@mcp.tool
|
|
112
|
+
async def load_csv(
|
|
113
|
+
file_path: str,
|
|
114
|
+
encoding: str = "utf-8",
|
|
115
|
+
delimiter: str = ",",
|
|
116
|
+
session_id: str | None = None,
|
|
117
|
+
ctx: Context = None,
|
|
118
|
+
) -> dict[str, Any]:
|
|
119
|
+
"""Load a CSV file into a session."""
|
|
120
|
+
return await _load_csv(file_path, encoding, delimiter, session_id, ctx=ctx)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@mcp.tool
|
|
124
|
+
async def load_csv_from_url(
|
|
125
|
+
url: str,
|
|
126
|
+
encoding: str = "utf-8",
|
|
127
|
+
delimiter: str = ",",
|
|
128
|
+
session_id: str | None = None,
|
|
129
|
+
ctx: Context = None,
|
|
130
|
+
) -> dict[str, Any]:
|
|
131
|
+
"""Load a CSV file from a URL."""
|
|
132
|
+
return await _load_csv_from_url(url, encoding, delimiter, session_id, ctx)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@mcp.tool
|
|
136
|
+
async def load_csv_from_content(
|
|
137
|
+
content: str,
|
|
138
|
+
delimiter: str = ",",
|
|
139
|
+
session_id: str | None = None,
|
|
140
|
+
has_header: bool = True,
|
|
141
|
+
ctx: Context = None,
|
|
142
|
+
) -> dict[str, Any]:
|
|
143
|
+
"""Load CSV data from string content."""
|
|
144
|
+
return await _load_csv_from_content(content, delimiter, session_id, has_header, ctx)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@mcp.tool
|
|
148
|
+
async def export_csv(
|
|
149
|
+
session_id: str,
|
|
150
|
+
file_path: str | None = None,
|
|
151
|
+
format: str = "csv",
|
|
152
|
+
encoding: str = "utf-8",
|
|
153
|
+
index: bool = False,
|
|
154
|
+
ctx: Context = None,
|
|
155
|
+
) -> dict[str, Any]:
|
|
156
|
+
"""Export session data to various formats."""
|
|
157
|
+
from .models import ExportFormat
|
|
158
|
+
|
|
159
|
+
format_enum = ExportFormat(format)
|
|
160
|
+
return await _export_csv(session_id, file_path, format_enum, encoding, index, ctx)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@mcp.tool
|
|
164
|
+
async def get_session_info(session_id: str, ctx: Context = None) -> dict[str, Any]:
|
|
165
|
+
"""Get information about a specific session."""
|
|
166
|
+
return await _get_session_info(session_id, ctx)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@mcp.tool
|
|
170
|
+
async def list_sessions(ctx: Context = None) -> dict[str, Any]:
|
|
171
|
+
"""List all active sessions."""
|
|
172
|
+
return await _list_sessions(ctx)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@mcp.tool
|
|
176
|
+
async def close_session(session_id: str, ctx: Context = None) -> dict[str, Any]:
|
|
177
|
+
"""Close and clean up a session."""
|
|
178
|
+
return await _close_session(session_id, ctx)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ============================================================================
|
|
182
|
+
# DATA TRANSFORMATION TOOLS
|
|
183
|
+
# ============================================================================
|
|
184
|
+
|
|
185
|
+
from .tools.transformations import add_column as _add_column
|
|
186
|
+
from .tools.transformations import change_column_type as _change_column_type
|
|
187
|
+
from .tools.transformations import fill_missing_values as _fill_missing_values
|
|
188
|
+
from .tools.transformations import filter_rows as _filter_rows
|
|
189
|
+
from .tools.transformations import remove_columns as _remove_columns
|
|
190
|
+
from .tools.transformations import remove_duplicates as _remove_duplicates
|
|
191
|
+
from .tools.transformations import rename_columns as _rename_columns
|
|
192
|
+
from .tools.transformations import select_columns as _select_columns
|
|
193
|
+
from .tools.transformations import sort_data as _sort_data
|
|
194
|
+
from .tools.transformations import update_column as _update_column
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@mcp.tool
|
|
198
|
+
async def filter_rows(
|
|
199
|
+
session_id: str, conditions: list[dict[str, Any]], mode: str = "and", ctx: Context = None
|
|
200
|
+
) -> dict[str, Any]:
|
|
201
|
+
"""Filter rows based on conditions."""
|
|
202
|
+
return await _filter_rows(session_id, conditions, mode, ctx)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@mcp.tool
|
|
206
|
+
async def sort_data(session_id: str, columns: list[Any], ctx: Context = None) -> dict[str, Any]:
|
|
207
|
+
"""Sort data by columns."""
|
|
208
|
+
return await _sort_data(session_id, columns, ctx)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@mcp.tool
|
|
212
|
+
async def select_columns(
|
|
213
|
+
session_id: str, columns: list[str], ctx: Context = None
|
|
214
|
+
) -> dict[str, Any]:
|
|
215
|
+
"""Select specific columns from the dataframe."""
|
|
216
|
+
return await _select_columns(session_id, columns, ctx)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@mcp.tool
|
|
220
|
+
async def rename_columns(
|
|
221
|
+
session_id: str, mapping: dict[str, str], ctx: Context = None
|
|
222
|
+
) -> dict[str, Any]:
|
|
223
|
+
"""Rename columns in the dataframe."""
|
|
224
|
+
return await _rename_columns(session_id, mapping, ctx)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@mcp.tool
|
|
228
|
+
async def add_column(
|
|
229
|
+
session_id: str, name: str, value: Any = None, formula: str | None = None, ctx: Context = None
|
|
230
|
+
) -> dict[str, Any]:
|
|
231
|
+
"""Add a new column to the dataframe."""
|
|
232
|
+
return await _add_column(session_id, name, value, formula, ctx)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@mcp.tool
|
|
236
|
+
async def remove_columns(
|
|
237
|
+
session_id: str, columns: list[str], ctx: Context = None
|
|
238
|
+
) -> dict[str, Any]:
|
|
239
|
+
"""Remove columns from the dataframe."""
|
|
240
|
+
return await _remove_columns(session_id, columns, ctx)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@mcp.tool
|
|
244
|
+
async def change_column_type(
|
|
245
|
+
session_id: str, column: str, dtype: str, errors: str = "coerce", ctx: Context = None
|
|
246
|
+
) -> dict[str, Any]:
|
|
247
|
+
"""Change the data type of a column."""
|
|
248
|
+
return await _change_column_type(session_id, column, dtype, errors, ctx)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@mcp.tool
|
|
252
|
+
async def fill_missing_values(
|
|
253
|
+
session_id: str,
|
|
254
|
+
strategy: str = "drop",
|
|
255
|
+
value: Any = None,
|
|
256
|
+
columns: list[str] | None = None,
|
|
257
|
+
ctx: Context = None,
|
|
258
|
+
) -> dict[str, Any]:
|
|
259
|
+
"""Fill or remove missing values."""
|
|
260
|
+
return await _fill_missing_values(session_id, strategy, value, columns, ctx)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@mcp.tool
|
|
264
|
+
async def remove_duplicates(
|
|
265
|
+
session_id: str, subset: list[str] | None = None, keep: str = "first", ctx: Context = None
|
|
266
|
+
) -> dict[str, Any]:
|
|
267
|
+
"""Remove duplicate rows."""
|
|
268
|
+
return await _remove_duplicates(session_id, subset, keep, ctx)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@mcp.tool
|
|
272
|
+
async def update_column(
|
|
273
|
+
session_id: str,
|
|
274
|
+
column: str,
|
|
275
|
+
operation: str,
|
|
276
|
+
value: Any | None = None,
|
|
277
|
+
pattern: str | None = None,
|
|
278
|
+
replacement: str | None = None,
|
|
279
|
+
ctx: Context = None,
|
|
280
|
+
) -> dict[str, Any]:
|
|
281
|
+
"""Update values in a specific column with simple operations like replace, extract, split, etc."""
|
|
282
|
+
return await _update_column(session_id, column, operation, value, pattern, replacement, ctx)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ============================================================================
|
|
286
|
+
# DATA ANALYTICS TOOLS
|
|
287
|
+
# ============================================================================
|
|
288
|
+
|
|
289
|
+
from .tools.analytics import detect_outliers as _detect_outliers
|
|
290
|
+
from .tools.analytics import get_column_statistics as _get_column_statistics
|
|
291
|
+
from .tools.analytics import get_correlation_matrix as _get_correlation_matrix
|
|
292
|
+
from .tools.analytics import get_statistics as _get_statistics
|
|
293
|
+
from .tools.analytics import get_value_counts as _get_value_counts
|
|
294
|
+
from .tools.analytics import group_by_aggregate as _group_by_aggregate
|
|
295
|
+
from .tools.analytics import profile_data as _profile_data
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@mcp.tool
|
|
299
|
+
async def get_statistics(
|
|
300
|
+
session_id: str,
|
|
301
|
+
columns: list[str] | None = None,
|
|
302
|
+
include_percentiles: bool = True,
|
|
303
|
+
ctx: Context = None,
|
|
304
|
+
) -> dict[str, Any]:
|
|
305
|
+
"""Get statistical summary of numerical columns."""
|
|
306
|
+
return await _get_statistics(session_id, columns, include_percentiles, ctx)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@mcp.tool
|
|
310
|
+
async def get_column_statistics(
|
|
311
|
+
session_id: str, column: str, ctx: Context = None
|
|
312
|
+
) -> dict[str, Any]:
|
|
313
|
+
"""Get detailed statistics for a specific column."""
|
|
314
|
+
return await _get_column_statistics(session_id, column, ctx)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@mcp.tool
|
|
318
|
+
async def get_correlation_matrix(
|
|
319
|
+
session_id: str,
|
|
320
|
+
method: str = "pearson",
|
|
321
|
+
columns: list[str] | None = None,
|
|
322
|
+
min_correlation: float | None = None,
|
|
323
|
+
ctx: Context = None,
|
|
324
|
+
) -> dict[str, Any]:
|
|
325
|
+
"""Calculate correlation matrix for numeric columns."""
|
|
326
|
+
return await _get_correlation_matrix(session_id, method, columns, min_correlation, ctx)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@mcp.tool
|
|
330
|
+
async def group_by_aggregate(
|
|
331
|
+
session_id: str, group_by: list[str], aggregations: dict[str, Any], ctx: Context = None
|
|
332
|
+
) -> dict[str, Any]:
|
|
333
|
+
"""Group data and apply aggregation functions."""
|
|
334
|
+
return await _group_by_aggregate(session_id, group_by, aggregations, ctx)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@mcp.tool
|
|
338
|
+
async def get_value_counts(
|
|
339
|
+
session_id: str,
|
|
340
|
+
column: str,
|
|
341
|
+
normalize: bool = False,
|
|
342
|
+
sort: bool = True,
|
|
343
|
+
ascending: bool = False,
|
|
344
|
+
top_n: int | None = None,
|
|
345
|
+
ctx: Context = None,
|
|
346
|
+
) -> dict[str, Any]:
|
|
347
|
+
"""Get value counts for a column."""
|
|
348
|
+
return await _get_value_counts(session_id, column, normalize, sort, ascending, top_n, ctx)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@mcp.tool
|
|
352
|
+
async def detect_outliers(
|
|
353
|
+
session_id: str,
|
|
354
|
+
columns: list[str] | None = None,
|
|
355
|
+
method: str = "iqr",
|
|
356
|
+
threshold: float = 1.5,
|
|
357
|
+
ctx: Context = None,
|
|
358
|
+
) -> dict[str, Any]:
|
|
359
|
+
"""Detect outliers in numeric columns."""
|
|
360
|
+
return await _detect_outliers(session_id, columns, method, threshold, ctx)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@mcp.tool
|
|
364
|
+
async def profile_data(
|
|
365
|
+
session_id: str,
|
|
366
|
+
include_correlations: bool = True,
|
|
367
|
+
include_outliers: bool = True,
|
|
368
|
+
ctx: Context = None,
|
|
369
|
+
) -> dict[str, Any]:
|
|
370
|
+
"""Generate comprehensive data profile."""
|
|
371
|
+
return await _profile_data(session_id, include_correlations, include_outliers, ctx)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# ============================================================================
|
|
375
|
+
# DATA VALIDATION TOOLS
|
|
376
|
+
# ============================================================================
|
|
377
|
+
|
|
378
|
+
from .tools.validation import check_data_quality as _check_data_quality
|
|
379
|
+
from .tools.validation import find_anomalies as _find_anomalies
|
|
380
|
+
from .tools.validation import validate_schema as _validate_schema
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@mcp.tool
|
|
384
|
+
async def validate_schema(
|
|
385
|
+
session_id: str, schema: dict[str, dict[str, Any]], ctx: Context = None
|
|
386
|
+
) -> dict[str, Any]:
|
|
387
|
+
"""Validate data against a schema definition."""
|
|
388
|
+
return await _validate_schema(session_id, schema, ctx)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@mcp.tool
|
|
392
|
+
async def check_data_quality(
|
|
393
|
+
session_id: str, rules: list[dict[str, Any]] | None = None, ctx: Context = None
|
|
394
|
+
) -> dict[str, Any]:
|
|
395
|
+
"""Check data quality based on predefined or custom rules."""
|
|
396
|
+
return await _check_data_quality(session_id, rules, ctx)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@mcp.tool
|
|
400
|
+
async def find_anomalies(
|
|
401
|
+
session_id: str,
|
|
402
|
+
columns: list[str] | None = None,
|
|
403
|
+
sensitivity: float = 0.95,
|
|
404
|
+
methods: list[str] | None = None,
|
|
405
|
+
ctx: Context = None,
|
|
406
|
+
) -> dict[str, Any]:
|
|
407
|
+
"""Find anomalies in the data using multiple detection methods."""
|
|
408
|
+
return await _find_anomalies(session_id, columns, sensitivity, methods, ctx)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# ============================================================================
|
|
412
|
+
# AUTO-SAVE TOOLS
|
|
413
|
+
# ============================================================================
|
|
414
|
+
|
|
415
|
+
from .tools.auto_save_operations import configure_auto_save as _configure_auto_save
|
|
416
|
+
from .tools.auto_save_operations import disable_auto_save as _disable_auto_save
|
|
417
|
+
from .tools.auto_save_operations import get_auto_save_status as _get_auto_save_status
|
|
418
|
+
from .tools.auto_save_operations import trigger_manual_save as _trigger_manual_save
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@mcp.tool
|
|
422
|
+
async def configure_auto_save(
|
|
423
|
+
session_id: str,
|
|
424
|
+
enabled: bool = True,
|
|
425
|
+
mode: str = "after_operation",
|
|
426
|
+
strategy: str = "backup",
|
|
427
|
+
interval_seconds: int | None = None,
|
|
428
|
+
max_backups: int | None = None,
|
|
429
|
+
backup_dir: str | None = None,
|
|
430
|
+
custom_path: str | None = None,
|
|
431
|
+
format: str = "csv",
|
|
432
|
+
encoding: str = "utf-8",
|
|
433
|
+
ctx: Context = None,
|
|
434
|
+
) -> dict[str, Any]:
|
|
435
|
+
"""Configure auto-save settings for a session."""
|
|
436
|
+
return await _configure_auto_save(
|
|
437
|
+
session_id,
|
|
438
|
+
enabled,
|
|
439
|
+
mode,
|
|
440
|
+
strategy,
|
|
441
|
+
interval_seconds,
|
|
442
|
+
max_backups,
|
|
443
|
+
backup_dir,
|
|
444
|
+
custom_path,
|
|
445
|
+
format,
|
|
446
|
+
encoding,
|
|
447
|
+
ctx,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@mcp.tool
|
|
452
|
+
async def disable_auto_save(session_id: str, ctx: Context = None) -> dict[str, Any]:
|
|
453
|
+
"""Disable auto-save for a session."""
|
|
454
|
+
return await _disable_auto_save(session_id, ctx)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
@mcp.tool
|
|
458
|
+
async def get_auto_save_status(session_id: str, ctx: Context = None) -> dict[str, Any]:
|
|
459
|
+
"""Get auto-save status for a session."""
|
|
460
|
+
return await _get_auto_save_status(session_id, ctx)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@mcp.tool
|
|
464
|
+
async def trigger_manual_save(session_id: str, ctx: Context = None) -> dict[str, Any]:
|
|
465
|
+
"""Manually trigger a save for a session."""
|
|
466
|
+
return await _trigger_manual_save(session_id, ctx)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# ============================================================================
|
|
470
|
+
# HISTORY OPERATIONS
|
|
471
|
+
# ============================================================================
|
|
472
|
+
|
|
473
|
+
from .tools.history_operations import clear_history as _clear_history
|
|
474
|
+
from .tools.history_operations import export_history as _export_history
|
|
475
|
+
from .tools.history_operations import get_operation_history as _get_operation_history
|
|
476
|
+
from .tools.history_operations import redo_operation as _redo_operation
|
|
477
|
+
from .tools.history_operations import restore_to_operation as _restore_to_operation
|
|
478
|
+
from .tools.history_operations import undo_operation as _undo_operation
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@mcp.tool
|
|
482
|
+
async def undo(session_id: str, ctx: Context = None) -> dict[str, Any]:
|
|
483
|
+
"""Undo the last operation in a session."""
|
|
484
|
+
return await _undo_operation(session_id, ctx)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
@mcp.tool
|
|
488
|
+
async def redo(session_id: str, ctx: Context = None) -> dict[str, Any]:
|
|
489
|
+
"""Redo a previously undone operation."""
|
|
490
|
+
return await _redo_operation(session_id, ctx)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@mcp.tool
|
|
494
|
+
async def get_history(
|
|
495
|
+
session_id: str, limit: int | None = None, ctx: Context = None
|
|
496
|
+
) -> dict[str, Any]:
|
|
497
|
+
"""Get operation history for a session."""
|
|
498
|
+
return await _get_operation_history(session_id, limit, ctx)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@mcp.tool
|
|
502
|
+
async def restore_to_operation(
|
|
503
|
+
session_id: str, operation_id: str, ctx: Context = None
|
|
504
|
+
) -> dict[str, Any]:
|
|
505
|
+
"""Restore session data to a specific operation point."""
|
|
506
|
+
return await _restore_to_operation(session_id, operation_id, ctx)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@mcp.tool
|
|
510
|
+
async def clear_history(session_id: str, ctx: Context = None) -> dict[str, Any]:
|
|
511
|
+
"""Clear all operation history for a session."""
|
|
512
|
+
return await _clear_history(session_id, ctx)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
@mcp.tool
|
|
516
|
+
async def export_history(
|
|
517
|
+
session_id: str, file_path: str, format: str = "json", ctx: Context = None
|
|
518
|
+
) -> dict[str, Any]:
|
|
519
|
+
"""Export operation history to a file."""
|
|
520
|
+
return await _export_history(session_id, file_path, format, ctx)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# ============================================================================
|
|
524
|
+
# RESOURCES
|
|
525
|
+
# ============================================================================
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
@mcp.resource("csv://{session_id}/data")
|
|
529
|
+
async def get_csv_data(session_id: str) -> dict[str, Any]:
|
|
530
|
+
"""Get current CSV data from a session."""
|
|
531
|
+
session_manager = get_session_manager()
|
|
532
|
+
session = session_manager.get_session(session_id)
|
|
533
|
+
|
|
534
|
+
if not session or session.df is None:
|
|
535
|
+
return {"error": "Session not found or no data loaded"}
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
"session_id": session_id,
|
|
539
|
+
"data": session.df.to_dict("records"),
|
|
540
|
+
"shape": session.df.shape,
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
@mcp.resource("csv://{session_id}/schema")
|
|
545
|
+
async def get_csv_schema(session_id: str) -> dict[str, Any]:
|
|
546
|
+
"""Get CSV schema information."""
|
|
547
|
+
session_manager = get_session_manager()
|
|
548
|
+
session = session_manager.get_session(session_id)
|
|
549
|
+
|
|
550
|
+
if not session or session.df is None:
|
|
551
|
+
return {"error": "Session not found or no data loaded"}
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
"session_id": session_id,
|
|
555
|
+
"columns": session.df.columns.tolist(),
|
|
556
|
+
"dtypes": {col: str(dtype) for col, dtype in session.df.dtypes.items()},
|
|
557
|
+
"shape": session.df.shape,
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
@mcp.resource("sessions://active")
|
|
562
|
+
async def list_active_sessions() -> list[dict[str, Any]]:
|
|
563
|
+
"""List all active CSV sessions."""
|
|
564
|
+
session_manager = get_session_manager()
|
|
565
|
+
sessions = session_manager.list_sessions()
|
|
566
|
+
return [s.dict() for s in sessions]
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# ============================================================================
|
|
570
|
+
# PROMPTS
|
|
571
|
+
# ============================================================================
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
@mcp.prompt
|
|
575
|
+
def analyze_csv_prompt(session_id: str, analysis_type: str = "summary") -> str:
|
|
576
|
+
"""Generate a prompt to analyze CSV data."""
|
|
577
|
+
return f"""Please analyze the CSV data in session {session_id}.
|
|
578
|
+
|
|
579
|
+
Analysis type: {analysis_type}
|
|
580
|
+
|
|
581
|
+
Provide insights about:
|
|
582
|
+
1. Data quality and completeness
|
|
583
|
+
2. Statistical patterns
|
|
584
|
+
3. Potential issues or anomalies
|
|
585
|
+
4. Recommended transformations or cleanups
|
|
586
|
+
"""
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@mcp.prompt
|
|
590
|
+
def data_cleaning_prompt(session_id: str) -> str:
|
|
591
|
+
"""Generate a prompt for data cleaning suggestions."""
|
|
592
|
+
return f"""Review the data in session {session_id} and suggest cleaning operations.
|
|
593
|
+
|
|
594
|
+
Consider:
|
|
595
|
+
- Missing values and how to handle them
|
|
596
|
+
- Duplicate rows
|
|
597
|
+
- Data type conversions needed
|
|
598
|
+
- Outliers that may need attention
|
|
599
|
+
- Column naming conventions
|
|
600
|
+
"""
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
# ============================================================================
|
|
604
|
+
# MAIN ENTRY POINT
|
|
605
|
+
# ============================================================================
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def main(argv: list[str] | None = None):
|
|
609
|
+
"""Main entry point for the server."""
|
|
610
|
+
import argparse
|
|
611
|
+
|
|
612
|
+
parser = argparse.ArgumentParser(description="CSV Editor")
|
|
613
|
+
parser.add_argument(
|
|
614
|
+
"--transport", choices=["stdio", "http"], default="stdio", help="Transport method (stdio for local clients, http for Streamable HTTP remote)"
|
|
615
|
+
)
|
|
616
|
+
parser.add_argument("--host", default="0.0.0.0", help="Host for HTTP/SSE transport")
|
|
617
|
+
parser.add_argument("--port", type=int, default=8000, help="Port for HTTP/SSE transport")
|
|
618
|
+
parser.add_argument(
|
|
619
|
+
"--log-level",
|
|
620
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
621
|
+
default="INFO",
|
|
622
|
+
help="Logging level",
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
args = parser.parse_args(argv)
|
|
626
|
+
|
|
627
|
+
# Set logging level
|
|
628
|
+
logging.getLogger().setLevel(getattr(logging, args.log_level))
|
|
629
|
+
|
|
630
|
+
logger.info(f"Starting CSV Editor with {args.transport} transport")
|
|
631
|
+
|
|
632
|
+
# Run the server
|
|
633
|
+
if args.transport == "stdio":
|
|
634
|
+
mcp.run()
|
|
635
|
+
else:
|
|
636
|
+
mcp.run(transport=args.transport, host=args.host, port=args.port)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
if __name__ == "__main__":
|
|
640
|
+
main()
|