import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const API_KEY = process.env.YELP_API_KEY || "";
const BASE_URL = "https://api.yelp.com/v3";
async function yelpRequest(endpoint: string, params: Record<string, string> = {}): Promise<any> {
const searchParams = new URLSearchParams(params);
const url = `${BASE_URL}/${endpoint}${searchParams.toString() ? '?' + searchParams.toString() : ''}`;
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${API_KEY}`,
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(`Yelp API error: ${response.status} - ${error.error?.description || 'Unknown error'}`);
}
return response.json();
}
const server = new Server(
{ name: "yelp", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "search_businesses",
description: "Search for businesses by location, category, or keyword. Returns up to 50 results with ratings, reviews, price level.",
inputSchema: {
type: "object",
properties: {
location: {
type: "string",
description: "Location to search (e.g., 'New York, NY', 'San Francisco', '90210')"
},
term: {
type: "string",
description: "Search term (e.g., 'restaurants', 'plumbers', 'coffee')"
},
categories: {
type: "string",
description: "Comma-separated category aliases (e.g., 'bars,french', 'restaurants'). Use list_categories to find aliases."
},
price: {
type: "string",
description: "Price levels to filter: '1'=$, '2'=$$, '3'=$$$, '4'=$$$$. Comma-separate for multiple (e.g., '1,2')"
},
radius: {
type: "number",
description: "Search radius in meters (max 40000 = ~25 miles)"
},
limit: {
type: "number",
description: "Number of results (max 50, default 20)"
},
sortBy: {
type: "string",
description: "Sort by: 'best_match', 'rating', 'review_count', 'distance'"
},
},
required: ["location"],
},
},
{
name: "get_business",
description: "Get detailed information about a specific business by ID",
inputSchema: {
type: "object",
properties: {
businessId: {
type: "string",
description: "Yelp business ID (from search results)"
},
},
required: ["businessId"],
},
},
{
name: "search_by_phone",
description: "Find a business by phone number",
inputSchema: {
type: "object",
properties: {
phone: {
type: "string",
description: "Phone number with country code (e.g., '+14157492060')"
},
},
required: ["phone"],
},
},
{
name: "get_reviews",
description: "Get up to 3 review excerpts for a business",
inputSchema: {
type: "object",
properties: {
businessId: {
type: "string",
description: "Yelp business ID"
},
},
required: ["businessId"],
},
},
{
name: "list_categories",
description: "List all Yelp business categories. Useful for finding category aliases for search.",
inputSchema: {
type: "object",
properties: {
locale: {
type: "string",
description: "Locale (default: 'en_US')"
},
},
required: [],
},
},
{
name: "autocomplete",
description: "Get autocomplete suggestions for businesses, categories, and terms",
inputSchema: {
type: "object",
properties: {
text: {
type: "string",
description: "Text to autocomplete"
},
latitude: {
type: "number",
description: "Optional latitude for location context"
},
longitude: {
type: "number",
description: "Optional longitude for location context"
},
},
required: ["text"],
},
},
{
name: "count_businesses",
description: "Shortcut: Get total business count for a location/category (useful for market sizing)",
inputSchema: {
type: "object",
properties: {
location: {
type: "string",
description: "Location (e.g., 'Los Angeles, CA')"
},
categories: {
type: "string",
description: "Category alias (e.g., 'restaurants', 'dentists', 'autorepair')"
},
},
required: ["location"],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "search_businesses": {
const { location, term, categories, price, radius, limit = 20, sortBy } = args as any;
const params: Record<string, string> = {
location,
limit: String(Math.min(limit, 50)),
};
if (term) params.term = term;
if (categories) params.categories = categories;
if (price) params.price = price;
if (radius) params.radius = String(radius);
if (sortBy) params.sort_by = sortBy;
const data = await yelpRequest("businesses/search", params);
const businesses = data.businesses?.map((b: any) => ({
id: b.id,
name: b.name,
rating: b.rating,
reviewCount: b.review_count,
price: b.price,
categories: b.categories?.map((c: any) => c.title).join(", "),
address: b.location?.display_address?.join(", "),
city: b.location?.city,
state: b.location?.state,
zipCode: b.location?.zip_code,
phone: b.display_phone,
isClosed: b.is_closed,
})) || [];
return {
content: [{
type: "text",
text: JSON.stringify({
total: data.total,
showing: businesses.length,
businesses,
}, null, 2),
}],
};
}
case "get_business": {
const { businessId } = args as any;
const data = await yelpRequest(`businesses/${businessId}`);
return {
content: [{
type: "text",
text: JSON.stringify({
id: data.id,
name: data.name,
rating: data.rating,
reviewCount: data.review_count,
price: data.price,
categories: data.categories?.map((c: any) => c.title),
address: data.location?.display_address?.join(", "),
phone: data.display_phone,
hours: data.hours?.[0]?.open?.map((h: any) => ({
day: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][h.day],
start: h.start,
end: h.end,
})),
photos: data.photos,
url: data.url,
}, null, 2),
}],
};
}
case "search_by_phone": {
const { phone } = args as any;
const data = await yelpRequest("businesses/search/phone", { phone });
const businesses = data.businesses?.map((b: any) => ({
id: b.id,
name: b.name,
rating: b.rating,
address: b.location?.display_address?.join(", "),
phone: b.display_phone,
})) || [];
return {
content: [{
type: "text",
text: JSON.stringify({ total: data.total, businesses }, null, 2),
}],
};
}
case "get_reviews": {
const { businessId } = args as any;
try {
const data = await yelpRequest(`businesses/${businessId}/reviews`);
const reviews = data.reviews?.map((r: any) => ({
rating: r.rating,
text: r.text,
timeCreated: r.time_created,
user: r.user?.name,
})) || [];
return {
content: [{ type: "text", text: JSON.stringify(reviews, null, 2) }],
};
} catch (error: any) {
if (error.message?.includes("404")) {
return {
content: [{
type: "text",
text: JSON.stringify({
error: "Reviews endpoint not accessible",
reason: "The Yelp Reviews API requires a Plus tier subscription ($9.99/1000 calls) or higher. Starter tier and free trial do not include reviews access.",
documentation: "https://business.yelp.com/data/products/fusion/",
}, null, 2),
}],
isError: true,
};
}
throw error;
}
}
case "list_categories": {
const { locale = "en_US" } = args as any;
const data = await yelpRequest("categories", { locale });
// Group by parent category for easier browsing
const categories = data.categories?.slice(0, 100).map((c: any) => ({
alias: c.alias,
title: c.title,
parentAliases: c.parent_aliases,
})) || [];
return {
content: [{
type: "text",
text: JSON.stringify({
note: "Showing first 100 categories. Use alias in search_businesses categories param.",
categories,
}, null, 2),
}],
};
}
case "autocomplete": {
const { text, latitude, longitude } = args as any;
const params: Record<string, string> = { text };
if (latitude) params.latitude = String(latitude);
if (longitude) params.longitude = String(longitude);
const data = await yelpRequest("autocomplete", params);
return {
content: [{
type: "text",
text: JSON.stringify({
terms: data.terms?.map((t: any) => t.text),
businesses: data.businesses?.map((b: any) => ({ id: b.id, name: b.name })),
categories: data.categories?.map((c: any) => ({ alias: c.alias, title: c.title })),
}, null, 2),
}],
};
}
case "count_businesses": {
const { location, categories } = args as any;
const params: Record<string, string> = {
location,
limit: "1", // We just want the total count
};
if (categories) params.categories = categories;
const data = await yelpRequest("businesses/search", params);
return {
content: [{
type: "text",
text: JSON.stringify({
location,
categories: categories || "all",
totalBusinesses: data.total,
}, null, 2),
}],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
isError: true,
};
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Yelp MCP server running");
}
main().catch(console.error);