Skip to content

Commit 0c39229

Browse files
authored
4 new MCP tools + 4 REST endpoints
IRR — checks all IRR databases (RIPE, RADB, ARIN, APNIC…) for route objects matching the claimed origin ASN; reports consistency and missing coverage Route Leak — detects BGP hijacks (multiple origin ASNs) and route leaks (AS-path loops / valley-free violations) with confidence scoring Looking Glass — shows full AS paths + BGP communities from up to 50 RIPE RIS vantage points around the world, with geographic region labels Route Stability — analyses withdrawal/re-announcement cycles over 1–168 hours, computing a 0–100 stability score and uptime %
2 parents 18453da + 2103ad2 commit 0c39229

8 files changed

Lines changed: 1034 additions & 17 deletions

File tree

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ If `peerglass` is not in your PATH, use:
7373
}
7474
```
7575

76-
Restart Claude Desktop. All **27 tools** become immediately available.
76+
Restart Claude Desktop. All **31 tools** become immediately available.
7777

7878
### Start the REST API
7979

@@ -138,7 +138,7 @@ Interactive docs at: http://localhost:8000/docs
138138

139139
---
140140

141-
## All 27 MCP Tools
141+
## All 31 MCP Tools
142142

143143
### Phase 1 — Registry Queries
144144

@@ -297,7 +297,7 @@ Interactive docs (Swagger UI): http://localhost:8000/docs
297297
│ Claude (LLM) REST Clients │
298298
│ │ MCP / stdio │ HTTP │
299299
│ ▼ ▼ │
300-
│ server.py (27 tools) api.py (25 endpoints) │
300+
│ server.py (31 tools) api.py (25 endpoints) │
301301
│ │ │ │
302302
│ └──────────┬───────────────┘ │
303303
│ ▼ │
@@ -374,10 +374,10 @@ You should run both — they catch different categories of problems.
374374
| 4 | Protocol header | `Accept: application/rdap+json` is set |
375375
| 5 | User-Agent | Updated to `peerglass/1.0.0` |
376376
| 6 | MCP server name | `"peerglass"` |
377-
| 7 | Tool count | Exactly 17 `@mcp.tool()` decorators in `server.py` |
377+
| 7 | Tool count | Exactly 27 `@mcp.tool()` decorators in `server.py` |
378378
| 8 | REST endpoints | All 25 routes present in `api.py` |
379379
| 9 | FastAPI runtime | TestClient hits 3 endpoints in-memory, validates responses |
380-
| 10 | README | PeerGlass branding, 27 tools, RDAP note all present |
380+
| 10 | README | PeerGlass branding, 31 tools, RDAP note all present |
381381

382382
**How to run:**
383383

@@ -402,7 +402,7 @@ PEERGLASS — COMPLETE TEST SUITE
402402
...
403403
404404
✅ ALL TESTS PASSED — 0 errors
405-
Python files: 7 | MCP tools: 27 | REST endpoints: 25
405+
Python files: 7 | MCP tools: 31 | REST endpoints: 29
406406
Protocol: RDAP throughout (RFC 7480-7484)
407407
Branding: PeerGlass throughout
408408
============================================================

api.py

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ def _real_ip(request: Request) -> str:
9494
format_ct_logs_md,
9595
format_threat_intel_md,
9696
format_passive_dns_md,
97+
format_irr_result_md,
98+
format_route_leak_md,
99+
format_looking_glass_md,
100+
format_route_stability_md,
97101
to_json,
98102
)
99103
from normalizer import normalize_ip_response, normalize_asn_response
@@ -185,7 +189,7 @@ async def root():
185189
"description": "Internet resource intelligence: RIR + BGP + RPKI + PeeringDB",
186190
"docs": "/docs",
187191
"redoc": "/redoc",
188-
"tools": 27,
192+
"tools": 31,
189193
"endpoints": {
190194
"ip": "/v1/ip/{ip}",
191195
"asn": "/v1/asn/{asn}",
@@ -212,6 +216,10 @@ async def root():
212216
"ct_logs": "/v1/ct/{domain}",
213217
"threat_intel": "/v1/threat/{ip}",
214218
"passive_dns": "/v1/pdns/{resource}",
219+
"irr": "/v1/irr?prefix=...&asn=...",
220+
"route_leak": "/v1/route-leak/{prefix}",
221+
"looking_glass": "/v1/looking-glass/{prefix}",
222+
"route_stability": "/v1/stability/{prefix}",
215223
"cache_stats": "/v1/meta/cache",
216224
"server_status": "/v1/meta/status",
217225
},
@@ -1089,6 +1097,105 @@ async def passive_dns(
10891097
return _resp(md, jsn, format)
10901098

10911099

1100+
# ──────────────────────────────────────────────────────────────
1101+
# Sprint 4 — BGP Depth endpoints
1102+
# ──────────────────────────────────────────────────────────────
1103+
1104+
@app.get(
1105+
"/v1/irr",
1106+
tags=["BGP"],
1107+
summary="IRR route-object consistency check",
1108+
)
1109+
@limiter.limit(_DEFAULT_RATE)
1110+
async def check_irr_endpoint(
1111+
request: Request,
1112+
prefix: str = Query(..., description="CIDR prefix e.g. 1.1.1.0/24"),
1113+
asn: str = Query(..., description="Origin ASN e.g. AS13335 or 13335"),
1114+
format: str = FORMAT_QUERY,
1115+
):
1116+
"""Check IRRExplorer for route objects covering *prefix* and validate against *asn*."""
1117+
cache_key = cache_module.make_irr_key(prefix, asn)
1118+
cached = cache_module.get(cache_key)
1119+
if cached:
1120+
return _resp(cached["markdown"], cached["json"], format)
1121+
result = await rir_client.check_irr(prefix, asn)
1122+
md = format_irr_result_md(result)
1123+
jsn = to_json(result)
1124+
cache_module.set(cache_key, {"markdown": md, "json": jsn}, cache_module.TTL_IRR)
1125+
return _resp(md, jsn, format)
1126+
1127+
1128+
@app.get(
1129+
"/v1/route-leak/{prefix:path}",
1130+
tags=["BGP"],
1131+
summary="BGP route leak / hijack detection",
1132+
)
1133+
@limiter.limit(_DEFAULT_RATE)
1134+
async def detect_route_leak_endpoint(
1135+
request: Request,
1136+
prefix: str,
1137+
format: str = FORMAT_QUERY,
1138+
):
1139+
"""Detect potential route leaks or hijacks for a prefix using RIPE Stat BGP-state."""
1140+
cache_key = cache_module.make_route_leak_key(prefix)
1141+
cached = cache_module.get(cache_key)
1142+
if cached:
1143+
return _resp(cached["markdown"], cached["json"], format)
1144+
result = await rir_client.detect_route_leak(prefix)
1145+
md = format_route_leak_md(result)
1146+
jsn = to_json(result)
1147+
cache_module.set(cache_key, {"markdown": md, "json": jsn}, cache_module.TTL_ROUTE_LEAK)
1148+
return _resp(md, jsn, format)
1149+
1150+
1151+
@app.get(
1152+
"/v1/looking-glass/{prefix:path}",
1153+
tags=["BGP"],
1154+
summary="BGP looking glass — AS paths from RIPE RIS collectors",
1155+
)
1156+
@limiter.limit(_DEFAULT_RATE)
1157+
async def looking_glass_endpoint(
1158+
request: Request,
1159+
prefix: str,
1160+
vantage_points: int = Query(default=10, ge=1, le=50, description="Max entries to return"),
1161+
format: str = FORMAT_QUERY,
1162+
):
1163+
"""Show BGP routing table entries with full AS paths from RIPE RIS vantage points."""
1164+
cache_key = cache_module.make_looking_glass_key(prefix, vantage_points)
1165+
cached = cache_module.get(cache_key)
1166+
if cached:
1167+
return _resp(cached["markdown"], cached["json"], format)
1168+
result = await rir_client.get_looking_glass(prefix, vantage_points)
1169+
md = format_looking_glass_md(result)
1170+
jsn = to_json(result)
1171+
cache_module.set(cache_key, {"markdown": md, "json": jsn}, cache_module.TTL_LOOKING_GLASS)
1172+
return _resp(md, jsn, format)
1173+
1174+
1175+
@app.get(
1176+
"/v1/stability/{prefix:path}",
1177+
tags=["BGP"],
1178+
summary="BGP route stability / flap analysis",
1179+
)
1180+
@limiter.limit(_DEFAULT_RATE)
1181+
async def route_stability_endpoint(
1182+
request: Request,
1183+
prefix: str,
1184+
hours: int = Query(default=24, ge=1, le=168, description="Analysis window in hours (max 168 = 7 days)"),
1185+
format: str = FORMAT_QUERY,
1186+
):
1187+
"""Analyse BGP route flap history and compute a stability score for a prefix."""
1188+
cache_key = cache_module.make_route_stability_key(prefix, hours)
1189+
cached = cache_module.get(cache_key)
1190+
if cached:
1191+
return _resp(cached["markdown"], cached["json"], format)
1192+
result = await rir_client.get_route_stability(prefix, hours)
1193+
md = format_route_stability_md(result)
1194+
jsn = to_json(result)
1195+
cache_module.set(cache_key, {"markdown": md, "json": jsn}, cache_module.TTL_ROUTE_STABILITY)
1196+
return _resp(md, jsn, format)
1197+
1198+
10921199
# ──────────────────────────────────────────────────────────────
10931200
# OpenAI / Gemini function-calling schema endpoint
10941201
# ──────────────────────────────────────────────────────────────
@@ -1375,5 +1482,64 @@ async def openai_tools(request: Request):
13751482
},
13761483
},
13771484
},
1485+
{
1486+
"type": "function",
1487+
"function": {
1488+
"name": "rir_check_irr",
1489+
"description": "IRR route-object consistency check via IRRExplorer — validates that a prefix has correct route objects matching the expected origin ASN.",
1490+
"parameters": {
1491+
"type": "object",
1492+
"properties": {
1493+
"prefix": {"type": "string", "description": "CIDR prefix e.g. 1.1.1.0/24"},
1494+
"asn": {"type": "string", "description": "Expected origin ASN e.g. AS13335"},
1495+
},
1496+
"required": ["prefix", "asn"],
1497+
},
1498+
},
1499+
},
1500+
{
1501+
"type": "function",
1502+
"function": {
1503+
"name": "rir_detect_route_leak",
1504+
"description": "Detect BGP route leaks or hijacks — checks for multiple origin ASNs and AS-path loops.",
1505+
"parameters": {
1506+
"type": "object",
1507+
"properties": {
1508+
"prefix": {"type": "string", "description": "CIDR prefix e.g. 1.1.1.0/24"},
1509+
},
1510+
"required": ["prefix"],
1511+
},
1512+
},
1513+
},
1514+
{
1515+
"type": "function",
1516+
"function": {
1517+
"name": "rir_looking_glass",
1518+
"description": "BGP looking glass — shows AS paths and communities from RIPE RIS vantage points around the world.",
1519+
"parameters": {
1520+
"type": "object",
1521+
"properties": {
1522+
"prefix": {"type": "string", "description": "CIDR prefix e.g. 1.1.1.0/24"},
1523+
"vantage_points": {"type": "integer", "description": "Max entries (default 10)"},
1524+
},
1525+
"required": ["prefix"],
1526+
},
1527+
},
1528+
},
1529+
{
1530+
"type": "function",
1531+
"function": {
1532+
"name": "rir_route_stability",
1533+
"description": "BGP route stability / flap analysis — computes a 0–100 stability score and lists state-change events.",
1534+
"parameters": {
1535+
"type": "object",
1536+
"properties": {
1537+
"prefix": {"type": "string", "description": "CIDR prefix e.g. 1.1.1.0/24"},
1538+
"hours": {"type": "integer", "description": "Analysis window in hours (default 24, max 168)"},
1539+
},
1540+
"required": ["prefix"],
1541+
},
1542+
},
1543+
},
13781544
]
13791545
return {"tools": tools, "count": len(tools), "base_url": "https://api.peerglass.io"}

cache.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@
5353
TTL_THREAT_INTEL = 3_600 # 1 hour — Shodan/GreyNoise refreshes hourly
5454
TTL_PASSIVE_DNS = 86_400 # 24 hours — PDNS history rarely changes
5555

56+
# Sprint 4 — BGP depth
57+
TTL_IRR = 3_600 # 1 hour — IRR route objects are fairly stable
58+
TTL_ROUTE_LEAK = 300 # 5 minutes — BGP state can shift quickly
59+
TTL_LOOKING_GLASS = 300 # 5 minutes — live routing table snapshots
60+
TTL_ROUTE_STABILITY = 900 # 15 minutes — stability window is time-sensitive
61+
5662
# ── In-memory store ─────────────────────────────────────────
5763
_STORE: dict[str, tuple[Any, float]] = {}
5864

@@ -165,6 +171,19 @@ def make_threat_intel_key(ip: str) -> str:
165171
def make_passive_dns_key(resource: str) -> str:
166172
return _make_key("passive_dns", resource.lower().strip())
167173

174+
# Sprint 4 — BGP depth
175+
def make_irr_key(prefix: str, asn: str) -> str:
176+
return _make_key("irr", prefix.strip(), asn.upper().lstrip("AS"))
177+
178+
def make_route_leak_key(prefix: str) -> str:
179+
return _make_key("route_leak", prefix.strip())
180+
181+
def make_looking_glass_key(prefix: str, vantage_points: int) -> str:
182+
return _make_key("looking_glass", prefix.strip(), int(vantage_points))
183+
184+
def make_route_stability_key(prefix: str, hours: int) -> str:
185+
return _make_key("route_stability", prefix.strip(), int(hours))
186+
168187

169188
# ── Monitor Store — persistent baselines for change detection ──
170189
# Separate from the TTL cache: baselines never expire automatically.

0 commit comments

Comments
 (0)