cd /news/ai-agents/show-hn-an-open-source-office-suite-… · home › topics › ai-agents › article
[ARTICLE · art-157700] src=github.com ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Show HN: An open-source office suite with built-in MCP

MCP Docs, an open-source office suite built by the freeflow-im project, launched with a built-in Model Context Protocol server that runs at http://127.0.0.1:39217/mcp (Streamable HTTP) and lets external agents such as Claude Code and Codex drive the application. The app edits Excel, PowerPoint and Word files plus images, HTML, PDF, Markdown and text, and is installed via `npx @freeflow-im/mcp-docs`, which also adds an mcp-docs plugin to Claude Code and Codex; it requires a Mac with Apple silicon or Windows x64. The suite is currently "single player," with document sharing stated as coming later.

read33 min views2 publishedOct 26, 2026
Show HN: An open-source office suite with built-in MCP
Image: Michielbdejong (auto-discovered)

MCP Docs is universal document editor, built with "native" AI integration. It started as sort of my own rewrite of Google Docs - but now its some kind of combination of Office suite, Google Docs, Sublime Text and Mac Preview.

It runs locally on your computer, but also runs in the cloud - you can access the cloud version from either the local app or just a browser.

It supports editing all the standard Office documents:

  • Spreadsheets (Excel)
  • Slides (Powerpoint)
  • Documents (Word)

But also offers quite capable editing for:

  • Images (including background removal)
  • HTML files
  • PDF Files
  • Markdown and text files

The app is a bit of a "bridge" between traditional productivity suites and the AI agents world.

mcp-docs-promo-github.mp4 #

MCP Docs offers two agent experiences:

  1. A built-in assistant available "in situ" that sits next to the current doc for editing:

  2. Any external agent can connect to the MCP server and drive the application:

Codex driving the MCP Docs app

For the internal assistant, you can provide either an LLM API key, OR you can connect your local coding agent (Codex or Claude). This second mode is convenient to take advantage of your existing coding agent setup and subscription, although the latency is a little higher than the built-in assistant.

