🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)
🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)

🔧 Programmierung 🕛 kürzlich 7 Min Lesezeit
0

I Rewrote a OneNote MCP Server in TypeScript — Here's What I Learned About Microsoft Graph Auth

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

If you use Claude, Cursor, or any MCP-compatible AI assistant, you've probably noticed how useful it is when your AI can actually read your notes. I wanted that for OneNote — so I grabbed an existing MCP server, hit a cryptic 401 error, and ended up rewriting the whole thing in TypeScript.



Here's what I learned.






What's an MCP Server?



, a JavaScript MCP server that uses Microsoft Graph to access OneNote. It had the right idea — device-code auth so you don't need to register an Azure app — but I hit a wall immediately.






The 401 That Broke Everything



After authenticating with my personal Microsoft account, every Graph API call returned:




CODE
HTTP 401 — "The request does not contain a valid authentication token"
(error code 40001)





The token looked fine. Authentication completed successfully. But Graph rejected every request.





The Root Cause: .All Scopes



The original server requested these scopes:



CODE
const scopes = ['Notes.Read.All', 'Notes.ReadWrite.All', 'User.Read'];





Those .All scopes are application-level permissions. Personal Microsoft accounts (MSA) cannot consent to them — only Azure AD work/school accounts can. When a personal account tries to use them, Azure does issue a token, but it's a token that Microsoft Graph won't accept.



No error during auth. No warning. Just a silent 401 on every subsequent call.





The Fix



Replace .All scopes with resource-qualified delegated scopes:



CODE
export const SCOPES = [
'https://graph.microsoft.com/Notes.Read',
'https://graph.microsoft.com/Notes.ReadWrite',
'https://graph.microsoft.com/User.Read',
];





These are delegated scopes that work for both personal and work/school accounts. They grant access to the signed-in user's own OneNote content, which is exactly what you want for an MCP server.





Bonus Gotcha: Non-JWT Tokens



Personal Microsoft accounts return compact (non-JWT) tokens — opaque strings that don't have the three-segment header.payload.signature structure. If your code validates token format by checking for JWTs, it will incorrectly reject perfectly valid personal-account tokens.



The right approach: don't validate token format at all. Let Microsoft Graph be the authority on whether a token is valid.





Why a Full TypeScript Rewrite?



Once I fixed the auth bug, I looked at the codebase and found:




  • ~10 standalone JavaScript files doing overlapping things (simple-onenote.js, list-sections.js, list-pages.js, get-page.js, get-page-content.js, get-all-page-contents.js...)

  • MCP tools with no parameter schemas — everything came in through params.random_string

  • A dependency on jsdom just for HTML-to-text conversion

  • A dependency on node-fetch (unnecessary with Node 18+ native fetch)

  • The deprecated Client.init callback pattern instead of Client.initWithMiddleware



So I rewrote it.





The New Architecture



Everything lives in src/ with clear separation:



CODE
src/
config.ts — Client ID, tenant, scopes, paths
logger.ts — stderr-only logging (stdout = JSON-RPC)
token-store.ts — Load/save/normalize access tokens
auth.ts — Device-code authentication
graph-client.ts — Graph SDK client factory
html.ts — HTML→text (dependency-free)
onenote.ts — OneNoteClient class
mcp-server.ts — MCP server with Zod-typed tools
cli.ts — Unified CLI (replaces 10 scripts)
index.ts — Barrel export







Key Design Decisions



1. Zod schemas on every tool



The old server used the SDK's tool() method without schemas, so parameters arrived as params.random_string. The new server defines explicit Zod schemas:



CODE
server.tool(
'getPage',
'Get page content by ID or title search.',
{ query: z.string().min(1).describe('Page ID or title substring') },
async ({ query }) => {
// query is typed as string, validated by Zod
},
);





This gives AI clients proper parameter descriptions and type information, so they know exactly what to pass.



2. One client class instead of scattered scripts



OneNoteClient encapsulates all Graph API operations:



