DEV Community

SUJIT GANGADHARAN
SUJIT GANGADHARAN

Posted on

How I Built an MCP Server to Let Claude Organize My OneNote Notebook (And Beat a 501 Error)

TL;DR: I built an open-source MCP server for Microsoft OneNote in Python. It connects Claude (or any MCP-compatible AI) directly to your OneNote notebooks. The tricky part: Microsoft's copyToSection API returns a 501 on personal accounts. Here's the workaround — and the full story of why I built it.


The Problem: A Notebook Full of Chaos

Three years of notes. 200+ pages. Dozens of sections with names that made sense at the time. Zero structure.

I wanted to reorganize my OneNote notebook, but doing it manually was out of the question. So I asked: what if I could give Claude access to OneNote directly, and let it do the work?

That question led me to build onenote-mcp-server.


What Is MCP?

Model Context Protocol (MCP) is an open standard from Anthropic that lets AI assistants connect to external tools and services through a structured, typed interface. You build an MCP server that exposes "tools" — essentially callable functions — and any MCP client (Claude Desktop, Cursor, Zed, Continue.dev) can call them.

It's the difference between telling your AI "here's a document, help me think about it" and telling your AI "here are your tools, go organize my notebook."


The Architecture

The server is built with Python + FastMCP and talks to OneNote through the Microsoft Graph API. Authentication is handled via MSAL with a local token cache — browser-based OAuth on first run, silent token refresh after that. Everything runs locally on your machine.

High-Level Architecture

The server exposes 8 tools:

onenote_list_notebooks       → list all notebooks
onenote_list_section_groups  → list section groups
onenote_list_sections        → list sections
onenote_list_pages           → list pages in a section
onenote_get_page_content     → read full HTML of a page
onenote_create_section       → create a new section
onenote_copy_page            → copy a page to another section
onenote_clone_page           → clone with full HTML fidelity
Enter fullscreen mode Exit fullscreen mode

The 501 Wall

Microsoft's Graph API has a copyToSection endpoint documented for moving OneNote pages between sections. It works great on organizational accounts (Office 365).

On personal accounts (Outlook.com, Hotmail, Live.com)? It returns:

HTTP 501 Not Implemented
Enter fullscreen mode Exit fullscreen mode

Not a permissions error. Not a rate limit. The feature is simply not implemented for personal accounts. The docs don't make this obvious — you find out the hard way.


The Workaround: Read HTML → POST to New Section

The fix is conceptually simple:

  1. Fetch the source page's HTML: GET /me/onenote/pages/{id}/content
  2. POST that HTML to the target section: POST /me/onenote/sections/{id}/pages
@mcp.tool()
async def onenote_clone_page(page_id: str, target_section_id: str) -> str:
    """Clone a OneNote page to a target section (works on personal accounts)."""

    # Step 1: Read the source page HTML
    html_content = await graph_get(f"/me/onenote/pages/{page_id}/content")

    # Step 2: Recreate it in the target section
    response = await graph_post(
        f"/me/onenote/sections/{target_section_id}/pages",
        data=html_content,
        content_type="text/html"
    )

    return f"Page cloned. New page ID: {response['id']}"
Enter fullscreen mode Exit fullscreen mode

The result is functionally identical to a copy — same title, same content, same formatting. This became the cornerstone of the whole migration: Claude could now move pages in bulk, reading each one to understand its content and routing it to the correct destination section.

Here's the full flow as a sequence diagram:

501 workaround sequence diagram


The Result

With the server running and connected to Claude Desktop, I reorganized 200+ pages in a single session. I described my target structure in plain English, Claude read the pages to understand their content, and moved each one to the right destination.

From chaos to structure in a few hours — work that would have taken a full day manually.


Setup (5 minutes)

Prerequisites: Python 3.8+, a Microsoft account, an Azure App Registration (free)

git clone https://github.com/jisujit/onenote-mcp-server
cd onenote-mcp-server
pip install -r requirements.txt
cp .env.example .env
# Fill in CLIENT_ID and TENANT_ID=common
Enter fullscreen mode Exit fullscreen mode

Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "onenote": {
      "command": "python",
      "args": ["/path/to/onenote-mcp-server/src/server.py"],
      "env": {
        "CLIENT_ID": "your-client-id",
        "TENANT_ID": "common"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Use TENANT_ID=common for personal Microsoft accounts. Full Azure setup guide in the README.


Key Takeaways

Test with the actual account type your users will have. Organizational accounts and personal accounts behave differently against the Graph API in ways the docs don't always surface.

FastMCP is excellent. It handles all the MCP protocol boilerplate. You write a decorated Python function, it becomes a callable tool. That's the whole API surface you need to learn.

AI as orchestrator + MCP as the hands = a genuinely new capability. The server provides structure; Claude provides judgment about where content belongs. Neither is sufficient alone.


Repo

GitHub: github.com/jisujit/onenote-mcp-server

Issues, PRs, and stars welcome. If you're building MCP servers for other Microsoft 365 tools, I'd love to hear what you're running into.


Built by Sujit G · LinkedIn · GitHub

Top comments (0)