EXIF MCP Server
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@EXIF MCP Serverinspect exif of photo.jpg"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
EXIF MCP Server
Inspect and remove EXIF metadata locally through MCP tools.
This project is a stdio-first Python MCP server for reading EXIF metadata, detecting GPS/location fields, summarizing privacy-sensitive metadata, and writing cleaned image copies with full or selective EXIF removal for supported formats.
What This Project Does
The server exposes eleven MCP tools for local image paths:
inspect_exifinspect_exif_detailedhas_gps_exiffind_images_with_gps_exiffind_images_with_exif_fieldssummarize_exif_privacystrip_exifstrip_selected_exif_fieldsbatch_strip_exifbatch_strip_gps_exifbatch_strip_selected_exif_fields
It also exposes two MCP resources and two MCP prompts:
resources:
exif://privacy-guideexif://supported-formats
prompts:
review-photo-privacyclean-photos-for-sharing
It is designed for AI clients and agent workflows, and this repository is focused on the MCP server itself.
Project Shape
This repository is MCP-first:
the shared EXIF logic lives under
src/exif_mcp_server/core/the MCP adapter lives under
src/exif_mcp_server/tools/,resources/, andprompts/tests, examples, and docs are included so the project can work as a sample MCP server for learning and reuse
Supported Formats
Current v1 support is intentionally narrow:
.jpg.jpeg.png.webp.tif.tiff
Do not assume IPTC or XMP support in this MCP server.
Install
Requirements:
Python 3.11+
Set up a local virtual environment and install dependencies:
python3 -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'Why quote '.[dev]':
Some shells such as zsh treat brackets as glob patterns.
Run Tests
Run the full test suite:
pytestRun one focused test file:
pytest tests/test_inspect.py
pytest tests/test_privacy.py
pytest tests/test_clean.py
pytest tests/test_batch.pyThe repo also includes manual-test sample images in examples/sample_images/.
Lint And Type Check
Run Ruff:
ruff check .Run mypy against the typed source tree:
mypyThe current mypy configuration checks src/ and ignores missing type stubs for
piexif, which does not ship typed metadata.
Continuous Integration
Use the same core verification steps before publishing changes:
ruff check .mypypytest
Run The Server
Start the MCP server over stdio:
python -m exif_mcp_server.serverOr use the installed console entrypoint:
exif-mcp-serverThe server will appear idle in the terminal because it is waiting for an MCP client over stdio.
The default transport is still stdio. Remote transport is now optional and
must be selected explicitly.
Remote Transport
The server can now run with:
stdiostreamable-httpsse
Recommended remote transport:
streamable-http
Streamable HTTP
Run the server over Streamable HTTP on 127.0.0.1:8001:
python -m exif_mcp_server.server --transport streamable-httpChoose a custom host, port, and endpoint path:
python -m exif_mcp_server.server \
--transport streamable-http \
--host 0.0.0.0 \
--port 9000 \
--streamable-http-path /mcpUseful optional flags:
--json-response--stateless-http
Equivalent environment variables:
EXIF_MCP_TRANSPORT=streamable-httpEXIF_MCP_HOST=0.0.0.0EXIF_MCP_PORT=9000EXIF_MCP_STREAMABLE_HTTP_PATH=/mcpEXIF_MCP_JSON_RESPONSE=trueEXIF_MCP_STATELESS_HTTP=true
SSE
Run the server over SSE:
python -m exif_mcp_server.server --transport sseCustomize host, port, mount path, and SSE endpoint paths:
python -m exif_mcp_server.server \
--transport sse \
--host 0.0.0.0 \
--port 9001 \
--mount-path /github \
--sse-path /events \
--message-path /messages/Equivalent environment variables:
EXIF_MCP_TRANSPORT=sseEXIF_MCP_HOST=0.0.0.0EXIF_MCP_PORT=9001EXIF_MCP_MOUNT_PATH=/githubEXIF_MCP_SSE_PATH=/eventsEXIF_MCP_MESSAGE_PATH=/messages/
Smoke Test
Quickly verify that the server can be created:
python -c "from exif_mcp_server.server import create_server; print(type(create_server()).__name__)"Expected output:
FastMCPMCP Inspector
This project is stdio-first. To test it in an MCP Inspector or another local MCP client, configure a stdio server with:
command:
.venv/bin/pythonargs:
-m exif_mcp_server.serverworking directory: this repo root
If your MCP client expects the installed entrypoint instead, you can use:
command:
.venv/bin/exif-mcp-server
For a remote client that supports Streamable HTTP, run:
python -m exif_mcp_server.server --transport streamable-http --host 127.0.0.1 --port 8001Then connect the client to:
http://127.0.0.1:8001/mcpExpected tools:
inspect_exifinspect_exif_detailedhas_gps_exiffind_images_with_gps_exiffind_images_with_exif_fieldssummarize_exif_privacystrip_exifstrip_selected_exif_fieldsbatch_strip_exifbatch_strip_gps_exifbatch_strip_selected_exif_fields
Expected resources:
exif://privacy-guideexif://supported-formats
Expected prompts:
review-photo-privacyclean-photos-for-sharing
Client Examples
The exact configuration shape depends on the MCP client. The examples below were checked against the official client docs on April 18, 2026.
Which Client To Use
Client | Best for | Local stdio | Remote HTTP | Notes |
Claude Code | terminal-first MCP workflows | yes | yes | best fit if you want quick local testing and CLI management |
VS Code | editor-integrated development | yes | yes | good default if you want MCP tools inside a coding workspace |
Cursor | editor-integrated AI workflows | yes | yes | good fit if your main coding flow already lives in Cursor |
MCP Inspector | debugging and manual verification | yes | yes | best choice for checking raw tool/resource/prompt behavior |
Claude Desktop | end-user desktop app workflows | limited | yes | local setup now centers on desktop extensions rather than raw stdio config |
Recommended starting points:
use
MCP Inspectorfor the first manual smoke testuse
Claude Codeif you want the fastest terminal-based setupuse
VS CodeorCursorif you want the server available inside your editoruse
streamable-httpwhen you want one running server shared by multiple clients
Claude Code
Add the local stdio server:
claude mcp add --transport stdio exif-mcp -- \
/absolute/path/to/image-mcp-server/.venv/bin/python \
-m exif_mcp_server.serverAdd the remote Streamable HTTP server:
claude mcp add --transport http exif-mcp-http \
http://127.0.0.1:8001/mcpIf you want to use remote transport first, start the server separately:
python -m exif_mcp_server.server \
--transport streamable-http \
--host 127.0.0.1 \
--port 8001Useful Claude Code commands:
claude mcp listclaude mcp get exif-mcp/mcp
VS Code
VS Code uses mcp.json with a "servers" object. For a workspace-local setup,
create .vscode/mcp.json with:
{
"servers": {
"exif-mcp": {
"command": "/absolute/path/to/image-mcp-server/.venv/bin/python",
"args": ["-m", "exif_mcp_server.server"]
}
}
}For remote Streamable HTTP, use:
{
"servers": {
"exif-mcp-http": {
"type": "http",
"url": "http://127.0.0.1:8001/mcp"
}
}
}Notes:
workspace config lives in
.vscode/mcp.jsonuser-level config is available via
MCP: Open User ConfigurationVS Code also supports auto-discovery from other apps such as Claude Desktop
Cursor
Cursor uses .cursor/mcp.json in the project, or ~/.cursor/mcp.json
globally, with an "mcpServers" object.
Project-local stdio example:
{
"mcpServers": {
"exif-mcp": {
"type": "stdio",
"command": "/absolute/path/to/image-mcp-server/.venv/bin/python",
"args": ["-m", "exif_mcp_server.server"]
}
}
}Cursor's docs also support remote MCP configuration with fields such as url
and headers. For this server, the remote endpoint is:
http://127.0.0.1:8001/mcpMCP Inspector
For a local stdio session:
npx @modelcontextprotocol/inspector \
/absolute/path/to/image-mcp-server/.venv/bin/python \
-m exif_mcp_server.serverFor remote testing, first start the server:
python -m exif_mcp_server.server \
--transport streamable-http \
--host 127.0.0.1 \
--port 8001Then connect the Inspector to:
http://127.0.0.1:8001/mcpClaude Desktop
Claude Desktop's current official direction is different for local and remote servers:
local tools are now primarily packaged as desktop extensions (
.mcpb)remote MCP servers are added through
Settings > Connectors
This repo does not currently ship a Claude Desktop extension bundle, so the most straightforward client setups today are Claude Code, VS Code, Cursor, or MCP Inspector.
MCP Resources
The server publishes two short static resources:
exif://privacy-guidepractical explanation of EXIF privacy risk
what the server removes
what the server does not remove
exif://supported-formatscurrently supported image formats
overwrite behavior summary
stdio-first transport note
MCP Prompts
The server publishes two prompt templates:
review-photo-privacyguides a client through
inspect_exif,has_gps_exif, andsummarize_exif_privacy
clean-photos-for-sharingguides a client through safe folder cleanup with
batch_strip_exif
Example Tool Calls
Useful local sample paths from this repo:
examples/sample_images/plain-no-exif.jpgexamples/sample_images/basic-exif.jpgexamples/sample_images/gps-exif.jpgexamples/sample_images/tiff-exif.tiff
inspect_exif
Input:
{
"image_path": "/absolute/path/to/photo.jpg"
}Example output:
{
"image_path": "/absolute/path/to/photo.jpg",
"has_exif": true,
"exif": {
"Make": "Apple",
"Model": "iPhone 14",
"DateTimeOriginal": "2026:04:16 10:30:00"
},
"warnings": []
}inspect_exif_detailed
Input:
{
"image_path": "/absolute/path/to/photo.jpg"
}Example output (trimmed):
{
"image_path": "/absolute/path/to/photo.jpg",
"has_exif": true,
"exif": {
"Artist": "Blue J.",
"Make": "Canon"
},
"warnings": [],
"tags": [
{
"ifd": "0th",
"tag_id": 315,
"field_name": "Artist",
"field_key": "Artist",
"value": "Blue J."
}
]
}has_gps_exif
Input:
{
"image_path": "/absolute/path/to/photo.jpg"
}find_images_with_gps_exif
Input:
{
"folder_path": "/absolute/path/to/folder",
"recursive": false,
"extensions": null
}Example output:
{
"folder_path": "/absolute/path/to/folder",
"scanned_count": 2,
"matched_count": 1,
"failed_count": 0,
"skipped_count": 1,
"matches": [
{
"image_path": "/absolute/path/to/folder/photo.jpg",
"gps_fields_present": [
"GPSLatitude",
"GPSLatitudeRef",
"GPSLongitude",
"GPSLongitudeRef"
]
}
],
"failures": []
}Example output:
{
"image_path": "/absolute/path/to/photo.jpg",
"has_gps": true,
"gps_fields_present": [
"GPSLatitude",
"GPSLatitudeRef",
"GPSLongitude",
"GPSLongitudeRef"
]
}find_images_with_exif_fields
Input:
{
"folder_path": "/absolute/path/to/folder",
"field_names": ["Artist", "XPAuthor", "Copyright"],
"match_mode": "any",
"recursive": false,
"extensions": null
}Example output:
{
"folder_path": "/absolute/path/to/folder",
"requested_fields": ["Artist", "XPAuthor", "Copyright"],
"match_mode": "any",
"scanned_count": 2,
"matched_count": 1,
"failed_count": 0,
"skipped_count": 1,
"matches": [
{
"image_path": "/absolute/path/to/folder/author.jpg",
"matched_fields": ["Artist"]
}
],
"failures": []
}summarize_exif_privacy
Input:
{
"image_path": "/absolute/path/to/photo.jpg"
}Example output:
{
"image_path": "/absolute/path/to/photo.jpg",
"has_exif": true,
"privacy_risk": "high",
"findings": [
{
"field": "GPSLatitude",
"severity": "high",
"reason": "Location metadata can reveal where the photo was taken."
}
],
"summary": "This image contains GPS metadata."
}strip_exif
Input:
{
"image_path": "/absolute/path/to/photo.jpg",
"output_path": null,
"overwrite": false,
"dry_run": false,
"include_comparison": false,
"write_report": false
}Example output:
{
"source_path": "/absolute/path/to/photo.jpg",
"output_path": "/absolute/path/to/photo.cleaned.jpg",
"removed_exif": true,
"notes": [
"Created sibling cleaned file.",
"Removed EXIF metadata from the written image."
]
}strip_selected_exif_fields
Input:
{
"image_path": "/absolute/path/to/photo.jpg",
"field_names": ["Artist", "XPAuthor", "Copyright"],
"output_path": null,
"overwrite": false,
"dry_run": false,
"include_comparison": false,
"write_report": false
}Example output:
{
"source_path": "/absolute/path/to/photo.jpg",
"output_path": "/absolute/path/to/photo.cleaned.jpg",
"removed_fields": ["Artist"],
"removed_tag_count": 1,
"notes": [
"Created sibling cleaned file.",
"Removed selected EXIF fields from the written image."
]
}batch_strip_exif
Input:
{
"folder_path": "/absolute/path/to/folder",
"output_folder": null,
"recursive": false,
"overwrite": false,
"extensions": null,
"dry_run": false,
"include_comparison": false,
"write_report": false
}batch_strip_selected_exif_fields
Input:
{
"folder_path": "/absolute/path/to/folder",
"field_names": ["Artist", "XPAuthor", "Copyright"],
"output_folder": "/absolute/path/to/cleaned",
"recursive": false,
"overwrite": false,
"extensions": null,
"dry_run": false,
"include_comparison": false,
"write_report": false
}Example output:
{
"folder_path": "/absolute/path/to/folder",
"requested_fields": ["Artist", "XPAuthor", "Copyright"],
"processed_count": 1,
"success_count": 1,
"failed_count": 0,
"skipped_count": 0,
"results": [
{
"source_path": "/absolute/path/to/folder/author.jpg",
"output_path": "/absolute/path/to/cleaned/author.cleaned.jpg",
"status": "success",
"message": "Selected EXIF fields removed.",
"removed_fields": ["Artist"],
"removed_tag_count": 1
}
]
}batch_strip_gps_exif
Input:
{
"folder_path": "/absolute/path/to/folder",
"output_folder": "/absolute/path/to/cleaned",
"recursive": false,
"overwrite": false,
"extensions": null,
"dry_run": false,
"include_comparison": false,
"write_report": false
}Example output:
{
"folder_path": "/absolute/path/to/folder",
"processed_count": 1,
"success_count": 1,
"failed_count": 0,
"skipped_count": 0,
"results": [
{
"source_path": "/absolute/path/to/folder/photo.jpg",
"output_path": "/absolute/path/to/cleaned/photo.cleaned.jpg",
"status": "success",
"message": "GPS EXIF removed.",
"removed_gps": true
}
]
}Example output:
{
"folder_path": "/absolute/path/to/folder",
"processed_count": 2,
"success_count": 1,
"failed_count": 0,
"skipped_count": 1,
"results": [
{
"source_path": "/absolute/path/to/folder/photo.jpg",
"output_path": "/absolute/path/to/folder/photo.cleaned.jpg",
"status": "success",
"message": "EXIF removed."
},
{
"source_path": "/absolute/path/to/folder/ignore.bmp",
"status": "skipped",
"message": "Skipped because the file extension is not selected for batch processing."
}
]
}Overwrite Safety
The server is safe by default:
read-only tools do not modify files
strip_exifdoes not overwrite the source file unlessoverwrite=truedefault sibling outputs such as
photo.cleaned.jpgorphoto.cleaned.pngwill not overwrite an existing file unlessoverwrite=truebatch_strip_exifcontinues even if one file failsselective cleanup tools follow the same safe defaults:
strip_selected_exif_fieldsbatch_strip_selected_exif_fields
When overwrite=true, the server may rewrite the source image or replace an
existing target file.
Optional Cleanup Features
strip_exif, strip_selected_exif_fields, batch_strip_exif,
batch_strip_gps_exif, and batch_strip_selected_exif_fields support three
optional features:
dry_runvalidate the request and show the predicted output path
no image files or report files are written
include_comparisoninclude a compact before/after EXIF summary in the result
fields:
before_has_exifafter_has_exifremoved_fieldsremaining_fields
write_reportwrite a sidecar JSON report next to each cleaned output image
example sidecar path:
photo.cleaned.exif-report.json
Example strip_exif dry run:
{
"image_path": "/absolute/path/to/photo.jpg",
"dry_run": true,
"include_comparison": true,
"write_report": true
}Example dry-run result:
{
"source_path": "/absolute/path/to/photo.jpg",
"output_path": "/absolute/path/to/photo.cleaned.jpg",
"removed_exif": true,
"dry_run": true,
"comparison": {
"before_has_exif": true,
"after_has_exif": false,
"removed_fields": ["DateTimeOriginal", "Make"],
"remaining_fields": []
},
"notes": [
"Created sibling cleaned file.",
"Dry run only; no files were written.",
"Dry run would remove EXIF metadata from the output image.",
"Dry run skipped writing the sidecar JSON report."
]
}Example sidecar report output:
{
"source_path": "/absolute/path/to/photo.jpg",
"output_path": "/absolute/path/to/photo.cleaned.jpg",
"removed_exif": true,
"dry_run": false,
"comparison": {
"before_has_exif": true,
"after_has_exif": false,
"removed_fields": ["DateTimeOriginal", "Make"],
"remaining_fields": []
},
"notes": [
"Created sibling cleaned file.",
"Removed EXIF metadata from the written image."
]
}Manual Testing With Sample Images
The repo includes small synthetic images under examples/sample_images/:
plain-no-exif.jpgbasic-exif.jpggps-exif.jpgtiff-exif.tiff
Useful manual checks:
Call
inspect_exifonbasic-exif.jpgand confirm device/timestamp fields are present.Call
has_gps_exifongps-exif.jpgand confirm GPS fields are detected.Call
summarize_exif_privacyongps-exif.jpgand confirm the risk ishigh.Call
strip_exifongps-exif.jpgand confirm the cleaned output hashas_exif: false.Call
find_images_with_gps_exifon a folder and confirm only GPS-bearing files are returned.Call
batch_strip_gps_exifon a folder and confirm GPS data is removed while other EXIF fields remain when possible.Call
batch_strip_exifonexamples/sample_images/and confirm supported files are processed and pre-existing*.cleaned.<ext>outputs are not overwritten unless requested.Call
inspect_exiforstrip_exifontiff-exif.tiffand confirm TIFF EXIF is inspected and cleaned correctly.Call
find_images_with_exif_fieldswith["Artist", "XPAuthor", "Copyright"]and confirm only author-bearing files match.Call
batch_strip_selected_exif_fieldswith anoutput_folderand confirm selected fields are removed while non-selected EXIF remains.
Tool Error Format
Successful tool responses keep their normal JSON result shapes.
Tool failures are exposed with a stable error string prefix so MCP clients can recognize and parse them predictably:
EXIF_TOOL_ERROR {"code":"file_not_found","message":"...","tool":"inspect_exif"}Current public error codes include:
file_not_foundinvalid_pathinvalid_metadata_selectionunsupported_image_typeexif_read_errorexif_write_errorunsafe_overwriteexif_errorinternal_error
Architecture Overview
The project is structured in three layers:
Shared core in
src/exif_mcp_server/core/Thin MCP tool wrappers in
src/exif_mcp_server/tools/Stdio server bootstrap in
src/exif_mcp_server/server.py
The MCP layer is intentionally thin. EXIF reading, GPS detection, privacy summary logic, GPS-folder scanning, single-file cleaning, and batch cleaning live in the shared core.
Current Status
The required MVP tools are implemented and the project now goes beyond the original MVP:
stdio,
streamable-http, andssetransports are availableJPG/JPEG/PNG/WebP/TIFF support is implemented and tested
optional MCP resources and prompts are implemented
GPS-focused folder scan and GPS-only batch cleanup tools are implemented
Still out of scope or future-facing:
IPTC or XMP editing
cloud storage workflows
production auth and deployment hardening for remote transport
License
MIT
Books by the Authors
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/nextframedev/exif_mcp_server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server