@sfranalytics/mcp 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +147 -0
  3. package/dist/config.d.ts +28 -0
  4. package/dist/config.js +101 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/http.d.ts +2 -0
  7. package/dist/http.js +252 -0
  8. package/dist/http.js.map +1 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +41 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/server.d.ts +3 -0
  13. package/dist/server.js +213 -0
  14. package/dist/server.js.map +1 -0
  15. package/dist/services/httpClient.d.ts +19 -0
  16. package/dist/services/httpClient.js +73 -0
  17. package/dist/services/httpClient.js.map +1 -0
  18. package/dist/services/plr.d.ts +26 -0
  19. package/dist/services/plr.js +75 -0
  20. package/dist/services/plr.js.map +1 -0
  21. package/dist/services/sfr.d.ts +33 -0
  22. package/dist/services/sfr.js +124 -0
  23. package/dist/services/sfr.js.map +1 -0
  24. package/dist/services/snowflake.d.ts +12 -0
  25. package/dist/services/snowflake.js +71 -0
  26. package/dist/services/snowflake.js.map +1 -0
  27. package/dist/tools/dateHelper.d.ts +26 -0
  28. package/dist/tools/dateHelper.js +86 -0
  29. package/dist/tools/dateHelper.js.map +1 -0
  30. package/dist/tools/formatters.d.ts +66 -0
  31. package/dist/tools/formatters.js +219 -0
  32. package/dist/tools/formatters.js.map +1 -0
  33. package/dist/tools/health.d.ts +5 -0
  34. package/dist/tools/health.js +38 -0
  35. package/dist/tools/health.js.map +1 -0
  36. package/dist/tools/nextActions.d.ts +10 -0
  37. package/dist/tools/nextActions.js +23 -0
  38. package/dist/tools/nextActions.js.map +1 -0
  39. package/dist/tools/plr/borrowerContacts.d.ts +3 -0
  40. package/dist/tools/plr/borrowerContacts.js +73 -0
  41. package/dist/tools/plr/borrowerContacts.js.map +1 -0
  42. package/dist/tools/plr/borrowerLoans.d.ts +3 -0
  43. package/dist/tools/plr/borrowerLoans.js +115 -0
  44. package/dist/tools/plr/borrowerLoans.js.map +1 -0
  45. package/dist/tools/plr/borrowerProfile.d.ts +3 -0
  46. package/dist/tools/plr/borrowerProfile.js +201 -0
  47. package/dist/tools/plr/borrowerProfile.js.map +1 -0
  48. package/dist/tools/plr/borrowerRankings.d.ts +3 -0
  49. package/dist/tools/plr/borrowerRankings.js +123 -0
  50. package/dist/tools/plr/borrowerRankings.js.map +1 -0
  51. package/dist/tools/plr/borrowerSearch.d.ts +3 -0
  52. package/dist/tools/plr/borrowerSearch.js +128 -0
  53. package/dist/tools/plr/borrowerSearch.js.map +1 -0
  54. package/dist/tools/plr/churnedBorrowers.d.ts +3 -0
  55. package/dist/tools/plr/churnedBorrowers.js +86 -0
  56. package/dist/tools/plr/churnedBorrowers.js.map +1 -0
  57. package/dist/tools/plr/lenderBorrowers.d.ts +3 -0
  58. package/dist/tools/plr/lenderBorrowers.js +71 -0
  59. package/dist/tools/plr/lenderBorrowers.js.map +1 -0
  60. package/dist/tools/plr/lenderRankings.d.ts +3 -0
  61. package/dist/tools/plr/lenderRankings.js +74 -0
  62. package/dist/tools/plr/lenderRankings.js.map +1 -0
  63. package/dist/tools/plr/loansNearby.d.ts +3 -0
  64. package/dist/tools/plr/loansNearby.js +113 -0
  65. package/dist/tools/plr/loansNearby.js.map +1 -0
  66. package/dist/tools/plr/marketTrends.d.ts +3 -0
  67. package/dist/tools/plr/marketTrends.js +95 -0
  68. package/dist/tools/plr/marketTrends.js.map +1 -0
  69. package/dist/tools/plr/msaRankings.d.ts +3 -0
  70. package/dist/tools/plr/msaRankings.js +74 -0
  71. package/dist/tools/plr/msaRankings.js.map +1 -0
  72. package/dist/tools/plr/negativeRemarks.d.ts +3 -0
  73. package/dist/tools/plr/negativeRemarks.js +94 -0
  74. package/dist/tools/plr/negativeRemarks.js.map +1 -0
  75. package/dist/tools/plr/ownerSearch.d.ts +3 -0
  76. package/dist/tools/plr/ownerSearch.js +57 -0
  77. package/dist/tools/plr/ownerSearch.js.map +1 -0
  78. package/dist/tools/plr/portfolioSummary.d.ts +3 -0
  79. package/dist/tools/plr/portfolioSummary.js +99 -0
  80. package/dist/tools/plr/portfolioSummary.js.map +1 -0
  81. package/dist/tools/plr/topBorrowers.d.ts +3 -0
  82. package/dist/tools/plr/topBorrowers.js +69 -0
  83. package/dist/tools/plr/topBorrowers.js.map +1 -0
  84. package/dist/tools/plr/topLenders.d.ts +3 -0
  85. package/dist/tools/plr/topLenders.js +75 -0
  86. package/dist/tools/plr/topLenders.js.map +1 -0
  87. package/dist/tools/plr/transactionHistory.d.ts +3 -0
  88. package/dist/tools/plr/transactionHistory.js +74 -0
  89. package/dist/tools/plr/transactionHistory.js.map +1 -0
  90. package/dist/tools/prompts.d.ts +7 -0
  91. package/dist/tools/prompts.js +157 -0
  92. package/dist/tools/prompts.js.map +1 -0
  93. package/dist/tools/registerToolSafe.d.ts +29 -0
  94. package/dist/tools/registerToolSafe.js +36 -0
  95. package/dist/tools/registerToolSafe.js.map +1 -0
  96. package/dist/tools/sfr/activityHighlights.d.ts +3 -0
  97. package/dist/tools/sfr/activityHighlights.js +70 -0
  98. package/dist/tools/sfr/activityHighlights.js.map +1 -0
  99. package/dist/tools/sfr/bestBuyers.d.ts +3 -0
  100. package/dist/tools/sfr/bestBuyers.js +60 -0
  101. package/dist/tools/sfr/bestBuyers.js.map +1 -0
  102. package/dist/tools/sfr/buyerGrowth.d.ts +3 -0
  103. package/dist/tools/sfr/buyerGrowth.js +68 -0
  104. package/dist/tools/sfr/buyerGrowth.js.map +1 -0
  105. package/dist/tools/sfr/buyerProfile.d.ts +3 -0
  106. package/dist/tools/sfr/buyerProfile.js +162 -0
  107. package/dist/tools/sfr/buyerProfile.js.map +1 -0
  108. package/dist/tools/sfr/distressSearch.d.ts +3 -0
  109. package/dist/tools/sfr/distressSearch.js +173 -0
  110. package/dist/tools/sfr/distressSearch.js.map +1 -0
  111. package/dist/tools/sfr/flipActivity.d.ts +3 -0
  112. package/dist/tools/sfr/flipActivity.js +110 -0
  113. package/dist/tools/sfr/flipActivity.js.map +1 -0
  114. package/dist/tools/sfr/flipStats.d.ts +3 -0
  115. package/dist/tools/sfr/flipStats.js +98 -0
  116. package/dist/tools/sfr/flipStats.js.map +1 -0
  117. package/dist/tools/sfr/getProperty.d.ts +3 -0
  118. package/dist/tools/sfr/getProperty.js +142 -0
  119. package/dist/tools/sfr/getProperty.js.map +1 -0
  120. package/dist/tools/sfr/institutionalOwners.d.ts +3 -0
  121. package/dist/tools/sfr/institutionalOwners.js +88 -0
  122. package/dist/tools/sfr/institutionalOwners.js.map +1 -0
  123. package/dist/tools/sfr/investorActivity.d.ts +3 -0
  124. package/dist/tools/sfr/investorActivity.js +130 -0
  125. package/dist/tools/sfr/investorActivity.js.map +1 -0
  126. package/dist/tools/sfr/marketHighlights.d.ts +3 -0
  127. package/dist/tools/sfr/marketHighlights.js +100 -0
  128. package/dist/tools/sfr/marketHighlights.js.map +1 -0
  129. package/dist/tools/sfr/msaResolver.d.ts +15 -0
  130. package/dist/tools/sfr/msaResolver.js +109 -0
  131. package/dist/tools/sfr/msaResolver.js.map +1 -0
  132. package/dist/tools/sfr/propertyBatch.d.ts +3 -0
  133. package/dist/tools/sfr/propertyBatch.js +73 -0
  134. package/dist/tools/sfr/propertyBatch.js.map +1 -0
  135. package/dist/tools/sfr/propertyComps.d.ts +3 -0
  136. package/dist/tools/sfr/propertyComps.js +56 -0
  137. package/dist/tools/sfr/propertyComps.js.map +1 -0
  138. package/dist/tools/sfr/propertyTransactions.d.ts +3 -0
  139. package/dist/tools/sfr/propertyTransactions.js +50 -0
  140. package/dist/tools/sfr/propertyTransactions.js.map +1 -0
  141. package/dist/tools/sfr/rentalComparables.d.ts +3 -0
  142. package/dist/tools/sfr/rentalComparables.js +91 -0
  143. package/dist/tools/sfr/rentalComparables.js.map +1 -0
  144. package/dist/tools/sfr/rentalMarketAnalysis.d.ts +3 -0
  145. package/dist/tools/sfr/rentalMarketAnalysis.js +134 -0
  146. package/dist/tools/sfr/rentalMarketAnalysis.js.map +1 -0
  147. package/dist/tools/sfr/rentalStats.d.ts +3 -0
  148. package/dist/tools/sfr/rentalStats.js +118 -0
  149. package/dist/tools/sfr/rentalStats.js.map +1 -0
  150. package/dist/tools/sfr/searchProperties.d.ts +3 -0
  151. package/dist/tools/sfr/searchProperties.js +157 -0
  152. package/dist/tools/sfr/searchProperties.js.map +1 -0
  153. package/dist/tools/sfr/topBuyers.d.ts +3 -0
  154. package/dist/tools/sfr/topBuyers.js +91 -0
  155. package/dist/tools/sfr/topBuyers.js.map +1 -0
  156. package/dist/tools/sfr/zipDetail.d.ts +3 -0
  157. package/dist/tools/sfr/zipDetail.js +85 -0
  158. package/dist/tools/sfr/zipDetail.js.map +1 -0
  159. package/dist/tools/sfr/zipFinder.d.ts +3 -0
  160. package/dist/tools/sfr/zipFinder.js +79 -0
  161. package/dist/tools/sfr/zipFinder.js.map +1 -0
  162. package/dist/tools/slugHelper.d.ts +2 -0
  163. package/dist/tools/slugHelper.js +5 -0
  164. package/dist/tools/slugHelper.js.map +1 -0
  165. package/dist/tools/snowflake/compareMarkets.d.ts +2 -0
  166. package/dist/tools/snowflake/compareMarkets.js +95 -0
  167. package/dist/tools/snowflake/compareMarkets.js.map +1 -0
  168. package/dist/tools/snowflake/hviTrend.d.ts +2 -0
  169. package/dist/tools/snowflake/hviTrend.js +77 -0
  170. package/dist/tools/snowflake/hviTrend.js.map +1 -0
  171. package/dist/tools/snowflake/investorActivity.d.ts +2 -0
  172. package/dist/tools/snowflake/investorActivity.js +138 -0
  173. package/dist/tools/snowflake/investorActivity.js.map +1 -0
  174. package/dist/tools/snowflake/marketSnapshot.d.ts +2 -0
  175. package/dist/tools/snowflake/marketSnapshot.js +185 -0
  176. package/dist/tools/snowflake/marketSnapshot.js.map +1 -0
  177. package/dist/tools/snowflake/marketTrends.d.ts +2 -0
  178. package/dist/tools/snowflake/marketTrends.js +81 -0
  179. package/dist/tools/snowflake/marketTrends.js.map +1 -0
  180. package/dist/tools/snowflake/rankRentalZips.d.ts +2 -0
  181. package/dist/tools/snowflake/rankRentalZips.js +94 -0
  182. package/dist/tools/snowflake/rankRentalZips.js.map +1 -0
  183. package/dist/tools/snowflake/rankZips.d.ts +2 -0
  184. package/dist/tools/snowflake/rankZips.js +86 -0
  185. package/dist/tools/snowflake/rankZips.js.map +1 -0
  186. package/dist/tools/snowflake/rentalMarket.d.ts +2 -0
  187. package/dist/tools/snowflake/rentalMarket.js +111 -0
  188. package/dist/tools/snowflake/rentalMarket.js.map +1 -0
  189. package/dist/tools/snowflake/rentalYield.d.ts +2 -0
  190. package/dist/tools/snowflake/rentalYield.js +143 -0
  191. package/dist/tools/snowflake/rentalYield.js.map +1 -0
  192. package/dist/tools/snowflake/zipProfile.d.ts +2 -0
  193. package/dist/tools/snowflake/zipProfile.js +110 -0
  194. package/dist/tools/snowflake/zipProfile.js.map +1 -0
  195. package/dist/version.d.ts +1 -0
  196. package/dist/version.js +5 -0
  197. package/dist/version.js.map +1 -0
  198. package/package.json +53 -0
