Skip to content

Proposal: Support for OTel Implementation in go-mcpΒ #7616

@flc1125

Description

@flc1125

Based on the official MCP SDK, which is expected to be released in August, I believe we can also implement OTel to support this capability.

Blocked:

  • Currently, there are no semantic conventions based on MCP standards.
  • The go-sdk has not yet released a stable version.

Implementation method:

Middleware injection is implemented via https://github.com/modelcontextprotocol/go-sdk/blob/8dd9a819bb283849de91c839f6f1734b86a9fc1b/mcp/server.go#L659-L663

A simple draft version:

package otel

import (
	"context"

	"github.com/modelcontextprotocol/go-sdk/mcp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/trace"
)

const scopeName = "otel"

var (
	mcpSessionIDKey  = attribute.Key("mcp.session.id")
	mcpMethodKey     = attribute.Key("mcp.method")
	mcpToolNameKey   = attribute.Key("mcp.tool.name")
	mcpPromptNameKey = attribute.Key("mcp.prompt.name")
)

type options struct {
	tp trace.TracerProvider
}

type Option func(*options)

func WithTracerProvider(tp trace.TracerProvider) Option {
	return func(opts *options) {
		opts.tp = tp
	}
}

func newOptions(opts ...Option) *options {
	o := &options{
		tp: otel.GetTracerProvider(),
	}
	for _, opt := range opts {
		opt(o)
	}
	return o
}

func Middleware[S mcp.Session](opts ...Option) mcp.Middleware[S] {
	o := newOptions(opts...)

	tracer := o.tp.Tracer(scopeName)

	// span kind is determined based on the type of session.
	var (
		spanKind    trace.SpanKind
		sessionZero = S(nil)
	)
	switch any(sessionZero).(type) {
	case *mcp.ClientSession:
		spanKind = trace.SpanKindClient
	case *mcp.ServerSession:
		spanKind = trace.SpanKindServer
	}

	return func(next mcp.MethodHandler[S]) mcp.MethodHandler[S] {
		return func(ctx context.Context, session S, method string, params mcp.Params) (result mcp.Result, err error) {
			var spanStartOptions []trace.SpanStartOption
			if spanKind != trace.SpanKindUnspecified {
				spanStartOptions = append(spanStartOptions, trace.WithSpanKind(spanKind))
			}

			ctx, span := tracer.Start(ctx, "mcp "+method, spanStartOptions...)
			defer span.End()
			defer func() {
				if err != nil {
					span.RecordError(err)
					span.SetStatus(codes.Error, err.Error())
				}
			}()

			attrs := []attribute.KeyValue{
				mcpSessionIDKey.String(session.ID()),
				mcpMethodKey.String(method),
			}

			switch p := params.(type) {
			case *mcp.CallToolParamsFor[json.RawMessage]:
				attrs = append(attrs, mcpToolNameKey.String(p.Name))
			case *mcp.GetPromptParams:
				attrs = append(attrs, mcpPromptNameKey.String(p.Name))
			}

			span.SetAttributes(attrs...)

			return next(ctx, session, method, params)
		}
	}
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions