UNPKG

ttbkk-mcp-server

Version:

MCP server providing access to Korean tteokbokki restaurant database with search, location-based queries, and brand information

383 lines 15.4 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; // Import tools and resources import { searchTteokbokkiRestaurants, getRestaurantsInArea } from './tools/restaurant-search.js'; import { getNearbyRestaurants } from './tools/nearby-search.js'; import { getRestaurantDetails } from './tools/restaurant-details.js'; import { getBrandList, getBrandDetails, searchBrands } from './tools/brand-list.js'; import { geocodeAddress } from './tools/geocoding.js'; import { FranchiseType } from './types/database.js'; import { supabase } from './lib/supabase.js'; const server = new Server({ name: 'ttbkk-mcp-server', version: '1.0.0', }); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'search_tteokbokki_restaurants', description: 'Search for tteokbokki restaurants by name, address, or brand name', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query (restaurant name, address, or brand name)', }, limit: { type: 'number', description: 'Maximum number of results to return (default: 10)', default: 10, }, }, required: ['query'], }, }, { name: 'get_restaurants_in_area', description: 'Get tteokbokki restaurants in a specific geographic area (bounding box)', inputSchema: { type: 'object', properties: { bottomLeftLat: { type: 'number', description: 'Bottom left latitude coordinate', }, bottomLeftLng: { type: 'number', description: 'Bottom left longitude coordinate', }, topRightLat: { type: 'number', description: 'Top right latitude coordinate', }, topRightLng: { type: 'number', description: 'Top right longitude coordinate', }, limit: { type: 'number', description: 'Maximum number of results to return (default: 100)', default: 100, }, }, required: ['bottomLeftLat', 'bottomLeftLng', 'topRightLat', 'topRightLng'], }, }, { name: 'get_nearby_restaurants', description: 'Get tteokbokki restaurants near specific coordinates with distance', inputSchema: { type: 'object', properties: { latitude: { type: 'number', description: 'Center point latitude coordinate', }, longitude: { type: 'number', description: 'Center point longitude coordinate', }, radius: { type: 'number', description: 'Search radius in kilometers (default: 1)', default: 1, }, limit: { type: 'number', description: 'Maximum number of results to return (default: 10)', default: 10, }, }, required: ['latitude', 'longitude'], }, }, { name: 'get_restaurant_details', description: 'Get detailed information about a specific restaurant including brand and hashtags', inputSchema: { type: 'object', properties: { restaurantId: { type: 'string', description: 'Unique identifier of the restaurant', }, }, required: ['restaurantId'], }, }, { name: 'get_brand_list', description: 'Get all tteokbokki brands sorted by number of restaurants', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_brand_details', description: 'Get detailed information about a specific brand', inputSchema: { type: 'object', properties: { brandId: { type: 'string', description: 'Unique identifier of the brand', }, }, required: ['brandId'], }, }, { name: 'search_brands', description: 'Search for brands by name', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query for brand name', }, limit: { type: 'number', description: 'Maximum number of results to return (default: 10)', default: 10, }, }, required: ['query'], }, }, { name: 'geocode_address', description: 'Convert address or place name to coordinates using Kakao Maps API', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Address or place name to geocode (e.g. "강남역", "홍대입구역")', }, provider: { type: 'string', description: 'Geocoding provider (default: "kakao")', default: 'kakao', }, }, required: ['query'], }, }, ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'search_tteokbokki_restaurants': { const { query, limit = 10 } = args; const results = await searchTteokbokkiRestaurants(query, limit); return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } case 'get_restaurants_in_area': { const { bottomLeftLat, bottomLeftLng, topRightLat, topRightLng, limit = 100 } = args; const results = await getRestaurantsInArea({ latitude: bottomLeftLat, longitude: bottomLeftLng }, { latitude: topRightLat, longitude: topRightLng }, limit); return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } case 'get_nearby_restaurants': { const { latitude, longitude, radius = 1, limit = 10 } = args; const results = await getNearbyRestaurants(latitude, longitude, radius, limit); return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } case 'get_restaurant_details': { const { restaurantId } = args; const result = await getRestaurantDetails(restaurantId); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } case 'get_brand_list': { const results = await getBrandList(); return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } case 'get_brand_details': { const { brandId } = args; const result = await getBrandDetails(brandId); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } case 'search_brands': { const { query, limit = 10 } = args; const results = await searchBrands(query, limit); return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } case 'geocode_address': { const { query, provider = 'kakao' } = args; const result = await geocodeAddress({ query, provider }); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error: ${errorMessage}`, }, ], isError: true, }; } }); // List available resources server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: [ { uri: 'ttbkk://franchise-types', name: 'Franchise Types', description: 'List of supported tteokbokki franchise brands with Korean names', mimeType: 'application/json', }, { uri: 'ttbkk://database-statistics', name: 'Database Statistics', description: 'Real-time statistics about tteokbokki restaurants and brands in the database', mimeType: 'application/json', }, ], }; }); // Handle resource reads server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; switch (uri) { case 'ttbkk://franchise-types': return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify({ franchiseTypes: FranchiseType, description: 'Supported tteokbokki franchise brands with Korean names from ttbkk-server crawler', total: Object.keys(FranchiseType).length, }, null, 2), }, ], }; case 'ttbkk://database-statistics': try { // 실시간 통계 조회 const [placesResult, brandsResult] = await Promise.all([ supabase.from('place').select('id', { count: 'exact', head: true }).eq('is_deleted', false), supabase.from('brand').select('id', { count: 'exact', head: true }), ]); return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify({ totalRestaurants: placesResult.count || 0, totalBrands: brandsResult.count || 0, lastUpdated: new Date().toISOString(), source: 'Supabase Database', }, null, 2), }, ], }; } catch (error) { return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify({ error: 'Failed to fetch statistics', message: error instanceof Error ? error.message : 'Unknown error', lastUpdated: new Date().toISOString(), }, null, 2), }, ], }; } default: throw new Error(`Unknown resource: ${uri}`); } }); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Tteokbokki MCP Server running on stdio'); console.error('Available tools: search_tteokbokki_restaurants, get_restaurants_in_area, get_nearby_restaurants, get_restaurant_details, get_brand_list, get_brand_details, search_brands'); console.error('Available resources: ttbkk://franchise-types, ttbkk://database-statistics'); } main().catch((error) => { console.error('Failed to start server:', error); process.exit(1); }); //# sourceMappingURL=index.js.map