# Directory Structure
```
├── .github
│ └── workflows
│ ├── claude-code-review.yml
│ ├── claude.yml
│ └── publish-to-test-pypi.yml
├── .gitignore
├── .python-version
├── CLAUDE.md
├── LICENSE
├── pyproject.toml
├── README.md
├── SEARCH_SYNTAX.md
├── src
│ └── mcp_server_everything_search
│ ├── __init__.py
│ ├── __main__.py
│ ├── everything_sdk.py
│ ├── platform_search.py
│ ├── search_interface.py
│ └── server.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
1 | 3.10
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Python
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | *.so
6 | .Python
7 | build/
8 | develop-eggs/
9 | dist/
10 | downloads/
11 | eggs/
12 | .eggs/
13 | lib/
14 | lib64/
15 | parts/
16 | sdist/
17 | var/
18 | wheels/
19 | *.egg-info/
20 | .installed.cfg
21 | *.egg
22 | MANIFEST
23 | .env
24 | venv/
25 | ENV/
26 | env/
27 | .venv/
28 | .pytest_cache/
29 | .coverage
30 | htmlcov/
31 | .tox/
32 | .mypy_cache/
33 |
34 | .history
35 | .aider*
36 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Everything Search MCP Server
2 |
3 | [](https://smithery.ai/server/mcp-server-everything-search)
4 |
5 | An MCP server that provides fast file searching capabilities across Windows, macOS, and Linux. On Windows, it uses the [Everything](https://www.voidtools.com/) SDK. On macOS, it uses the built-in `mdfind` command. On Linux, it uses the `locate`/`plocate` command.
6 |
7 | ## Tools
8 |
9 | ### search
10 |
11 | Search for files and folders across your system. The search capabilities and syntax support vary by platform:
12 |
13 | - Windows: Full Everything SDK features (see syntax guide below)
14 | - macOS: Basic filename and content search using Spotlight database
15 | - Linux: Basic filename search using locate database
16 |
17 | Parameters:
18 |
19 | - `query` (required): Search query string. See platform-specific notes below.
20 | - `max_results` (optional): Maximum number of results to return (default: 100, max: 1000)
21 | - `match_path` (optional): Match against full path instead of filename only (default: false)
22 | - `match_case` (optional): Enable case-sensitive search (default: false)
23 | - `match_whole_word` (optional): Match whole words only (default: false)
24 | - `match_regex` (optional): Enable regex search (default: false)
25 | - `sort_by` (optional): Sort order for results (default: 1). Available options:
26 |
27 | ```
28 | - 1: Sort by filename (A to Z)
29 | - 2: Sort by filename (Z to A)
30 | - 3: Sort by path (A to Z)
31 | - 4: Sort by path (Z to A)
32 | - 5: Sort by size (smallest first)
33 | - 6: Sort by size (largest first)
34 | - 7: Sort by extension (A to Z)
35 | - 8: Sort by extension (Z to A)
36 | - 11: Sort by creation date (oldest first)
37 | - 12: Sort by creation date (newest first)
38 | - 13: Sort by modification date (oldest first)
39 | - 14: Sort by modification date (newest first)
40 | ```
41 |
42 | Examples:
43 |
44 | ```json
45 | {
46 | "query": "*.py",
47 | "max_results": 50,
48 | "sort_by": 6
49 | }
50 | ```
51 |
52 | ```json
53 | {
54 | "query": "ext:py datemodified:today",
55 | "max_results": 10
56 | }
57 | ```
58 |
59 | Response includes:
60 |
61 | - File/folder path
62 | - File size in bytes
63 | - Last modified date
64 |
65 | ### Search Syntax Guide
66 |
67 | For detailed information about the search syntax supported on each platform (Windows, macOS, and Linux), please see [SEARCH_SYNTAX.md](SEARCH_SYNTAX.md).
68 |
69 | ## Prerequisites
70 |
71 | ### Windows
72 |
73 | 1. [Everything](https://www.voidtools.com/) search utility:
74 | - Download and install from https://www.voidtools.com/
75 | - **Make sure the Everything service is running**
76 | 2. Everything SDK:
77 | - Download from https://www.voidtools.com/support/everything/sdk/
78 | - Extract the SDK files to a location on your system
79 |
80 | ### Linux
81 |
82 | 1. Install and initialize the `locate` or `plocate` command:
83 | - Ubuntu/Debian: `sudo apt-get install plocate` or `sudo apt-get install mlocate`
84 | - Fedora: `sudo dnf install mlocate`
85 | 2. After installation, update the database:
86 | - For plocate: `sudo updatedb`
87 | - For mlocate: `sudo /etc/cron.daily/mlocate`
88 |
89 | ### macOS
90 |
91 | No additional setup required. The server uses the built-in `mdfind` command.
92 |
93 | ## Installation
94 |
95 | ### Installing via Smithery
96 |
97 | To install Everything Search for Claude Desktop automatically via [Smithery](https://smithery.ai/server/mcp-server-everything-search):
98 |
99 | ```bash
100 | npx -y @smithery/cli install mcp-server-everything-search --client claude
101 | ```
102 |
103 | ### Using uv (recommended)
104 |
105 | When using [`uv`](https://docs.astral.sh/uv/) no specific installation is needed. We will
106 | use [`uvx`](https://docs.astral.sh/uv/guides/tools/) to directly run _mcp-server-everything-search_.
107 |
108 | ### Using PIP
109 |
110 | Alternatively you can install `mcp-server-everything-search` via pip:
111 |
112 | ```
113 | pip install mcp-server-everything-search
114 | ```
115 |
116 | After installation, you can run it as a script using:
117 |
118 | ```
119 | python -m mcp_server_everything_search
120 | ```
121 |
122 | ## Configuration
123 |
124 | ### Windows
125 |
126 | The server requires the Everything SDK DLL to be available:
127 |
128 | Environment variable:
129 |
130 | ```
131 | EVERYTHING_SDK_PATH=path\to\Everything-SDK\dll\Everything64.dll
132 | ```
133 |
134 | ### Linux and macOS
135 |
136 | No additional configuration required.
137 |
138 | ### Usage with Claude Desktop
139 |
140 | Add one of these configurations to your `claude_desktop_config.json` based on your platform:
141 |
142 | <details>
143 | <summary>Windows (using uvx)</summary>
144 |
145 | ```json
146 | "mcpServers": {
147 | "everything-search": {
148 | "command": "uvx",
149 | "args": ["mcp-server-everything-search"],
150 | "env": {
151 | "EVERYTHING_SDK_PATH": "path/to/Everything-SDK/dll/Everything64.dll"
152 | }
153 | }
154 | }
155 | ```
156 |
157 | </details>
158 |
159 | <details>
160 | <summary>Windows (using pip installation)</summary>
161 |
162 | ```json
163 | "mcpServers": {
164 | "everything-search": {
165 | "command": "python",
166 | "args": ["-m", "mcp_server_everything_search"],
167 | "env": {
168 | "EVERYTHING_SDK_PATH": "path/to/Everything-SDK/dll/Everything64.dll"
169 | }
170 | }
171 | }
172 | ```
173 |
174 | </details>
175 |
176 | <details>
177 | <summary>Linux and macOS</summary>
178 |
179 | ```json
180 | "mcpServers": {
181 | "everything-search": {
182 | "command": "uvx",
183 | "args": ["mcp-server-everything-search"]
184 | }
185 | }
186 | ```
187 |
188 | Or if using pip installation:
189 |
190 | ```json
191 | "mcpServers": {
192 | "everything-search": {
193 | "command": "python",
194 | "args": ["-m", "mcp_server_everything_search"]
195 | }
196 | }
197 | ```
198 |
199 | </details>
200 |
201 | ## Debugging
202 |
203 | You can use the MCP inspector to debug the server. For uvx installations:
204 |
205 | ```
206 | npx @modelcontextprotocol/inspector uvx mcp-server-everything-search
207 | ```
208 |
209 | Or if you've installed the package in a specific directory or are developing on it:
210 |
211 | ```
212 | git clone https://github.com/mamertofabian/mcp-everything-search.git
213 | cd mcp-everything-search/src/mcp_server_everything_search
214 | npx @modelcontextprotocol/inspector uv run mcp-server-everything-search
215 | ```
216 |
217 | To view server logs:
218 |
219 | Linux/macOS:
220 |
221 | ```bash
222 | tail -f ~/.config/Claude/logs/mcp*.log
223 | ```
224 |
225 | Windows (PowerShell):
226 |
227 | ```powershell
228 | Get-Content -Path "$env:APPDATA\Claude\logs\mcp*.log" -Tail 20 -Wait
229 | ```
230 |
231 | ## Development
232 |
233 | If you are doing local development, there are two ways to test your changes:
234 |
235 | 1. Run the MCP inspector to test your changes. See [Debugging](#debugging) for run instructions.
236 |
237 | 2. Test using the Claude desktop app. Add the following to your `claude_desktop_config.json`:
238 |
239 | ```json
240 | "everything-search": {
241 | "command": "uv",
242 | "args": [
243 | "--directory",
244 | "/path/to/mcp-everything-search/src/mcp_server_everything_search",
245 | "run",
246 | "mcp-server-everything-search"
247 | ],
248 | "env": {
249 | "EVERYTHING_SDK_PATH": "path/to/Everything-SDK/dll/Everything64.dll"
250 | }
251 | }
252 | ```
253 |
254 | ## License
255 |
256 | This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
257 |
258 | ## Disclaimer
259 |
260 | This project is not affiliated with, endorsed by, or sponsored by voidtools (the creators of Everything search utility). This is an independent project that utilizes the publicly available Everything SDK.
261 |
```
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
```markdown
1 | # CLAUDE.md
2 |
3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4 |
5 | ## Project Overview
6 |
7 | This is an MCP (Model Context Protocol) server that provides cross-platform file search capabilities:
8 | - **Windows**: Uses the Everything SDK for fast indexed search
9 | - **macOS**: Uses `mdfind` (Spotlight) for native search
10 | - **Linux**: Uses `locate`/`plocate` for filesystem search
11 |
12 | The server exposes a single `search` tool with platform-specific parameter schemas and syntax support.
13 |
14 | ## Development Commands
15 |
16 | ### Testing and Running
17 |
18 | ```bash
19 | # Run the server with MCP inspector for testing
20 | npx @modelcontextprotocol/inspector uv run mcp-server-everything-search
21 |
22 | # Run directly with uv
23 | uv run mcp-server-everything-search
24 |
25 | # Run as Python module (after pip install)
26 | python -m mcp_server_everything_search
27 | ```
28 |
29 | ### Code Quality
30 |
31 | ```bash
32 | # Type checking
33 | uv run pyright
34 |
35 | # Linting
36 | uv run ruff check
37 |
38 | # Formatting
39 | uv run ruff format
40 |
41 | # Run tests
42 | uv run pytest
43 | ```
44 |
45 | ### Building
46 |
47 | ```bash
48 | # Build the package
49 | uv build
50 | ```
51 |
52 | ## Architecture
53 |
54 | ### Core Components
55 |
56 | 1. **server.py** - Main MCP server implementation
57 | - Defines the `search` tool with platform-specific schemas
58 | - Handles parameter parsing and validation using Pydantic
59 | - Routes search requests to appropriate platform provider
60 | - Returns formatted search results as TextContent
61 |
62 | 2. **search_interface.py** - Abstract search provider interface
63 | - `SearchProvider`: Abstract base class defining the search contract
64 | - `WindowsSearchProvider`: Everything SDK wrapper
65 | - `MacSearchProvider`: mdfind command wrapper
66 | - `LinuxSearchProvider`: locate/plocate command wrapper
67 | - `SearchResult`: Universal dataclass for all platforms
68 |
69 | 3. **platform_search.py** - Platform-specific parameter models
70 | - `BaseSearchQuery`: Common search parameters (query, max_results)
71 | - `WindowsSpecificParams`: Everything SDK options (match_path, match_case, etc.)
72 | - `MacSpecificParams`: mdfind options (live_updates, search_directory, etc.)
73 | - `LinuxSpecificParams`: locate options (ignore_case, regex_search, etc.)
74 | - `UnifiedSearchQuery`: Combines all parameter models with platform-aware schema generation
75 | - `build_search_command()`: Builds platform-specific command arrays
76 |
77 | 4. **everything_sdk.py** - Windows Everything SDK wrapper
78 | - `EverythingSDK`: ctypes wrapper for Everything64.dll
79 | - Comprehensive constant definitions for request flags and sort options
80 | - Windows filetime to Python datetime conversion
81 | - Full error handling with custom EverythingError exception
82 |
83 | ### Key Design Patterns
84 |
85 | - **Strategy Pattern**: Platform-specific search providers implementing common SearchProvider interface
86 | - **Factory Pattern**: `SearchProvider.get_provider()` returns the correct provider based on platform.system()
87 | - **Adapter Pattern**: Each provider adapts platform-specific tools to unified SearchResult format
88 |
89 | ### Platform-Specific Notes
90 |
91 | **Windows**:
92 | - Requires `EVERYTHING_SDK_PATH` environment variable pointing to Everything64.dll
93 | - Uses ctypes to call SDK functions directly (no subprocess calls)
94 | - Supports full Everything search syntax with advanced filters and sorting
95 |
96 | **macOS**:
97 | - Uses subprocess to call `mdfind` command
98 | - No additional dependencies required (built-in)
99 |
100 | **Linux**:
101 | - Checks for `plocate` first, falls back to `locate`
102 | - Provides detailed error messages if database not initialized
103 | - Uses subprocess to call locate commands
104 |
105 | ### Parameter Flow
106 |
107 | 1. Client sends search request with platform-specific parameters
108 | 2. `server.py` parses `base` and platform-specific params (e.g., `windows_params`)
109 | 3. Creates `UnifiedSearchQuery` from combined parameters
110 | 4. Routes to appropriate `SearchProvider` based on `platform.system()`
111 | 5. Provider executes search and returns list of `SearchResult` objects
112 | 6. Server formats results as TextContent with file details
113 |
114 | ## Environment Variables
115 |
116 | - `EVERYTHING_SDK_PATH` (Windows only): Path to Everything64.dll (default: `D:\dev\tools\Everything-SDK\dll\Everything64.dll`)
117 |
118 | ## Installation Methods
119 |
120 | The server supports three installation methods:
121 | 1. **Smithery CLI**: `npx -y @smithery/cli install mcp-server-everything-search --client claude`
122 | 2. **uv (recommended)**: Use `uvx mcp-server-everything-search`
123 | 3. **pip**: `pip install mcp-server-everything-search` then `python -m mcp_server_everything_search`
124 |
```
--------------------------------------------------------------------------------
/src/mcp_server_everything_search/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """Everything Search MCP Server."""
2 |
3 | from .server import main
4 |
5 | __all__ = ["main"]
6 |
```
--------------------------------------------------------------------------------
/src/mcp_server_everything_search/__main__.py:
--------------------------------------------------------------------------------
```python
1 | """Main entry point for Everything Search MCP server."""
2 |
3 | from .server import main
4 |
5 | if __name__ == "__main__":
6 | main()
7 |
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [project]
2 | name = "mcp-server-everything-search"
3 | version = "0.2.1"
4 | description = "A Model Context Protocol server providing fast file searching using Everything SDK"
5 | readme = "README.md"
6 | requires-python = ">=3.10"
7 | authors = [
8 | { name = "Mamerto Fabian", email = "[email protected]" },
9 | ]
10 | keywords = ["everything", "search", "mcp", "llm"]
11 | license = { file = "LICENSE" }
12 | classifiers = [
13 | "Programming Language :: Python :: 3",
14 | "Programming Language :: Python :: 3.10",
15 | ]
16 | dependencies = [
17 | "mcp>=1.0.0",
18 | "pydantic>=2.0.0",
19 | ]
20 |
21 | [project.scripts]
22 | mcp-server-everything-search = "mcp_server_everything_search:main"
23 |
24 | [build-system]
25 | requires = ["hatchling"]
26 | build-backend = "hatchling.build"
27 |
28 | [tool.uv]
29 | dev-dependencies = [
30 | "pyright>=1.1.389",
31 | "pytest>=8.3.3",
32 | "ruff>=0.8.1",
33 | ]
34 |
```
--------------------------------------------------------------------------------
/.github/workflows/claude.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Claude Code
2 |
3 | on:
4 | issue_comment:
5 | types: [created]
6 | pull_request_review_comment:
7 | types: [created]
8 | issues:
9 | types: [opened, assigned]
10 | pull_request_review:
11 | types: [submitted]
12 |
13 | jobs:
14 | claude:
15 | if: |
16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20 | runs-on: ubuntu-latest
21 | permissions:
22 | contents: read
23 | pull-requests: read
24 | issues: read
25 | id-token: write
26 | actions: read # Required for Claude to read CI results on PRs
27 | steps:
28 | - name: Checkout repository
29 | uses: actions/checkout@v4
30 | with:
31 | fetch-depth: 1
32 |
33 | - name: Run Claude Code
34 | id: claude
35 | uses: anthropics/claude-code-action@v1
36 | with:
37 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38 |
39 | # This is an optional setting that allows Claude to read CI results on PRs
40 | additional_permissions: |
41 | actions: read
42 |
43 | # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
44 | # prompt: 'Update the pull request description to include a summary of changes.'
45 |
46 | # Optional: Add claude_args to customize behavior and configuration
47 | # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
48 | # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
49 | # claude_args: '--allowed-tools Bash(gh pr:*)'
50 |
51 |
```
--------------------------------------------------------------------------------
/.github/workflows/claude-code-review.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Claude Code Review
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize]
6 | # Optional: Only run on specific file changes
7 | # paths:
8 | # - "src/**/*.ts"
9 | # - "src/**/*.tsx"
10 | # - "src/**/*.js"
11 | # - "src/**/*.jsx"
12 |
13 | jobs:
14 | claude-review:
15 | # Optional: Filter by PR author
16 | # if: |
17 | # github.event.pull_request.user.login == 'external-contributor' ||
18 | # github.event.pull_request.user.login == 'new-developer' ||
19 | # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
20 |
21 | runs-on: ubuntu-latest
22 | permissions:
23 | contents: read
24 | pull-requests: read
25 | issues: read
26 | id-token: write
27 |
28 | steps:
29 | - name: Checkout repository
30 | uses: actions/checkout@v4
31 | with:
32 | fetch-depth: 1
33 |
34 | - name: Run Claude Code Review
35 | id: claude-review
36 | uses: anthropics/claude-code-action@v1
37 | with:
38 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39 | prompt: |
40 | REPO: ${{ github.repository }}
41 | PR NUMBER: ${{ github.event.pull_request.number }}
42 |
43 | Please review this pull request and provide feedback on:
44 | - Code quality and best practices
45 | - Potential bugs or issues
46 | - Performance considerations
47 | - Security concerns
48 | - Test coverage
49 |
50 | Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
51 |
52 | Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
53 |
54 | # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
55 | # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
56 | claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
57 |
58 |
```
--------------------------------------------------------------------------------
/.github/workflows/publish-to-test-pypi.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI
2 |
3 | on:
4 | push:
5 | branches:
6 | - dev
7 | - main
8 | tags:
9 | - '*' # This will trigger the workflow on any tag push
10 |
11 | jobs:
12 | build:
13 | name: Build distribution 📦
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v4
18 | - name: Set up Python
19 | uses: actions/setup-python@v5
20 | with:
21 | python-version: "3.10"
22 | - name: Install pypa/build
23 | run: >-
24 | python3 -m pip install build
25 | - name: Build a binary wheel and a source tarball
26 | run: python3 -m build
27 | - name: Store the distribution packages
28 | uses: actions/upload-artifact@v4
29 | with:
30 | name: python-package-distributions
31 | path: dist/
32 |
33 | publish-to-pypi:
34 | name: >-
35 | Publish Python 🐍 distribution 📦 to PyPI
36 | if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
37 | needs:
38 | - build
39 | runs-on: ubuntu-latest
40 | environment:
41 | name: pypi
42 | url: https://pypi.org/p/mcp-server-everything-search
43 | permissions:
44 | id-token: write
45 |
46 | steps:
47 | - name: Download all the dists
48 | uses: actions/download-artifact@v4
49 | with:
50 | name: python-package-distributions
51 | path: dist/
52 |
53 | - name: Publish distribution 📦 to PyPI
54 | uses: pypa/gh-action-pypi-publish@release/v1
55 |
56 |
57 | github-release:
58 | name: >-
59 | Sign the Python 🐍 distribution 📦 with Sigstore
60 | and upload them to GitHub Release
61 | needs:
62 | - publish-to-pypi
63 | runs-on: ubuntu-latest
64 |
65 | permissions:
66 | contents: write
67 | id-token: write
68 |
69 | steps:
70 | - name: Download all the dists
71 | uses: actions/download-artifact@v4
72 | with:
73 | name: python-package-distributions
74 | path: dist/
75 | - name: Sign the dists with Sigstore
76 | uses: sigstore/[email protected]
77 | with:
78 | inputs: >-
79 | ./dist/*.tar.gz
80 | ./dist/*.whl
81 | - name: Create GitHub Release
82 | env:
83 | GITHUB_TOKEN: ${{ github.token }}
84 | run: >-
85 | gh release create
86 | '${{ github.ref_name }}'
87 | --repo '${{ github.repository }}'
88 | --notes ""
89 | - name: Upload artifact signatures to GitHub Release
90 | env:
91 | GITHUB_TOKEN: ${{ github.token }}
92 | # Upload to GitHub Release using the `gh` CLI.
93 | # `dist/` contains the built packages, and the
94 | # sigstore-produced signatures and certificates.
95 | run: >-
96 | gh release upload
97 | '${{ github.ref_name }}' dist/**
98 | --repo '${{ github.repository }}'
99 |
100 | publish-to-testpypi:
101 | name: Publish Python 🐍 distribution 📦 to TestPyPI
102 | if: github.ref == 'refs/heads/dev'
103 | needs:
104 | - build
105 | runs-on: ubuntu-latest
106 |
107 | environment:
108 | name: testpypi
109 | url: https://test.pypi.org/p/mcp-server-everything-search
110 |
111 | permissions:
112 | id-token: write
113 |
114 | steps:
115 | - name: Download all the dists
116 | uses: actions/download-artifact@v4
117 | with:
118 | name: python-package-distributions
119 | path: dist/
120 | - name: Publish distribution 📦 to TestPyPI
121 | uses: pypa/gh-action-pypi-publish@release/v1
122 | with:
123 | repository-url: https://test.pypi.org/legacy/
124 |
```
--------------------------------------------------------------------------------
/src/mcp_server_everything_search/platform_search.py:
--------------------------------------------------------------------------------
```python
1 | """Platform-specific search implementations with dedicated parameter models."""
2 |
3 | from typing import Optional, List, Dict, Any
4 | from pydantic import BaseModel, Field
5 | from enum import Enum
6 | import platform
7 |
8 | class BaseSearchQuery(BaseModel):
9 | """Base search parameters common to all platforms."""
10 | query: str = Field(
11 | description="Search query string. See platform-specific documentation for syntax details."
12 | )
13 | max_results: int = Field(
14 | default=100,
15 | ge=1,
16 | le=1000,
17 | description="Maximum number of results to return (1-1000)"
18 | )
19 |
20 | class MacSpecificParams(BaseModel):
21 | """macOS-specific search parameters for mdfind."""
22 | live_updates: bool = Field(
23 | default=False,
24 | description="Provide live updates to search results"
25 | )
26 | search_directory: Optional[str] = Field(
27 | default=None,
28 | description="Limit search to specific directory (-onlyin parameter)"
29 | )
30 | literal_query: bool = Field(
31 | default=False,
32 | description="Treat query as literal string without interpretation"
33 | )
34 | interpret_query: bool = Field(
35 | default=False,
36 | description="Interpret query as if typed in Spotlight menu"
37 | )
38 |
39 | class LinuxSpecificParams(BaseModel):
40 | """Linux-specific search parameters for locate."""
41 | ignore_case: bool = Field(
42 | default=True,
43 | description="Ignore case distinctions (-i parameter)"
44 | )
45 | regex_search: bool = Field(
46 | default=False,
47 | description="Use regular expressions in patterns (-r parameter)"
48 | )
49 | existing_files: bool = Field(
50 | default=True,
51 | description="Only output existing files (-e parameter)"
52 | )
53 | count_only: bool = Field(
54 | default=False,
55 | description="Only display count of matches (-c parameter)"
56 | )
57 |
58 | class WindowsSortOption(int, Enum):
59 | """Sort options for Windows Everything search."""
60 | NAME_ASC = 1
61 | NAME_DESC = 2
62 | PATH_ASC = 3
63 | PATH_DESC = 4
64 | SIZE_ASC = 5
65 | SIZE_DESC = 6
66 | EXT_ASC = 7
67 | EXT_DESC = 8
68 | CREATED_ASC = 11
69 | CREATED_DESC = 12
70 | MODIFIED_ASC = 13
71 | MODIFIED_DESC = 14
72 |
73 | class WindowsSpecificParams(BaseModel):
74 | """Windows-specific search parameters for Everything SDK."""
75 | match_path: bool = Field(
76 | default=False,
77 | description="Match against full path instead of filename only"
78 | )
79 | match_case: bool = Field(
80 | default=False,
81 | description="Enable case-sensitive search"
82 | )
83 | match_whole_word: bool = Field(
84 | default=False,
85 | description="Match whole words only"
86 | )
87 | match_regex: bool = Field(
88 | default=False,
89 | description="Enable regex search"
90 | )
91 | sort_by: WindowsSortOption = Field(
92 | default=WindowsSortOption.NAME_ASC,
93 | description="Sort order for results"
94 | )
95 |
96 | class UnifiedSearchQuery(BaseSearchQuery):
97 | """Combined search parameters model."""
98 | mac_params: Optional[MacSpecificParams] = None
99 | linux_params: Optional[LinuxSpecificParams] = None
100 | windows_params: Optional[WindowsSpecificParams] = None
101 |
102 | @classmethod
103 | def get_schema_for_platform(cls) -> Dict[str, Any]:
104 | """Get the appropriate schema based on the current platform."""
105 | system = platform.system().lower()
106 |
107 | schema = {
108 | "type": "object",
109 | "properties": {
110 | "base": BaseSearchQuery.model_json_schema()
111 | },
112 | "required": ["base"]
113 | }
114 |
115 | # Add platform-specific parameters
116 | if system == "darwin":
117 | schema["properties"]["mac_params"] = MacSpecificParams.model_json_schema()
118 | elif system == "linux":
119 | schema["properties"]["linux_params"] = LinuxSpecificParams.model_json_schema()
120 | elif system == "windows":
121 | schema["properties"]["windows_params"] = WindowsSpecificParams.model_json_schema()
122 |
123 | return schema
124 |
125 | def get_platform_params(self) -> Optional[BaseModel]:
126 | """Get the parameters specific to the current platform."""
127 | system = platform.system().lower()
128 | if system == "darwin":
129 | return self.mac_params
130 | elif system == "linux":
131 | return self.linux_params
132 | elif system == "windows":
133 | return self.windows_params
134 | return None
135 |
136 | def build_search_command(query: UnifiedSearchQuery) -> List[str]:
137 | """Build the appropriate search command based on platform and parameters."""
138 | system = platform.system().lower()
139 | platform_params = query.get_platform_params()
140 |
141 | if system == "darwin":
142 | cmd = ["mdfind"]
143 | if platform_params:
144 | if platform_params.live_updates:
145 | cmd.append("-live")
146 | if platform_params.search_directory:
147 | cmd.extend(["-onlyin", platform_params.search_directory])
148 | if platform_params.literal_query:
149 | cmd.append("-literal")
150 | if platform_params.interpret_query:
151 | cmd.append("-interpret")
152 | cmd.append(query.query) # Use query directly from UnifiedSearchQuery
153 | return cmd
154 |
155 | elif system == "linux":
156 | cmd = ["locate"]
157 | if platform_params:
158 | if platform_params.ignore_case:
159 | cmd.append("-i")
160 | if platform_params.regex_search:
161 | cmd.append("-r")
162 | if platform_params.existing_files:
163 | cmd.append("-e")
164 | if platform_params.count_only:
165 | cmd.append("-c")
166 | cmd.append(query.query) # Use query directly from UnifiedSearchQuery
167 | return cmd
168 |
169 | elif system == "windows":
170 | # For Windows, return None as we'll use the Everything SDK directly
171 | return []
172 |
173 | raise NotImplementedError(f"Unsupported platform: {system}")
174 |
```
--------------------------------------------------------------------------------
/SEARCH_SYNTAX.md:
--------------------------------------------------------------------------------
```markdown
1 | # Search Syntax Guide
2 |
3 | ## Windows Search (Everything SDK)
4 |
5 | The following advanced search features are only available on Windows when using the Everything SDK:
6 |
7 | ### Basic Operators
8 |
9 | - `space`: AND operator
10 | - `|`: OR operator
11 | - `!`: NOT operator
12 | - `< >`: Grouping
13 | - `" "`: Search for an exact phrase
14 |
15 | ### Wildcards
16 |
17 | - `*`: Matches zero or more characters
18 | - `?`: Matches exactly one character
19 |
20 | Note: Wildcards match the whole filename by default. Disable Match whole filename to match wildcards anywhere.
21 |
22 | ### Functions
23 |
24 | #### Size and Count
25 |
26 | - `size:<size>[kb|mb|gb]`: Search by file size
27 | - `count:<max>`: Limit number of results
28 | - `childcount:<count>`: Folders with specific number of children
29 | - `childfilecount:<count>`: Folders with specific number of files
30 | - `childfoldercount:<count>`: Folders with specific number of subfolders
31 | - `len:<length>`: Match filename length
32 |
33 | #### Dates
34 |
35 | - `datemodified:<date>, dm:<date>`: Modified date
36 | - `dateaccessed:<date>, da:<date>`: Access date
37 | - `datecreated:<date>, dc:<date>`: Creation date
38 | - `daterun:<date>, dr:<date>`: Last run date
39 | - `recentchange:<date>, rc:<date>`: Recently changed date
40 |
41 | Date formats: YYYY[-MM[-DD[Thh[:mm[:ss[.sss]]]]]] or today, yesterday, lastweek, etc.
42 |
43 | #### File Attributes and Types
44 |
45 | - `attrib:<attributes>, attributes:<attributes>`: Search by file attributes (A:Archive, H:Hidden, S:System, etc.)
46 | - `type:<type>`: Search by file type
47 | - `ext:<list>`: Search by semicolon-separated extensions
48 |
49 | #### Path and Name
50 |
51 | - `path:<path>`: Search in specific path
52 | - `parent:<path>, infolder:<path>, nosubfolders:<path>`: Search in path excluding subfolders
53 | - `startwith:<text>`: Files starting with text
54 | - `endwith:<text>`: Files ending with text
55 | - `child:<filename>`: Folders containing specific child
56 | - `depth:<count>, parents:<count>`: Files at specific folder depth
57 | - `root`: Files with no parent folder
58 | - `shell:<name>`: Search in known shell folders
59 |
60 | #### Duplicates and Lists
61 |
62 | - `dupe, namepartdupe, attribdupe, dadupe, dcdupe, dmdupe, sizedupe`: Find duplicates
63 | - `filelist:<list>`: Search pipe-separated (|) file list
64 | - `filelistfilename:<filename>`: Search files from list file
65 | - `frn:<frnlist>`: Search by File Reference Numbers
66 | - `fsi:<index>`: Search by file system index
67 | - `empty`: Find empty folders
68 |
69 | ### Function Syntax
70 |
71 | - `function:value`: Equal to value
72 | - `function:<=value`: Less than or equal
73 | - `function:<value`: Less than
74 | - `function:=value`: Equal to
75 | - `function:>value`: Greater than
76 | - `function:>=value`: Greater than or equal
77 | - `function:start..end`: Range of values
78 | - `function:start-end`: Range of values
79 |
80 | ### Modifiers
81 |
82 | - `case:, nocase:`: Enable/disable case sensitivity
83 | - `file:, folder:`: Match only files or folders
84 | - `path:, nopath:`: Match full path or filename only
85 | - `regex:, noregex:`: Enable/disable regex
86 | - `wfn:, nowfn:`: Match whole filename or anywhere
87 | - `wholeword:, ww:`: Match whole words only
88 | - `wildcards:, nowildcards:`: Enable/disable wildcards
89 |
90 | ### Examples
91 |
92 | 1. Find Python files modified today:
93 | `ext:py datemodified:today`
94 |
95 | 2. Find large video files:
96 | `ext:mp4|mkv|avi size:>1gb`
97 |
98 | 3. Find files in specific folder:
99 | `path:C:\Projects *.js`
100 |
101 | ## macOS Search (mdfind)
102 |
103 | macOS uses Spotlight's metadata search capabilities through the `mdfind` command. The following features are supported:
104 |
105 | ### Command Options
106 |
107 | - `-live`: Provides live updates to search results as files change
108 | - `-count`: Show only the number of matches
109 | - `-onlyin directory`: Limit search to specific directory
110 | - `-literal`: Treat query as literal text without interpretation
111 | - `-interpret`: Interpret query as if typed in Spotlight menu
112 |
113 | ### Basic Search
114 |
115 | - Simple text search looks for matches in any metadata attribute
116 | - Wildcards (`*`) are supported in search strings
117 | - Multiple words are treated as AND conditions
118 | - Whitespace is significant in queries
119 | - Use parentheses () to group expressions
120 |
121 | ### Search Operators
122 |
123 | - `|` (OR): Match either word, e.g., `"image|photo"`
124 | - `-` (NOT): Exclude matches, e.g., `-screenshot`
125 | - `=`, `==` (equal)
126 | - `!=` (not equal)
127 | - `<`, `>` (less/greater than)
128 | - `<=`, `>=` (less/greater than or equal)
129 |
130 | ### Value Comparison Modifiers
131 |
132 | Use brackets with these modifiers:
133 |
134 | - `[c]`: Case-insensitive comparison
135 | - `[d]`: Diacritical marks insensitive
136 | - Can be combined, e.g., `[cd]` for both
137 |
138 | ### Content Types (kind:)
139 |
140 | - `application`, `app`: Applications
141 | - `audio`, `music`: Audio/Music files
142 | - `bookmark`: Bookmarks
143 | - `contact`: Contacts
144 | - `email`, `mail message`: Email messages
145 | - `event`: Calendar events
146 | - `folder`: Folders
147 | - `font`: Fonts
148 | - `image`: Images
149 | - `movie`: Movies
150 | - `pdf`: PDF documents
151 | - `preferences`: System preferences
152 | - `presentation`: Presentations
153 | - `todo`: Calendar to-dos
154 |
155 | ### Date Filters (date:)
156 |
157 | Time-based search using these keywords:
158 |
159 | - `today`, `yesterday`, `tomorrow`
160 | - `this week`, `next week`
161 | - `this month`, `next month`
162 | - `this year`, `next year`
163 |
164 | Or use time functions:
165 |
166 | - `$time.today()`
167 | - `$time.yesterday()`
168 | - `$time.this_week()`
169 | - `$time.this_month()`
170 | - `$time.this_year()`
171 | - `$time.tomorrow()`
172 | - `$time.next_week()`
173 | - `$time.next_month()`
174 | - `$time.next_year()`
175 |
176 | ### Common Metadata Attributes
177 |
178 | Search specific metadata using these attributes:
179 |
180 | - `kMDItemAuthors`: Document authors
181 | - `kMDItemContentType`: File type
182 | - `kMDItemContentTypeTree`: File type hierarchy
183 | - `kMDItemCreator`: Creating application
184 | - `kMDItemDescription`: File description
185 | - `kMDItemDisplayName`: Display name
186 | - `kMDItemFSContentChangeDate`: File modification date
187 | - `kMDItemFSCreationDate`: File creation date
188 | - `kMDItemFSName`: Filename
189 | - `kMDItemKeywords`: Keywords/tags
190 | - `kMDItemLastUsedDate`: Last used date
191 | - `kMDItemNumberOfPages`: Page count
192 | - `kMDItemTitle`: Document title
193 | - `kMDItemUserTags`: User-assigned tags
194 |
195 | ### Examples
196 |
197 | 1. Find images modified yesterday:
198 | `kind:image date:yesterday`
199 |
200 | 2. Find documents by author (case-insensitive):
201 | `kMDItemAuthors ==[c] "John Doe"`
202 |
203 | 3. Find files in specific directory:
204 | `mdfind -onlyin ~/Documents "query"`
205 |
206 | 4. Find files by tag:
207 | `kMDItemUserTags = "Important"`
208 |
209 | 5. Find files created by application:
210 | `kMDItemCreator = "Pixelmator*"`
211 |
212 | 6. Find PDFs with specific text:
213 | `kind:pdf "search term"`
214 |
215 | 7. Find recent presentations:
216 | `kind:presentation date:this week`
217 |
218 | 8. Count matching files:
219 | `mdfind -count "kind:image date:today"`
220 |
221 | 9. Monitor for new matches:
222 | `mdfind -live "kind:pdf"`
223 |
224 | 10. Complex metadata search:
225 | `kMDItemContentTypeTree = "public.image" && kMDItemUserTags = "vacation" && kMDItemFSContentChangeDate >= $time.this_month()`
226 |
227 | Note: Use `mdls filename` to see all available metadata attributes for a specific file.
228 |
229 | ## Linux Search (locate/plocate)
230 |
231 | Linux uses the locate/plocate command for fast filename searching. The following features are supported:
232 |
233 | ### Basic Search
234 |
235 | - Simple text search matches against filenames
236 | - Multiple words are treated as AND conditions
237 | - Wildcards (`*` and `?`) are supported
238 | - Case-insensitive by default
239 |
240 | ### Search Options
241 |
242 | - `-i`: Case-insensitive search (default)
243 | - `-c`: Count matches instead of showing them
244 | - `-r` or `--regex`: Use regular expressions
245 | - `-b`: Match only the basename
246 | - `-w`: Match whole words only
247 |
248 | ### Examples
249 |
250 | 1. Find all Python files:
251 | `*.py`
252 |
253 | 2. Find files in home directory:
254 | `/home/username/*`
255 |
256 | 3. Case-sensitive search for specific file:
257 | `--regex "^/etc/[A-Z].*\.conf$"`
258 |
259 | 4. Count matching files:
260 | Use with `-c` parameter
261 |
262 | Note: The locate database must be up to date for accurate results. Run `sudo updatedb` to update the database manually.
263 |
```
--------------------------------------------------------------------------------
/src/mcp_server_everything_search/search_interface.py:
--------------------------------------------------------------------------------
```python
1 | """Platform-agnostic search interface for MCP."""
2 |
3 | import abc
4 | import platform
5 | import subprocess
6 | import os
7 | from datetime import datetime
8 | from typing import Optional, List
9 | from dataclasses import dataclass
10 | from pathlib import Path
11 |
12 | @dataclass
13 | class SearchResult:
14 | """Universal search result structure."""
15 | path: str
16 | filename: str
17 | extension: Optional[str] = None
18 | size: Optional[int] = None
19 | created: Optional[datetime] = None
20 | modified: Optional[datetime] = None
21 | accessed: Optional[datetime] = None
22 | attributes: Optional[str] = None
23 |
24 | class SearchProvider(abc.ABC):
25 | """Abstract base class for platform-specific search implementations."""
26 |
27 | @abc.abstractmethod
28 | def search_files(
29 | self,
30 | query: str,
31 | max_results: int = 100,
32 | match_path: bool = False,
33 | match_case: bool = False,
34 | match_whole_word: bool = False,
35 | match_regex: bool = False,
36 | sort_by: Optional[int] = None
37 | ) -> List[SearchResult]:
38 | """Execute a file search using platform-specific methods."""
39 | pass
40 |
41 | @classmethod
42 | def get_provider(cls) -> 'SearchProvider':
43 | """Factory method to get the appropriate search provider for the current platform."""
44 | system = platform.system().lower()
45 | if system == 'darwin':
46 | return MacSearchProvider()
47 | elif system == 'linux':
48 | return LinuxSearchProvider()
49 | elif system == 'windows':
50 | return WindowsSearchProvider()
51 | else:
52 | raise NotImplementedError(f"No search provider available for {system}")
53 |
54 | def _convert_path_to_result(self, path: str) -> SearchResult:
55 | """Convert a path to a SearchResult with file information."""
56 | try:
57 | path_obj = Path(path)
58 | stat = path_obj.stat()
59 | return SearchResult(
60 | path=str(path_obj),
61 | filename=path_obj.name,
62 | extension=path_obj.suffix[1:] if path_obj.suffix else None,
63 | size=stat.st_size,
64 | created=datetime.fromtimestamp(stat.st_ctime),
65 | modified=datetime.fromtimestamp(stat.st_mtime),
66 | accessed=datetime.fromtimestamp(stat.st_atime)
67 | )
68 | except (OSError, ValueError) as e:
69 | # If we can't access the file, return basic info
70 | return SearchResult(
71 | path=str(path),
72 | filename=os.path.basename(path)
73 | )
74 |
75 | class MacSearchProvider(SearchProvider):
76 | """macOS search implementation using mdfind."""
77 |
78 | def search_files(
79 | self,
80 | query: str,
81 | max_results: int = 100,
82 | match_path: bool = False,
83 | match_case: bool = False,
84 | match_whole_word: bool = False,
85 | match_regex: bool = False,
86 | sort_by: Optional[int] = None
87 | ) -> List[SearchResult]:
88 | try:
89 | # Build mdfind command
90 | cmd = ['mdfind']
91 | if match_path:
92 | # When matching path, don't use -name
93 | cmd.append(query)
94 | else:
95 | cmd.extend(['-name', query])
96 |
97 | # Execute search
98 | result = subprocess.run(cmd, capture_output=True, text=True)
99 | if result.returncode != 0:
100 | raise RuntimeError(f"mdfind failed: {result.stderr}")
101 |
102 | # Process results
103 | paths = result.stdout.splitlines()[:max_results]
104 | return [self._convert_path_to_result(path) for path in paths]
105 |
106 | except subprocess.CalledProcessError as e:
107 | raise RuntimeError(f"Search failed: {e}")
108 |
109 | class LinuxSearchProvider(SearchProvider):
110 | """Linux search implementation using locate/plocate."""
111 |
112 | def __init__(self):
113 | """Check if locate/plocate is installed and the database is ready."""
114 | self.locate_cmd = None
115 | self.locate_type = None
116 |
117 | # Check for plocate first (newer version)
118 | plocate_check = subprocess.run(['which', 'plocate'], capture_output=True)
119 | if plocate_check.returncode == 0:
120 | self.locate_cmd = 'plocate'
121 | self.locate_type = 'plocate'
122 | else:
123 | # Check for mlocate
124 | mlocate_check = subprocess.run(['which', 'locate'], capture_output=True)
125 | if mlocate_check.returncode == 0:
126 | self.locate_cmd = 'locate'
127 | self.locate_type = 'mlocate'
128 | else:
129 | raise RuntimeError(
130 | "Neither 'locate' nor 'plocate' is installed. Please install one:\n"
131 | "Ubuntu/Debian: sudo apt-get install plocate\n"
132 | " or\n"
133 | " sudo apt-get install mlocate\n"
134 | "Fedora: sudo dnf install mlocate\n"
135 | "After installation, the database will be updated automatically, or run:\n"
136 | "For plocate: sudo updatedb\n"
137 | "For mlocate: sudo /etc/cron.daily/mlocate"
138 | )
139 |
140 | def _update_database(self):
141 | """Update the locate database."""
142 | if self.locate_type == 'plocate':
143 | subprocess.run(['sudo', 'updatedb'], check=True)
144 | else: # mlocate
145 | subprocess.run(['sudo', '/etc/cron.daily/mlocate'], check=True)
146 |
147 | def search_files(
148 | self,
149 | query: str,
150 | max_results: int = 100,
151 | match_path: bool = False,
152 | match_case: bool = False,
153 | match_whole_word: bool = False,
154 | match_regex: bool = False,
155 | sort_by: Optional[int] = None
156 | ) -> List[SearchResult]:
157 | try:
158 | # Build locate command
159 | cmd = [self.locate_cmd]
160 | if not match_case:
161 | cmd.append('-i')
162 | if match_regex:
163 | cmd.append('--regex' if self.locate_type == 'mlocate' else '-r')
164 | cmd.append(query)
165 |
166 | # Execute search
167 | result = subprocess.run(cmd, capture_output=True, text=True)
168 | if result.returncode != 0:
169 | error_msg = result.stderr.lower()
170 | if "no such file or directory" in error_msg or "database" in error_msg:
171 | raise RuntimeError(
172 | f"The {self.locate_type} database needs to be created. "
173 | f"Please run: sudo updatedb"
174 | )
175 | raise RuntimeError(f"{self.locate_cmd} failed: {result.stderr}")
176 |
177 | # Process results
178 | paths = result.stdout.splitlines()[:max_results]
179 | return [self._convert_path_to_result(path) for path in paths]
180 |
181 | except FileNotFoundError:
182 | raise RuntimeError(
183 | f"The {self.locate_cmd} command disappeared. Please reinstall:\n"
184 | "Ubuntu/Debian: sudo apt-get install plocate\n"
185 | " or\n"
186 | " sudo apt-get install mlocate\n"
187 | "Fedora: sudo dnf install mlocate"
188 | )
189 | except subprocess.CalledProcessError as e:
190 | raise RuntimeError(f"Search failed: {e}")
191 |
192 |
193 | class WindowsSearchProvider(SearchProvider):
194 | """Windows search implementation using Everything SDK."""
195 |
196 | def __init__(self):
197 | """Initialize Everything SDK."""
198 | import os
199 | from .everything_sdk import EverythingSDK
200 | dll_path = os.getenv('EVERYTHING_SDK_PATH', 'D:\\dev\\tools\\Everything-SDK\\dll\\Everything64.dll')
201 | self.everything_sdk = EverythingSDK(dll_path)
202 |
203 | def search_files(
204 | self,
205 | query: str,
206 | max_results: int = 100,
207 | match_path: bool = False,
208 | match_case: bool = False,
209 | match_whole_word: bool = False,
210 | match_regex: bool = False,
211 | sort_by: Optional[int] = None
212 | ) -> List[SearchResult]:
213 | # Replace double backslashes with single backslashes
214 | query = query.replace("\\\\", "\\")
215 | # If the query.query contains forward slashes, replace them with backslashes
216 | query = query.replace("/", "\\")
217 |
218 | return self.everything_sdk.search_files(
219 | query=query,
220 | max_results=max_results,
221 | match_path=match_path,
222 | match_case=match_case,
223 | match_whole_word=match_whole_word,
224 | match_regex=match_regex,
225 | sort_by=sort_by
226 | )
227 |
```
--------------------------------------------------------------------------------
/src/mcp_server_everything_search/everything_sdk.py:
--------------------------------------------------------------------------------
```python
1 | """Everything SDK wrapper class."""
2 |
3 | import ctypes
4 | import datetime
5 | import struct
6 | import sys
7 | from typing import Any, List
8 | from pydantic import BaseModel
9 |
10 | # Everything SDK constants
11 | EVERYTHING_OK = 0
12 | EVERYTHING_ERROR_MEMORY = 1
13 | EVERYTHING_ERROR_IPC = 2
14 | EVERYTHING_ERROR_REGISTERCLASSEX = 3
15 | EVERYTHING_ERROR_CREATEWINDOW = 4
16 | EVERYTHING_ERROR_CREATETHREAD = 5
17 | EVERYTHING_ERROR_INVALIDINDEX = 6
18 | EVERYTHING_ERROR_INVALIDCALL = 7
19 |
20 | # Request flags
21 | EVERYTHING_REQUEST_FILE_NAME = 0x00000001
22 | EVERYTHING_REQUEST_PATH = 0x00000002
23 | EVERYTHING_REQUEST_FULL_PATH_AND_FILE_NAME = 0x00000004
24 | EVERYTHING_REQUEST_EXTENSION = 0x00000008
25 | EVERYTHING_REQUEST_SIZE = 0x00000010
26 | EVERYTHING_REQUEST_DATE_CREATED = 0x00000020
27 | EVERYTHING_REQUEST_DATE_MODIFIED = 0x00000040
28 | EVERYTHING_REQUEST_DATE_ACCESSED = 0x00000080
29 | EVERYTHING_REQUEST_ATTRIBUTES = 0x00000100
30 | EVERYTHING_REQUEST_FILE_LIST_FILE_NAME = 0x00000200
31 | EVERYTHING_REQUEST_RUN_COUNT = 0x00000400
32 | EVERYTHING_REQUEST_DATE_RUN = 0x00000800
33 | EVERYTHING_REQUEST_DATE_RECENTLY_CHANGED = 0x00001000
34 | EVERYTHING_REQUEST_HIGHLIGHTED_FILE_NAME = 0x00002000
35 | EVERYTHING_REQUEST_HIGHLIGHTED_PATH = 0x00004000
36 | EVERYTHING_REQUEST_HIGHLIGHTED_FULL_PATH_AND_FILE_NAME = 0x00008000
37 |
38 | # Sort options
39 | EVERYTHING_SORT_NAME_ASCENDING = 1
40 | EVERYTHING_SORT_NAME_DESCENDING = 2
41 | EVERYTHING_SORT_PATH_ASCENDING = 3
42 | EVERYTHING_SORT_PATH_DESCENDING = 4
43 | EVERYTHING_SORT_SIZE_ASCENDING = 5
44 | EVERYTHING_SORT_SIZE_DESCENDING = 6
45 | EVERYTHING_SORT_EXTENSION_ASCENDING = 7
46 | EVERYTHING_SORT_EXTENSION_DESCENDING = 8
47 | EVERYTHING_SORT_TYPE_NAME_ASCENDING = 9
48 | EVERYTHING_SORT_TYPE_NAME_DESCENDING = 10
49 | EVERYTHING_SORT_DATE_CREATED_ASCENDING = 11
50 | EVERYTHING_SORT_DATE_CREATED_DESCENDING = 12
51 | EVERYTHING_SORT_DATE_MODIFIED_ASCENDING = 13
52 | EVERYTHING_SORT_DATE_MODIFIED_DESCENDING = 14
53 | EVERYTHING_SORT_ATTRIBUTES_ASCENDING = 15
54 | EVERYTHING_SORT_ATTRIBUTES_DESCENDING = 16
55 | EVERYTHING_SORT_FILE_LIST_FILENAME_ASCENDING = 17
56 | EVERYTHING_SORT_FILE_LIST_FILENAME_DESCENDING = 18
57 | EVERYTHING_SORT_RUN_COUNT_ASCENDING = 19
58 | EVERYTHING_SORT_RUN_COUNT_DESCENDING = 20
59 | EVERYTHING_SORT_DATE_RECENTLY_CHANGED_ASCENDING = 21
60 | EVERYTHING_SORT_DATE_RECENTLY_CHANGED_DESCENDING = 22
61 | EVERYTHING_SORT_DATE_ACCESSED_ASCENDING = 23
62 | EVERYTHING_SORT_DATE_ACCESSED_DESCENDING = 24
63 | EVERYTHING_SORT_DATE_RUN_ASCENDING = 25
64 | EVERYTHING_SORT_DATE_RUN_DESCENDING = 26
65 |
66 | # Windows time conversion constants
67 | WINDOWS_TICKS = int(1/10**-7)
68 | WINDOWS_EPOCH = datetime.datetime.strptime('1601-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')
69 | POSIX_EPOCH = datetime.datetime.strptime('1970-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')
70 | EPOCH_DIFF = (POSIX_EPOCH - WINDOWS_EPOCH).total_seconds()
71 | WINDOWS_TICKS_TO_POSIX_EPOCH = EPOCH_DIFF * WINDOWS_TICKS
72 |
73 | class SearchResult(BaseModel):
74 | """Model for search results."""
75 | path: str
76 | filename: str
77 | extension: str | None = None
78 | size: int
79 | created: str | None = None
80 | modified: str | None = None
81 | accessed: str | None = None
82 | attributes: int | None = None
83 | run_count: int | None = None
84 | highlighted_filename: str | None = None
85 | highlighted_path: str | None = None
86 |
87 | class EverythingError(Exception):
88 | """Custom exception for Everything SDK errors."""
89 | def __init__(self, error_code: int):
90 | self.error_code = error_code
91 | super().__init__(self._get_error_message())
92 |
93 | def _get_error_message(self) -> str:
94 | error_messages = {
95 | EVERYTHING_ERROR_MEMORY: "Failed to allocate memory",
96 | EVERYTHING_ERROR_IPC: "IPC failed (Everything service not running?)",
97 | EVERYTHING_ERROR_REGISTERCLASSEX: "Failed to register window class",
98 | EVERYTHING_ERROR_CREATEWINDOW: "Failed to create window",
99 | EVERYTHING_ERROR_CREATETHREAD: "Failed to create thread",
100 | EVERYTHING_ERROR_INVALIDINDEX: "Invalid index",
101 | EVERYTHING_ERROR_INVALIDCALL: "Invalid call"
102 | }
103 | return error_messages.get(self.error_code, f"Unknown error: {self.error_code}")
104 |
105 | class EverythingSDK:
106 | """Wrapper for Everything SDK functionality."""
107 |
108 | def __init__(self, dll_path: str):
109 | """Initialize Everything SDK with the specified DLL path."""
110 | try:
111 | self.dll = ctypes.WinDLL(dll_path)
112 | self._configure_dll()
113 | except Exception as e:
114 | print(f"Failed to load Everything SDK DLL: {e}", file=sys.stderr)
115 | raise
116 |
117 | def _configure_dll(self):
118 | """Configure DLL function signatures."""
119 | # Search configuration
120 | self.dll.Everything_SetSearchW.argtypes = [ctypes.c_wchar_p]
121 | self.dll.Everything_SetMatchPath.argtypes = [ctypes.c_bool]
122 | self.dll.Everything_SetMatchCase.argtypes = [ctypes.c_bool]
123 | self.dll.Everything_SetMatchWholeWord.argtypes = [ctypes.c_bool]
124 | self.dll.Everything_SetRegex.argtypes = [ctypes.c_bool]
125 | self.dll.Everything_SetMax.argtypes = [ctypes.c_uint]
126 | self.dll.Everything_SetSort.argtypes = [ctypes.c_uint]
127 | self.dll.Everything_SetRequestFlags.argtypes = [ctypes.c_uint]
128 |
129 | # Query function
130 | self.dll.Everything_QueryW.argtypes = [ctypes.c_bool]
131 | self.dll.Everything_QueryW.restype = ctypes.c_bool
132 |
133 | # Result getters
134 | self.dll.Everything_GetNumResults.restype = ctypes.c_uint
135 | self.dll.Everything_GetLastError.restype = ctypes.c_uint
136 |
137 | self.dll.Everything_GetResultFileNameW.argtypes = [ctypes.c_uint]
138 | self.dll.Everything_GetResultFileNameW.restype = ctypes.c_wchar_p
139 | self.dll.Everything_GetResultExtensionW.argtypes = [ctypes.c_uint]
140 | self.dll.Everything_GetResultExtensionW.restype = ctypes.c_wchar_p
141 | self.dll.Everything_GetResultPathW.argtypes = [ctypes.c_uint]
142 | self.dll.Everything_GetResultPathW.restype = ctypes.c_wchar_p
143 |
144 | self.dll.Everything_GetResultFullPathNameW.argtypes = [
145 | ctypes.c_uint,
146 | ctypes.c_wchar_p,
147 | ctypes.c_uint
148 | ]
149 |
150 | self.dll.Everything_GetResultDateCreated.argtypes = [
151 | ctypes.c_uint,
152 | ctypes.POINTER(ctypes.c_ulonglong)
153 | ]
154 | self.dll.Everything_GetResultDateModified.argtypes = [
155 | ctypes.c_uint,
156 | ctypes.POINTER(ctypes.c_ulonglong)
157 | ]
158 | self.dll.Everything_GetResultDateAccessed.argtypes = [
159 | ctypes.c_uint,
160 | ctypes.POINTER(ctypes.c_ulonglong)
161 | ]
162 | self.dll.Everything_GetResultSize.argtypes = [
163 | ctypes.c_uint,
164 | ctypes.POINTER(ctypes.c_ulonglong)
165 | ]
166 | self.dll.Everything_GetResultAttributes.argtypes = [ctypes.c_uint]
167 | self.dll.Everything_GetResultRunCount.argtypes = [ctypes.c_uint]
168 |
169 | self.dll.Everything_GetResultHighlightedFileNameW.argtypes = [ctypes.c_uint]
170 | self.dll.Everything_GetResultHighlightedFileNameW.restype = ctypes.c_wchar_p
171 | self.dll.Everything_GetResultHighlightedPathW.argtypes = [ctypes.c_uint]
172 | self.dll.Everything_GetResultHighlightedPathW.restype = ctypes.c_wchar_p
173 |
174 | def _check_error(self):
175 | """Check for Everything SDK errors and raise appropriate exception."""
176 | error_code = self.dll.Everything_GetLastError()
177 | if error_code != EVERYTHING_OK:
178 | raise EverythingError(error_code)
179 |
180 | def _get_time(self, filetime: int) -> datetime.datetime:
181 | """Convert Windows filetime to Python datetime."""
182 | microsecs = (filetime - WINDOWS_TICKS_TO_POSIX_EPOCH) / WINDOWS_TICKS
183 | return datetime.datetime.fromtimestamp(microsecs)
184 |
185 | def search_files(
186 | self,
187 | query: str,
188 | max_results: int = 100,
189 | match_path: bool = False,
190 | match_case: bool = False,
191 | match_whole_word: bool = False,
192 | match_regex: bool = False,
193 | sort_by: int = EVERYTHING_SORT_NAME_ASCENDING,
194 | request_flags: int | None = None
195 | ) -> List[SearchResult]:
196 | """Perform file search using Everything SDK."""
197 | print(f"Debug: Setting up search with query: {query}", file=sys.stderr)
198 |
199 | # Set up search parameters
200 | self.dll.Everything_SetSearchW(query)
201 | self.dll.Everything_SetMatchPath(match_path)
202 | self.dll.Everything_SetMatchCase(match_case)
203 | self.dll.Everything_SetMatchWholeWord(match_whole_word)
204 | self.dll.Everything_SetRegex(match_regex)
205 | self.dll.Everything_SetMax(max_results)
206 | self.dll.Everything_SetSort(sort_by)
207 |
208 | # Set request flags
209 | if request_flags is None:
210 | request_flags = (
211 | EVERYTHING_REQUEST_FILE_NAME |
212 | EVERYTHING_REQUEST_PATH |
213 | EVERYTHING_REQUEST_EXTENSION |
214 | EVERYTHING_REQUEST_SIZE |
215 | EVERYTHING_REQUEST_DATE_CREATED |
216 | EVERYTHING_REQUEST_DATE_MODIFIED |
217 | EVERYTHING_REQUEST_DATE_ACCESSED |
218 | EVERYTHING_REQUEST_ATTRIBUTES |
219 | EVERYTHING_REQUEST_RUN_COUNT |
220 | EVERYTHING_REQUEST_HIGHLIGHTED_FILE_NAME |
221 | EVERYTHING_REQUEST_HIGHLIGHTED_PATH
222 | )
223 | self.dll.Everything_SetRequestFlags(request_flags)
224 |
225 | # Execute search
226 | print("Debug: Executing search query", file=sys.stderr)
227 | if not self.dll.Everything_QueryW(True):
228 | self._check_error()
229 | raise RuntimeError("Search query failed")
230 |
231 | # Get results
232 | print("Debug: Getting search results", file=sys.stderr)
233 | num_results = min(self.dll.Everything_GetNumResults(), max_results)
234 | results = []
235 |
236 | filename_buffer = ctypes.create_unicode_buffer(260)
237 | date_created = ctypes.c_ulonglong()
238 | date_modified = ctypes.c_ulonglong()
239 | date_accessed = ctypes.c_ulonglong()
240 | file_size = ctypes.c_ulonglong()
241 |
242 | for i in range(num_results):
243 | try:
244 | self.dll.Everything_GetResultFullPathNameW(i, filename_buffer, 260)
245 |
246 | # Get timestamps
247 | self.dll.Everything_GetResultDateCreated(i, date_created)
248 | self.dll.Everything_GetResultDateModified(i, date_modified)
249 | self.dll.Everything_GetResultDateAccessed(i, date_accessed)
250 | self.dll.Everything_GetResultSize(i, file_size)
251 |
252 | # Get other attributes
253 | filename = self.dll.Everything_GetResultFileNameW(i)
254 | extension = self.dll.Everything_GetResultExtensionW(i)
255 | attributes = self.dll.Everything_GetResultAttributes(i)
256 | run_count = self.dll.Everything_GetResultRunCount(i)
257 | highlighted_filename = self.dll.Everything_GetResultHighlightedFileNameW(i)
258 | highlighted_path = self.dll.Everything_GetResultHighlightedPathW(i)
259 |
260 | results.append(SearchResult(
261 | path=filename_buffer.value,
262 | filename=filename,
263 | extension=extension,
264 | size=file_size.value,
265 | created=self._get_time(date_created.value).isoformat() if date_created.value else None,
266 | modified=self._get_time(date_modified.value).isoformat() if date_modified.value else None,
267 | accessed=self._get_time(date_accessed.value).isoformat() if date_accessed.value else None,
268 | attributes=attributes,
269 | run_count=run_count,
270 | highlighted_filename=highlighted_filename,
271 | highlighted_path=highlighted_path
272 | ))
273 | except Exception as e:
274 | print(f"Debug: Error processing result {i}: {e}", file=sys.stderr)
275 | continue
276 |
277 | print("Debug: Resetting Everything SDK", file=sys.stderr)
278 | self.dll.Everything_Reset()
279 |
280 | return results
281 |
```
--------------------------------------------------------------------------------
/src/mcp_server_everything_search/server.py:
--------------------------------------------------------------------------------
```python
1 | """MCP server implementation for cross-platform file search."""
2 |
3 | import json
4 | import platform
5 | import sys
6 | from typing import List
7 | from mcp.server import Server
8 | from mcp.server.stdio import stdio_server
9 | from mcp.types import TextContent, Tool, Resource, ResourceTemplate, Prompt
10 | from pydantic import BaseModel, Field
11 |
12 | from .platform_search import UnifiedSearchQuery, WindowsSpecificParams, build_search_command
13 | from .search_interface import SearchProvider
14 |
15 | class SearchQuery(BaseModel):
16 | """Model for search query parameters."""
17 | query: str = Field(
18 | description="Search query string. See the search syntax guide for details."
19 | )
20 | max_results: int = Field(
21 | default=100,
22 | ge=1,
23 | le=1000,
24 | description="Maximum number of results to return (1-1000)"
25 | )
26 | match_path: bool = Field(
27 | default=False,
28 | description="Match against full path instead of filename only"
29 | )
30 | match_case: bool = Field(
31 | default=False,
32 | description="Enable case-sensitive search"
33 | )
34 | match_whole_word: bool = Field(
35 | default=False,
36 | description="Match whole words only"
37 | )
38 | match_regex: bool = Field(
39 | default=False,
40 | description="Enable regex search"
41 | )
42 | sort_by: int = Field(
43 | default=1,
44 | description="Sort order for results (Note: Not all sort options available on all platforms)"
45 | )
46 |
47 | async def serve() -> None:
48 | """Run the server."""
49 | current_platform = platform.system().lower()
50 | search_provider = SearchProvider.get_provider()
51 |
52 | server = Server("universal-search")
53 |
54 | @server.list_resources()
55 | async def list_resources() -> list[Resource]:
56 | """Return an empty list since this server doesn't provide any resources."""
57 | return []
58 |
59 | @server.list_resource_templates()
60 | async def list_resource_templates() -> list[ResourceTemplate]:
61 | """Return an empty list since this server doesn't provide any resource templates."""
62 | return []
63 |
64 | @server.list_prompts()
65 | async def list_prompts() -> list[Prompt]:
66 | """Return an empty list since this server doesn't provide any prompts."""
67 | return []
68 |
69 | @server.list_tools()
70 | async def list_tools() -> List[Tool]:
71 | """Return the search tool with platform-specific documentation and schema."""
72 | platform_info = {
73 | 'windows': "Using Everything SDK with full search capabilities",
74 | 'darwin': "Using mdfind (Spotlight) with native macOS search capabilities",
75 | 'linux': "Using locate with Unix-style search capabilities"
76 | }
77 |
78 | syntax_docs = {
79 | 'darwin': """macOS Spotlight (mdfind) Search Syntax:
80 |
81 | Basic Usage:
82 | - Simple text search: Just type the words you're looking for
83 | - Phrase search: Use quotes ("exact phrase")
84 | - Filename search: -name "filename"
85 | - Directory scope: -onlyin /path/to/dir
86 |
87 | Special Parameters:
88 | - Live updates: -live
89 | - Literal search: -literal
90 | - Interpreted search: -interpret
91 |
92 | Metadata Attributes:
93 | - kMDItemDisplayName
94 | - kMDItemTextContent
95 | - kMDItemKind
96 | - kMDItemFSSize
97 | - And many more OS X metadata attributes""",
98 |
99 | 'linux': """Linux Locate Search Syntax:
100 |
101 | Basic Usage:
102 | - Simple pattern: locate filename
103 | - Case-insensitive: -i pattern
104 | - Regular expressions: -r pattern
105 | - Existing files only: -e pattern
106 | - Count matches: -c pattern
107 |
108 | Pattern Wildcards:
109 | - * matches any characters
110 | - ? matches single character
111 | - [] matches character classes
112 |
113 | Examples:
114 | - locate -i "*.pdf"
115 | - locate -r "/home/.*\.txt$"
116 | - locate -c "*.doc"
117 | """,
118 | 'windows': """Search for files and folders using Everything SDK.
119 |
120 | Features:
121 | - Fast file and folder search across all indexed drives
122 | - Support for wildcards and boolean operators
123 | - Multiple sort options (name, path, size, dates)
124 | - Case-sensitive and whole word matching
125 | - Regular expression support
126 | - Path matching
127 | Search Syntax Guide:
128 | 1. Basic Operators:
129 | - space: AND operator
130 | - |: OR operator
131 | - !: NOT operator
132 | - < >: Grouping
133 | - " ": Search for an exact phrase
134 | 2. Wildcards:
135 | - *: Matches zero or more characters
136 | - ?: Matches exactly one character
137 | Note: Wildcards match the whole filename by default. Disable Match whole filename to match wildcards anywhere.
138 | 3. Functions:
139 | Size and Count:
140 | - size:<size>[kb|mb|gb]: Search by file size
141 | - count:<max>: Limit number of results
142 | - childcount:<count>: Folders with specific number of children
143 | - childfilecount:<count>: Folders with specific number of files
144 | - childfoldercount:<count>: Folders with specific number of subfolders
145 | - len:<length>: Match filename length
146 | Dates:
147 | - datemodified:<date>, dm:<date>: Modified date
148 | - dateaccessed:<date>, da:<date>: Access date
149 | - datecreated:<date>, dc:<date>: Creation date
150 | - daterun:<date>, dr:<date>: Last run date
151 | - recentchange:<date>, rc:<date>: Recently changed date
152 |
153 | Date formats: YYYY[-MM[-DD[Thh[:mm[:ss[.sss]]]]]] or today, yesterday, lastweek, etc.
154 |
155 | File Attributes and Types:
156 | - attrib:<attributes>, attributes:<attributes>: Search by file attributes (A:Archive, H:Hidden, S:System, etc.)
157 | - type:<type>: Search by file type
158 | - ext:<list>: Search by semicolon-separated extensions
159 |
160 | Path and Name:
161 | - path:<path>: Search in specific path
162 | - parent:<path>, infolder:<path>, nosubfolders:<path>: Search in path excluding subfolders
163 | - startwith:<text>: Files starting with text
164 | - endwith:<text>: Files ending with text
165 | - child:<filename>: Folders containing specific child
166 | - depth:<count>, parents:<count>: Files at specific folder depth
167 | - root: Files with no parent folder
168 | - shell:<name>: Search in known shell folders
169 | Duplicates and Lists:
170 | - dupe, namepartdupe, attribdupe, dadupe, dcdupe, dmdupe, sizedupe: Find duplicates
171 | - filelist:<list>: Search pipe-separated (|) file list
172 | - filelistfilename:<filename>: Search files from list file
173 | - frn:<frnlist>: Search by File Reference Numbers
174 | - fsi:<index>: Search by file system index
175 | - empty: Find empty folders
176 | 4. Function Syntax:
177 | - function:value: Equal to value
178 | - function:<=value: Less than or equal
179 | - function:<value: Less than
180 | - function:=value: Equal to
181 | - function:>value: Greater than
182 | - function:>=value: Greater than or equal
183 | - function:start..end: Range of values
184 | - function:start-end: Range of values
185 | 5. Modifiers:
186 | - case:, nocase:: Enable/disable case sensitivity
187 | - file:, folder:: Match only files or folders
188 | - path:, nopath:: Match full path or filename only
189 | - regex:, noregex:: Enable/disable regex
190 | - wfn:, nowfn:: Match whole filename or anywhere
191 | - wholeword:, ww:: Match whole words only
192 | - wildcards:, nowildcards:: Enable/disable wildcards
193 | Examples:
194 | 1. Find Python files modified today:
195 | ext:py datemodified:today
196 | 2. Find large video files:
197 | ext:mp4|mkv|avi size:>1gb
198 | 3. Find files in specific folder:
199 | path:C:\Projects *.js
200 | """
201 | }
202 |
203 | description = f"""Universal file search tool for {platform.system()}
204 |
205 | Current Implementation:
206 | {platform_info.get(current_platform, "Unknown platform")}
207 |
208 | Search Syntax Guide:
209 | {syntax_docs.get(current_platform, "Platform-specific syntax guide not available")}
210 | """
211 |
212 | return [
213 | Tool(
214 | name="search",
215 | description=description,
216 | inputSchema=UnifiedSearchQuery.get_schema_for_platform()
217 | )
218 | ]
219 |
220 | @server.call_tool()
221 | async def call_tool(name: str, arguments: dict) -> List[TextContent]:
222 | if name != "search":
223 | raise ValueError(f"Unknown tool: {name}")
224 |
225 | try:
226 | # Parse and validate inputs
227 | base_params = {}
228 | windows_params = {}
229 |
230 | # Handle base parameters
231 | if 'base' in arguments:
232 | if isinstance(arguments['base'], str):
233 | try:
234 | base_params = json.loads(arguments['base'])
235 | except json.JSONDecodeError:
236 | # If not valid JSON string, treat as simple query string
237 | base_params = {'query': arguments['base']}
238 | elif isinstance(arguments['base'], dict):
239 | # If already a dict, use directly
240 | base_params = arguments['base']
241 | else:
242 | raise ValueError("'base' parameter must be a string or dictionary")
243 |
244 | # Handle Windows-specific parameters
245 | if 'windows_params' in arguments:
246 | if isinstance(arguments['windows_params'], str):
247 | try:
248 | windows_params = json.loads(arguments['windows_params'])
249 | except json.JSONDecodeError:
250 | raise ValueError("Invalid JSON in windows_params")
251 | elif isinstance(arguments['windows_params'], dict):
252 | # If already a dict, use directly
253 | windows_params = arguments['windows_params']
254 | else:
255 | raise ValueError("'windows_params' must be a string or dictionary")
256 |
257 | # Combine parameters
258 | query_params = {
259 | **base_params,
260 | 'windows_params': windows_params
261 | }
262 |
263 | # Create unified query
264 | query = UnifiedSearchQuery(**query_params)
265 |
266 | if current_platform == "windows":
267 | # Use Everything SDK directly
268 | platform_params = query.windows_params or WindowsSpecificParams()
269 | results = search_provider.search_files(
270 | query=query.query,
271 | max_results=query.max_results,
272 | match_path=platform_params.match_path,
273 | match_case=platform_params.match_case,
274 | match_whole_word=platform_params.match_whole_word,
275 | match_regex=platform_params.match_regex,
276 | sort_by=platform_params.sort_by
277 | )
278 | else:
279 | # Use command-line tools (mdfind/locate)
280 | platform_params = None
281 | if current_platform == 'darwin':
282 | platform_params = query.mac_params or {}
283 | elif current_platform == 'linux':
284 | platform_params = query.linux_params or {}
285 |
286 | results = search_provider.search_files(
287 | query=query.query,
288 | max_results=query.max_results,
289 | **platform_params.dict() if platform_params else {}
290 | )
291 |
292 | return [TextContent(
293 | type="text",
294 | text="\n".join([
295 | f"Path: {r.path}\n"
296 | f"Filename: {r.filename}"
297 | f"{f' ({r.extension})' if r.extension else ''}\n"
298 | f"Size: {r.size:,} bytes\n"
299 | f"Created: {r.created if r.created else 'N/A'}\n"
300 | f"Modified: {r.modified if r.modified else 'N/A'}\n"
301 | f"Accessed: {r.accessed if r.accessed else 'N/A'}\n"
302 | for r in results
303 | ])
304 | )]
305 | except Exception as e:
306 | return [TextContent(
307 | type="text",
308 | text=f"Search failed: {str(e)}"
309 | )]
310 |
311 | options = server.create_initialization_options()
312 | async with stdio_server() as (read_stream, write_stream):
313 | await server.run(read_stream, write_stream, options, raise_exceptions=True)
314 |
315 | def configure_windows_console():
316 | """Configure Windows console for UTF-8 output."""
317 | import ctypes
318 |
319 | if sys.platform == "win32":
320 | # Enable virtual terminal processing
321 | kernel32 = ctypes.windll.kernel32
322 | STD_OUTPUT_HANDLE = -11
323 | ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
324 |
325 | handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
326 | mode = ctypes.c_ulong()
327 | kernel32.GetConsoleMode(handle, ctypes.byref(mode))
328 | mode.value |= ENABLE_VIRTUAL_TERMINAL_PROCESSING
329 | kernel32.SetConsoleMode(handle, mode)
330 |
331 | # Set UTF-8 encoding for console output
332 | sys.stdout.reconfigure(encoding='utf-8')
333 | sys.stderr.reconfigure(encoding='utf-8')
334 |
335 | def main() -> None:
336 | """Main entry point."""
337 | import asyncio
338 | import logging
339 | logging.basicConfig(
340 | level=logging.WARNING,
341 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
342 | )
343 |
344 | configure_windows_console()
345 |
346 | try:
347 | asyncio.run(serve())
348 | except KeyboardInterrupt:
349 | logging.info("Server stopped by user")
350 | sys.exit(0)
351 | except Exception as e:
352 | logging.error(f"Server error: {e}", exc_info=True)
353 | sys.exit(1)
354 |
```