Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
23 changes: 23 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,28 @@ 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
```

2. Set the environment variable and 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
13 changes: 12 additions & 1 deletion servers/basic_mcp_http.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import csv
import logging
import os
from datetime import date
from enum import Enum
from pathlib import Path
from typing import Annotated

from fastmcp import FastMCP
from opentelemetry_middleware import OpenTelemetryMiddleware, configure_aspire_dashboard

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


# Configure Aspire Dashboard telemetry if OTEL_EXPORTER_OTLP_ENDPOINT is set
if os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"):
logger.info("Setting up Aspire Dashboard instrumentation (OTLP)")
configure_aspire_dashboard(service_name="expenses-mcp")


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


mcp = FastMCP("Expenses Tracker")
mcp = FastMCP(
"Expenses Tracker",
middleware=[OpenTelemetryMiddleware(tracer_name="expenses.mcp")]
)


class PaymentMethod(Enum):
Expand Down
60 changes: 59 additions & 1 deletion servers/opentelemetry_middleware.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,66 @@
import logging
import os
from urllib.parse import urlparse

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 SimpleSpanProcessor
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")
parsed_endpoint = urlparse(otlp_endpoint)
use_insecure = parsed_endpoint.scheme not in ("https", "grpcs")

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

# Configure Tracing
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(
SimpleSpanProcessor(OTLPSpanExporter(endpoint=otlp_endpoint, insecure=use_insecure))
)
trace.set_tracer_provider(tracer_provider)

# Configure Metrics
metric_reader = PeriodicExportingMetricReader(
OTLPMetricExporter(endpoint=otlp_endpoint, insecure=use_insecure)
)
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, insecure=use_insecure))
)
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