diff --git a/agentops/instrumentation/__init__.py b/agentops/instrumentation/__init__.py index e47b6e7fb..1a0ae8163 100644 --- a/agentops/instrumentation/__init__.py +++ b/agentops/instrumentation/__init__.py @@ -118,6 +118,12 @@ class InstrumentorConfig(TypedDict): "min_version": "1.0.0", "package_name": "xpander-sdk", }, + "mcp_agent": { + "module_name": "agentops.instrumentation.agentic.mcp_agent", + "class_name": "MCPAgentInstrumentor", + "min_version": "0.1.0", + "package_name": "mcp-agent", + }, } # Combine all target packages for monitoring diff --git a/agentops/instrumentation/agentic/mcp_agent/INTEGRATION_GUIDE.md b/agentops/instrumentation/agentic/mcp_agent/INTEGRATION_GUIDE.md new file mode 100644 index 000000000..9d6e2d5df --- /dev/null +++ b/agentops/instrumentation/agentic/mcp_agent/INTEGRATION_GUIDE.md @@ -0,0 +1,348 @@ +# MCP Agent Integration Guide + +This guide explains how to integrate MCP Agent with AgentOps for comprehensive observability and monitoring. + +## Quick Start + +### 1. Install Dependencies + +```bash +pip install agentops mcp-agent +``` + +### 2. Initialize AgentOps + +```python +import agentops + +# Initialize AgentOps with your API key +agentops.init("your-api-key-here") +``` + +### 3. Use MCP Agent + +The integration works automatically when both libraries are imported: + +```python +import mcp_agent +from mcp_agent.tracing.telemetry import telemetry + +@telemetry.traced("my_agent_operation") +def my_agent_function(): + # This will be automatically captured by AgentOps + return "result" +``` + +## Integration Methods + +### Method 1: Automatic Integration (Recommended) + +The integration automatically hooks into MCP Agent's telemetry system when both libraries are imported: + +```python +import agentops +import mcp_agent + +# Initialize AgentOps +agentops.init("your-api-key") + +# Use MCP Agent normally - everything is automatically captured +from mcp_agent.core.context import Context +from mcp_agent.tracing.telemetry import telemetry + +@telemetry.traced("agent_execution") +def run_agent(context: Context): + result = context.run_agent("my_agent") + return result +``` + +### Method 2: Manual Integration + +For more control, you can manually hook into the telemetry system: + +```python +from agentops.instrumentation.agentic.mcp_agent import ( + hook_mcp_agent_telemetry, + unhook_mcp_agent_telemetry +) + +# Hook into MCP Agent telemetry +hook_mcp_agent_telemetry() + +# Your MCP Agent code here... + +# Unhook when done (optional) +unhook_mcp_agent_telemetry() +``` + +### Method 3: Custom Spans + +Create custom spans with MCP Agent context: + +```python +from agentops.instrumentation.agentic.mcp_agent import mcp_agent_span + +def custom_workflow(): + with mcp_agent_span( + "custom_workflow", + operation="workflow_execution", + session_id="session-123", + agent_name="my_agent", + workflow_id="workflow-456" + ): + # Your workflow logic here + result = execute_workflow() + return result +``` + +### Method 4: Custom Decorators + +Use custom decorators for functions: + +```python +from agentops.instrumentation.agentic.mcp_agent import mcp_agent_traced + +@mcp_agent_traced( + name="decorated_function", + operation="my_operation", + agent_name="my_agent", + session_id="session-789" +) +def my_function(): + # This function will be captured with the specified attributes + return "result" +``` + +## Advanced Usage + +### Enhancing Existing Spans + +You can enhance existing OpenTelemetry spans with MCP Agent context: + +```python +from opentelemetry import trace +from agentops.instrumentation.agentic.mcp_agent import enhance_mcp_agent_span + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("external_span") as span: + # Enhance the span with MCP Agent attributes + enhance_mcp_agent_span( + span, + operation="external_operation", + session_id="session-123", + agent_name="external_agent" + ) + + # Your code here +``` + +### Tool Call Tracking + +Track tool calls with detailed information: + +```python +from agentops.instrumentation.agentic.mcp_agent import mcp_agent_span + +def execute_tool(tool_name: str, arguments: dict): + with mcp_agent_span( + f"tool_call.{tool_name}", + operation="tool_execution", + tool_name=tool_name, + tool_arguments=arguments + ): + try: + result = call_tool(tool_name, arguments) + return result + except Exception as e: + # Error will be automatically captured + raise +``` + +### Session Management + +Track sessions and workflows: + +```python +from agentops.instrumentation.agentic.mcp_agent import mcp_agent_span + +def run_session(session_id: str, workflow_config: dict): + with mcp_agent_span( + "session_execution", + operation="session", + session_id=session_id, + workflow_id=workflow_config.get("workflow_id") + ): + # Session execution logic + for step in workflow_config["steps"]: + execute_step(step) +``` + +## Configuration + +### Environment Variables + +- `AGENTOPS_MCP_AGENT_METRICS_ENABLED`: Enable/disable metrics collection (default: true) + +### AgentOps Configuration + +```python +import agentops + +agentops.init( + api_key="your-api-key", + application_name="my-mcp-agent-app", + environment="production", + # Other configuration options... +) +``` + +## Captured Data + +### Span Attributes + +The integration captures the following MCP Agent specific attributes: + +- `mcp_agent.operation`: The operation being performed +- `mcp_agent.session_id`: Session identifier +- `mcp_agent.context_id`: Context identifier +- `mcp_agent.workflow_id`: Workflow identifier +- `mcp_agent.agent_id`: Agent identifier +- `mcp_agent.agent_name`: Human-readable agent name +- `mcp_agent.agent_description`: Agent description +- `mcp_agent.function`: Function name being executed +- `mcp_agent.module`: Module name + +### Tool Call Attributes + +For tool executions: + +- `mcp_agent.tool_name`: Name of the tool being called +- `mcp_agent.tool_description`: Tool description +- `mcp_agent.tool_arguments`: Tool arguments +- `mcp_agent.tool_result_content`: Tool result content +- `mcp_agent.tool_result_type`: Type of tool result +- `mcp_agent.tool_error`: Whether the tool call resulted in an error +- `mcp_agent.tool_error_message`: Error message if applicable + +## Best Practices + +### 1. Use Descriptive Operation Names + +```python +@telemetry.traced("agent.workflow.execution") # Good +def execute_workflow(): + pass + +@telemetry.traced("func") # Avoid +def execute_workflow(): + pass +``` + +### 2. Include Context Information + +```python +with mcp_agent_span( + "workflow_execution", + operation="workflow", + session_id=session_id, + agent_name=agent_name, + workflow_id=workflow_id +): + # Your code here +``` + +### 3. Handle Errors Gracefully + +```python +@telemetry.traced("agent_operation") +def agent_operation(): + try: + result = risky_operation() + return result + except Exception as e: + # Error will be automatically captured by the integration + logger.error(f"Operation failed: {e}") + raise +``` + +### 4. Use Consistent Naming + +```python +# Use consistent naming patterns +@telemetry.traced("agent.workflow.step1") +def step1(): + pass + +@telemetry.traced("agent.workflow.step2") +def step2(): + pass +``` + +## Troubleshooting + +### Common Issues + +1. **Integration not working**: Ensure both `agentops` and `mcp-agent` are installed and imported +2. **Missing spans**: Check that AgentOps is properly initialized with a valid API key +3. **Telemetry conflicts**: The integration is designed to work alongside MCP Agent's existing telemetry + +### Debug Mode + +Enable debug logging to see detailed integration information: + +```python +import logging +logging.getLogger("agentops.instrumentation.agentic.mcp_agent").setLevel(logging.DEBUG) +``` + +### Testing the Integration + +Run the example to test the integration: + +```python +from agentops.instrumentation.agentic.mcp_agent.example import example_mcp_agent_integration + +example_mcp_agent_integration() +``` + +## Migration from Other Systems + +### From Manual Instrumentation + +If you were manually creating spans, you can now use the automatic integration: + +```python +# Before (manual) +with tracer.start_as_current_span("agent_operation") as span: + span.set_attribute("agent.name", "my_agent") + # Your code here + +# After (automatic) +@telemetry.traced("agent_operation") +def agent_operation(): + # Your code here - automatically captured + pass +``` + +### From Other Observability Systems + +The integration is designed to work alongside other observability systems. You can gradually migrate: + +```python +# Keep existing instrumentation +with existing_tracer.start_span("legacy_span"): + # Add MCP Agent integration + with mcp_agent_span("mcp_agent_operation"): + # Your code here + pass +``` + +## Support + +For issues and questions: + +1. Check the troubleshooting section above +2. Review the example code +3. Enable debug logging for detailed information +4. Check the AgentOps documentation for general issues \ No newline at end of file diff --git a/agentops/instrumentation/agentic/mcp_agent/README.md b/agentops/instrumentation/agentic/mcp_agent/README.md new file mode 100644 index 000000000..f2c884c81 --- /dev/null +++ b/agentops/instrumentation/agentic/mcp_agent/README.md @@ -0,0 +1,254 @@ +# MCP Agent Integration for AgentOps + +This module provides integration between MCP Agent and AgentOps for comprehensive observability and monitoring of agent operations, tool calls, and workflow execution. + +## Overview + +MCP Agent is a framework for building effective agents with Model Context Protocol (MCP). This integration hooks into MCP Agent's existing telemetry system to capture: + +- Agent operations and workflow execution +- Tool calls and their results +- Session and context information +- Error handling and debugging information + +## Features + +- **Automatic Instrumentation**: Hooks into MCP Agent's telemetry system automatically +- **Enhanced Spans**: Adds AgentOps-specific attributes to all MCP Agent spans +- **Tool Call Tracking**: Captures tool execution details and results +- **Session Management**: Tracks session IDs and context information +- **Error Handling**: Comprehensive error tracking and debugging support + +## Installation + +The integration is automatically available when both `agentops` and `mcp-agent` are installed: + +```bash +pip install agentops mcp-agent +``` + +## Usage + +### Automatic Integration + +The integration works automatically when you import both libraries: + +```python +import agentops +import mcp_agent + +# Initialize AgentOps +agentops.init("your-api-key") + +# Your MCP Agent code will be automatically instrumented +from mcp_agent.core.context import Context +from mcp_agent.tracing.telemetry import telemetry + +@telemetry.traced("my_agent_operation") +def my_agent_function(): + # This will be automatically captured by AgentOps + pass +``` + +### Manual Integration + +You can also manually control the integration: + +```python +from agentops.instrumentation.agentic.mcp_agent import ( + hook_mcp_agent_telemetry, + unhook_mcp_agent_telemetry, + mcp_agent_span, + mcp_agent_traced +) + +# Hook into MCP Agent telemetry +hook_mcp_agent_telemetry() + +# Use custom spans +with mcp_agent_span( + "custom_operation", + operation="my_custom_operation", + session_id="session-123", + agent_name="my_agent" +): + # Your code here + pass + +# Use custom decorators +@mcp_agent_traced( + name="decorated_function", + operation="my_decorated_operation", + agent_name="my_agent" +) +def my_function(): + pass +``` + +### Enhanced Spans + +You can enhance existing MCP Agent spans with additional AgentOps attributes: + +```python +from agentops.instrumentation.agentic.mcp_agent import enhance_mcp_agent_span +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("my_span") as span: + # Enhance the span with MCP Agent attributes + enhance_mcp_agent_span( + span, + operation="my_operation", + session_id="session-123", + agent_name="my_agent", + workflow_id="workflow-456" + ) +``` + +## Configuration + +### Environment Variables + +- `AGENTOPS_MCP_AGENT_METRICS_ENABLED`: Enable/disable metrics collection (default: true) + +### AgentOps Configuration + +The integration respects all standard AgentOps configuration options: + +```python +import agentops + +agentops.init( + api_key="your-api-key", + application_name="my-mcp-agent-app", + environment="production" +) +``` + +## Captured Data + +### Span Attributes + +The integration captures the following MCP Agent specific attributes: + +- `mcp_agent.operation`: The operation being performed +- `mcp_agent.session_id`: Session identifier +- `mcp_agent.context_id`: Context identifier +- `mcp_agent.workflow_id`: Workflow identifier +- `mcp_agent.agent_id`: Agent identifier +- `mcp_agent.agent_name`: Human-readable agent name +- `mcp_agent.agent_description`: Agent description +- `mcp_agent.function`: Function name being executed +- `mcp_agent.module`: Module name + +### Tool Call Attributes + +For tool executions, additional attributes are captured: + +- `mcp_agent.tool_name`: Name of the tool being called +- `mcp_agent.tool_description`: Tool description +- `mcp_agent.tool_arguments`: Tool arguments +- `mcp_agent.tool_result_content`: Tool result content +- `mcp_agent.tool_result_type`: Type of tool result +- `mcp_agent.tool_error`: Whether the tool call resulted in an error +- `mcp_agent.tool_error_message`: Error message if applicable + +## Examples + +### Basic Agent Usage + +```python +import agentops +from mcp_agent.core.context import Context +from mcp_agent.tracing.telemetry import telemetry + +# Initialize AgentOps +agentops.init("your-api-key") + +@telemetry.traced("agent_execution") +def run_agent(context: Context): + # This will be automatically captured with full context + result = context.run_agent("my_agent") + return result +``` + +### Custom Tool Integration + +```python +from agentops.instrumentation.agentic.mcp_agent import mcp_agent_span + +def custom_tool_call(tool_name: str, arguments: dict): + with mcp_agent_span( + f"tool_call.{tool_name}", + operation="tool_execution", + tool_name=tool_name, + tool_arguments=arguments + ): + # Tool execution logic + result = execute_tool(tool_name, arguments) + return result +``` + +### Workflow Tracking + +```python +from agentops.instrumentation.agentic.mcp_agent import mcp_agent_traced + +@mcp_agent_traced( + name="workflow_execution", + operation="workflow", + workflow_id="workflow-123", + agent_name="workflow_agent" +) +def execute_workflow(workflow_config: dict): + # Workflow execution logic + pass +``` + +## Troubleshooting + +### Common Issues + +1. **Integration not working**: Ensure both `agentops` and `mcp-agent` are installed and imported +2. **Missing spans**: Check that AgentOps is properly initialized with a valid API key +3. **Telemetry conflicts**: The integration is designed to work alongside MCP Agent's existing telemetry + +### Debug Mode + +Enable debug logging to see detailed integration information: + +```python +import logging +logging.getLogger("agentops.instrumentation.agentic.mcp_agent").setLevel(logging.DEBUG) +``` + +## API Reference + +### Functions + +- `hook_mcp_agent_telemetry()`: Hook into MCP Agent's telemetry system +- `unhook_mcp_agent_telemetry()`: Unhook from MCP Agent's telemetry system +- `enhance_mcp_agent_span(span, **attributes)`: Enhance an existing span with MCP Agent attributes +- `mcp_agent_span(name, **kwargs)`: Context manager for creating MCP Agent spans +- `mcp_agent_traced(**kwargs)`: Decorator for MCP Agent functions + +### Classes + +- `MCPAgentInstrumentor`: Main instrumentor class +- `MCPAgentTelemetryHook`: Telemetry hook implementation +- `MCPAgentSpanAttributes`: Span attribute definitions + +## Contributing + +To contribute to this integration: + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests +5. Submit a pull request + +## License + +This integration is part of the AgentOps project and follows the same license terms. \ No newline at end of file diff --git a/agentops/instrumentation/agentic/mcp_agent/__init__.py b/agentops/instrumentation/agentic/mcp_agent/__init__.py new file mode 100644 index 000000000..fd19d0898 --- /dev/null +++ b/agentops/instrumentation/agentic/mcp_agent/__init__.py @@ -0,0 +1,42 @@ +""" +AgentOps instrumentation for MCP Agent. + +This module provides integration with MCP Agent's telemetry system to capture +agent operations, tool calls, and workflow execution for observability. +""" + +from .instrumentation import ( + MCPAgentInstrumentor, + instrument_mcp_agent, + uninstrument_mcp_agent, + instrument_mcp_agent_tool_calls, +) +from .mcp_agent_span_attributes import ( + MCPAgentSpanAttributes, + set_mcp_agent_span_attributes, + set_mcp_agent_tool_attributes, +) +from .telemetry_hook import ( + hook_mcp_agent_telemetry, + unhook_mcp_agent_telemetry, + enhance_mcp_agent_span, + mcp_agent_span, + mcp_agent_traced, +) +from .version import __version__ + +__all__ = [ + "MCPAgentInstrumentor", + "instrument_mcp_agent", + "uninstrument_mcp_agent", + "instrument_mcp_agent_tool_calls", + "MCPAgentSpanAttributes", + "set_mcp_agent_span_attributes", + "set_mcp_agent_tool_attributes", + "hook_mcp_agent_telemetry", + "unhook_mcp_agent_telemetry", + "enhance_mcp_agent_span", + "mcp_agent_span", + "mcp_agent_traced", + "__version__", +] \ No newline at end of file diff --git a/agentops/instrumentation/agentic/mcp_agent/example.py b/agentops/instrumentation/agentic/mcp_agent/example.py new file mode 100644 index 000000000..ffa8724cc --- /dev/null +++ b/agentops/instrumentation/agentic/mcp_agent/example.py @@ -0,0 +1,184 @@ +""" +Example usage of MCP Agent integration with AgentOps. + +This example demonstrates how to use the integration to capture +agent operations, tool calls, and workflow execution. +""" + +import asyncio +import logging +from typing import Dict, Any + +# Configure logging to see integration details +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Example function that simulates MCP Agent usage +def example_mcp_agent_integration(): + """Example of how the integration works with MCP Agent.""" + + # This would typically be done at the start of your application + import agentops + agentops.init("your-api-key-here") + + # The integration automatically hooks into MCP Agent's telemetry + # when both libraries are imported + try: + from mcp_agent.tracing.telemetry import telemetry + from agentops.instrumentation.agentic.mcp_agent import ( + mcp_agent_span, + mcp_agent_traced, + enhance_mcp_agent_span + ) + + # Example 1: Using MCP Agent's telemetry with automatic AgentOps integration + @telemetry.traced("example_agent_operation") + def agent_operation(): + """This function will be automatically captured by both MCP Agent and AgentOps.""" + logger.info("Executing agent operation") + return "operation_result" + + # Example 2: Using custom AgentOps spans + def custom_agent_workflow(): + with mcp_agent_span( + "custom_workflow", + operation="workflow_execution", + session_id="session-123", + agent_name="example_agent", + workflow_id="workflow-456" + ): + logger.info("Executing custom workflow") + # Simulate some work + result = agent_operation() + return result + + # Example 3: Using custom decorators + @mcp_agent_traced( + name="decorated_function", + operation="decorated_operation", + agent_name="decorated_agent", + session_id="session-789" + ) + def decorated_function(): + """This function uses the custom AgentOps decorator.""" + logger.info("Executing decorated function") + return "decorated_result" + + # Example 4: Tool call simulation + def simulate_tool_call(tool_name: str, arguments: Dict[str, Any]): + with mcp_agent_span( + f"tool_call.{tool_name}", + operation="tool_execution", + tool_name=tool_name, + tool_arguments=arguments + ): + logger.info(f"Executing tool: {tool_name}") + # Simulate tool execution + if tool_name == "search": + return {"results": ["result1", "result2"]} + elif tool_name == "calculate": + return {"result": arguments.get("value", 0) * 2} + else: + raise ValueError(f"Unknown tool: {tool_name}") + + # Run examples + logger.info("Running MCP Agent integration examples...") + + # Example 1 + result1 = agent_operation() + logger.info(f"Agent operation result: {result1}") + + # Example 2 + result2 = custom_agent_workflow() + logger.info(f"Custom workflow result: {result2}") + + # Example 3 + result3 = decorated_function() + logger.info(f"Decorated function result: {result3}") + + # Example 4 + search_result = simulate_tool_call("search", {"query": "example"}) + logger.info(f"Search tool result: {search_result}") + + calc_result = simulate_tool_call("calculate", {"value": 42}) + logger.info(f"Calculate tool result: {calc_result}") + + # Example 5: Error handling + try: + simulate_tool_call("unknown_tool", {}) + except ValueError as e: + logger.info(f"Expected error: {e}") + + logger.info("All examples completed successfully!") + + except ImportError as e: + logger.warning(f"MCP Agent not available: {e}") + logger.info("This is expected if mcp-agent is not installed") + + +async def async_example(): + """Example of async MCP Agent integration.""" + + try: + from mcp_agent.tracing.telemetry import telemetry + from agentops.instrumentation.agentic.mcp_agent import mcp_agent_span + + @telemetry.traced("async_agent_operation") + async def async_agent_operation(): + """Async function that will be captured by both systems.""" + logger.info("Executing async agent operation") + await asyncio.sleep(0.1) # Simulate async work + return "async_result" + + async def async_workflow(): + with mcp_agent_span( + "async_workflow", + operation="async_workflow_execution", + session_id="async-session-123" + ): + logger.info("Executing async workflow") + result = await async_agent_operation() + return result + + logger.info("Running async MCP Agent integration examples...") + result = await async_workflow() + logger.info(f"Async workflow result: {result}") + + except ImportError as e: + logger.warning(f"MCP Agent not available for async example: {e}") + + +def example_with_context(): + """Example showing how to enhance existing spans with MCP Agent context.""" + + try: + from opentelemetry import trace + from agentops.instrumentation.agentic.mcp_agent import enhance_mcp_agent_span + + tracer = trace.get_tracer(__name__) + + with tracer.start_as_current_span("external_span") as span: + # Enhance the span with MCP Agent attributes + enhance_mcp_agent_span( + span, + operation="external_operation", + session_id="external-session-123", + agent_name="external_agent", + workflow_id="external-workflow-456" + ) + + logger.info("Enhanced external span with MCP Agent attributes") + + except ImportError as e: + logger.warning(f"OpenTelemetry not available: {e}") + + +if __name__ == "__main__": + # Run synchronous examples + example_mcp_agent_integration() + example_with_context() + + # Run async examples + asyncio.run(async_example()) + + logger.info("All examples completed!") \ No newline at end of file diff --git a/agentops/instrumentation/agentic/mcp_agent/instrumentation.py b/agentops/instrumentation/agentic/mcp_agent/instrumentation.py new file mode 100644 index 000000000..828fec19f --- /dev/null +++ b/agentops/instrumentation/agentic/mcp_agent/instrumentation.py @@ -0,0 +1,343 @@ +""" +AgentOps instrumentation for MCP Agent. + +This module provides integration with MCP Agent's telemetry system to capture +agent operations, tool calls, and workflow execution for observability. +""" + +import os +import time +import logging +from typing import Dict, Any, Optional, Callable +from contextlib import contextmanager +import functools + +from opentelemetry.trace import SpanKind, get_current_span +from opentelemetry.metrics import Meter +from opentelemetry.instrumentation.utils import unwrap + +from agentops.instrumentation.common import ( + CommonInstrumentor, + InstrumentorConfig, + StandardMetrics, + create_wrapper_factory, + create_span, + SpanAttributeManager, + safe_set_attribute, + set_token_usage_attributes, + TokenUsageExtractor, +) +from agentops.instrumentation.agentic.mcp_agent.version import __version__ +from agentops.semconv import SpanAttributes, AgentOpsSpanKindValues, ToolAttributes, MessageAttributes +from agentops.semconv.core import CoreAttributes +from agentops.instrumentation.agentic.mcp_agent.mcp_agent_span_attributes import ( + MCPAgentSpanAttributes, + set_mcp_agent_span_attributes, + set_mcp_agent_tool_attributes, +) +from agentops import get_client + +# Initialize logger +logger = logging.getLogger(__name__) + +_instruments = ("mcp-agent >= 0.1.0",) + +# Global context to store MCP Agent specific data +_mcp_agent_context = {} + + +def is_metrics_enabled() -> bool: + """Check if metrics are enabled for MCP Agent instrumentation.""" + return os.getenv("AGENTOPS_MCP_AGENT_METRICS_ENABLED", "true").lower() == "true" + + +class MCPAgentInstrumentor(CommonInstrumentor): + """Instrumentor for MCP Agent framework.""" + + def __init__(self): + config = InstrumentorConfig( + library_name="mcp-agent", + library_version=__version__, + wrapped_methods=[], # We'll use custom wrapping for MCP Agent + metrics_enabled=is_metrics_enabled(), + dependencies=_instruments, + ) + super().__init__(config) + self._attribute_manager = None + self._original_telemetry_manager = None + + def _initialize(self, **kwargs): + """Initialize attribute manager and hook into MCP Agent telemetry.""" + application_name = kwargs.get("application_name", "default_application") + environment = kwargs.get("environment", "default_environment") + self._attribute_manager = SpanAttributeManager( + service_name=application_name, deployment_environment=environment + ) + + # Hook into MCP Agent's telemetry system + self._hook_into_mcp_agent_telemetry() + + def _create_metrics(self, meter: Meter) -> Dict[str, Any]: + """Create metrics for MCP Agent instrumentation.""" + return StandardMetrics.create_standard_metrics(meter) + + def _custom_wrap(self, **kwargs): + """Perform custom wrapping for MCP Agent methods.""" + from wrapt import wrap_function_wrapper + + # Hook into the telemetry manager's traced decorator + try: + from mcp_agent.tracing.telemetry import TelemetryManager + + # Store the original telemetry manager + self._original_telemetry_manager = TelemetryManager + + # Wrap the traced decorator to add AgentOps instrumentation + wrap_function_wrapper( + "mcp_agent.tracing.telemetry", + "TelemetryManager.traced", + self._wrap_traced_decorator + ) + + logger.info("Successfully hooked into MCP Agent telemetry system") + + except ImportError as e: + logger.warning(f"Could not import MCP Agent telemetry: {e}") + except Exception as e: + logger.warning(f"Failed to hook into MCP Agent telemetry: {e}") + + def _hook_into_mcp_agent_telemetry(self): + """Hook into MCP Agent's telemetry system to capture spans.""" + try: + # Import MCP Agent modules + from mcp_agent.tracing.telemetry import telemetry + from mcp_agent.core.context import Context + + # Store the original telemetry instance + _mcp_agent_context["original_telemetry"] = telemetry + + # Create a wrapper around the telemetry manager + self._wrap_telemetry_manager(telemetry) + + logger.info("Successfully hooked into MCP Agent telemetry system") + + except ImportError as e: + logger.warning(f"Could not import MCP Agent modules: {e}") + except Exception as e: + logger.warning(f"Failed to hook into MCP Agent telemetry: {e}") + + def _wrap_telemetry_manager(self, telemetry_manager): + """Wrap the telemetry manager to add AgentOps instrumentation.""" + original_traced = telemetry_manager.traced + + @functools.wraps(original_traced) + def wrapped_traced(name=None, kind=None, attributes=None): + def decorator(func): + # Call the original decorator + wrapped_func = original_traced(name, kind, attributes)(func) + + # Add AgentOps instrumentation + @functools.wraps(wrapped_func) + def agentops_wrapper(*args, **kwargs): + # Create AgentOps span + span_name = name or f"mcp_agent.{func.__qualname__}" + + with create_span( + span_name, + kind=SpanKind.INTERNAL, + attributes={ + SpanAttributes.AGENTOPS_SPAN_KIND: AgentOpsSpanKindValues.AGENTIC, + "mcp_agent.function": func.__qualname__, + "mcp_agent.module": func.__module__, + } + ) as span: + # Set MCP Agent specific attributes + set_mcp_agent_span_attributes( + span, + operation=func.__qualname__, + **attributes or {} + ) + + # Execute the original function + try: + result = wrapped_func(*args, **kwargs) + return result + except Exception as e: + span.record_exception(e) + span.set_status(span.Status(span.StatusCode.ERROR)) + raise + + return agentops_wrapper + + return decorator + + # Replace the traced method + telemetry_manager.traced = wrapped_traced + + def _wrap_traced_decorator(self, original_traced, instance, args, kwargs): + """Wrap the traced decorator to add AgentOps instrumentation.""" + def decorator(func): + # Call the original decorator + wrapped_func = original_traced(func) + + # Add AgentOps instrumentation + @functools.wraps(wrapped_func) + def agentops_wrapper(*args, **kwargs): + # Create AgentOps span + span_name = f"mcp_agent.{func.__qualname__}" + + with create_span( + span_name, + kind=SpanKind.INTERNAL, + attributes={ + SpanAttributes.AGENTOPS_SPAN_KIND: AgentOpsSpanKindValues.AGENTIC, + "mcp_agent.function": func.__qualname__, + "mcp_agent.module": func.__module__, + } + ) as span: + # Set MCP Agent specific attributes + set_mcp_agent_span_attributes( + span, + operation=func.__qualname__, + ) + + # Execute the original function + try: + result = wrapped_func(*args, **kwargs) + return result + except Exception as e: + span.record_exception(e) + span.set_status(span.Status(span.StatusCode.ERROR)) + raise + + return agentops_wrapper + + return decorator + + def _uninstrument(self, **kwargs): + """Uninstrument MCP Agent.""" + # Restore original telemetry manager if available + if self._original_telemetry_manager: + try: + from mcp_agent.tracing.telemetry import TelemetryManager + # Restore the original traced method + unwrap(TelemetryManager, "traced") + logger.info("Successfully uninstrumented MCP Agent telemetry") + except Exception as e: + logger.warning(f"Failed to uninstrument MCP Agent telemetry: {e}") + + +# Global instrumentor instance +_mcp_agent_instrumentor = None + + +def instrument_mcp_agent(**kwargs): + """Instrument MCP Agent for AgentOps observability.""" + global _mcp_agent_instrumentor + + if _mcp_agent_instrumentor is None: + _mcp_agent_instrumentor = MCPAgentInstrumentor() + _mcp_agent_instrumentor.instrument(**kwargs) + logger.info("MCP Agent instrumentation initialized") + + return _mcp_agent_instrumentor + + +def uninstrument_mcp_agent(): + """Uninstrument MCP Agent.""" + global _mcp_agent_instrumentor + + if _mcp_agent_instrumentor is not None: + _mcp_agent_instrumentor.uninstrument() + _mcp_agent_instrumentor = None + logger.info("MCP Agent instrumentation removed") + + +# Hook into MCP Agent tool calls +def instrument_mcp_agent_tool_calls(): + """Instrument MCP Agent tool calls for better observability.""" + try: + from mcp_agent.core.context import Context + from mcp.types import CallToolResult + + # Hook into tool call execution + def wrap_tool_call(original_func): + @functools.wraps(original_func) + def wrapper(*args, **kwargs): + # Extract tool information from args/kwargs + tool_name = kwargs.get("tool_name") or "unknown_tool" + + with create_span( + f"mcp_agent.tool_call.{tool_name}", + kind=SpanKind.INTERNAL, + attributes={ + SpanAttributes.AGENTOPS_SPAN_KIND: AgentOpsSpanKindValues.TOOL, + ToolAttributes.TOOL_NAME: tool_name, + } + ) as span: + # Set tool-specific attributes + set_mcp_agent_tool_attributes( + span, + tool_name=tool_name, + tool_arguments=kwargs.get("arguments"), + ) + + try: + result = original_func(*args, **kwargs) + + # If result is a CallToolResult, extract information + if hasattr(result, "isError"): + set_mcp_agent_tool_attributes( + span, + tool_error=result.isError, + tool_result_type="CallToolResult", + ) + + if result.isError: + span.set_status(span.Status(span.StatusCode.ERROR)) + # Extract error message if available + if hasattr(result, "content") and result.content: + error_message = "Tool execution failed" + if result.content[0].type == "text": + error_message = result.content[0].text + set_mcp_agent_tool_attributes( + span, + tool_error_message=error_message, + ) + + return result + except Exception as e: + span.record_exception(e) + span.set_status(span.Status(span.StatusCode.ERROR)) + set_mcp_agent_tool_attributes( + span, + tool_error=True, + tool_error_message=str(e), + ) + raise + + return wrapper + + # Apply the wrapper to relevant functions + # This would need to be customized based on the specific MCP Agent API + + except ImportError as e: + logger.warning(f"Could not instrument MCP Agent tool calls: {e}") + except Exception as e: + logger.warning(f"Failed to instrument MCP Agent tool calls: {e}") + + +# Auto-instrumentation when the module is imported +def _auto_instrument(): + """Automatically instrument MCP Agent when this module is imported.""" + try: + # Check if MCP Agent is available + import mcp_agent + instrument_mcp_agent() + logger.info("Auto-instrumented MCP Agent") + except ImportError: + logger.debug("MCP Agent not available for auto-instrumentation") + + +# Run auto-instrumentation +_auto_instrument() \ No newline at end of file diff --git a/agentops/instrumentation/agentic/mcp_agent/mcp_agent_span_attributes.py b/agentops/instrumentation/agentic/mcp_agent/mcp_agent_span_attributes.py new file mode 100644 index 000000000..cc7decc4c --- /dev/null +++ b/agentops/instrumentation/agentic/mcp_agent/mcp_agent_span_attributes.py @@ -0,0 +1,83 @@ +""" +MCP Agent specific span attributes for AgentOps instrumentation. +""" + +from typing import Any, Dict, Optional +from opentelemetry.trace import Span + +from agentops.semconv import SpanAttributes, AgentOpsSpanKindValues, ToolAttributes, MessageAttributes +from agentops.semconv.core import CoreAttributes + + +class MCPAgentSpanAttributes: + """MCP Agent specific span attributes.""" + + # MCP Agent specific attributes + MCP_AGENT_OPERATION = "mcp_agent.operation" + MCP_AGENT_TOOL_CALL = "mcp_agent.tool_call" + MCP_AGENT_TOOL_RESULT = "mcp_agent.tool_result" + MCP_AGENT_SESSION_ID = "mcp_agent.session_id" + MCP_AGENT_CONTEXT_ID = "mcp_agent.context_id" + MCP_AGENT_WORKFLOW_ID = "mcp_agent.workflow_id" + MCP_AGENT_AGENT_ID = "mcp_agent.agent_id" + MCP_AGENT_AGENT_NAME = "mcp_agent.agent_name" + MCP_AGENT_AGENT_DESCRIPTION = "mcp_agent.agent_description" + MCP_AGENT_TOOL_NAME = "mcp_agent.tool_name" + MCP_AGENT_TOOL_DESCRIPTION = "mcp_agent.tool_description" + MCP_AGENT_TOOL_ARGUMENTS = "mcp_agent.tool_arguments" + MCP_AGENT_TOOL_RESULT_CONTENT = "mcp_agent.tool_result_content" + MCP_AGENT_TOOL_RESULT_TYPE = "mcp_agent.tool_result_type" + MCP_AGENT_TOOL_ERROR = "mcp_agent.tool_error" + MCP_AGENT_TOOL_ERROR_MESSAGE = "mcp_agent.tool_error_message" + + +def set_span_attribute(span: Span, key: str, value: Any) -> None: + """Set a span attribute if the value is not None.""" + if value is not None: + span.set_attribute(key, str(value)) + + +def set_mcp_agent_span_attributes( + span: Span, + operation: Optional[str] = None, + session_id: Optional[str] = None, + context_id: Optional[str] = None, + workflow_id: Optional[str] = None, + agent_id: Optional[str] = None, + agent_name: Optional[str] = None, + agent_description: Optional[str] = None, + **kwargs +) -> None: + """Set MCP Agent specific span attributes.""" + set_span_attribute(span, MCPAgentSpanAttributes.MCP_AGENT_OPERATION, operation) + set_span_attribute(span, MCPAgentSpanAttributes.MCP_AGENT_SESSION_ID, session_id) + set_span_attribute(span, MCPAgentSpanAttributes.MCP_AGENT_CONTEXT_ID, context_id) + set_span_attribute(span, MCPAgentSpanAttributes.MCP_AGENT_WORKFLOW_ID, workflow_id) + set_span_attribute(span, MCPAgentSpanAttributes.MCP_AGENT_AGENT_ID, agent_id) + set_span_attribute(span, MCPAgentSpanAttributes.MCP_AGENT_AGENT_NAME, agent_name) + set_span_attribute(span, MCPAgentSpanAttributes.MCP_AGENT_AGENT_DESCRIPTION, agent_description) + + # Set any additional attributes + for key, value in kwargs.items(): + if value is not None: + span.set_attribute(f"mcp_agent.{key}", str(value)) + + +def set_mcp_agent_tool_attributes( + span: Span, + tool_name: Optional[str] = None, + tool_description: Optional[str] = None, + tool_arguments: Optional[Dict[str, Any]] = None, + tool_result_content: Optional[str] = None, + tool_result_type: Optional[str] = None, + tool_error: Optional[bool] = None, + tool_error_message: Optional[str] = None, +) -> None: + """Set MCP Agent tool specific span attributes.""" + set_span_attribute(span, MCPAgentSpanAttributes.MCP_AGENT_TOOL_NAME, tool_name) + set_span_attribute(span, MCPAgentSpanAttributes.MCP_AGENT_TOOL_DESCRIPTION, tool_description) + set_span_attribute(span, MCPAgentSpanAttributes.MCP_AGENT_TOOL_ARGUMENTS, str(tool_arguments) if tool_arguments else None) + set_span_attribute(span, MCPAgentSpanAttributes.MCP_AGENT_TOOL_RESULT_CONTENT, tool_result_content) + set_span_attribute(span, MCPAgentSpanAttributes.MCP_AGENT_TOOL_RESULT_TYPE, tool_result_type) + set_span_attribute(span, MCPAgentSpanAttributes.MCP_AGENT_TOOL_ERROR, tool_error) + set_span_attribute(span, MCPAgentSpanAttributes.MCP_AGENT_TOOL_ERROR_MESSAGE, tool_error_message) \ No newline at end of file diff --git a/agentops/instrumentation/agentic/mcp_agent/telemetry_hook.py b/agentops/instrumentation/agentic/mcp_agent/telemetry_hook.py new file mode 100644 index 000000000..f63bfb464 --- /dev/null +++ b/agentops/instrumentation/agentic/mcp_agent/telemetry_hook.py @@ -0,0 +1,251 @@ +""" +Telemetry hook for MCP Agent integration with AgentOps. + +This module provides hooks into MCP Agent's existing telemetry system to capture +spans and events for AgentOps observability. +""" + +import logging +import functools +from typing import Any, Dict, Optional, Callable +from contextlib import contextmanager + +from opentelemetry.trace import SpanKind, get_current_span +from opentelemetry.trace.span import Span + +from agentops.instrumentation.common import create_span +from agentops.semconv import SpanAttributes, AgentOpsSpanKindValues, ToolAttributes +from agentops.instrumentation.agentic.mcp_agent.mcp_agent_span_attributes import ( + set_mcp_agent_span_attributes, + set_mcp_agent_tool_attributes, +) + +logger = logging.getLogger(__name__) + + +class MCPAgentTelemetryHook: + """Hook into MCP Agent's telemetry system to add AgentOps instrumentation.""" + + def __init__(self): + self._original_telemetry = None + self._original_traced = None + self._hooked = False + + def hook_into_telemetry(self): + """Hook into MCP Agent's telemetry system.""" + if self._hooked: + logger.debug("Already hooked into MCP Agent telemetry") + return + + try: + from mcp_agent.tracing.telemetry import telemetry, TelemetryManager + + # Store original telemetry + self._original_telemetry = telemetry + self._original_traced = telemetry.traced + + # Replace the traced decorator with our enhanced version + telemetry.traced = self._enhanced_traced_decorator + + self._hooked = True + logger.info("Successfully hooked into MCP Agent telemetry system") + + except ImportError as e: + logger.warning(f"Could not import MCP Agent telemetry: {e}") + except Exception as e: + logger.warning(f"Failed to hook into MCP Agent telemetry: {e}") + + def unhook_from_telemetry(self): + """Unhook from MCP Agent's telemetry system.""" + if not self._hooked: + return + + try: + if self._original_telemetry and self._original_traced: + self._original_telemetry.traced = self._original_traced + self._hooked = False + logger.info("Successfully unhooked from MCP Agent telemetry system") + except Exception as e: + logger.warning(f"Failed to unhook from MCP Agent telemetry: {e}") + + def _enhanced_traced_decorator(self, name=None, kind=None, attributes=None): + """Enhanced traced decorator that adds AgentOps instrumentation.""" + def decorator(func): + # Get the original traced decorator result + original_wrapper = self._original_traced(name, kind, attributes)(func) + + @functools.wraps(func) + def agentops_wrapper(*args, **kwargs): + # Create AgentOps span + span_name = name or f"mcp_agent.{func.__qualname__}" + + with create_span( + span_name, + kind=SpanKind.INTERNAL, + attributes={ + SpanAttributes.AGENTOPS_SPAN_KIND: AgentOpsSpanKindValues.AGENTIC, + "mcp_agent.function": func.__qualname__, + "mcp_agent.module": func.__module__, + } + ) as span: + # Set MCP Agent specific attributes + set_mcp_agent_span_attributes( + span, + operation=func.__qualname__, + **attributes or {} + ) + + # Execute the original function + try: + result = original_wrapper(*args, **kwargs) + return result + except Exception as e: + span.record_exception(e) + span.set_status(span.Status(span.StatusCode.ERROR)) + raise + + return agentops_wrapper + + return decorator + + +# Global telemetry hook instance +_telemetry_hook = MCPAgentTelemetryHook() + + +def hook_mcp_agent_telemetry(): + """Hook into MCP Agent's telemetry system.""" + _telemetry_hook.hook_into_telemetry() + + +def unhook_mcp_agent_telemetry(): + """Unhook from MCP Agent's telemetry system.""" + _telemetry_hook.unhook_from_telemetry() + + +def enhance_mcp_agent_span(span: Span, **attributes): + """Enhance an existing MCP Agent span with AgentOps attributes.""" + if span is None: + return + + # Add AgentOps span kind + span.set_attribute(SpanAttributes.AGENTOPS_SPAN_KIND, AgentOpsSpanKindValues.AGENTIC) + + # Set MCP Agent specific attributes + set_mcp_agent_span_attributes(span, **attributes) + + +def create_agentops_span_for_mcp_agent( + name: str, + operation: Optional[str] = None, + session_id: Optional[str] = None, + context_id: Optional[str] = None, + workflow_id: Optional[str] = None, + agent_id: Optional[str] = None, + agent_name: Optional[str] = None, + agent_description: Optional[str] = None, + **kwargs +): + """Create an AgentOps span for MCP Agent operations.""" + return create_span( + name, + kind=SpanKind.INTERNAL, + attributes={ + SpanAttributes.AGENTOPS_SPAN_KIND: AgentOpsSpanKindValues.AGENTIC, + "mcp_agent.function": operation or name, + } + ) + + +@contextmanager +def mcp_agent_span( + name: str, + operation: Optional[str] = None, + session_id: Optional[str] = None, + context_id: Optional[str] = None, + workflow_id: Optional[str] = None, + agent_id: Optional[str] = None, + agent_name: Optional[str] = None, + agent_description: Optional[str] = None, + **kwargs +): + """Context manager for creating MCP Agent spans with AgentOps instrumentation.""" + with create_agentops_span_for_mcp_agent( + name, + operation=operation, + session_id=session_id, + context_id=context_id, + workflow_id=workflow_id, + agent_id=agent_id, + agent_name=agent_name, + agent_description=agent_description, + **kwargs + ) as span: + # Set MCP Agent specific attributes + set_mcp_agent_span_attributes( + span, + operation=operation, + session_id=session_id, + context_id=context_id, + workflow_id=workflow_id, + agent_id=agent_id, + agent_name=agent_name, + agent_description=agent_description, + **kwargs + ) + + try: + yield span + except Exception as e: + span.record_exception(e) + span.set_status(span.Status(span.StatusCode.ERROR)) + raise + + +def mcp_agent_traced( + name: Optional[str] = None, + operation: Optional[str] = None, + session_id: Optional[str] = None, + context_id: Optional[str] = None, + workflow_id: Optional[str] = None, + agent_id: Optional[str] = None, + agent_name: Optional[str] = None, + agent_description: Optional[str] = None, + **kwargs +): + """Decorator for MCP Agent functions with AgentOps instrumentation.""" + def decorator(func): + span_name = name or f"mcp_agent.{func.__qualname__}" + + @functools.wraps(func) + def wrapper(*args, **kwargs_inner): + with mcp_agent_span( + span_name, + operation=operation or func.__qualname__, + session_id=session_id, + context_id=context_id, + workflow_id=workflow_id, + agent_id=agent_id, + agent_name=agent_name, + agent_description=agent_description, + **kwargs + ): + return func(*args, **kwargs_inner) + + return wrapper + + return decorator + + +# Auto-hook when module is imported +def _auto_hook(): + """Automatically hook into MCP Agent telemetry when this module is imported.""" + try: + import mcp_agent + hook_mcp_agent_telemetry() + except ImportError: + logger.debug("MCP Agent not available for auto-hooking") + + +# Run auto-hook +_auto_hook() \ No newline at end of file diff --git a/agentops/instrumentation/agentic/mcp_agent/test_integration.py b/agentops/instrumentation/agentic/mcp_agent/test_integration.py new file mode 100644 index 000000000..90db258b6 --- /dev/null +++ b/agentops/instrumentation/agentic/mcp_agent/test_integration.py @@ -0,0 +1,252 @@ +""" +Tests for MCP Agent integration with AgentOps. + +These tests verify that the integration works correctly and captures +the expected spans and attributes. +""" + +import unittest +import logging +from unittest.mock import Mock, patch, MagicMock +from contextlib import contextmanager + +# Configure logging for tests +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + + +class TestMCPAgentIntegration(unittest.TestCase): + """Test cases for MCP Agent integration.""" + + def setUp(self): + """Set up test environment.""" + # Mock AgentOps client + self.mock_client = Mock() + + # Mock OpenTelemetry span + self.mock_span = Mock() + self.mock_span.set_attribute = Mock() + self.mock_span.record_exception = Mock() + self.mock_span.set_status = Mock() + + # Mock span context manager + self.mock_span_context = Mock() + self.mock_span_context.__enter__ = Mock(return_value=self.mock_span) + self.mock_span_context.__exit__ = Mock(return_value=None) + + def test_mcp_agent_span_attributes(self): + """Test MCP Agent span attributes are set correctly.""" + from agentops.instrumentation.agentic.mcp_agent.mcp_agent_span_attributes import ( + set_mcp_agent_span_attributes, + set_mcp_agent_tool_attributes + ) + + # Test basic span attributes + set_mcp_agent_span_attributes( + self.mock_span, + operation="test_operation", + session_id="test_session", + agent_name="test_agent" + ) + + # Verify attributes were set + expected_calls = [ + (("mcp_agent.operation", "test_operation"),), + (("mcp_agent.session_id", "test_session"),), + (("mcp_agent.agent_name", "test_agent"),), + ] + + for call_args in expected_calls: + self.mock_span.set_attribute.assert_any_call(*call_args) + + # Test tool attributes + set_mcp_agent_tool_attributes( + self.mock_span, + tool_name="test_tool", + tool_arguments={"arg1": "value1"}, + tool_error=False + ) + + # Verify tool attributes were set + tool_calls = [ + (("mcp_agent.tool_name", "test_tool"),), + (("mcp_agent.tool_arguments", "{'arg1': 'value1'}"),), + (("mcp_agent.tool_error", "False"),), + ] + + for call_args in tool_calls: + self.mock_span.set_attribute.assert_any_call(*call_args) + + @patch('agentops.instrumentation.agentic.mcp_agent.telemetry_hook.create_span') + def test_mcp_agent_span_context_manager(self, mock_create_span): + """Test MCP Agent span context manager.""" + from agentops.instrumentation.agentic.mcp_agent.telemetry_hook import mcp_agent_span + + mock_create_span.return_value = self.mock_span_context + + with mcp_agent_span( + "test_span", + operation="test_operation", + session_id="test_session" + ): + pass + + # Verify span was created with correct parameters + mock_create_span.assert_called_once() + call_args = mock_create_span.call_args + self.assertEqual(call_args[0][0], "test_span") + + # Verify attributes were set + self.mock_span.set_attribute.assert_called() + + @patch('agentops.instrumentation.agentic.mcp_agent.telemetry_hook.create_span') + def test_mcp_agent_traced_decorator(self, mock_create_span): + """Test MCP Agent traced decorator.""" + from agentops.instrumentation.agentic.mcp_agent.telemetry_hook import mcp_agent_traced + + mock_create_span.return_value = self.mock_span_context + + @mcp_agent_traced( + name="test_decorated", + operation="test_operation", + agent_name="test_agent" + ) + def test_function(): + return "test_result" + + result = test_function() + + # Verify function executed + self.assertEqual(result, "test_result") + + # Verify span was created + mock_create_span.assert_called_once() + + @patch('agentops.instrumentation.agentic.mcp_agent.telemetry_hook.create_span') + def test_mcp_agent_span_error_handling(self, mock_create_span): + """Test error handling in MCP Agent spans.""" + from agentops.instrumentation.agentic.mcp_agent.telemetry_hook import mcp_agent_span + + mock_create_span.return_value = self.mock_span_context + + test_exception = ValueError("Test error") + + with self.assertRaises(ValueError): + with mcp_agent_span("test_error_span"): + raise test_exception + + # Verify exception was recorded + self.mock_span.record_exception.assert_called_once_with(test_exception) + self.mock_span.set_status.assert_called_once() + + def test_enhance_mcp_agent_span(self): + """Test enhancing existing spans with MCP Agent attributes.""" + from agentops.instrumentation.agentic.mcp_agent.telemetry_hook import enhance_mcp_agent_span + + enhance_mcp_agent_span( + self.mock_span, + operation="enhanced_operation", + session_id="enhanced_session" + ) + + # Verify AgentOps span kind was set + self.mock_span.set_attribute.assert_any_call( + "agentops.span.kind", "agentic" + ) + + # Verify MCP Agent attributes were set + self.mock_span.set_attribute.assert_any_call( + "mcp_agent.operation", "enhanced_operation" + ) + self.mock_span.set_attribute.assert_any_call( + "mcp_agent.session_id", "enhanced_session" + ) + + @patch('agentops.instrumentation.agentic.mcp_agent.telemetry_hook.MCPAgentTelemetryHook') + def test_telemetry_hook_initialization(self, mock_hook_class): + """Test telemetry hook initialization.""" + from agentops.instrumentation.agentic.mcp_agent.telemetry_hook import ( + hook_mcp_agent_telemetry, + unhook_mcp_agent_telemetry + ) + + mock_hook = Mock() + mock_hook_class.return_value = mock_hook + + # Test hooking + hook_mcp_agent_telemetry() + mock_hook.hook_into_telemetry.assert_called_once() + + # Test unhooking + unhook_mcp_agent_telemetry() + mock_hook.unhook_from_telemetry.assert_called_once() + + def test_instrumentor_initialization(self): + """Test MCP Agent instrumentor initialization.""" + from agentops.instrumentation.agentic.mcp_agent.instrumentation import ( + MCPAgentInstrumentor, + instrument_mcp_agent, + uninstrument_mcp_agent + ) + + # Test instrumentor creation + instrumentor = MCPAgentInstrumentor() + self.assertIsNotNone(instrumentor) + self.assertEqual(instrumentor.config.library_name, "mcp-agent") + + # Test instrumentation functions exist + self.assertTrue(callable(instrument_mcp_agent)) + self.assertTrue(callable(uninstrument_mcp_agent)) + + +class TestMCPAgentIntegrationWithMocks(unittest.TestCase): + """Test cases that mock MCP Agent dependencies.""" + + def setUp(self): + """Set up test environment with mocked dependencies.""" + self.mock_telemetry = Mock() + self.mock_telemetry_manager = Mock() + + # Mock the telemetry module + self.telemetry_patcher = patch.dict('sys.modules', { + 'mcp_agent.tracing.telemetry': Mock(), + 'mcp_agent.core.context': Mock(), + 'mcp': Mock(), + }) + self.telemetry_patcher.start() + + def tearDown(self): + """Clean up test environment.""" + self.telemetry_patcher.stop() + + @patch('agentops.instrumentation.agentic.mcp_agent.telemetry_hook.MCPAgentTelemetryHook') + def test_auto_hook_with_mcp_agent_available(self, mock_hook_class): + """Test auto-hooking when MCP Agent is available.""" + from agentops.instrumentation.agentic.mcp_agent.telemetry_hook import _auto_hook + + mock_hook = Mock() + mock_hook_class.return_value = mock_hook + + # Simulate MCP Agent being available + with patch('builtins.__import__', return_value=Mock()): + _auto_hook() + mock_hook.hook_into_telemetry.assert_called_once() + + @patch('agentops.instrumentation.agentic.mcp_agent.telemetry_hook.MCPAgentTelemetryHook') + def test_auto_hook_without_mcp_agent(self, mock_hook_class): + """Test auto-hooking when MCP Agent is not available.""" + from agentops.instrumentation.agentic.mcp_agent.telemetry_hook import _auto_hook + + mock_hook = Mock() + mock_hook_class.return_value = mock_hook + + # Simulate MCP Agent not being available + with patch('builtins.__import__', side_effect=ImportError("No module named 'mcp_agent'")): + _auto_hook() + # Should not call hook_into_telemetry when MCP Agent is not available + mock_hook.hook_into_telemetry.assert_not_called() + + +if __name__ == "__main__": + # Run tests + unittest.main(verbosity=2) \ No newline at end of file diff --git a/agentops/instrumentation/agentic/mcp_agent/version.py b/agentops/instrumentation/agentic/mcp_agent/version.py new file mode 100644 index 000000000..a68927d6c --- /dev/null +++ b/agentops/instrumentation/agentic/mcp_agent/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" \ No newline at end of file