evryth-mcp 0.1.1__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.
- evryth_mcp-0.1.1/.gitignore +7 -0
- evryth_mcp-0.1.1/CONTRIBUTING.md +81 -0
- evryth_mcp-0.1.1/PKG-INFO +109 -0
- evryth_mcp-0.1.1/README.md +99 -0
- evryth_mcp-0.1.1/pyproject.toml +30 -0
- evryth_mcp-0.1.1/src/everything_mcp/__init__.py +5 -0
- evryth_mcp-0.1.1/src/everything_mcp/__main__.py +6 -0
- evryth_mcp-0.1.1/src/everything_mcp/es.py +40 -0
- evryth_mcp-0.1.1/src/everything_mcp/matching.py +31 -0
- evryth_mcp-0.1.1/src/everything_mcp/models.py +59 -0
- evryth_mcp-0.1.1/src/everything_mcp/query.py +50 -0
- evryth_mcp-0.1.1/src/everything_mcp/server.py +32 -0
- evryth_mcp-0.1.1/src/everything_mcp/service.py +358 -0
- evryth_mcp-0.1.1/tests/__init__.py +1 -0
- evryth_mcp-0.1.1/tests/integration.py +108 -0
- evryth_mcp-0.1.1/tests/test_es.py +39 -0
- evryth_mcp-0.1.1/tests/test_server.py +68 -0
- evryth_mcp-0.1.1/tests/test_service.py +73 -0
- evryth_mcp-0.1.1/uv.lock +769 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
## Layout
|
|
4
|
+
|
|
5
|
+
- src/everything_mcp/server.py: MCP registration and application entry point.
|
|
6
|
+
- src/everything_mcp/service.py: search use cases and result assembly.
|
|
7
|
+
- src/everything_mcp/es.py: subprocess and IPC error boundary.
|
|
8
|
+
- src/everything_mcp/query.py: ES argument validation and expressions.
|
|
9
|
+
- src/everything_mcp/models.py: validated public filters and parameter types.
|
|
10
|
+
- src/everything_mcp/matching.py: pinyin matching algorithm.
|
|
11
|
+
- tests/: offline unit/contract tests and opt-in live integration.
|
|
12
|
+
|
|
13
|
+
## Checks
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
uv sync --locked
|
|
17
|
+
uv run ruff check src tests
|
|
18
|
+
uv run ruff format --check src tests
|
|
19
|
+
uv run python -m unittest discover -v
|
|
20
|
+
uv build
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The src layout requires installing the project before importing it. Both
|
|
24
|
+
`evryth-mcp` and `python -m everything_mcp` start the STDIO server.
|
|
25
|
+
|
|
26
|
+
Live integration requires Windows, an accessible Everything IPC instance,
|
|
27
|
+
and the sample file asserted in tests/integration.py:
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
uv run python -m tests.integration
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Live integration is excluded from normal discovery. Adjust the fixed sample
|
|
34
|
+
before using a different machine. Never emit diagnostics on protocol stdout.
|
|
35
|
+
Changes to public tool names or schemas require explicit compatibility review.
|
|
36
|
+
|
|
37
|
+
## GitHub Actions and PyPI
|
|
38
|
+
|
|
39
|
+
`.github/workflows/publish.yml` runs offline tests, Ruff, and distribution checks
|
|
40
|
+
on Windows for branch pushes and pull requests. A push to `master` in the original
|
|
41
|
+
repository also publishes the checked artifacts to PyPI if the version's `v*`
|
|
42
|
+
tag does not exist. After publishing succeeds, it creates that tag at the tested
|
|
43
|
+
commit and a GitHub Release with generated notes and the wheel/source archives.
|
|
44
|
+
PRs and other branches never publish. Only stable public versions are automated.
|
|
45
|
+
The separate Linux publishing job uploads artifacts; it does not run Everything.
|
|
46
|
+
No local publishing script or long-lived PyPI token is required.
|
|
47
|
+
|
|
48
|
+
One-time setup:
|
|
49
|
+
|
|
50
|
+
1. In the GitHub repository settings, create an environment named `pypi`.
|
|
51
|
+
Allow deployment from the `master` branch (not only `v*` tags); enable required
|
|
52
|
+
reviewers if available. Update this rule if you used the old tag workflow.
|
|
53
|
+
2. In PyPI, configure a GitHub Trusted Publisher. For a new project, use a pending
|
|
54
|
+
publisher at https://pypi.org/manage/account/publishing/ with:
|
|
55
|
+
- PyPI project: `evryth-mcp`
|
|
56
|
+
- Owner: `dengbojing`
|
|
57
|
+
- Repository: `everything-mcp`
|
|
58
|
+
- Workflow filename: `publish.yml`
|
|
59
|
+
- Environment: `pypi`
|
|
60
|
+
For an existing project, add the publisher in its publishing settings.
|
|
61
|
+
3. Protect `master` and release tags. Ensure repository rules permit the workflow's
|
|
62
|
+
`GITHUB_TOKEN` to create release tags; only the release job has `contents: write`.
|
|
63
|
+
|
|
64
|
+
To release, update `project.version` in `pyproject.toml`, run `uv lock`, and commit
|
|
65
|
+
and push the changes to `master`:
|
|
66
|
+
|
|
67
|
+
```powershell
|
|
68
|
+
git push origin master
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Use a new version for each release; published distribution filenames cannot be
|
|
72
|
+
reused. Configure the PyPI publisher before pushing an untagged version to master.
|
|
73
|
+
Do not create tags manually: an existing version tag skips automatic publishing.
|
|
74
|
+
If PyPI succeeds but the release job fails, rerun only the failed job in the same
|
|
75
|
+
Actions run; do not rebuild and re-upload the published version. If a partial
|
|
76
|
+
GitHub Release already exists, inspect and complete it manually before retrying.
|
|
77
|
+
Branch runs are serialized; GitHub may replace older pending runs with newer ones.
|
|
78
|
+
If the repository is renamed, update both the workflow repository guard and PyPI
|
|
79
|
+
publisher settings.
|
|
80
|
+
|
|
81
|
+
Reference: https://docs.pypi.org/trusted-publishers/using-a-publisher/
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: evryth-mcp
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Read-only Windows Everything search over MCP stdio
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: mcp<2,>=1.10.1
|
|
7
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
8
|
+
Requires-Dist: pypinyin<1,>=0.55
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# evryth-mcp
|
|
12
|
+
|
|
13
|
+
通过 MCP 调用 Windows 本机 Everything 索引,查找文件和目录。提供 12 个搜索及辅助工具,支持路径、扩展名、大小和修改日期筛选,返回完整路径及文件元数据。
|
|
14
|
+
|
|
15
|
+
## 安装准备
|
|
16
|
+
|
|
17
|
+
当前版本仅支持在 Windows 上运行,依赖本机 Everything 和 ES;macOS、Linux 及其他 Unix 类系统暂不支持原生运行。
|
|
18
|
+
|
|
19
|
+
需安装 Everything、ES、uv(提供 uvx)及 Git,Python 版本要求为 3.11+。
|
|
20
|
+
|
|
21
|
+
1. 从 [voidtools 下载页](https://www.voidtools.com/downloads/) 安装 Everything,并保持运行。
|
|
22
|
+
2. 在同一页面下载 **Everything Command-line Interface(ES)**,解压到固定目录。
|
|
23
|
+
3. 将 **es.exe 所在目录**加入 Windows 用户或系统 PATH,然后重启终端和 MCP 客户端。
|
|
24
|
+
|
|
25
|
+
Everything 与 ES 是不同组件。如果已安装全局 ES,且客户端能从 PATH 找到它,**无需设置 `EVERYTHING_ES_PATH`**。
|
|
26
|
+
|
|
27
|
+
验证安装:
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
es -version
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## MCP 配置
|
|
34
|
+
|
|
35
|
+
适用于使用 `mcpServers` JSON 格式的客户端:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"everything-es": {
|
|
41
|
+
"command": "uvx",
|
|
42
|
+
"args": [
|
|
43
|
+
"--from",
|
|
44
|
+
"git+https://github.com/dengbojing/everything-mcp.git",
|
|
45
|
+
"evryth-mcp"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
以上 GitHub 配置需要仓库已包含新的启动入口。发布到 PyPI 后,可将 `args` 简化为 `["evryth-mcp"]`,即 `uvx evryth-mcp`,届时无需 Git。
|
|
53
|
+
|
|
54
|
+
如果 ES 已在客户端 PATH 中且使用默认 Everything 实例,上面的配置即可,无需添加环境变量。
|
|
55
|
+
|
|
56
|
+
需要指定 ES 路径或命名实例时,在 `everything-es` 配置中添加 `env`:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"mcpServers": {
|
|
61
|
+
"everything-es": {
|
|
62
|
+
"command": "uvx",
|
|
63
|
+
"args": [
|
|
64
|
+
"--from",
|
|
65
|
+
"git+https://github.com/dengbojing/everything-mcp.git",
|
|
66
|
+
"evryth-mcp"
|
|
67
|
+
],
|
|
68
|
+
"env": {
|
|
69
|
+
"EVERYTHING_ES_PATH": "D:\\Tools\\Everything\\es.exe",
|
|
70
|
+
"EVERYTHING_INSTANCE": "Work"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- `EVERYTHING_ES_PATH`:替换为实际 `es.exe` 的绝对路径,不是 `Everything.exe` 或目录;客户端能从 PATH 找到 ES 时可删除此项。
|
|
78
|
+
- `EVERYTHING_INSTANCE`:填写已运行的 Everything 命名实例名称,示例为 `Work`;使用默认实例时删除此项。它不会自动创建或启动实例。
|
|
79
|
+
|
|
80
|
+
这两项可独立使用;如果都不需要,删除整个 `env`。
|
|
81
|
+
|
|
82
|
+
## 工具
|
|
83
|
+
|
|
84
|
+
| 工具 | 功能 |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| everything_search | Everything 原生查询、排序和分页 |
|
|
87
|
+
| everything_search_exact | 完整文件名匹配 |
|
|
88
|
+
| everything_search_keywords | 全部/任一关键词及排除词 |
|
|
89
|
+
| everything_search_wildcard | * 和 ? 通配符 |
|
|
90
|
+
| everything_search_regex | 正则表达式 |
|
|
91
|
+
| everything_search_fuzzy | 连续、非连续及无序关键词匹配 |
|
|
92
|
+
| everything_search_pinyin | 全拼、首字母、中文混输及多音字 |
|
|
93
|
+
| everything_search_by_type | 文档、图片、视频、音频、代码、压缩包 |
|
|
94
|
+
| everything_search_typo | 文件名字符相似度匹配 |
|
|
95
|
+
| everything_list_directory | 直属或递归目录索引项 |
|
|
96
|
+
| everything_count | 匹配数量 |
|
|
97
|
+
| everything_status | 版本与 IPC 状态 |
|
|
98
|
+
|
|
99
|
+
例如,让客户端“查找包含合同和2025、排除草稿的 PDF”,或“按拼音 niandubaogao 搜索年度报告”。具体参数由客户端工具描述提供。
|
|
100
|
+
|
|
101
|
+
## 使用说明
|
|
102
|
+
|
|
103
|
+
- 默认搜索 Everything 全部索引,不额外限制目录;未索引的文件不会出现。
|
|
104
|
+
- 没有专门的文件内容搜索或文件修改工具。
|
|
105
|
+
- 拼音和容错按候选分页匹配,单页无结果不代表全盘不存在;容错分数仅在当前页排序。
|
|
106
|
+
- IPC 不可达时,检查 Everything 是否运行,以及客户端会话和沙箱权限。
|
|
107
|
+
- 终端能执行 ES 而客户端找不到时,先完全重启客户端,使其继承最新 PATH。
|
|
108
|
+
|
|
109
|
+
[ES 官方文档](https://www.voidtools.com/support/everything/command_line_interface/)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# evryth-mcp
|
|
2
|
+
|
|
3
|
+
通过 MCP 调用 Windows 本机 Everything 索引,查找文件和目录。提供 12 个搜索及辅助工具,支持路径、扩展名、大小和修改日期筛选,返回完整路径及文件元数据。
|
|
4
|
+
|
|
5
|
+
## 安装准备
|
|
6
|
+
|
|
7
|
+
当前版本仅支持在 Windows 上运行,依赖本机 Everything 和 ES;macOS、Linux 及其他 Unix 类系统暂不支持原生运行。
|
|
8
|
+
|
|
9
|
+
需安装 Everything、ES、uv(提供 uvx)及 Git,Python 版本要求为 3.11+。
|
|
10
|
+
|
|
11
|
+
1. 从 [voidtools 下载页](https://www.voidtools.com/downloads/) 安装 Everything,并保持运行。
|
|
12
|
+
2. 在同一页面下载 **Everything Command-line Interface(ES)**,解压到固定目录。
|
|
13
|
+
3. 将 **es.exe 所在目录**加入 Windows 用户或系统 PATH,然后重启终端和 MCP 客户端。
|
|
14
|
+
|
|
15
|
+
Everything 与 ES 是不同组件。如果已安装全局 ES,且客户端能从 PATH 找到它,**无需设置 `EVERYTHING_ES_PATH`**。
|
|
16
|
+
|
|
17
|
+
验证安装:
|
|
18
|
+
|
|
19
|
+
```powershell
|
|
20
|
+
es -version
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## MCP 配置
|
|
24
|
+
|
|
25
|
+
适用于使用 `mcpServers` JSON 格式的客户端:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"everything-es": {
|
|
31
|
+
"command": "uvx",
|
|
32
|
+
"args": [
|
|
33
|
+
"--from",
|
|
34
|
+
"git+https://github.com/dengbojing/everything-mcp.git",
|
|
35
|
+
"evryth-mcp"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
以上 GitHub 配置需要仓库已包含新的启动入口。发布到 PyPI 后,可将 `args` 简化为 `["evryth-mcp"]`,即 `uvx evryth-mcp`,届时无需 Git。
|
|
43
|
+
|
|
44
|
+
如果 ES 已在客户端 PATH 中且使用默认 Everything 实例,上面的配置即可,无需添加环境变量。
|
|
45
|
+
|
|
46
|
+
需要指定 ES 路径或命名实例时,在 `everything-es` 配置中添加 `env`:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"everything-es": {
|
|
52
|
+
"command": "uvx",
|
|
53
|
+
"args": [
|
|
54
|
+
"--from",
|
|
55
|
+
"git+https://github.com/dengbojing/everything-mcp.git",
|
|
56
|
+
"evryth-mcp"
|
|
57
|
+
],
|
|
58
|
+
"env": {
|
|
59
|
+
"EVERYTHING_ES_PATH": "D:\\Tools\\Everything\\es.exe",
|
|
60
|
+
"EVERYTHING_INSTANCE": "Work"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- `EVERYTHING_ES_PATH`:替换为实际 `es.exe` 的绝对路径,不是 `Everything.exe` 或目录;客户端能从 PATH 找到 ES 时可删除此项。
|
|
68
|
+
- `EVERYTHING_INSTANCE`:填写已运行的 Everything 命名实例名称,示例为 `Work`;使用默认实例时删除此项。它不会自动创建或启动实例。
|
|
69
|
+
|
|
70
|
+
这两项可独立使用;如果都不需要,删除整个 `env`。
|
|
71
|
+
|
|
72
|
+
## 工具
|
|
73
|
+
|
|
74
|
+
| 工具 | 功能 |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| everything_search | Everything 原生查询、排序和分页 |
|
|
77
|
+
| everything_search_exact | 完整文件名匹配 |
|
|
78
|
+
| everything_search_keywords | 全部/任一关键词及排除词 |
|
|
79
|
+
| everything_search_wildcard | * 和 ? 通配符 |
|
|
80
|
+
| everything_search_regex | 正则表达式 |
|
|
81
|
+
| everything_search_fuzzy | 连续、非连续及无序关键词匹配 |
|
|
82
|
+
| everything_search_pinyin | 全拼、首字母、中文混输及多音字 |
|
|
83
|
+
| everything_search_by_type | 文档、图片、视频、音频、代码、压缩包 |
|
|
84
|
+
| everything_search_typo | 文件名字符相似度匹配 |
|
|
85
|
+
| everything_list_directory | 直属或递归目录索引项 |
|
|
86
|
+
| everything_count | 匹配数量 |
|
|
87
|
+
| everything_status | 版本与 IPC 状态 |
|
|
88
|
+
|
|
89
|
+
例如,让客户端“查找包含合同和2025、排除草稿的 PDF”,或“按拼音 niandubaogao 搜索年度报告”。具体参数由客户端工具描述提供。
|
|
90
|
+
|
|
91
|
+
## 使用说明
|
|
92
|
+
|
|
93
|
+
- 默认搜索 Everything 全部索引,不额外限制目录;未索引的文件不会出现。
|
|
94
|
+
- 没有专门的文件内容搜索或文件修改工具。
|
|
95
|
+
- 拼音和容错按候选分页匹配,单页无结果不代表全盘不存在;容错分数仅在当前页排序。
|
|
96
|
+
- IPC 不可达时,检查 Everything 是否运行,以及客户端会话和沙箱权限。
|
|
97
|
+
- 终端能执行 ES 而客户端找不到时,先完全重启客户端,使其继承最新 PATH。
|
|
98
|
+
|
|
99
|
+
[ES 官方文档](https://www.voidtools.com/support/everything/command_line_interface/)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "evryth-mcp"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Read-only Windows Everything search over MCP stdio"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = ["mcp>=1.10.1,<2", "pypinyin>=0.55,<1", "pydantic>=2.7,<3"]
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
[project.scripts]
|
|
10
|
+
evryth-mcp = "everything_mcp.server:main"
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["hatchling"]
|
|
14
|
+
build-backend = "hatchling.build"
|
|
15
|
+
|
|
16
|
+
[tool.hatch.build.targets.wheel]
|
|
17
|
+
packages = ["src/everything_mcp"]
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.sdist]
|
|
20
|
+
include = ["/src", "/tests", "/README.md", "/CONTRIBUTING.md", "/pyproject.toml", "/uv.lock"]
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = ["ruff>=0.11,<1"]
|
|
24
|
+
|
|
25
|
+
[tool.ruff]
|
|
26
|
+
target-version = "py311"
|
|
27
|
+
line-length = 100
|
|
28
|
+
|
|
29
|
+
[tool.ruff.lint]
|
|
30
|
+
select = ["E4", "E7", "E9", "F", "I"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Execute the local ES process without a shell."""
|
|
2
|
+
|
|
3
|
+
import locale
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run_es(args: list[str], search: bool = False) -> str:
|
|
10
|
+
executable = os.environ.get("EVERYTHING_ES_PATH") or shutil.which("es.exe")
|
|
11
|
+
if not executable:
|
|
12
|
+
raise RuntimeError("ES_NOT_FOUND: install voidtools ES or set EVERYTHING_ES_PATH")
|
|
13
|
+
instance = os.environ.get("EVERYTHING_INSTANCE")
|
|
14
|
+
command = [executable, *(["-instance", instance] if instance else []), *args]
|
|
15
|
+
# ES consumes a Windows search expression. list2cmdline would quote the
|
|
16
|
+
# entire expression containing spaces, changing AND terms into a phrase.
|
|
17
|
+
if search:
|
|
18
|
+
command = subprocess.list2cmdline(command[:-1]) + " " + command[-1]
|
|
19
|
+
try:
|
|
20
|
+
result = subprocess.run(command, capture_output=True, timeout=20, shell=False)
|
|
21
|
+
except subprocess.TimeoutExpired as exc:
|
|
22
|
+
raise RuntimeError("ES_TIMEOUT: Everything did not respond within 20 seconds") from exc
|
|
23
|
+
except OSError as exc:
|
|
24
|
+
raise RuntimeError(f"ES_START_FAILED: {exc}") from exc
|
|
25
|
+
|
|
26
|
+
def decode(raw: bytes) -> str:
|
|
27
|
+
try:
|
|
28
|
+
return raw.decode("utf-8-sig")
|
|
29
|
+
except UnicodeDecodeError:
|
|
30
|
+
return raw.decode(locale.getpreferredencoding(False), errors="strict")
|
|
31
|
+
|
|
32
|
+
output, error = decode(result.stdout), decode(result.stderr)
|
|
33
|
+
if result.returncode:
|
|
34
|
+
details = (error or output).strip()
|
|
35
|
+
if "IPC not found" in details:
|
|
36
|
+
raise RuntimeError(
|
|
37
|
+
"ES_IPC_UNAVAILABLE: Everything IPC is inaccessible. Check running instance, Windows session and host sandbox permissions; this is not a zero-match result."
|
|
38
|
+
)
|
|
39
|
+
raise RuntimeError(f"ES_ERROR ({result.returncode}): {details[:2000]}")
|
|
40
|
+
return output
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Filename pinyin matching independent of MCP and ES."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def pinyin_match(name: str, needle: str, mode: str, heteronym: bool) -> bool:
|
|
5
|
+
"""Bounded dynamic matching; no exponential polyphonic combinations."""
|
|
6
|
+
from pypinyin import Style, pinyin
|
|
7
|
+
|
|
8
|
+
readings = (
|
|
9
|
+
[pinyin(char, style=Style.NORMAL, heteronym=True)[0] for char in name]
|
|
10
|
+
if heteronym
|
|
11
|
+
else pinyin(name, style=Style.NORMAL, errors=lambda chars: list(chars))
|
|
12
|
+
)
|
|
13
|
+
states = {0}
|
|
14
|
+
for char, sounds in zip(name, readings):
|
|
15
|
+
choices = set()
|
|
16
|
+
if mode in ("full", "both", "mixed"):
|
|
17
|
+
choices.update(sounds)
|
|
18
|
+
if mode in ("initials", "both", "mixed"):
|
|
19
|
+
choices.update(s[0] for s in sounds if s)
|
|
20
|
+
if mode == "mixed":
|
|
21
|
+
choices.add(char)
|
|
22
|
+
next_states = {0}
|
|
23
|
+
for index in states:
|
|
24
|
+
for choice in choices:
|
|
25
|
+
if needle.startswith(choice.lower(), index):
|
|
26
|
+
end = index + len(choice)
|
|
27
|
+
if end == len(needle):
|
|
28
|
+
return True
|
|
29
|
+
next_states.add(end)
|
|
30
|
+
states = next_states
|
|
31
|
+
return False
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Validated search filters and public parameter types."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from datetime import date, timedelta
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
8
|
+
|
|
9
|
+
Sort = Literal["name", "path", "size", "extension", "date-created", "date-modified"]
|
|
10
|
+
Kind = Literal["all", "file", "folder"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Filters(BaseModel):
|
|
14
|
+
"""Index metadata only. Dates include the whole calendar day; size is bytes."""
|
|
15
|
+
|
|
16
|
+
model_config = ConfigDict(extra="forbid")
|
|
17
|
+
extensions: list[str] = Field(default_factory=list, max_length=30)
|
|
18
|
+
min_bytes: int | None = Field(default=None, ge=0)
|
|
19
|
+
max_bytes: int | None = Field(default=None, ge=0)
|
|
20
|
+
modified_from: date | None = None
|
|
21
|
+
modified_to: date | None = None
|
|
22
|
+
recent_days: int | None = Field(default=None, ge=1, le=36500)
|
|
23
|
+
|
|
24
|
+
@model_validator(mode="after")
|
|
25
|
+
def validate_ranges(self):
|
|
26
|
+
if (
|
|
27
|
+
self.min_bytes is not None
|
|
28
|
+
and self.max_bytes is not None
|
|
29
|
+
and self.min_bytes > self.max_bytes
|
|
30
|
+
):
|
|
31
|
+
raise ValueError("min_bytes exceeds max_bytes")
|
|
32
|
+
if self.modified_from and self.modified_to and self.modified_from > self.modified_to:
|
|
33
|
+
raise ValueError("modified_from exceeds modified_to")
|
|
34
|
+
if self.recent_days and (self.modified_from or self.modified_to):
|
|
35
|
+
raise ValueError("use recent_days or a date range, not both")
|
|
36
|
+
for ext in self.extensions:
|
|
37
|
+
if not re.fullmatch(r"\.?[A-Za-z0-9]+", ext):
|
|
38
|
+
raise ValueError("extensions must be plain extensions, e.g. pdf or .docx")
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
def expression(self):
|
|
42
|
+
terms = []
|
|
43
|
+
if self.extensions:
|
|
44
|
+
terms.append("ext:" + ";".join(e.lstrip(".") for e in self.extensions))
|
|
45
|
+
if self.min_bytes is not None:
|
|
46
|
+
terms.append(f"size:>={self.min_bytes}")
|
|
47
|
+
if self.max_bytes is not None:
|
|
48
|
+
terms.append(f"size:<={self.max_bytes}")
|
|
49
|
+
start = (
|
|
50
|
+
date.today() - timedelta(days=self.recent_days - 1)
|
|
51
|
+
if self.recent_days
|
|
52
|
+
else self.modified_from
|
|
53
|
+
)
|
|
54
|
+
end = date.today() if self.recent_days else self.modified_to
|
|
55
|
+
if start:
|
|
56
|
+
terms.append(f"dm:>={start.isoformat()}")
|
|
57
|
+
if end:
|
|
58
|
+
terms.append(f"dm:<{(end + timedelta(days=1)).isoformat()}")
|
|
59
|
+
return " ".join(terms)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Build and validate ES search arguments."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from .models import Kind
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def regex_term(pattern: str) -> str:
|
|
10
|
+
if not pattern or len(pattern) > 2048 or any(c in pattern for c in '\x00\r\n"'):
|
|
11
|
+
raise ValueError("pattern must be 1..2048 characters without quotes or line breaks")
|
|
12
|
+
return 'regex:"' + pattern + '"'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def query_args(
|
|
16
|
+
query: str, path: str | None, kind: Kind, regex: bool, match_path: bool, case_sensitive: bool
|
|
17
|
+
) -> list[str]:
|
|
18
|
+
if not query.strip() or len(query) > 4096 or any(c in query for c in "\x00\r\n"):
|
|
19
|
+
raise ValueError("query must be nonempty, single-line and at most 4096 characters")
|
|
20
|
+
# ES parses switches itself even with shell=False. Never pass a switch as a query.
|
|
21
|
+
if re.search(r"(?:^|\s)[-/]", query) or query.count('"') % 2:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
"query must not start with an ES switch; use Everything syntax such as regex: or name:"
|
|
24
|
+
)
|
|
25
|
+
if kind not in ("all", "file", "folder"):
|
|
26
|
+
raise ValueError("invalid kind")
|
|
27
|
+
args = [
|
|
28
|
+
"-timeout",
|
|
29
|
+
"10000",
|
|
30
|
+
"-no-case",
|
|
31
|
+
"-no-whole-word",
|
|
32
|
+
"-no-match-path",
|
|
33
|
+
"-no-prefix",
|
|
34
|
+
"-no-suffix",
|
|
35
|
+
]
|
|
36
|
+
if path:
|
|
37
|
+
if any(c in path for c in '\x00\r\n"') or not os.path.isabs(path):
|
|
38
|
+
raise ValueError(
|
|
39
|
+
"path must be an absolute directory path without quotes or control characters"
|
|
40
|
+
)
|
|
41
|
+
args += ["-path", path]
|
|
42
|
+
if kind != "all":
|
|
43
|
+
args += ["/a-d" if kind == "file" else "/ad"]
|
|
44
|
+
if match_path:
|
|
45
|
+
args += ["-match-path"]
|
|
46
|
+
if case_sensitive:
|
|
47
|
+
args += ["-case"]
|
|
48
|
+
if regex:
|
|
49
|
+
args += ["-regex"]
|
|
50
|
+
return [*args, query]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""MCP tool registration and STDIO entry point."""
|
|
2
|
+
|
|
3
|
+
from mcp.server.fastmcp import FastMCP
|
|
4
|
+
from mcp.types import ToolAnnotations
|
|
5
|
+
|
|
6
|
+
from . import service
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_server() -> FastMCP:
|
|
10
|
+
"""Create an independent server with the stable public tool contract."""
|
|
11
|
+
server = FastMCP("everything-es")
|
|
12
|
+
annotations = ToolAnnotations(readOnlyHint=True, destructiveHint=False, openWorldHint=False)
|
|
13
|
+
for tool in (
|
|
14
|
+
service.everything_search,
|
|
15
|
+
service.everything_count,
|
|
16
|
+
service.everything_status,
|
|
17
|
+
service.everything_search_regex,
|
|
18
|
+
service.everything_search_fuzzy,
|
|
19
|
+
service.everything_search_pinyin,
|
|
20
|
+
service.everything_search_exact,
|
|
21
|
+
service.everything_search_keywords,
|
|
22
|
+
service.everything_search_wildcard,
|
|
23
|
+
service.everything_search_by_type,
|
|
24
|
+
service.everything_list_directory,
|
|
25
|
+
service.everything_search_typo,
|
|
26
|
+
):
|
|
27
|
+
server.add_tool(tool, annotations=annotations)
|
|
28
|
+
return server
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main() -> None:
|
|
32
|
+
create_server().run(transport="stdio")
|