github quazardous/nelson-mcp v0.14.0

latest releases: v0.14.5, v0.14.4, v0.14.3...
one month ago

Writer content is now exchanged as Markdown by default. An agent that
parses the HTML of text_get_range must pass format: "html", or set
Options > Nelson > Document format back to HTML. That change, the access token,
and the new tools (calc_query, calc_columns, style_set, doc_insert_toc,
writer.change_author) make this a minor version. It also fixes the
cold-start deadlock of GitHub #35/#37: LibreOffice could hang for good when
started with a document.

Security

  • Nelson could be exposed to the network or the internet with no
    authentication at all
    — there was no access control anywhere in the
    server. Binding to 0.0.0.0, or following the Tailscale guide, which runs
    tailscale funnel on and publishes the server on the public internet, gave
    whoever found the URL read and write access to every open document and
    doc_open on any file the user can read — and the guide said none of that.
    There is now an access token (http.auth_token, Options > Nelson MCP >
    HTTP): once set, every request needs it, as Authorization: Bearer <token>
    or ?token=<token> for clients that can only be given a URL, compared in
    constant time, applied without a restart. Nelson refuses to bind to
    anything but localhost, and refuses to start a tunnel, until a token is
    set
    — a tunnel's traffic arrives from localhost, so only a token can tell
    it apart from a local client. The built-in launchers write the token into
    the Claude Code, Gemini CLI and OpenCode configs they generate. Local use
    with no token set is unchanged. The Tailscale guide now opens with what
    exposing Nelson gives away, and the README has a Security section.
    make smoke checks the token live: 401 without it, 200 with either form,
    applied without a restart, and the server reopened to local clients once it
    is cleared

