okular-mcp
It gives an LLM session awareness of what the user is reading in Okular and provides PDF reading/annotation tools.
See every open Okular document (each window/tab), its page, page count and which entry is active.
Get the user's current mouse selection or clipboard text, attributed to the active document and page, with LaTeX source lines when SyncTeX data exists.
Jump Okular to a given page of a path (opening the file if needed) to show the user something.
Read numbered page text, the paragraph around a selection, links, and the document outline/table of contents.
List annotations in the PDF, and add highlights/underlines/strikeouts, margin notes or replies, and file attachments; read attachments back and delete annotations.
Requires saving in Okular first before writing, since unsaved Okular marks are lost when the file changes.
Integrates with KDE's Okular PDF viewer, reading the current document, page, mouse selection, and saved annotations, and writing highlights, notes, and file attachments back into the PDF.
Click on "Deploy 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., "@okular-mcp summarize the highlights in this PDF"
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.
okular-mcp
An MCP server that tells an LLM session what
you are reading in Okular: which documents are open
(in every window and tab) and on what page, the text you have selected with the
mouse (with its LaTeX source lines when the PDF has SyncTeX data), and (when you
ask) showing you a page.
A prompt hook for Claude Code adds the document, the page and your selection to
every prompt you type in a project that contains the PDF, and okular-mcp-open
lets LaTeX tools open a PDF or do a forward search in Okular.
This server does not read or write PDF content. It implements the viewer-state
contract in CONTRACT.md, and the PDF work is done by sibling
servers:
pdf-reader-writer-mcp: page text with line numbers, links, outline, annotations, marks and notes written back, andlocate(selection to line numbers). Every tool takes thepathandpagethis server reports.vscode-pdf-mcp: the same contract for the PDF viewer inside VS Code.
CONTRACT.md is shared: both viewer servers keep an identical copy, and its
clauses (C1 to C8) are pinned by tests named after them (test_c3_...). Change it
in both repositories together, never in one.
Tools
Tool | What it does |
| One entry per document open in Okular (each tab of each window), the active one first (C1) |
| The text selected with the mouse ( |
| Show |
An entry is
{"viewer": "okular", "window": "org.kde.okular-12345", "path": "/abs/paper.pdf",
"page": 3, "pages": 12, "active": true}window is the Okular process's D-Bus service name, followed by the tab's D-Bus
object for tabs other than the process's first (org.kde.okular-12345/okular2),
page is 1-based, pages the page count. The active entry is the tab on show
in the Okular window that has the keyboard focus, else in the first window that has
a document open (Okular processes in the order they joined the bus, each one's
windows in the order they were made). With nothing open (or Okular not running),
viewer_state and goto without a path fail with a message starting
no Okular window with a PDF open.
Tabs and windows
Okular gives every document view (a tab, or the document of a window without tabs)
its own D-Bus object, /okular, /okular2, /okular3, ... in the order they were
made, and every main window an object /okular/okular__Shell_<n> with its Qt
properties. The server reads all of them, so each tab is an entry; a process
started as okular a.pdf b.pdf with two windows gives two. Which tab a window shows
is not reported as such: the window's title names its document, so the server
matches the title against each tab's file name, its path, or the document's own
title (Okular's settings choose which of them the title shows), and isActiveWindow
says which window has the focus. When you type a prompt, the focus is in the
terminal, not in Okular, so the active entry is then the tab on show in the first
window. Two tabs with the same file name (two projects' main.pdf) look the same
in the title; the one opened last is taken. Setting Okular to put the path in the
title tells them apart (see Setup tips). The reads of all tabs run at
the same time, so many tabs cost about as much as one: the hook takes about 0.15 s
with nine tabs.
get_selection returns {"text", "source"}, plus viewer, path and page
for source="primary" when a document is open. text is "" when nothing is
selected. The primary selection is system-wide: it is whatever was last selected
in any window, so it is attributed to Okular's active document on trust, and
Okular does not say where in the document it is. So the server looks for the
selected words in the text of the active document's current page, then the next
page, then the previous one (with poppler's pdftotext, the same text layer
Okular selects from), and page is the first page that has them. Okular also
selects over a page break, giving the end of one page's text (its page number
included), then the start of the next one's: words found that way across these
pages give the page the selection starts on. Otherwise page is the current
page. When the words occur exactly once there and the PDF has a
.synctex.gz (or .synctex) next to it, tex gives their LaTeX source lines,
from synctex edit at the first and last word and one word of each printed line
between (except the printed lines right before and after a page break, which hold a
page number or a running head that SyncTeX places where the page was shipped out):
{"text": "...", "source": "primary", "viewer": "okular", "path": "/abs/paper.pdf",
"page": 4, "tex": [{"file": "/abs/sec/intro.tex", "lines": [12, 15]}]}One entry per source file, in document order. A selection may start or end inside
a word, and a word TeX hyphenated at a line end matches with or without its
hyphen. pdfTeX tags the space after a source line's first word with the line
before, so synctex edit alone gives that word the previous line; the server
checks with synctex view whether the next line, if it has text, starts right
after the word and then takes that line, and it maps a paragraph's last word, which SyncTeX puts on
the blank line after it, to the line it is on.
goto brings a tab that is not on show to the front first, by asking Okular to
open the file the way its own launcher does (/okularshell, which switches to the
tab that has it open; Okular's "Switch to tab if already open" setting, on by
default). A file that no tab has opens next to the active entry: in a new tab when
Okular opens new files in tabs (Settings, General: "Open new files in tabs"),
otherwise in place of the active entry's document, as before. It fails with
no such file: <path> for a path that does not exist, cannot open <path>
if Okular fails to load it, and page N outside 1-M before moving if the page is out of
range. For a file it cannot load, Okular shows a modal error dialog and answers the
D-Bus call only once the user closes it, so goto (and okular-mcp-open) take a
few seconds to report cannot open; the dialog stays in Okular until it is closed.
Errors reach the model as their message (as an MCP tool error), not as a generic "Error executing tool".
Related MCP server: omapdf-mcp
Prompt hook (Claude Code)
okular-mcp-hook is a UserPromptSubmit hook. Add it to a Claude Code
settings.json (user or project):
{
"hooks": {
"UserPromptSubmit": [
{"hooks": [{"type": "command", "command": "okular-mcp-hook", "timeout": 5}]}
]
}
}When the active Okular document lies inside the session's working directory (after resolving symlinks; a PDF in a sibling directory does not count), it adds
The user is reading /abs/project/paper.pdf in Okular, page 3 of 12.
Selected text (page 4):
the words you selected
Source: sec/intro.tex:12-15to the prompt's context. Otherwise it adds nothing. The selection lines come only
when the selected words are found on the document's current page or a page next
to it, or over a page break between them (as for get_selection): the system
primary selection cannot be tied to Okular otherwise, and is often the text just
selected in the terminal the prompt is typed in. The text is cut at 2000
characters ( […] marks the cut). The
Source: lines follow when the PDF has SyncTeX data (:12 alone for a single
line), with the file relative to the working directory when it lies inside it.
The hook always exits 0, also on bad input or when Okular or the bus does not
answer (each qdbus call may take 0.3 s, and a 0.8 s watchdog ends it silently), so
it never blocks a prompt. The selection part has its own deadline, 0.6 s after the
hook starts: if reading the selection, pdftotext or synctex is slow or
missing, the hook leaves the selection out and still reports the page. Measured
on a 54-page paper with SyncTeX data, the whole hook takes about 0.26 s with a
selection found and mapped (about 0.14 s of it for the selection part) and about
0.13 s without one.
LaTeX: okular-mcp-open
okular-mcp-open <pdf> # open the PDF
okular-mcp-open --tex <file> --line <n> [--column <n>] <pdf> # forward searchThe same command line as vscode-pdf-mcp-open, so a LaTeX tool can be pointed
at either viewer. A tab that has the PDF open is brought to the front and otherwise
left as it is. Otherwise the PDF opens as okular <pdf> would open it: as a new tab
of the running Okular when Okular opens new files in tabs (or into an empty Okular
window), else in a new Okular window (okular started without arguments, which
never joins a running Okular). The forward search asks synctex view
for the page first, then hands Okular the URL file://<pdf>#src:<line> <file>
(the --unique forward search), in the tab that has the PDF open (brought to the
front), else in a new tab or window as above. Okular has no column in a forward
search, so --column is accepted and ignored (a negative one means none, as LaTeX
Workshop may pass). It prints
<pdf>: page <n> of <pages> (what Okular shows once it has settled), exits 1
with the reason on stderr when it cannot (no such file: <pdf>, cannot open <pdf>, no SyncTeX data for <file>:<line> in <pdf>, Okular not starting) and
2 on bad arguments.
A forward search opens the document anew, like okular --unique, and Okular would
first ask to save unsaved annotations. So when the tab has unsaved changes (its
window says so: KDE's "modified" mark, windowModified on D-Bus), okular-mcp-open
goes to the page of that line instead, without the precise place, keeps your marks,
and says so on stderr; save in Okular for the exact forward search. Okular reports
this only for the tab on show, which a forward search brings to the front first.
LaTeX Workshop, in VS Code's settings.json:
"latex-workshop.view.pdf.viewer": "external",
"latex-workshop.view.pdf.external.viewer.command": "okular-mcp-open",
"latex-workshop.view.pdf.external.viewer.args": ["%PDF%"],
"latex-workshop.view.pdf.external.synctex.command": "okular-mcp-open",
"latex-workshop.view.pdf.external.synctex.args": ["--tex", "%TEX%", "--line", "%LINE%", "%PDF%"]Then View LaTeX PDF opens Okular and LaTeX Workshop's forward search
(Ctrl+Alt+J) lands in it. Use the absolute path of okular-mcp-open as the
command if VS Code does not have it on its PATH. Inverse search is Okular's
own: Shift+click in the page opens the source in the editor set under Settings →
Configure Okular → Editor (for VS Code, see Setup tips).
Setup tips
Okular's own settings, none of them needed:
Inverse search (Shift+click in the page opens the source line) is Okular's: Settings → Configure Okular → Editor. For VS Code, pick Custom Text Editor with the command
code -g %f:%l(inokularpartrc:ExternalEditor=CustomandExternalEditorCommand=code -g %f:%lunder[General]).Tabs: Settings → Configure Okular → General → "Open new files in tabs" (
ShellOpenFileInTabs=true) makesokular <pdf>,gotoandokular-mcp-openopen a PDF as a tab of the running Okular.Two tabs with the same file name: "Display document title in titlebar if available" off and "When not displaying document title": "Display full file path" (
DisplayDocumentTitle=false,DisplayDocumentNameOrPath=Path) put the path in the window title, so the server can tellone/main.pdffromtwo/main.pdf.Your marks' author: Settings → Configure Okular → Annotations → Author (
IdentityAuthorunder[Identity]; default: your full name). Set it to the same name as the VS Code viewer'svscodePdfMcp.authorif marks made in both viewers should carry one author.
No reload tool, and saving
Nothing here writes the PDF, and there is no reload tool (C4). Okular's
"Reload document on file change" (WatchFile, on by default) re-renders the
document by itself after another tool saves it: an incremental save by
pdf-reader-writer-mcp showed up in about 1.5 s. But Okular keeps your own
unsaved annotations in memory and loses them when the file changes, so save
in Okular (Ctrl+S) before another tool writes to the file; the server's
instructions tell the model to ask you first.
Requirements
Linux with KDE's Okular on the session D-Bus, plus:
qdbus(Qt tools; installed with Okular on most distributions)wl-clipboardon Wayland, orxclip/xselon X11, forget_selectionoptional:
pdftotext(poppler;popplerorpoppler-utils), to find the page of a selection; without it,pageis the current page and the hook leaves the selection outoptional:
synctex(TeX Live), for thetexsource lines, the hook'sSource:lines andokular-mcp-open --texPython 3.10+;
mcpis the only dependency
Install
With uv:
uv tool install git+https://github.com/david-wang-0/okular-mcpThis installs okular-mcp (the server), okular-mcp-hook and
okular-mcp-open. Register the server with your MCP client. Claude Code, user scope:
claude mcp add okular -s user -- okular-mcpCodex CLI:
codex mcp add okular -- okular-mcpAny other client: run okular-mcp as a stdio server.
Gotchas
Tabs. Okular's
/okularobject answers for the first-opened tab, not the one on show (KDE bug 502482), and no D-Bus call names the tab on show. The server therefore reads every tab's own object and goes by the window title (see Tabs and windows), which needs a KDE Frameworks Okular that exports its main windows on D-Bus (checked with Okular 26.08). Where it does not, or no title matches a tab, the first tab is the active entry, as before, andgotodoes not bring a tab to the front.Selection text is Okular's text layer, returned verbatim: hyphenation and column order can be messy. A selection whose words the text of the current page and the pages next to it does not have in that order (it reaches further, or Okular split a formula differently from
pdftotext) is not found:pageis then the current page, withouttex, and the hook leaves it out.Minimal client environments. Some MCP clients (Codex, the Python SDK's client) start servers with only
HOME,PATH,SHELLandUSER. Before anyqdbus,wl-paste,xcliporxselcall, the server (and the hook) recoverDBUS_SESSION_BUS_ADDRESSandWAYLAND_DISPLAYfromXDG_RUNTIME_DIR(or/run/user/<uid>), so no per-clientenvblock is needed on a systemd desktop (C5).
Development
uv venv && uv pip install -e '.[test]'
.venv/bin/pytest # the default suite: fakes only, about 15 s
.venv/bin/pytest -m integration # real Okular, headless on a private bus, about 30 sThe default suite never starts Okular or touches the real bus or selection. Unit
tests run against fakes (a fake session bus behind viewer._call, a fake
subprocess.run and PATH, fake pdftotext/synctex output); one fake Okular
(fakes.okular: tabs, windows, their titles and focus, /okularshell) answers for
both the in-process bus and the fake qdbus executable; the tests of a
CONTRACT.md clause are named after it (test_c3_...). The process-level tests run the real entry points with the fake
qdbus, wl-paste, pdftotext and synctex executables of tests/fakes.py on
PATH: tests/test_mcp.py drives okular-mcp over stdio with the mcp
package's client (tool list, instructions, results, error messages),
tests/test_hook_process.py runs okular-mcp-hook as Claude Code does (C6-C8,
timing included), and tests/test_packaging.py checks the console scripts. With
pdflatex, synctex and pdftotext installed, tests/test_sourcelines.py also
compiles small LaTeX documents and checks the selections' pages and source lines
with the real tools.
The integration suite (tests/integration/, marker integration, deselected by
default) needs okular, dbus-run-session, qdbus (or qdbus6), pdflatex,
synctex and pdftotext, and skips with the list of what is missing; its C4 test
also needs pdf-reader-writer-mcp on PATH. It is headless and never touches your
Okular or screen: dbus-run-session provides a private bus, and every Okular and
every process under test runs on it with QT_QPA_PLATFORM=offscreen, without
DISPLAY/WAYLAND_DISPLAY, with temporary XDG config, data, cache, state and
runtime directories, and (for the code under test) a fake wl-paste as the
selection. Okular's output goes to files in pytest's temporary directory, and
everything on the private bus (Okulars, and daemons it started such as ksecretd)
is killed at the end. It covers C1 (not running, no document, several windows), C3
goto (moving, out of range, no such file, a broken PDF, a closed file, a symlink,
a path with spaces and #), okular-mcp-open (new window, reuse, forward search
into an \input file, no SyncTeX data, exit codes), the hook with a selection on
and off the page and over a page break, and C4: a mark written by
pdf-reader-writer-mcp, after which Okular reloads the file and stays on the
document and page. With Okular set to open files in tabs (in the suite's own config
directory), it checks that every tab is an entry and the one on show active, goto
into a background tab and into a new one, two windows of one process,
okular-mcp-open opening a tab and forward-searching into a background one, and
the hook naming the tab on show.
Coverage, including the processes the tests start ([tool.coverage.run] in
pyproject.toml):
uv pip install coverage
.venv/bin/coverage run -m pytest && .venv/bin/coverage combine && .venv/bin/coverage report -mokular_mcp/viewer.py is the only Okular-specific module (windows, views,
entries, active, goto, and open_in_tab, reveal, new_window, open_url,
wait_loaded for okular-mcp-open), selection.py reads the system selection,
sourcelines.py
finds it on the page and maps it to source lines, hook.py is the prompt hook,
open.py is okular-mcp-open.
License
MIT
Available Tools
13 toolsadd_notePDF: margin note or replyA
Put a comment icon in the margin, or reply to an annotation.
Comment icon in the margin: level with near_text, or as a threaded reply to the annotation reply_to (an xref from annotations, e.g. to answer the user's own note), else at the top of the page. color as in mark_text. Same save-Okular-first caveat as mark_text.
Inputs: note (required), page, path, color, +2 more. Returns: object
annotationsPDF: highlights and notesBRead-only
All highlights, notes and attachments saved in the PDF.
All annotations in the PDF (default: the open document): page, type, author, highlighted text, note. Only annotations saved to the file are visible.
Inputs: page, path. Returns: result
attach_filePDF: attach a text fileB
Embed a text file (Markdown, Mermaid) as a paperclip annotation.
Embed a text file (e.g. notes.md, diagram.mmd) as a paperclip annotation in the margin, level with near_text or at the top of the page; or replace the file of an existing attachment replace_xref. Okular saves it via the icon's context menu. Same save-Okular-first caveat as mark_text.
Inputs: name (required), content (required), note, page, +4 more. Returns: object
contextPDF: paragraph around the selectionARead-only
The paragraph around the user's mouse selection.
The paragraph around the user's current mouse selection (or text) on the page shown, with neighbours paragraphs before and after and the line numbers, so you can see what they are pointing at without reading the whole page.
Inputs: page, path, text, neighbours. Returns: object
get_selectionOkular: mouse selectionARead-only
Text the user has selected with the mouse, with page and lines.
Text currently selected with the mouse ('primary') or on the clipboard ('clipboard'), with the document path, page and line numbers when it is found on the page Okular shows (line numbers match page_text and context).
Inputs: source. Returns: object
gotoOkular: show a page to the userA
Jump Okular to a page for the user.
Jump Okular to page of path (default: current document; opens it if needed).
Inputs: page (required), path. Returns: object
linksPDF: links on a pageARead-only
Links on a page with their targets, to follow citations yourself.
Links on a page (default: the page shown): anchor text and line, then the target page and line for internal links (citations, sections, figures) or the URL. Follow one with page_text(page=..., lines="<target_line>-...").
Inputs: page, path. Returns: result
mark_textPDF: highlight or underline textA
Highlight, underline, squiggle or strike out a passage, with a note.
Mark every occurrence of text on a page (default: the one shown) as highlight, underline, squiggly or strikeout, saved into the PDF under a distinct author; quote enough words to be unique. A note sits on the mark, or with note_in_margin on a comment icon in the margin replying to it (least intrusive). color: blue, yellow, green, orange, pink, purple, red, cyan, grey, #rrggbb or r,g,b in 0-1 (default light blue; red for strikeout). The user must save Okular first or their unsaved marks are lost.
Inputs: text (required), note, page, path, +3 more. Returns: object
outlinePDF: table of contentsCRead-only
The document's table of contents.
The document's bookmark tree (sections with page numbers), a map of the paper.
Inputs: path. Returns: result
page_textPDF: page text, numbered linesBRead-only
Numbered text lines of a page range.
Numbered text lines of a page range (default: the page shown in Okular); lines such as "12-30" narrows to a window. Line numbers match context.
Inputs: to, page, path, lines. Returns: result
read_attachmentPDF: read an attached fileBRead-only
Read an attached file back as text.
Contents of a file-attachment annotation (an xref from annotations with a file field), decoded as text.
Inputs: xref (required), path. Returns: object
remove_annotationPDF: delete an annotationBDestructive
Delete an annotation by xref.
Delete an annotation by the xref reported by annotations (default: the open document) and save. Same save-in-Okular-first caveat as mark_text.
Inputs: xref (required), path. Returns: object
viewer_stateOkular: open documents and pagesARead-only
Which PDF and page Okular is showing.
Open Okular windows: document path, current page (1-based) and page count.
Inputs: no named parameters. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 13 tools
Each tool targets a distinct resource or action: viewer state, selection, page text, context around selection, links, outline, annotations, annotation creation/reading/removal, and navigation. Slight conceptual overlap exists between context/page_text and mark_text/add_note, but the descriptions clearly delimit them.
All names use snake_case, but the pattern mixes noun phrases (viewer_state, annotations, outline) with verb_noun forms (get_selection, mark_text, remove_annotation, goto). It remains readable, but there is no single predictable convention across the set.
13 tools is well within the ideal range and matches a PDF reading/annotation server's scope. Each tool represents a distinct operation, and none feels redundant or excessively granular.
The surface covers reading, navigation, annotation creation/reading/deletion, and attachments. Minor gaps remain: no explicit save or full-text search operation, and annotations cannot be modified in place, but the core lifecycle is covered.
Maintenance
Related MCP Connectors
Parse, extract, split, and ask over digital PDFs (text layer, no OCR) from Cursor and Claude.
Search arXiv/Semantic Scholar/OpenAlex + medical evidence (PubMed/Europe PMC) + LaTeX/PDF tools.
Merge, split, extract, rotate, reorder and stamp PDF pages from your AI chat, all offline.
Merge, split, extract, rotate, reorder and stamp PDF pages from your AI chat, all offline.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants to search and query PDF documents through a local RAG system with vector embeddings. Provides semantic document search capabilities while keeping all data stored locally without external dependencies.-
- AlicenseNot gradedqualityBmaintenanceGives AI agents tools to read PDFs, list form fields, apply operations, highlight, add notes, fill fields, place signatures, list signatures, and flatten PDFs.1AGPL 3.0
- AlicenseAqualityBmaintenanceEnables a conversational workflow to read local PDFs, propose exact word-level passages, apply colored highlights, and inspect saved annotated copies, plus recolor or remove existing highlights. All PDF and image I/O stays confined to a single workspace directory, with fingerprint verification and atomic saves so originals are never overwritten.10MIT
- AlicenseAqualityCmaintenanceEnables clients to inspect local PDFs by searching text, rendering pages, and cropping selected regions at high resolution directly from the source document. It runs locally over stdio without requiring models, embeddings, GPUs, or API keys, and does not modify source PDFs or send network requests.6MIT