-
Notifications
You must be signed in to change notification settings - Fork 693
Open
Description
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)
}
}
}
akats7
Metadata
Metadata
Assignees
Labels
No labels