index.tsā¢31.1 kB
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { config } from './services/config.js';
import { rentcastAPI } from './services/rentcast-api.js';
import {
PropertySearchSchema,
RandomPropertiesSchema,
MarketAnalysisSchema,
AVMSchema,
ListingSearchSchema,
PropertyDetailSchema,
RentEstimateSchema,
RentEstimateResponse,
ListingTypeSchema
} from './types/index.js';
import { z } from 'zod';
// ========================================
// š ļø SMART HELPER FUNCTION
// ========================================
/**
* Smart property formatter that handles all Rentcast API data structures
* Automatically detects property type and formats accordingly
*/
function formatPropertyInfo(prop: any): string {
const address = prop.formattedAddress || 'Address not available';
// Smart price detection and formatting
let priceDisplay = 'N/A';
let priceType = '';
if (prop.price) {
// Sale listing or rental listing
if (prop.rent || prop.listingType === 'Rental') {
// Rental listing - price is monthly rent
priceDisplay = `$${Number(prop.price).toLocaleString()}/month`;
priceType = ' (Monthly Rent)';
} else {
// Sale listing - price is sale price
priceDisplay = `$${Number(prop.price).toLocaleString()}`;
if (prop.status === 'Active') {
priceType = ' (Active Listing)';
} else {
priceType = ' (Listing)';
}
}
} else if (prop.rent) {
// Rental listing with rent field
priceDisplay = `$${Number(prop.rent).toLocaleString()}/month`;
priceType = ' (Monthly Rent)';
} else if (prop.lastSalePrice) {
// Property record with last sale price
priceDisplay = `$${Number(prop.lastSalePrice).toLocaleString()}`;
priceType = ' (Last Sale)';
} else if (prop.history && Object.keys(prop.history).length > 0) {
// Try to get price from history
const saleDates = Object.keys(prop.history).filter(date => {
const historyEntry = prop.history[date as keyof typeof prop.history];
return historyEntry?.event === 'Sale' || historyEntry?.event === 'Sale Listing';
}).sort().reverse();
if (saleDates.length > 0) {
const latestSale = prop.history[saleDates[0]!];
if (latestSale?.price) {
priceDisplay = `$${Number(latestSale.price).toLocaleString()}`;
priceType = ` (${saleDates[0]})`;
} else if (latestSale?.event === 'Sale' || latestSale?.event === 'Sale Listing') {
priceDisplay = `Sale recorded (${saleDates[0]})`;
priceType = ' - No price data';
}
}
}
// If still no price data, show property type and year built
if (priceDisplay === 'N/A') {
priceDisplay = prop.propertyType || 'Property';
priceType = prop.yearBuilt ? ` (${prop.yearBuilt})` : '';
}
// Safe null/undefined checking for all fields
const beds = prop.bedrooms != null ? `${prop.bedrooms} bed` : 'N/A bed';
const baths = prop.bathrooms != null ? `${prop.bathrooms} bath` : 'N/A bath';
const sqft = prop.squareFootage != null ? `${Number(prop.squareFootage).toLocaleString()} sqft` : 'N/A';
// Additional details with safe checking
const lotSize = prop.lotSize != null ? ` | š³ ${Number(prop.lotSize).toLocaleString()} sqft lot` : '';
const yearBuilt = prop.yearBuilt != null ? ` | š
${prop.yearBuilt} built` : '';
const lastSaleDate = prop.lastSaleDate ? ` | Date: ${prop.lastSaleDate.split('T')[0]} last sale` : '';
const status = prop.status ? ` | Status: ${prop.status}` : '';
const daysOnMarket = prop.daysOnMarket != null ? ` | Days: ${prop.daysOnMarket} days on market` : '';
const propertyType = prop.propertyType ? ` | Type: ${prop.propertyType}` : '';
// Build comprehensive property info
let propertyInfo = `Address: ${address}\nPrice: ${priceDisplay}${priceType} | Beds: ${beds} | Baths: ${baths} | SqFt: ${sqft}`;
// Add optional details
if (lotSize || yearBuilt || lastSaleDate || status || daysOnMarket || propertyType) {
propertyInfo += `${lotSize}${yearBuilt}${lastSaleDate}${status}${daysOnMarket}${propertyType}`;
}
return propertyInfo;
}
/**
* Format sale market data for market analysis tool
*/
function formatSaleMarketData(saleData: any): string {
if (!saleData) return '';
let result = `\nSales Market:`;
// Current month data
if (saleData.averagePrice !== undefined) {
result += `\nAverage Price: $${Number(saleData.averagePrice).toLocaleString()}`;
}
if (saleData.medianPrice !== undefined) {
result += `\nš Median Price: $${Number(saleData.medianPrice).toLocaleString()}`;
}
if (saleData.averagePricePerSquareFoot !== undefined) {
result += `\nš Avg Price/Sqft: $${Number(saleData.averagePricePerSquareFoot).toFixed(2)}`;
}
if (saleData.averageDaysOnMarket !== undefined) {
result += `\nAvg Days on Market: ${Number(saleData.averageDaysOnMarket).toFixed(1)}`;
}
if (saleData.newListings !== undefined) {
result += `\nš New Listings: ${saleData.newListings}`;
}
if (saleData.totalListings !== undefined) {
result += `\nš Total Listings: ${saleData.totalListings}`;
}
// Property type breakdown
if (saleData.dataByPropertyType && saleData.dataByPropertyType.length > 0) {
result += `\n\nš By Property Type:`;
saleData.dataByPropertyType.slice(0, 3).forEach((typeData: any) => {
const avgPrice = typeData.averagePrice ? `$${Number(typeData.averagePrice).toLocaleString()}` : 'N/A';
result += `\n⢠${typeData.propertyType}: ${avgPrice} avg`;
});
}
return result;
}
/**
* Format rental market data for market analysis tool
*/
function formatRentalMarketData(rentalData: any): string {
if (!rentalData) return '';
let result = `\n\nšļø Rental Market:`;
// Current month data
if (rentalData.averageRent !== undefined) {
result += `\nš° Average Rent: $${Number(rentalData.averageRent).toLocaleString()}/month`;
}
if (rentalData.medianRent !== undefined) {
result += `\nš Median Rent: $${Number(rentalData.medianRent).toLocaleString()}/month`;
}
if (rentalData.averageRentPerSquareFoot !== undefined) {
result += `\nš Avg Rent/Sqft: $${Number(rentalData.averageRentPerSquareFoot).toFixed(2)}`;
}
if (rentalData.newListings !== undefined) {
result += `\nš New Listings: ${rentalData.newListings}`;
}
if (rentalData.totalListings !== undefined) {
result += `\nš Total Listings: ${rentalData.totalListings}`;
}
// Property type breakdown
if (rentalData.dataByPropertyType && rentalData.dataByPropertyType.length > 0) {
result += `\n\nš By Property Type:`;
rentalData.dataByPropertyType.slice(0, 3).forEach((typeData: any) => {
const avgRent = typeData.averageRent ? `$${Number(typeData.averageRent).toLocaleString()}/month` : 'N/A';
result += `\n⢠${typeData.propertyType}: ${avgRent} avg`;
});
}
return result;
}
/**
* Format comparables data for AVM and rent estimates tools
*/
function formatComparables(comparables: any[], isRental: boolean = false): string {
if (!comparables || comparables.length === 0) return '';
let resultText = `\n\nš Comparable Properties (${comparables.length}):`;
comparables.slice(0, 3).forEach((comp: any, index: number) => {
const priceUnit = isRental ? '/month' : '';
const compPrice = comp.price ? `$${Number(comp.price).toLocaleString()}${priceUnit}` : 'N/A';
const distance = comp.distance ? `${(comp.distance * 0.621371).toFixed(2)} miles` : 'N/A';
const correlation = comp.correlation ? `${(comp.correlation * 100).toFixed(1)}% match` : 'N/A';
resultText += `\n${index + 1}. š ${comp.formattedAddress}`;
resultText += `\n š° ${compPrice} | šļø ${comp.bedrooms || 'N/A'} bed | šæ ${comp.bathrooms || 'N/A'} bath`;
resultText += `\n š ${comp.squareFootage ? `${Number(comp.squareFootage).toLocaleString()} sqft` : 'N/A'} | š ${distance} | šÆ ${correlation}`;
});
return resultText;
}
/**
* Build search parameters for property search tools
*/
function buildPropertySearchParams(params: any, includeLimit: boolean = true): any {
const searchParams: any = {};
if (includeLimit && params.limit) {
searchParams.limit = params.limit;
}
if (params.city) searchParams.city = params.city;
if (params.state) searchParams.state = params.state;
if (params.zipCode) searchParams.zipCode = params.zipCode;
if (params.bedrooms) searchParams.bedrooms = params.bedrooms;
if (params.bathrooms) searchParams.bathrooms = params.bathrooms;
if (params.propertyType) searchParams.propertyType = params.propertyType;
return searchParams;
}
/**
* Build search parameters for AVM and rent estimate tools
*/
function buildAVMSearchParams(params: any): any {
const searchParams: any = {};
// Prioritize address if provided, otherwise use other parameters
if (params.address) {
searchParams.address = params.address;
} else if (params.latitude && params.longitude) {
searchParams.latitude = params.latitude;
searchParams.longitude = params.longitude;
} else if (params.propertyId) {
searchParams.propertyId = params.propertyId;
}
// Add additional parameters if available (these improve accuracy)
if (params.propertyType) searchParams.propertyType = params.propertyType;
if (params.bedrooms !== undefined && params.bedrooms !== null) searchParams.bedrooms = params.bedrooms;
if (params.bathrooms !== undefined && params.bathrooms !== null) searchParams.bathrooms = params.bathrooms;
if (params.squareFootage !== undefined && params.squareFootage !== null) searchParams.squareFootage = params.squareFootage;
return searchParams;
}
/**
* Create standardized error response
*/
function createErrorResponse(message: string, error?: any): any {
const errorText = error ? `${message}: ${error}` : message;
return {
content: [{
type: "text",
text: errorText
}]
};
}
/**
* Create standardized success response
*/
function createSuccessResponse(text: string): any {
return {
content: [{
type: "text",
text: text
}]
};
}
/**
* Extract property parameters for estimation tools
*/
function extractPropertyParams(property: any): string {
return `š Address: ${property.formattedAddress || 'N/A'}\n` +
`š Latitude: ${property.latitude || 'N/A'}\n` +
`š Longitude: ${property.longitude || 'N/A'}\n` +
`š Property Type: ${property.propertyType || 'N/A'}\n` +
`šļø Bedrooms: ${property.bedrooms || 'N/A'}\n` +
`šæ Bathrooms: ${property.bathrooms || 'N/A'}\n` +
`š Square Footage: ${property.squareFootage || 'N/A'}`;
}
/**
* Create property value estimation parameters display
*/
function createPropertyValueParams(property: any, currentPrice?: number): string {
const baseParams = extractPropertyParams(property);
const priceInfo = currentPrice ? `\nš° Current Price: $${Number(currentPrice).toLocaleString()}` : '';
return `\n\nš” **Property Value Estimation Parameters (Copy to get_property_value tool):**\n` +
`${baseParams}${priceInfo}\n\n` +
`š” **Copy the Address, Latitude, Longitude, Property Type, Bedrooms, Bathrooms, and Square Footage values above to the get_property_value tool fields!**`;
}
/**
* Create rent estimation parameters display
*/
function createRentEstimationParams(property: any, currentRent?: number): string {
const baseParams = extractPropertyParams(property);
const rentInfo = currentRent ? `\nš° Current Rent: $${Number(currentRent).toLocaleString()}/month` : '';
return `\n\nš” **Rent Estimation Parameters (Copy to get_rent_estimates tool):**\n` +
`${baseParams}${rentInfo}\n\n` +
`š” **Copy the Address, Latitude, Longitude, Property Type, Bedrooms, Bathrooms, and Square Footage values above to the get_rent_estimates tool fields!**`;
}
// ========================================
// š MCP SERVER SETUP
// ========================================
const server = new McpServer({
name: "rentcast-mcp",
version: "1.0.0"
});
// ========================================
// š ļø MCP TOOLS (Rentcast API Endpoints)
// ========================================
// Tool 1: Search Properties
server.tool(
"search_properties",
"Search for properties with basic property information (default: 15, max: 50 for free tier) including city, state, bedrooms, bathrooms, square footage, lot size, and year built. Note: Price data may not be available for all properties.",
PropertySearchSchema.shape,
async (params) => {
try {
const searchParams = buildPropertySearchParams(params);
const result = await rentcastAPI.searchProperties(searchParams);
if (!result.success) {
return createErrorResponse("Error searching properties", result.error);
}
const properties = result.data as any[];
const summary = `Found ${properties.length} properties`;
// Process each property individually based on actual API structure
const propertyDetails = properties.slice(0, 10).map(prop => {
return formatPropertyInfo(prop);
}).join('\n\n');
const resultText = `${summary}\n\n${propertyDetails}${properties.length > 10 ? '\n\n... and more properties available' : ''}`;
return createSuccessResponse(resultText);
} catch (error) {
return createErrorResponse("Failed to search properties", error instanceof Error ? error.message : 'Unknown error');
}
}
);
// Tool 2: Get Random Properties
server.tool(
"get_random_properties",
"Get random properties with comprehensive info (default: 10, max: 50 for free tier) for market analysis including price history, lot size, and year built",
RandomPropertiesSchema.shape,
async (params) => {
try {
const searchParams = buildPropertySearchParams(params);
const result = await rentcastAPI.getRandomProperties(searchParams);
if (!result.success) {
return createErrorResponse("Error getting random properties", result.error);
}
const properties = result.data as any[];
const summary = `Retrieved ${properties.length} random properties`;
// Process each property individually
const sampleProperties = properties.slice(0, 5).map(prop => {
return formatPropertyInfo(prop);
}).join('\n\n');
const resultText = `${summary}\n\nSample Properties:\n\n${sampleProperties}${properties.length > 5 ? '\n\n... and more properties available' : ''}`;
return createSuccessResponse(resultText);
} catch (error) {
return createErrorResponse("Failed to get random properties", error instanceof Error ? error.message : 'Unknown error');
}
}
);
// Tool 3: Market Analysis
server.tool(
"analyze_market",
"Get comprehensive market statistics and trends for specific locations",
MarketAnalysisSchema.shape,
async (params) => {
try {
const searchParams: any = { dataType: params.dataType };
if (params.zipCode) searchParams.zipCode = params.zipCode;
if (params.city) searchParams.city = params.city;
if (params.state) searchParams.state = params.state;
const result = await rentcastAPI.getMarketData(searchParams);
if (!result.success) {
return createErrorResponse("Error analyzing market", result.error);
}
// Simplified market data handling - focus on the structure we know API returns
const market = Array.isArray(result.data) ? result.data[0] : result.data;
if (!market || (!market.saleData && !market.rentalData)) {
return createErrorResponse("No market data found for the specified location");
}
let resultText = `š Market Statistics for ${market.zipCode ? `ZIP: ${market.zipCode}` : (params.city ? `${params.city}, ${params.state}` : 'Location')}\n`;
// Add location info
if (market.zipCode) {
resultText += `\nš Location: ZIP ${market.zipCode}`;
}
if (market.city && market.state) {
resultText += `\nšļø ${market.city}, ${market.state}`;
}
// Format market data
if (market.saleData) {
resultText += formatSaleMarketData(market.saleData);
}
if (market.rentalData) {
resultText += formatRentalMarketData(market.rentalData);
}
return createSuccessResponse(resultText);
} catch (error) {
return createErrorResponse("Failed to analyze market", error instanceof Error ? error.message : 'Unknown error');
}
}
);
// Tool 4: Property Valuation (AVM)
server.tool(
"get_property_value",
"Get automated property value estimates with comparable properties",
AVMSchema.shape,
async (params: z.infer<typeof AVMSchema>) => {
try {
const searchParams = buildAVMSearchParams(params);
// Additional validation to ensure we have required parameters
if (!searchParams.propertyId && !searchParams.address && (!searchParams.latitude || !searchParams.longitude)) {
return createErrorResponse(
"ā **Missing Required Parameters for Property Valuation**\n\n" +
"š” **You must provide ONE of the following options:**\n\n" +
"**Option 1: Property Address**\n" +
"⢠`address`: Full property address (e.g., '1011 W 23rd St, Apt 101, Austin, TX 78705')\n\n" +
"**Option 2: GPS Coordinates**\n" +
"⢠`latitude`: Property latitude (e.g., 30.287007)\n" +
"⢠`longitude`: Property longitude (e.g., -97.748941)\n\n" +
"**Option 3: Property ID**\n" +
"⢠`propertyId`: Unique identifier from Rentcast database\n\n" +
"š **Optional Parameters (improve accuracy):**\n" +
"⢠`propertyType`: Apartment, House, Condo, etc.\n" +
"⢠`bedrooms`: Number of bedrooms\n" +
"⢠`bathrooms`: Number of bathrooms\n" +
"⢠`squareFootage`: Property size in sq ft"
);
}
const result = await rentcastAPI.getPropertyValue(searchParams);
if (!result.success) {
return createErrorResponse("Error getting property value", result.error);
}
const avm = result.data as any;
if (!avm) {
return createErrorResponse("No property value data found");
}
let resultText = `š° Estimated Value: ${avm.price ? `$${Number(avm.price).toLocaleString()}` : 'N/A'}`;
const range = avm.priceRangeLow && avm.priceRangeHigh
? ` (Range: $${Number(avm.priceRangeLow).toLocaleString()} - $${Number(avm.priceRangeHigh).toLocaleString()})`
: '';
resultText += `${range}`;
if (avm.comparables && avm.comparables.length > 0) {
resultText += formatComparables(avm.comparables);
}
return createSuccessResponse(resultText);
} catch (error) {
return createErrorResponse("Failed to get property value", error instanceof Error ? error.message : 'Unknown error');
}
}
);
// Tool 5: Rent Estimates
server.tool(
"get_rent_estimates",
"Get long-term rent estimates with comparable rental properties. This tool helps you estimate monthly rental prices for properties based on location, property characteristics, and market data.",
RentEstimateSchema.shape,
async (params: z.infer<typeof RentEstimateSchema>) => {
try {
// Validate parameters using Zod schema
const validatedParams = RentEstimateSchema.parse(params);
// Build search parameters for rent estimates
const searchParams: Record<string, any> = {};
if (validatedParams.propertyId) searchParams.propertyId = validatedParams.propertyId;
if (validatedParams.address) searchParams.address = validatedParams.address;
if (validatedParams.latitude) searchParams.latitude = validatedParams.latitude;
if (validatedParams.longitude) searchParams.longitude = validatedParams.longitude;
if (validatedParams.propertyType) searchParams.propertyType = validatedParams.propertyType;
if (validatedParams.bedrooms) searchParams.bedrooms = validatedParams.bedrooms;
if (validatedParams.bathrooms) searchParams.bathrooms = validatedParams.bathrooms;
if (validatedParams.squareFootage) searchParams.squareFootage = validatedParams.squareFootage;
// Additional validation to ensure we have required parameters
if (!searchParams.propertyId && !searchParams.address && (!searchParams.latitude || !searchParams.longitude)) {
return createErrorResponse(
"ā **Missing Required Parameters for Rent Estimates**\n\n" +
"š” **You must provide ONE of the following options:**\n\n" +
"**Option 1: Property Address**\n" +
"⢠`address`: Full property address (e.g., '1011 W 23rd St, Apt 101, Austin, TX 78705')\n\n" +
"**Option 2: GPS Coordinates**\n" +
"⢠`latitude`: Property latitude (e.g., 30.287007)\n" +
"⢠`longitude`: Property longitude (e.g., -97.748941)\n\n" +
"**Option 3: Property ID**\n" +
"⢠`propertyId`: Unique identifier from Rentcast database\n\n" +
"š **Optional Parameters (improve accuracy):**\n" +
"⢠`propertyType`: Apartment, House, Condo, etc.\n" +
"⢠`bedrooms`: Number of bedrooms\n" +
"⢠`bathrooms`: Number of bathrooms\n" +
"⢠`squareFootage`: Property size in sq ft\n\n" +
"š **Example Usage:**\n" +
"```json\n" +
"{\n" +
' "address": "1011 W 23rd St, Apt 101, Austin, TX 78705",\n' +
' "propertyType": "Apartment",\n' +
' "bedrooms": 1,\n' +
' "bathrooms": 1,\n' +
' "squareFootage": 450\n' +
"}\n" +
"```"
);
}
const result = await rentcastAPI.getRentEstimates(searchParams);
if (!result.success) {
return createErrorResponse("Error getting rent estimates", result.error);
}
const rentData = result.data as RentEstimateResponse;
if (!rentData) {
return createErrorResponse("No rent estimate data found");
}
// Format the response
let resultText = `š **Rent Estimate Results**\n\n`;
// Add usage tips
resultText += `š” **Tool Usage Tips:**\n`;
resultText += `⢠Use this tool to estimate monthly rental prices for properties\n`;
resultText += `⢠Provide more details (bedrooms, bathrooms, square footage) for better accuracy\n`;
resultText += `⢠Results include comparable properties for market analysis\n\n`;
// Property identification
if (rentData.address) {
resultText += `š **Property:** ${rentData.address}\n`;
}
if (rentData.propertyType) {
resultText += `š **Type:** ${rentData.propertyType}\n`;
}
if (rentData.bedrooms !== undefined) {
resultText += `šļø **Bedrooms:** ${rentData.bedrooms}\n`;
}
if (rentData.bathrooms !== undefined) {
resultText += `šæ **Bathrooms:** ${rentData.bathrooms}\n`;
}
if (rentData.squareFootage) {
resultText += `š **Square Footage:** ${rentData.squareFootage.toLocaleString()} sqft\n`;
}
resultText += `\nš° **Estimated Monthly Rent:** `;
if (rentData.rent) {
resultText += `$${Number(rentData.rent).toLocaleString()}/month`;
// Add rent range if available
if (rentData.rentRangeLow && rentData.rentRangeHigh) {
resultText += `\nš **Rent Range:** $${Number(rentData.rentRangeLow).toLocaleString()} - $${Number(rentData.rentRangeHigh).toLocaleString()}/month`;
}
} else {
resultText += `N/A`;
}
// Add comparables if available
if (rentData.comparables && rentData.comparables.length > 0) {
resultText += `\n\nšļø **Comparable Properties:**\n`;
rentData.comparables.slice(0, 5).forEach((comp, index) => {
resultText += `\n${index + 1}. **${comp.address}**\n`;
resultText += ` š° Rent: $${Number(comp.rent).toLocaleString()}/month`;
if (comp.bedrooms !== undefined) resultText += ` | šļø ${comp.bedrooms} bed`;
if (comp.bathrooms !== undefined) resultText += ` | šæ ${comp.bathrooms} bath`;
if (comp.squareFootage) resultText += ` | š ${comp.squareFootage.toLocaleString()} sqft`;
if (comp.distance) resultText += ` | š ${comp.distance.toFixed(1)} miles away`;
});
}
// Add helpful footer
resultText += `\n\nš **Need More Data?**\n`;
resultText += `⢠Use \`get_property_details\` to get comprehensive property information\n`;
resultText += `⢠Use \`get_rental_listings\` to see actual rental listings in the area\n`;
resultText += `⢠Use \`analyze_market\` to understand rental market trends\n\n`;
return createSuccessResponse(resultText);
} catch (error) {
if (error instanceof z.ZodError) {
const errorDetails = error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
return createErrorResponse(`Invalid parameters: ${errorDetails}`);
}
return createErrorResponse("Failed to get rent estimates", error instanceof Error ? error.message : 'Unknown error');
}
}
);
// Tool 6: Sale Listings
server.tool(
"get_sale_listings",
"Get sale listings with comprehensive property information. This tool searches for properties currently for sale.",
ListingSearchSchema.shape,
async (params) => {
try {
const searchParams = buildPropertySearchParams(params);
const result = await rentcastAPI.getSaleListings(searchParams);
if (!result.success) {
return createErrorResponse("Error getting sale listings", result.error);
}
const listings = result.data as any[];
const summary = `Found ${listings.length} sale listings`;
const listingDetails = listings.slice(0, 8).map(listing => {
// Use actual Rentcast API data structure
const propertyInfo = formatPropertyInfo(listing);
// Add compact parameter suggestions
const params = `\nš” **Quick Parameters:** Address: "${listing.formattedAddress}", Lat: ${listing.latitude}, Lng: ${listing.longitude}, Type: "${listing.propertyType}", Beds: ${listing.bedrooms || 'N/A'}, Baths: ${listing.bathrooms || 'N/A'}, SqFt: ${listing.squareFootage || 'N/A'}`;
return propertyInfo + params;
}).join('\n\n');
const resultText = `${summary}\n\n${listingDetails}${listings.length > 8 ? '\n\n... and more listings available' : ''}`;
return createSuccessResponse(resultText);
} catch (error) {
return createErrorResponse("Failed to get sale listings", error instanceof Error ? error.message : 'Unknown error');
}
}
);
// Tool 7: Property Details (Enhanced)
server.tool(
"get_property_details",
"Get detailed property information and prepare parameters for property value estimation",
PropertyDetailSchema.shape,
async (params) => {
try {
// This tool helps prepare property data for other tools
const result = await rentcastAPI.getProperty(params.id);
if (!result.success) {
return createErrorResponse("Error getting property details", result.error);
}
const property = result.data as any;
if (!property) {
return createErrorResponse("No property details found");
}
// Format property details
const propertyInfo = formatPropertyInfo(property);
// Prepare parameters for property value estimation
const valueEstimationParams = createPropertyValueParams(property);
const rentEstimationParams = createRentEstimationParams(property);
const additionalInfo = `\n\nš” **Copy these values to the get_property_value tool to get an automated valuation estimate!**\n\n` +
`š **Rent Estimation Parameters:**\n` +
`${extractPropertyParams(property)}\n\n` +
`š” **Copy these values to the get_rent_estimates tool to get rent estimates!**`;
const resultText = propertyInfo + valueEstimationParams + additionalInfo;
return createSuccessResponse(resultText);
} catch (error) {
return createErrorResponse("Failed to get property details", error instanceof Error ? error.message : 'Unknown error');
}
}
);
// Tool 8: Rental Listings
server.tool(
"get_rental_listings",
"Get rental listings with comprehensive property information. This tool searches for properties currently for rent.",
ListingSearchSchema.shape,
async (params) => {
try {
const searchParams = buildPropertySearchParams(params);
const result = await rentcastAPI.getRentalListings(searchParams);
if (!result.success) {
return createErrorResponse("Error getting rental listings", result.error);
}
const listings = result.data as any[];
const summary = `Found ${listings.length} rental listings`;
const listingDetails = listings.slice(0, 8).map(listing => {
// Use actual Rentcast API data structure
const propertyInfo = formatPropertyInfo(listing);
// Add compact parameter suggestions
const params = `\nš” **Quick Parameters:** Address: "${listing.formattedAddress}", Lat: ${listing.latitude}, Lng: ${listing.longitude}, Type: "${listing.propertyType}", Beds: ${listing.bedrooms || 'N/A'}, Baths: ${listing.bathrooms || 'N/A'}, SqFt: ${listing.squareFootage || 'N/A'}`;
return propertyInfo + params;
}).join('\n\n');
const resultText = `${summary}\n\n${listingDetails}${listings.length > 8 ? '\n\n... and more listings available' : ''}`;
return createSuccessResponse(resultText);
} catch (error) {
return createErrorResponse("Failed to get rental listings", error instanceof Error ? error.message : 'Unknown error');
}
}
);
// Tool 8: Property Details (Enhanced - already defined above)
// This tool was moved to Tool 7 above for better organization
// ========================================
// š START SERVER
// ========================================
async function main() {
try {
const transport = new StdioServerTransport();
await server.connect(transport);
} catch (error) {
process.exit(1);
}
}
// Handle process termination
process.on('SIGINT', () => {
process.exit(0);
});
process.on('SIGTERM', () => {
process.exit(0);
});
// Start the server
main().catch((error) => {
process.exit(1);
});