Besides editing standard Office files (don't need to pay for Office!), and using the AI integration to supercharge your editing, some other interesting features include:

  • Excellent PDF OCR parsing (Convert to markdown ) usingliteparse from LlamaIndex
  • PDF signing and editing support, similar to Preview
  • Live Markdown preview
  • JSON tree viewer

But probably the biggest feature is open source - add your own new features as you need them.

Current version of MCP Docs is "single player" - document sharing is coming later.

npx @freeflow-im/mcp-docs

This downloads the latest MCP Docs into /Applications (on Windows it runs its setup program, which installs it for you), offers to install the mcp-docs plugin (the app's MCP server and a skill on using it) into Claude Code and Codex, whichever you have, starts the app, and prints the app's MCP address for any other client. Run it again to update everything. --claude or --codex picks the agents without asking, --yes takes every one found. You can also download the app from the latest release (MCP-Docs-<version>-mac-arm64.zip for a Mac, MCP-Docs-<version>-win-x64-setup.exe for Windows); its notes say how to install it by hand. It needs a Mac with Apple silicon or Windows (x64).

npx @freeflow-im/mcp-docs --uninstall

removes the app, its data and settings, and agent config.

While the app runs it is an MCP server at http://127.0.0.1:39217/mcp (Streamable HTTP).

  • Claude Code : the installer adds the plugin. Restart Claude Code, run/mcp , chooseplugin:mcp-docs:mcp-docs and sign in; MCP Docs asks you to allow it. In Claude Code the plugin alone is/plugin install mcp-docs --marketplace freeflow-im/mcp-docs , or add the server without it:claude mcp add --scope user --transport http mcp-docs http://127.0.0.1:39217/mcp .
  • Codex : the installer adds the same plugin from the repository's Codex marketplace (.agents/plugins/marketplace.json ). Sign in once withcodex mcp login mcp-docs and start a new session; MCP Docs asks you to allow it. By hand:codex plugin marketplace add freeflow-im/mcp-docs andcodex plugin add mcp-docs@mcp-docs .
  • Any other MCP client : point it at the same address. It signs in with OAuth 2.1 (dynamic client registration and PKCE, as MCP clients do), and MCP Docs shows a dialog asking you to allow each client; nothing is typed into a browser.
  • File › Connect an Agent (MCP)… (or theConnect your agent tile on the home page) shows the address and the command, lists the agents connected and disconnects them.

An agent gets about 60 tools: list, search, open, create, rename, move and delete files and folders; read and edit documents (as Markdown), presentations (slides, their elements and themes; render_slide shows it a slide as drawn) and spreadsheets (cells, formulas, formatting, sorting, filters, rows, columns and tabs); read PDFs as Markdown or convert them; look at and edit pictures, background removal included; import files from the web, or upload them from disk.

  • You see what it does. A file it opens is shown in the window, and its edits to the file open there are made in the editor: they appear as they happen, one ⌘Z undoes its whole run, your own unsaved edits are kept, and deleting rows, slides or blocks asks you first.
  • The open file is the default. With no id, the tools act on the file shown in the app, and see your selection.
  • The folder is the library. The files are the real files on disk, saved back in their own format.

Assistant in the header (or ⌘K) opens a chat panel beside the file. Settings (File › Settings…, ⌘,) chooses what powers it:

  • Use an API key : Anthropic, OpenAI or OpenRouter, with your key, billed for each use to your account there. On the first run a key already set in your login shell (ANTHROPIC_API_KEY ,OPENAI_API_KEY ,OPENROUTER_API_KEY ) is used.
  • Use a local coding agent : the Claude Code installed and signed in on your Mac runs the assistant, in the open folder with all of its own tools and settings, on your Claude plan. Routine steps go ahead on their own; it asks in the panel only when it is unsure.

Each file has its own chat, and the home page has one. See Assistant for how it works.

The folder's files are the file list and its directories are the library's folders (see Folders below; hidden entries and node_modules are skipped). A file's name in the list is its whole file name, extension included: the app never hides one. File → Open Folder… switches to another folder. Your folders need no account and nothing to sign in to; your web files do (see Your web files below).

  • Markdown (.md ) and CSV (.csv ) files open in their editors and are saved back as the plain text they are.
  • Spreadsheets, presentations and documents are Excel, PowerPoint and Word files (.xlsx ,.pptx ,.docx ), edited in place; new ones are made as those too, and a CSV file that outgrows CSV becomes an.xlsx file. A file made elsewhere that holds something the app can't keep opens read-only until you allow editing, and the first save over it keeps a copy in the app's trash. Only a file its Office format cannot hold (a picture in a cell, a gradient, a video in a document) is kept in the app's own format (.ffsheet ,.ffslides ,.ffdoc ), after asking.
  • PDFs and images open in a preview, and videos (.mp4 ,.m4v ,.mov ,.webm ,.ogv ) in a player.
  • Renaming a file in the app renames it on disk, and Move to… moves it into another directory. Deleting moves the file out of the folder into the app's data directory (trash/ ), not the system trash.
  • A file changed by another program while it is open in the app is not overwritten: the save is refused.

The app's own data for a folder (the assistant's chats, pasted images, branch snapshots, deleted files) is kept under its data folder (~/Library/Application Support/MCP Docs/folders/ on macOS), not in the folder, so images pasted into a file are not part of the file. Its settings are kept there too, for every folder. The app talks to a server it starts on a loopback port (desktop/server.ts), which only answers the app's own windows and, through the MCP port, the agents you allowed.

npm install
npm run desktop                 # build the client, then open the current directory in the desktop app
npm run desktop -- ~/Documents  # or a folder you name
npm link                        # once, to get the `mcp-docs-dev` command:
cd ~/Documents && mcp-docs-dev  # open the directory you are in (after `npm run build`)
npm test                        # vitest
npm run typecheck

Requires Node 22.18+. The server runs TypeScript directly through Node's built-in type stripping and uses the built-in node:sqlite. CLAUDE.md explains the codebase.

On macOS the mcp-docs-dev command starts the app through LaunchServices, so the app itself (listed as Electron until it is packaged) is what macOS asks about and grants access to Downloads, Documents and Desktop, not the terminal; a folder it may not read shows a message saying so.

npm run desktop:win builds the Windows app with electron-builder into release/: its setup program ( MCP-Docs-<version>-win-x64-setup.exe), for x64. It can be run on macOS; there the .exe keeps Electron's icon (the release, built on Windows, has the app's). It is not code-signed, so Windows shows its "unknown publisher" warning on first run. npm run desktop:pack builds the unpacked app for the machine it runs on, to try the packaged layout.

The packaged app holds the server's TypeScript as plain files (asar is off), which Electron's Node runs as it does in development. Started from its icon it reopens the folder it showed last, or opens your home folder the first time; a folder can also be given on its command line. It reads no .env.

