@@ -331,6 +331,263 @@ func (s *NodesSuite) TestNodesStatsSummaryDenied() {
331331 })
332332}
333333
334+ func (s * NodesSuite ) TestNodesTop () {
335+ s .mockServer .Handle (http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
336+ w .Header ().Set ("Content-Type" , "application/json" )
337+ // Request Performed by DiscoveryClient to Kube API (Get API Groups legacy -core-)
338+ if req .URL .Path == "/api" {
339+ _ , _ = w .Write ([]byte (`{"kind":"APIVersions","versions":["v1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0"}]}` ))
340+ return
341+ }
342+ // Request Performed by DiscoveryClient to Kube API (Get API Groups)
343+ if req .URL .Path == "/apis" {
344+ _ , _ = w .Write ([]byte (`{"kind":"APIGroupList","apiVersion":"v1","groups":[{"name":"metrics.k8s.io","versions":[{"groupVersion":"metrics.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"metrics.k8s.io/v1beta1","version":"v1beta1"}}]}` ))
345+ return
346+ }
347+ // Request Performed by DiscoveryClient to Kube API (Get API Resources)
348+ if req .URL .Path == "/apis/metrics.k8s.io/v1beta1" {
349+ _ , _ = w .Write ([]byte (`{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"metrics.k8s.io/v1beta1","resources":[{"name":"nodes","singularName":"","namespaced":false,"kind":"NodeMetrics","verbs":["get","list"]}]}` ))
350+ return
351+ }
352+ // List Nodes
353+ if req .URL .Path == "/api/v1/nodes" {
354+ _ , _ = w .Write ([]byte (`{
355+ "apiVersion": "v1",
356+ "kind": "NodeList",
357+ "items": [
358+ {
359+ "metadata": {
360+ "name": "node-1",
361+ "labels": {
362+ "node-role.kubernetes.io/worker": ""
363+ }
364+ },
365+ "status": {
366+ "allocatable": {
367+ "cpu": "4",
368+ "memory": "16Gi"
369+ },
370+ "nodeInfo": {
371+ "swap": {
372+ "capacity": 0
373+ }
374+ }
375+ }
376+ },
377+ {
378+ "metadata": {
379+ "name": "node-2",
380+ "labels": {
381+ "node-role.kubernetes.io/worker": ""
382+ }
383+ },
384+ "status": {
385+ "allocatable": {
386+ "cpu": "4",
387+ "memory": "16Gi"
388+ },
389+ "nodeInfo": {
390+ "swap": {
391+ "capacity": 0
392+ }
393+ }
394+ }
395+ }
396+ ]
397+ }` ))
398+ return
399+ }
400+ // Get NodeMetrics
401+ if req .URL .Path == "/apis/metrics.k8s.io/v1beta1/nodes" {
402+ _ , _ = w .Write ([]byte (`{
403+ "apiVersion": "metrics.k8s.io/v1beta1",
404+ "kind": "NodeMetricsList",
405+ "items": [
406+ {
407+ "metadata": {
408+ "name": "node-1"
409+ },
410+ "timestamp": "2025-10-29T09:00:00Z",
411+ "window": "30s",
412+ "usage": {
413+ "cpu": "500m",
414+ "memory": "2Gi"
415+ }
416+ },
417+ {
418+ "metadata": {
419+ "name": "node-2"
420+ },
421+ "timestamp": "2025-10-29T09:00:00Z",
422+ "window": "30s",
423+ "usage": {
424+ "cpu": "1000m",
425+ "memory": "4Gi"
426+ }
427+ }
428+ ]
429+ }` ))
430+ return
431+ }
432+ // Get specific NodeMetrics
433+ if req .URL .Path == "/apis/metrics.k8s.io/v1beta1/nodes/node-1" {
434+ _ , _ = w .Write ([]byte (`{
435+ "apiVersion": "metrics.k8s.io/v1beta1",
436+ "kind": "NodeMetrics",
437+ "metadata": {
438+ "name": "node-1"
439+ },
440+ "timestamp": "2025-10-29T09:00:00Z",
441+ "window": "30s",
442+ "usage": {
443+ "cpu": "500m",
444+ "memory": "2Gi"
445+ }
446+ }` ))
447+ return
448+ }
449+ w .WriteHeader (http .StatusNotFound )
450+ }))
451+ s .InitMcpClient ()
452+
453+ s .Run ("nodes_top() - all nodes" , func () {
454+ toolResult , err := s .CallTool ("nodes_top" , map [string ]interface {}{})
455+ s .Require ().NotNil (toolResult , "toolResult should not be nil" )
456+ s .Run ("no error" , func () {
457+ s .Falsef (toolResult .IsError , "call tool should succeed" )
458+ s .Nilf (err , "call tool should not return error object" )
459+ })
460+ s .Run ("returns metrics for all nodes" , func () {
461+ content := toolResult .Content [0 ].(mcp.TextContent ).Text
462+ s .Contains (content , "node-1" , "expected metrics to contain node-1" )
463+ s .Contains (content , "node-2" , "expected metrics to contain node-2" )
464+ s .Contains (content , "CPU(cores)" , "expected header with CPU column" )
465+ s .Contains (content , "MEMORY(bytes)" , "expected header with MEMORY column" )
466+ })
467+ })
468+
469+ s .Run ("nodes_top(name=node-1) - specific node" , func () {
470+ toolResult , err := s .CallTool ("nodes_top" , map [string ]interface {}{
471+ "name" : "node-1" ,
472+ })
473+ s .Require ().NotNil (toolResult , "toolResult should not be nil" )
474+ s .Run ("no error" , func () {
475+ s .Falsef (toolResult .IsError , "call tool should succeed" )
476+ s .Nilf (err , "call tool should not return error object" )
477+ })
478+ s .Run ("returns metrics for specific node" , func () {
479+ content := toolResult .Content [0 ].(mcp.TextContent ).Text
480+ s .Contains (content , "node-1" , "expected metrics to contain node-1" )
481+ s .Contains (content , "500m" , "expected CPU usage of 500m" )
482+ s .Contains (content , "2048Mi" , "expected memory usage of 2048Mi" )
483+ })
484+ })
485+
486+ s .Run ("nodes_top(label_selector=node-role.kubernetes.io/worker=)" , func () {
487+ toolResult , err := s .CallTool ("nodes_top" , map [string ]interface {}{
488+ "label_selector" : "node-role.kubernetes.io/worker=" ,
489+ })
490+ s .Require ().NotNil (toolResult , "toolResult should not be nil" )
491+ s .Run ("no error" , func () {
492+ s .Falsef (toolResult .IsError , "call tool should succeed" )
493+ s .Nilf (err , "call tool should not return error object" )
494+ })
495+ s .Run ("returns metrics for filtered nodes" , func () {
496+ content := toolResult .Content [0 ].(mcp.TextContent ).Text
497+ s .Contains (content , "node-1" , "expected metrics to contain node-1" )
498+ s .Contains (content , "node-2" , "expected metrics to contain node-2" )
499+ })
500+ })
501+ }
502+
503+ func (s * NodesSuite ) TestNodesTopMetricsUnavailable () {
504+ s .mockServer .Handle (http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
505+ // List Nodes
506+ if req .URL .Path == "/api/v1/nodes" {
507+ w .Header ().Set ("Content-Type" , "application/json" )
508+ w .WriteHeader (http .StatusOK )
509+ _ , _ = w .Write ([]byte (`{
510+ "apiVersion": "v1",
511+ "kind": "NodeList",
512+ "items": [
513+ {
514+ "metadata": {
515+ "name": "node-1"
516+ },
517+ "status": {
518+ "allocatable": {
519+ "cpu": "4",
520+ "memory": "16Gi"
521+ }
522+ }
523+ }
524+ ]
525+ }` ))
526+ return
527+ }
528+ // Metrics server not available
529+ if req .URL .Path == "/apis/metrics.k8s.io/v1beta1/nodes" {
530+ w .WriteHeader (http .StatusNotFound )
531+ return
532+ }
533+ w .WriteHeader (http .StatusNotFound )
534+ }))
535+ s .InitMcpClient ()
536+
537+ s .Run ("nodes_top() - metrics unavailable" , func () {
538+ toolResult , err := s .CallTool ("nodes_top" , map [string ]interface {}{})
539+ s .Require ().NotNil (toolResult , "toolResult should not be nil" )
540+ s .Run ("has error" , func () {
541+ s .Truef (toolResult .IsError , "call tool should fail when metrics unavailable" )
542+ s .Nilf (err , "call tool should not return error object" )
543+ })
544+ s .Run ("describes metrics unavailable" , func () {
545+ content := toolResult .Content [0 ].(mcp.TextContent ).Text
546+ s .Contains (content , "failed to get nodes top" , "expected error message about failing to get nodes top" )
547+ })
548+ })
549+ }
550+
551+ func (s * NodesSuite ) TestNodesTopDenied () {
552+ s .Require ().NoError (toml .Unmarshal ([]byte (`
553+ denied_resources = [ { group = "metrics.k8s.io", version = "v1beta1" } ]
554+ ` ), s .Cfg ), "Expected to parse denied resources config" )
555+ s .mockServer .Handle (http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
556+ w .Header ().Set ("Content-Type" , "application/json" )
557+ // Request Performed by DiscoveryClient to Kube API (Get API Groups legacy -core-)
558+ if req .URL .Path == "/api" {
559+ _ , _ = w .Write ([]byte (`{"kind":"APIVersions","versions":["v1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0"}]}` ))
560+ return
561+ }
562+ // Request Performed by DiscoveryClient to Kube API (Get API Groups)
563+ if req .URL .Path == "/apis" {
564+ _ , _ = w .Write ([]byte (`{"kind":"APIGroupList","apiVersion":"v1","groups":[{"name":"metrics.k8s.io","versions":[{"groupVersion":"metrics.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"metrics.k8s.io/v1beta1","version":"v1beta1"}}]}` ))
565+ return
566+ }
567+ // Request Performed by DiscoveryClient to Kube API (Get API Resources)
568+ if req .URL .Path == "/apis/metrics.k8s.io/v1beta1" {
569+ _ , _ = w .Write ([]byte (`{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"metrics.k8s.io/v1beta1","resources":[{"name":"nodes","singularName":"","namespaced":false,"kind":"NodeMetrics","verbs":["get","list"]}]}` ))
570+ return
571+ }
572+ w .WriteHeader (http .StatusNotFound )
573+ }))
574+ s .InitMcpClient ()
575+
576+ s .Run ("nodes_top (denied)" , func () {
577+ toolResult , err := s .CallTool ("nodes_top" , map [string ]interface {}{})
578+ s .Require ().NotNil (toolResult , "toolResult should not be nil" )
579+ s .Run ("has error" , func () {
580+ s .Truef (toolResult .IsError , "call tool should fail" )
581+ s .Nilf (err , "call tool should not return error object" )
582+ })
583+ s .Run ("describes denial" , func () {
584+ expectedMessage := "failed to get nodes top: resource not allowed: metrics.k8s.io/v1beta1, Kind=NodeMetrics"
585+ s .Equalf (expectedMessage , toolResult .Content [0 ].(mcp.TextContent ).Text ,
586+ "expected descriptive error '%s', got %v" , expectedMessage , toolResult .Content [0 ].(mcp.TextContent ).Text )
587+ })
588+ })
589+ }
590+
334591func TestNodes (t * testing.T ) {
335592 suite .Run (t , new (NodesSuite ))
336593}
0 commit comments