@@ -8,9 +8,11 @@ package mcpgo // import "github.com/DataDog/dd-trace-go/contrib/mark3labs/mcp-go
88import (
99 "context"
1010 "encoding/json"
11+ "time"
1112
1213 "github.com/DataDog/dd-trace-go/v2/instrumentation"
1314 "github.com/DataDog/dd-trace-go/v2/llmobs"
15+ "github.com/jellydator/ttlcache/v3"
1416
1517 "github.com/mark3labs/mcp-go/mcp"
1618 "github.com/mark3labs/mcp-go/server"
@@ -22,6 +24,27 @@ func init() {
2224 instr = instrumentation .Load (instrumentation .PackageMark3LabsMcpGo )
2325}
2426
27+ type hooks struct {
28+ spanCache * ttlcache.Cache [any , llmobs.Span ]
29+ }
30+
31+ type textIOAnnotator interface {
32+ AnnotateTextIO (input , output string , opts ... llmobs.AnnotateOption )
33+ }
34+
35+ // AddServerHooks appends Datadog tracing hooks to an existing server.Hooks object.
36+ // Returns a cleanup function that should be called upon server shutdown.
37+ func AddServerHooks (hooks * server.Hooks ) func () {
38+ ddHooks := newHooks ()
39+ hooks .AddBeforeInitialize (ddHooks .onBeforeInitialize )
40+ hooks .AddAfterInitialize (ddHooks .onAfterInitialize )
41+ hooks .AddOnError (ddHooks .onError )
42+
43+ return func () {
44+ ddHooks .stop ()
45+ }
46+ }
47+
2548func NewToolHandlerMiddleware () server.ToolHandlerMiddleware {
2649 return func (next server.ToolHandlerFunc ) server.ToolHandlerFunc {
2750 return func (ctx context.Context , request mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
@@ -48,3 +71,63 @@ func NewToolHandlerMiddleware() server.ToolHandlerMiddleware {
4871 }
4972 }
5073}
74+
75+ func newHooks () * hooks {
76+ spanCache := ttlcache.New [any , llmobs.Span ](
77+ ttlcache.WithTTL [any , llmobs.Span ](5 * time .Minute ),
78+ )
79+ spanCache .OnEviction (func (ctx context.Context , reason ttlcache.EvictionReason , item * ttlcache.Item [any , llmobs.Span ]) {
80+ if span := item .Value (); span != nil {
81+ if reason == ttlcache .EvictionReasonExpired {
82+ span .Finish ()
83+ }
84+ }
85+ })
86+ go spanCache .Start ()
87+
88+ return & hooks {
89+ spanCache : spanCache ,
90+ }
91+ }
92+
93+ func (h * hooks ) onBeforeInitialize (ctx context.Context , id any , request * mcp.InitializeRequest ) {
94+ taskSpan , _ := llmobs .StartTaskSpan (ctx , "mcp.initialize" )
95+ h .spanCache .Set (id , taskSpan , ttlcache .DefaultTTL )
96+ }
97+
98+ func (h * hooks ) onAfterInitialize (ctx context.Context , id any , request * mcp.InitializeRequest , result * mcp.InitializeResult ) {
99+ finishSpanWithIO (h , id , request , result )
100+ }
101+
102+ func (h * hooks ) onError (ctx context.Context , id any , method mcp.MCPMethod , message any , err error ) {
103+ if method == mcp .MethodInitialize {
104+ if item := h .spanCache .Get (id ); item != nil {
105+ span := item .Value ()
106+ if annotator , ok := span .(textIOAnnotator ); ok {
107+ inputJSON , _ := json .Marshal (message )
108+ annotator .AnnotateTextIO (string (inputJSON ), err .Error ())
109+ span .Finish (llmobs .WithError (err ))
110+ }
111+ h .spanCache .Delete (id )
112+ }
113+ }
114+ }
115+
116+ func (h * hooks ) stop () {
117+ h .spanCache .Stop ()
118+ }
119+
120+ func finishSpanWithIO [Req any , Res any ](h * hooks , id any , request Req , result Res ) {
121+ if item := h .spanCache .Get (id ); item != nil {
122+ span := item .Value ()
123+ if annotator , ok := span .(textIOAnnotator ); ok {
124+ inputJSON , _ := json .Marshal (request )
125+ resultJSON , _ := json .Marshal (result )
126+ outputText := string (resultJSON )
127+
128+ annotator .AnnotateTextIO (string (inputJSON ), outputText )
129+ span .Finish ()
130+ }
131+ h .spanCache .Delete (id )
132+ }
133+ }
0 commit comments