mcp-stata 1.6.2__py3-none-any.whl → 1.7.3__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.
Potentially problematic release.
This version of mcp-stata might be problematic. Click here for more details.
- mcp_stata/discovery.py +315 -77
- mcp_stata/server.py +7 -0
- mcp_stata/stata_client.py +328 -77
- mcp_stata/ui_http.py +61 -6
- {mcp_stata-1.6.2.dist-info → mcp_stata-1.7.3.dist-info}/METADATA +68 -2
- mcp_stata-1.7.3.dist-info/RECORD +14 -0
- mcp_stata-1.6.2.dist-info/RECORD +0 -14
- {mcp_stata-1.6.2.dist-info → mcp_stata-1.7.3.dist-info}/WHEEL +0 -0
- {mcp_stata-1.6.2.dist-info → mcp_stata-1.7.3.dist-info}/entry_points.txt +0 -0
- {mcp_stata-1.6.2.dist-info → mcp_stata-1.7.3.dist-info}/licenses/LICENSE +0 -0
mcp_stata/ui_http.py
CHANGED
|
@@ -27,6 +27,7 @@ class ViewHandle:
|
|
|
27
27
|
view_id: str
|
|
28
28
|
dataset_id: str
|
|
29
29
|
frame: str
|
|
30
|
+
filter_expr: str
|
|
30
31
|
obs_indices: list[int]
|
|
31
32
|
filtered_n: int
|
|
32
33
|
created_at: float
|
|
@@ -86,7 +87,7 @@ class UIChannelManager:
|
|
|
86
87
|
return UIChannelInfo(base_url=base_url, token=self._token or "", expires_at=self._expires_at)
|
|
87
88
|
|
|
88
89
|
def capabilities(self) -> dict[str, bool]:
|
|
89
|
-
return {"dataBrowser": True, "filtering": True}
|
|
90
|
+
return {"dataBrowser": True, "filtering": True, "sorting": True}
|
|
90
91
|
|
|
91
92
|
def current_dataset_id(self) -> str:
|
|
92
93
|
with self._lock:
|
|
@@ -138,6 +139,7 @@ class UIChannelManager:
|
|
|
138
139
|
view_id=view_id,
|
|
139
140
|
dataset_id=current_id,
|
|
140
141
|
frame=frame,
|
|
142
|
+
filter_expr=filter_expr,
|
|
141
143
|
obs_indices=obs_indices,
|
|
142
144
|
filtered_n=len(obs_indices),
|
|
143
145
|
created_at=now,
|
|
@@ -290,11 +292,16 @@ class UIChannelManager:
|
|
|
290
292
|
body = self._read_json()
|
|
291
293
|
if body is None:
|
|
292
294
|
return
|
|
295
|
+
# Debug logging to diagnose limit parameter issues
|
|
296
|
+
import sys
|
|
297
|
+
print(f"[DEBUG] /v1/page request body: {body}", file=sys.stderr, flush=True)
|
|
298
|
+
print(f"[DEBUG] limit value: {body.get('limit')!r} (type: {type(body.get('limit')).__name__})", file=sys.stderr, flush=True)
|
|
293
299
|
try:
|
|
294
300
|
resp = handle_page_request(manager, body, view_id=None)
|
|
295
301
|
self._send_json(200, resp)
|
|
296
302
|
return
|
|
297
303
|
except HTTPError as e:
|
|
304
|
+
print(f"[DEBUG] HTTPError: {e.code} - {e.message}", file=sys.stderr, flush=True)
|
|
298
305
|
self._error(e.status, e.code, e.message, stata_rc=e.stata_rc)
|
|
299
306
|
return
|
|
300
307
|
except Exception as e:
|
|
@@ -347,11 +354,16 @@ class UIChannelManager:
|
|
|
347
354
|
body = self._read_json()
|
|
348
355
|
if body is None:
|
|
349
356
|
return
|
|
357
|
+
# Debug logging to diagnose limit parameter issues
|
|
358
|
+
import sys
|
|
359
|
+
print(f"[DEBUG] /v1/views/{view_id}/page request body: {body}", file=sys.stderr, flush=True)
|
|
360
|
+
print(f"[DEBUG] limit value: {body.get('limit')!r} (type: {type(body.get('limit')).__name__})", file=sys.stderr, flush=True)
|
|
350
361
|
try:
|
|
351
362
|
resp = handle_page_request(manager, body, view_id=view_id)
|
|
352
363
|
self._send_json(200, resp)
|
|
353
364
|
return
|
|
354
365
|
except HTTPError as e:
|
|
366
|
+
print(f"[DEBUG] HTTPError: {e.code} - {e.message}", file=sys.stderr, flush=True)
|
|
355
367
|
self._error(e.status, e.code, e.message, stata_rc=e.stata_rc)
|
|
356
368
|
return
|
|
357
369
|
except Exception as e:
|
|
@@ -455,16 +467,42 @@ def handle_page_request(manager: UIChannelManager, body: dict[str, Any], *, view
|
|
|
455
467
|
dataset_id = view.dataset_id
|
|
456
468
|
frame = view.frame
|
|
457
469
|
|
|
458
|
-
|
|
459
|
-
|
|
470
|
+
# Parse offset (default 0 is valid since offset >= 0)
|
|
471
|
+
try:
|
|
472
|
+
offset = int(body.get("offset") or 0)
|
|
473
|
+
except (ValueError, TypeError) as e:
|
|
474
|
+
raise HTTPError(400, "invalid_request", f"offset must be a valid integer, got: {body.get('offset')!r}")
|
|
475
|
+
|
|
476
|
+
# Parse limit (no default - must be explicitly provided)
|
|
477
|
+
limit_raw = body.get("limit")
|
|
478
|
+
if limit_raw is None:
|
|
479
|
+
raise HTTPError(400, "invalid_request", "limit is required")
|
|
480
|
+
try:
|
|
481
|
+
limit = int(limit_raw)
|
|
482
|
+
except (ValueError, TypeError) as e:
|
|
483
|
+
raise HTTPError(400, "invalid_request", f"limit must be a valid integer, got: {limit_raw!r}")
|
|
484
|
+
|
|
460
485
|
vars_req = body.get("vars", [])
|
|
461
486
|
include_obs_no = bool(body.get("includeObsNo", False))
|
|
462
|
-
|
|
487
|
+
|
|
488
|
+
# Parse sortBy parameter
|
|
489
|
+
sort_by = body.get("sortBy", [])
|
|
490
|
+
if sort_by is not None and not isinstance(sort_by, list):
|
|
491
|
+
raise HTTPError(400, "invalid_request", f"sortBy must be an array, got: {type(sort_by).__name__}")
|
|
492
|
+
if sort_by and not all(isinstance(s, str) for s in sort_by):
|
|
493
|
+
raise HTTPError(400, "invalid_request", "sortBy must be an array of strings")
|
|
494
|
+
|
|
495
|
+
# Parse maxChars
|
|
496
|
+
max_chars_raw = body.get("maxChars", max_chars)
|
|
497
|
+
try:
|
|
498
|
+
max_chars_req = int(max_chars_raw or max_chars)
|
|
499
|
+
except (ValueError, TypeError) as e:
|
|
500
|
+
raise HTTPError(400, "invalid_request", f"maxChars must be a valid integer, got: {max_chars_raw!r}")
|
|
463
501
|
|
|
464
502
|
if offset < 0:
|
|
465
|
-
raise HTTPError(400, "invalid_request", "offset must be >= 0")
|
|
503
|
+
raise HTTPError(400, "invalid_request", f"offset must be >= 0, got: {offset}")
|
|
466
504
|
if limit <= 0:
|
|
467
|
-
raise HTTPError(400, "invalid_request", "limit must be > 0")
|
|
505
|
+
raise HTTPError(400, "invalid_request", f"limit must be > 0, got: {limit}")
|
|
468
506
|
if limit > max_limit:
|
|
469
507
|
raise HTTPError(400, "request_too_large", f"limit must be <= {max_limit}")
|
|
470
508
|
if max_chars_req <= 0:
|
|
@@ -490,6 +528,20 @@ def handle_page_request(manager: UIChannelManager, body: dict[str, Any], *, view
|
|
|
490
528
|
filtered_n = view.filtered_n
|
|
491
529
|
|
|
492
530
|
try:
|
|
531
|
+
# Apply sorting if requested
|
|
532
|
+
if sort_by:
|
|
533
|
+
try:
|
|
534
|
+
manager._client.apply_sort(sort_by)
|
|
535
|
+
# If sorting with a filtered view, re-compute indices after sort
|
|
536
|
+
if view_id is not None:
|
|
537
|
+
assert view is not None
|
|
538
|
+
obs_indices = manager._client.compute_view_indices(view.filter_expr)
|
|
539
|
+
filtered_n = len(obs_indices)
|
|
540
|
+
except ValueError as e:
|
|
541
|
+
raise HTTPError(400, "invalid_request", f"Invalid sort specification: {e}")
|
|
542
|
+
except RuntimeError as e:
|
|
543
|
+
raise HTTPError(500, "internal_error", f"Failed to apply sort: {e}")
|
|
544
|
+
|
|
493
545
|
dataset_state = manager._client.get_dataset_state()
|
|
494
546
|
page = manager._client.get_page(
|
|
495
547
|
offset=offset,
|
|
@@ -499,6 +551,9 @@ def handle_page_request(manager: UIChannelManager, body: dict[str, Any], *, view
|
|
|
499
551
|
max_chars=max_chars_req,
|
|
500
552
|
obs_indices=obs_indices,
|
|
501
553
|
)
|
|
554
|
+
except HTTPError:
|
|
555
|
+
# Re-raise HTTPError exceptions as-is
|
|
556
|
+
raise
|
|
502
557
|
except RuntimeError as e:
|
|
503
558
|
# StataClient uses RuntimeError("No data in memory") for empty dataset.
|
|
504
559
|
msg = str(e) or "No data in memory"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-stata
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.3
|
|
4
4
|
Summary: A lightweight Model Context Protocol (MCP) server for Stata. Execute commands, inspect data, retrieve stored results (`r()`/`e()`), and view graphs in your chat interface. Built for economists who want to integrate LLM assistance into their Stata workflow.
|
|
5
5
|
Project-URL: Homepage, https://github.com/tmonk/mcp-stata
|
|
6
6
|
Project-URL: Repository, https://github.com/tmonk/mcp-stata
|
|
@@ -259,6 +259,14 @@ VS Code documents `.vscode/mcp.json` and the `servers` schema, including `type`
|
|
|
259
259
|
* `get_stored_results()`: Get `r()` and `e()` scalars/macros as JSON.
|
|
260
260
|
* `get_variable_list()`: JSON list of variables and labels.
|
|
261
261
|
|
|
262
|
+
### Cancellation
|
|
263
|
+
|
|
264
|
+
- Clients may cancel an in-flight request by sending the MCP notification `notifications/cancelled` with `params.requestId` set to the original tool call ID.
|
|
265
|
+
- Client guidance:
|
|
266
|
+
1. Pass a `_meta.progressToken` when invoking the tool if you want progress updates (optional).
|
|
267
|
+
2. If you need to cancel, send `notifications/cancelled` with the same requestId. You may also stop tailing the log file path once you receive cancellation confirmation (the tool call will return an error indicating cancellation).
|
|
268
|
+
3. Be prepared for partial output in the log file; cancellation is best-effort and depends on Stata surfacing `BreakError`.
|
|
269
|
+
|
|
262
270
|
Resources exposed for MCP clients:
|
|
263
271
|
|
|
264
272
|
* `stata://data/summary` → `summarize`
|
|
@@ -292,7 +300,8 @@ Call the MCP tool `get_ui_channel()` and parse the JSON:
|
|
|
292
300
|
"expiresAt": 1730000000,
|
|
293
301
|
"capabilities": {
|
|
294
302
|
"dataBrowser": true,
|
|
295
|
-
"filtering": true
|
|
303
|
+
"filtering": true,
|
|
304
|
+
"sorting": true
|
|
296
305
|
}
|
|
297
306
|
}
|
|
298
307
|
```
|
|
@@ -333,10 +342,67 @@ curl -sS \
|
|
|
333
342
|
"$BASE_URL/v1/page"
|
|
334
343
|
```
|
|
335
344
|
|
|
345
|
+
#### Sorting
|
|
346
|
+
|
|
347
|
+
The `/v1/page` and `/v1/views/:viewId/page` endpoints support sorting via the optional `sortBy` parameter:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
# Sort by price ascending
|
|
351
|
+
curl -sS \
|
|
352
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
353
|
+
-H "Content-Type: application/json" \
|
|
354
|
+
-d '{"datasetId":"...","offset":0,"limit":50,"vars":["price","mpg"],"sortBy":["price"]}' \
|
|
355
|
+
"$BASE_URL/v1/page"
|
|
356
|
+
|
|
357
|
+
# Sort by price descending
|
|
358
|
+
curl -sS \
|
|
359
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
360
|
+
-H "Content-Type: application/json" \
|
|
361
|
+
-d '{"datasetId":"...","offset":0,"limit":50,"vars":["price","mpg"],"sortBy":["-price"]}' \
|
|
362
|
+
"$BASE_URL/v1/page"
|
|
363
|
+
|
|
364
|
+
# Multi-variable sort: foreign ascending, then price descending
|
|
365
|
+
curl -sS \
|
|
366
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
367
|
+
-H "Content-Type: application/json" \
|
|
368
|
+
-d '{"datasetId":"...","offset":0,"limit":50,"vars":["foreign","price","mpg"],"sortBy":["foreign","-price"]}' \
|
|
369
|
+
"$BASE_URL/v1/page"
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Sort specification format:**
|
|
373
|
+
- `sortBy` is an array of strings (variable names with optional prefix)
|
|
374
|
+
- No prefix or `+` prefix = ascending order (e.g., `"price"` or `"+price"`)
|
|
375
|
+
- `-` prefix = descending order (e.g., `"-price"`)
|
|
376
|
+
- Multiple variables are supported for multi-level sorting
|
|
377
|
+
- Uses Stata's `gsort` command internally
|
|
378
|
+
|
|
379
|
+
**Sorting with filtered views:**
|
|
380
|
+
- Sorting is fully supported with filtered views
|
|
381
|
+
- The sort is applied to the entire dataset, then filtered indices are re-computed
|
|
382
|
+
- Example: Filter for `price < 5000`, then sort descending by price
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
# Create a filtered view
|
|
386
|
+
curl -sS \
|
|
387
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
388
|
+
-H "Content-Type: application/json" \
|
|
389
|
+
-d '{"datasetId":"...","frame":"default","filterExpr":"price < 5000"}' \
|
|
390
|
+
"$BASE_URL/v1/views"
|
|
391
|
+
# Returns: {"view": {"id": "view_abc123", "filteredN": 37}}
|
|
392
|
+
|
|
393
|
+
# Get sorted page from filtered view
|
|
394
|
+
curl -sS \
|
|
395
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
396
|
+
-H "Content-Type: application/json" \
|
|
397
|
+
-d '{"offset":0,"limit":50,"vars":["price","mpg"],"sortBy":["-price"]}' \
|
|
398
|
+
"$BASE_URL/v1/views/view_abc123/page"
|
|
399
|
+
```
|
|
400
|
+
|
|
336
401
|
Notes:
|
|
337
402
|
|
|
338
403
|
- `datasetId` is used for cache invalidation. If the dataset changes due to running Stata commands, the server will report a new dataset id and view handles become invalid.
|
|
339
404
|
- Filter expressions are evaluated in Python using values read from Stata via `sfi.Data.get`. Use boolean operators like `==`, `!=`, `<`, `>`, and `and`/`or` (Stata-style `&`/`|` are also accepted).
|
|
405
|
+
- Sorting modifies the dataset order in memory using `gsort`. When combined with views, the filtered indices are automatically re-computed after sorting.
|
|
340
406
|
|
|
341
407
|
## License
|
|
342
408
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
mcp_stata/__init__.py,sha256=kJKKRn7lGuVCuS2-GaN5VoVcvnxtNlfuswW_VOlYqwg,98
|
|
2
|
+
mcp_stata/discovery.py,sha256=jQN9uvBNHF_hCCU9k6BDtSdDxiUVpvXcOJwpWYwo55c,17430
|
|
3
|
+
mcp_stata/graph_detector.py,sha256=-dJIU1Dq_c1eQSk4eegUi0gU2N-tFqjFGM0tE1E32KM,16066
|
|
4
|
+
mcp_stata/models.py,sha256=QETpYKO3yILy_L6mhouVEanvUIvu4ww_CAAFuiP2YdM,1201
|
|
5
|
+
mcp_stata/server.py,sha256=PV8ragGMeHT72zgVx5DJp3vt8CPqT8iwdvJ8GXSctds,15989
|
|
6
|
+
mcp_stata/stata_client.py,sha256=06cA5K4vwXc_kNCwIifUL8eSSYsIYtM5zArhJcLcUlo,101267
|
|
7
|
+
mcp_stata/streaming_io.py,sha256=GVaXgTtxx8YLY6RWqdTcO2M3QSqxLsefqkmnlNO1nTI,6974
|
|
8
|
+
mcp_stata/ui_http.py,sha256=w1tYxNuwuhkjyfWHxUnpd1DcVBaakjPkEnWr-Fo1lWo,24193
|
|
9
|
+
mcp_stata/smcl/smcl2html.py,sha256=wi91mOMeV9MCmHtNr0toihNbaiDCNZ_NP6a6xEAzWLM,2624
|
|
10
|
+
mcp_stata-1.7.3.dist-info/METADATA,sha256=cOSWlFgl296f5UhvozBLCPpe7tWS7kcVWGBNlnqO2Hs,15951
|
|
11
|
+
mcp_stata-1.7.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
mcp_stata-1.7.3.dist-info/entry_points.txt,sha256=TcOgrtiTL4LGFEDb1pCrQWA-fUZvIujDOvQ-bWFh5Z8,52
|
|
13
|
+
mcp_stata-1.7.3.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
|
14
|
+
mcp_stata-1.7.3.dist-info/RECORD,,
|
mcp_stata-1.6.2.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
mcp_stata/__init__.py,sha256=kJKKRn7lGuVCuS2-GaN5VoVcvnxtNlfuswW_VOlYqwg,98
|
|
2
|
-
mcp_stata/discovery.py,sha256=K8SK4oEnLESxnJWcBsLK76s85yNVdbLlfOql_Rf94B4,8390
|
|
3
|
-
mcp_stata/graph_detector.py,sha256=-dJIU1Dq_c1eQSk4eegUi0gU2N-tFqjFGM0tE1E32KM,16066
|
|
4
|
-
mcp_stata/models.py,sha256=QETpYKO3yILy_L6mhouVEanvUIvu4ww_CAAFuiP2YdM,1201
|
|
5
|
-
mcp_stata/server.py,sha256=9_0i8xux11NpZ6yJwOniqQo-Cs3ph2mrkr0gwZ9GZ1I,15671
|
|
6
|
-
mcp_stata/stata_client.py,sha256=floQMie0j7xI1PUeVu3mbmv4_eztw2CLjviZ7fdmClo,90673
|
|
7
|
-
mcp_stata/streaming_io.py,sha256=GVaXgTtxx8YLY6RWqdTcO2M3QSqxLsefqkmnlNO1nTI,6974
|
|
8
|
-
mcp_stata/ui_http.py,sha256=UhIPzCpBelVpZWUgwBnGpOCAEpG2vFVvX-TxRuyhkR0,21210
|
|
9
|
-
mcp_stata/smcl/smcl2html.py,sha256=wi91mOMeV9MCmHtNr0toihNbaiDCNZ_NP6a6xEAzWLM,2624
|
|
10
|
-
mcp_stata-1.6.2.dist-info/METADATA,sha256=buX6iqLIsnmtBlVueebezon9o3OYxL6I0DYmmwHOV0I,13141
|
|
11
|
-
mcp_stata-1.6.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
-
mcp_stata-1.6.2.dist-info/entry_points.txt,sha256=TcOgrtiTL4LGFEDb1pCrQWA-fUZvIujDOvQ-bWFh5Z8,52
|
|
13
|
-
mcp_stata-1.6.2.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
|
14
|
-
mcp_stata-1.6.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|