2727
2828mcp = FastMCP ("mfs-context" )
2929
30+ # Optional access boundary: a comma-separated list of URI / path prefixes this
31+ # server may search and read. Empty = unrestricted (the whole MFS index). Set it
32+ # on the MCP registration, e.g.
33+ # --env MFS_ALLOWED_SCOPES=github://org/repo,file://local/abs/path
34+ _ALLOWED = [s .strip () for s in os .getenv ("MFS_ALLOWED_SCOPES" , "" ).split ("," ) if s .strip ()]
35+
36+
37+ def _within (uri : str ) -> bool :
38+ """True when `uri` is one of, or under, an allowed prefix (or no allowlist is set)."""
39+ if not _ALLOWED :
40+ return True
41+ return any (uri == p or uri .startswith (p .rstrip ("/" ) + "/" ) for p in _ALLOWED )
42+
3043
3144def _token () -> str | None :
3245 if os .getenv ("MFS_TOKEN" ):
@@ -48,15 +61,25 @@ def _api(api_cls):
4861@mcp .tool ()
4962def search (query : str , scope : str = "" , top_k : int = 8 ) -> str :
5063 """Search MFS-indexed sources (code, docs, issues, chat, databases) by meaning
51- or keyword. Leave ``scope`` empty to search everything, or pass a path / URI
52- prefix to narrow it (e.g. ``github://org/repo`` or a local path). Returns
53- ranked hits, each with a snippet and the ``source`` URI to pass to ``read``.
64+ or keyword. Leave ``scope`` empty to search every allowed source, or pass a
65+ path / URI prefix to narrow it (e.g. ``github://org/repo`` or a local path).
66+ Returns ranked hits, each with a snippet and the ``source`` URI to pass to
67+ ``read``.
5468 """
55- resp = _api (mfs_sdk .RetrievalApi ).search (q = query , path = scope or None , top_k = top_k )
56- if not resp .results :
69+ if scope and not _within (scope ):
70+ return f"Refused: scope { scope !r} is outside the allowed scopes ({ ', ' .join (_ALLOWED )} )."
71+ targets = [scope ] if scope else (list (_ALLOWED ) or [None ])
72+
73+ api = _api (mfs_sdk .RetrievalApi )
74+ hits = []
75+ for target in targets :
76+ hits .extend (api .search (q = query , path = target , top_k = top_k ).results )
77+ hits = [h for h in hits if _within (h .source )] # belt-and-suspenders
78+ hits .sort (key = lambda h : h .score if h .score is not None else 0.0 , reverse = True )
79+ if not hits :
5780 return "No matches."
5881 blocks = []
59- for h in resp . results :
82+ for h in hits [: top_k ] :
6083 score = f"{ h .score :.2f} " if h .score is not None else "n/a"
6184 blocks .append (f"## { h .source } (score={ score } )\n { h .content .strip ()} " )
6285 return "\n \n " .join (blocks )
@@ -67,6 +90,8 @@ def read(source: str, lines: str = "") -> str:
6790 """Read a source in full, or a line range like ``"40:80"``. ``source`` is the
6891 URI from a ``search`` hit. Use this to pull the exact code or text into context
6992 after ``search`` locates it."""
93+ if not _within (source ):
94+ return f"Refused: { source !r} is outside the allowed scopes ({ ', ' .join (_ALLOWED )} )."
7095 resp = _api (mfs_sdk .BrowseApi ).cat (source , range = lines or None )
7196 return resp .content
7297
0 commit comments