Skip to main content
Glama

Aladin Book Search MCP Server

by TeeDDub
index.ts28.3 kB
#!/usr/bin/env node /** * 알라딘 도서 검색 MCP 서버 (Node.js) */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import axios from 'axios'; import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; // ES 모듈에서 __dirname 대체 const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // 카테고리 정보 로드 const categoryDataPath = path.join(__dirname, 'aladin_book_categories.json'); const categoryData = JSON.parse(fs.readFileSync(categoryDataPath, 'utf8')); // 환경 변수 설정 const ALADIN_BASE_URL = 'https://www.aladin.co.kr/ttb/api'; const ALADIN_TTB_KEY = process.env.ALADIN_TTB_KEY?.trim(); // 로깅 설정 (stderr 사용하여 JSON-RPC 충돌 방지) const logger = { error: (message: string) => console.error(`[ERROR] ${message}`) }; // 카테고리 정보 인터페이스 interface CategoryInfo { cid: string; name: string; mall: string; } interface CategoryNode { name: string; level: number; children?: { [key: string]: CategoryNode }; categories?: CategoryInfo[]; } // 카테고리 검색 함수 (레벨 우선순위 적용) function searchCategories(searchTerm: string, data: any): CategoryInfo[] { const results: Array<CategoryInfo & { level: number; fullPath: string }> = []; function traverse(node: CategoryNode, path: string[] = [], currentLevel: number = 1) { // 현재 노드의 카테고리 정보 확인 if (node.categories) { for (const category of node.categories) { if (category.name.toLowerCase().includes(searchTerm.toLowerCase())) { results.push({ ...category, level: currentLevel, fullPath: `${path.join(' > ')} > ${category.name}`, name: category.name // 원본 이름 유지 }); } } } // 자식 노드 순회 if (node.children) { for (const [childName, childNode] of Object.entries(node.children)) { traverse(childNode, [...path, childName], currentLevel + 1); } } } for (const [rootName, rootNode] of Object.entries(data)) { traverse(rootNode as CategoryNode, [rootName]); } // 레벨별로 정렬 (level 1이 가장 우선, 같은 레벨에서는 이름순) results.sort((a, b) => { if (a.level !== b.level) { return a.level - b.level; } return a.name.localeCompare(b.name); }); // 결과 반환 시 fullPath를 name으로 설정 return results.map(result => ({ cid: result.cid, name: result.fullPath, mall: result.mall })); } // 전체 카테고리 목록 추출 함수 function getAllCategories(data: any): CategoryInfo[] { const results: CategoryInfo[] = []; function traverse(node: CategoryNode, path: string[] = []) { if (node.categories) { for (const category of node.categories) { results.push({ ...category, name: `${path.join(' > ')} > ${category.name}` }); } } if (node.children) { for (const [childName, childNode] of Object.entries(node.children)) { traverse(childNode, [...path, childName]); } } } for (const [rootName, rootNode] of Object.entries(data)) { traverse(rootNode as CategoryNode, [rootName]); } return results; } // 도서 검색 결과 타입 정의 interface BookSearchResult { title: string; author: string; publisher: string; pubDate: string; isbn: string; isbn13: string; cover: string; categoryName: string; description: string; priceStandard: number; priceSales: number; link: string; pages?: number; // 페이지 수 추가 pricePerPage?: number; // 쪽단가 추가 } interface BookDetailResult extends BookSearchResult { fullDescription: string; customerReviewRank?: number; } // 알라딘 API 호출 함수 async function callAladinApi(endpoint: string, params: Record<string, any>): Promise<any> { if (!ALADIN_TTB_KEY || ALADIN_TTB_KEY.length === 0) { throw new Error('알라딘 API 키가 설정되지 않았습니다. ALADIN_TTB_KEY 환경변수를 올바르게 설정해주세요.'); } const baseParams = { ttbkey: ALADIN_TTB_KEY, output: 'js', version: '20131101' }; const finalParams = { ...baseParams, ...params }; const url = `${ALADIN_BASE_URL}/${endpoint}`; try { const response = await axios.get(url, { params: finalParams }); return response.data; } catch (error) { logger.error(`알라딘 API 호출 오류: ${error}`); throw error; } } // MCP 서버 생성 const server = new McpServer({ name: '알라딘 도서 검색', version: '1.0.0' }); // 도서 검색 도구 등록 server.registerTool( 'search_books', { title: '도서 검색', description: '알라딘에서 도서를 검색합니다.', inputSchema: { query: z.string().describe('검색어'), searchType: z.enum(['Title', 'Author', 'Publisher', 'Keyword']).default('Title').describe('검색 타입'), maxResults: z.number().min(1).max(100).default(10).describe('최대 결과 개수'), start: z.number().min(1).default(1).describe('검색 시작 번호') } }, async ({ query, searchType, maxResults, start }) => { try { if (!query) { throw new Error('검색어를 입력해주세요.'); } const params = { Query: query, QueryType: searchType, MaxResults: maxResults, start: start, SearchTarget: 'Book', Cover: 'Big' }; const result = await callAladinApi('ItemSearch.aspx', params); const books: BookSearchResult[] = result.item?.map((item: any) => ({ title: item.title || '', author: item.author || '', publisher: item.publisher || '', pubDate: item.pubDate || '', isbn: item.isbn || '', isbn13: item.isbn13 || '', cover: item.cover || '', categoryName: item.categoryName || '', description: item.description || '', priceStandard: item.priceStandard || 0, priceSales: item.priceSales || 0, link: item.link || '', pages: item.subInfo?.itemPage || undefined, pricePerPage: (item.priceStandard > 0 && item.subInfo?.itemPage > 0) ? parseFloat((item.priceStandard / item.subInfo.itemPage).toFixed(2)) : undefined })) || []; return { content: [{ type: 'text', text: `📚 도서 검색 결과 (${query})\n\n검색된 도서 수: ${books.length}권\n\n${JSON.stringify(books, null, 2)}` }] }; } catch (error) { logger.error(`도서 검색 중 오류 발생: ${error}`); return { content: [{ type: 'text', text: `도서 검색 중 오류가 발생했습니다: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // 도서 상세 정보 도구 등록 server.registerTool( 'get_book_detail', { title: '도서 상세 정보', description: 'ISBN을 이용해 도서의 상세 정보를 조회합니다.', inputSchema: { isbn: z.string().describe('도서의 ISBN (10자리 또는 13자리)') } }, async ({ isbn }) => { try { if (!isbn) { throw new Error('ISBN을 입력해주세요.'); } const params = { ItemId: isbn, ItemIdType: 'ISBN', Cover: 'Big', OptResult: 'description,fulldescription,ratingInfo,subInfo' }; const result = await callAladinApi('ItemLookUp.aspx', params); const items = result.item || []; if (items.length === 0) { throw new Error('해당 ISBN의 도서를 찾을 수 없습니다.'); } const item = items[0]; const bookDetail: BookDetailResult = { title: item.title || '', author: item.author || '', publisher: item.publisher || '', pubDate: item.pubDate || '', isbn: item.isbn || '', isbn13: item.isbn13 || '', cover: item.cover || '', categoryName: item.categoryName || '', description: item.description || '', fullDescription: item.fullDescription || '', priceStandard: item.priceStandard || 0, priceSales: item.priceSales || 0, link: item.link || '', customerReviewRank: item.customerReviewRank, pages: item.subInfo?.itemPage || undefined, pricePerPage: (item.priceStandard > 0 && item.subInfo?.itemPage > 0) ? parseFloat((item.priceStandard / item.subInfo.itemPage).toFixed(2)) : undefined }; return { content: [{ type: 'text', text: `📚 도서 상세 정보 (ISBN: ${isbn})\n\n${JSON.stringify(bookDetail, null, 2)}` }] }; } catch (error) { logger.error(`도서 상세 정보 조회 중 오류 발생: ${error}`); return { content: [{ type: 'text', text: `도서 상세 정보 조회 중 오류가 발생했습니다: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // 베스트셀러 도구 등록 server.registerTool( 'get_bestsellers', { title: '도서 베스트셀러', description: '알라딘 도서 베스트셀러 목록을 조회합니다. 카테고리별로 검색할 수 있으며, 특정 주간의 베스트셀러를 조회할 수 있습니다. 특정 주간을 조회할 때는 연도, 월, 주를 모두 입력해주세요.', inputSchema: { maxResults: z.number().min(1).max(100).default(10).describe('최대 결과 개수'), start: z.number().min(1).default(1).describe('검색 시작 번호'), categoryId: z.string().optional().describe('카테고리 ID (CID) - 특정 카테고리로 검색을 제한할 때 사용'), year: z.number().min(2000).max(2030).optional().describe('조회할 연도 (예: 2025) - 생략하면 현재 주간'), month: z.number().min(1).max(12).optional().describe('조회할 월 (1-12) - 생략하면 현재 주간'), week: z.number().min(1).max(5).optional().describe('조회할 주 (1-5) - 생략하면 현재 주간') } }, async ({ maxResults, start, categoryId, year, month, week }) => { try { const params: any = { QueryType: 'Bestseller', MaxResults: maxResults, start: start, SearchTarget: 'Book', Cover: 'Big' }; if (categoryId) { params.CategoryId = categoryId; } if (year) { params.Year = year; } if (month) { params.Month = month; } if (week) { params.Week = week; } const result = await callAladinApi('ItemList.aspx', params); const books: BookSearchResult[] = result.item?.map((item: any) => ({ title: item.title || '', author: item.author || '', publisher: item.publisher || '', pubDate: item.pubDate || '', isbn: item.isbn || '', isbn13: item.isbn13 || '', cover: item.cover || '', categoryName: item.categoryName || '', description: item.description || '', priceStandard: item.priceStandard || 0, priceSales: item.priceSales || 0, link: item.link || '', pages: item.subInfo?.itemPage || undefined, pricePerPage: (item.priceStandard > 0 && item.subInfo?.itemPage > 0) ? parseFloat((item.priceStandard / item.subInfo.itemPage).toFixed(2)) : undefined })) || []; const categoryText = categoryId ? ` (카테고리: ${categoryId})` : ''; const timeText = (year && month && week) ? ` (${year}년 ${month}월 ${week}주)` : ''; return { content: [{ type: 'text', text: `📈 베스트셀러 목록${categoryText}${timeText}\n\n검색된 도서 수: ${books.length}권\n\n${JSON.stringify(books, null, 2)}` }] }; } catch (error) { logger.error(`베스트셀러 조회 중 오류 발생: ${error}`); return { content: [{ type: 'text', text: `베스트셀러 조회 중 오류가 발생했습니다: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // 신간 전체 리스트 도구 등록 server.registerTool( 'get_new_books', { title: '신간 전체 리스트', description: '알라딘 신간 전체 리스트를 조회합니다. 카테고리별로 검색할 수 있습니다.', inputSchema: { maxResults: z.number().min(1).max(100).default(10).describe('최대 결과 개수'), start: z.number().min(1).default(1).describe('검색 시작 번호'), categoryId: z.string().optional().describe('카테고리 ID (CID) - 특정 카테고리로 검색을 제한할 때 사용') } }, async ({ maxResults, start, categoryId }) => { try { const params: any = { QueryType: 'ItemNewAll', MaxResults: maxResults, start: start, SearchTarget: 'Book', Cover: 'Big' }; if (categoryId) { params.CategoryId = categoryId; } const result = await callAladinApi('ItemList.aspx', params); const books: BookSearchResult[] = result.item?.map((item: any) => ({ title: item.title || '', author: item.author || '', publisher: item.publisher || '', pubDate: item.pubDate || '', isbn: item.isbn || '', isbn13: item.isbn13 || '', cover: item.cover || '', categoryName: item.categoryName || '', description: item.description || '', priceStandard: item.priceStandard || 0, priceSales: item.priceSales || 0, link: item.link || '', pages: item.subInfo?.itemPage || undefined, pricePerPage: (item.priceStandard > 0 && item.subInfo?.itemPage > 0) ? parseFloat((item.priceStandard / item.subInfo.itemPage).toFixed(2)) : undefined })) || []; const categoryText = categoryId ? ` (카테고리: ${categoryId})` : ''; return { content: [{ type: 'text', text: `🆕 신간 전체 리스트${categoryText}\n\n검색된 도서 수: ${books.length}권\n\n${JSON.stringify(books, null, 2)}` }] }; } catch (error) { logger.error(`신간 전체 리스트 조회 중 오류 발생: ${error}`); return { content: [{ type: 'text', text: `신간 전체 리스트 조회 중 오류가 발생했습니다: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // 주목할 만한 신간 리스트 도구 등록 server.registerTool( 'get_special_new_books', { title: '주목할 만한 신간 리스트', description: '알라딘 주목할 만한 신간 리스트를 조회합니다. 카테고리별로 검색할 수 있습니다.', inputSchema: { maxResults: z.number().min(1).max(100).default(10).describe('최대 결과 개수'), start: z.number().min(1).default(1).describe('검색 시작 번호'), categoryId: z.string().optional().describe('카테고리 ID (CID) - 특정 카테고리로 검색을 제한할 때 사용') } }, async ({ maxResults, start, categoryId }) => { try { const params: any = { QueryType: 'ItemNewSpecial', MaxResults: maxResults, start: start, SearchTarget: 'Book', Cover: 'Big' }; if (categoryId) { params.CategoryId = categoryId; } const result = await callAladinApi('ItemList.aspx', params); const books: BookSearchResult[] = result.item?.map((item: any) => ({ title: item.title || '', author: item.author || '', publisher: item.publisher || '', pubDate: item.pubDate || '', isbn: item.isbn || '', isbn13: item.isbn13 || '', cover: item.cover || '', categoryName: item.categoryName || '', description: item.description || '', priceStandard: item.priceStandard || 0, priceSales: item.priceSales || 0, link: item.link || '', pages: item.subInfo?.itemPage || undefined, pricePerPage: (item.priceStandard > 0 && item.subInfo?.itemPage > 0) ? parseFloat((item.priceStandard / item.subInfo.itemPage).toFixed(2)) : undefined })) || []; const categoryText = categoryId ? ` (카테고리: ${categoryId})` : ''; return { content: [{ type: 'text', text: `⭐ 주목할 만한 신간 리스트${categoryText}\n\n검색된 도서 수: ${books.length}권\n\n${JSON.stringify(books, null, 2)}` }] }; } catch (error) { logger.error(`주목할 만한 신간 리스트 조회 중 오류 발생: ${error}`); return { content: [{ type: 'text', text: `주목할 만한 신간 리스트 조회 중 오류가 발생했습니다: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // 편집자 추천 리스트 도구 등록 server.registerTool( 'get_editor_choice', { title: '편집자 추천 리스트', description: '알라딘 편집자 추천 리스트를 조회합니다. 카테고리별로 검색할 수 있습니다.', inputSchema: { maxResults: z.number().min(1).max(100).default(10).describe('최대 결과 개수'), start: z.number().min(1).default(1).describe('검색 시작 번호'), categoryId: z.string().optional().describe('카테고리 ID (CID) - 특정 카테고리로 검색을 제한할 때 사용') } }, async ({ maxResults, start, categoryId }) => { try { const params: any = { QueryType: 'ItemEditorChoice', MaxResults: maxResults, start: start, SearchTarget: 'Book', Cover: 'Big' }; if (categoryId) { params.CategoryId = categoryId; } const result = await callAladinApi('ItemList.aspx', params); const books: BookSearchResult[] = result.item?.map((item: any) => ({ title: item.title || '', author: item.author || '', publisher: item.publisher || '', pubDate: item.pubDate || '', isbn: item.isbn || '', isbn13: item.isbn13 || '', cover: item.cover || '', categoryName: item.categoryName || '', description: item.description || '', priceStandard: item.priceStandard || 0, priceSales: item.priceSales || 0, link: item.link || '', pages: item.subInfo?.itemPage || undefined, pricePerPage: (item.priceStandard > 0 && item.subInfo?.itemPage > 0) ? parseFloat((item.priceStandard / item.subInfo.itemPage).toFixed(2)) : undefined })) || []; const categoryText = categoryId ? ` (카테고리: ${categoryId})` : ''; return { content: [{ type: 'text', text: `👨‍💼 편집자 추천 리스트${categoryText}\n\n검색된 도서 수: ${books.length}권\n\n${JSON.stringify(books, null, 2)}` }] }; } catch (error) { logger.error(`편집자 추천 리스트 조회 중 오류 발생: ${error}`); return { content: [{ type: 'text', text: `편집자 추천 리스트 조회 중 오류가 발생했습니다: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // 블로거 베스트셀러 도구 등록 server.registerTool( 'get_blogger_best', { title: '블로거 베스트셀러', description: '알라딘 블로거 베스트셀러 목록을 조회합니다. 국내도서만 조회 가능하며, 카테고리별로 검색할 수 있습니다.', inputSchema: { maxResults: z.number().min(1).max(100).default(10).describe('최대 결과 개수'), start: z.number().min(1).default(1).describe('검색 시작 번호'), categoryId: z.string().optional().describe('카테고리 ID (CID) - 특정 카테고리로 검색을 제한할 때 사용') } }, async ({ maxResults, start, categoryId }) => { try { const params: any = { QueryType: 'BlogBest', MaxResults: maxResults, start: start, SearchTarget: 'Book', Cover: 'Big' }; if (categoryId) { params.CategoryId = categoryId; } const result = await callAladinApi('ItemList.aspx', params); const books: BookSearchResult[] = result.item?.map((item: any) => ({ title: item.title || '', author: item.author || '', publisher: item.publisher || '', pubDate: item.pubDate || '', isbn: item.isbn || '', isbn13: item.isbn13 || '', cover: item.cover || '', categoryName: item.categoryName || '', description: item.description || '', priceStandard: item.priceStandard || 0, priceSales: item.priceSales || 0, link: item.link || '', pages: item.subInfo?.itemPage || undefined, pricePerPage: (item.priceStandard > 0 && item.subInfo?.itemPage > 0) ? parseFloat((item.priceStandard / item.subInfo.itemPage).toFixed(2)) : undefined })) || []; const categoryText = categoryId ? ` (카테고리: ${categoryId})` : ''; return { content: [{ type: 'text', text: `📝 블로거 베스트셀러${categoryText} (국내도서만)\n\n검색된 도서 수: ${books.length}권\n\n${JSON.stringify(books, null, 2)}` }] }; } catch (error) { logger.error(`블로거 베스트셀러 조회 중 오류 발생: ${error}`); return { content: [{ type: 'text', text: `블로거 베스트셀러 조회 중 오류가 발생했습니다: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // 카테고리 검색 도구 등록 server.registerTool( 'search_categories', { title: '카테고리 검색', description: '알라딘 도서 카테고리를 검색합니다. 상위 레벨 카테고리를 우선으로 표시합니다.', inputSchema: { searchTerm: z.string().describe('검색할 카테고리 이름'), maxResults: z.number().min(1).max(50).default(20).describe('최대 결과 개수') } }, async ({ searchTerm, maxResults }) => { try { const allCategories = searchCategories(searchTerm, categoryData); const categories = allCategories.slice(0, maxResults); if (categories.length === 0) { return { content: [{ type: 'text', text: `'${searchTerm}'과 관련된 카테고리를 찾을 수 없습니다.` }] }; } // 레벨별로 그룹화 const levelGroups: { [level: string]: any[] } = {}; categories.forEach(cat => { const level = cat.name.split(' > ').length - 1; if (!levelGroups[level]) { levelGroups[level] = []; } levelGroups[level].push(cat); }); let resultText = `'${searchTerm}' 검색 결과 (${categories.length}개${allCategories.length > maxResults ? `, 총 ${allCategories.length}개 중 상위 ${maxResults}개 표시` : ''}):\n\n`; // 레벨별로 표시 Object.keys(levelGroups).sort((a, b) => Number(a) - Number(b)).forEach(level => { const levelName = level === '1' ? '대분류' : level === '2' ? '중분류' : '소분류'; resultText += `📚 ${levelName} (Level ${level}):\n`; levelGroups[level].forEach((cat, index) => { resultText += `${index + 1}. ${cat.name}\n`; resultText += ` CID: ${cat.cid} | 몰: ${cat.mall}\n`; }); resultText += '\n'; }); return { content: [{ type: 'text', text: resultText }] }; } catch (error) { logger.error(`카테고리 검색 중 오류 발생: ${error}`); return { content: [{ type: 'text', text: `카테고리 검색 중 오류가 발생했습니다: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // 인기 카테고리 목록 도구 등록 server.registerTool( 'get_popular_categories', { title: '인기 카테고리', description: '자주 사용되는 주요 카테고리 목록을 조회합니다. 상위 레벨 카테고리를 우선으로 표시합니다.', inputSchema: { limit: z.number().min(1).max(50).default(20).describe('표시할 카테고리 개수') } }, async ({ limit }) => { try { // 주요 카테고리들을 수동으로 선별 const popularCategories = [ { name: '소설', searchTerm: '소설' }, { name: '에세이', searchTerm: '에세이' }, { name: '자기계발', searchTerm: '자기계발' }, { name: '경제경영', searchTerm: '경제경영' }, { name: '시/에세이', searchTerm: '시' }, { name: '인문학', searchTerm: '인문학' }, { name: '역사', searchTerm: '역사' }, { name: '철학', searchTerm: '철학' }, { name: '과학', searchTerm: '과학' }, { name: '건강', searchTerm: '건강' }, { name: '요리', searchTerm: '요리' }, { name: '육아', searchTerm: '육아' }, { name: '교육', searchTerm: '교육' }, { name: '컴퓨터', searchTerm: '컴퓨터' }, { name: '외국어', searchTerm: '외국어' }, { name: '여행', searchTerm: '여행' }, { name: '예술', searchTerm: '예술' }, { name: '종교', searchTerm: '종교' }, { name: '만화', searchTerm: '만화' }, { name: '아동', searchTerm: '아동' } ]; const results: any[] = []; for (const popular of popularCategories.slice(0, limit)) { const categories = searchCategories(popular.searchTerm, categoryData); if (categories.length > 0) { // 상위 레벨 카테고리 우선 선택 (대분류 > 중분류 > 소분류) const topCategories = categories.slice(0, 3); results.push({ name: popular.name, categories: topCategories }); } } let resultText = `인기 카테고리 목록 (상위 레벨 우선):\n\n`; results.forEach((result, index) => { resultText += `🔥 ${index + 1}. ${result.name}\n`; result.categories.forEach((cat: any, catIndex: number) => { const level = cat.name.split(' > ').length - 1; const levelIcon = level === 1 ? '📚' : level === 2 ? '📖' : '📄'; const categoryName = cat.name.split(' > ').pop(); resultText += ` ${levelIcon} ${categoryName} (CID: ${cat.cid})\n`; }); resultText += '\n'; }); return { content: [{ type: 'text', text: resultText }] }; } catch (error) { logger.error(`인기 카테고리 조회 중 오류 발생: ${error}`); return { content: [{ type: 'text', text: `인기 카테고리 조회 중 오류가 발생했습니다: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // 서버 시작 async function main() { try { const transport = new StdioServerTransport(); await server.connect(transport); // 시작 로그 제거 (JSON-RPC 충돌 방지) } catch (error) { logger.error(`서버 시작 중 오류 발생: ${error}`); process.exit(1); } } // 메인 함수 실행 main().catch(error => { logger.error(`메인 함수 실행 중 오류 발생: ${error}`); process.exit(1); });

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/TeeDDub/mcp-aladin-books-server'

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