1414Discovery runs once at module import; results are persisted to snapshot.json.
1515"""
1616
17- import json
1817import logging
19- import os
20- import re
21- from contextvars import ContextVar
2218from typing import Annotated , Optional
23- from urllib .parse import urljoin , urlparse
19+ from urllib .parse import urlparse
2420
2521import httpx
26- from markdownify import markdownify
2722from mcp .server .fastmcp import FastMCP
2823from pydantic import Field
2924
3328logger = logging .getLogger (__name__ )
3429
3530
36- # ----------------------------------------------------------------------------
37- # Per-request credentials (ContextVar populated by middleware in juspay_mcp.main)
38- # ----------------------------------------------------------------------------
39-
40- juspay_request_credentials : ContextVar [Optional [dict ]] = ContextVar (
41- "juspay_request_credentials" , default = None
42- )
43-
31+ # ---------------------------------------------------------------------------
32+ # Static domain allowlist
33+ # ---------------------------------------------------------------------------
4434
45- def set_juspay_request_credentials (credentials ):
46- """Set Juspay credentials for the current request context."""
47- juspay_request_credentials .set (credentials )
35+ _ALLOWED_SUFFIXES = (".juspay.io" , ".juspay.in" )
36+ _ALLOWED_EXACT = frozenset ({"juspay.io" , "juspay.in" , "dth95m2xtyv8v.cloudfront.net" })
37+ _ALLOWED_SCHEMES = frozenset ({"http" , "https" })
38+ _ALLOWED_DOMAINS_DISPLAY = "*.juspay.io, *.juspay.in, dth95m2xtyv8v.cloudfront.net"
4839
4940
50- def get_juspay_request_credentials ():
51- """Get Juspay credentials from current request context."""
52- return juspay_request_credentials .get ()
53-
54-
55- # ----------------------------------------------------------------------------
56- # Transcripts (curated commentary appended to specific URLs in doc_fetch_tool)
57- # ----------------------------------------------------------------------------
58-
59- def _load_transcripts () -> dict :
60- path = os .path .join (os .path .dirname (__file__ ), "transcripts.json" )
61- try :
62- with open (path , "r" ) as f :
63- return {str (k ): str (v ) for k , v in json .load (f ).items ()}
64- except FileNotFoundError :
65- logger .info ("No transcripts.json at %s; running without transcripts" , path )
66- return {}
67- except json .JSONDecodeError as e :
68- logger .warning ("Failed to parse transcripts.json: %s" , e )
69- return {}
70-
71-
72- _TRANSCRIPTS_MAP = _load_transcripts ()
41+ def _url_allowed (url : str ) -> bool :
42+ parsed = urlparse (url )
43+ if parsed .scheme not in _ALLOWED_SCHEMES :
44+ return False
45+ # hostname is lowercased and strips the port.
46+ host = parsed .hostname or ""
47+ if host in _ALLOWED_EXACT :
48+ return True
49+ return any (host .endswith (s ) for s in _ALLOWED_SUFFIXES )
7350
7451
7552# ----------------------------------------------------------------------------
@@ -112,10 +89,10 @@ def _slug_for(entry: dict) -> Optional[str]:
11289_CAT_HINT : str = ", " .join (_CATEGORIES ) if _CATEGORIES else "(none discovered)"
11390
11491logger .info (
115- "Docs MCP initialized: %d products across %d categories, %d allowed domains " ,
92+ "Docs MCP initialized: %d products across %d categories ( allowed: %s) " ,
11693 len (_ENRICHED_SOURCES ),
11794 len (_CATEGORIES ),
118- len ( _DOMAINS ) ,
95+ _ALLOWED_DOMAINS_DISPLAY ,
11996)
12097
12198
@@ -134,39 +111,15 @@ def _slug_for(entry: dict) -> Optional[str]:
134111# Helpers
135112# ----------------------------------------------------------------------------
136113
137- _META_REFRESH_RE = re .compile (
138- r'<meta http-equiv="refresh" content="[^;]+;\s*url=([^"]+)"' ,
139- re .IGNORECASE ,
140- )
141-
142114
143- def _url_allowed (url : str ) -> bool :
144- return any (url .startswith (d ) for d in _DOMAINS )
145115
146116
147- async def _fetch_with_processing (url : str ) -> str :
148- """Fetch URL, follow meta-refresh once, inject transcript, markdownify ."""
117+ async def _fetch (url : str ) -> str :
118+ """Fetch URL and return the raw response text ."""
149119 try :
150120 response = await _HTTPX .get (url )
151121 response .raise_for_status ()
152- content = response .text
153-
154- m = _META_REFRESH_RE .search (content )
155- if m :
156- new_url = urljoin (str (response .url ), m .group (1 ))
157- if not _url_allowed (new_url ):
158- return (
159- f"Error: redirect URL not allowed. "
160- f"Allowed domains: { ', ' .join (sorted (_DOMAINS ))} "
161- )
162- response = await _HTTPX .get (new_url )
163- response .raise_for_status ()
164- content = response .text
165-
166- if url in _TRANSCRIPTS_MAP :
167- content = content + "\n \n ---\n \n " + _TRANSCRIPTS_MAP [url ]
168-
169- return markdownify (content )
122+ return response .text
170123 except (httpx .HTTPStatusError , httpx .RequestError ) as e :
171124 return f"Encountered an HTTP error: { e } "
172125
@@ -255,11 +208,10 @@ async def explore_product(
255208) -> str :
256209 """Fetch the llms.txt index for a specific Juspay product.
257210
258- Looks up the product slug in the discovered catalog and returns its
259- documentation index as markdown. The index contains .md content links
260- that you can read via doc_fetch_tool(url).
211+ Looks up the product slug in the catalog and returns the raw llms.txt
212+ content. The index contains .md content links readable via doc_fetch_tool(url).
261213
262- Returns an error if the slug isn't in the catalog - call list_products()
214+ Returns an error if the slug isn't in the catalog — call list_products()
263215 to see valid slugs.
264216 """
265217 entry = _SLUG_INDEX .get (product )
@@ -274,7 +226,7 @@ async def explore_product(
274226 f"Call list_products() to see all slugs. "
275227 f"Examples: { sample } { more } ."
276228 )
277- return await _fetch_with_processing (entry ["llms_txt" ])
229+ return await _fetch (entry ["llms_txt" ])
278230
279231
280232@mcp .tool ()
@@ -283,25 +235,24 @@ async def doc_fetch_tool(
283235 str ,
284236 Field (
285237 description = (
286- "URL to fetch. Must be on an allowed domain (auto-derived "
287- "from the discovered product catalog, typically juspay.io)."
238+ f"URL to fetch. Must be on an allowed domain: { _ALLOWED_DOMAINS_DISPLAY } ."
288239 ),
289240 ),
290241 ],
291242) -> str :
292- """Fetch any allowed Juspay docs URL as markdown .
243+ """Fetch any allowed Juspay docs URL and return its raw text content .
293244
294245 Use this after explore_product() to read specific pages by URL.
295- Follows meta-refresh redirects once. Returns markdown-converted
296- content. Returns an error if the URL is on a disallowed domain.
246+ Returns markdown mirror of the URL.
247+ Returns an error if the URL is on a disallowed domain.
297248 """
298249 url = url .strip ()
299250 if not _url_allowed (url ):
300251 return (
301- f"Error: URL not allowed. Must start with one of: "
302- f"{ ', ' . join ( sorted ( _DOMAINS )) } "
252+ f"Error: URL not on an allowed domain. "
253+ f"Allowed: { _ALLOWED_DOMAINS_DISPLAY } "
303254 )
304- return await _fetch_with_processing (url )
255+ return await _fetch (url )
305256
306257
307258# Export the underlying low-level Server for main.py / stdio.py
0 commit comments