shutter-mcp
Server Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| ROOT | Yes | A directory to allow. Can be specified multiple times (e.g., --root /path1 --root /path2). |
Instructions
Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.
This server publishes no instructions, or was last inspected before Glama recorded them.
Capabilities
Features and capabilities supported by this server
Protocol revision2025-11-25
| Capability | Details |
|---|---|
| tools | {
"listChanged": false
} |
| prompts | {
"listChanged": false
} |
| resources | {
"subscribe": false,
"listChanged": false
} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| scan_libraryA | Recursively scan a directory and summarize what is in it. Walks root recursively (symlinks are not followed) and reports file counts by extension, total bytes, and the earliest/latest file modification time. Stops after 50000 files and reports truncation if the library is larger than that. Does not read file contents or EXIF; use library_stats for EXIF aggregates. Args: params (ScanLibraryInput): Validated input containing: - root (str): Directory to scan, must be inside an allowed --root Returns: dict: { "schema_version": 1, "root": str, "total_files": int, "total_bytes": int, "counts_by_extension": {".jpg": int, ...}, "earliest_mtime": str | None, # ISO 8601 UTC "latest_mtime": str | None, # ISO 8601 UTC "truncated": bool, "truncation_note": str | None, } On error: {"schema_version": 1, "error": str} Examples: - Use when: "How many files are in my 2026 shoot folder and how big is it?" - Don't use when: you need EXIF camera/lens breakdowns (use library_stats) |
| library_statsA | Aggregate EXIF metadata across a library: cameras, lenses, ISO, focal length, dates. Scans EXIF-readable files (jpg, jpeg, tif, tiff, png, dng, arw, raf, nef, cr2, cr3) under root. If more files are found than sample_limit, the newest files by modification time are sampled and this is noted in the result. Files with no readable EXIF are counted in skipped_no_exif. Args: params (LibraryStatsInput): Validated input containing: - root (str): Directory to aggregate over, must be inside an allowed --root - sample_limit (int): Max files to inspect, default 2000, 1-50000 Returns: dict: { "schema_version": 1, "root": str, "files_considered": int, "files_sampled": int, "sampled": bool, "sample_note": str | None, "skipped_no_exif": int, "counts_by_camera_model": {str: int, ...}, "counts_by_lens": {str: int, ...}, "counts_by_focal_length_bucket": {str: int, ...}, "counts_by_iso_bucket": {str: int, ...}, "counts_by_year_month": {"YYYY-MM": int, ...}, } On error: {"schema_version": 1, "error": str} Examples: - Use when: "What cameras and lenses did I use most this year?" - Use when: "Break down this shoot by ISO." - Don't use when: you need per-file EXIF (use read_exif) |
| read_exifA | Read flattened EXIF fields for a single image file. Best effort across jpg, jpeg, tif, tiff, png, dng, arw, raf, nef, cr2, cr3. A file with no EXIF data, or an unsupported extension, is not an error: fields comes back empty with a note explaining why. Args: params (ReadExifInput): Validated input containing: - path (str): Path to one image file, must be inside an allowed --root Returns: dict: { "schema_version": 1, "path": str, "fields": { "camera_make": str, "camera_model": str, "lens_model": str, "focal_length": str, "focal_length_35mm_equiv": str, "iso": str, "exposure_time": str, "f_number": str, "exposure_program": str, "date_time_original": str, "date_time": str, "width": str, "height": str, "orientation": str, "software": str, "gps_latitude": float, "gps_longitude": float, }, # only keys actually present in the file are included "note": str | None, # e.g. "no EXIF data found" } On error: {"schema_version": 1, "error": str} Examples: - Use when: "What lens and settings were used for this specific frame?" - Don't use when: you want aggregates across many files (use library_stats) |
| find_duplicatesA | Find duplicate files under a directory, exact or visually similar. 'exact' mode groups byte-identical files (any file type) by sha256 hash. 'visual' mode groups jpg/png images whose perceptual hash (phash) is within a hamming distance of 5, catching near-duplicate bursts even when bytes differ. Visual mode internally samples up to 3000 newest files for performance on very large libraries. Returns at most 200 duplicate groups; truncated indicates whether more groups exist beyond that cap. Args: params (FindDuplicatesInput): Validated input containing: - root (str): Directory to scan, must be inside an allowed --root - mode ("exact" | "visual"): Duplicate detection mode, default "exact" Returns: dict: { "schema_version": 1, "root": str, "mode": str, "files_considered": int, "duplicate_group_count": int, "truncated": bool, "groups": [{"count": int, "files": [str, ...]}, ...], # visual mode additionally includes: files_hashed, sampled, sample_note, truncation_note # exact mode additionally includes: skipped, total_duplicate_groups_found } On error: {"schema_version": 1, "error": str} Examples: - Use when: "Find exact duplicate files I can safely delete one copy of." - Use when: "Find near-duplicate frames from burst shooting." - Don't use when: you want blur ranking, not duplication (use blur_scores) |
| blur_scoresA | Score jpg files under a directory for blur using Laplacian variance. jpg/jpeg only in the MVP. Lower variance means less high-frequency detail, i.e. more likely blurry. Results are sorted ascending (blurriest first); the bottom decile is flagged likely_blurry. Args: params (BlurScoresInput): Validated input containing: - root (str): Directory of jpg files to score, must be inside an allowed --root - limit (int): Max files to score, default 500, 1-5000 Returns: dict: { "schema_version": 1, "root": str, "files_considered": int, "files_scored": int, "sampled": bool, "sample_note": str | None, "skipped": int, "results": [ {"path": str, "laplacian_variance": float, "likely_blurry": bool}, ... ], # sorted ascending by laplacian_variance } On error: {"schema_version": 1, "error": str} Examples: - Use when: "Which jpgs in this folder are probably out of focus?" - Don't use when: the library has RAW files only (RAW pixel decode is out of scope for MVP) |
| cull_reportA | Build a markdown cull report combining scan, EXIF stats, dupes, and blur. Runs scan_library, library_stats, find_duplicates (exact mode), and blur_scores under the hood, and combines them into one markdown report: library summary, top EXIF stats, exact duplicate groups, and the blurriest 20 jpg files. Returns markdown text only; never writes a file. Args: params (CullReportInput): Validated input containing: - root (str): Directory to report on, must be inside an allowed --root Returns: str: Markdown-formatted report, or "Error: " on failure. Examples: - Use when: "Give me a summary I can read before I start culling this shoot." - Don't use when: you need machine-readable output (use the individual tools instead) |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
No prompts | |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
TDQS
Scored across 6 tools
Each tool targets a clearly distinct level of analysis: whole-library scanning, EXIF aggregation, single-file EXIF, duplicate detection, blur scoring, and a combined report. Descriptions explicitly cross-reference each other with 'use X instead' notes, leaving no realistic chance of misselection.
All names use consistent snake_case, which is the important part. Most are verb_noun or clear noun_phrase (scan_library, read_exif, find_duplicates), though library_stats, blur_scores, and cull_report are noun-first, a minor stylistic deviation.
Six tools is well scoped for a photo-library analysis server. Each tool earns its place, and the composite cull_report cleanly wraps the primitives without duplicating their interfaces.
The analysis surface is strong: scanning, metadata aggregation, per-file inspection, deduplication, blur detection, and a report. Minor gaps remain for a culling workflow — no way to flag, move, or delete rejected files, and blur scoring is jpg-only — but these are acknowledged limitations rather than dead ends for the stated read-only purpose.