Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ OLLAMA_API_KEY=ollama
# OpenAI Configuration (default if API_HOST not set)
OPENAI_MODEL=gpt-4o-mini
OPENAI_API_KEY=your_openai_api_key_here

# OpenTelemetry Configuration (for Aspire Dashboard)
# Uncomment to enable tracing, metrics, and logs export
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ A demonstration project showcasing Model Context Protocol (MCP) implementations
- [Use with GitHub Copilot](#use-with-github-copilot)
- [Debug with VS Code](#debug-with-vs-code)
- [Inspect with MCP inspector](#inspect-with-mcp-inspector)
- [View traces with Aspire Dashboard](#view-traces-with-aspire-dashboard)
- [Run local Agents <-> MCP](#run-local-agents---mcp)
- [Deploy to Azure](#deploy-to-azure)
- [Deploy to Azure with private networking](#deploy-to-azure-with-private-networking)
Expand Down Expand Up @@ -156,6 +157,42 @@ The inspector provides a web interface to:
- Inspect server responses and errors
- Debug server communication

### View traces with Aspire Dashboard

You can use the [.NET Aspire Dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone) to view OpenTelemetry traces, metrics, and logs from the MCP server.

> **Note:** Aspire Dashboard integration is only configured for the HTTP server (`basic_mcp_http.py`).

1. Start the Aspire Dashboard:

```bash
docker run --rm -d -p 18888:18888 -p 4317:18889 --name aspire-dashboard \
mcr.microsoft.com/dotnet/aspire-dashboard:latest
```

> The Aspire Dashboard exposes its OTLP endpoint on container port 18889. The mapping `-p 4317:18889` makes it available on the host's standard OTLP port 4317.

Get the dashboard URL and login token from the container logs:

```bash
docker logs aspire-dashboard 2>&1 | grep "Login to the dashboard"
```

2. Enable OpenTelemetry by adding this to your `.env` file:

```bash
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
```

3. Start the HTTP server:

```bash
uv run servers/basic_mcp_http.py
```


3. View the dashboard at: http://localhost:18888

---

## Run local Agents <-> MCP
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies = [
"azure-cosmos>=4.9.0",
"azure-monitor-opentelemetry>=1.6.4",
"opentelemetry-instrumentation-starlette>=0.49b0",
"opentelemetry-exporter-otlp-proto-grpc>=1.28.0",
"logfire>=4.15.1",
"azure-core-tracing-opentelemetry>=1.0.0b12"
]
Expand Down
16 changes: 15 additions & 1 deletion servers/basic_mcp_http.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import csv
import logging
import os
from datetime import date
from enum import Enum
from pathlib import Path
from typing import Annotated

from dotenv import load_dotenv
from fastmcp import FastMCP
from opentelemetry_middleware import OpenTelemetryMiddleware, configure_aspire_dashboard

load_dotenv(override=True)

logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s")
logger = logging.getLogger("ExpensesMCP")


otel_endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
middleware: list = []

if otel_endpoint:
logger.info("Setting up Aspire Dashboard instrumentation (OTLP)")
configure_aspire_dashboard(service_name="expenses-mcp")
middleware = [OpenTelemetryMiddleware(tracer_name="expenses.mcp")]


SCRIPT_DIR = Path(__file__).parent
EXPENSES_FILE = SCRIPT_DIR / "expenses.csv"


mcp = FastMCP("Expenses Tracker")
mcp = FastMCP("Expenses Tracker", middleware=middleware)


class PaymentMethod(Enum):
Expand Down
50 changes: 49 additions & 1 deletion servers/opentelemetry_middleware.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,56 @@
import logging
import os

from fastmcp.server.middleware import Middleware, MiddlewareContext
from opentelemetry import trace
from opentelemetry import metrics, trace
from opentelemetry._logs import set_logger_provider
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.trace import Status, StatusCode


def configure_aspire_dashboard(service_name: str = "expenses-mcp"):
"""Configure OpenTelemetry to send telemetry to the Aspire standalone dashboard."""
otlp_endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317")

# Create resource with service name
resource = Resource.create({"service.name": service_name})

# Configure Tracing
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=otlp_endpoint)))
trace.set_tracer_provider(tracer_provider)

# Configure Metrics
metric_reader = PeriodicExportingMetricReader(OTLPMetricExporter(endpoint=otlp_endpoint))
meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
metrics.set_meter_provider(meter_provider)

# Configure Logging
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter(endpoint=otlp_endpoint)))
set_logger_provider(logger_provider)

# Add logging handler to send Python logs to OTLP
root_logger = logging.getLogger()
handler_exists = any(
isinstance(existing, LoggingHandler) and getattr(existing, "logger_provider", None) is logger_provider
for existing in root_logger.handlers
)

if not handler_exists:
handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider)
root_logger.addHandler(handler)


class OpenTelemetryMiddleware(Middleware):
"""Middleware that creates OpenTelemetry spans for MCP operations."""

Expand Down
Loading