Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ require (
github.com/keys-pub/go-libfido2 v1.5.3-0.20220306005615-8ab03fb1ec27 // replaced
github.com/lib/pq v1.10.9
github.com/mailgun/mailgun-go/v4 v4.23.0
github.com/mark3labs/mcp-go v0.30.1
github.com/mattn/go-shellwords v1.0.12
github.com/mattn/go-sqlite3 v1.14.28
github.com/mdlayher/netlink v1.7.2
Expand Down Expand Up @@ -544,6 +545,7 @@ require (
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
github.com/zeebo/errs v1.4.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1842,6 +1842,8 @@ github.com/mailgun/mailgun-go/v4 v4.23.0 h1:jPEMJzzin2s7lvehcfv/0UkyBu18GvcURPr2
github.com/mailgun/mailgun-go/v4 v4.23.0/go.mod h1:imTtizoFtpfZqPqGP8vltVBB6q9yWcv6llBhfFeElZU=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-go v0.30.1 h1:3R1BPvNT/rC1iPpLx+EMXFy+gvux/Mz/Nio3c6XEU9E=
github.com/mark3labs/mcp-go v0.30.1/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
Expand Down Expand Up @@ -2288,6 +2290,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
Expand Down
33 changes: 33 additions & 0 deletions lib/utils/mcputils/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Teleport
* Copyright (C) 2025 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package mcputils

import (
"errors"
"io"

"github.com/gravitational/teleport/lib/utils"
)

// IsOKCloseError checks if provided error is a common close error that
// indicates the connection is ended.
func IsOKCloseError(err error) bool {
return errors.Is(err, io.ErrClosedPipe) ||
utils.IsOKNetworkError(err)
}
80 changes: 80 additions & 0 deletions lib/utils/mcputils/id_tracker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Teleport
* Copyright (C) 2025 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package mcputils

import (
"sync"

"github.com/gravitational/trace"
"github.com/hashicorp/golang-lru/v2/simplelru"
"github.com/mark3labs/mcp-go/mcp"
)

// IDTracker tracks message information like method based on ID. IDTracker
// internally uses an LRU cache to keep track the last X messages to avoid
// growing infinitely. IDTracker is safe for concurrent use.
type IDTracker struct {
mu sync.Mutex
lruCache *simplelru.LRU[mcp.RequestId, mcp.MCPMethod]
}

// NewIDTracker creates a new IDTracker with provided maximum size.
func NewIDTracker(size int) (*IDTracker, error) {
lruCache, err := simplelru.NewLRU[mcp.RequestId, mcp.MCPMethod](size, nil)
if err != nil {
return nil, trace.Wrap(err)
}
return &IDTracker{
lruCache: lruCache,
}, nil
}

// PushRequest tracks a request. Returns true if the request has been added to
// cache.
func (t *IDTracker) PushRequest(msg *JSONRPCRequest) bool {
if msg == nil || msg.ID.IsNil() || msg.Method == "" {
return false
}
t.mu.Lock()
defer t.mu.Unlock()
t.lruCache.Add(msg.ID, msg.Method)
return true
Comment on lines +56 to +57
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Add returns a value indicating whether the key was evicted or not.

Suggested change
t.lruCache.Add(msg.ID, msg.Method)
return true
return !t.lruCache.Add(msg.ID, msg.Method)

Copy link
Copy Markdown
Contributor Author

@greedy52 greedy52 May 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's if an old key is evicted because of the size, which has different meaning than current return. unless we return two different bool. i will leave it out for now.

}

// PopByID retrieves the tracked information and remove it from the tracker.
func (t *IDTracker) PopByID(id mcp.RequestId) (mcp.MCPMethod, bool) {
if id.IsNil() {
return "", false
}

t.mu.Lock()
defer t.mu.Unlock()

retrieved, ok := t.lruCache.Get(id)
if !ok {
return "", false
}
t.lruCache.Remove(id)
return retrieved, true
}

// Len returns the size of the tracker cache.
func (t *IDTracker) Len() int {
return t.lruCache.Len()
}
109 changes: 109 additions & 0 deletions lib/utils/mcputils/id_tracker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Teleport
* Copyright (C) 2025 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package mcputils

import (
"fmt"
"slices"
"testing"

"github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/require"
)

func TestIDTracker(t *testing.T) {
tracker, err := NewIDTracker(5)
require.NoError(t, err)
require.Empty(t, tracker.Len())

t.Run("request missing ID not tracked", func(t *testing.T) {
require.False(t, tracker.PushRequest(&JSONRPCRequest{
Method: "bad",
}))
require.Empty(t, tracker.Len())
})

t.Run("request tracked", func(t *testing.T) {
require.True(t, tracker.PushRequest(&JSONRPCRequest{
ID: mcp.NewRequestId(0),
Method: mcp.MethodToolsList,
}))
require.Equal(t, 1, tracker.Len())
})

t.Run("pop unknown id", func(t *testing.T) {
unknownIDs := []mcp.RequestId{
mcp.NewRequestId(5),
mcp.NewRequestId("0"),
mcp.NewRequestId(nil),
}
for id := range slices.Values(unknownIDs) {
t.Run(fmt.Sprintf("%T", id), func(t *testing.T) {
_, ok := tracker.PopByID(id)
require.False(t, ok)
require.Equal(t, 1, tracker.Len())
})
}
})

t.Run("pop tracked id", func(t *testing.T) {
method, ok := tracker.PopByID(mcp.NewRequestId(0))
require.True(t, ok)
require.Equal(t, mcp.MethodToolsList, method)
require.Empty(t, tracker.Len())
})

t.Run("track last 5", func(t *testing.T) {
for i := range 20 {
tracker.PushRequest(&JSONRPCRequest{
ID: mcp.NewRequestId(i + 1),
Method: mcp.MethodToolsCall,
})
require.LessOrEqual(t, tracker.Len(), 10)
}
for i := range 5 {
method, ok := tracker.PopByID(mcp.NewRequestId(20 - i))
require.True(t, ok)
require.Equal(t, mcp.MethodToolsCall, method)
}
require.Empty(t, tracker.Len())
})
}

func BenchmarkIDTracker(b *testing.B) {
idTracker, err := NewIDTracker(100)
require.NoError(b, err)

for i := 0; i < 100; i++ {
idTracker.PushRequest(&JSONRPCRequest{
ID: mcp.NewRequestId(i),
Method: mcp.MethodToolsList,
})
}

// cpu: Apple M3 Pro
// BenchmarkIDTracker-12 12267649 81.85 ns/op
for b.Loop() {
idTracker.PushRequest(&JSONRPCRequest{
ID: mcp.NewRequestId(2000),
Method: mcp.MethodToolsList,
})
idTracker.PopByID(mcp.NewRequestId(2000))
}
}
Loading
Loading