Moondream MCP Server
by NightTrek
- src
- mcp_server_everything_search
"""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)