环网MCP服务器
MCP(模型上下文协议)服务器,为Claude Code提供对商业房地产数据的实时访问 环网,美国最大的CRE市场。Loopnet没有公共的API-该服务器通过任何Claude Code会话都可以通过自然语言调用的三个工具来抓取网站并公开结构化数据。
它的作用
在Claude Code注册后,您可以问以下问题:
- *“在德克萨斯州休斯顿寻找售价低于500万美元的办公楼”*
- *“获取此Loopnet列表的详细信息:https://www.loopnet.com/Listing/..."*
- *“给我一个佛罗里达州迈阿密零售物业的市场概述”*
服务器从Loopnet获取实时数据,解析HTML,并返回结构化结果——属性名称、地址、价格、大小、上限率、经纪人信息、图像等。
工具
| 工具 | 说明 | 关键参数 |
|---|---|---|
search_properties | 按位置和筛选器搜索CRE列表 | location, property_type, listing_type, price_min/max, size_min/max |
get_property_details | 获取特定列表的完整详细信息 | url_or_id (环网URL或列表ID) |
get_market_overview | 某一地区的综合市场统计数据 | location, property_type |
支持的属性类型
office, retail, industrial, multifamily, land, hospitality, special-purpose, health-care
支持的列表类型
for-sale, for-lease
位置格式
- 城市和州:
"Houston, TX","New York, NY" - 州缩写:
"TX" - 邮政编码:
"77001"
设置
先决条件
- Python 3.10+
- Claude 代码命令行界面 安装
安装
git clone && cd LoopnetMCP
# Create virtual environment and install
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"使用克劳德代码注册
claude mcp add \
--scope user \
--transport stdio \
loopnet \
-- python3 /path/to/LoopnetMCP/src/loopnet_mcp/server.py使用 --scope user 使服务器在所有Claude Code会话中可用,或 --scope project 将其限制在单个项目目录中。
注册后,重新启动Claude Code。证实 /mcp --the loopnet 服务器应显示为已连接3个工具。
配置
所有设置都是可选的,并具有合理的默认值。通过环境变量或 .env 文件(参见 .env.example):
| 变量 | 默认值 | 描述 |
|---|---|---|
LOOPNET_REQUEST_DELAY_SECONDS | 3.0 | HTTP请求之间的最小延迟 |
LOOPNET_MAX_CONCURRENT_REQUESTS | 1 | 最大并发HTTP请求数 |
LOOPNET_TIMEOUT_SECONDS | 30.0 | HTTP请求超时 |
LOOPNET_MAX_RETRIES | 3 | 失败请求的重试次数 |
LOOPNET_CACHE_TTL_SECONDS | 300 | 缓存TTL(5分钟) |
LOOPNET_CACHE_MAX_ENTRIES | 500 | 最大缓存响应数 |
LOOPNET_BROWSER_ENABLED | True | 为JS挑战启用无头浏览器回退 |
LOOPNET_BROWSER_HEADLESS | True | 在无头模式下运行回退浏览器 |
LOOPNET_IMPERSONATE_BROWSER | chrome136 | 要模拟的TLS指纹 |
建筑
src/loopnet_mcp/
├── server.py # FastMCP server + 3 tool definitions (entry point)
├── models.py # Pydantic v2 data models
├── config.py # Configuration via pydantic-settings
├── cache.py # In-memory TTL cache
├── __main__.py # python -m loopnet_mcp support
└── scraper/
├── client.py # HTTP client (curl_cffi) with rate limiting, retries, caching
├── browser.py # nodriver-based browser fallback for JS challenges
├── urls.py # URL construction and normalization
└── parsers/
├── search.py # Search results HTML → PropertySummary list
├── detail.py # Property detail HTML → PropertyDetail
├── market.py # Price/size parsing + market aggregation
└── utils.py # Shared address parsing请求流
Claude Code natural language query
→ MCP tool call (server.py)
→ URL builder (urls.py)
→ HTTP client (client.py, curl_cffi)
→ [If JS challenge detected] → Browser fallback (browser.py, nodriver)
→ HTML Parser (parsers/)
→ Pydantic model (models.py)
→ JSON dict response back to Claude反机器人绕过
Loopnet使用Akamai Bot Manager进行两层机器人检测:
- TLS指纹识别(JA3/JA4) --标准Python HTTP库(
httpx,requests,aiohttp)被HTTP 403立即阻止,因为它们的TLS客户端Hello握手具有非浏览器签名。此项目使用curl_cffi它包裹着curl-impersonate复制Chrome 136的确切TLS指纹。
- JavaScript挑战赛 --即使有正确的TLS指纹,Akamai有时也会提供一个简短的JS挑战页面(HTTP 200,约2500个字符
sec-if-cpt-container标记),必须由真正的浏览器执行。检测到后,客户端将回退到nodriver(无法检测到的无头Chrome)来解决挑战并提取真实页面内容。
数据模型
PropertySummary --搜索结果卡:姓名、地址、城市、州、邮政编码、房产类型、价格、大小、URL、图片、经纪人信息。
PropertyDetail --完整列表页面:所有内容汇总,加上上限率、NOI、建造年份、建筑类别、分区、停车位、楼层、单位、描述、亮点、图片、经纪人电话。
SearchResult --容器:查询元数据+列表 PropertySummary.
MarketOverview --汇总统计数据:总房源、平均价格、平均价格/平方英尺、平均规模、价格范围、规模范围、房源类型细分、物业子类型细分、样本房源。
缓存
内存中的TTL缓存(默认5分钟,最多500个条目)可防止冗余请求。缓存键是完整的URL。当达到容量时,最旧的条目会被删除。缓存不会在服务器重新启动时持久化。
速率限制
请求具有可配置的最小延迟(默认3秒)和并发限制器(默认1个并发请求)的速率限制。重试时应用指数回退(2^尝试秒)。
错误处理
MCP工具从不向框架引发异常。所有错误都会被捕获并作为JSON字典返回,并带有 "error" 按键:
{"error": "Blocked by Loopnet (403) for URL: ...", "query_location": "Dallas, TX", "properties": []}这允许LLM读取错误并向用户提供人性化的解释。
发展
运行测试
# All tests
python3 -m pytest tests/ -v
# Specific test file
python3 -m pytest tests/test_parsers.py -v
# Specific test
python3 -m pytest tests/test_market.py::TestParsePrice::test_parse_price_dollars -v所有测试都使用来自的模拟HTML夹具 tests/fixtures/ --不进行真正的HTTP请求。这 conftest.py 自动修补预热请求和浏览器启动器。
测试文件
| 文件 | 它测试什么 |
|---|---|
test_models.py | Pydantic模型的构建与验证 |
test_cache.py | TTL缓存获取/设置/驱逐/过期 |
test_urls.py | URL规范化、搜索URL构建、列表ID提取 |
test_client.py | HTTP客户端重试、速率限制、错误处理、缓存 |
test_parsers.py | 根据夹具搜索和详细说明HTML解析器 |
test_market.py | 价格/规模/资本化率解析、市场聚合 |
test_server.py | MCP工具集成(模拟获取) |
test_search_integration.py | 完整搜索管道(模拟HTTP) |
test_detail_integration.py | 完整细节管道(模拟HTTP) |
test_market_integration.py | 完整的市场概览管道(模拟HTTP) |
test_browser.py | 浏览器回退和挑战检测 |
更新HTML更改的分析器
Loopnet会定期更改其HTML结构,这将破坏CSS选择器 parsers/search.py 和 parsers/detail.py当这种情况发生时:
- 将真实浏览器中的新HTML保存到
tests/fixtures/ - 更新相关解析器中的CSS选择器
- 更新相应的测试断言
- 运行完整的测试套件以检查回归
诊断命令
# Check if Loopnet returns real content or a challenge page
python3 -c "
import asyncio
from loopnet_mcp.scraper.client import LoopnetClient
async def test():
client = LoopnetClient()
try:
html = await client.fetch('https://www.loopnet.com')
if 'sec-if-cpt-container' in html:
print('CHALLENGE PAGE (Akamai JS challenge)')
elif len(html) > 10000:
print('REAL CONTENT (' + str(len(html)) + ' chars)')
else:
print('UNKNOWN (' + str(len(html)) + ' chars)')
except Exception as e:
print(f'ERROR: {e}')
finally:
await client.close()
asyncio.run(test())
"
# Verify MCP server registration
claude mcp get loopnet
# Verify server starts cleanly
python3 -c "from loopnet_mcp.server import mcp; print('OK:', mcp.name)"技术栈
| 组件 | 库 | 目的 |
|---|---|---|
| MCP框架 | FastMCP v2 | MCP服务器和工具注册 |
| HTTP客户端 | curl-cffi | TLS指纹感知HTTP请求 |
| 浏览器回退 | nodrive | 无头Chrome JS挑战绕过 |
| HTML解析 | 美丽的Soup4 + lxml 文件 | HTML→ 结构化数据 |
| 数据模型 | 派丹蒂克 v2 | 验证和序列化 |
| 配置 | 媒染剂设置 | Env-var驱动配置 |
已知限制
- 仅第一页聚合:
get_market_overview仅获取搜索结果的第一页。由于速率限制,多页爬行会很慢。 - Akamai JS挑战:如果无头浏览器回退无法解决挑战,该工具将返回一个错误字典。如果Akamai升级到验证码级别的挑战,则可能会出现这种情况。
- 价格解析:价格在搜索/详细信息结果中以字符串形式保存。仅
get_market_overview将它们转换为数字以进行聚合。不寻常的格式(例如。,"$25/SF/YR")被跳过。 - 无实时更新:缓存TTL为5分钟。列表在该窗口内可能会显得过时。
