data-filter-mcp 0.2.4__tar.gz → 0.2.6__tar.gz

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.
Files changed (32) hide show
  1. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/PKG-INFO +1 -1
  2. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp/_version.py +3 -3
  3. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp/server.py +1 -1
  4. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp/validator.py +20 -2
  5. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp.egg-info/PKG-INFO +1 -1
  6. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/tests/test_validator.py +40 -0
  7. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/.github/workflows/ci.yml +0 -0
  8. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/.github/workflows/release.yml +0 -0
  9. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/.gitignore +0 -0
  10. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/LICENSE +0 -0
  11. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/README.md +0 -0
  12. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp/__init__.py +0 -0
  13. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp/loaders/__init__.py +0 -0
  14. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp/loaders/base.py +0 -0
  15. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp/loaders/factory.py +0 -0
  16. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp/loaders/json_loader.py +0 -0
  17. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp/loaders/txt_loader.py +0 -0
  18. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp/loaders/yaml_loader.py +0 -0
  19. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp/models.py +0 -0
  20. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp/registry.py +0 -0
  21. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp.egg-info/SOURCES.txt +0 -0
  22. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp.egg-info/dependency_links.txt +0 -0
  23. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp.egg-info/entry_points.txt +0 -0
  24. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp.egg-info/requires.txt +0 -0
  25. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/data_filter_mcp.egg-info/top_level.txt +0 -0
  26. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/pyproject.toml +0 -0
  27. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/server.py +0 -0
  28. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/setup.cfg +0 -0
  29. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/tests/test_loaders.py +0 -0
  30. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/tests/test_mcp_tools.py +0 -0
  31. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/tests/test_registry.py +0 -0
  32. {data_filter_mcp-0.2.4 → data_filter_mcp-0.2.6}/tests/test_server_flow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-filter-mcp
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Local MCP server for running restricted Python text filters over files
5
5
  Author: Aleksei Arakelian
6
6
  License-Expression: MIT
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.4'
22
- __version_tuple__ = version_tuple = (0, 2, 4)
21
+ __version__ = version = '0.2.6'
22
+ __version_tuple__ = version_tuple = (0, 2, 6)
23
23
 
24
- __commit_id__ = commit_id = 'gd2ae7c43f'
24
+ __commit_id__ = commit_id = 'ga0ff2aab6'
@@ -239,7 +239,7 @@ def create_mcp_server(service: FilterService | None = None) -> FastMCP:
239
239
  The function must return a text result (str). The returned text may contain any
240
240
  format you want, such as plain text, YAML, CSV-like text, or a custom report.
241
241
 
242
- Preloaded standard-library modules:
242
+ Preloaded standard-library modules (don't try to import them in your functions):
243
243
  - json, yaml, re
244
244
  - math, statistics, datetime, decimal
245
245
  - collections, itertools, functools, operator
@@ -56,6 +56,10 @@ SAFE_ATTRIBUTE_READS = {
56
56
  ("itertools", "chain"),
57
57
  }
58
58
 
59
+ SAFE_DUNDER_ATTRIBUTES = {
60
+ "__name__",
61
+ }
62
+
59
63
  SAFE_BUILTINS: dict[str, Any] = {
60
64
  "all": all,
61
65
  "any": any,
@@ -71,10 +75,14 @@ SAFE_BUILTINS: dict[str, Any] = {
71
75
  "range": range,
72
76
  "reversed": reversed,
73
77
  "sorted": sorted,
78
+ "set": set,
74
79
  "str": str,
75
80
  "sum": sum,
76
81
  "tuple": tuple,
82
+ "type": type,
77
83
  "zip": zip,
84
+ "isinstance": isinstance,
85
+ "Exception": Exception,
78
86
  }
79
87
 
80
88
  SAFE_METHODS = {
@@ -280,6 +288,7 @@ SAFE_METHODS = {
280
288
  "sha384",
281
289
  "sha512",
282
290
  "shorten",
291
+ "sort",
283
292
  "span",
284
293
  "split",
285
294
  "splitlines",
@@ -355,6 +364,8 @@ ALLOWED_NODE_TYPES = {
355
364
  ast.Constant,
356
365
  ast.List,
357
366
  ast.Tuple,
367
+ ast.Try,
368
+ ast.ExceptHandler,
358
369
  ast.Set,
359
370
  ast.Dict,
360
371
  ast.Subscript,
@@ -472,14 +483,21 @@ class FilterValidator(ast.NodeVisitor):
472
483
  self.generic_visit(node)
473
484
 
474
485
  def visit_Attribute(self, node: ast.Attribute) -> None:
475
- if node.attr.startswith("_"):
486
+ is_safe_dunder = node.attr in SAFE_DUNDER_ATTRIBUTES
487
+ if node.attr.startswith("_") and not is_safe_dunder:
476
488
  raise FilterValidationError(f"Attribute access is not allowed: {node.attr}")
477
489
 
478
490
  parent = self._parents.get(id(node))
479
491
  if self._is_safe_attribute_read(node):
480
492
  return
481
493
 
482
- if not isinstance(parent, ast.Call) or parent.func is not node:
494
+ is_call_target = isinstance(parent, ast.Call) and parent.func is node
495
+
496
+ if is_safe_dunder and not is_call_target:
497
+ self.visit(node.value)
498
+ return
499
+
500
+ if not is_call_target:
483
501
  raise FilterValidationError(
484
502
  "Attribute access is restricted to approved method calls"
485
503
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-filter-mcp
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Local MCP server for running restricted Python text filters over files
5
5
  Author: Aleksei Arakelian
6
6
  License-Expression: MIT
@@ -144,6 +144,46 @@ def test_compile_filter_rejects_attribute_reads() -> None:
144
144
  compile_filter(code)
145
145
 
146
146
 
147
+ def test_compile_filter_allows_dunder_name_attribute() -> None:
148
+ code = textwrap.dedent(
149
+ """
150
+ def filter_item(data):
151
+ return type(data).__name__
152
+ """
153
+ )
154
+
155
+ filter_fn = compile_filter(code)
156
+
157
+ assert filter_fn([1, 2, 3]) == "list"
158
+ assert filter_fn({"k": 1}) == "dict"
159
+ assert filter_fn("hello") == "str"
160
+
161
+
162
+ def test_compile_filter_allows_dunder_name_in_lambda() -> None:
163
+ code = textwrap.dedent(
164
+ """
165
+ def filter_item(data):
166
+ return ",".join(sorted([type(item).__name__ for item in data]))
167
+ """
168
+ )
169
+
170
+ filter_fn = compile_filter(code)
171
+
172
+ assert filter_fn([1, "x", 2.0]) == "float,int,str"
173
+
174
+
175
+ def test_compile_filter_rejects_calling_dunder_name() -> None:
176
+ code = textwrap.dedent(
177
+ """
178
+ def filter_item(data):
179
+ return data.__name__()
180
+ """
181
+ )
182
+
183
+ with pytest.raises(FilterValidationError, match="Method is not allowed: __name__"):
184
+ compile_filter(code)
185
+
186
+
147
187
  def test_compile_filter_rejects_string_format_methods() -> None:
148
188
  code = textwrap.dedent(
149
189
  """
File without changes