npm run desktop:mac builds the macOS app and its zip in release/. A release is made by GitHub Actions (.github/workflows/desktop-release.yml):

  1. Write what is new in release-notes/<version>.md (a Markdown list;release-notes/template.md adds the install steps).
  2. npm run release -- <version> sets the version of the app, the Claude Code and Codex plugin (claude-plugin/ ), its Claude marketplace entry (.claude-plugin/marketplace.json ) and the installer (installer/ , the@freeflow-im/mcp-docs npm package), commits that and tags itdesktop-v<version> .
  3. git push origin main desktop-v<version> . The workflow checks and builds the app on Apple silicon and on Windows, makes the GitHub release with the Mac zip and the Windows setup program, and publishes the installer to npm (trusted publishing: no npm token or one-time code).

Running the workflow by hand (Actions › Desktop release › Run workflow) builds both without publishing anything; the builds are kept as the run's artifacts for three days.

A video file (.mp4, .m4v, .mov, .webm, .ogv) opens in a page with the browser's player. On the web, import one from the home page (the Import tile or a drop, 200 MB maximum); it is stored as the file it is, like a PDF. The server answers Range requests for stored files, so the player can seek without down the whole file. Whether a file plays depends on its codec: H.264 and VP8/VP9 play everywhere, while HEVC (the default of recent iPhones) does not play in every browser.

The file list shows one folder at a time. New folder creates one inside the folder being shown, clicking a folder opens it, and the breadcrumbs over the list lead back up; the folder is part of the address (/?folder=Reports/2026). New and imported files go into the folder being shown, Move to… in a file's menu moves it, and a folder can be deleted once it is empty. Typing anywhere on the page filters the folder being shown; Find files, under the filtered list, looks for the same text in the names of files and folders in every folder (GET /api/library/search?q=) and shows where each match is. In the desktop app that walks the mounted folder level by level for up to a minute, and says so when a very large tree could not be covered; with a home directory mounted, its Library is left out. The search is part of the address too (&q=plan, and &find=1 for Find files), and an open file's logo and breadcrumb links lead back to the list as it was, so the same results are there after looking at a file. Folders cannot be renamed or moved yet, and the assistant lists and creates files without regard to folders (what it creates lands at the top).

The switch over the files shows them as a list or as thumbnails (remembered per browser). A thumbnail is the image itself, a frame of a video, the first page of a PDF (drawn by pdf.js, loaded only then), or a small rendering of how a document, spreadsheet or presentation starts (shared/preview.ts, GET /api/library/preview/:id); each is fetched when its card scrolls into view. Excel, PowerPoint and Word files show an icon.

Clicking a file opens it in place, inside its folder: the page header shows the folders down to the file as breadcrumbs, each a link back to the file list there. The Open ↗ button on a row opens the file in a new browser tab instead; in the desktop app that is a new tab of the window (macOS) or a new window.

On the server a folder is a row (folders table, server/folders.ts) and each file records the path of its folder; in the desktop app folders are the directories of the mounted folder. GET /api/library?folder= returns a folder's folders, documents and stored files.

Path Contents
shared/ Workbook types, A1 helpers, value parsing and formatting, and the formula engine (tokenizer, parser, dependency-tracking evaluator, ~120 functions). Used by both client and server.
server/ Fastify API: auth (scrypt password hashes, hashed session tokens in httpOnly cookies) and sheet CRUD.
client/src/state/ WorkbookStore (patch-based undo/redo, incremental recalculation),AutoSaver , spreadsheet operations (ops.ts ) andSheetController (selection, editing, clipboard and commands).
client/src/grid/ Canvas grid: virtualized rendering, frozen panes, hit testing, and the mouse and keyboard interaction.
client/src/deck/ ,client/src/doc/ The presentation and document editors (stores with undo/redo, controllers, views and toolbars).

Assistant in the header (or ⌘K / Ctrl+K) opens a chat panel on the right. Ask it in plain language to read, write, edit, format, sort or restructure the open file, or to find, read, create and open others. It knows which file, tab and selection you're looking at. In the desktop app it runs on your API key or your local coding agent (see The assistant in the app); in the web version it uses Claude Sonnet 5.5 through the server's key by default (AGENT_MODEL switches it), and Settings lets a user use their own key instead, which is then not counted against the daily limit.

  • Edits appear immediately and save like your own. Everything the assistant changes for one message undoes as a single step with ⌘Z. Deleting tabs, rows, columns, slides or blocks, and clearing more than 100 cells, asks you first.
  • A chat for each file , and one for the home page, kept across page loads. A reply stays in the chat it was asked in, even when the assistant opens another file.New chat starts that chat over (old conversations stay in the database).
  • Click an action in the chat (such as "Wrote 8 cells at A1") to select that range.

