TypeScript로 OneNote MCP 서버를 다시 작성하며 배운 Microsoft Graph 인증에 관한 것들
요약
TypeScript를 사용하여 OneNote MCP 서버를 재구현하며 겪은 Microsoft Graph 인증 오류와 해결 방법을 다룹니다. 특히 개인 계정에서 발생하는 401 에러의 원인이 잘못된 권한 범위(Scopes) 설정에 있음을 설명합니다.
핵심 포인트
- MCP는 AI 어시스턴트가 외부 도구를 호출할 수 있게 하는 표준 프로토콜임
- 개인 계정 사용 시 .All 범위의 애플리케이션 권한은 사용할 수 없음
- 리소스가 지정된 위임된 범위(Delegated Scopes)를 사용하여 인증 문제를 해결함
- 개인 Microsoft 계정은 JWT가 아닌 불투명한(Opaque) 토큰을 반환함
Claude, Cursor, 또는 MCP 호환 AI 어시스턴트를 사용하고 있다면, AI가 실제로 당신의 노트를 읽을 수 있을 때 얼마나 유용한지 이미 느끼셨을 것입니다. 저는 OneNote에서도 그 기능을 구현하고 싶었습니다. 그래서 기존의 MCP 서버를 가져왔지만, 난해한 401 에러를 마주하게 되었고, 결국 전체를 TypeScript로 다시 작성하게 되었습니다.
제가 배운 점들을 공유합니다.
MCP 서버란 무엇인가?
Model Context Protocol (MCP)는 AI 어시스턴트가 JSON-RPC를 통해 외부 도구를 호출할 수 있게 해주는 표준입니다. 도구(예: listNotebooks 또는 createPage)를 노출하는 서버를 작성하면, Claude Desktop, Cursor, Claude Code와 같은 모든 MCP 클라이언트가 이를 호출할 수 있습니다.
서버는 stdio를 통해 통신합니다: stdout에는 JSON-RPC가, stderr에는 진단(diagnostics) 정보가 전달됩니다. 이는 나중에 중요하게 작용할 핵심적인 세부 사항입니다.
시작점
저는 Microsoft Graph를 사용하여 OneNote에 접근하는 JavaScript MCP 서버인 danosb/onenote-mcp로 시작했습니다. Azure 앱을 등록할 필요가 없는 디바이스 코드 인증(device-code auth) 방식을 사용하는 등 아이디어는 좋았지만, 저는 즉시 벽에 부딪혔습니다.
모든 것을 망가뜨린 401 에러
개인 Microsoft 계정으로 인증을 마친 후, 모든 Graph API 호출에서 다음과 같은 응답이 반환되었습니다:
HTTP 401 — "The request does not contain a valid authentication token"
(error code 40001)
토큰은 멀쩡해 보였습니다. 인증은 성공적으로 완료되었습니다. 하지만 Graph는 모든 요청을 거부했습니다.
근본 원인: .All 범위 (Scopes)
기존 서버는 다음과 같은 범위(scopes)를 요청했습니다:
const scopes = ['Notes.Read.All', 'Notes.ReadWrite.All', 'User.Read'];
이러한 .All 범위는 **애플리케이션 수준 권한 (application-level permissions)**입니다. 개인 Microsoft 계정(MSA)은 이에 동의할 수 없으며, 오직 Azure AD 업무/학교 계정만이 가능합니다. 개인 계정이 이를 사용하려고 하면 Azure는 토큰을 발급하긴 하지만, Microsoft Graph가 수락하지 않는 토큰을 발급하게 됩니다.
인증 과정 중에는 오류도, 경고도 없었습니다. 그저 이후의 모든 호출에서 조용히 401 에러가 발생할 뿐이었습니다.
해결 방법
.All 범위를 리소스가 지정된 위임된 범위(resource-qualified delegated scopes)로 교체합니다:
export const SCOPES = [
'https://graph.microsoft.com/Notes.Read',
'https://graph.microsoft.com/Notes.ReadWrite',
...
이것들은 개인 계정과 업무/학교 계정
_모두_에서 작동하는 위임된 범위 (delegated scopes)입니다. 이 범위들은 로그인한 사용자의 본인 OneNote 콘텐츠에 대한 액세스 권한을 부여하며, 이는 MCP 서버에 정확히 필요한 기능입니다.
보너스 주의사항: Non-JWT 토큰
개인 Microsoft 계정은 컴팩트 (non-JWT) 토큰을 반환합니다. 이는 header.payload.signature라는 세 부분의 구조를 갖지 않는 불투명한 문자열 (opaque strings)입니다. 만약 작성하신 코드가 JWT 여부를 확인하여 토큰 형식을 검증한다면, 완전히 유효한 개인 계정 토큰을 잘못된 것으로 판단하여 거부할 것입니다.
올바른 접근 방식은 토큰 형식을 아예 검증하지 않는 것입니다. 토큰이 유효한지에 대한 권한은 Microsoft Graph가 갖도록 하세요.
왜 전체를 TypeScript로 다시 작성했는가?
인증 버그를 해결한 후 코드베이스를 살펴보니 다음과 같은 문제점들을 발견했습니다:
- 중복된 기능을 수행하는 약 10개의 독립적인 JavaScript 파일들 (
simple-onenote.js,list-sections.js,list-pages.js,get-page.js,get-page-content.js,get-all-page-contents.js...) - 파라미터 스키마 (parameter schemas)가 없는 MCP 도구들 — 모든 데이터가
params.random_string을 통해 들어옴 - 단순히 HTML-to-text 변환을 위해
jsdom에 의존함 node-fetch에 의존함 (Node 18+ 이상의 네이티브 fetch를 사용하면 불필요함)Client.initWithMiddleware대신 지원이 중단된(deprecated)Client.init콜백 패턴 사용
그래서 저는 다시 작성했습니다.
새로운 아키텍처
모든 것은 명확한 분리를 통해 src/ 디렉토리에 위치합니다:
src/
config.ts — Client ID, tenant, scopes, paths
logger.ts — stderr 전용 로깅 (stdout = JSON-RPC)
...
주요 설계 결정 사항
1. 모든 도구에 Zod 스키마 적용
이전 서버는 스키마 없이 SDK의 tool() 메서드를 사용하여 파라미터가 params.random_string 형태로 들어왔습니다. 새로운 서버는 명시적인 Zod 스키마를 정의합니다:
server.tool(
'getPage',
'ID 또는 제목 검색을 통해 페이지 콘텐츠를 가져옵니다.',
...
이를 통해 AI 클라이언트에게 적절한 파라미터 설명과 타입 정보가 제공되어, 클라이언트가 무엇을 전달해야 하는지 정확히 알 수 있게 됩니다.
2. 흩어진 스크립트 대신 하나의 클라이언트 클래스 사용
OneNoteClient는 모든 Graph API 작업을 캡슐화(encapsulate)합니다:
const client = OneNoteClient.fromStoredToken();
const notebooks = await client.listNotebooks();
const page = await client.findPage("meeting notes");
...
3. 의존성 없는 HTML→text 변환
OneNote의 Graph API는 페이지 콘텐츠를 HTML 형식으로 반환합니다. 기존 코드에서는 이를 파싱하기 위해 jsdom(무거운 의존성)을 사용했습니다. 새로운 htmlToText() 함수는 정규 표현식(regex)을 사용하여 이를 처리합니다. 즉, script/style 블록을 제거하고, 블록 요소를 줄바꿈으로 변환하며, 엔티티(entities)를 디코딩하고, 공백을 압축합니다:
export function htmlToText(html: string): string {
return html
.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, ' ')
...
OneNote가 반환하는 구조화된 HTML의 경우, 이 방식만으로도 충분하며 거대한 의존성을 제거할 수 있습니다.
4. stderr 전용 로깅
이 부분은 미묘하지만 매우 중요합니다. MCP stdio 서버는 stdout을 오직 JSON-RPC 메시지 전송용으로만 사용합니다. 어떤 console.log() 호출이라도 프로토콜 스트림을 손상시켜 연결을 중단시킵니다. 모든 로깅은 stderr에 기록하는 log() 헬퍼 함수를 통해 이루어집니다:
export function log(...args: unknown[]): void {
console.error('[onenote-mcp]', ...args);
}
테스트 (Testing)
테스트 스위트는 모킹(mocked)된 Graph 클라이언트와 함께 Vitest를 사용하므로 실제 API 호출이 필요하지 않습니다:
vi.mock('../src/graph-client.js', () => ({
createGraphClient: vi.fn(),
}));
...
34개의 테스트가 HTML 변환, 토큰 처리, 그리고 모든 OneNote 클라이언트 작업을 커버합니다.
사용 방법 (How to Use It)
git clone https://github.com/singhAmandeep007/onenote-mcp.git
cd onenote-mcp
npm install
...
Claude Desktop 설정에 추가하세요. 이 방식은 빌드 단계 없이 tsx를 통해 TypeScript 소스를 직접 실행합니다:
{
"mcpServers": {
"onenote": {
...
Claude Desktop을 재시작하여 새로운 설정을 적용한 뒤, 다음과 같이 질문해 보세요: "내 OneNote 노트북에 무엇이 들어있지?"
요약 (TL;DR)
Microsoft Graph와 통신하는 MCP 서버를 구축하고 있다면:
- 위임된(delegated), non-
.All스코프를 사용하세요 — 개인 계정은.All스코프를 사용할 경우 조용히 실패합니다. - 토큰 형식을 검증하지 마세요 — 개인 계정은 유효하지만 JWT가 아닌 토큰을 반환합니다.
Client.initWithMiddleware를 사용하세요 — 콜백 기반의Client.init은 지원 중단(deprecated)되었습니다.- 절대로 stdout에 쓰지 마세요 — MCP는 이를 JSON-RPC 용도로 사용합니다. 모든 로깅은 stderr로 보내야 합니다.
- 도구(tools)에 Zod 스키마를 추가하세요 — AI 클라이언트가 도구를 올바르게 사용하려면 매개변수 설명이 필요합니다.
전체 소스:
GitHub logo singhAmandeep007 / onenote-mcp
Microsoft OneNote를 위한 MCP 서버
OneNote MCP Server
AI 어시스턴트(Claude, Cursor 등)가 Microsoft Graph API를 통해 사용자의 Microsoft OneNote 노트북에 읽기/쓰기 권한을 가질 수 있도록 하는 TypeScript Model Context Protocol (MCP) 서버입니다.
Azure 설정이 전혀 필요하지 않습니다 — 인증은 사전 동의된 퍼블릭 클라이언트(public client)와 함께 디바이스 코드 흐름(device-code flow)을 사용하므로, Microsoft 계정만 있으면 됩니다.
Zubeid Hendricks의 azure-onenote-mcp-server를 기반으로 합니다.
주요 기능 (Features)
- 디바이스 코드 인증 — 개인 Microsoft 계정과 업무/학교(Azure AD) 계정 모두 지원
- 노트북, 섹션 및 페이지 목록 조회
- 페이지 콘텐츠를 일반 텍스트로 읽기 (HTML은 자동으로 제거됨)
- HTML 콘텐츠를 포함한 페이지 생성
- 모든 노트북에서 제목으로 페이지 검색
- 신뢰할 수 있는 AI 통합을 위해 모든 MCP 도구에 타입이 지정된 Zod 스키마 적용
- 스크립팅 및 디버깅을 위한 통합 CLI (
onenote-cli)
사전 요구 사항 (Prerequisites)
- Node.js ≥ 18.18 (
.nvmrc는 v24.14.1로 고정됨) - OneNote에 접근 가능한 Microsoft 계정
빠른 시작 (Quick Start)
git clone https://github.com/danosb/onenote-mcp.git
cd onenote-mcp
npm install
npm run build # TypeScript 컴파일
npm
…
PR 환영합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기