22
33import json
44import re
5+ import time
56from collections import OrderedDict
67from dataclasses import dataclass
78from typing import Any , Callable
1112import mcp .types as types
1213from mcp .server .lowlevel .helper_types import ReadResourceContents
1314
15+ from . import telemetry
1416from .docs_search import DocsSearch
1517from .tool_manager import ToolManager
1618
@@ -253,6 +255,19 @@ def has_public_tool(self, name: str) -> bool:
253255
254256 def execute_public_tool (
255257 self , name : str , arguments : dict [str , Any ] | None
258+ ) -> list [ToolContent ]:
259+ start = time .monotonic ()
260+ outcome = "success"
261+ try :
262+ return self ._dispatch_public_tool (name , arguments )
263+ except Exception :
264+ outcome = "error"
265+ raise
266+ finally :
267+ telemetry .record_tool_call (name , outcome , time .monotonic () - start )
268+
269+ def _dispatch_public_tool (
270+ self , name : str , arguments : dict [str , Any ] | None
256271 ) -> list [ToolContent ]:
257272 if name == "appwrite_get_context" :
258273 return self ._get_context (arguments or {})
@@ -269,6 +284,15 @@ def _get_context(self, arguments: dict[str, Any]) -> list[ToolContent]:
269284 if self ._context_provider is None :
270285 raise RuntimeError ("Appwrite context provider is not configured." )
271286 context = self ._context_provider (arguments )
287+ mode = "unknown"
288+ if isinstance (context , dict ):
289+ connection = context .get ("connection" )
290+ if isinstance (connection , dict ):
291+ mode = str (connection .get ("mode" , "unknown" ))
292+ include_services = bool (
293+ arguments .get ("include_services" , arguments .get ("includeServices" , True ))
294+ )
295+ telemetry .record_context_request (mode = mode , include_services = include_services )
272296 return [types .TextContent (type = "text" , text = json .dumps (context , indent = 2 ))]
273297
274298 def list_resources (self ) -> list [types .Resource ]:
@@ -381,6 +405,9 @@ def _search_tools(self, arguments: dict[str, Any]) -> list[ToolContent]:
381405 include_mutating = include_mutating ,
382406 limit = _normalize_limit (arguments .get ("limit" ), self ._search_limit ),
383407 )
408+ telemetry .record_search_tools (
409+ include_mutating = include_mutating , match_count = len (matches )
410+ )
384411
385412 lines : list [str ] = []
386413 if not matches :
@@ -426,10 +453,13 @@ def _call_hidden_tool(self, raw_arguments: dict[str, Any]) -> list[ToolContent]:
426453 confirm_write = bool (
427454 raw_arguments .get ("confirm_write" , raw_arguments .get ("confirmWrite" , False ))
428455 )
429- if entry .classification != "read" and not confirm_write :
430- raise RuntimeError (
431- f"Tool { tool_name } is { entry .classification } . Re-run appwrite_call_tool with confirm_write=true if you intend to mutate Appwrite state."
432- )
456+ if entry .classification != "read" :
457+ if not confirm_write :
458+ telemetry .record_write_confirmation (entry .classification , "blocked" )
459+ raise RuntimeError (
460+ f"Tool { tool_name } is { entry .classification } . Re-run appwrite_call_tool with confirm_write=true if you intend to mutate Appwrite state."
461+ )
462+ telemetry .record_write_confirmation (entry .classification , "confirmed" )
433463
434464 project_id = raw_arguments .get ("project_id" , raw_arguments .get ("projectId" ))
435465 organization_id = raw_arguments .get (
@@ -454,6 +484,7 @@ def _preview_or_store_result(
454484 stored_result = self ._result_store .save (
455485 tool_name , content , _serialize_content (content )
456486 )
487+ telemetry .record_result_stored (tool_name )
457488 preview = full_text [: self ._preview_threshold ]
458489 return [
459490 types .TextContent (
@@ -468,6 +499,7 @@ def _preview_or_store_result(
468499 stored_result = self ._result_store .save (
469500 tool_name , content , _serialize_content (content )
470501 )
502+ telemetry .record_result_stored (tool_name )
471503 summary = ", " .join (_summarize_content_item (item ) for item in content )
472504 return [
473505 types .TextContent (
0 commit comments