Skip to main content
Glama
noelroehrig

OneNote MCP Server

by noelroehrig
README.md
# OneNote MCP Server

A local MCP (Model Context Protocol) server that lets Claude Desktop — or any MCP
client — read and edit pages in the **OneNote desktop app** on Windows through
OneNote's COM automation API.

No Azure app registration, no Microsoft Graph, no cloud authentication: the server
talks directly to the OneNote application running on your machine.

## Foreword

I'm a software developer but built this MCP server by heavily relying an AI Coding Agent since I'm not a python developer. I needed a reliable MCP server to edit and build OneNote pages and couldn't find a proper existing solution, therefore I built it myself. Other solutions seemed to focus on pulling out page content to leverage search and summarization with an LLM.

I tried to keep Token usage as low as possible and built in some bare minimum security by [restricting notebook access](#configuration). I still recommend using regular backups before allowing an LLM to access your Notebooks.

I hope this helps someone in a similar situation like me! Feel free to use the content of this Repo in any way you like.

## Requirements

- Windows 10/11
- OneNote desktop (2013, 2016, or Microsoft 365)
- Python 3.10+

## Installation

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

## Claude Desktop configuration

Add this to `%APPDATA%\Claude\claude_desktop_config.json` (adjust the path to
where you cloned the repo):

```json
{
  "mcpServers": {
    "onenote": {
      "command": "C:\\Projects\\onenote-mcp\\.venv\\Scripts\\python.exe",
      "args": ["-m", "onenote_mcp.server"]
    }
  }
}
```

Alternatively, installation creates a console script, so `command` can simply be the absolute path to `".venv\\Scripts\\onenote-mcp.exe"` inside the repo, with no `args` at all (relative paths won't work — Claude Desktop does not launch servers from the repo directory).

Restart Claude Desktop; the OneNote tools should appear in the tools list.
OneNote desktop must be running (or startable) with at least one notebook open.

## Installing as an executable

Install straight from GitHub into a fresh venv:

```
py -m venv %USERPROFILE%\onenote-mcp
%USERPROFILE%\onenote-mcp\Scripts\python.exe -m pip install git+https://github.com/noelroehrig/onenote-mcp
```

(cmd syntax — in PowerShell write `$env:USERPROFILE` instead of `%USERPROFILE%`.)

Then point Claude Desktop at the installed console script. On a machine with production onenote notebooks, restrict the server to a dedicated notebook right away:

```json
{
  "mcpServers": {
    "onenote": {
      "command": "C:\\Users\\<name>\\onenote-mcp\\Scripts\\onenote-mcp.exe",
      "env": { "ONENOTE_ALLOWED_NOTEBOOKS": "SharedNotebook" }
    }
  }
}
```

Verify the whole chain: restart Claude Desktop and ask Claude to call the
`ping` tool — `{"server": "ok", "onenote_responsive": true}` means the server,
COM bridge, and OneNote are all talking to each other.

Note: the Microsoft Store "OneNote for Windows 10" app has no COM interface;
the desktop edition (2016 or Microsoft 365) is required.

## Tools

### Navigation

| Tool            | What it does                                                                   |
| --------------- | ------------------------------------------------------------------------------ |
| `get_notebooks` | Open notebooks with their sections (no pages) — the small navigation skeleton. |
| `list_pages`    | The pages of one section as `{id, name}` pairs.                                |

The intended flow: `get_notebooks` → pick a section → `list_pages(section_id)` →
pick a page → read/write tools below.

### Reading

| Tool               | What it does                                                                                                                                                           |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get_page`         | A page as structured JSON: paragraphs, headings, lists, inline and floating images. Image bytes are **not** included — images carry compact `mcpref:` handles instead. |
| `get_image_data`   | The base64 bytes for a single image handle, fetched on demand.                                                                                                         |
| `validate_handles` | Check whether image handles are still writable, without writing anything.                                                                                              |

### Writing

| Tool           | What it does                                                                                       |
| -------------- | -------------------------------------------------------------------------------------------------- |
| `create_page`  | Create a page in a section; optionally as a sub-page of an existing page. Returns the new page id. |
| `replace_page` | Replace a page's entire content from structured JSON (outlines + floating images).                 |
| `append_page`  | Append one structured content block to an existing page, preserving current content.               |

### Health

| Tool   | What it does                                                                                                                   |
| ------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `ping` | Fast health check: is the server alive, and is OneNote responding? Works even while another call is stuck on a wedged OneNote. |

### Raw-XML escape hatches

Prefer the structured tools above; these exist for direct schema control and debugging.
Token usage is significantly higher with the whole XML-overhead.

| Tool                 | What it does                                                                      |
| -------------------- | --------------------------------------------------------------------------------- |
| `list_hierarchy_xml` | The entire notebook → section → page tree as raw OneNote XML (can be very large). |
| `get_page_xml`       | The full `<one:Page>` XML of a page, with images as `mcpref:` handles.            |
| `replace_page_xml`   | Replace a page from a caller-supplied `<one:Page>` document.                      |
| `append_page_xml`    | Append a single raw XML element (e.g. `<one:Outline>`) to a page.                 |

## Image handles (`mcpref:`)

Reading a page never ships image bytes to the client. Each image is represented
by a short opaque handle such as `mcpref:a1b2c3d4e5f6`, together with its size and
position. This keeps reads of image-heavy pages small and fast.

- To **preserve** images when rewriting a page, copy the handles (or the whole
  `images` list from `get_page`) verbatim into `replace_page` / `append_page`.
  The server resolves handles back to real bytes just before writing to OneNote.
- To **inspect** an image's pixels, call `get_image_data` with its handle.
- Handles live for the lifetime of the server process. If a handle has gone
  stale (e.g. after a restart), re-read the source page with `get_page` to mint
  fresh ones.

## Error codes

Tool errors start with a machine-readable code so clients can react without parsing prose:

| Code            | Meaning                                                                                                                                                                           |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `timeout`       | OneNote did not respond within the per-operation deadline — it is likely showing a modal dialog or syncing. Dismiss any dialog and retry; `ping` tells you when it has recovered. |
| `backend_error` | OneNote returned a COM failure (bad id, locked content, …).                                                                                                                       |
| `bad_request`   | The request itself was invalid (unknown image handle, malformed content, …).                                                                                                      |

## Configuration

All configuration is via environment variables (set them in the `env` block of
the Claude Desktop server entry if needed):

| Variable                    | Default                    | Meaning                                                                                                                                                                                                                             |
| --------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ONENOTE_ALLOWED_NOTEBOOKS` | _(unset — no restriction)_ | Comma-separated notebook names, e.g. `ClaudeSpike, Mathe 5a`. When set, all other notebooks are invisible and untouchable: they are hidden from notebook listings and every read/write against their sections or pages is rejected. |
| `ONENOTE_READ_TIMEOUT`      | `20`                       | Timeout (s) for page/hierarchy reads and image fetches                                                                                                                                                                              |
| `ONENOTE_WRITE_TIMEOUT`     | `25`                       | Timeout (s) for page creates and writes                                                                                                                                                                                             |
| `ONENOTE_PING_TIMEOUT`      | `8`                        | Timeout (s) for the `ping` health probe                                                                                                                                                                                             |
| `ONENOTE_IMAGE_CACHE_MB`    | `200`                      | In-memory cap for cached image bytes; least-recently-used entries are evicted beyond it                                                                                                                                             |

## Development

Source layout:

```
src/onenote_mcp/
  server.py    MCP tool definitions (FastMCP, stdio transport)
  com.py       COM wrapper: threading, timeouts, image-handle cache
  builders.py  model <-> OneNote XML conversion
  models.py    Pydantic models = the JSON schema of the structured tools
```

Run the server manually:

```
.venv\Scripts\python.exe -m onenote_mcp.server
```

### Tests

Unit tests (no OneNote required):

```
.venv\Scripts\python.exe -m pytest tests/unit/ -q
```

End-to-end tests — these drive the real MCP server against the real OneNote
desktop app and create pages in a test notebook. Open the **ClaudeSpike**
notebook in OneNote first (they fall back to the first open notebook otherwise):

```
.venv\Scripts\python.exe -m pytest -m e2e tests/e2e/ -v
```

## License

MIT — see [LICENSE](LICENSE).