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