@@ -0,0 +1,118 @@
1
+ import { z } from "zod";
2
+ import { structuredResult, kvSummary, markdownTable, truncatedJson } from "../formatters.js";
3
+ import { action, locationArgs } from "../nextActions.js";
4
+ import { registerToolSafe } from "../registerToolSafe.js";
5
+ const Input = z.object({
6
+ search_type: z
7
+ .enum(["state", "city", "zip", "county", "neighborhood"])
8
+ .describe("Location search type"),
9
+ state: z.string().optional().describe("State abbreviation, e.g. 'AZ'"),
10
+ city: z.string().optional().describe("City name, e.g. 'Phoenix'"),
11
+ zips: z.string().optional().describe("Comma-separated zip codes, e.g. '85281'"),
12
+ county: z.string().optional().describe("County name"),
13
+ neighborhood: z.string().optional().describe("Neighborhood name (used with search_type='neighborhood', e.g. 'Downtown' — try city-level first if unsure of valid names)"),
14
+ bedrooms: z.coerce.number().optional().describe("Filter by number of bedrooms"),
15
+ look_back: z.coerce.number().default(365).describe("Days to look back (default 365, max 365)"),
16
+ });
17
+ export function registerRentalStatsTool(server, sfr) {
18
+ registerToolSafe(server, "sfr_rental_stats", {
19
+ title: "Rental market statistics (SFR)",
20
+ description: "Get rental market STATISTICS for a location: median, average, percentiles (p25/p75), " +
21
+ "min/max rent, and price distributions. Requires search_type + location param. " +
22
+ "Supports county and neighborhood search_types (unique to this tool). " +
23
+ "search_type='city' requires state param. " +
24
+ "For individual rental comps near a specific address, use sfr_rental_comparables.",
25
+ inputSchema: Input,
26
+ }, async (args) => {
27
+ const query = {
28
+ search_type: args.search_type,
29
+ look_back: args.look_back,
30
+ };
31
+ if (args.state)
32
+ query.state = args.state;
33
+ if (args.city)
34
+ query.city = args.city;
35
+ if (args.zips)
36
+ query.zips = args.zips;
37
+ if (args.county)
38
+ query.county = args.county;
39
+ if (args.neighborhood)
40
+ query.neighborhood = args.neighborhood;
41
+ if (args.bedrooms)
42
+ query.bedrooms = args.bedrooms;
43
+ const data = await sfr.getRentalStats(query);
44
+ const loc = args.zips ?? args.city ?? args.county ?? args.state ?? "All";
45
+ const lines = [
46
+ `## Rental Market Stats — ${loc}`,
47
+ `_${args.look_back}-day lookback_`,
48
+ "",
49
+ ];
50
+ if (data == null) {
51
+ lines.push("_No data returned. The query may be too broad (e.g. state-level) — try narrowing to a city or zip._");
52
+ return structuredResult(lines.join("\n"), { location: { search_type: args.search_type, value: loc }, error: "no_data" });
53
+ }
54
+ if (data?.stats) {
55
+ const fmtRent = (v) => v != null ? `$${Math.round(Number(v)).toLocaleString()}/mo` : undefined;
56
+ lines.push(kvSummary([
57
+ ["Median Rent", fmtRent(data.stats.median)],
58
+ ["Average Rent", fmtRent(data.stats.average)],
59
+ ["25th Percentile", fmtRent(data.stats.p25)],
60
+ ["75th Percentile", fmtRent(data.stats.p75)],
61
+ ["Min", fmtRent(data.stats.min)],
62
+ ["Max", fmtRent(data.stats.max)],
63
+ ]));
64
+ if (data.prices?.data && Array.isArray(data.prices.data)) {
65
+ lines.push("");
66
+ lines.push("### Price Distribution");
67
+ // Aggregate duplicate price bins by summing counts
68
+ const binMap = new Map();
69
+ for (const d of data.prices.data) {
70
+ if (d.price != null) {
71
+ const p = Number(d.price);
72
+ binMap.set(p, (binMap.get(p) ?? 0) + (d.count ?? 0));
73
+ }
74
+ }
75
+ const rows = [...binMap.entries()]
76
+ .sort((a, b) => a[0] - b[0])
77
+ .map(([price, count]) => [
78
+ `$${price.toLocaleString()}`,
79
+ count,
80
+ ]);
81
+ lines.push(markdownTable(["Rent Range", "Listings"], rows));
82
+ }
83
+ }
84
+ else {
85
+ lines.push("```json");
86
+ lines.push(truncatedJson(data, 3000));
87
+ lines.push("```");
88
+ }
89
+ // Extract key metrics instead of passing the entire raw upstream response
90
+ // (which can be 150K+ chars for some locations)
91
+ const structured = {};
92
+ if (data?.stats)
93
+ structured.stats = data.stats;
94
+ if (data?.prices?.data && Array.isArray(data.prices.data)) {
95
+ // Aggregate price distribution bins (same logic as markdown)
96
+ const binMap = new Map();
97
+ for (const d of data.prices.data) {
98
+ if (d.price != null) {
99
+ const p = Number(d.price);
100
+ binMap.set(p, (binMap.get(p) ?? 0) + (d.count ?? 0));
101
+ }
102
+ }
103
+ structured.priceDistribution = [...binMap.entries()]
104
+ .sort((a, b) => a[0] - b[0])
105
+ .map(([price, count]) => ({ price, count }));
106
+ }
107
+ structured.location = { search_type: args.search_type, value: loc };
108
+ structured.lookBack = args.look_back;
109
+ const locArgs = locationArgs(args);
110
+ const actions = [
111
+ ...(args.zips ? [action("sfr_zip_detail", "Deep-dive zip metrics", { zipCode: args.zips.split(",")[0] })] : []),
112
+ action("sfr_top_buyers", "Active buyers in this area", locArgs),
113
+ action("sfr_search_properties", "Browse individual transactions", locArgs),
114
+ ];
115
+ return structuredResult(lines.join("\n"), structured, actions);
116
+ });
117
+ }
118
+ //# sourceMappingURL=rentalStats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rentalStats.js","sourceRoot":"","sources":["../../../src/tools/sfr/rentalStats.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { SfrClient } from "../../services/sfr.js";
3
+ export declare function registerSearchPropertiesTool(server: McpServer, sfr: SfrClient): void;
@@ -0,0 +1,157 @@
1
+ import { z } from "zod";
2
+ import { structuredResult, markdownTable, fmtDollars } from "../formatters.js";
3
+ import { action, locationArgs } from "../nextActions.js";
4
+ import { registerToolSafe } from "../registerToolSafe.js";
5
+ import { autoFillDateRange } from "../dateHelper.js";
6
+ const Input = z.object({
7
+ search_type: z
8
+ .enum(["state", "city", "zip", "msa", "address"])
9
+ .describe("Location search type"),
10
+ state: z.string().optional().describe("State abbreviation, e.g. 'AZ'"),
11
+ city: z.string().optional().describe("City name, e.g. 'Phoenix'"),
12
+ zips: z.string().optional().describe("Comma-separated zip codes, e.g. '85281,85282'"),
13
+ msa: z.string().optional().describe("MSA name, e.g. 'Phoenix-Mesa-Chandler, AZ'"),
14
+ latitude: z.coerce.number().optional().describe("Latitude for address search"),
15
+ longitude: z.coerce.number().optional().describe("Longitude for address search"),
16
+ radius: z.coerce.number().optional().describe("Radius in miles for address search"),
17
+ sales_price_min: z.coerce.number().optional().describe("Minimum sale price"),
18
+ sales_price_max: z.coerce.number().optional().describe("Maximum sale price"),
19
+ sales_date_min: z.string().optional().describe("Earliest sale date, e.g. '2024-01-01'"),
20
+ sales_date_max: z.string().optional().describe("Latest sale date"),
21
+ beds_min: z.coerce.number().optional().describe("Minimum bedrooms"),
22
+ beds_max: z.coerce.number().optional().describe("Maximum bedrooms"),
23
+ building_sqft_min: z.coerce.number().optional().describe("Minimum sqft"),
24
+ building_sqft_max: z.coerce.number().optional().describe("Maximum sqft"),
25
+ property_types: z.string().optional().describe("Property type filter: 'SFR', 'Condo', 'Multi-Family', 'Townhouse', 'Mobile Home'"),
26
+ seller_name: z.string().optional().describe("Filter by seller name (partial match)"),
27
+ purchase_type: z.string().optional().describe("Discount filter: 'all' (default), 'discounted' (below-AVM), or 'non-discount'. " +
28
+ "NOTE: This is NOT a cash/financed filter. For cash buyer data, use sfr_investor_activity which includes isCashBuyer per transaction."),
29
+ buyer_type: z.string().optional().describe("Buyer type: 'individual', 'corporate', or 'all'"),
30
+ page: z.coerce.number().default(1).describe("Page number (default 1)"),
31
+ page_size: z.coerce.number().default(25).describe("Results per page (default 25, max 100)"),
32
+ sort: z.string().optional().describe("Sort field: 'recording_date', 'sale_value', 'sale_date', 'bedrooms', 'building_area', 'city', 'buyer_name'. Prefix with - for descending, e.g. '-recording_date'"),
33
+ });
34
+ export function registerSearchPropertiesTool(server, sfr) {
35
+ registerToolSafe(server, "sfr_search_properties", {
36
+ title: "Search buyer activity / property transactions (SFR)",
37
+ description: "Search property transactions by location with filters for price, date, size, and buyer type. " +
38
+ "Returns individual transaction records with address, price, date, and buyer name. " +
39
+ "Requires search_type + matching location param (e.g. search_type='zip' + zips='85281'). " +
40
+ "BEST TOOL for 'where is [institution] selling?' queries — use seller_name filter (e.g. seller_name='INVITATION' to find Invitation Homes dispositions). " +
41
+ "Supports broader date ranges than sfr_investor_activity. " +
42
+ "To calculate yields, chain results with sfr_rental_stats using the property's zip code: yield = (annual rent / purchase price). " +
43
+ "For market-level summaries without individual records, use sfr_activity_highlights instead.",
44
+ inputSchema: Input,
45
+ }, async (args) => {
46
+ const query = {
47
+ search_type: args.search_type,
48
+ page: args.page,
49
+ page_size: Math.min(args.page_size, 100),
50
+ };
51
+ if (args.state)
52
+ query.state = args.state;
53
+ if (args.city)
54
+ query.city = args.city;
55
+ if (args.zips)
56
+ query.zips = args.zips;
57
+ if (args.msa)
58
+ query.msa = args.msa;
59
+ if (args.latitude)
60
+ query.latitude = args.latitude;
61
+ if (args.longitude)
62
+ query.longitude = args.longitude;
63
+ if (args.radius)
64
+ query.radius = args.radius;
65
+ if (args.sales_price_min)
66
+ query.sales_price_min = args.sales_price_min;
67
+ if (args.sales_price_max)
68
+ query.sales_price_max = args.sales_price_max;
69
+ if (args.sales_date_min)
70
+ query.sales_date_min = args.sales_date_min;
71
+ if (args.sales_date_max)
72
+ query.sales_date_max = args.sales_date_max;
73
+ if (args.beds_min)
74
+ query.beds_min = args.beds_min;
75
+ if (args.beds_max)
76
+ query.beds_max = args.beds_max;
77
+ if (args.building_sqft_min)
78
+ query.building_sqft_min = args.building_sqft_min;
79
+ if (args.building_sqft_max)
80
+ query.building_sqft_max = args.building_sqft_max;
81
+ if (args.property_types)
82
+ query.property_types = args.property_types;
83
+ if (args.seller_name)
84
+ query.seller_name = args.seller_name;
85
+ if (args.purchase_type)
86
+ query.purchase_type = args.purchase_type;
87
+ if (args.buyer_type)
88
+ query.buyer_type = args.buyer_type;
89
+ if (args.sort)
90
+ query.sort = args.sort;
91
+ autoFillDateRange(query);
92
+ // Fetch page + total count in parallel
93
+ const [data, countData] = await Promise.all([
94
+ sfr.getBuyerActivity(query),
95
+ sfr.getActivityCount(query).catch(() => null),
96
+ ]);
97
+ // External API returns a flat array
98
+ const items = Array.isArray(data) ? data : (data?.data ?? []);
99
+ const total = countData?.count ?? countData ?? items.length;
100
+ const lines = [
101
+ `## Property Transactions`,
102
+ `**Total:** ${typeof total === "number" ? total.toLocaleString() : total} | **Page:** ${args.page} | **Showing:** ${items.length}`,
103
+ "",
104
+ ];
105
+ if (items.length > 0) {
106
+ const rows = items.slice(0, 25).map((p) => [
107
+ p.address ?? "—",
108
+ p.city ?? "—",
109
+ p.zipCode ?? "—",
110
+ p.bedrooms ?? "—",
111
+ p.buildingArea ? Number(p.buildingArea).toLocaleString() : "—",
112
+ fmtDollars(p.saleValue),
113
+ p.recordingDate ?? p.saleDate ?? "—",
114
+ p.buyerName ?? "—",
115
+ ]);
116
+ lines.push(markdownTable(["Address", "City", "ZIP", "Beds", "Sqft", "Price", "Date", "Buyer"], rows));
117
+ }
118
+ else {
119
+ lines.push("_No transactions found matching criteria._ " +
120
+ "Try relaxing filters: widen price range, extend date range, or remove seller_name. " +
121
+ "For MSA searches, check that the MSA name matches exactly.");
122
+ }
123
+ // Slim items — raw API has 30+ fields, many null (exit, profit, lat/lng, URL slugs)
124
+ const slimItems = items.slice(0, 25).map((p) => ({
125
+ address: p.address,
126
+ city: p.city,
127
+ zip: p.zipCode,
128
+ state: p.state,
129
+ beds: p.bedrooms || undefined,
130
+ baths: p.bathrooms || undefined,
131
+ sqft: p.buildingArea || undefined,
132
+ yearBuilt: p.yearBuilt || undefined,
133
+ propertyType: p.propertyType,
134
+ saleValue: p.saleValue,
135
+ saleDate: p.saleDate,
136
+ recordingDate: p.recordingDate,
137
+ avmValue: p.avmValue || undefined,
138
+ buyerName: p.buyerName,
139
+ sellerName: p.sellerName,
140
+ isCorporate: p.isCorporate || undefined,
141
+ isCashBuyer: p.isCashBuyer || undefined,
142
+ isDiscountedPurchase: p.isDiscountedPurchase || undefined,
143
+ loanAmount: p.loanAmount || undefined,
144
+ lenderName: p.lenderName || undefined,
145
+ buyerPropertiesCount: p.buyerPropertiesCount || undefined,
146
+ msa: p.msa,
147
+ }));
148
+ const firstAddr = items[0] ? `${items[0].address}, ${items[0].city}, ${items[0].state} ${items[0].zipCode}` : undefined;
149
+ const actions = [
150
+ ...(firstAddr ? [action("sfr_get_property", "Full details on first result", { address: firstAddr })] : []),
151
+ action("sfr_top_buyers", "Who's buying here", locationArgs(args)),
152
+ action("sfr_rental_stats", "Rental market for this area", locationArgs(args)),
153
+ ];
154
+ return structuredResult(lines.join("\n"), { items: slimItems, total, page: args.page }, actions);
155
+ });
156
+ }
157
+ //# sourceMappingURL=searchProperties.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"searchProperties.js","sourceRoot":"","sources":["../../../src/tools/sfr/searchProperties.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { SfrClient } from "../../services/sfr.js";
3
+ export declare function registerTopBuyersTool(server: McpServer, sfr: SfrClient): void;
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ import { structuredResult, markdownTable, argsToQuery } from "../formatters.js";
3
+ import { action } from "../nextActions.js";
4
+ import { registerToolSafe } from "../registerToolSafe.js";
5
+ import { autoFillDateRange } from "../dateHelper.js";
6
+ import { resolveCanonicalMsa } from "./msaResolver.js";
7
+ const Input = z.object({
8
+ search_type: z
9
+ .enum(["state", "city", "zip", "msa"])
10
+ .describe("Location search type"),
11
+ state: z.string().optional().describe("State abbreviation, e.g. 'AZ'"),
12
+ city: z.string().optional().describe("City name (requires state param when search_type='city'), e.g. 'Birmingham'"),
13
+ zips: z.string().optional().describe("Comma-separated zip codes, e.g. '85281'"),
14
+ msa: z.string().optional().describe("MSA name"),
15
+ sales_date_min: z.string().optional().describe("Start date, e.g. '2024-01-01'"),
16
+ sales_date_max: z.string().optional().describe("End date"),
17
+ });
18
+ export function registerTopBuyersTool(server, sfr) {
19
+ registerToolSafe(server, "sfr_top_buyers", {
20
+ title: "Top buyers by geography — state/city/zip/MSA (SFR)",
21
+ description: "Find the most active property buyers/investors in ANY geography (state, city, zip, or MSA) " +
22
+ "ranked by recent acquisition count. Use for 'who is buying in [location]' or " +
23
+ "'where are investors buying in [city/state]' queries. " +
24
+ "Returns buyer names, acquisition counts, contact availability, and profile availability. " +
25
+ "NOTE: search_type='city' requires state param (e.g. city='Birmingham', state='AL'). " +
26
+ "For MSA-only with city/date filters, use sfr_market_highlights. " +
27
+ "For institutional REITs/funds only, use sfr_institutional_owners. " +
28
+ "To drill into a buyer, use sfr_buyer_profile. " +
29
+ "CHAIN: Use plr_borrower_search with a buyer name to check if they borrow from private lenders.",
30
+ inputSchema: Input,
31
+ }, async (args) => {
32
+ const query = argsToQuery(args);
33
+ autoFillDateRange(query);
34
+ let effectiveMsa = args.msa;
35
+ let resolutionMethod = "exact";
36
+ const data = await sfr.getTopBuyers(query);
37
+ let buyers = Array.isArray(data) ? data : data?.data ?? [];
38
+ if (buyers.length === 0 && args.search_type === "msa" && args.msa) {
39
+ resolutionMethod = "none";
40
+ const resolved = await resolveCanonicalMsa(sfr, { requestedMsa: args.msa });
41
+ if (resolved.resolvedMsa
42
+ && resolved.resolvedMsa.toLowerCase() !== args.msa.trim().toLowerCase()) {
43
+ effectiveMsa = resolved.resolvedMsa;
44
+ const retryData = await sfr.getTopBuyers({ ...query, msa: effectiveMsa });
45
+ buyers = Array.isArray(retryData) ? retryData : retryData?.data ?? [];
46
+ if (buyers.length > 0)
47
+ resolutionMethod = "fallback";
48
+ }
49
+ }
50
+ const loc = args.zips ?? args.city ?? (effectiveMsa ?? args.msa) ?? args.state ?? "All";
51
+ const lines = [
52
+ `## Top Buyers — ${loc}`,
53
+ `**Showing:** ${buyers.length} investors`,
54
+ "",
55
+ ];
56
+ if (resolutionMethod === "fallback") {
57
+ lines.push(`_MSA normalized: '${args.msa}' -> '${effectiveMsa}'_`, "");
58
+ }
59
+ if (buyers.length > 0) {
60
+ const sorted = [...buyers].sort((a, b) => (b.acquisitions ?? 0) - (a.acquisitions ?? 0));
61
+ const rows = sorted.slice(0, 30).map((b, i) => [
62
+ i + 1,
63
+ b.name ?? "—",
64
+ b.acquisitions ?? "—",
65
+ b.hasContactInfo ? "Yes" : "No",
66
+ b.hasProfilePage ? "Yes" : "No",
67
+ ]);
68
+ lines.push(markdownTable(["#", "Buyer", "Acquisitions", "Contact Info", "Profile"], rows));
69
+ }
70
+ else {
71
+ lines.push("_No buyers found for this location._ " +
72
+ "Verify search_type matches the location param (e.g. search_type='city' needs city= and state=). " +
73
+ "For MSA searches, the name must match exactly — try sfr_activity_highlights for a quick check.");
74
+ }
75
+ // Slim buyers — strip formattedName URL slugs and unnecessary metadata
76
+ const slimBuyers = buyers.slice(0, 50).map((b) => ({
77
+ name: b.name ?? b.buyer_name,
78
+ acquisitions: b.acquisitions ?? b.count,
79
+ hasProfile: b.hasProfilePage || undefined,
80
+ hasContact: b.hasContactInfo || undefined,
81
+ }));
82
+ const firstName = buyers[0]?.name ?? buyers[0]?.buyer_name;
83
+ const actions = [
84
+ ...(firstName ? [action("sfr_buyer_profile", "Profile top buyer", { name: firstName })] : []),
85
+ ...(firstName ? [action("sfr_buyer_growth", "Growth trend for top buyer", { name: firstName })] : []),
86
+ ...(firstName ? [action("plr_borrower_search", "Check if top buyer borrows privately", { search: firstName })] : []),
87
+ ];
88
+ return structuredResult(lines.join("\n"), { items: slimBuyers, total: buyers.length, page: 1 }, actions);
89
+ });
90
+ }
91
+ //# sourceMappingURL=topBuyers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"topBuyers.js","sourceRoot":"","sources":["../../../src/tools/sfr/topBuyers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { SfrClient } from "../../services/sfr.js";
3
+ export declare function registerZipDetailTool(server: McpServer, sfr: SfrClient): void;
@@ -0,0 +1,85 @@
1
+ import { z } from "zod";
2
+ import { structuredResult, kvSummary, truncatedJson } from "../formatters.js";
3
+ import { action } from "../nextActions.js";
4
+ import { registerToolSafe } from "../registerToolSafe.js";
5
+ const Input = z.object({
6
+ zipCode: z.string().describe("5-digit zip code, e.g. '85281'"),
7
+ });
8
+ export function registerZipDetailTool(server, sfr) {
9
+ registerToolSafe(server, "sfr_zip_detail", {
10
+ title: "Detailed stats for a single zip code (SFR)",
11
+ description: "Get comprehensive investment metrics for a specific zip code: population, income, " +
12
+ "home values, rent, yield, institutional ownership, transaction volume, and trends. " +
13
+ "More detailed than sfr_zip_finder which ranks many zips — this deep-dives into one.",
14
+ inputSchema: Input,
15
+ }, async (args) => {
16
+ const data = await sfr.getZipDetail(args.zipCode);
17
+ if (!data || typeof data !== "object") {
18
+ return structuredResult(`## Zip Code Detail — ${args.zipCode}\n\n_No data found._`, {});
19
+ }
20
+ const fmt$ = (v) => v != null ? `$${Math.round(Number(v)).toLocaleString()}` : undefined;
21
+ const fmtPct01 = (v) => v != null ? `${(Number(v) * 100).toFixed(1)}%` : undefined;
22
+ const fmtNum = (v) => v != null ? Number(v).toLocaleString() : undefined;
23
+ const lines = [
24
+ `## Zip Code Detail — ${data.zipCode ?? args.zipCode}`,
25
+ "",
26
+ "### Demographics",
27
+ kvSummary([
28
+ ["Population", fmtNum(data.population)],
29
+ ["Population Density", data.populationDensity != null
30
+ ? `${Number(data.populationDensity).toFixed(0)}/sq mi` : undefined],
31
+ ["Housing Units", fmtNum(data.housingUnits)],
32
+ ["Median Household Income", fmt$(data.medianHouseholdIncome)],
33
+ ["Income vs MSA", fmtPct01(data.incomeRelativeToMsa)],
34
+ ]),
35
+ "",
36
+ "### Investment Metrics",
37
+ kvSummary([
38
+ ["Median Home Value", fmt$(data.medianHomeValue)],
39
+ ["Median Rent", data.medianRent != null ? `$${Math.round(Number(data.medianRent)).toLocaleString()}/mo` : undefined],
40
+ ["Gross Yield", fmtPct01(data.grossYield)],
41
+ ["Inst. Ownership", fmtPct01(data.instOwnPercentage)],
42
+ ]),
43
+ ];
44
+ // Append any additional fields as raw JSON
45
+ const knownKeys = new Set([
46
+ "zipCode", "population", "populationDensity", "populationGrowthForecast",
47
+ "housingUnits", "medianHouseholdIncome", "medianHomeValue", "medianRent",
48
+ "instOwnPercentage", "grossYield", "incomeRelativeToMsa",
49
+ ]);
50
+ const extra = {};
51
+ for (const [k, v] of Object.entries(data)) {
52
+ if (!knownKeys.has(k) && v != null)
53
+ extra[k] = v;
54
+ }
55
+ if (Object.keys(extra).length > 0) {
56
+ lines.push("");
57
+ lines.push("### Additional Data");
58
+ lines.push("```json");
59
+ lines.push(truncatedJson(extra, 2000));
60
+ lines.push("```");
61
+ }
62
+ const zip = args.zipCode;
63
+ const actions = [
64
+ action("sfr_rental_stats", "Rental market stats", { search_type: "zip", zips: zip }),
65
+ action("sfr_top_buyers", "Active buyers in zip", { search_type: "zip", zips: zip }),
66
+ action("sfr_search_properties", "Browse transactions", { search_type: "zip", zips: zip }),
67
+ action("sfr_institutional_owners", "Institutional owners in zip", { search_type: "zip", zips: zip }),
68
+ ];
69
+ const structured = {
70
+ zipCode: data.zipCode,
71
+ population: data.population,
72
+ populationDensity: data.populationDensity,
73
+ populationGrowthForecast: data.populationGrowthForecast,
74
+ housingUnits: data.housingUnits,
75
+ medianHouseholdIncome: data.medianHouseholdIncome,
76
+ medianHomeValue: data.medianHomeValue,
77
+ medianRent: data.medianRent,
78
+ grossYield: data.grossYield,
79
+ instOwnPercentage: data.instOwnPercentage,
80
+ incomeRelativeToMsa: data.incomeRelativeToMsa,
81
+ };
82
+ return structuredResult(lines.join("\n"), structured, actions);
83
+ });
84
+ }
85
+ //# sourceMappingURL=zipDetail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zipDetail.js","sourceRoot":"","sources":["../../../src/tools/sfr/zipDetail.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { SfrClient } from "../../services/sfr.js";
3
+ export declare function registerZipFinderTool(server: McpServer, sfr: SfrClient): void;
@@ -0,0 +1,79 @@
1
+ import { z } from "zod";
2
+ import { structuredResult, markdownTable, truncatedJson } from "../formatters.js";
3
+ import { action } from "../nextActions.js";
4
+ import { registerToolSafe } from "../registerToolSafe.js";
5
+ const Input = z.object({
6
+ search_type: z
7
+ .enum(["all", "zip"])
8
+ .default("all")
9
+ .describe("Search type: 'all' for nationwide or 'zip' to filter specific zips"),
10
+ zips: z.string().optional().describe("Comma-separated zip codes (required when search_type='zip')"),
11
+ page: z.coerce.number().default(1).describe("Page number (default 1)"),
12
+ page_size: z.coerce.number().default(25).describe("Results per page (default 25, max 100)"),
13
+ sort: z.string().optional().describe("Sort field, e.g. '-gross_yield' for descending gross yield"),
14
+ });
15
+ export function registerZipFinderTool(server, sfr) {
16
+ registerToolSafe(server, "sfr_zip_finder", {
17
+ title: "Find & rank zip codes by investment metrics (SFR)",
18
+ description: "Screen and rank zip codes by investment metrics: gross yield, median home value, " +
19
+ "rent, income, population, and institutional ownership. " +
20
+ "Use search_type='all' for nationwide ranking or 'zip' with specific codes. " +
21
+ "Sort by '-gross_yield' for highest-yielding zips. " +
22
+ "For detailed stats on a single zip, use sfr_zip_detail.",
23
+ inputSchema: Input,
24
+ }, async (args) => {
25
+ const query = {
26
+ search_type: args.search_type,
27
+ page: args.page,
28
+ page_size: args.page_size,
29
+ };
30
+ if (args.zips)
31
+ query.zips = args.zips;
32
+ if (args.sort)
33
+ query.sort = args.sort;
34
+ const data = await sfr.getZipFinder(query);
35
+ const items = data?.data ?? data?.results ?? (Array.isArray(data) ? data : []);
36
+ const total = data?.total ?? data?.count ?? items.length;
37
+ const lines = [
38
+ `## Zip Code Finder`,
39
+ `**Total:** ${total} zips | **Page:** ${args.page}`,
40
+ "",
41
+ ];
42
+ if (items.length > 0) {
43
+ const fmt = (v, prefix = "") => v != null ? `${prefix}${Math.round(Number(v)).toLocaleString()}` : "—";
44
+ // grossYield is 0-1 scale; API returns corrupt values >1.0 for zips with null rent
45
+ const fmtYield = (v) => {
46
+ if (v == null)
47
+ return "—";
48
+ const n = Number(v);
49
+ if (n > 0.40)
50
+ return "⚠️ suspect";
51
+ return `${(n * 100).toFixed(1)}%`;
52
+ };
53
+ const pctScale01 = (v) => v != null ? `${(Number(v) * 100).toFixed(1)}%` : "—";
54
+ const rows = items.slice(0, 30).map((z) => [
55
+ z.zip_code ?? z.zipCode ?? z.zip,
56
+ fmt(z.population),
57
+ fmt(z.medianHouseholdIncome ?? z.median_household_income, "$"),
58
+ fmt(z.medianHomeValue ?? z.median_home_value, "$"),
59
+ fmt(z.medianRent ?? z.median_rent, "$"),
60
+ fmtYield(z.grossYield ?? z.gross_yield),
61
+ pctScale01(z.instOwnPercentage ?? z.inst_own_percentage),
62
+ ]);
63
+ lines.push(markdownTable(["Zip", "Pop", "Income", "Home Value", "Rent", "Yield", "Inst Own"], rows));
64
+ }
65
+ else {
66
+ lines.push("```json");
67
+ lines.push(truncatedJson(data, 3000));
68
+ lines.push("```");
69
+ }
70
+ const topZip = items[0]?.zip_code ?? items[0]?.zipCode ?? items[0]?.zip;
71
+ const actions = [
72
+ ...(topZip ? [action("sfr_zip_detail", "Deep-dive into top zip", { zipCode: String(topZip) })] : []),
73
+ ...(topZip ? [action("sfr_rental_stats", "Rental market for top zip", { search_type: "zip", zips: String(topZip) })] : []),
74
+ ...(topZip ? [action("sfr_top_buyers", "Active buyers in top zip", { search_type: "zip", zips: String(topZip) })] : []),
75
+ ];
76
+ return structuredResult(lines.join("\n"), { items, total, page: args.page }, actions);
77
+ });
78
+ }
79
+ //# sourceMappingURL=zipFinder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zipFinder.js","sourceRoot":"","sources":["../../../src/tools/sfr/zipFinder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ /** Convert a buyer display name like "INVITATION HOMES" to a URL slug like "invitation-homes" */
2
+ export declare function slugifyBuyerName(name: string): string;
@@ -0,0 +1,5 @@
1
+ /** Convert a buyer display name like "INVITATION HOMES" to a URL slug like "invitation-homes" */
2
+ export function slugifyBuyerName(name) {
3
+ return name.trim().toLowerCase().replace(/\s+/g, "-");
4
+ }
5
+ //# sourceMappingURL=slugHelper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slugHelper.js","sourceRoot":"","sources":["../../src/tools/slugHelper.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACxD,CAAC"}