Skip to main content
Glama
README.md
# onenote-mcp

A local MCP server that lets Claude read your OneNote notebooks, **including the
pictures you pasted into them**.

It talks to the OneNote desktop application through its COM interface. There is
no Azure app registration and no tenant consent. Nothing leaves the machine.

## Why this exists

The Microsoft 365 connector does not support OneNote. It refuses with
`mime type 'application/msonenote' is not allowed`. Every Graph API route needs
an Entra app registration with `Notes.ReadWrite.All`, which on a corporate
tenant means an IT request. The desktop COM API needs neither.

## What it can do

- Search every open notebook, **including the text inside pasted screenshots**,
  because OneNote's own index covers OCR.
- Read a page as text, with a list of the images on it.
- Return those images as pictures, downscaled to fit sensibly in a transcript.

It is **read-only**. It cannot change, create or delete anything.

## Requirements

- Windows, with the **OneNote desktop application** installed.
  The OneNote app from the Microsoft Store is a different program and has no
  COM interface.
- Python 3.11 or later. Bitness does not matter.

## Install

```powershell
python -m venv .venv
.venv\Scripts\python.exe -m pip install -e .
```

## Register

`.mcp.json` in this folder registers the server for this project. To use it
everywhere:

```powershell
claude mcp add onenote -s user -- C:\path\to\onenote_COM_mcp\.venv\Scripts\python.exe -m onenote_mcp.server
```

The same command works for Claude Desktop and the VS Code extension.

## Tools

| Tool | What it gives you |
|---|---|
| `list_notebooks` | Open notebooks, filtered by name or location; sections on request |
| `list_pages` | The pages under a notebook, section group or section |
| `search_pages` | Pages matching a query, across everything or under one scope |
| `get_page` | One page as text, plus a manifest of its images. No pixels |
| `get_page_images` | The pictures, by index, downscaled to `max_edge` |

Every `scope_id` and `page_id` also takes a readable path, so you rarely need
to copy an id at all:

```
list_pages(scope_id="Christian Cruger @ Ramboll/Personal")
list_pages(modified_after="2026-09-14", sort="modified")   # yesterday's notes
```

The tail of a path is enough while it is unambiguous. If two notebooks hold a
section of the same name, the error names both.

`list_notebooks` leaves sections out unless you ask for them, and filters on
`name_filter` and `location`. A full tree of four notebooks is several thousand
tokens, and most tasks need one section.

`search_pages` reads OneNote's index, which covers body text and the text
inside pasted screenshots, **and** matches the page titles from the hierarchy.
Each hit says which of the two found it. The index can lag a page edited
minutes ago; the title match does not, which is why both run.

`get_page` and `get_page_images` are deliberately separate. A page can hold a
dozen full-size screenshots, and reading its text should not drag all of them
into the conversation.

Ask `get_page` for `include_ocr` when you want the text OneNote recognised
inside an image. It is often enough to answer a question without fetching the
picture at all.

## The first call is slow

The server does not contact OneNote until the first tool call. That call starts
OneNote if it is closed, which can take a while. Later calls are fast: a
ten-page section reads in about two seconds.

## Development

```powershell
.venv\Scripts\python.exe -m pytest        # no OneNote needed
.venv\Scripts\python.exe tools\smoke.py   # live check, needs OneNote
```

Read `CLAUDE.md` before changing `com.py`. The COM interface has two traps that
cost a day to find, and one of them kills the OneNote process.