Skip to main content
Glama

OneNote MCP Server

by eshlon
onenote-mcp.mjs33.4 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { Client } from '@microsoft/microsoft-graph-client'; import { DeviceCodeCredential } from '@azure/identity'; import { JSDOM } from 'jsdom'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import fetch from 'node-fetch'; import { z } from "zod"; // --- Configuration --- const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const tokenFilePath = path.join(__dirname, '.access-token.txt'); const clientId = process.env.AZURE_CLIENT_ID || '14d82eec-204b-4c2f-b7e8-296a70dab67e'; // Default: Microsoft Graph Explorer App ID const scopes = ['Notes.Read', 'Notes.ReadWrite', 'Notes.Create', 'User.Read']; // --- Global State --- let accessToken = null; let graphClient = null; // --- MCP Server Initialization --- const server = new McpServer({ name: 'onenote', version: '1.0.0', description: 'OneNote MCP Server - Read, Write, and Edit OneNote content.' }); // ============================================================================ // AUTHENTICATION & MICROSOFT GRAPH CLIENT MANAGEMENT // ============================================================================ /** * Loads an existing access token from the local file system. */ function loadExistingToken() { try { if (fs.existsSync(tokenFilePath)) { const tokenData = fs.readFileSync(tokenFilePath, 'utf8'); try { const parsedToken = JSON.parse(tokenData); // New format: JSON object accessToken = parsedToken.token; console.error('Loaded existing token from file (JSON format).'); } catch (parseError) { accessToken = tokenData; // Old format: plain token string console.error('Loaded existing token from file (plain text format).'); } } } catch (error) { console.error(`Error loading token: ${error.message}`); } } /** * Initializes the Microsoft Graph client if an access token is available. * @returns {Client | null} The initialized Graph client or null. */ function initializeGraphClient() { if (accessToken && !graphClient) { graphClient = Client.init({ authProvider: (done) => { done(null, accessToken); } }); console.error('Microsoft Graph client initialized.'); } return graphClient; } /** * Ensures the Graph client is initialized and authenticated. * Loads token if not present, then initializes client. * @throws {Error} If no access token is available after attempting to load. * @returns {Promise<Client>} The initialized and authenticated Graph client. */ async function ensureGraphClient() { if (!accessToken) { loadExistingToken(); } if (!accessToken) { throw new Error('No access token available. Please authenticate first using the "authenticate" tool.'); } if (!graphClient) { initializeGraphClient(); } return graphClient; } // ============================================================================ // HTML CONTENT PROCESSING UTILITIES // ============================================================================ /** * Extracts readable plain text from HTML content. * Removes scripts, styles, and formats headings, paragraphs, lists, and tables. * @param {string} html - The HTML content string. * @returns {string} The extracted readable text. */ function extractReadableText(html) { try { if (!html) return ''; const dom = new JSDOM(html); const document = dom.window.document; document.querySelectorAll('script, style').forEach(element => element.remove()); let text = ''; document.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => { const headingText = heading.textContent?.trim(); if (headingText) text += `\n${headingText}\n${'-'.repeat(headingText.length)}\n`; }); document.querySelectorAll('p').forEach(paragraph => { const content = paragraph.textContent?.trim(); if (content) text += `${content}\n\n`; }); document.querySelectorAll('ul, ol').forEach(list => { text += '\n'; list.querySelectorAll('li').forEach((item, index) => { const content = item.textContent?.trim(); if (content) text += `${list.tagName === 'OL' ? index + 1 + '.' : '-'} ${content}\n`; }); text += '\n'; }); document.querySelectorAll('table').forEach(table => { text += '\n📊 Table content:\n'; table.querySelectorAll('tr').forEach(row => { const cells = Array.from(row.querySelectorAll('td, th')) .map(cell => cell.textContent?.trim()) .join(' | '); if (cells.trim()) text += `${cells}\n`; }); text += '\n'; }); if (!text.trim() && document.body) { text = document.body.textContent?.trim().replace(/\s+/g, ' ') || ''; } return text.trim(); } catch (error) { console.error(`Error extracting readable text: ${error.message}`); return 'Error: Could not extract readable text from HTML content.'; } } /** * Extracts a short summary from HTML content. * @param {string} html - The HTML content string. * @param {number} [maxLength=300] - The maximum length of the summary. * @returns {string} A text summary. */ function extractTextSummary(html, maxLength = 300) { try { if (!html) return 'No content to summarize.'; const dom = new JSDOM(html); const document = dom.window.document; const bodyText = document.body?.textContent?.trim().replace(/\s+/g, ' ') || ''; if (!bodyText) return 'No text content found in HTML body.'; const summary = bodyText.substring(0, maxLength); return summary.length < bodyText.length ? `${summary}...` : summary; } catch (error) { console.error(`Error extracting text summary: ${error.message}`); return 'Could not extract text summary.'; } } /** * Converts plain text (with simple markdown) to HTML. * @param {string} text - The plain text to convert. * @returns {string} The HTML representation. */ function textToHtml(text) { if (!text) return ''; if (text.includes('<html>') || text.includes('<!DOCTYPE html>')) return text; // Already HTML let html = String(text) // Ensure text is a string .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') // Basic HTML escaping first .replace(/```([\s\S]*?)```/g, (match, code) => `<pre><code>${code.trim()}</code></pre>`) .replace(/`([^`]+)`/g, '<code>$1</code>') .replace(/^### (.+)$/gm, '<h3>$1</h3>') .replace(/^## (.+)$/gm, '<h2>$1</h2>') .replace(/^# (.+)$/gm, '<h1>$1</h1>') .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/__(.*?)__/g, '<strong>$1</strong>') .replace(/\*(.*?)\*/g, '<em>$1</em>').replace(/_(.*?)_/g, '<em>$1</em>') .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>') .replace(/^---+$/gm, '<hr>') .replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>') .replace(/^[\*\-\+] (.+)$/gm, '<li>$1</li>') .replace(/^(\d+)\. (.+)$/gm, '<li>$2</li>'); html = html.split('\n').map(line => { const trimmed = line.trim(); if (!trimmed) return ''; if (/^<(h[1-6]|li|hr|blockquote|pre|code|strong|em|a)/.test(trimmed) || /^<\/(h[1-6]|li|hr|blockquote|pre|code|strong|em|a)>/.test(trimmed)) { return trimmed; // Already an HTML element we processed or a closing tag } return `<p>${trimmed}</p>`; }).filter(line => line).join('\n'); html = html.replace(/(<li>.*?<\/li>(?:\s*<li>.*?<\/li>)*)/gs, '<ul>$1</ul>'); html = html.replace(/(<blockquote>.*?<\/blockquote>(?:\s*<blockquote>.*?<\/blockquote>)*)/gs, '<blockquote>$1</blockquote>'); return html; } // ============================================================================ // ONENOTE API UTILITIES // ============================================================================ /** * Fetches the content of a OneNote page. * @param {string} pageId - The ID of the page. * @param {'httpDirect' | 'direct'} [method='httpDirect'] - The method to use for fetching. * @returns {Promise<string>} The HTML content of the page. */ async function fetchPageContentAdvanced(pageId, method = 'httpDirect') { await ensureGraphClient(); if (method === 'httpDirect') { const url = `https://graph.microsoft.com/v1.0/me/onenote/pages/${pageId}/content`; const response = await fetch(url, { headers: { 'Authorization': `Bearer ${accessToken}` } }); if (!response.ok) throw new Error(`HTTP error fetching page content! Status: ${response.status} ${response.statusText}`); return await response.text(); } else { // 'direct' return await graphClient.api(`/me/onenote/pages/${pageId}/content`).get(); } } /** * Formats OneNote page information for display. * @param {object} page - The OneNote page object from Graph API. * @param {number | null} [index=null] - Optional index for numbered lists. * @returns {string} Formatted page information string. */ function formatPageInfo(page, index = null) { const prefix = index !== null ? `${index + 1}. ` : ''; return `${prefix}**${page.title}** ID: ${page.id} Created: ${new Date(page.createdDateTime).toLocaleDateString()} Modified: ${new Date(page.lastModifiedDateTime).toLocaleDateString()}`; } // ============================================================================ // MCP TOOL DEFINITIONS // ============================================================================ // --- Authentication Tools --- server.tool( 'authenticate', { // No input parameters expected for this tool }, async () => { try { console.error('Starting device code authentication...'); let deviceCodeInfo = null; const credential = new DeviceCodeCredential({ clientId: clientId, userPromptCallback: (info) => { deviceCodeInfo = info; console.error(`\n=== AUTHENTICATION REQUIRED ===\n${info.message}\n================================\n`); } }); const authPromise = credential.getToken(scopes); await new Promise(resolve => setTimeout(resolve, 2000)); // Allow time for userPromptCallback if (deviceCodeInfo) { const authMessage = `🔐 **AUTHENTICATION REQUIRED** Please complete the following steps: 1. **Open this URL in your browser:** https://microsoft.com/devicelogin 2. **Enter this code:** ${deviceCodeInfo.userCode} 3. **Sign in with your Microsoft account that has OneNote access.** 4. **After completing authentication, use the 'saveAccessToken' tool.** Token will be saved automatically upon successful browser authentication.`; authPromise.then(tokenResponse => { accessToken = tokenResponse.token; const tokenData = { token: accessToken, clientId: clientId, scopes: scopes, createdAt: new Date().toISOString(), expiresOn: tokenResponse.expiresOnTimestamp ? new Date(tokenResponse.expiresOnTimestamp).toISOString() : null }; fs.writeFileSync(tokenFilePath, JSON.stringify(tokenData, null, 2)); console.error('Token saved successfully!'); initializeGraphClient(); }).catch(error => { console.error(`Background authentication failed: ${error.message}`); }); return { content: [{ type: 'text', text: authMessage }] }; } else { return { isError: true, content: [{ type: 'text', text: 'Could not retrieve device code information. Please try again or check console logs.' }] }; } } catch (error) { return { isError: true, content: [{ type: 'text', text: `Authentication failed: ${error.message}` }] }; } } ); // Note: For the above tool, the Zod schema `z.object({}).describe(...)` was simplified to `{}` as per the user's specific finding // about the SDK's `server.tool(name, {param: z.type()}, handler)` signature. // If the SDK *does* support a top-level describe on the Zod object itself, that would be: // `z.object({}).describe('Start the authentication flow...')` server.tool( 'saveAccessToken', { // No input parameters }, async () => { try { loadExistingToken(); if (accessToken) { initializeGraphClient(); const testResponse = await graphClient.api('/me').get(); return { content: [{ type: 'text', text: `✅ **Authentication Successful!** Token loaded and verified. **Account Info:** - Name: ${testResponse.displayName || 'Unknown'} - Email: ${testResponse.userPrincipalName || 'Unknown'} 🚀 You can now use OneNote tools!` }] }; } else { return { isError: true, content: [{ type: 'text', text: `❌ **No Token Found.** Please run the 'authenticate' tool first.` }] }; } } catch (error) { return { isError: true, content: [{ type: 'text', text: `Failed to load or verify token: ${error.message}` }] }; } } ); // --- Page Reading Tools --- server.tool( 'listNotebooks', { // No input parameters }, async () => { try { await ensureGraphClient(); const response = await graphClient.api('/me/onenote/notebooks').get(); if (response.value && response.value.length > 0) { const notebookList = response.value.map((nb, i) => formatPageInfo(nb, i)).join('\n\n'); return { content: [{ type: 'text', text: `📚 **Your OneNote Notebooks** (${response.value.length} found):\n\n${notebookList}` }] }; } else { return { content: [{ type: 'text', text: '📚 No OneNote notebooks found.' }] }; } } catch (error) { return { isError: true, content: [{ type: 'text', text: error.message.includes('authenticate') ? '🔐 Authentication Required. Run `authenticate` tool.' : `Failed to list notebooks: ${error.message}` }] }; } } ); server.tool( 'searchPages', { query: z.string().describe('The search term for page titles.').optional() }, async ({ query }) => { try { await ensureGraphClient(); const apiResponse = await graphClient.api('/me/onenote/pages').get(); let pages = apiResponse.value || []; if (query) { const searchTerm = query.toLowerCase(); pages = pages.filter(page => page.title && page.title.toLowerCase().includes(searchTerm)); } if (pages.length > 0) { const pageList = pages.slice(0, 10).map((page, i) => formatPageInfo(page, i)).join('\n\n'); const morePages = pages.length > 10 ? `\n\n... and ${pages.length - 10} more pages.` : ''; return { content: [{ type: 'text', text: `🔍 **Search Results** ${query ? `for "${query}"` : ''} (${pages.length} found):\n\n${pageList}${morePages}` }] }; } else { return { content: [{ type: 'text', text: query ? `🔍 No pages found matching "${query}".` : '📄 No pages found.' }] }; } } catch (error) { return { isError: true, content: [{ type: 'text', text: `Failed to search pages: ${error.message}` }] }; } } ); server.tool( 'getPageContent', { pageId: z.string().describe('The ID of the page to retrieve content from.'), format: z.enum(['text', 'html', 'summary']) .default('text') .describe('Format of the content: text (readable), html (raw), or summary (brief).') .optional() }, async ({ pageId, format }) => { try { await ensureGraphClient(); const pageInfo = await graphClient.api(`/me/onenote/pages/${pageId}`).get(); const htmlContent = await fetchPageContentAdvanced(pageId, 'httpDirect'); let resultText = ''; if (format === 'html') { resultText = `📄 **${pageInfo.title}** (HTML Format)\n\n${htmlContent}`; } else if (format === 'summary') { const summary = extractTextSummary(htmlContent, 300); resultText = `📄 **${pageInfo.title}** (Summary)\n\n${summary}`; } else { // 'text' const textContent = extractReadableText(htmlContent); resultText = `📄 **${pageInfo.title}**\n📅 Modified: ${new Date(pageInfo.lastModifiedDateTime).toLocaleString()}\n\n${textContent}`; } return { content: [{ type: 'text', text: resultText }] }; } catch (error) { return { isError: true, content: [{ type: 'text', text: `Failed to get page content for ID "${pageId}": ${error.message}` }] }; } } ); server.tool( 'getPageByTitle', { title: z.string().describe('The title (or partial title) of the page to find.'), format: z.enum(['text', 'html', 'summary']) .default('text') .describe('Format of the content: text, html, or summary.') .optional() }, async ({ title, format }) => { try { await ensureGraphClient(); const pagesResponse = await graphClient.api('/me/onenote/pages').get(); const matchingPage = (pagesResponse.value || []).find(p => p.title && p.title.toLowerCase().includes(title.toLowerCase())); if (!matchingPage) { const availablePages = (pagesResponse.value || []).slice(0, 10).map(p => `- ${p.title}`).join('\n'); return { isError: true, content: [{ type: 'text', text: `❌ No page found with title containing "${title}".\n\nAvailable pages (up to 10):\n${availablePages || 'None'}` }] }; } const htmlContent = await fetchPageContentAdvanced(matchingPage.id, 'httpDirect'); let resultText = ''; if (format === 'html') { resultText = `📄 **${matchingPage.title}** (HTML Format)\n\n${htmlContent}`; } else if (format === 'summary') { const summary = extractTextSummary(htmlContent, 300); resultText = `📄 **${matchingPage.title}** (Summary)\n\n${summary}`; } else { // 'text' const textContent = extractReadableText(htmlContent); resultText = `📄 **${matchingPage.title}**\n📅 Modified: ${new Date(matchingPage.lastModifiedDateTime).toLocaleString()}\n\n${textContent}`; } return { content: [{ type: 'text', text: resultText }] }; } catch (error) { return { isError: true, content: [{ type: 'text', text: `Failed to get page by title "${title}": ${error.message}` }] }; } } ); // --- Page Editing & Content Manipulation Tools --- server.tool( 'updatePageContent', { pageId: z.string().describe('The ID of the page to update.'), content: z.string().describe('New page content (HTML or markdown-style text).'), preserveTitle: z.boolean() .default(true) .describe('Keep the original title (default: true).') .optional() }, async ({ pageId, content: newContent, preserveTitle }) => { try { await ensureGraphClient(); const pageInfo = await graphClient.api(`/me/onenote/pages/${pageId}`).get(); console.error(`Updating content for page: "${pageInfo.title}" (ID: ${pageId})`); const htmlContentForUpdate = textToHtml(newContent); const finalHtml = ` <div> ${preserveTitle ? `<h1>${pageInfo.title}</h1>` : ''} ${htmlContentForUpdate} <hr> <p><em>Updated via OneNote MCP on ${new Date().toLocaleString()}</em></p> </div> `; const url = `https://graph.microsoft.com/v1.0/me/onenote/pages/${pageId}/content`; const response = await fetch(url, { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify([{ target: 'body', action: 'replace', content: finalHtml }]) }); if (!response.ok) throw new Error(`Update failed: ${response.status} ${response.statusText}`); return { content: [{ type: 'text', text: `✅ **Page Content Updated!**\nPage: ${pageInfo.title}\nUpdated: ${new Date().toLocaleString()}\nContent Length: ${newContent.length} chars.` }] }; } catch (error) { return { isError: true, content: [{ type: 'text', text: `❌ Failed to update page content for ID "${pageId}": ${error.message}` }] }; } } ); server.tool( 'appendToPage', { pageId: z.string().describe('The ID of the page to append content to.'), content: z.string().describe('Content to append (HTML or markdown-style).'), addTimestamp: z.boolean().default(true).describe('Add a timestamp (default: true).').optional(), addSeparator: z.boolean().default(true).describe('Add a visual separator (default: true).').optional() }, async ({ pageId, content: newContent, addTimestamp, addSeparator }) => { try { await ensureGraphClient(); const pageInfo = await graphClient.api(`/me/onenote/pages/${pageId}`).get(); console.error(`Appending content to page: "${pageInfo.title}" (ID: ${pageId})`); const htmlContentToAppend = textToHtml(newContent); let appendHtml = ''; if (addSeparator) appendHtml += '<hr>'; if (addTimestamp) appendHtml += `<p><em>Added on ${new Date().toLocaleString()}</em></p>`; appendHtml += htmlContentToAppend; const url = `https://graph.microsoft.com/v1.0/me/onenote/pages/${pageId}/content`; const response = await fetch(url, { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify([{ target: 'body', action: 'append', content: appendHtml }]) }); if (!response.ok) throw new Error(`Append failed: ${response.status} ${response.statusText}`); return { content: [{ type: 'text', text: `✅ **Content Appended!**\nPage: ${pageInfo.title}\nAppended: ${new Date().toLocaleString()}\nLength: ${newContent.length} chars.` }] }; } catch (error) { return { isError: true, content: [{ type: 'text', text: `❌ Failed to append content to page ID "${pageId}": ${error.message}` }] }; } } ); server.tool( 'updatePageTitle', { pageId: z.string().describe('The ID of the page whose title is to be updated.'), newTitle: z.string().describe('The new title for the page.') }, async ({ pageId, newTitle }) => { try { await ensureGraphClient(); const pageInfo = await graphClient.api(`/me/onenote/pages/${pageId}`).get(); const oldTitle = pageInfo.title; console.error(`Updating page title from "${oldTitle}" to "${newTitle}" for page ID "${pageId}"`); const url = `https://graph.microsoft.com/v1.0/me/onenote/pages/${pageId}/content`; const response = await fetch(url, { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify([{ target: 'title', action: 'replace', content: newTitle }]) }); if (!response.ok) throw new Error(`Title update failed: ${response.status} ${response.statusText}`); return { content: [{ type: 'text', text: `✅ **Page Title Updated!**\nOld Title: ${oldTitle}\nNew Title: ${newTitle}\nUpdated: ${new Date().toLocaleString()}` }] }; } catch (error) { return { isError: true, content: [{ type: 'text', text: `❌ Failed to update page title for ID "${pageId}": ${error.message}` }] }; } } ); server.tool( 'replaceTextInPage', { pageId: z.string().describe('The ID of the page to modify.'), findText: z.string().describe('The text to find and replace.'), replaceText: z.string().describe('The text to replace with.'), caseSensitive: z.boolean().default(false).describe('Case-sensitive search (default: false).').optional() }, async ({ pageId, findText, replaceText, caseSensitive }) => { try { await ensureGraphClient(); const pageInfo = await graphClient.api(`/me/onenote/pages/${pageId}`).get(); const htmlContent = await fetchPageContentAdvanced(pageId, 'httpDirect'); console.error(`Replacing text in page: "${pageInfo.title}" (ID: ${pageId})`); const flags = caseSensitive ? 'g' : 'gi'; const regex = new RegExp(findText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags); const matches = (htmlContent.match(regex) || []).length; if (matches === 0) { return { content: [{ type: 'text', text: `ℹ️ **No matches found** for "${findText}" in page: ${pageInfo.title}.` }] }; } const updatedContent = htmlContent.replace(regex, replaceText); const url = `https://graph.microsoft.com/v1.0/me/onenote/pages/${pageId}/content`; const response = await fetch(url, { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify([{ target: 'body', action: 'replace', content: `<div>${updatedContent}</div>` }]) }); if (!response.ok) throw new Error(`Replace failed: ${response.status} ${response.statusText}`); return { content: [{ type: 'text', text: `✅ **Text Replaced!**\nPage: ${pageInfo.title}\nFound: "${findText}" (${matches} occurrences)\nReplaced with: "${replaceText}".` }] }; } catch (error) { return { isError: true, content: [{ type: 'text', text: `❌ Failed to replace text in page ID "${pageId}": ${error.message}` }] }; } } ); server.tool( 'addNoteToPage', { pageId: z.string().describe('The ID of the page to add a note to.'), note: z.string().describe('The note/comment content.'), noteType: z.enum(['note', 'todo', 'important', 'question']) .default('note') .describe('Type of note (note, todo, important, question).') .optional(), position: z.enum(['top', 'bottom']) .default('bottom') .describe('Position to add the note (top or bottom).') .optional() }, async ({ pageId, note, noteType, position }) => { try { await ensureGraphClient(); const pageInfo = await graphClient.api(`/me/onenote/pages/${pageId}`).get(); console.error(`Adding ${noteType} to page: "${pageInfo.title}" (ID: ${pageId}) at ${position}`); const icons = { note: '📝', todo: '✅', important: '🚨', question: '❓' }; const colors = { note: '#e3f2fd', todo: '#e8f5e8', important: '#ffebee', question: '#fff3e0' }; const noteHtml = ` <div style="border-left: 4px solid #2196f3; background-color: ${colors[noteType]}; padding: 10px; margin: 10px 0;"> <p><strong>${icons[noteType]} ${noteType.charAt(0).toUpperCase() + noteType.slice(1)}</strong> - <em>${new Date().toLocaleString()}</em></p> <p>${textToHtml(note)}</p> </div>`; const action = position === 'top' ? 'prepend' : 'append'; const url = `https://graph.microsoft.com/v1.0/me/onenote/pages/${pageId}/content`; const response = await fetch(url, { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify([{ target: 'body', action: action, content: noteHtml }]) }); if (!response.ok) throw new Error(`Add note failed: ${response.status} ${response.statusText}`); return { content: [{ type: 'text', text: `✅ **${noteType.charAt(0).toUpperCase() + noteType.slice(1)} Added!**\nPage: ${pageInfo.title}\nPosition: ${position}.` }] }; } catch (error) { return { isError: true, content: [{ type: 'text', text: `❌ Failed to add note to page ID "${pageId}": ${error.message}` }] }; } } ); server.tool( 'addTableToPage', { pageId: z.string().describe('The ID of the page to add a table to.'), tableData: z.string().describe('Table data in CSV format (header row, then data rows).'), title: z.string().describe('Optional title for the table.').optional(), position: z.enum(['top', 'bottom']) .default('bottom') .describe('Position to add the table (top or bottom).') .optional() }, async ({ pageId, tableData, title, position }) => { try { await ensureGraphClient(); const pageInfo = await graphClient.api(`/me/onenote/pages/${pageId}`).get(); console.error(`Adding table to page: "${pageInfo.title}" (ID: ${pageId}) at ${position}`); const rows = tableData.trim().split('\n').map(row => row.split(',').map(cell => cell.trim())); if (rows.length < 2) throw new Error('Table data must have at least a header row and one data row.'); const headerRow = rows[0]; const dataRows = rows.slice(1); let tableHtml = title ? `<h3>📊 ${textToHtml(title)}</h3>` : ''; tableHtml += `<table style="border-collapse: collapse; width: 100%; margin: 10px 0;"><thead><tr style="background-color: #f5f5f5;">${headerRow.map(cell => `<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">${textToHtml(cell)}</th>`).join('')}</tr></thead><tbody>${dataRows.map(row => `<tr>${row.map(cell => `<td style="border: 1px solid #ddd; padding: 8px;">${textToHtml(cell)}</td>`).join('')}</tr>`).join('')}</tbody></table>`; const action = position === 'top' ? 'prepend' : 'append'; const url = `https://graph.microsoft.com/v1.0/me/onenote/pages/${pageId}/content`; const response = await fetch(url, { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify([{ target: 'body', action: action, content: tableHtml }]) }); if (!response.ok) throw new Error(`Add table failed: ${response.status} ${response.statusText}`); return { content: [{ type: 'text', text: `✅ **Table Added!**\nPage: ${pageInfo.title}\nTitle: ${title || 'Untitled'}\nPosition: ${position}.` }] }; } catch (error) { return { isError: true, content: [{ type: 'text', text: `❌ Failed to add table to page ID "${pageId}": ${error.message}` }] }; } } ); // --- Page Creation Tool --- server.tool( 'createPage', { title: z.string().min(1, { message: "Title cannot be empty." }).describe('The title for the new page.'), content: z.string().min(1, { message: "Content cannot be empty." }).describe('The content for the new page (HTML or markdown-style).') }, async ({ title, content }) => { try { await ensureGraphClient(); console.error(`Attempting to create page with title: "${title}"`); const sectionsResponse = await graphClient.api('/me/onenote/sections').get(); if (!sectionsResponse.value || sectionsResponse.value.length === 0) { throw new Error('No sections found in your OneNote. Cannot create a page.'); } const targetSectionId = sectionsResponse.value[0].id; const targetSectionName = sectionsResponse.value[0].displayName; const htmlContent = textToHtml(content); const pageHtml = `<!DOCTYPE html> <html> <head> <title>${textToHtml(title)}</title> <meta charset="utf-8"> </head> <body> <h1>${textToHtml(title)}</h1> ${htmlContent} <hr> <p><em>Created via OneNote MCP on ${new Date().toLocaleString()}</em></p> </body> </html>`; const response = await graphClient .api(`/me/onenote/sections/${targetSectionId}/pages`) .header('Content-Type', 'application/xhtml+xml') .post(pageHtml); return { content: [{ type: 'text', text: `✅ **Page Created Successfully!** **Title:** ${response.title} **Page ID:** ${response.id} **In Section:** ${targetSectionName} **Created:** ${new Date(response.createdDateTime).toLocaleString()}` }] }; } catch (error) { console.error(`CREATE PAGE ERROR: ${error.message}`, error.stack); return { isError: true, content: [{ type: 'text', text: `❌ **Error creating page:** ${error.message}` }] }; } } ); // ============================================================================ // SERVER STARTUP // ============================================================================ /** * Main function to initialize and start the MCP server. */ async function main() { loadExistingToken(); // Attempt to load token at startup if (accessToken) { initializeGraphClient(); // Initialize client if token was loaded } try { const transport = new StdioServerTransport(); await server.connect(transport); console.error('🚀✨ OneNote Ultimate MCP Server is now LIVE! ✨🚀'); console.error(` Client ID: ${clientId.substring(0, 8)}... (Using ${process.env.AZURE_CLIENT_ID ? 'environment variable' : 'default'})`); console.error(' Ready to manage your OneNote like never before!'); console.error('--- Available Tool Categories ---'); console.error(' 🔐 Auth: authenticate, saveAccessToken'); console.error(' 📚 Read: listNotebooks, searchPages, getPageContent, getPageByTitle'); console.error(' ✏️ Edit: updatePageContent, appendToPage, updatePageTitle, replaceTextInPage, addNoteToPage, addTableToPage'); console.error(' ➕ Create: createPage'); console.error('---------------------------------'); process.on('SIGINT', () => { console.error('\n🔌 OneNote MCP Server shutting down gracefully...'); process.exit(0); }); process.on('SIGTERM', () => { console.error('\n🔌 OneNote MCP Server terminated...'); process.exit(0); }); } catch (error) { console.error(`💀 Critical error starting server: ${error.message}`, error.stack); process.exit(1); } } main();

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/eshlon/onenotemcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server