mle-kit-mcp 1.0.2__tar.gz → 1.1.0__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 (29) hide show
  1. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/PKG-INFO +1 -1
  2. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp/tools/__init__.py +3 -0
  3. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp/tools/bash.py +8 -0
  4. mle_kit_mcp-1.1.0/mle_kit_mcp/tools/file_system.py +155 -0
  5. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp.egg-info/PKG-INFO +1 -1
  6. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp.egg-info/SOURCES.txt +2 -0
  7. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/pyproject.toml +1 -1
  8. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/tests/test_bash.py +7 -0
  9. mle_kit_mcp-1.1.0/tests/test_file_system.py +74 -0
  10. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/LICENSE +0 -0
  11. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/README.md +0 -0
  12. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp/__init__.py +0 -0
  13. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp/__main__.py +0 -0
  14. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp/files.py +0 -0
  15. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp/llm_proxy.py +0 -0
  16. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp/py.typed +0 -0
  17. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp/server.py +0 -0
  18. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp/tools/llm_proxy.py +0 -0
  19. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp/tools/remote_gpu.py +0 -0
  20. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp/tools/text_editor.py +0 -0
  21. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp/utils.py +0 -0
  22. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp.egg-info/dependency_links.txt +0 -0
  23. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp.egg-info/entry_points.txt +0 -0
  24. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp.egg-info/requires.txt +0 -0
  25. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/mle_kit_mcp.egg-info/top_level.txt +0 -0
  26. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/setup.cfg +0 -0
  27. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/tests/test_llm_proxy.py +0 -0
  28. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/tests/test_text_editor.py +0 -0
  29. {mle_kit_mcp-1.0.2 → mle_kit_mcp-1.1.0}/tests/test_truncate_context.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mle-kit-mcp
3
- Version: 1.0.2
3
+ Version: 1.1.0
4
4
  Summary: MCP server that provides different tools for MLE
5
5
  Author-email: Ilya Gusev <phoenixilya@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/IlyaGusev/mle_kit_mcp
@@ -8,6 +8,7 @@ from .llm_proxy import (
8
8
  llm_proxy_local,
9
9
  llm_proxy_remote,
10
10
  )
11
+ from .file_system import glob, grep
11
12
 
12
13
 
