DEVONthink-focused MCP server for macOS.
This repo exposes DEVONthink automation as MCP tools (AppleScript-backed), with a practical default toolset for search, metadata work, OCR flows, linking, reminders, messaging actions, and batch organization.
- What You Can Do
- Signal Model
- Link Intelligence Layer
- AppleScript Compatibility Notes
- Connectivity Shape Classifier
- Tool Surface
- Tool Profiles
- Safe Composition Patterns
- Configuration
- Snapshots
- Maintenance Pass
- Recommended Workflow
- Testing
- Requirements
- Install
- Start Server
- Client Connector Setup (VS Code / Codex / Claude Code)
- Validation
- Project Layout
Prompted with the following: "identify the tags in DEVONthink, and create a hub note that presents the tags in a hierarchy with related tags grouped together" Deepseek 4 output:
Core workflows verified in this repo:
- Search and lookup
- advanced search queries
- lookup by UUID /
x-devonthink-item://reference URL - list recent documents in a folder (e.g., Inbox)
- Organize and process
- create/move records and groups
- batch classify/move/tag/comment operations
- build tracker/hub notes during processing
- Metadata and linking
- tags, comments, custom metadata
- record links (
reference URL) for cross-link workflows
- OCR and extraction
- OCR images/PDFs
- set searchable text on image/PDF records
- pull page/time/frame-oriented media properties where supported
- Reminder and notifications
- add reminders with alarm types (
notification,sound, etc.)
- add reminders with alarm types (
- Messaging/email actions
- send record links by email
- send file attachments through Messages (subject to local account routing)
For long-running wiki-style databases (zettelkasten / research graph), use:
- Square-bracket links as primary (
[[...]])
- best precision and maintainability at scale
- fewer accidental auto-links than free-form matching
- Names/aliases with discipline
- enable only when aliases are curated and specific
- avoid broad/common aliases that create noisy links
- Use links + mentions as retrieval signals
- rely on
reference URL(x-devonthink-item://...) for durable linking - use mention/backlink workflows to find related notes and maintain hub indexes
Practical pattern:
- record link generation -> mention search -> summarize mentions -> tracker/hub updates
This repo supports actionable link intelligence:
- Generate item links from records (
reference URL) - Find mention/backlink candidates by searching for item-link strings
- Build mention summaries with
summarize mentions of - Use authoritative edge properties on records:
incoming referencesoutgoing referencesincoming Wiki referencesoutgoing Wiki references
These can be chained in batch workflows (e.g., gather related notes, create hub pages, keep done/remaining state).
Link-intelligence tools use a three-tier signal hierarchy:
authoritative: native DEVONthink properties (incoming references,outgoing references,incoming Wiki references,outgoing Wiki references,aliases,tags,reference URL)structural: deterministic derivations (title/path/group context, metadata-derived structure)inferred: probabilistic derivations (fuzzy title similarity, mention-text and related heuristics)
Every response includes lowest_signal_tier.
Graph outputs derived from inferred signals are labeled and should not be treated as authoritative edge data.
Use mode=authoritative when a tool exposes it unless you explicitly need inferred content-derived signals. mode=authoritative uses only native DEVONthink graph properties such as incoming/outgoing references, wiki references, aliases, tags, and reference URL; these paths are the fast default and typically complete in 1-3 Apple Events. mode=full adds content scan and inferred wikilink extraction, so it is slower and should be used only when [[wikilink]]-style or mention-text signals are required.
Operational rule:
- never call
mode=fullinside a loop, batch operation, or broad folder traversal - prefer
devonthink-link-audit-folderover repeateddevonthink-link-audit-recordcalls when you need folder-wide health data - treat inferred outputs as advisory, not authoritative graph truth
Composed tools are available for zettelkasten-scale link operations:
- Inspect (canonical)
devonthink-link-resolvedevonthink-link-audit-recorddevonthink-link-audit-folder
- Analyze (canonical)
devonthink-link-map-neighborhooddevonthink-link-find-orphansdevonthink-link-suggest-relateddevonthink-link-scoredevonthink-link-detect-bridgesdevonthink-link-check-reciprocal
- Act (advanced/full profile)
devonthink-link-build-hubdevonthink-link-enrich-metadatadevonthink-link-repair-linksdevonthink-link-maintenance-passdevonthink-link-traverse-folderdevonthink-link-compare-snapshotsdevonthink-link-prune-snapshots
All link-intelligence tools return a versioned response envelope with:
contract.contract_versioncontract.signal_model_versioncontract.toolset_versionobservability(duration, warnings, stats)
devonthink-link-traverse-folder additionally supports:
- cursor-based resume (
cursor) - node-first adjacency map + flat edge list
- dedup by UUID (replicant-safe)
- recursive traversal (
mode=recursive,max_depth, cycle-safe group traversal) - optional group path tracking (
group_path_tracking=true) - optional baseline snapshot files (
snapshots/*.json+.meta.json)
devonthink-link-compare-snapshots additionally supports:
- explicit baseline/current snapshot paths (with optional explicit meta paths)
- auto-discovery mode by
folder_refusing the two most recent snapshots insnapshot_dir - tombstone-aware edge removals (
removal_reason: tombstoned|unlinked) diff_confidenceandhealth_verdictsummary fields
devonthink-link-prune-snapshots additionally supports:
- conservative tiered retention policy with non-destructive defaults
- explicit dry-run mode (
mode=report) before any file mutations - archive-first apply mode (
mode=apply) with optional hard-delete threshold
Note: DEVONthink returns
Invalid argument (-50)whensearchis called with a database object as theinargument. Link tools handle this by degrading gracefully and incrementingobservability.stats.search_calls_degraded. Thedevonthink-search-recordstool resolves this automatically: ifdatabase_uuidreceives a database UUID, it fetches the database'sincoming groupand uses that as the search scope instead. Pass a group UUID to scope search to a specific folder; pass a database UUID to scope to the database root. The rawdevonthink-searchdictionary wrapper now applies the same group-or-database UUID guard for itsinparameter. Note: Smart Groups are traversed as saved queries (viasearch predicates+search group), not as physical children. Output marks this withmembership_type: virtualandchildren_source: smart_group_query.
DEVONthink's AppleScript model mixes typed enumerations and plain-string properties. Knowing which is which prevents silent failures.
| Property | Comparison form | Example values |
|---|---|---|
type |
AppleScript enum token (unquoted) | markdown, txt, rtf, group, pdf document, picture, bookmark |
kind |
String (quoted) | "Markdown", "Plain Text", "Formatted Note", "PDF Document" |
Use type of theRecord is markdown (enum), not type of theRecord is "markdown" (string).
Forum-confirmed: if type of theRecord is markdown then ... works; string comparison against type does not.
For media, prefer devonthink-search-media-records over kind:Movie/kind:Audio searches.
DEVONthink's own dictionary warns that kind is human-readable/localized and should not be used
for type checks. The media wrapper filters on record type=multimedia and returns structured
mime_type, filename, extension, duration, and size fields so EPUB/PDF false positives are
visible immediately.
For combined search + metadata constraints, prefer devonthink-filter-records. It searches or
enumerates records and filters the returned structured properties directly:
- PDF/RTF/PNG queries: use
file_extensions:["pdf"],record_types:["rtf"], etc. instead ofkind:*. - Filename/title lookup: use
filename_contains:"todo.txt"orname_contains:"Meeting Notes"instead of broad content search. - Exact tags: use
tags:["archived"]instead oflabel:archivedor browsing the Tags group. - Date ranges: use
created_from:"2024-01-01",created_to:"2024-03-31"instead of free-form date predicates. - Dimensions and size: use
min_width,max_width,min_size, etc. - Rich text phrase checks: use
content_mode:"plain_text"when full-text search misses RTF content.
For record content, prefer devonthink-read-record-text after resolving the record UUID. It reads
DEVONthink's own plain text/transcription property for text, rich text, Markdown, and searchable
PDF records before falling back to external filesystem tools.
DEVONthink label is a numeric color label only:
| Value | Meaning |
|---|---|
0 |
None |
1 |
Red |
2 |
Orange |
3 |
Yellow |
4 |
Green |
5 |
Blue |
6 |
Purple |
7 |
Teal |
For textual workflow state such as TODO, use devonthink-batch-update-record-metadata with
tags:["TODO"], comment:"TODO", or both. Use devonthink-set-label /
devonthink-batch-set-label only when you intentionally want a color label.
Use structured MCP wrappers first. When a workflow is blocked by DEVONthink AppleScript quirks,
devonthink-run-applescript can run an ad hoc AppleScript probe from a managed scratch directory:
.devonthink-mcp/osascript-runs/
The tool writes both the .applescript source and a .result.json transcript. This gives agents a
controlled fallback without scattering scripts in /tmp, and makes it easy to review repeated
fallbacks later and promote common patterns into first-class wrappers. The scratch directory is
ignored by git. Treat this tool as advanced/high-risk because it executes arbitrary AppleScript.
DEVONthink has two distinct annotation concepts:
devonthink-create-annotation-notecreates a record in the database Annotations group and attaches it through the target record'sannotationproperty. Live testing confirmed this works for PDFs, normal records, groups, smart groups, and annotation-note records.devonthink-summarize-annotationssummarizes PDF/internal highlights, comments, and page markup using the required syntaxsummarize annotations of records theRecords to markdown in destinationGroup.
Attached annotation notes do not count as PDF-internal markup. If a record only has an attached annotation note, DEVONthink can return missing value from summarize annotations; the wrapper reports data: null with an observability warning.
DEVONthink is strict about some dictionary command shapes:
- Use
duplicate record theRecord to destinationGroup, after resolvingtheRecordwithget record with uuid .... The shorterduplicate theRecord to destinationGroupform can fail with a missing-parameter error. - Use a group object for
search ... in .... Database UUID scopes must be converted to the database'sincoming group. - Use
summarize annotations of records {recordList} to markdown in destinationGroup; omittingrecords, output format, or destination can fail or return no output.
devonthink-create-rtfd is text-only. DEVONthink can silently create an RTF record instead of a true RTFD record when no binary attachment data is provided. The wrapper now returns actual_type and warns when DEVONthink downgrades the result.
Pass the AppleScript enum token, not the human-readable label:
| Token | Human label |
|---|---|
markdown |
Markdown |
txt |
Plain Text |
rtf |
Rich Text |
group |
Group |
pdf document |
PDF Document |
picture |
Image |
bookmark |
Bookmark |
Passing an unsupported string (e.g., "plain text" instead of "txt") causes DEVONthink to return
missing value, which surfaces as an opaque -1728 AppleScript error. The tool validates this
before dispatching the AppleScript call and returns {"ok": false, "error": "..."} with a
clear message.
DEVONthink's lookup records with tags tagList requires a record to have every tag in the list.
To find records sharing any of a source record's tags (OR semantics), query one tag at a time
and deduplicate by UUID. devonthink-link-suggest-related does this automatically.
To replicate OR-style tag search manually use DEVONthink search syntax:
any: tags:concordance tags:methodology
get links of fails with -1708 ("doesn't understand the message") for markdown and txt
record types in DEVONthink 4.1.1. Link-intelligence tools degrade gracefully to wikilink and
mention-text scanning when this command fails, and record the degradation in observability.warnings.
Explicit x-devonthink-item:// links pasted into markdown files are still auto-converted by
DEVONthink and are detectable via content scanning.
| Shape | Default criteria | Meaning |
|---|---|---|
hub |
outgoing >= 5 and incoming >= 2 | Central organizing note |
spoke |
incoming >= 2 and outgoing <= 2 | Well-referenced leaf |
bridge |
outgoing >= 3 and connects >= 2 clusters | Cross-topic linker |
sink |
incoming >= 3 and outgoing = 0 | Referenced but no outbound links |
near_orphan |
total edges <= 1 | Weakly connected |
isolated |
incoming = 0 and outgoing = 0 | True orphan |
connected |
has links but no stronger shape rule match | General connectivity |
The server exposes two layers:
- Specialized structured tools (recommended default)
devonthink-search-recordsdevonthink-get-record-by-uuiddevonthink-get-database-by-uuiddevonthink-get-database-incoming-groupdevonthink-list-group-childrendevonthink-create-recorddevonthink-link-*(tiered link intelligence tools)devonthink-link-maintenance-pass— delta-based health reporting against snapshot baseline
- Generated dictionary tools (broad coverage)
- names follow
devonthink-<action> - powered from bundled command specs:
app/data/devonthink_command_specs.json
Control how many tools are exposed:
minimal: specialized tools onlycanonical: specialized + canonical link tools + canonical dictionary tools (default)full: specialized + canonical/advanced link tools + all dictionary tools
Treat these as an escalation ladder, not a flat menu:
minimal
- use when you want the smallest, safest surface
- best for retrieval, lookup, simple creation, Inbox/database-root resolution, and direct child listing
canonical
- use when you need the normal operating surface
- adds read-heavy dictionary tools and canonical link-analysis tools
- best default for most clients and weaker models
full
- use only when you explicitly need advanced/native act-layer commands
- exposes destructive, UI-coupled, and maintenance-oriented tools
- required for tools like
devonthink-link-repair-links,devonthink-link-enrich-metadata,devonthink-link-build-hub,devonthink-link-maintenance-pass, anddevonthink-link-traverse-folder
Warning: act-layer tools mutate DEVONthink records (
devonthink-link-repair-links,devonthink-link-enrich-metadata,devonthink-link-maintenance-pass,devonthink-link-build-hub). Prefer--profile canonicalfor read-only usage and use report/dry-run modes first.
Deployment status:
devonthink-link-repair-links: production ready (idempotent apply verified)devonthink-link-build-hub: production ready (dedup on re-run verified)devonthink-link-enrich-metadata: production readydevonthink-link-maintenance-pass: production ready (delta-wired against snapshot baseline)devonthink-link-compare-snapshots: production readydevonthink-link-prune-snapshots: production ready (mode=applyrequires explicit invocation)
Example:
DEVONTHINK_TOOL_PROFILE=full python3 main.py --transport=stdioProfile guidance for clients:
- start in
minimalif the client is only doing stable lookups, search, or simple creation - move to
canonicalwhen the client needs analysis or native dictionary coverage - escalate to
fullonly after the task clearly requires act-layer or low-level DEVONthink commands
Use these sanctioned workflows instead of improvising ad hoc multi-step edits.
devonthink-get-database-by-uuiddevonthink-get-database-incoming-group- pass the returned group UUID into
devonthink-search-recordsordevonthink-list-group-children
Notes:
devonthink-search-recordsaccepts either a group UUID or a database UUID indatabase_uuid- when given a database UUID, it auto-resolves to the database's incoming group instead of issuing the raw DEVONthink
search ... in databasecall that can fail withInvalid argument (-50)
devonthink-get-record-by-uuiddevonthink-list-group-childrenordevonthink-get-database-incoming-groupto resolve the destination scopedevonthink-move- verify with
devonthink-get-record-by-uuidordevonthink-search-records
devonthink-link-traverse-folderwithwrite_snapshot=truedevonthink-link-maintenance-passinmode=report- review
observability.warnings,health_verdict, and candidate actions - only then use
mode=applywhere supported
devonthink-search-records- inspect returned UUIDs with
devonthink-get-record-by-uuidordevonthink-link-audit-record - apply writes with the smallest possible mutating tool
- verify by re-running search or lookup after the write
devonthink-get-database-incoming-groupif you start from a database UUIDdevonthink-list-group-childrenfor direct childrendevonthink-link-traverse-folderonly when you need recursive traversal, smart-group handling, adjacency output, or snapshots
Link-analysis tuning is configurable via environment variables:
DEVONTHINK_LINK_SEARCH_MIN_LENGTH(default2)DEVONTHINK_LINK_FUZZY_SKIP_THRESHOLD(default3)DEVONTHINK_LINK_MAX_TRAVERSAL_DEPTH(default3)- shape thresholds:
DEVONTHINK_LINK_HUB_OUTGOING,DEVONTHINK_LINK_HUB_INCOMING,DEVONTHINK_LINK_SPOKE_INCOMING,DEVONTHINK_LINK_SPOKE_OUTGOING_MAX,DEVONTHINK_LINK_BRIDGE_OUTGOING,DEVONTHINK_LINK_BRIDGE_CLUSTERS,DEVONTHINK_LINK_SINK_INCOMING,DEVONTHINK_LINK_NEAR_ORPHAN_TOTAL_MAX DEVONTHINK_LINK_BENCHMARK_GROUP_UUID(optional, default group UUID for benchmark runner when CLI arg is omitted)
devonthink-link-traverse-folder can write snapshot pairs to:
snapshots/
<prefix>_<timestamp>.json
<prefix>_<timestamp>.meta.json
Snapshots are append-only; no automatic cleanup is performed.
For long-running knowledge bases, prune old snapshots based on your retention window.
The most recent snapshot pair for a folder is automatically used by maintenance-pass as its baseline.
Snapshots are never auto-deleted; use devonthink-link-prune-snapshots for lifecycle management.
devonthink-link-maintenance-pass is the scheduled health tool for long-running
knowledge bases. It compares your current graph state against the most recent
snapshot baseline and reports only what changed — not the full absolute state.
On the first run against a folder, no baseline exists yet. The tool captures the current state as the baseline and returns:
{
"first_run": true,
"message": "No baseline found. Current state captured as baseline. Run again to see deltas."
}Run it a second time after making changes to see the first real delta report.
Every run after the first compares against the previous snapshot and returns
a health_verdict plus actionable rows:
health_verdict |
Meaning |
|---|---|
stable |
No significant changes — zero actionable rows |
improved |
Coverage increased, no new isolated records |
degraded |
New isolated/sink records or lost hub nodes |
restructured |
Large node churn but coverage is stable |
Rows are only emitted when something changed. A stable run produces zero rows.
| Row type | Severity | Suggested action |
|---|---|---|
⚠️ became isolated |
high | Link or archive the record |
⚠️ new sink node |
medium | Add outgoing links |
⚠️ hub degraded |
high | Check for removed links |
🗑 record tombstoned |
info | Hub notes auto-cleaned in apply mode |
✅ orphan resolved |
info | No action needed |
→ shape changed |
info | Review if expected |
mode=report— dry run, no writes, no snapshot updatemode=apply— writes repairs, updates snapshot, includes prune advisory
In apply mode, the tool:
- Runs a fresh traversal and writes a new snapshot
- Cleans dead links from hub notes for any tombstoned records
- Includes a
snapshot_prune_advisoryshowing what old snapshots could be pruned (pruning requires a separate explicitdevonthink-link-prune-snapshots(mode=apply)call)
{
"health_verdict": "stable",
"coverage_delta_pct": 0.0,
"actionable_rows": [],
"summary": {
"rows_by_severity": {"high": 0, "medium": 0, "info": 0},
"tombstoned_count": 0,
"resolved_orphan_count": 0,
"hub_notes_repaired": 0
}
}{
"health_verdict": "degraded",
"coverage_delta_pct": -4.2,
"actionable_rows": [
{
"row_type": "⚠️ became isolated",
"severity": "high",
"uuid": "4B3ED807-...",
"title": "Book Card Catalogue Template",
"suggestion": "consider linking or archiving",
"from_shape": "spoke",
"to_shape": "isolated"
}
]
}For a new group or database, run tools in this order:
# 1. Resolve a record to confirm connectivity
devonthink-link-resolve <uuid>
# 2. Audit the folder baseline
devonthink-link-audit-folder <folder-uuid>
# 3. Find orphans and weak nodes
devonthink-link-find-orphans <folder-uuid>
# 4. Get related suggestions for weak nodes
devonthink-link-suggest-related <record-uuid>
# 5. Build hub notes for well-connected groups
devonthink-link-build-hub <group-uuid> --title "My Hub"
# 6. Run first maintenance pass to capture baseline
devonthink-link-maintenance-pass <folder-uuid> --mode apply
# 7. On subsequent runs, maintenance-pass reports only deltas
devonthink-link-maintenance-pass <folder-uuid> --mode applyFor ongoing maintenance, run step 7 periodically. Use
devonthink-link-compare-snapshots for ad-hoc graph health checks
between maintenance passes.
Not exposed by design:
print settingsprintdownload image for prompt- chat/image engine usage commands (
display chat dialog,get chat capabilities/models/response)
Integration tests run against a live DEVONthink instance using a disposable fixture corpus:
python tests/test_scholar_corpus.py
# or
python -m pytest tests/ -vThe main test fixture (MCP Test - Scholar Corpus in Inbox) consists of 12 records across three
groups (Concordance Discovery, Archive Reconstruction, Chronological Assembly) and covers all
canonical tools.
The edge-case stress fixture is MCP Chaos Lab 20260424-080344
(3EFAAB4A-5BCD-4699-A472-1F66EF3C7882). It was created from live files to validate unusual
combinations: annotation notes on PDFs/groups/smart groups/annotation notes, duplicate-record
syntax, smart groups, RTF/RTFD caveats, labels/ratings on annotation notes, and summarize-annotation
missing-value behavior. Keep it as a named fixture unless intentionally regenerating live stress data.
Fixture UUIDs are exposed in tests/conftest.py. Test data is erasable unless explicitly kept as a
fixture. Tests require DEVONthink to be running and Automation permission granted to the Python host.
- macOS
- Python 3.11+
- DEVONthink installed and running
- macOS Automation permission granted to the host app/process
If a tool call fails with automation errors, check:
System Settings->Privacy & Security->Automation- allow your host app (Terminal, VS Code, Codex host, etc.) to control
DEVONthink
The recommended install uses pipx, which puts the
server in its own isolated environment and exposes a devonthink-mcp command
on your PATH — no manual venv to activate, and no absolute paths in your MCP
client config.
brew install pipx # if you don't already have it
pipx install . # run from the repo root
cp .env.example .env # optional: local overridesAfter install, point your MCP client at the command directly:
command: devonthink-mcpIf you prefer a manual venv (e.g. you don't want pipx, or you're hacking on the source):
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
cp .env.example .envThen point your MCP client at the venv's Python and the entry script — these
need to be absolute paths because most MCP clients don't honor PATH or cwd
the way a shell does:
command: /absolute/path/to/devonthink-mcp/.venv/bin/python
args: ["/absolute/path/to/devonthink-mcp/main.py"]Default (stdio):
python3 main.pyExplicit transports:
python3 main.py --transport=stdio
python3 main.py --transport=sse
MCP_TRANSPORT=web python3 main.pyPrecedence:
--transportMCP_TRANSPORT- default
stdio
Use the macOS connector manager:
python3 ./scripts/manage_connectors.pyNon-interactive install examples:
python3 ./scripts/manage_connectors.py install codex vscode claude --profile canonical
python3 ./scripts/manage_connectors.py install hermes openclaw --profile full
python3 ./scripts/manage_connectors.py status
python3 ./scripts/manage_connectors.py stop-serverCompatibility wrapper:
./scripts/install-connectors.sh --profile full codex vscode claudeBenchmark AppleScript/traversal performance baseline:
./scripts/benchmark_applescript.sh [group_uuid]This writes a benchmark artifact JSON into benchmarks/.
If no CLI argument is provided, the script uses DEVONTHINK_LINK_BENCHMARK_GROUP_UUID when set, otherwise it defaults to the Inbox group UUID.
Client guides:
docs/client-setup-vscode.mddocs/client-setup-codex.mddocs/client-setup-claude-code.mddocs/client-setup-hermes-agent.mddocs/client-setup-openclaw.md
python3 -m compileall app main.py
python3 main.py --helpapp/tools/devonthink_tools.py: specialized wrappers (search, get, create)app/tools/devonthink_link_tools.py: link-intelligence layerapp/tools/devonthink_dictionary_tools.py: broad generated command toolsapp/data/devonthink_command_specs.json: bundled command spec sourcetests/test_scholar_corpus.py: integration test suite (91 tests, live DEVONthink)open-issues.md: confirmed bugs, DX gaps, and performance findings with fix statuscatalog-runtime/registry/: runtime catalog metadataclients/: connector templatesscripts/manage_connectors.py: interactive connector manager and server stopperscripts/install-connectors.sh: compatibility wrapper around the connector managerscripts/benchmark_applescript.sh: AppleScript/traversal benchmark runnerbenchmarks/: saved benchmark artifacts (JSON)snapshots/: link-graph snapshot files written bytraverse-folderskills/devonthink-research-mcp/: reusable skill package
