@sfranalytics/mcp 0.6.2 → 0.6.3

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 (42) hide show
  1. package/README.md +67 -50
  2. package/dist/analytics/logger.d.ts +3 -0
  3. package/dist/analytics/logger.js +39 -0
  4. package/dist/analytics/logger.js.map +1 -0
  5. package/dist/analytics/requestContext.d.ts +21 -0
  6. package/dist/analytics/requestContext.js +39 -0
  7. package/dist/analytics/requestContext.js.map +1 -0
  8. package/dist/analytics/sanitize.d.ts +1 -0
  9. package/dist/analytics/sanitize.js +78 -0
  10. package/dist/analytics/sanitize.js.map +1 -0
  11. package/dist/http.js +193 -40
  12. package/dist/http.js.map +1 -1
  13. package/dist/index.js +17 -4
  14. package/dist/index.js.map +1 -1
  15. package/dist/landing.d.ts +3 -0
  16. package/dist/landing.js +891 -0
  17. package/dist/landing.js.map +1 -0
  18. package/dist/server.d.ts +5 -1
  19. package/dist/server.js +12 -4
  20. package/dist/server.js.map +1 -1
  21. package/dist/services/httpClient.d.ts +22 -0
  22. package/dist/services/httpClient.js +147 -26
  23. package/dist/services/httpClient.js.map +1 -1
  24. package/dist/tools/formatters.js +29 -1
  25. package/dist/tools/formatters.js.map +1 -1
  26. package/dist/tools/health.js +84 -7
  27. package/dist/tools/health.js.map +1 -1
  28. package/dist/tools/plr/borrowerContacts.js +47 -10
  29. package/dist/tools/plr/borrowerContacts.js.map +1 -1
  30. package/dist/tools/plr/borrowerProfile.js +11 -5
  31. package/dist/tools/plr/borrowerProfile.js.map +1 -1
  32. package/dist/tools/registerToolSafe.d.ts +5 -1
  33. package/dist/tools/registerToolSafe.js +110 -4
  34. package/dist/tools/registerToolSafe.js.map +1 -1
  35. package/dist/tools/sfr/rentalStats.js +19 -3
  36. package/dist/tools/sfr/rentalStats.js.map +1 -1
  37. package/dist/tools/sfr/topBuyers.js +1 -0
  38. package/dist/tools/sfr/topBuyers.js.map +1 -1
  39. package/dist/tools/welcome.d.ts +3 -0
  40. package/dist/tools/welcome.js +58 -0
  41. package/dist/tools/welcome.js.map +1 -0
  42. package/package.json +2 -2
