Skip to content

Commit 111e180

Browse files
authored
Merge pull request #30 from nimishamehta5/rate-limit-session-id
Implement rate-limiting based on session ID
2 parents 8f973cd + 990eac7 commit 111e180

22 files changed

+757
-92
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ MKP is a Model Context Protocol (MCP) server for Kubernetes that allows LLM-powe
1515
- Apply (create or update) clustered resources
1616
- Apply (create or update) namespaced resources
1717
- Generic and pluggable implementation using API Machinery's unstructured client
18+
- Built-in rate limiting for protection against excessive API calls
1819

1920
## Why MKP?
2021

@@ -47,6 +48,7 @@ MKP offers several key advantages as a Model Context Protocol server for Kuberne
4748
### Production-Ready Architecture
4849
- Designed for reliability and performance in production environments
4950
- Proper error handling and resource management
51+
- Built-in rate limiting to protect against excessive API calls
5052
- Testable design with comprehensive unit tests
5153
- Follows Kubernetes development best practices
5254

@@ -291,6 +293,24 @@ By default, MKP operates in read-only mode, meaning it does not allow write oper
291293
./build/mkp-server --kubeconfig=/path/to/kubeconfig --read-write=true
292294
```
293295

296+
### Rate Limiting
297+
298+
MKP includes a built-in rate limiting mechanism to protect the server from excessive API calls, which is particularly important when used with AI agents. The rate limiter uses a token bucket algorithm and applies different limits based on the operation type:
299+
300+
- Read operations (list_resources, get_resource): 120 requests per minute
301+
- Write operations (apply_resource, delete_resource): 30 requests per minute
302+
- Default for other operations: 60 requests per minute
303+
304+
Rate limits are applied per client session, ensuring fair resource allocation across multiple clients. The rate limiting feature can be enabled or disabled via the command line flag:
305+
306+
```bash
307+
# Run with rate limiting enabled (default)
308+
./build/mkp-server
309+
310+
# Run with rate limiting disabled
311+
./build/mkp-server --enable-rate-limiting=false
312+
```
313+
294314
## Development
295315

296316
### Running tests

cmd/server/main.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ func main() {
2424
"Whether to allow write operations on the cluster. When false, the server operates in read-only mode")
2525
kubeconfigRefreshInterval := flag.Duration("kubeconfig-refresh-interval", 0,
2626
"Interval to periodically re-read the kubeconfig (e.g., 5m for 5 minutes). If 0, no refresh will be performed")
27+
enableRateLimiting := flag.Bool("enable-rate-limiting", true,
28+
"Whether to enable rate limiting for tool calls. When false, no rate limiting will be applied")
29+
2730
flag.Parse()
2831

2932
// Create a context that can be cancelled
@@ -61,8 +64,9 @@ func main() {
6164

6265
// Create MCP server config
6366
config := &mcp.Config{
64-
ServeResources: *serveResources,
65-
ReadWrite: *readWrite,
67+
ServeResources: *serveResources,
68+
ReadWrite: *readWrite,
69+
EnableRateLimiting: *enableRateLimiting,
6670
}
6771

6872
// Create MCP server using the helper function
@@ -99,10 +103,17 @@ func main() {
99103
shutdownCh := make(chan error, 1)
100104
go func() {
101105
log.Println("Initiating server shutdown...")
106+
107+
// Stop the SSE server
102108
err := sseServer.Shutdown(shutdownCtx)
103109
if err != nil {
104110
log.Printf("Error during shutdown: %v", err)
105111
}
112+
113+
// Stop the MCP server resources (including rate limiter)
114+
log.Println("Stopping MCP server resources...")
115+
mcp.StopServer()
116+
106117
shutdownCh <- err
107118
close(shutdownCh)
108119
}()

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ require (
3232
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
3333
github.com/spf13/cast v1.7.1 // indirect
3434
github.com/spf13/pflag v1.0.5 // indirect
35+
github.com/stretchr/objx v0.5.2 // indirect
3536
github.com/x448/float16 v0.8.4 // indirect
3637
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
3738
golang.org/x/net v0.38.0 // indirect

pkg/mcp/apply_resource.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"github.com/mark3labs/mcp-go/mcp"
1010
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1111
"k8s.io/apimachinery/pkg/runtime/schema"
12+
13+
"github.com/StacklokLabs/mkp/pkg/types"
1214
)
1315

1416
// HandleApplyResource handles the apply_resource tool
@@ -31,7 +33,7 @@ func (m *Implementation) HandleApplyResource(ctx context.Context, request mcp.Ca
3133
if resource == "" {
3234
return mcp.NewToolResultError("resource is required"), nil
3335
}
34-
if resourceType == ResourceTypeNamespaced && namespace == "" {
36+
if resourceType == types.ResourceTypeNamespaced && namespace == "" {
3537
return mcp.NewToolResultError("namespace is required for namespaced resources"), nil
3638
}
3739
if manifestMap == nil {
@@ -52,9 +54,9 @@ func (m *Implementation) HandleApplyResource(ctx context.Context, request mcp.Ca
5254
var result *unstructured.Unstructured
5355
var err error
5456
switch resourceType {
55-
case ResourceTypeClustered:
57+
case types.ResourceTypeClustered:
5658
result, err = m.k8sClient.ApplyClusteredResource(ctx, gvr, obj)
57-
case ResourceTypeNamespaced:
59+
case types.ResourceTypeNamespaced:
5860
result, err = m.k8sClient.ApplyNamespacedResource(ctx, gvr, namespace, obj)
5961
default:
6062
return mcp.NewToolResultError(fmt.Sprintf("Invalid resource_type: %s", resourceType)), nil

pkg/mcp/apply_resource_test.go

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
ktesting "k8s.io/client-go/testing"
1414

1515
"github.com/StacklokLabs/mkp/pkg/k8s"
16+
"github.com/StacklokLabs/mkp/pkg/types"
1617
)
1718

1819
func TestHandleApplyResourceClusteredSuccess(t *testing.T) {
@@ -59,9 +60,9 @@ func TestHandleApplyResourceClusteredSuccess(t *testing.T) {
5960

6061
// Create a test request
6162
request := mcp.CallToolRequest{}
62-
request.Params.Name = ApplyResourceToolName
63+
request.Params.Name = types.ApplyResourceToolName
6364
request.Params.Arguments = map[string]interface{}{
64-
"resource_type": "clustered",
65+
"resource_type": types.ResourceTypeClustered,
6566
"group": "rbac.authorization.k8s.io",
6667
"version": "v1",
6768
"resource": "clusterroles",
@@ -146,9 +147,9 @@ func TestHandleApplyResourceNamespacedSuccess(t *testing.T) {
146147

147148
// Create a test request
148149
request := mcp.CallToolRequest{}
149-
request.Params.Name = ApplyResourceToolName
150+
request.Params.Name = types.ApplyResourceToolName
150151
request.Params.Arguments = map[string]interface{}{
151-
"resource_type": ResourceTypeNamespaced,
152+
"resource_type": types.ResourceTypeNamespaced,
152153
"group": "",
153154
"version": "v1",
154155
"resource": "services",
@@ -216,7 +217,7 @@ func TestHandleApplyResourceMissingParameters(t *testing.T) {
216217
{
217218
name: "Missing version",
218219
arguments: map[string]interface{}{
219-
"resource_type": "clustered",
220+
"resource_type": types.ResourceTypeClustered,
220221
"group": "apps",
221222
"resource": "deployments",
222223
"manifest": map[string]interface{}{},
@@ -226,7 +227,7 @@ func TestHandleApplyResourceMissingParameters(t *testing.T) {
226227
{
227228
name: "Missing resource",
228229
arguments: map[string]interface{}{
229-
"resource_type": "clustered",
230+
"resource_type": types.ResourceTypeClustered,
230231
"group": "apps",
231232
"version": "v1",
232233
"manifest": map[string]interface{}{},
@@ -236,7 +237,7 @@ func TestHandleApplyResourceMissingParameters(t *testing.T) {
236237
{
237238
name: "Missing namespace for namespaced resource",
238239
arguments: map[string]interface{}{
239-
"resource_type": ResourceTypeNamespaced,
240+
"resource_type": types.ResourceTypeNamespaced,
240241
"group": "apps",
241242
"version": "v1",
242243
"resource": "deployments",
@@ -247,7 +248,7 @@ func TestHandleApplyResourceMissingParameters(t *testing.T) {
247248
{
248249
name: "Missing manifest",
249250
arguments: map[string]interface{}{
250-
"resource_type": "clustered",
251+
"resource_type": types.ResourceTypeClustered,
251252
"group": "apps",
252253
"version": "v1",
253254
"resource": "deployments",
@@ -260,7 +261,7 @@ func TestHandleApplyResourceMissingParameters(t *testing.T) {
260261
t.Run(tc.name, func(t *testing.T) {
261262
// Create a test request
262263
request := mcp.CallToolRequest{}
263-
request.Params.Name = ApplyResourceToolName
264+
request.Params.Name = types.ApplyResourceToolName
264265
request.Params.Arguments = tc.arguments
265266

266267
// Test HandleApplyResource
@@ -293,7 +294,7 @@ func TestHandleApplyResourceInvalidResourceType(t *testing.T) {
293294

294295
// Create a test request with invalid resource_type
295296
request := mcp.CallToolRequest{}
296-
request.Params.Name = ApplyResourceToolName
297+
request.Params.Name = types.ApplyResourceToolName
297298
request.Params.Arguments = map[string]interface{}{
298299
"resource_type": "invalid",
299300
"group": "apps",
@@ -347,9 +348,9 @@ func TestHandleApplyResourceApplyError(t *testing.T) {
347348

348349
// Create a test request
349350
request := mcp.CallToolRequest{}
350-
request.Params.Name = ApplyResourceToolName
351+
request.Params.Name = types.ApplyResourceToolName
351352
request.Params.Arguments = map[string]interface{}{
352-
"resource_type": "clustered",
353+
"resource_type": types.ResourceTypeClustered,
353354
"group": "rbac.authorization.k8s.io",
354355
"version": "v1",
355356
"resource": "clusterroles",

pkg/mcp/delete_resource.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77

88
"github.com/mark3labs/mcp-go/mcp"
99
"k8s.io/apimachinery/pkg/runtime/schema"
10+
11+
"github.com/StacklokLabs/mkp/pkg/types"
1012
)
1113

1214
// HandleDeleteResource handles the delete_resource tool
@@ -32,13 +34,13 @@ func (m *Implementation) HandleDeleteResource(ctx context.Context, request mcp.C
3234
if name == "" {
3335
return mcp.NewToolResultError("name is required"), nil
3436
}
35-
if resourceType == ResourceTypeNamespaced && namespace == "" {
37+
if resourceType == types.ResourceTypeNamespaced && namespace == "" {
3638
return mcp.NewToolResultError("namespace is required for namespaced resources"), nil
3739
}
3840

3941
// Create GVR
4042
// Validate resource_type
41-
if resourceType != ResourceTypeClustered && resourceType != ResourceTypeNamespaced {
43+
if resourceType != types.ResourceTypeClustered && resourceType != types.ResourceTypeNamespaced {
4244
return mcp.NewToolResultError("Invalid resource_type: " + resourceType), nil
4345
}
4446

@@ -51,9 +53,9 @@ func (m *Implementation) HandleDeleteResource(ctx context.Context, request mcp.C
5153
// Delete resource
5254
var err error
5355
switch resourceType {
54-
case ResourceTypeClustered:
56+
case types.ResourceTypeClustered:
5557
err = m.k8sClient.DeleteClusteredResource(ctx, gvr, name)
56-
case ResourceTypeNamespaced:
58+
case types.ResourceTypeNamespaced:
5759
err = m.k8sClient.DeleteNamespacedResource(ctx, gvr, namespace, name)
5860
default:
5961
return mcp.NewToolResultError(fmt.Sprintf("Invalid resource_type: %s", resourceType)), nil
@@ -68,7 +70,7 @@ func (m *Implementation) HandleDeleteResource(ctx context.Context, request mcp.C
6870

6971
// NewDeleteResourceTool creates a new delete_resource tool
7072
func NewDeleteResourceTool() mcp.Tool {
71-
return mcp.NewTool("delete_resource",
73+
return mcp.NewTool(types.DeleteResourceToolName,
7274
mcp.WithDescription("Delete a Kubernetes resource"),
7375
mcp.WithString("resource_type",
7476
mcp.Description("Type of resource to delete (clustered or namespaced)"),

pkg/mcp/delete_resource_test.go

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
ktesting "k8s.io/client-go/testing"
1313

1414
"github.com/StacklokLabs/mkp/pkg/k8s"
15+
"github.com/StacklokLabs/mkp/pkg/types"
1516
)
1617

1718
func TestHandleDeleteResourceClusteredSuccess(t *testing.T) {
@@ -35,9 +36,9 @@ func TestHandleDeleteResourceClusteredSuccess(t *testing.T) {
3536

3637
// Create a test request
3738
request := mcp.CallToolRequest{}
38-
request.Params.Name = DeleteResourceToolName
39+
request.Params.Name = types.DeleteResourceToolName
3940
request.Params.Arguments = map[string]interface{}{
40-
"resource_type": "clustered",
41+
"resource_type": types.ResourceTypeClustered,
4142
"group": "rbac.authorization.k8s.io",
4243
"version": "v1",
4344
"resource": "clusterroles",
@@ -84,9 +85,9 @@ func TestHandleDeleteResourceNamespacedSuccess(t *testing.T) {
8485

8586
// Create a test request
8687
request := mcp.CallToolRequest{}
87-
request.Params.Name = DeleteResourceToolName
88+
request.Params.Name = types.DeleteResourceToolName
8889
request.Params.Arguments = map[string]interface{}{
89-
"resource_type": ResourceTypeNamespaced,
90+
"resource_type": types.ResourceTypeNamespaced,
9091
"group": "",
9192
"version": "v1",
9293
"resource": "services",
@@ -139,7 +140,7 @@ func TestHandleDeleteResourceMissingParameters(t *testing.T) {
139140
{
140141
name: "Missing version",
141142
arguments: map[string]interface{}{
142-
"resource_type": "clustered",
143+
"resource_type": types.ResourceTypeClustered,
143144
"group": "apps",
144145
"resource": "deployments",
145146
"name": "test-deployment",
@@ -149,7 +150,7 @@ func TestHandleDeleteResourceMissingParameters(t *testing.T) {
149150
{
150151
name: "Missing resource",
151152
arguments: map[string]interface{}{
152-
"resource_type": "clustered",
153+
"resource_type": types.ResourceTypeClustered,
153154
"group": "apps",
154155
"version": "v1",
155156
"name": "test-deployment",
@@ -159,7 +160,7 @@ func TestHandleDeleteResourceMissingParameters(t *testing.T) {
159160
{
160161
name: "Missing name",
161162
arguments: map[string]interface{}{
162-
"resource_type": "clustered",
163+
"resource_type": types.ResourceTypeClustered,
163164
"group": "apps",
164165
"version": "v1",
165166
"resource": "deployments",
@@ -169,7 +170,7 @@ func TestHandleDeleteResourceMissingParameters(t *testing.T) {
169170
{
170171
name: "Missing namespace for namespaced resource",
171172
arguments: map[string]interface{}{
172-
"resource_type": ResourceTypeNamespaced,
173+
"resource_type": types.ResourceTypeNamespaced,
173174
"group": "apps",
174175
"version": "v1",
175176
"resource": "deployments",
@@ -183,7 +184,7 @@ func TestHandleDeleteResourceMissingParameters(t *testing.T) {
183184
t.Run(tc.name, func(t *testing.T) {
184185
// Create a test request
185186
request := mcp.CallToolRequest{}
186-
request.Params.Name = DeleteResourceToolName
187+
request.Params.Name = types.DeleteResourceToolName
187188
request.Params.Arguments = tc.arguments
188189

189190
// Test HandleDeleteResource
@@ -216,7 +217,7 @@ func TestHandleDeleteResourceInvalidResourceType(t *testing.T) {
216217

217218
// Create a test request with invalid resource_type
218219
request := mcp.CallToolRequest{}
219-
request.Params.Name = DeleteResourceToolName
220+
request.Params.Name = types.DeleteResourceToolName
220221
request.Params.Arguments = map[string]interface{}{
221222
"resource_type": "invalid",
222223
"group": "apps",
@@ -265,9 +266,9 @@ func TestHandleDeleteResourceDeleteError(t *testing.T) {
265266

266267
// Create a test request
267268
request := mcp.CallToolRequest{}
268-
request.Params.Name = DeleteResourceToolName
269+
request.Params.Name = types.DeleteResourceToolName
269270
request.Params.Arguments = map[string]interface{}{
270-
"resource_type": "clustered",
271+
"resource_type": types.ResourceTypeClustered,
271272
"group": "rbac.authorization.k8s.io",
272273
"version": "v1",
273274
"resource": "clusterroles",

pkg/mcp/get_resource.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77

88
"github.com/mark3labs/mcp-go/mcp"
99
"k8s.io/apimachinery/pkg/runtime/schema"
10+
11+
"github.com/StacklokLabs/mkp/pkg/types"
1012
)
1113

1214
// HandleGetResource handles the get_resource tool
@@ -50,13 +52,13 @@ func (m *Implementation) HandleGetResource(ctx context.Context, request mcp.Call
5052
if name == "" {
5153
return mcp.NewToolResultError("name is required"), nil
5254
}
53-
if resourceType == ResourceTypeNamespaced && namespace == "" {
55+
if resourceType == types.ResourceTypeNamespaced && namespace == "" {
5456
return mcp.NewToolResultError("namespace is required for namespaced resources"), nil
5557
}
5658

5759
// Create GVR
5860
// Validate resource_type
59-
if resourceType != "clustered" && resourceType != ResourceTypeNamespaced {
61+
if resourceType != types.ResourceTypeClustered && resourceType != types.ResourceTypeNamespaced {
6062
return mcp.NewToolResultError("Invalid resource_type: " + resourceType), nil
6163
}
6264

@@ -83,7 +85,7 @@ func (m *Implementation) HandleGetResource(ctx context.Context, request mcp.Call
8385

8486
// NewGetResourceTool creates a new get_resource tool
8587
func NewGetResourceTool() mcp.Tool {
86-
return mcp.NewTool("get_resource",
88+
return mcp.NewTool(types.GetResourceToolName,
8789
mcp.WithDescription("Get a Kubernetes resource or its subresource"),
8890
mcp.WithString("resource_type",
8991
mcp.Description("Type of resource to get (clustered or namespaced)"),

0 commit comments

Comments
 (0)