Changed

  • Writer content is exchanged as Markdown by default (#2650).
    text_get_range returned HTML unless told otherwise. Measured on the bench
    documents, Markdown is about a third smaller: the RGPD .docx reads in one
    call as Markdown (362 000 characters), while its HTML (626 000) exceeds the
    500 000-character read limit, so rewriting it from HTML lost 40% of the
    text. Rewriting from Markdown loses what Markdown cannot carry (text
    colours, underline, footnotes, merged cells); text_apply_range now says
    so and recommends format: "html" or a targeted rewrite for formatted
    documents. HTML stays available per call and through Options > Nelson >
    Document format; a setting already saved as HTML is kept

Fixed

  • Comments added by an agent showed "(no date)" — comment_add, the
    reply of comment_resolve and the workflow status comment are now dated
    when they are created

  • LibreOffice sometimes never came up when started with a document
    (GitHub #35, #37; #2625). Two threads waited on each other: the main
    thread, opening the document, held LibreOffice's global lock and waited for
    the menu configuration, which a Nelson background thread held while it
    waited for the global lock to set menu icons. Reproduced on 5 of 20 cold
    starts with /health polled during launch. Nelson now does all its UNO
    work at startup on the main thread (menu icons, module start, cache
    prebuild removed), the sidebar panel and menu commands wait for startup
    instead of forcing it, and /health and tools/list answer from a
    document-event watcher instead of querying LibreOffice from the HTTP
    thread. 50 cold starts out of 50 came up. make coldstart-wbox repeats
    the scenario and saves a gdb backtrace of any hang; make smoke fails
    when UNO is used off the main thread

  • A crash in a tool was reported as retryable (#2637) — only
    disconnections, timeouts and a disposed document now are. Parameters that
    belong to another action of a merged tool are refused with the action and
    the parameters it accepts, instead of being silently ignored

  • text_search did not look inside tables, and comment_add could not
    anchor there
    (#2637). text_search searches table cells by default
    (include_tables), reporting the table and cell of each match;
    comment_add takes occurrence, anchors in cells, frames and footnotes,
    says where it anchored, and refuses text it cannot find as not retryable

  • track_changes_list returned every change in one block (#2636) — 120
    changes on a reviewed contract. It now returns a summary by author and
    type, filters by author and type, pages with offset/limit, and gives
    each change its text and paragraph index

  • Writing one formula into a range put the same formula in every cell
    (#2633) — =C2+D2 written into E2:E9 referenced row 2 eight times. The
    formula is now filled like Calc's fill handle: relative references shift,
    $ references stay

  • Charts had no legend, and chart_create ignored has_legend (#2641);
    a chart with several series, or a pie, now gets one by default

  • calc_read_range read cell by cell (#2630) — four UNO calls per cell,
    5.4 s on the main thread for A1:CA2000. It now reads a range in two calls
    (getDataArray, getFormulaArray), with the same output. format="rows"
    returns values as plain rows and formulas listed apart, about ten times
    smaller than the per-cell objects, and allows 100 000 cells per call

  • FILTER, SORT, UNIQUE and other array formulas returned one value with
    status: ok
    (#2631). LibreOffice does not spill array results: written
    into a single cell, =SORT(FILTER(...)) showed its first value and the
    rest of the table silently disappeared. calc_write_formula (and a lone
    formula in calc_write_range) now recognises a formula whose outermost
    function returns an array, measures its result, and enters it as an array
    formula over exactly those cells — refusing, without writing anything, if
    any of them is occupied. An error result (a FILTER that matches nothing) is
    reported as an error, a result over 100 000 cells is refused, and the
    answer gives the range, its size and the first rows. =SUM(FILTER(...))
    stays an ordinary formula; array forces either way. On an explicit range
    the formula fills that range and warns when the result does not fit.
    calc_read_range reports array_range for cells of an array formula

  • Closing a document could crash LibreOffice (#2651). Nelson wraps every
    change in an undo context, so one Ctrl+Z undoes a whole agent action — and
    doc_close counted as a change: the document was destroyed while its undo
    manager still had that context open. After a rewrite involving tables,
    notes or comments, LibreOffice aborted, taking every open document with
    it. It was in every version since 0.7.1. doc_close now runs outside any
    undo context and leaves and clears the undo stack before closing; a tool
    declares opens_undo_context = False when it destroys its document.
    make smoke rewrites a document with a table twice and closes it, three
    times over — which crashed LibreOffice before the fix

  • Markdown given to text_apply_range landed as literal # and **
    (#2635). The tool says it takes Markdown or HTML, but the import filter came
    from a global format setting, HTML in practice, so a Markdown report went
    through the HTML filter and came out as plain text with its syntax showing —
    with status: ok. The filter now follows the content: HTML when it has
    HTML tags, Markdown when it has Markdown syntax (headings, lists, tables,
    emphasis, code, links), using LibreOffice's own Markdown filter; format
    forces one or the other

  • The first full-text search on a long document took seconds — building
    the index stemmed every word through the pure-Python Snowball stemmer, 90%
    of the time. Moby Dick is 213 000 words but 17 000 distinct ones: each is
    now stemmed once, same index: in LibreOffice the build logs 0.6 s instead
    of several seconds. A first call right after opening a long document can
    still wait a few seconds while LibreOffice lays the document out on the
    thread tools run on; the tool description now says so

  • Reads had no size limit, and the two settings meant to set one were
    never read
    (#39). text_get_range returned Moby Dick as 1.36 MB of HTML
    in one result, and calc_read_range 13 MB for A1:CA2000, while
    writer.max_content_chars and calc.max_rows_display sat in Options with
    no effect. text_get_range now stops at max_chars, or the Max Content
    Size setting (50 000 characters), never above 500 000, cutting on a clean
    boundary rather than inside a tag; calc_read_range stops at the Max Rows
    Display setting (1 000 rows) and 20 000 cells per call, across all the
    ranges asked for. A capped result is never silent: truncated: true, the
    totals, a hint on reading the rest, and for Calc next_range, the exact
    range to read next. Both settings take effect without a restart. make smoke reads a 65 000-character document and 1 500 rows, follows
    next_range to the last row, and changes both settings live

  • Listing documents marked them modified and wrote an id into them
    (#2627). doc_id was stored in each document as a NelsonDocId custom
    property, written the first time Nelson touched it — doc_list_open, the
    _resolved of any call, even resolving id: — so documents the agent had
    only listed could ask "Save changes?" on close, and a saved .odt/.docx
    carried the id, visible in File > Properties > Custom Properties to
    whoever received it. The id now lives in memory: stable while the document
    is open (Save and Save As included), distinct for every open document,
    forgotten when it closes, and nothing is written into the document. A
    doc_id therefore no longer survives a LibreOffice restart — use path:
    to find a document again across sessions. doc_save_as keeps the
    document's doc_id instead of minting a new one. Files saved by earlier
    versions may still hold a NelsonDocId property; Nelson ignores it and
    does not remove it (that would modify the document) — delete it from
    File > Properties > Custom Properties if you want it gone. make smoke
    opens a file outside Nelson, lists and addresses it, and checks over UNO
    that it is neither modified nor carrying the property

  • make wbox-down killed itself — its pkill -f pattern matched the
    shell running pkill, whose command line contains the pattern, so make
    stopped there and make wbox-deploy never deployed. The pattern no longer
    matches its own text

  • A read could return another document's paragraphs, and edits made in
    the GUI were invisible to navigation and search
    — two faults in the
    document caches. The paragraph cache was keyed by the id() of a pyuno
    proxy, but every lookup of a document returns a new proxy: the cache
    almost never hit, never emptied (holding on to closed documents'
    paragraphs), and when Python reused an id a call read a different or
    closed document — a search after doc_open counting 0, or
    "SwXParagraph: disposed or invalid" in the middle of an edit. And the
    document service was never connected to the event bus, so the
    heading-tree, bookmark, proximity and full-text-index caches, which are
    keyed by URL and do persist, were never told of any change: after an edit
    nav_tree and text_search_fulltext kept answering about the old text.
    Caches are now matched by UNO identity, each document has a modify
    listener, so an edit from any source — a tool or the user — invalidates
    them, and a closed document's entry is dropped. A tool still sees stable
    paragraph numbering while it runs (a batch across all its steps) and
    invalidates once when it returns; Nelson's hidden heading bookmarks and
    saves invalidate nothing. make smoke churns 30 documents, then edits a
    document over UNO from outside Nelson and checks nav_tree and the index
    see it

  • nav_heading_content could return the wrong section — it walked the
    outline to the heading, then looked the heading up again by its title, so
    with two headings of the same title and level (a "Notes" section in every
    chapter) it returned the first one's text, without an error. It now reads
    from the heading's own paragraph index. nav_outline gives every heading
    its path ("2.4", what nav_heading_content takes) and para_index,
    so an agent no longer counts positions by hand; nav_heading_content also
    accepts an exact title when only one heading has it, lists the matching
    paths when several do, and every lookup error states the expected format

  • Responses named two documents at once — _resolved was read after the
    tool ran, from the document that was active before the call: doc_open
    and doc_create returned the new document's doc_id next to a _resolved
    naming the previous one, and doc_close a _resolved with no id, since
    the document it described was gone. _resolved is now read before the
    call, and only for tools that work on a document

  • doc_close reported success whether or not it closed anything (#36,
    fault 2). It returned "Document closed." whenever close() did not raise,
    and a close LibreOffice declined or ignored looked exactly like a real one.
    It now checks the document is really gone, returns document_not_closed
    (with the reason when LibreOffice vetoed) otherwise, and says which
    document it closed (closed: title and URL), so a caller can tell a close
    of the wrong document from the right one. make smoke closes a document
    that is not the active one and checks which one went. Faults 1 and 3 of
    #36 are not reproduced yet

  • Calc charts landed on the sheet their data came from (#31). A chart over
    'Data Sheet'.A1:B5 was added to Data Sheet, sheet_name was ignored by
    calc_chart create, and position was measured on the active sheet while
    the chart went on another. Charts now go where Insert > Chart puts them —
    on sheet_name if given, else the sheet named in position, else the
    active sheet — with the data read from wherever data_range points. The
    response names the chart and both sheets

  • A second chart on another sheet failed with an empty error (#32). New
    charts were named after the count of charts on their own sheet, but names
    must be unique across the document, so Chart_0 came back on the second
    sheet and addNewByName refused it with an exception that has no text.
    Names are now unique across the workbook, and chart errors always carry a
    message

  • Calc writes accepted a sheet prefix that contradicted sheet_name
    (#33). The refusal introduced in 0.12.1 lived in the read path only;
    calc_write_range and calc_comment picked the prefix silently. Both now
    refuse the contradiction, and an unknown sheet lists the sheets that exist
    on writes as it already did on reads. Sheet names compare case-insensitively,
    as Calc does. A single-cell read no longer echoes the address with an
    upper-cased sheet prefix ('DATA SHEET'.B2): it returns B2, like a range

  • Reading a Writer document's structure changed the document, and could
    save it behind the user's back
    — nav_tree, nav_heading_children,
    nav_heading and nav_surroundings give every heading a hidden
    _mcp_<hex> bookmark so agents can address it by a stable name. Adding
    them marked the document modified, left undo steps, and — when the
    document had a file — called store() right away, so a pure read saved
    whatever the user had typed, and the bookmarks ended up in .odt/.docx
    files shared with others. bookmark_cleanup saved silently too. Neither
    saves any more, and creating the bookmarks no longer touches the modified
    flag or the undo stack. A new option decides what happens to them,
    writer.nav.heading_bookmarks (Options > Nelson MCP > Writer navigation):
    strip_on_save (default) keeps them while the document is open and
    leaves them out of every save — removed just before the file is written and
    put back under the same names right after, so agents' names stay valid;
    keep saves them with the document, as before; off never creates
    them, and headings are addressed by paragraph index. make smoke saves a
    document under each value and counts the bookmarks in content.xml

  • The call right after doc_open or doc_create could miss the new
    document
    (#34) — both returned as soon as LibreOffice had loaded it, but
    its window only became the active one a few tens of milliseconds later. The
    next call, which resolves the active document, then failed with
    no_document (retryable: false), or silently worked on whatever document
    was active before. It only happened with the real GUI and when the Start
    Center was the current component, which is why headless tests never saw it.
    Both tools now activate the new document's frame before returning — the
    synchronous step the _document path always took, and the reason naming the
    document explicitly never raced — and say so in a warning if it still is
    not active. When documents are open but none is active, the error is now
    no_active_document, retryable, with a hint to pass _document.
    make smoke-wbox reproduced the race on every run before the fix and passes
    after it; make smoke gains a check that the first opened document is the
    one the next call targets

  • A LibreOffice restart left every MCP client silently dead (#38) — a
    session id from before the restart was answered with 409, which no client
    acts on, so the client kept listing the server as connected and failed on
    every call until someone reconnected it by hand. It is now 404, the signal
    the Streamable HTTP spec requires a client to answer with a fresh
    initialize on the same URL — which Nelson already accepted. Conformant
    clients now recover by themselves. It restores the connection, not the work:
    documents that were open when LibreOffice went down are still gone

  • DELETE /mcp claimed to end a session it did not end (#38) — it answered
    200 and terminated nothing. Nelson has one session id for the whole process,
    shared by every client, so ending it on one client's request would cut all
    the others off. It now answers 405, the spec's answer for a server that
    does not let clients terminate sessions

Added

  • writer.change_author: sign the agent's tracked changes with a name of
    its own
    (#2636). Writer signs a tracked change with the user's name
    (Tools > Options > User Data), so an agent's edits could not be told
    from the user's, or read "Unknown Author" on a profile without a name.
    Set this option, e.g. to "AI agent (Nelson)", and Nelson puts that name
    in the LibreOffice profile for the length of each editing call, then
    puts the user's name back. If LibreOffice dies mid-call, the name is
    restored at the next start. change_set now says who will sign, and
    warns when that is "Unknown Author"

  • ISO dates are written as dates, and calc_columns sizes columns and
    freezes panes
    (#2632). calc_write_range and calc_write_formula stored
    "2026-10-01" as text, so a planning column neither sorted nor formatted as
    dates. ISO dates and date-times (2026-10-01, 2026-10-01 14:30) now
    become real date values, counted from the document's own NullDate, and get
    a YYYY-MM-DD format only if the cell still has the standard one; a leading
    apostrophe keeps one as text. Nothing else changes: "1-2" stays text. The
    new calc_columns sets a width in mm, auto-fits, hides or shows columns,
    and freezes or unfreezes panes at a cell, answering with the widths before
    and after

  • calc_query — answer a question about a sheet without reading it all
    (#2630). Filters (=, !=, <…, contains, in, empty,
    not_empty), column selection, sorting, group_by with sum, avg,
    min, max and count, on columns named by header or letter; numbers
    stored as text compare as numbers. It reads only the columns the query
    uses, in bulk, and never changes the sheet. "Top 10 emitters of 2022" on a
    50 000-row sheet is one call instead of megabytes of cells or sorting the
    user's table in place. calc_sheet_overview now also gives each column's
    header, type and filled count, and five sample rows

  • Page numbers, tables of contents and page breaks for reports (#2634).
    header_footer_set turns {page}, {pages}, {date} and {title} into
    real fields, so Page {page} of {pages} numbers pages instead of printing
    the braces ({{ for a literal brace); doc_insert_toc inserts a table of
    contents built from the headings, at the start, after the first heading or
    before a paragraph; text_insert_break starts a paragraph on a new page or
    column. make smoke saves such a report as .docx and finds the PAGE and
    NUMPAGES fields, the TOC field and the page break in the XML

  • Choose Markdown or HTML for reading Writer content (#2650). The format
    setting existed in the code but was never declared, so reads were always
    HTML. It is now Document format for agents in Options (HTML by default
    for now), and text_get_range takes format per call and says which
    format it returned. Markdown costs about a third less than HTML on a
    formatted document

  • style_set — change a style (#2647). "Change the font of the body
    text" had no answer: style_list and style_info only read. style_set
    changes a paragraph, character or page style through readable properties —
    font_name, font_size (pt), bold, italic, color, parent_style;
    space_before, space_after (mm), line_spacing (%), align for
    paragraphs; margins (mm) and orientation for pages — rather than raw UNO
    names and units. It answers with each property's value before and after,
    refuses an unknown property or a bad value with what is allowed, suggests
    close names for a misspelt style, and warns when a font is not installed
    (LibreOffice accepts any name and shows a substitute). One undo step.
    make smoke restyles the body text and checks styles.xml after saving

  • text_search and text_read report each paragraph's style, and
    text_search filters on it
    (style, exclude_style). A table of
    contents repeats every chapter title in body text; without the style an
    agent searching for "CHAPTER" could not tell an entry from the heading, and
    restyling the results restyled the table of contents too. A style filter
    searches the body with the direct backend

  • make smoke-wbox and the wbox-* targets — run LibreOffice's real GUI
    inside a nested wbox compositor from make, offscreen by default and visible
    with WBOX_VISIBLE=1. Headless LibreOffice has no event loop and no Start
    Center, which hides a class of bugs; the smoke suite run this way already
    reproduces the document-activation race of #34 on every run

Don't miss a new nelson-mcp release

NewReleases is sending notifications on new releases.