CODE
const client = OneNoteClient.fromStoredToken();
const notebooks = await client.listNotebooks();
const page = await client.findPage("meeting notes");
const content = await client.getPageContent(page.id);
console.log(content.text); // HTML already converted to plain text





3. Dependency-free HTML→text



OneNote's Graph API returns page content as HTML. The old code used jsdom (a heavy dependency) to parse it. The new htmlToText() function handles it with regex — strip script/style blocks, convert block elements to newlines, decode entities, collapse whitespace:



CODE
export function htmlToText(html: string): string {
return html
.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, ' ')
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/(p|div|h[1-6]|li|tr|table|ul|ol)>/gi, '\n')
.replace(/<[^>]+>/g, ' ')
// ... entity decoding, whitespace collapsing
.trim();
}





For the structured HTML that OneNote returns, this is more than sufficient and eliminates a large dependency.



4. stderr-only logging



This one is subtle but critical. An MCP stdio server uses stdout exclusively for JSON-RPC messages. Any console.log() call corrupts the protocol stream and crashes the connection. All logging goes through a log() helper that writes to stderr:



CODE
export function log(...args: unknown[]): void {
console.error('[onenote-mcp]', ...args);
}







Testing



The test suite uses Vitest with mocked Graph clients — no real API calls needed:



CODE
vi.mock('../src/graph-client.js', () => ({
createGraphClient: vi.fn(),
}));

// Mock returns controlled data
mockGraphClient({
'/me/onenote/pages': {
value: [{ id: 'p1', title: 'Meeting Notes 2024' }]
},
});

const client = new OneNoteClient('fake-token');
const found = await client.findPage('meeting notes');
expect(found?.id).toBe('p1');





34 tests cover HTML conversion, token handling, and all OneNote client operations.





How to Use It





CODE
git clone https://github.com/singhAmandeep007/onenote-mcp.git
cd onenote-mcp
npm install
npm run build
npm run auth # sign in with your Microsoft account
npm run verify # confirm it works





Add to Claude Desktop's config:



CODE
{
"mcpServers": {
"onenote": {
"command": "node",
"args": ["/path/to/onenote-mcp/dist/mcp-server.js"]
}
}
}





Then ask Claude: "What's in my OneNote notebooks?"





TL;DR



If you're building an MCP server that talks to Microsoft Graph:





  1. Use delegated, non-.All scopes — personal accounts silently fail with .All scopes


  2. Don't validate token format — personal accounts return non-JWT tokens that are valid


  3. Use Client.initWithMiddleware — the callback-based Client.init is deprecated


  4. Never write to stdout — MCP uses it for JSON-RPC; all logging goes to stderr


  5. Add Zod schemas to your tools — AI clients need parameter descriptions to use them correctly



The full source:







GitHub logo



MCP server for Microsoft OneNote






OneNote MCP Server




A TypeScript by Zubeid Hendricks.





Features





  • Device-code authentication — works with both personal Microsoft accounts and work/school (Azure AD) accounts

  • List notebooks, sections, and pages

  • Read page content as plain text (HTML is stripped automatically)

  • Create pages with HTML content

  • Search pages by title across all notebooks

  • Typed Zod schemas on every MCP tool for reliable AI integration

  • Unified CLI for scripting and debugging (onenote-cli)




Prerequisites






  • Node.js ≥ 18.18 (see .nvmrc)

  • A Microsoft account with access to OneNote




Quick Start






git clone https://github.com/danosb/onenote-mcp.git
cd onenote-mcp
npm install
npm run auth # sign in with your








PRs welcome.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Hackers Just Poisoned the Rust Supply Chain | Threat Wire
1 Quelle
Hackers Found a Way Into Humanoid Robots | Threat Wire
1 Quelle
Bits und so #1021 (Passwort für Laufwerk)
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten I Rewrote a OneNote MCP Server in TypeScript — Here's What I Learned About Microsoft Graph Auth

Thematisch verwandte Begriffe: Rewrote, OneNote, Server, TypeScript · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...