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
JavaScript
#!/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