Screenshots: paste, drop or attach images (PNG, JPEG, GIF, WebP; up to four per message) in the chat and the assistant sees them, for example to transcribe a table from a screenshot into the sheet. Images are downscaled in the browser and stored with the conversation.

How it works:

  • The server (server/agent/ ) runs the agent loop and holds the API key, system prompt and tools. Conversations are stored in SQLite exactly as sent to the API and are only ever appended to. That keeps prompt caching effective and lets the model's thinking blocks be passed back unchanged.
  • Account tools (list_sheets ,read_other_sheet ,create_sheet ) run on the server.
  • File tools (read_range ,write_range ,insert_content ,update_slide and the others) run in thebrowser , on the open file (client/src/agent/clientTools.ts ), so edits render, autosave and undo like any other edit. When the model calls one, the server streams the call to the browser and s; the browser runs it and posts the result back, and the loop continues.
  • Every request carries the current context: the page, the file, its tabs or slides, and the selection. It's added in front of the user's message, so the system prompt stays cacheable.
  • Tool inputs are checked against Zod schemas (server/agent/tools.ts ) before anything runs. The same schemas generate the JSON Schema sent to the model.
  • With a local coding agent (desktop app), each message is a Claude Agent SDK query on the user's own Claude Code (server/agent/claudeAgent.ts ), which works through the app's MCP server, so its edits go through the window like any MCP agent's.

