[{'Text': 'Edited point-topic-mcp/src/point_topic_mcp/tools/gbs_tools.py:\n\n```diff\n@@ -53,6 +53,102 @@\n     def _admin_url(operator_id: str) -> str:\n         return f"{ADMIN_BASE}/operators/{operator_id}/statistics"\n \n+    def _validate_pipeline(pipeline: list) -> None:\n+        """Validate pipeline is read-only. Raises ValueError if not."""\n+        if not isinstance(pipeline, list):\n+            raise ValueError("Pipeline must be a JSON array of stage objects")\n+\n+        for i, stage in enumerate(pipeline):\n+            if not isinstance(stage, dict):\n+                raise ValueError(f"Stage {i} is not a JSON object: {stage}")\n+            if not stage:\n+                raise ValueError(f"Stage {i} is empty")\n+            stage_name = next(iter(stage))\n+            if stage_name in _GBS_WRITE_STAGES:\n+                raise ValueError(\n+                    f"Stage {i} uses \'{stage_name}\' which is write-only. "\n+                    f"Only read-only aggregation pipelines are allowed."\n+                )\n+\n+    def read_gbs(pipeline_json: str = "", pipeline: list | None = None) -> str:\n+        """Run a read-only MongoDB aggregation pipeline against GBS collections.\n+\n+        Primary collections:\n+          - Operator        — 1300+ operators (fields: name, country, technologies)\n+          - Statistics      — Subscriber counts (fields: type, operatorId, period, tech,\n+                              channel, domain, subscribers, state)\n+          - Sources         — Source URLs/files linked to operator/period\n+          - GlobalVariables — System config (currentStatsPeriod, currentTariffsPeriod)\n+\n+        Provide pipeline_json (JSON string) or pipeline (Python list for internal use).\n+        Supports EJSON syntax: {"$oid": "..."} for ObjectId, {"$date": "..."} for dates.\n+\n+        Returns:\n+            Formatted JSON string of results, or error message.\n+        """\n+        if pipeline_json:\n+            try:\n+                pipeline = json.loads(pipeline_json)\n+            except json.JSONDecodeError as e:\n+                return f"Error: Invalid JSON pipeline — {e}"\n+        elif pipeline is None:\n+            return "Error: Provide pipeline_json (JSON string) or pipeline (list)"\n+\n+        if not isinstance(pipeline, list):\n+            return f"Error: Pipeline must be a JSON array, got {type(pipeline).__name__}"\n+\n+        try:\n+            _validate_pipeline(pipeline)\n+        except ValueError as e:\n+            return f"Error: {e}"\n+\n+        # Auto-detect target collection from $match keys\n+        collection = "Statistics"\n+        first_stage = pipeline[0] if pipeline else {}\n+        if "$match" in first_stage:\n+            match = first_stage["$match"]\n+            if isinstance(match, dict):\n+                mkeys = set(match.keys())\n+                if "isCurrent" in mkeys or "currentStatsPeriod" in mkeys:\n+                    collection = "GlobalVariables"\n+                elif (\n+                    "operatorId" in mkeys or "type" in mkeys or "period" in mkeys\n+                    or "state" in mkeys or "subscribers" in mkeys\n+                ):\n+                    collection = "Statistics"\n+                elif "year" in mkeys and "quarter" in mkeys:\n+                    collection = "Sources"\n+                elif mkeys & {"name", "country", "isArchived", "technologies", "asn", "_id"}:\n+                    collection = "Operator"\n+\n+        if collection not in _GBS_COLLECTIONS:\n+            return f"Error: Collection \'{collection}\' not allowed: {sorted(_GBS_COLLECTIONS)}"\n+\n+        try:\n+            result = mongo.run_aggregation(pipeline, collection=collection, timeout=60)\n+        except RuntimeError as e:\n+            return f"Query error: {e}"\n+        except json.JSONDecodeError as e:\n+            return f"Result parsing error: {e}"\n+        except Exception as e:\n+            return f"Unexpected error: {e}"\n+\n+        if not result:\n+            return "[] — no documents matched the query"\n+\n+        try:\n+            formatted = json.dumps(result, indent=2, default=str)\n+        except Exception:\n+            formatted = str(result)\n+\n+        MAX_CHARS = 50000\n+        if len(formatted) > MAX_CHARS:\n+            formatted = formatted[:MAX_CHARS] + (\n+                f"\\n\\n... [truncated: {len(result)} total docs]"\n+            )\n+\n+        return formatted\n+\n     def add_statistic(type: str, operator_id: str, period: str, tech: str, channel: str,\n                       domain: str, subscribers: int, is_estimate: bool = False,\n                       restated: bool = False, notes: str = "", created_by: str = "gbs-agent",\n\n```'}]