@@ -0,0 +1,891 @@
1
+ import { VERSION } from "./version.js";
2
+ /* Inline the marketing-site logo SVG with white-background rect removed */
3
+ const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="174" height="53" fill="none" viewBox="0 0 174 53"><g clip-path="url(#a)"><path fill="#7D7DB6" stroke="#fff" stroke-width="3" d="m49.88-1.06-1.06-1.061-1.061 1.06-29.345 29.345-1.06 1.06 1.06 1.062 4.95 4.95 1.06 1.06 1.06-1.06L48.82 12.02l23.335 23.334 1.06 1.061 1.061-1.06 4.95-4.95 1.06-1.061-1.06-1.06L49.88-1.062z" opacity=".3"/><path fill="#7D7DB6" d="m39.88 8.94-1.06-1.061-1.061 1.06L8.414 38.284l-1.06 1.06 1.06 1.062 4.95 4.95 1.06 1.06 1.06-1.06L38.82 22.02l23.335 23.334 1.06 1.061 1.061-1.06 4.95-4.95 1.06-1.061-1.06-1.06L39.88 8.938z"/><path stroke="#fff" stroke-width="3" d="m39.88 8.94-1.06-1.061-1.061 1.06L8.414 38.284l-1.06 1.06 1.06 1.062 4.95 4.95 1.06 1.06 1.06-1.06L38.82 22.02l23.335 23.334 1.06 1.061 1.061-1.06 4.95-4.95 1.06-1.061-1.06-1.06L39.88 8.938z"/><path fill="#000085" d="m30.405 17.99-1.06-1.061-1.06 1.06-29.346 29.345-1.06 1.061 1.06 1.06 4.95 4.95 1.06 1.061 1.061-1.06L29.345 31.07l23.334 23.335 1.06 1.06 1.062-1.06 4.95-4.95 1.06-1.06-1.06-1.062L30.404 17.99z"/><path stroke="#fff" stroke-width="3" d="m30.405 17.99-1.06-1.061-1.06 1.06-29.346 29.345-1.06 1.061 1.06 1.06 4.95 4.95 1.06 1.061 1.061-1.06L29.345 31.07l23.334 23.335 1.06 1.06 1.062-1.06 4.95-4.95 1.06-1.06-1.06-1.062L30.404 17.99z"/><path fill="#000085" d="M129.727 27V3.727h9.181c1.758 0 3.258.315 4.5.943 1.25.622 2.201 1.504 2.853 2.648.659 1.137.988 2.474.988 4.011 0 1.546-.333 2.875-1 3.99-.666 1.105-1.632 1.954-2.897 2.545-1.258.59-2.781.886-4.569.886h-6.147v-3.955h5.352c.939 0 1.72-.128 2.341-.386.621-.258 1.083-.644 1.386-1.159.311-.515.466-1.155.466-1.92 0-.773-.155-1.425-.466-1.955-.303-.53-.769-.932-1.398-1.205-.621-.28-1.405-.42-2.352-.42h-3.318V27h-4.92zm12.568-10.59L148.079 27h-5.432l-5.659-10.59h5.307zM111.648 27V3.727h15.41v4.057h-10.489v5.545h9.466v4.057h-9.466V27h-4.921zm-7.704-16.58c-.091-.916-.481-1.628-1.17-2.136-.69-.507-1.625-.761-2.807-.761-.803 0-1.481.113-2.034.34-.553.22-.977.527-1.273.921a2.215 2.215 0 0 0-.432 1.341c-.015.417.072.78.262 1.09.197.312.466.58.807.808.34.22.734.413 1.181.58.447.158.925.295 1.432.408l2.091.5c1.015.228 1.947.53 2.796.91a8.489 8.489 0 0 1 2.204 1.397 5.812 5.812 0 0 1 1.443 1.955c.349.75.527 1.61.534 2.58-.007 1.423-.371 2.658-1.09 3.704-.713 1.038-1.743 1.845-3.091 2.42-1.341.569-2.959.852-4.853.852-1.879 0-3.515-.287-4.909-.863-1.386-.576-2.47-1.428-3.25-2.557-.772-1.136-1.178-2.542-1.216-4.216h4.762c.053.78.276 1.432.67 1.955.402.515.936.905 1.602 1.17.675.258 1.436.386 2.285.386.833 0 1.556-.12 2.17-.363.621-.242 1.102-.58 1.443-1.012.341-.431.512-.928.512-1.488 0-.523-.156-.962-.466-1.318-.303-.356-.75-.66-1.341-.91-.584-.25-1.3-.477-2.148-.681l-2.534-.636c-1.962-.478-3.511-1.224-4.648-2.24-1.136-1.014-1.7-2.382-1.693-4.101-.008-1.41.367-2.64 1.125-3.694.765-1.053 1.814-1.875 3.148-2.466 1.333-.59 2.848-.886 4.545-.886 1.727 0 3.235.296 4.523.886 1.295.591 2.303 1.413 3.023 2.466.719 1.053 1.091 2.273 1.113 3.66h-4.716z"/><path fill="#7D7DB6" d="m172.851 39.754-1.925.341a2.272 2.272 0 0 0-.383-.703 1.906 1.906 0 0 0-.696-.547c-.294-.142-.661-.213-1.101-.213-.601 0-1.103.135-1.506.405-.402.265-.603.608-.603 1.03 0 .364.134.658.404.88.27.223.706.405 1.307.547l1.733.398c1.004.232 1.752.59 2.244 1.072.493.483.739 1.11.739 1.883a2.86 2.86 0 0 1-.568 1.747c-.374.506-.897.904-1.57 1.193-.667.289-1.441.433-2.322.433-1.222 0-2.218-.26-2.99-.781-.772-.526-1.245-1.271-1.421-2.237l2.053-.313c.128.535.391.94.788 1.215.398.27.916.404 1.556.404.696 0 1.252-.144 1.669-.433.416-.293.625-.65.625-1.072 0-.341-.128-.627-.384-.86-.251-.232-.637-.407-1.157-.525l-1.847-.405c-1.018-.232-1.771-.601-2.259-1.108-.483-.506-.724-1.148-.724-1.925 0-.644.18-1.207.54-1.69.36-.483.857-.86 1.491-1.13.635-.274 1.361-.411 2.181-.411 1.179 0 2.107.256 2.784.767.677.506 1.124 1.186 1.342 2.038zm-14.527 8.466c-1.055 0-1.964-.239-2.727-.717a4.751 4.751 0 0 1-1.747-1.996c-.407-.847-.611-1.818-.611-2.912 0-1.108.209-2.085.625-2.933.417-.852 1.004-1.518 1.762-1.996.757-.478 1.65-.717 2.677-.717.829 0 1.567.154 2.216.462a3.976 3.976 0 0 1 1.57 1.278c.402.55.641 1.19.717 1.925h-2.067a2.473 2.473 0 0 0-.781-1.321c-.402-.37-.942-.554-1.619-.554-.592 0-1.111.156-1.556.468-.44.308-.783.748-1.03 1.321-.246.569-.369 1.24-.369 2.017 0 .796.121 1.483.362 2.06.242.578.583 1.025 1.023 1.342.445.318.968.476 1.57.476.402 0 .767-.073 1.093-.22.332-.151.609-.367.831-.646.228-.28.386-.616.476-1.009h2.067a4.013 4.013 0 0 1-.689 1.89c-.383.553-.897.99-1.541 1.306-.639.318-1.39.476-2.252.476zm-9.175-.22V37.09h2.123V48h-2.123zm1.072-12.592c-.369 0-.686-.123-.951-.37a1.2 1.2 0 0 1-.391-.894c0-.35.13-.65.391-.895.265-.251.582-.377.951-.377.37 0 .684.126.945.377.265.246.398.544.398.895 0 .345-.133.643-.398.894-.261.247-.575.37-.945.37zm-3.022 1.682v1.705h-5.959v-1.704h5.959zm-4.361-2.613h2.124v10.32c0 .412.061.722.184.93a.968.968 0 0 0 .476.42c.199.07.415.106.647.106.17 0 .319-.012.447-.036.128-.023.227-.042.298-.056l.384 1.754a3.55 3.55 0 0 1-.526.142 4.15 4.15 0 0 1-.852.085 3.734 3.734 0 0 1-1.562-.298 2.678 2.678 0 0 1-1.172-.966c-.299-.436-.448-.983-.448-1.64v-10.76zM132.521 52.09c-.317 0-.606-.025-.866-.077a2.725 2.725 0 0 1-.583-.157l.512-1.74c.388.105.733.15 1.036.135a1.24 1.24 0 0 0 .803-.34c.237-.214.445-.562.625-1.044l.263-.725-3.992-11.051h2.273l2.763 8.466h.113l2.763-8.466h2.28l-4.496 12.365a4.925 4.925 0 0 1-.795 1.442 3.09 3.09 0 0 1-1.151.895c-.445.199-.961.298-1.548.298zm-3.799-18.635V48h-2.123V33.455h2.123zm-9.935 14.787c-.692 0-1.317-.128-1.875-.384a3.152 3.152 0 0 1-1.328-1.13c-.322-.492-.483-1.095-.483-1.81 0-.616.118-1.123.355-1.52a2.61 2.61 0 0 1 .959-.945 5.002 5.002 0 0 1 1.349-.526 13.42 13.42 0 0 1 1.52-.27l1.591-.184c.407-.052.703-.135.888-.249.184-.113.277-.298.277-.554v-.05c0-.62-.176-1.1-.526-1.441-.346-.34-.862-.511-1.548-.511-.715 0-1.279.158-1.691.476-.407.312-.689.66-.845 1.044l-1.996-.455c.237-.663.583-1.198 1.037-1.605a4.22 4.22 0 0 1 1.584-.895 6.185 6.185 0 0 1 1.882-.284c.436 0 .897.052 1.385.156.493.1.952.284 1.378.554.431.27.784.656 1.058 1.158.275.497.412 1.143.412 1.939V48h-2.074v-1.492h-.085a3.01 3.01 0 0 1-.618.81 3.274 3.274 0 0 1-1.058.66c-.431.176-.947.264-1.548.264zm.461-1.705c.587 0 1.089-.116 1.506-.348.421-.232.741-.535.959-.91a2.36 2.36 0 0 0 .334-1.214V42.66c-.076.076-.223.147-.441.213a6.843 6.843 0 0 1-.731.163c-.275.043-.542.083-.803.121-.26.033-.478.062-.653.086a5.31 5.31 0 0 0-1.129.262c-.337.123-.607.3-.81.533-.199.227-.298.53-.298.909 0 .526.194.923.582 1.193.388.265.883.398 1.484.398zm-13.066-5.014V48h-2.123V37.09h2.038v1.776h.135a3.119 3.119 0 0 1 1.179-1.392c.54-.35 1.219-.525 2.038-.525.744 0 1.395.156 1.953.469.559.307.992.767 1.3 1.377.308.611.462 1.366.462 2.266V48h-2.124v-6.683c0-.791-.206-1.409-.618-1.854-.412-.45-.978-.675-1.697-.675-.493 0-.931.107-1.314.32-.379.213-.68.526-.902.937-.218.408-.327.9-.327 1.478zM91.854 48h-2.33l5.235-14.545h2.535L102.53 48h-2.33l-4.112-11.903h-.114L91.854 48zm.39-5.696h7.557v1.847h-7.556v-1.847z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h174v53H0z"/></clipPath></defs></svg>`;
4
+ export function renderLandingPage(opts) {
5
+ const crispId = opts?.crispWebsiteId || "";
6
+ return `<!DOCTYPE html>
7
+ <html lang="en">
8
+ <head>
9
+ <meta charset="utf-8">
10
+ <meta name="viewport" content="width=device-width, initial-scale=1">
11
+ <title>SFR Analytics — MCP Server</title>
12
+ <meta name="description" content="MCP server for single-family residential property data, buyer intelligence, and private lending insights.">
13
+
14
+ <!-- Open Graph / Social -->
15
+ <meta property="og:type" content="website">
16
+ <meta property="og:title" content="SFR Analytics — MCP Server">
17
+ <meta property="og:description" content="39 tools for single-family residential data: property transactions, buyer intelligence, rental analytics, and private lending insights. Connect via Claude Code, Claude Desktop, or any MCP client.">
18
+ <meta property="og:url" content="https://mcp.sfranalytics.com">
19
+ <meta property="og:image" content="https://sfranalytics.com/logo.png">
20
+ <meta property="og:image:width" content="800">
21
+ <meta property="og:image:height" content="600">
22
+ <meta property="og:image:alt" content="SFR Analytics Logo">
23
+ <meta property="og:site_name" content="SFR Analytics">
24
+
25
+ <!-- Twitter -->
26
+ <meta name="twitter:card" content="summary_large_image">
27
+ <meta name="twitter:title" content="SFR Analytics — MCP Server">
28
+ <meta name="twitter:description" content="39 tools for single-family residential data: property transactions, buyer intelligence, rental analytics, and private lending insights.">
29
+ <meta name="twitter:image" content="https://sfranalytics.com/logo.png">
30
+
31
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%23000085'/><text x='16' y='23' font-size='20' font-weight='bold' text-anchor='middle' fill='white'>S</text></svg>">
32
+ <link rel="preconnect" href="https://fonts.googleapis.com">
33
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
34
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
35
+ <style>
36
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
37
+
38
+ :root {
39
+ --brand: #000085;
40
+ --brand-light: #4D4DAA;
41
+ --brand-lavender: #7D7DB6;
42
+ --bg: #fbfcfe;
43
+ --bg2: #f4f6fb;
44
+ --surface: rgba(255,255,255,0.72);
45
+ --surface-solid: #ffffff;
46
+ --border: rgba(15,23,42,0.08);
47
+ --border-strong: rgba(15,23,42,0.14);
48
+ --text: rgba(15,23,42,0.85);
49
+ --text-strong: rgba(15,23,42,0.92);
50
+ --text-secondary: rgba(15,23,42,0.62);
51
+ --text-muted: rgba(15,23,42,0.48);
52
+ --green: #16a34a;
53
+ --red: #dc2626;
54
+ --amber: #d97706;
55
+ --code-bg: rgba(2,6,23,0.92);
56
+ --radius: 14px;
57
+ --radius-sm: 10px;
58
+ --radius-xs: 6px;
59
+ --shadow: 0 1px 2px rgba(15,23,42,0.04), 0 4px 16px rgba(15,23,42,0.05);
60
+ --max-w: 1060px;
61
+ --transition: 160ms ease;
62
+ --mono: 'SF Mono', 'Fira Code', ui-monospace, monospace;
63
+ }
64
+
65
+ body {
66
+ font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
67
+ background:
68
+ radial-gradient(1200px 600px at 10% -8%, rgba(0,0,133,0.06), transparent 60%),
69
+ radial-gradient(900px 500px at 90% 5%, rgba(125,125,182,0.05), transparent 55%),
70
+ linear-gradient(180deg, var(--bg), var(--bg2));
71
+ color: var(--text);
72
+ line-height: 1.55;
73
+ -webkit-font-smoothing: antialiased;
74
+ }
75
+
76
+ a { color: var(--brand); text-decoration: none; }
77
+ a:hover { text-decoration: underline; }
78
+
79
+ :focus-visible {
80
+ outline: 2px solid rgba(0,0,133,0.25);
81
+ outline-offset: 2px;
82
+ border-radius: var(--radius-xs);
83
+ }
84
+
85
+ @media (prefers-reduced-motion: reduce) {
86
+ *, *::before, *::after { transition: none !important; animation: none !important; }
87
+ }
88
+
89
+ /* --- Nav --- */
90
+ .nav-wrap {
91
+ position: sticky; top: 0; z-index: 10;
92
+ background: rgba(251,252,254,0.8);
93
+ backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
94
+ border-bottom: 1px solid var(--border);
95
+ }
96
+ nav {
97
+ display: flex; align-items: center; justify-content: space-between;
98
+ max-width: var(--max-w); margin: 0 auto; padding: 0 1.5rem; height: 56px;
99
+ }
100
+ .logo { display: flex; align-items: center; }
101
+ .logo svg { height: 30px; width: auto; }
102
+ .nav-right { display: flex; align-items: center; gap: 1.5rem; font-size: 0.875rem; }
103
+ .nav-right a { color: var(--text-secondary); font-weight: 500; }
104
+ .nav-right a:hover { color: var(--text); text-decoration: none; }
105
+ .version-badge {
106
+ background: rgba(15,23,42,0.04); border: 1px solid var(--border); border-radius: 999px;
107
+ padding: 0.15rem 0.55rem; font-size: 0.72rem; font-weight: 500;
108
+ color: var(--text-muted); font-family: var(--mono);
109
+ }
110
+ .nav-cta {
111
+ background: var(--brand); color: #fff !important; font-weight: 600;
112
+ font-size: 0.8rem; padding: 0.35rem 0.9rem; border-radius: 8px;
113
+ transition: opacity var(--transition);
114
+ }
115
+ .nav-cta:hover { opacity: 0.88; text-decoration: none !important; }
116
+
117
+ /* --- Hero (stacked) --- */
118
+ .hero {
119
+ text-align: center;
120
+ padding: 4.5rem 1.5rem 2.5rem;
121
+ max-width: var(--max-w); margin: 0 auto;
122
+ }
123
+ .hero h1 {
124
+ font-size: clamp(2rem, 1.4rem + 2.6vw, 2.8rem); font-weight: 750;
125
+ letter-spacing: -0.025em; line-height: 1.08; margin-bottom: 1rem;
126
+ color: var(--text-strong);
127
+ }
128
+ .hero h1 .accent {
129
+ background: linear-gradient(135deg, var(--brand), var(--brand-light));
130
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
131
+ }
132
+ .hero-lead {
133
+ font-size: 1.05rem; color: var(--text-secondary); line-height: 1.6;
134
+ max-width: 54ch; margin: 0 auto 1.75rem;
135
+ }
136
+ .hero-cta-row { display: flex; gap: 0.65rem; flex-wrap: wrap; justify-content: center; }
137
+ .btn-primary {
138
+ display: inline-flex; align-items: center; height: 40px;
139
+ background: var(--brand); color: #fff;
140
+ font-weight: 600; font-size: 0.875rem; padding: 0 1.25rem;
141
+ border-radius: 10px; transition: transform var(--transition), box-shadow var(--transition);
142
+ }
143
+ .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,133,0.2); text-decoration: none; }
144
+ .btn-secondary {
145
+ display: inline-flex; align-items: center; height: 40px;
146
+ background: var(--surface); color: var(--text);
147
+ font-weight: 600; font-size: 0.875rem; padding: 0 1.25rem;
148
+ border: 1px solid var(--border-strong); border-radius: 10px;
149
+ transition: border-color var(--transition), background var(--transition);
150
+ }
151
+ .btn-secondary:hover { border-color: rgba(15,23,42,0.22); background: rgba(255,255,255,0.9); text-decoration: none; }
152
+
153
+ /* --- Stepper --- */
154
+ .hero-panel {
155
+ background: var(--surface); border: 1px solid var(--border);
156
+ border-radius: var(--radius); padding: 1.75rem 2rem;
157
+ max-width: 720px; margin: 2rem auto 0; text-align: left;
158
+ }
159
+ .stepper { display: flex; flex-direction: column; gap: 0.25rem; }
160
+ .step {
161
+ display: grid; grid-template-columns: 20px 1fr; gap: 0.85rem;
162
+ padding: 0.75rem 0.5rem; border-radius: var(--radius-sm);
163
+ transition: background var(--transition);
164
+ }
165
+ .step-indicator { display: flex; flex-direction: column; align-items: center; }
166
+ .step-dot {
167
+ width: 8px; height: 8px; border-radius: 50%;
168
+ background: rgba(0,0,133,0.35); margin-top: 6px; flex-shrink: 0;
169
+ }
170
+ .step-line {
171
+ width: 1px; flex: 1; background: var(--border-strong); margin: 4px 0;
172
+ }
173
+ .step:last-child .step-line { display: none; }
174
+ .step-content { min-width: 0; }
175
+ .step-title {
176
+ font-size: 0.9rem; font-weight: 600; color: var(--text-strong); margin-bottom: 0.25rem;
177
+ }
178
+ .step-desc {
179
+ font-size: 0.82rem; color: var(--text-secondary); margin-bottom: 0.5rem; line-height: 1.5;
180
+ }
181
+ .step-content .code-block { margin-top: 0.25rem; }
182
+ .step-content .code-block pre { padding-right: 4rem; }
183
+ .step-label {
184
+ font-size: 0.78rem; font-weight: 500; color: var(--text-muted);
185
+ margin-bottom: 0.4rem; letter-spacing: 0.01em;
186
+ }
187
+ .stepper .copy-btn {
188
+ opacity: 1; transform: translateY(0);
189
+ background: rgba(99,102,241,0.25); border-color: rgba(99,102,241,0.35);
190
+ color: rgba(226,232,240,0.95);
191
+ }
192
+ .stepper .copy-btn:hover { background: rgba(99,102,241,0.4); color: #fff; }
193
+
194
+ /* --- Segmented control --- */
195
+ .seg-control {
196
+ display: inline-flex; background: rgba(15,23,42,0.04); border: 1px solid var(--border);
197
+ border-radius: 8px; padding: 2px; margin-bottom: 0.5rem;
198
+ }
199
+ .seg-btn {
200
+ background: transparent; border: none; padding: 0.25rem 0.7rem;
201
+ font-size: 0.75rem; font-weight: 500; font-family: inherit;
202
+ color: var(--text-muted); border-radius: 6px; cursor: pointer;
203
+ transition: background var(--transition), color var(--transition);
204
+ }
205
+ .seg-btn.active { background: var(--surface-solid); color: var(--text-strong); box-shadow: 0 1px 2px rgba(0,0,0,0.06); }
206
+ .client-block { display: none; }
207
+ .client-block.active { display: block; }
208
+
209
+ /* --- Desktop toggle --- */
210
+ .desktop-toggle {
211
+ font-size: 0.72rem; color: var(--text-muted); cursor: pointer;
212
+ margin-top: 0.3rem; display: inline-block;
213
+ }
214
+ .desktop-toggle:hover { color: var(--text-secondary); }
215
+ .desktop-content { display: none; margin-top: 0.5rem; }
216
+ .desktop-content.show { display: block; }
217
+
218
+ /* --- Section shared --- */
219
+ section {
220
+ max-width: var(--max-w); margin: 0 auto; padding: 0 1.5rem 3.5rem;
221
+ }
222
+ section h2 {
223
+ font-size: 1.25rem; font-weight: 650; letter-spacing: -0.015em;
224
+ margin-bottom: 1rem; color: var(--text-strong);
225
+ }
226
+
227
+ /* --- Code blocks --- */
228
+ .code-block {
229
+ position: relative; background: var(--code-bg);
230
+ border: 1px solid rgba(148,163,184,0.14);
231
+ border-radius: var(--radius-sm);
232
+ padding: 0.85rem 1rem; overflow-x: auto;
233
+ }
234
+ .code-block pre {
235
+ margin: 0; font-family: var(--mono); font-size: 0.8rem;
236
+ line-height: 1.65; white-space: pre; color: rgba(226,232,240,0.88);
237
+ }
238
+ .code-block.chat-prompt { border-color: rgba(99,102,241,0.22); }
239
+ .code-block .comment { color: rgba(148,163,184,0.5); }
240
+ .code-block .string { color: #34d399; }
241
+ .code-block .key { color: #60a5fa; }
242
+ .copy-btn {
243
+ position: absolute; top: 0.5rem; right: 0.5rem;
244
+ background: rgba(148,163,184,0.18); border: 1px solid rgba(148,163,184,0.25);
245
+ border-radius: 6px; color: rgba(226,232,240,0.85);
246
+ padding: 0.3rem 0.7rem; font-size: 0.74rem; font-weight: 500;
247
+ cursor: pointer; font-family: inherit;
248
+ opacity: 0; transform: translateY(-2px);
249
+ transition: opacity var(--transition), transform var(--transition), color var(--transition), background var(--transition);
250
+ }
251
+ .code-block:hover .copy-btn { opacity: 1; transform: translateY(0); }
252
+ .copy-btn:hover { color: #fff; background: rgba(148,163,184,0.32); }
253
+ .copy-btn.visible { opacity: 1; transform: translateY(0); }
254
+
255
+ /* --- Prompt Gallery --- */
256
+ .gallery {
257
+ display: grid; grid-template-columns: 220px 1fr;
258
+ background: var(--surface); border: 1px solid var(--border);
259
+ border-radius: var(--radius); overflow: hidden; min-height: 360px;
260
+ }
261
+ .gallery-sidebar {
262
+ border-right: 1px solid var(--border); padding: 0.5rem 0;
263
+ overflow-y: auto; max-height: 420px;
264
+ }
265
+ .gallery-item {
266
+ display: block; width: 100%; text-align: left; background: none; border: none;
267
+ padding: 0.55rem 0.85rem; cursor: pointer; font-family: inherit;
268
+ border-left: 2px solid transparent;
269
+ transition: background var(--transition), border-color var(--transition);
270
+ }
271
+ .gallery-item:hover { background: rgba(15,23,42,0.025); }
272
+ .gallery-item.active { background: rgba(15,23,42,0.03); border-left-color: var(--brand); }
273
+ .gallery-item-title {
274
+ font-size: 0.8rem; font-weight: 600; color: var(--text); line-height: 1.3;
275
+ }
276
+ .gallery-item-persona {
277
+ font-size: 0.68rem; color: var(--text-muted); margin-top: 0.1rem;
278
+ }
279
+ .gallery-detail { padding: 1.25rem; overflow-y: auto; max-height: 420px; }
280
+ .gallery-persona-badge {
281
+ display: inline-block; font-size: 0.65rem; font-weight: 600;
282
+ text-transform: uppercase; letter-spacing: 0.06em;
283
+ padding: 0.12rem 0.45rem; border-radius: var(--radius-xs);
284
+ background: rgba(0,0,133,0.05); color: var(--brand); margin-bottom: 0.5rem;
285
+ }
286
+ .gallery-detail h3 {
287
+ font-size: 1rem; font-weight: 650; margin-bottom: 0.65rem; color: var(--text-strong);
288
+ }
289
+ .gallery-prompt {
290
+ background: rgba(15,23,42,0.025); border: 1px solid var(--border); border-radius: var(--radius-sm);
291
+ padding: 0.75rem 0.85rem; font-size: 0.84rem; line-height: 1.5;
292
+ color: var(--text); margin-bottom: 0.85rem; position: relative;
293
+ }
294
+ .gallery-prompt .copy-prompt-btn {
295
+ position: absolute; top: 0.45rem; right: 0.45rem;
296
+ background: var(--surface-solid); border: 1px solid var(--border);
297
+ border-radius: 999px; padding: 0.15rem 0.45rem;
298
+ font-size: 0.65rem; cursor: pointer; font-family: inherit;
299
+ color: var(--text-muted); transition: color var(--transition);
300
+ }
301
+ .gallery-prompt .copy-prompt-btn:hover { color: var(--text); }
302
+ .gallery-tools-label {
303
+ font-size: 0.68rem; font-weight: 600; text-transform: uppercase;
304
+ letter-spacing: 0.06em; color: var(--text-muted); margin-bottom: 0.35rem;
305
+ }
306
+ .gallery-tools {
307
+ display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.85rem;
308
+ }
309
+ .gallery-tool-tag {
310
+ font-family: var(--mono); font-size: 0.68rem;
311
+ background: rgba(15,23,42,0.035); border: 1px solid var(--border);
312
+ border-radius: var(--radius-xs); padding: 0.1rem 0.4rem;
313
+ color: var(--text-secondary);
314
+ }
315
+ .gallery-output-label {
316
+ font-size: 0.68rem; font-weight: 600; text-transform: uppercase;
317
+ letter-spacing: 0.06em; color: var(--text-muted); margin-bottom: 0.35rem;
318
+ }
319
+ .gallery-output {
320
+ background: var(--code-bg); border-radius: var(--radius-sm);
321
+ padding: 0.75rem 0.85rem; font-family: var(--mono); font-size: 0.72rem;
322
+ line-height: 1.55; color: rgba(148,163,184,0.85); overflow-x: auto; white-space: pre;
323
+ }
324
+
325
+ /* --- Tool Catalog --- */
326
+ .tool-search {
327
+ width: 100%; padding: 0.5rem 0.85rem; border: 1px solid var(--border);
328
+ border-radius: var(--radius-sm); font-size: 0.85rem; font-family: inherit;
329
+ background: var(--surface); margin-bottom: 0.65rem;
330
+ transition: border-color var(--transition);
331
+ }
332
+ .tool-search:focus { outline: none; border-color: rgba(0,0,133,0.25); }
333
+ .tool-list {
334
+ background: var(--surface); border: 1px solid var(--border);
335
+ border-radius: var(--radius-sm); overflow: hidden;
336
+ }
337
+ .tool-group { border-bottom: 1px solid var(--border); }
338
+ .tool-group:last-child { border-bottom: none; }
339
+ .tool-group summary {
340
+ cursor: pointer; padding: 0.5rem 0.85rem; font-size: 0.84rem; font-weight: 600;
341
+ background: transparent; border: none; list-style: none;
342
+ display: flex; align-items: center; gap: 0.4rem;
343
+ transition: background var(--transition); color: var(--text);
344
+ }
345
+ .tool-group summary:hover { background: rgba(15,23,42,0.02); }
346
+ .tool-group summary::after {
347
+ content: '\\25B6'; font-size: 0.5rem; color: var(--text-muted);
348
+ transition: transform var(--transition); flex-shrink: 0;
349
+ }
350
+ .tool-group[open] summary::after { transform: rotate(90deg); }
351
+ .tool-label { display: inline-flex; align-items: center; gap: 0.4rem; }
352
+ .api-badge {
353
+ font-size: 0.58rem; font-weight: 600; letter-spacing: 0.04em;
354
+ padding: 0.08rem 0.3rem; border-radius: 4px;
355
+ }
356
+ .api-badge-sfr { background: rgba(0,0,133,0.06); color: var(--brand); }
357
+ .api-badge-plr { background: rgba(125,125,182,0.1); color: var(--brand-lavender); }
358
+ .tool-group summary .tool-count {
359
+ font-size: 0.7rem; font-weight: 500; color: var(--text-muted);
360
+ }
361
+ .tool-group summary .tool-spacer { margin-left: auto; }
362
+ .tool-group-body {
363
+ padding: 0.2rem 0.85rem 0.55rem;
364
+ display: flex; flex-wrap: wrap; gap: 0.25rem;
365
+ }
366
+ .tool-group-body .tool-tag {
367
+ font-family: var(--mono); font-size: 0.68rem;
368
+ background: rgba(15,23,42,0.035); border: 1px solid var(--border);
369
+ border-radius: var(--radius-xs); padding: 0.1rem 0.4rem;
370
+ color: var(--text-secondary);
371
+ }
372
+ .tool-group-body .tool-tag.hidden { display: none; }
373
+ .tool-group.hidden { display: none; }
374
+
375
+ /* --- Status --- */
376
+ .status-row {
377
+ display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.65rem;
378
+ }
379
+ .status-card {
380
+ background: var(--surface); border: 1px solid var(--border);
381
+ border-radius: var(--radius-sm); padding: 0.75rem 0.85rem;
382
+ }
383
+ .status-card-header { display: flex; align-items: center; gap: 0.35rem; margin-bottom: 0.15rem; }
384
+ .status-dot {
385
+ width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted); flex-shrink: 0;
386
+ }
387
+ .status-dot.ok { background: var(--green); }
388
+ .status-dot.down { background: var(--red); }
389
+ .status-dot.checking { background: var(--amber); animation: pulse 1.5s ease-in-out infinite; }
390
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
391
+ .status-card h3 { font-size: 0.8rem; font-weight: 600; color: var(--text); }
392
+ .status-detail { font-size: 0.72rem; color: var(--text-muted); }
393
+
394
+ /* --- CTA --- */
395
+ .cta-wrap {
396
+ background: linear-gradient(135deg, var(--brand) 0%, var(--brand-light) 50%, #00008B 100%);
397
+ margin-top: 0.5rem;
398
+ }
399
+ .cta {
400
+ text-align: center; padding: 3rem 1.5rem;
401
+ max-width: var(--max-w); margin: 0 auto;
402
+ }
403
+ .cta h2 { color: #fff; font-size: 1.35rem; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 0.5rem; }
404
+ .cta p { color: rgba(255,255,255,0.65); font-size: 0.9rem; margin-bottom: 1.5rem; }
405
+ .cta-cards {
406
+ display: grid; grid-template-columns: 1fr 1fr; gap: 0.85rem;
407
+ max-width: 600px; margin: 0 auto; text-align: left;
408
+ }
409
+ .cta-card {
410
+ background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15);
411
+ border-radius: var(--radius-sm); padding: 1.15rem;
412
+ backdrop-filter: blur(4px);
413
+ }
414
+ .cta-card-label {
415
+ font-size: 0.62rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;
416
+ color: rgba(255,255,255,0.45); margin-bottom: 0.3rem;
417
+ }
418
+ .cta-card h3 { color: #fff; font-size: 0.95rem; font-weight: 700; margin-bottom: 0.3rem; }
419
+ .cta-card p { color: rgba(255,255,255,0.6); font-size: 0.8rem; margin-bottom: 0.75rem; line-height: 1.5; }
420
+ .cta-btn {
421
+ display: inline-block; background: #fff; color: var(--brand);
422
+ font-weight: 600; font-size: 0.8rem; padding: 0.45rem 1.1rem;
423
+ border-radius: 8px; transition: transform var(--transition);
424
+ }
425
+ .cta-btn:hover { transform: translateY(-1px); text-decoration: none; }
426
+
427
+ /* --- Footer --- */
428
+ footer { background: #0f172a; color: rgba(148,163,184,0.65); font-size: 0.78rem; }
429
+ .footer-inner {
430
+ max-width: var(--max-w); margin: 0 auto; padding: 1.15rem 1.5rem;
431
+ display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.75rem;
432
+ }
433
+ footer .links { display: flex; gap: 1.25rem; }
434
+ footer a { color: rgba(148,163,184,0.65); }
435
+ footer a:hover { color: #e2e8f0; text-decoration: none; }
436
+
437
+ /* --- Responsive --- */
438
+ @media (max-width: 768px) {
439
+ .hero { padding: 3rem 1.5rem 2rem; }
440
+ .hero-panel { max-width: none; }
441
+ .gallery { grid-template-columns: 1fr; }
442
+ .gallery-sidebar {
443
+ border-right: none; border-bottom: 1px solid var(--border);
444
+ display: flex; overflow-x: auto; max-height: none; padding: 0;
445
+ }
446
+ .gallery-item { border-left: none; border-bottom: 2px solid transparent; white-space: nowrap; padding: 0.45rem 0.75rem; }
447
+ .gallery-item.active { border-bottom-color: var(--brand); border-left-color: transparent; }
448
+ .gallery-detail { max-height: none; }
449
+ .status-row { grid-template-columns: 1fr; }
450
+ .cta-cards { grid-template-columns: 1fr; }
451
+ }
452
+ </style>
453
+ </head>
454
+ <body>
455
+
456
+ <div class="nav-wrap">
457
+ <nav>
458
+ <a href="https://www.sfranalytics.com" class="logo" aria-label="SFR Analytics">${LOGO_SVG}</a>
459
+ <div class="nav-right">
460
+ <a href="#examples">Examples</a>
461
+ <a href="#tools">Tools</a>
462
+ <a href="https://www.npmjs.com/package/@sfranalytics/mcp">npm</a>
463
+ <span class="version-badge">v${VERSION}</span>
464
+ <a href="https://www.sfranalytics.com" class="nav-cta" data-open-chat>Get API Key</a>
465
+ </div>
466
+ </nav>
467
+ </div>
468
+
469
+ <!-- Hero -->
470
+ <div class="hero">
471
+ <h1>Screen deals, research buyers, and underwrite loans &mdash; from <span class="accent">Claude Code</span> or <span class="accent">Cursor</span></h1>
472
+ <p class="hero-lead">39 MCP tools that give your AI assistant live access to property transactions, rental comps, market rankings, and private lending data. Set up in 60 seconds.</p>
473
+ <div class="hero-cta-row">
474
+ <a href="https://www.sfranalytics.com" class="btn-primary" data-open-chat>Get API Key</a>
475
+ <a href="#tools" class="btn-secondary">View 39 Tools</a>
476
+ </div>
477
+ <div class="hero-panel">
478
+ <div class="stepper">
479
+ <!-- Step 1 -->
480
+ <div class="step">
481
+ <div class="step-indicator"><div class="step-dot"></div><div class="step-line"></div></div>
482
+ <div class="step-content">
483
+ <div class="step-title">Create your account</div>
484
+ <div class="step-desc">Sign up at <a href="https://www.sfranalytics.com">sfranalytics.com</a> and grab your API key from the dashboard.</div>
485
+ </div>
486
+ </div>
487
+ <!-- Step 2 -->
488
+ <div class="step">
489
+ <div class="step-indicator"><div class="step-dot"></div><div class="step-line"></div></div>
490
+ <div class="step-content">
491
+ <div class="step-title">Connect your client</div>
492
+ <div class="seg-control">
493
+ <button class="seg-btn active" data-client="claude-code">Claude Code</button>
494
+ <button class="seg-btn" data-client="cursor">Cursor</button>
495
+ </div>
496
+ <div class="client-block active" id="client-claude-code">
497
+ <div class="step-label">Paste this into Claude Code:</div>
498
+ <div class="code-block chat-prompt">
499
+ <button class="copy-btn" data-copy="claude-code">Copy</button>
500
+ <pre style="color:rgba(226,232,240,0.92);white-space:pre-wrap;">Set up the SFR Analytics MCP server for me. The server URL is https://mcp.sfranalytics.com/mcp and it needs two HTTP headers: SFR-Api-Token set to <span class="string">YOUR_SFR_KEY</span> and PLR-Api-Token set to <span class="string">YOUR_PLR_KEY</span></pre>
501
+ </div>
502
+ </div>
503
+ <div class="client-block" id="client-cursor">
504
+ <div class="step-label">Paste this into Cursor chat:</div>
505
+ <div class="code-block chat-prompt">
506
+ <button class="copy-btn" data-copy="cursor">Copy</button>
507
+ <pre style="color:rgba(226,232,240,0.92);white-space:pre-wrap;">Add the SFR Analytics MCP server to my mcp.json config. Use npx with the package @sfranalytics/mcp. Set env vars SFR_API_TOKEN to <span class="string">YOUR_SFR_KEY</span> and PLR_API_TOKEN to <span class="string">YOUR_PLR_KEY</span></pre>
508
+ </div>
509
+ </div>
510
+ <span class="desktop-toggle" id="desktopToggle">Using Claude Desktop? &#9656;</span>
511
+ <div class="desktop-content" id="desktopContent">
512
+ <div class="code-block">
513
+ <button class="copy-btn" data-copy="claude-desktop">Copy</button>
514
+ <pre><span class="comment">// Add to claude_desktop_config.json</span>
515
+ {
516
+ <span class="key">"mcpServers"</span>: {
517
+ <span class="key">"sfranalytics"</span>: {
518
+ <span class="key">"command"</span>: <span class="string">"npx"</span>,
519
+ <span class="key">"args"</span>: [<span class="string">"-y"</span>, <span class="string">"@sfranalytics/mcp"</span>],
520
+ <span class="key">"env"</span>: {
521
+ <span class="key">"SFR_API_TOKEN"</span>: <span class="string">"YOUR_SFR_KEY"</span>,
522
+ <span class="key">"PLR_API_TOKEN"</span>: <span class="string">"YOUR_PLR_KEY"</span>
523
+ }
524
+ }
525
+ }
526
+ }</pre>
527
+ </div>
528
+ </div>
529
+ </div>
530
+ </div>
531
+ <!-- Step 3 -->
532
+ <div class="step">
533
+ <div class="step-indicator"><div class="step-dot"></div></div>
534
+ <div class="step-content">
535
+ <div class="step-title">Run your first query</div>
536
+ <div class="step-desc">Ask your AI assistant:</div>
537
+ <div class="code-block">
538
+ <button class="copy-btn" data-copy="first-query">Copy</button>
539
+ <pre style="color:rgba(226,232,240,0.92);white-space:pre-wrap;">Find the top 5 zip codes in Phoenix for SFR investment under $400k, sorted by gross yield</pre>
540
+ </div>
541
+ </div>
542
+ </div>
543
+ </div>
544
+ </div>
545
+ </div>
546
+
547
+ <!-- Prompt Gallery -->
548
+ <section id="examples">
549
+ <h2>See It in Action</h2>
550
+ <div class="gallery">
551
+ <div class="gallery-sidebar" id="gallerySidebar"></div>
552
+ <div class="gallery-detail" id="galleryDetail"></div>
553
+ </div>
554
+ </section>
555
+
556
+ <!-- Tool Catalog -->
557
+ <section id="tools">
558
+ <h2>Tool Catalog <span style="font-size:0.75rem;font-weight:500;color:var(--text-muted);">(39 tools)</span></h2>
559
+ <input class="tool-search" id="toolSearch" placeholder="Search tools&hellip;" autocomplete="off">
560
+ <div class="tool-list" id="toolGroups"></div>
561
+ </section>
562
+
563
+ <!-- Status -->
564
+ <section>
565
+ <h2>Server Status</h2>
566
+ <div class="status-row">
567
+ <div class="status-card">
568
+ <div class="status-card-header">
569
+ <div class="status-dot checking" id="dot-mcp"></div>
570
+ <h3>MCP Server</h3>
571
+ </div>
572
+ <div class="status-detail" id="detail-mcp">Checking&hellip;</div>
573
+ </div>
574
+ <div class="status-card">
575
+ <div class="status-card-header">
576
+ <div class="status-dot checking" id="dot-sfr"></div>
577
+ <h3>SFR API</h3>
578
+ </div>
579
+ <div class="status-detail" id="detail-sfr">Checking&hellip;</div>
580
+ </div>
581
+ <div class="status-card">
582
+ <div class="status-card-header">
583
+ <div class="status-dot checking" id="dot-plr"></div>
584
+ <h3>PLR API</h3>
585
+ </div>
586
+ <div class="status-detail" id="detail-plr">Checking&hellip;</div>
587
+ </div>
588
+ </div>
589
+ </section>
590
+
591
+ <!-- CTA -->
592
+ <div class="cta-wrap">
593
+ <div class="cta">
594
+ <h2>Get Your API Keys</h2>
595
+ <p>Provide at least one API token &mdash; SFR, PLR, or both. Tools are enabled per key.</p>
596
+ <div class="cta-cards">
597
+ <div class="cta-card">
598
+ <div class="cta-card-label">For investors &amp; developers</div>
599
+ <h3>SFR API</h3>
600
+ <p>Property transactions, buyer intelligence, rental analytics, and market data. 21 tools.</p>
601
+ <a class="cta-btn" href="https://www.sfranalytics.com/products/api" data-open-chat>Get API Access</a>
602
+ </div>
603
+ <div class="cta-card">
604
+ <div class="cta-card-label">For private lenders</div>
605
+ <h3>PLR API</h3>
606
+ <p>Borrower search, lender rankings, loan history, and market trends. 17 tools.</p>
607
+ <a class="cta-btn" href="https://www.sfranalytics.com/products/private-lender" data-open-chat>Book a Demo</a>
608
+ </div>
609
+ </div>
610
+ </div>
611
+ </div>
612
+
613
+ <footer>
614
+ <div class="footer-inner">
615
+ <span>&copy; ${new Date().getFullYear()} SFR Analytics</span>
616
+ <div class="links">
617
+ <a href="https://www.sfranalytics.com">sfranalytics.com</a>
618
+ <a href="https://www.npmjs.com/package/@sfranalytics/mcp">npm</a>
619
+ <a href="mailto:support@sfranalytics.com">Support</a>
620
+ </div>
621
+ </div>
622
+ </footer>
623
+
624
+ <script>
625
+ // --- Segmented control ---
626
+ document.querySelectorAll('.seg-btn').forEach(function(btn) {
627
+ btn.addEventListener('click', function() {
628
+ document.querySelectorAll('.seg-btn').forEach(function(b) { b.classList.remove('active'); });
629
+ document.querySelectorAll('.client-block').forEach(function(b) { b.classList.remove('active'); });
630
+ btn.classList.add('active');
631
+ document.getElementById('client-' + btn.dataset.client).classList.add('active');
632
+ });
633
+ });
634
+
635
+ // --- Desktop toggle ---
636
+ document.getElementById('desktopToggle').addEventListener('click', function() {
637
+ var content = document.getElementById('desktopContent');
638
+ var showing = content.classList.toggle('show');
639
+ this.textContent = 'Using Claude Desktop? ' + (showing ? '\\u25BE' : '\\u25B8');
640
+ });
641
+
642
+ // --- Copy ---
643
+ var snippets = {
644
+ 'claude-code': 'Set up the SFR Analytics MCP server for me. The server URL is https://mcp.sfranalytics.com/mcp and it needs two HTTP headers: SFR-Api-Token set to YOUR_SFR_KEY and PLR-Api-Token set to YOUR_PLR_KEY',
645
+ 'cursor': 'Add the SFR Analytics MCP server to my mcp.json config. Use npx with the package @sfranalytics/mcp. Set env vars SFR_API_TOKEN to YOUR_SFR_KEY and PLR_API_TOKEN to YOUR_PLR_KEY',
646
+ 'claude-desktop': JSON.stringify({mcpServers:{sfranalytics:{command:"npx",args:["-y","@sfranalytics/mcp"],env:{SFR_API_TOKEN:"YOUR_SFR_KEY",PLR_API_TOKEN:"YOUR_PLR_KEY"}}}}, null, 2),
647
+ 'first-query': 'Find the top 5 zip codes in Phoenix for SFR investment under $400k, sorted by gross yield'
648
+ };
649
+ document.querySelectorAll('.copy-btn').forEach(function(btn) {
650
+ btn.addEventListener('click', function() {
651
+ var key = btn.dataset.copy;
652
+ navigator.clipboard.writeText(snippets[key]).then(function() {
653
+ btn.textContent = 'Copied!';
654
+ setTimeout(function() { btn.textContent = 'Copy'; }, 1200);
655
+ });
656
+ });
657
+ });
658
+
659
+ // --- Prompt Gallery ---
660
+ var EXAMPLES = [
661
+ {
662
+ title: 'Deal screen in 60 seconds',
663
+ persona: 'SFR Investor',
664
+ prompt: 'Analyze 123 Main St, Mesa AZ 85201. Estimate market rent, cap rate, and cash-on-cash at 25% down. Use recent nearby rentals and recent sales comps. Show assumptions and a sensitivity table.',
665
+ tools: ['sfr_get_property', 'sfr_property_comps', 'sfr_rental_comparables'],
666
+ output: \`{
667
+ "property": { "beds": 3, "baths": 2, "sqft": 1420, "year": 2004 },
668
+ "rent_estimate": { "median": 2150, "p25": 1950, "p75": 2350 },
669
+ "cap_rate": 0.061,
670
+ "cash_on_cash": 0.074,
671
+ "comps_used": { "sales": 6, "rentals": 10 },
672
+ "assumptions": { "vacancy": 0.05, "maintenance": 0.08 }
673
+ }\`
674
+ },
675
+ {
676
+ title: 'Rank zip codes by yield',
677
+ persona: 'Acquisitions Analyst',
678
+ prompt: 'Rank the top 10 zip codes in Phoenix metro for SFR acquisitions under $450k. Sort by gross yield. Include median rent, home value, and institutional ownership percentage.',
679
+ tools: ['sfr_zip_finder', 'sfr_zip_detail', 'sfr_rental_stats'],
680
+ output: \`{
681
+ "top_zips": [
682
+ { "zip": "85009", "gross_yield": 0.082, "med_value": 285000, "med_rent": 1950, "inst_own": 0.04 },
683
+ { "zip": "85033", "gross_yield": 0.079, "med_value": 310000, "med_rent": 2040, "inst_own": 0.06 },
684
+ { "zip": "85035", "gross_yield": 0.076, "med_value": 295000, "med_rent": 1870, "inst_own": 0.03 }
685
+ ],
686
+ "total_zips_screened": 89
687
+ }\`
688
+ },
689
+ {
690
+ title: 'Who\\'s buying in Dallas?',
691
+ persona: 'Fund Manager',
692
+ prompt: 'Identify the top 10 repeat buyers of SFR in Dallas-Fort Worth in the last 90 days. For each, show deal count, typical price band, and whether they are mostly cash or financed.',
693
+ tools: ['sfr_top_buyers', 'sfr_buyer_profile', 'sfr_investor_activity'],
694
+ output: \`{
695
+ "top_buyers": [
696
+ { "name": "INVITATION HOMES", "deals": 47, "avg_price": 342000, "cash_pct": 0.15 },
697
+ { "name": "PROGRESS RESIDENTIAL", "deals": 31, "avg_price": 298000, "cash_pct": 0.22 },
698
+ { "name": "FIRSTKEY HOMES", "deals": 24, "avg_price": 275000, "cash_pct": 0.83 }
699
+ ],
700
+ "period": "last_90_days",
701
+ "total_institutional_deals": 284
702
+ }\`
703
+ },
704
+ {
705
+ title: 'Bridge loan underwriting',
706
+ persona: 'Private Lender',
707
+ prompt: 'Underwrite a bridge loan request: purchase $320k, rehab $60k, ARV target $460k at 456 Oak Ave, Jacksonville FL. Validate ARV from comps, flag risks, and propose terms.',
708
+ tools: ['sfr_property_comps', 'plr_loans_nearby', 'plr_borrower_search'],
709
+ output: \`{
710
+ "arv_validation": { "comp_median": 448000, "comp_count": 8, "confidence": "moderate" },
711
+ "ltc": 0.826,
712
+ "arv_ltv": 0.696,
713
+ "risks": ["ARV comps show 12% dispersion", "Borrower has 2 active bridge loans"],
714
+ "proposed_terms": { "rate": 0.115, "points": 2.0, "term_months": 12 }
715
+ }\`
716
+ },
717
+ {
718
+ title: 'Rent optimization audit',
719
+ persona: 'Asset Manager',
720
+ prompt: 'I have 8 rental properties in Charlotte, NC. For each zip (28205, 28208, 28210, 28212), find the current median rent for 3bd/2ba and compare to my current rents. Identify the biggest rent lift opportunities.',
721
+ tools: ['sfr_rental_stats', 'sfr_rental_comparables'],
722
+ output: \`{
723
+ "zip_analysis": [
724
+ { "zip": "28205", "market_median": 1850, "your_rent": 1550, "lift": 300, "pct": 0.19 },
725
+ { "zip": "28208", "market_median": 1720, "your_rent": 1650, "lift": 70, "pct": 0.04 },
726
+ { "zip": "28210", "market_median": 2100, "your_rent": 1900, "lift": 200, "pct": 0.11 },
727
+ { "zip": "28212", "market_median": 1680, "your_rent": 1700, "lift": -20, "pct": -0.01 }
728
+ ],
729
+ "total_annual_lift": 6600
730
+ }\`
731
+ },
732
+ {
733
+ title: 'Private lending market overview',
734
+ persona: 'Market Analyst',
735
+ prompt: 'What are the nationwide private lending trends? Show total loan volume, top MSAs by growth, and the leading lenders by market share.',
736
+ tools: ['plr_portfolio_summary', 'plr_market_trends', 'plr_msa_rankings', 'plr_top_lenders'],
737
+ output: \`{
738
+ "nationwide": { "total_loans": 184200, "total_volume_bn": 42.8, "avg_loan": 232000 },
739
+ "top_growth_msas": [
740
+ { "msa": "Austin-Round Rock, TX", "yoy_growth": 0.34 },
741
+ { "msa": "Phoenix-Mesa-Chandler, AZ", "yoy_growth": 0.28 },
742
+ { "msa": "Tampa-St. Petersburg, FL", "yoy_growth": 0.22 }
743
+ ],
744
+ "top_lenders": [
745
+ { "name": "Kiavi", "market_share": 0.087 },
746
+ { "name": "Lima One Capital", "market_share": 0.064 }
747
+ ]
748
+ }\`
749
+ }
750
+ ];
751
+
752
+ var activeExample = 0;
753
+ var sidebar = document.getElementById('gallerySidebar');
754
+ var detail = document.getElementById('galleryDetail');
755
+
756
+ function renderGallery() {
757
+ sidebar.innerHTML = EXAMPLES.map(function(ex, i) {
758
+ return '<button class="gallery-item' + (i === activeExample ? ' active' : '') + '" data-idx="' + i + '">' +
759
+ '<div class="gallery-item-title">' + ex.title + '</div>' +
760
+ '<div class="gallery-item-persona">' + ex.persona + '</div>' +
761
+ '</button>';
762
+ }).join('');
763
+
764
+ sidebar.querySelectorAll('.gallery-item').forEach(function(btn) {
765
+ btn.addEventListener('click', function() {
766
+ activeExample = parseInt(btn.dataset.idx);
767
+ renderGallery();
768
+ });
769
+ });
770
+
771
+ var ex = EXAMPLES[activeExample];
772
+ detail.innerHTML =
773
+ '<div class="gallery-persona-badge">' + ex.persona + '</div>' +
774
+ '<h3>' + ex.title + '</h3>' +
775
+ '<div class="gallery-prompt"><button class="copy-prompt-btn" id="copyPrompt">Copy</button>' + ex.prompt + '</div>' +
776
+ '<div class="gallery-tools-label">Tools called</div>' +
777
+ '<div class="gallery-tools">' + ex.tools.map(function(t) { return '<span class="gallery-tool-tag">' + t + '</span>'; }).join('') + '</div>' +
778
+ '<div class="gallery-output-label">Sample output</div>' +
779
+ '<div class="gallery-output">' + escapeHtml(ex.output) + '</div>';
780
+
781
+ document.getElementById('copyPrompt').addEventListener('click', function() {
782
+ var self = this;
783
+ navigator.clipboard.writeText(ex.prompt).then(function() {
784
+ self.textContent = 'Copied!';
785
+ setTimeout(function() { self.textContent = 'Copy'; }, 1200);
786
+ });
787
+ });
788
+ }
789
+
790
+ function escapeHtml(s) {
791
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
792
+ }
793
+
794
+ renderGallery();
795
+
796
+ // --- Tool Catalog ---
797
+ var TOOL_GROUPS = [
798
+ { label: 'Property Research', api: 'SFR', tools: ['sfr_search_properties','sfr_get_property','sfr_property_transactions','sfr_property_batch','sfr_property_comps','sfr_distress_search'] },
799
+ { label: 'Rental Analysis', api: 'SFR', tools: ['sfr_rental_stats','sfr_rental_comparables','sfr_rental_market_analysis'] },
800
+ { label: 'Zip & Market Screening', api: 'SFR', tools: ['sfr_zip_finder','sfr_zip_detail','sfr_institutional_owners'] },
801
+ { label: 'Buyer Intelligence', api: 'SFR', tools: ['sfr_top_buyers','sfr_buyer_profile','sfr_buyer_growth','sfr_best_buyers','sfr_market_highlights'] },
802
+ { label: 'Market Activity', api: 'SFR', tools: ['sfr_investor_activity','sfr_flip_stats','sfr_flip_activity','sfr_activity_highlights'] },
803
+ { label: 'Borrower Intelligence', api: 'PLR', tools: ['plr_borrower_search','plr_borrower_profile','plr_borrower_rankings','plr_top_borrowers','plr_borrower_loans','plr_borrower_contacts'] },
804
+ { label: 'Lender Intelligence', api: 'PLR', tools: ['plr_lender_rankings','plr_top_lenders','plr_lender_borrowers','plr_churned_borrowers'] },
805
+ { label: 'Market & Portfolio', api: 'PLR', tools: ['plr_market_trends','plr_msa_rankings','plr_loans_nearby','plr_transaction_history','plr_portfolio_summary'] },
806
+ { label: 'Risk & Research', api: 'PLR', tools: ['plr_negative_remarks','plr_owner_search'] },
807
+ { label: 'Shared', api: '', tools: ['sfra_welcome','sfra_health'] }
808
+ ];
809
+
810
+ var toolGroupsEl = document.getElementById('toolGroups');
811
+ var toolSearchEl = document.getElementById('toolSearch');
812
+
813
+ function renderToolGroups(filter) {
814
+ var q = (filter || '').toLowerCase();
815
+ toolGroupsEl.innerHTML = TOOL_GROUPS.map(function(g) {
816
+ var matchingTools = g.tools.filter(function(t) { return !q || t.includes(q) || g.label.toLowerCase().includes(q); });
817
+ if (matchingTools.length === 0) return '';
818
+ var apiTag = g.api ? '<span class="api-badge api-badge-' + g.api.toLowerCase() + '">' + g.api + '</span>' : '';
819
+ return '<details class="tool-group"' + (!q ? '' : ' open') + '>' +
820
+ '<summary><span class="tool-label">' + g.label + apiTag + '<span class="tool-count">' + matchingTools.length + (matchingTools.length === 1 ? ' tool' : ' tools') + '</span></span><span class="tool-spacer"></span></summary>' +
821
+ '<div class="tool-group-body">' + matchingTools.map(function(t) { return '<span class="tool-tag">' + t + '</span>'; }).join('') + '</div>' +
822
+ '</details>';
823
+ }).join('');
824
+ }
825
+
826
+ toolSearchEl.addEventListener('input', function() { renderToolGroups(toolSearchEl.value); });
827
+ renderToolGroups('');
828
+
829
+ // --- Status ---
830
+ function setStatus(id, ok, txt) {
831
+ var dot = document.getElementById('dot-' + id);
832
+ var el = document.getElementById('detail-' + id);
833
+ dot.classList.remove('checking');
834
+ dot.classList.add(ok ? 'ok' : 'down');
835
+ el.textContent = txt;
836
+ }
837
+
838
+ function formatUptime(s) {
839
+ if (s < 60) return s + 's';
840
+ if (s < 3600) return Math.floor(s / 60) + 'm';
841
+ if (s < 86400) return Math.floor(s / 3600) + 'h ' + Math.floor((s % 3600) / 60) + 'm';
842
+ return Math.floor(s / 86400) + 'd ' + Math.floor((s % 86400) / 3600) + 'h';
843
+ }
844
+
845
+ fetch('/status')
846
+ .then(function(r) { return r.json(); })
847
+ .then(function(data) {
848
+ setStatus('mcp', true, 'v' + data.version + ' \\u00b7 up ' + formatUptime(data.uptimeSeconds));
849
+ if (data.apis.sfr) {
850
+ setStatus('sfr', data.apis.sfr.reachable,
851
+ data.apis.sfr.reachable ? 'Reachable \\u00b7 ' + data.apis.sfr.latencyMs + 'ms' : 'Unreachable');
852
+ } else { setStatus('sfr', false, 'Not configured'); }
853
+ if (data.apis.plr) {
854
+ setStatus('plr', data.apis.plr.reachable,
855
+ data.apis.plr.reachable ? 'Reachable \\u00b7 ' + data.apis.plr.latencyMs + 'ms' : 'Unreachable');
856
+ } else { setStatus('plr', false, 'Not configured'); }
857
+ })
858
+ .catch(function() {
859
+ setStatus('mcp', false, 'Could not reach server');
860
+ setStatus('sfr', false, 'Unknown');
861
+ setStatus('plr', false, 'Unknown');
862
+ });
863
+
864
+ // --- Crisp Chat ---
865
+ var CRISP_ID = '${crispId}';
866
+ if (CRISP_ID) {
867
+ window.$crisp = [];
868
+ window.CRISP_WEBSITE_ID = CRISP_ID;
869
+ var cs = document.createElement('script');
870
+ cs.src = 'https://client.crisp.chat/l.js';
871
+ cs.async = 1;
872
+ document.head.appendChild(cs);
873
+ }
874
+
875
+ // --- Wire "Get API Key" buttons to open Crisp chat ---
876
+ document.querySelectorAll('[data-open-chat]').forEach(function(el) {
877
+ el.addEventListener('click', function(e) {
878
+ if (window.$crisp && window.$crisp.push) {
879
+ e.preventDefault();
880
+ window.$crisp.push(['do', 'chat:open']);
881
+ window.$crisp.push(['do', 'message:send', ['text', 'Hi! I\\'d like to get set up with an API key.']]);
882
+ }
883
+ // If Crisp not loaded, falls through to href
884
+ });
885
+ });
886
+ </script>
887
+
888
+ </body>
889
+ </html>`;
890
+ }
891
+ //# sourceMappingURL=landing.js.map