Blank presentation on the home page creates a slide deck (/d/<id>). A slide is a fixed 960×540 canvas (PowerPoint's 16:9 size) holding text boxes, images and shapes (rectangles, ellipses, triangles, diamonds, polygons, stars, arrows, chevrons, speech bubbles, hearts and lines; the table in shared/shapes.ts also maps each one to its PowerPoint preset). Slides are made from layouts (title, section header, title and body, two columns, title and image, blank), and a deck has a theme (light, dark, ocean, forest, sunset, paper) that sets its colors and fonts.

  • Click a slide in the strip on the left to edit it; drag to reorder; right-click for slide commands. Double-click a text box or shape to edit its text; Tab indents a bullet. Drag elements to move them and use the handles to resize (corner handles keep an image's aspect ratio). Arrow keys nudge,Delete removes, ⌘D duplicates, ⌘C/⌘V copy and paste elements, and ⌘Z undoes.
  • Insert → Image… , dropping an image file on the slide, or pasting one adds an image element (stored like cell images).
  • Present (F5) shows the deck full screen; arrow keys and clicks move between slides,N shows the speaker notes and the next slide, and Esc leaves.
  • File → Download as PowerPoint (.pptx) exports the deck (text with bullets, images, shapes, backgrounds and notes) with pptxgenjs in the browser;File → Print / Save as PDF… lays out one slide per page.
  • PowerPoint import : drop a.pptx on the home page (or use the Import tile) to create a presentation from it, orFile → Import PowerPoint slides… inside a deck to add its slides after the current one. The conversion runs on the server (server/pptxImport.ts ) and keeps text boxes with bullets, per-paragraph sizes, weights and colors, fonts, line and paragraph spacing, alignment and insets, theme colors, translucent fills and outlines, pictures (PNG, JPEG, GIF, WebP), shapes (text on a filled shape becomes a text element on top of it), horizontal and vertical lines, solid slide backgrounds and speaker notes. Fonts that are on Google Fonts (Inter, Poppins, Montserrat, ...) are loaded when a deck uses them. Tables, charts, SmartArt, other picture formats, gradients and picture backgrounds are dropped and listed in a banner.
  • Speaker notes live under the slide.

The assistant edits presentations too: read_deck, add_slides (layout plus plain content), update_slide (change the title, body, notes or layout of one slide), edit_elements (move, resize, restyle, add or remove elements), delete_slides, move_slide and set_deck_theme run in the browser against the open deck, and list_decks, create_deck (optionally with all its slides) and open_deck find, create and open decks. "Turn this spreadsheet into a short presentation" reads the sheet, creates the deck and opens it.

Decks are stored like spreadsheets (shared/deck.ts is the format; the sheets table has a kind column) and are listed with them on the home page. Branches and compare are for spreadsheets only.

Blank document on the home page creates a text document (/doc/<id>): a WYSIWYG editor for writing, built on ProseMirror. A document is a sequence of blocks (paragraphs, a title and subtitle, headings, bulleted and numbered lists, quotes, code blocks, images and horizontal rules) whose text can be bold, italic, underlined, struck through, code, linked, colored or highlighted, set in another font or size (the toolbar lists system fonts and a set loaded from Google Fonts), and aligned left, center, right or justified.

  • The toolbar and the Format menu set the paragraph style and text formatting; the usual shortcuts work (⌘B/I/U, ⌘⇧X strikethrough, ⌘E code, ⌘K link, ⌘⇧7/8 lists, ⌘⌥0–3 paragraph and headings, ⌘⇧L/E/R/J alignment, Tab and ⇧Tab to indent list items). Typing# ,- ,1. ,> ,``` or--- at the start of a line turns it into the matching block, and a web address followed by a space or Enter becomes a link.
  • Insert → Image… , pasting or dropping an image adds an image block (stored like cell images); drag its corner handle to resize it, and use the alignment buttons to place it. Clicking into a link shows a small card with the address (click it to open in a new tab), Edit and Remove; ⌘-click a link to open it directly.
  • Every edit is a command (a ProseMirror transaction whose steps know how to invert themselves), recorded by DocStore (client/src/doc/store.ts ): ⌘Z undoes a run of typing, or everything the assistant changed for one request, as a single step. Changes save automatically.
  • File downloads the document as Markdown or as a web page, or prints it (Save as PDF).
  • Document style (Format → Document style…): the default font, body size, line spacing and space after paragraphs, stored on the document and used by all text without a font or size of its own; headings scale with the body size. Format → Line spacing sets the spacing of the selected paragraphs.
  • Pages : a document is paginated by default (View → Pages; View → Pageless gives one continuous column). Format → Page setup… sets the paper size (Letter, Legal, A4), orientation, margins, page numbers, and a header and footer ({page} and{pages} are replaced). Insert → Page break (⌘⏎) starts a new page. The editor stays one continuous column;client/src/doc/pagination.ts measures where lines and blocks land and inserts spacer decorations at page boundaries (whole images and rules move, paragraphs split between lines, and headings stay with the block after them), and the page frames, numbers, header and footer are drawn from the same result. Printing in Pages mode lays out one page per sheet from the same measurements, so the printout matches the screen. View → Zoom fits the page to the window or sets a percentage. A rail on the left shows a thumbnail of every page with the current page highlighted; click one to jump to it, drag its edge to resize it, and collapse it with the arrow (or View → Page thumbnails).
  • Word import : drop a.docx on the home page (or use the Import tile) to create a document from it, orFile → Import Word document… inside a document to add its content after the cursor. The conversion runs on the server (server/docxImport.ts ) and keeps the document's default font, size, line and paragraph spacing (as the document style), paragraph styles (title, subtitle, headings, quotes) with their fonts and sizes, paragraph spacing, bulleted and numbered lists with nesting, bold, italic, underline, strikethrough, colors, highlights, fonts and sizes, hyperlinks, line breaks, page breaks, pictures, and the page size, orientation and margins. Tables become one paragraph per row; headers, footers, footnotes, comments, charts and SmartArt are dropped and listed in a banner.

The assistant edits documents too. It sees a document as numbered blocks in Markdown (read_doc) and writes Markdown back: insert_content, replace_blocks and delete_blocks work on whole blocks, replace_text changes words in place, format_text makes text bold, italic, underlined, colored, highlighted or a link without retyping it, format_blocks changes a block's kind or alignment, insert_image adds a picture, set_page_setup changes the page setup, set_doc_style changes the document's default font, size and spacing, and \newpage in Markdown is a page break. list_docs, create_doc (with the content as Markdown), read_other_doc and open_doc find, create, read and open documents. Deleting blocks, or replacing ten or more at once, asks for confirmation.

The file format (shared/doc.ts) is the ProseMirror JSON of the document under a schema that is the single definition of what a document may contain; shared/docMarkdown.ts converts documents to and from Markdown (plus # text {.title}, ## text {.subtitle}, <u>, <mark> and <span style="color: …; font-family: …; font-size: 14pt"> for what Markdown cannot say), and the server validates every save against the schema.

File → Create branch… (or Create branch in a spreadsheet's menu on the home page) makes a branch: a copy you can edit freely that stays connected to its original. Branches are listed under their original on the home page.

In a branch, Compare with original opens a three-way comparison. When the branch is created, a snapshot of the original is saved as the base (data/sheets/<id>.base.json). Each difference is then labeled by who made it:

  • Yours (green): changed in your branch since it was created.
  • Original (purple): changed in the original since then.
  • Conflict (red): changed differently on both sides. Identical changes on both sides don't show.

Changed cells are tinted on the grid, and hovering one shows the base, your and the original's values. Rows that exist only on the other side are drawn as a line where they would be. The side panel lists every change; click one to jump to it. The comparison updates as you edit, and ↻ fetches the original's latest state.

How the comparison works (shared/diff.ts):

  • Rows are aligned with a Myers diff (the algorithm behindgit diff ), so an inserted or deleted row is one change rather than every row below it. Columns are compared by position.
  • Formulas are rewritten into the base's row numbers and sheet IDs before comparing. Formulas that the app adjusted because of row inserts, deletes or sheet renames don't show as edits.
  • Detached branches: if the original is deleted, the branch keeps working and compares against the base.

Beside the folders on your computer, the app shows your MCP Docs account on the web (mcpdocs.freeflow.im) in a Web tab, in the same window. The Local | Web switch beside the logo goes from one to the other (File → Open Web Tab too), and each tab's title ends in "— Local" or "— Web". The two are separate libraries: nothing syncs, and a copy is a new file.

  • Signing in: only when you first open a Web tab. Email and password sign in inside the tab;Continue with Google finishes in your browser, and the tab signs itself in a moment later.File → Sign Out of Web signs out.
  • Upload to Web… (a file's ⋮ menu in a folder) copies it into one of your web folders, pictures and all.
  • Download to Folder… (a file's ⋮ menu in a Web tab) copies a web file into a folder on your computer as a Word, PowerPoint or Excel file, in the app's own format (which keeps everything), or as a PDF; the dialog says what an Office file would not keep. On the web in any browser the same item isDownload to MCP Docs , which opens the app's dialog.

Developing, a Web tab can point at a local server instead: start the app with UNIVERSAL_WEB_URL=http://localhost:5173. How it works is in docs/web-and-pdf.md.

File → Download as PDF in a document, presentation or Markdown file, and Download as PDF on a saved web page (or in a file's ⋮ menu), downloads it as a PDF laid out as it prints: documents with their page setup, one slide per page, Markdown as its preview with diagrams drawn. Agents can make one with export_pdf, which keeps the PDF next to the file. The desktop app prints them itself; the web version needs Chromium (see below).

Press ⌘F (Ctrl+F on Windows and Linux), or use Edit → Find…, to open the find bar. Inside a spreadsheet it replaces the browser's page search.

  • Search runs as you type, highlights every match, and jumps to the nearest one.
  • Enter andShift+Enter step through matches, andEsc closes the bar.
  • Options: match case, match the entire cell, also search formula text, and search all sheets.
  • It matches values as displayed (for example $1,500 ) and skips rows hidden by a filter.

There are two ways to import an Excel workbook:

  • Home page (theImport Excel tile, or drop a file on the page): creates a new spreadsheet from the file.
  • File → Import Excel file inside a spreadsheet: adds the file's sheets as new tabs after the existing ones, as one undoable change. Sheets whose names are already taken get a number added (Summary →Summary 2 ), and references between the imported sheets are updated to match.

The conversion runs on the server (server/xlsxImport.ts). .xlsx files are read with ExcelJS. Older .xls files are first converted to .xlsx with SheetJS (pinned to 0.20.3 from the vendor's CDN, because the npm copy is outdated), so only their values, formulas, number formats and column widths carry over, not fonts, colors or alignment. The format is detected from the file contents, not the extension. For .xlsx files it keeps:

  • values, formulas (including shared formulas), dates, and text that looks like a number (such as 001 )
  • bold, italic, underline, strikethrough, text and fill colors, alignment and number formats
  • column widths, row heights, frozen panes, the filter range, and multiple sheets with the references between them

Features the app doesn't support are reported in a banner after import: unsupported functions (shown as #NAME?), table references or links to other files (shown as #ERROR!), merged cells (unmerged, with the value kept in the top-left cell) and hidden sheets. Charts, images, comments, conditional formatting, data validation and filter criteria are dropped. Uploads are limited to 20 MB, 300 MB uncompressed and 1 million cells.

Drop a .csv file on the home page (or use the Import tile) and it opens in the spreadsheet editor as the CSV file it is: one sheet of plain values, listed with type CSV and marked with a CSV badge next to its name. Edits are saved back as CSV text (shared/csv.ts), keeping the file's line endings; formulas are stored as typed (=B2*2) and still calculate. File → Download → Comma-separated values gives the file back.

CSV cannot hold formatting, images, column widths and row heights, frozen rows and columns, filters, sheet names or a second sheet. Using one of these asks to convert the file to a spreadsheet first (also File → Convert to spreadsheet…), then makes the change. Converting keeps the same file and content; it can still be downloaded as CSV afterwards. The assistant works on a CSV file like on any spreadsheet, and tells you when what you asked for needs the conversion. Imports are limited to 10 MB.

Inside a spreadsheet, File → Import CSV file… adds a CSV file as a new sheet, and File → Save a copy as CSV file… saves the current sheet as a new CSV file in the app, with the values as shown and without formatting, formulas, images or the other sheets. The home page menu of a spreadsheet has Download as CSV for its first sheet.

The Connectors page (from the home page, or File > Data connectors… in a sheet) connects external data sources, starting with Brex (card and cash transactions, cash accounts, cards, users, expenses, budgets). For Brex, paste a read-only user token from the Brex dashboard (Developer > User Tokens). The token is tested, stored encrypted (key from CONNECTOR_ENCRYPTION_KEY, or one generated in data/connector.key) and never shown again. The assistant can then pull data in: "ingest my Brex card transactions from the last 30 days into a tab called Brex" calls list_connections and then ingest_connector_data.

See docs/connectors.md for how it works and how to add a connector or dataset (API key or OAuth 2.0).

The same app runs as a web service with accounts (npm run dev, or npm run build && npm start), hosted as MCP Docs at docs.freeflow.im, where it is also a plugin for ChatGPT (plugin/, named Universal Docs there). The desktop app shows it in its Web tabs (see Your web files).

npm run dev        # API on :3001 and Vite on :5173; open http://localhost:5173
npm run build && npm start   # production: serves dist/client and the API on :3001

Users register and sign in with email and password (verified by an emailed link) or with Google.

Password reset: Forgot your password? on the sign-in page emails a one-time link (valid for an hour) that opens /reset to set a new password and signs the user in; other sessions are signed out. Email goes through Cloudflare Email Service when CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN are set (MAIL_FROM must then be an address on a domain verified for sending in that account), else through Resend when RESEND_API_KEY (and optionally MAIL_FROM) is set; otherwise the message, link included, is written to the server log, so on a single-operator deployment the link can be copied from the server's log.

Email verification: a password sign-up gets no session until the link in the verification email (valid 24 hours) is opened at /verify; signing in before that is refused with a "send it again" option, and signing up again with the same address just replaces the password and resends the link. Opening a password reset link also verifies the address. Accounts that existed before verification was added count as verified.

Google sign-in: with GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET set (an OAuth web client whose authorised redirect URIs include <APP_URL>/api/auth/google/callback), the sign-in and registration pages show Continue with Google. The first Google sign-in links to an existing verified account with the same email, takes over an unverified one (its password stops working, since whoever set it never proved they own the address), or creates a new one with no password (set one later with Forgot your password?). APP_URL sets the origin used in the link (defaults to the requesting origin). LEGACY_HOSTS lists old host names (comma-separated) that redirect to APP_URL with the same path, for moving the app to a new domain.

Environment variables: PORT (default 3001), HOST (default 127.0.0.1), DATA_DIR (default ./data), and SECURE_COOKIES=1 for HTTPS deployments. Download as PDF needs a Chromium: CHROMIUM_PATH names one (in the hosted app's image, Debian's chromium at /usr/bin/chromium), else a Playwright-installed Chromium or Chrome on the machine is used; in development the pages it prints come from Vite (PRINT_ORIGIN, default http://localhost:5173). The assistant needs ANTHROPIC_API_KEY. For local development, copy .env.example to .env (git-ignored) and fill it in; the server loads it on start (server/env.ts), so restart it after editing. You can also set AGENT_MODEL (claude-sonnet-5-5 by default, or claude-opus-5-5), AGENT_EFFORT (low, medium (default) or high) and AGENT_DAILY_REQUEST_LIMIT (model calls per user per day, default 500). The coding and research jobs run Claude Code with the same key and its default model unless CODING_API_KEY (a separate API key, or a Claude Code OAuth token from claude setup-token, billed apart) or CODING_MODEL (a model id to pin) is set.

An experiment, for the web version running from a source code checkout (and its hosted deployment): not in the desktop app.

When the assistant can't do something, it offers to add the capability to the app. If the user agrees, it calls request_app_change with a spec, the user confirms in the panel, and after the reply ends the server starts a detached worker (server/agent/worker.ts) that runs Claude Code in the repository through the Claude Agent SDK. Claude Code reads CLAUDE.md (which explains how to add an assistant tool), makes the change, and runs typecheck and tests; the worker verifies both again. When the change is live the browser reloads and sends the assistant a message with the summary, and it finishes the original request with its new tool.

The assistant can also hand off research: request_research queues a job that runs Claude Code in a scratch folder with web search and read-only tools (never the app's code), optionally with the open spreadsheet exported as CSV, and its report comes back to the conversation as a message so the assistant can act on it, for example by writing results into the sheet. Research jobs appear on the Changes page with their report and cannot be reverted.

Every job is a record of the change (request, the coding agent's summary, files, cost, pull request) in the agent_jobs table, listed on the Changes page (/changes), where any finished change has a Revert button. A revert is a job that applies the change's patch in reverse (or asks the coding agent to undo it when the code has moved on) and goes through the same checks, restart and publishing. One job runs at a time; full Claude Code output is in data/jobs/<id>.log.

  • Local development : the dev server restarts and Vite hot-reloads as files change. WithoutGITHUB_TOKEN the change is left uncommitted in the working tree for review.

  • A failed job (checks still failing after the fix rounds, the time limit, or the server not starting again with the change) puts back every file it changed, and rebuilds the client if it had rebuilt it, so nothing unverified is left for the next start to load.

  • Production (AWS, deploy/aws) : the app edits its own source in the container. The worker makes the source tree a git checkout of the deployed commit (GIT_COMMIT_SHA , set in the image), runs Claude Code, verifies, rebuilds the client (npm run build ), asks the server to restart (npm start runsserver/supervise.ts , which restarts it on exit code 75), then commits the job's files plus an entry inCHANGELOG.md on a branch, pushes it, opens a pull request and squash-merges it. The push to main then redeploys the same code. NeedsGITHUB_TOKEN (a fine-grained token with Contents and Pull requests read/write on the repo) andGITHUB_REPO=owner/name (set bydeploy.sh ). The image (deploy/aws/Dockerfile ) has git and keeps the dev dependencies.

  • data/app.db : SQLite database holding users, sessions, sheet metadata (owner, title, timestamps), assistant conversations and per-user assistant usage.

  • data/sheets/<id>.json : one file per spreadsheet in the native JSON workbook format (shared/types.ts ). Cells are stored sparsely by A1 address as the raw input plus optional style. Files are written atomically (temp file, then rename). The client autosaves the full workbook 800 ms after the last change.

With R2_BUCKET, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY and CLOUDFLARE_ACCOUNT_ID set (or R2_ENDPOINT for another S3-compatible store), every save also puts the file to the bucket as users/<owner>/<kind>/<id>/<revision>.json, so the bucket holds a history; images go up on upload and branch bases beside their branch (server/backup.ts). Uploads are queued and retried, so a save never fails because the bucket is down. Deleting a file writes a marker instead of removing the objects: the home page's Trash lists files deleted in the last 30 days and restores them. Once a day (and at npm run r2 -- maintenance) the server snapshots app.db and plugin.db into backups/<date>/, re-uploads anything the bucket lacks, prunes snapshots older than 30 days and purges files deleted more than 30 days ago. npm run r2 -- restore <empty dir> rebuilds a data directory from the bucket; npm run r2 -- list [prefix] shows what is there. Without the variables, nothing changes.

Each file's menu on the home page and the editors' File menus download it: documents as Markdown (or JSON), presentations as PowerPoint (or JSON), spreadsheets as Excel, CSV of one tab, or JSON (GET /api/files/<id>/export?format=md|pptx|xlsx|csv|json, server/export.ts). Export all on the home page downloads every file of the account as a zip with a readable format and JSON per file plus an images/ folder, with Markdown image links pointing into it (GET /api/export.zip).

── more in #ai-agents 4 stories · sorted by recency
── more on @mcp docs 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/show-hn-an-open-sour…] indexed:0 read:33min 2026-10-26 · —