13
14
  __all__ = [
@@ -17,4 +18,6 @@ __all__ = [
17
18
  "remote_download",
18
19
  "llm_proxy_local",
19
20
  "llm_proxy_remote",
21
+ "glob",
22
+ "grep",
20
23
  ]
@@ -1,6 +1,7 @@
1
1
  import atexit
2
2
  import signal
3
3
  import shlex
4
+ import os
4
5
  from typing import Optional, Any
5
6
 
6
7
  from docker import from_env as docker_from_env # type: ignore
@@ -26,6 +27,8 @@ def get_docker_client() -> DockerClient:
26
27
 
27
28
  def create_container() -> Container:
28
29
  client = get_docker_client()
30
+ uid = os.getuid()
31
+ gid = os.getgid()
29
32
  container = client.containers.run(
30
33
  BASE_IMAGE,
31
34
  "tail -f /dev/null",
@@ -33,6 +36,11 @@ def create_container() -> Container:
33
36
  remove=True,
34
37
  tty=True,
35
38
  stdin_open=True,
39
+ user=f"{uid}:{gid}",
40
+ environment={
41
+ "HOME": DOCKER_WORKSPACE_DIR_PATH,
42
+ "XDG_CACHE_HOME": f"{DOCKER_WORKSPACE_DIR_PATH}/.cache",
43
+ },
36
44
  volumes={
37
45
  get_workspace_dir(): {
38
46
  "bind": DOCKER_WORKSPACE_DIR_PATH,
@@ -0,0 +1,155 @@
1
+ from typing import List, Optional
2
+ from pathlib import Path
3
+ import subprocess
4
+
5
+ from mle_kit_mcp.files import get_workspace_dir
6
+
7
+
8
+ def glob(pattern: str, path: Optional[str] = None) -> List[str]:
9
+ """
10
+ - Fast file pattern matching tool that works with any codebase size
11
+ - Supports glob patterns like "**/*.js" or "src/**/*.ts"
12
+ - Returns matching file paths sorted by modification time
13
+ - Use this tool when you need to find files by name patterns
14
+
15
+ Args:
16
+ pattern: The glob pattern to match files against, required.
17
+ path: The directory to search in. If not specified, the current working directory will be used.
18
+ IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null".
19
+ Simply omit it for the default behavior.
20
+ Must be a valid directory path if provided.
21
+
22
+ Returns:
23
+ A list of matching file paths.
24
+ """
25
+ full_path: Path = get_workspace_dir()
26
+ if path is not None:
27
+ full_path = get_workspace_dir() / path
28
+ files = [p for p in full_path.glob(pattern)]
29
+ files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
30
+ resolved_files = [str(p.resolve()) for p in files]
31
+ resolved_files = [f.replace(str(get_workspace_dir()) + "/", "") for f in resolved_files]
32
+ return resolved_files
33
+
34
+
35
+ def grep(
36
+ pattern: str,
37
+ path: Optional[str] = None,
38
+ glob: Optional[str] = None,
39
+ output_mode: str = "files_with_matches",
40
+ before_context: Optional[int] = None,
41
+ after_context: Optional[int] = None,
42
+ center_context: Optional[int] = None,
43
+ insensitive: bool = False,
44
+ type: Optional[str] = None,
45
+ head_limit: Optional[int] = None,
46
+ multiline: bool = False,
47
+ ) -> str:
48
+ r"""
49
+ A powerful search tool built on ripgrep
50
+
51
+ Usage:
52
+ - ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command.
53
+ - The Grep tool has been optimized for correct permissions and access.
54
+ - Supports full regex syntax (e.g., "log.*Error")
55
+ - Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
56
+ - Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
57
+ - Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use `interface\{\}` to find `interface{}` in Go code)
58
+ - Multiline matching: By default patterns match within single lines only. For cross-line patterns like `struct \{[\s\S]*?field`, use `multiline: true`
59
+
60
+ Args:
61
+ pattern: The regular expression pattern to search for in file contents
62
+ path: File or directory to search. Defaults to current working directory.
63
+ glob: Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}")
64
+ output_mode: The output mode to use. Possible values are "content", "files_with_matches", "count".
65
+ "content" shows matching lines (supports -A/-B/-C context, -n line numbers, head_limit)
66
+ "files_with_matches" shows file paths (supports head_limit)
67
+ "count" shows match counts (supports head_limit).
68
+ Defaults to "files_with_matches"
69
+ before_context: Number of lines to show before each match (rg -B). Requires output_mode: "content", ignored otherwise.
70
+ after_context: Number of lines to show after each match (rg -A). Requires output_mode: "content", ignored otherwise.
71
+ center_context: Number of lines to show before and after each match (rg -C). Requires output_mode: "content", ignored otherwise.
72
+ insensitive: Case insensitive search (rg -i). Defaults to False.
73
+ type: File type to search (rg --type). Common types: js, py, rust, go, java, etc. More efficient than include for standard file types.
74
+ head_limit: Limit output to first N lines/entries, equivalent to "| head -N". Works across all output modes: content (limits output lines), files_with_matches (limits file paths), count (limits count entries). When unspecified, shows all results from ripgrep.
75
+ multiline: Enable multiline mode where . matches newlines and patterns can span lines (rg -U --multiline-dotall). Default: false.
76
+
77
+ Returns:
78
+ A string with the search results.
79
+ """
80
+ assert pattern, "'pattern' must be a non-empty string"
81
+
82
+ valid_output_modes = {"content", "files_with_matches", "count"}
83
+ assert (
84
+ output_mode in valid_output_modes
85
+ ), f"Invalid output_mode: {output_mode}. Expected one of {sorted(valid_output_modes)}"
86
+
87
+ if center_context is not None:
88
+ assert (
89
+ before_context is None and after_context is None
90
+ ), "Use either 'center_context' or ('before_context'/'after_context'), not both"
91
+
92
+ cmd: List[str] = [
93
+ "rg",
94
+ "--color=never",
95
+ "--no-heading",
96
+ ]
97
+
98
+ if output_mode == "files_with_matches":
99
+ cmd.append("-l")
100
+ elif output_mode == "count":
101
+ cmd.append("-c")
102
+ else:
103
+ cmd.append("-n")
104
+
105
+ if insensitive:
106
+ cmd.append("-i")
107
+
108
+ if multiline:
109
+ cmd.extend(["--multiline", "--multiline-dotall"])
110
+
111
+ if type:
112
+ cmd.extend(["--type", type])
113
+
114
+ if glob:
115
+ cmd.extend(["--glob", glob])
116
+
117
+ if output_mode == "content":
118
+ if center_context is not None:
119
+ cmd.extend(["-C", str(center_context)])
120
+ else:
121
+ if before_context is not None:
122
+ cmd.extend(["-B", str(before_context)])
123
+ if after_context is not None:
124
+ cmd.extend(["-A", str(after_context)])
125
+
126
+ cmd.append("--")
127
+ cmd.append(pattern)
128
+
129
+ search_root: Path = get_workspace_dir()
130
+ if path is not None:
131
+ search_root = search_root / path
132
+ cmd.append(str(search_root))
133
+
134
+ try:
135
+ result = subprocess.run(
136
+ cmd,
137
+ stdout=subprocess.PIPE,
138
+ stderr=subprocess.PIPE,
139
+ text=True,
140
+ check=False,
141
+ )
142
+ except FileNotFoundError:
143
+ return "ripgrep (rg) not found. Please install ripgrep in the environment where this tool runs."
144
+
145
+ stdout = (result.stdout or "").rstrip("\n")
146
+ stderr = (result.stderr or "").strip()
147
+
148
+ if head_limit is not None and head_limit >= 0 and stdout:
149
+ stdout_lines = stdout.splitlines()
150
+ stdout = "\n".join(stdout_lines[:head_limit])
151
+
152
+ if result.returncode not in (0, 1) and stderr:
153
+ return stderr
154
+
155
+ return stdout
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mle-kit-mcp
3
- Version: 1.0.2
3
+ Version: 1.1.0
4
4
  Summary: MCP server that provides different tools for MLE
5
5
  Author-email: Ilya Gusev <phoenixilya@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/IlyaGusev/mle_kit_mcp
@@ -16,10 +16,12 @@ mle_kit_mcp.egg-info/requires.txt
16
16
  mle_kit_mcp.egg-info/top_level.txt
17
17
  mle_kit_mcp/tools/__init__.py
18
18
  mle_kit_mcp/tools/bash.py
19
+ mle_kit_mcp/tools/file_system.py
19
20
  mle_kit_mcp/tools/llm_proxy.py
20
21
  mle_kit_mcp/tools/remote_gpu.py
21
22
  mle_kit_mcp/tools/text_editor.py
22
23
  tests/test_bash.py
24
+ tests/test_file_system.py
23
25
  tests/test_llm_proxy.py
24
26
  tests/test_text_editor.py
25
27
  tests/test_truncate_context.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mle-kit-mcp"
7
- version = "1.0.2"
7
+ version = "1.1.0"
8
8
  description = "MCP server that provides different tools for MLE"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -33,3 +33,10 @@ def test_bash_timeout_with_output() -> None:
33
33
  result = bash("echo 'hello' && sleep 100", timeout=5)
34
34
  assert "hello" in result
35
35
  assert "Command timed out" in result
36
+
37
+
38
+ def test_bash_ownership() -> None:
39
+ bash("touch dummy")
40
+ assert os.path.exists(get_workspace_dir() / "dummy")
41
+ assert os.stat(get_workspace_dir() / "dummy").st_uid == os.getuid()
42
+ assert os.stat(get_workspace_dir() / "dummy").st_gid == os.getgid()
@@ -0,0 +1,74 @@
1
+
2
+ import shutil
3
+ import pytest
4
+
5
+ from mle_kit_mcp.tools import glob, bash, grep
6
+
7
+ rg_missing = shutil.which("rg") is None
8
+
9
+
10
+ def test_glob_base():
11
+ bash("mkdir -p dummy_dir")
12
+ bash("touch dummy.txt", cwd="dummy_dir")
13
+ assert glob("**/*.txt") == ["dummy_dir/dummy.txt"]
14
+
15
+
16
+ @pytest.mark.skipif(rg_missing, reason="ripgrep (rg) is not installed")
17
+ def test_grep_files_with_matches_basic():
18
+ bash("mkdir -p rg_dummy")
19
+ bash("printf 'hello\\nworld\\n' > a.txt", cwd="rg_dummy")
20
+ bash("printf 'print(\"Hello\")\\n' > b.py", cwd="rg_dummy")
21
+ bash("printf 'bye\\n' > c.txt", cwd="rg_dummy")
22
+
23
+ out = grep("hello", path="rg_dummy")
24
+ assert "a.txt" in out
25
+ assert "b.py" not in out
26
+
27
+
28
+ @pytest.mark.skipif(rg_missing, reason="ripgrep (rg) is not installed")
29
+ def test_grep_content_mode_with_context_and_insensitive():
30
+ bash("mkdir -p rg_dummy2")
31
+ bash("printf 'alpha\\nBravo\\ncharlie\\n' > notes.txt", cwd="rg_dummy2")
32
+ bash("printf 'print(\"Hello\")\\n' > code.py", cwd="rg_dummy2")
33
+
34
+ out = grep(
35
+ "hello",
36
+ path="rg_dummy2",
37
+ output_mode="content",
38
+ center_context=1,
39
+ insensitive=True,
40
+ )
41
+ assert "code.py" in out
42
+ assert ":" in out
43
+
44
+
45
+ @pytest.mark.skipif(rg_missing, reason="ripgrep (rg) is not installed")
46
+ def test_grep_count_mode_with_glob_and_head_limit():
47
+ bash("mkdir -p rg_dummy3")
48
+ bash("printf 'hello\\nworld\\n' > a.txt", cwd="rg_dummy3")
49
+ bash("printf 'foo\\nbar\\n' > m.txt", cwd="rg_dummy3")
50
+
51
+ out = grep(
52
+ "o",
53
+ path="rg_dummy3",
54
+ output_mode="count",
55
+ glob="*.txt",
56
+ head_limit=1,
57
+ )
58
+ lines = [line for line in out.splitlines() if line.strip()]
59
+ assert len(lines) <= 1
60
+ assert ".txt:" in lines[0]
61
+
62
+
63
+ @pytest.mark.skipif(rg_missing, reason="ripgrep (rg) is not installed")
64
+ def test_grep_multiline_matching():
65
+ bash("mkdir -p rg_dummy4")
66
+ bash("printf 'foo\\nbar\\n' > multi.txt", cwd="rg_dummy4")
67
+
68
+ out = grep(
69
+ "foo.*bar",
70
+ path="rg_dummy4",
71
+ output_mode="content",
72
+ multiline=True,
73
+ )
74
+ assert "multi.txt" in out
File without changes
File without changes
File without changes