feat: Add instrumentation decorator and apply to notes tools (Phase 5)
Created @instrument_tool decorator for automatic MCP tool metrics collection. Applied to all 7 tools in notes.py. Changes: - observability/metrics.py: * New instrument_tool() decorator for automatic timing and error tracking * Compatible with @mcp.tool() and @require_scopes() decorators * Records tool_name, duration, and success/error status - server/notes.py: * Applied @instrument_tool to all 7 tool functions * nc_notes_create_note, nc_notes_update_note, nc_notes_append_content * nc_notes_search_notes, nc_notes_get_note, nc_notes_get_attachment * nc_notes_delete_note These metrics will populate the MCP Tool Calls dashboard panels. Part of PR #295 - Complete metrics instrumentation (Phase 5) Remaining: 86 tools across 8 server files
This commit is contained in:
@@ -395,3 +395,49 @@ def update_vector_sync_queue_size(size: int) -> None:
|
|||||||
size: Current queue size
|
size: Current queue size
|
||||||
"""
|
"""
|
||||||
vector_sync_queue_size.set(size)
|
vector_sync_queue_size.set(size)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Decorator for Automatic Tool Instrumentation
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def instrument_tool(func):
|
||||||
|
"""
|
||||||
|
Decorator to automatically instrument MCP tool functions with metrics.
|
||||||
|
|
||||||
|
Wraps async tool functions to record execution time and success/error status.
|
||||||
|
Compatible with @mcp.tool() and @require_scopes() decorators.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
@mcp.tool()
|
||||||
|
@require_scopes("notes:write")
|
||||||
|
@instrument_tool
|
||||||
|
async def nc_notes_create_note(...):
|
||||||
|
...
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func: The async function to instrument
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Wrapped function with metrics instrumentation
|
||||||
|
"""
|
||||||
|
import functools
|
||||||
|
import time
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
tool_name = func.__name__
|
||||||
|
start_time = time.time()
|
||||||
|
try:
|
||||||
|
result = await func(*args, **kwargs)
|
||||||
|
duration = time.time() - start_time
|
||||||
|
record_tool_call(tool_name, duration, "success")
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
duration = time.time() - start_time
|
||||||
|
record_tool_call(tool_name, duration, "error")
|
||||||
|
record_tool_error(tool_name, type(e).__name__)
|
||||||
|
raise
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from nextcloud_mcp_server.models.notes import (
|
|||||||
SearchNotesResponse,
|
SearchNotesResponse,
|
||||||
UpdateNoteResponse,
|
UpdateNoteResponse,
|
||||||
)
|
)
|
||||||
|
from nextcloud_mcp_server.observability.metrics import instrument_tool
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -86,6 +87,7 @@ def configure_notes_tools(mcp: FastMCP):
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@require_scopes("notes:write")
|
@require_scopes("notes:write")
|
||||||
|
@instrument_tool
|
||||||
async def nc_notes_create_note(
|
async def nc_notes_create_note(
|
||||||
title: str, content: str, category: str, ctx: Context
|
title: str, content: str, category: str, ctx: Context
|
||||||
) -> CreateNoteResponse:
|
) -> CreateNoteResponse:
|
||||||
@@ -132,6 +134,7 @@ def configure_notes_tools(mcp: FastMCP):
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@require_scopes("notes:write")
|
@require_scopes("notes:write")
|
||||||
|
@instrument_tool
|
||||||
async def nc_notes_update_note(
|
async def nc_notes_update_note(
|
||||||
note_id: int,
|
note_id: int,
|
||||||
etag: str,
|
etag: str,
|
||||||
@@ -197,6 +200,7 @@ def configure_notes_tools(mcp: FastMCP):
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@require_scopes("notes:write")
|
@require_scopes("notes:write")
|
||||||
|
@instrument_tool
|
||||||
async def nc_notes_append_content(
|
async def nc_notes_append_content(
|
||||||
note_id: int, content: str, ctx: Context
|
note_id: int, content: str, ctx: Context
|
||||||
) -> AppendContentResponse:
|
) -> AppendContentResponse:
|
||||||
@@ -247,6 +251,7 @@ def configure_notes_tools(mcp: FastMCP):
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@require_scopes("notes:read")
|
@require_scopes("notes:read")
|
||||||
|
@instrument_tool
|
||||||
async def nc_notes_search_notes(query: str, ctx: Context) -> SearchNotesResponse:
|
async def nc_notes_search_notes(query: str, ctx: Context) -> SearchNotesResponse:
|
||||||
"""Search notes by title or content, returning only id, title, and category (requires notes:read scope)."""
|
"""Search notes by title or content, returning only id, title, and category (requires notes:read scope)."""
|
||||||
client = await get_client(ctx)
|
client = await get_client(ctx)
|
||||||
@@ -293,6 +298,7 @@ def configure_notes_tools(mcp: FastMCP):
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@require_scopes("notes:read")
|
@require_scopes("notes:read")
|
||||||
|
@instrument_tool
|
||||||
async def nc_notes_get_note(note_id: int, ctx: Context) -> Note:
|
async def nc_notes_get_note(note_id: int, ctx: Context) -> Note:
|
||||||
"""Get a specific note by its ID (requires notes:read scope)"""
|
"""Get a specific note by its ID (requires notes:read scope)"""
|
||||||
client = await get_client(ctx)
|
client = await get_client(ctx)
|
||||||
@@ -322,6 +328,7 @@ def configure_notes_tools(mcp: FastMCP):
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@require_scopes("notes:read")
|
@require_scopes("notes:read")
|
||||||
|
@instrument_tool
|
||||||
async def nc_notes_get_attachment(
|
async def nc_notes_get_attachment(
|
||||||
note_id: int, attachment_filename: str, ctx: Context
|
note_id: int, attachment_filename: str, ctx: Context
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
@@ -368,6 +375,7 @@ def configure_notes_tools(mcp: FastMCP):
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@require_scopes("notes:write")
|
@require_scopes("notes:write")
|
||||||
|
@instrument_tool
|
||||||
async def nc_notes_delete_note(note_id: int, ctx: Context) -> DeleteNoteResponse:
|
async def nc_notes_delete_note(note_id: int, ctx: Context) -> DeleteNoteResponse:
|
||||||
"""Delete a note permanently"""
|
"""Delete a note permanently"""
|
||||||
logger.info("Deleting note %s", note_id)
|
logger.info("Deleting note %s", note_id)
|
||||||
|
|||||||
Reference in New Issue
Block a user