diff --git a/Makefile b/Makefile index 3955ae4..351a6e0 100644 --- a/Makefile +++ b/Makefile @@ -85,6 +85,12 @@ docker-build: @echo "===========> Building docker image" docker buildx build --build-arg=VERSION="$$(git describe --tags --abbrev=0)" --build-arg=COMMIT="$$(git rev-parse --short HEAD)" --build-arg DATE="$$(date +%FT%TZ)" --platform="linux/amd64,linux/arm64" -t ${IMG} -f container/Dockerfile . --push +## docker-build-local: Build docker image for local testing +.PHONY: docker-build-local +docker-build-local: + @echo "===========> Building docker image for local testing" + docker build --build-arg=VERSION="$$(git describe --tags --abbrev=0)" --build-arg=COMMIT="$$(git rev-parse --short HEAD)" --build-arg DATE="$$(date +%FT%TZ)" -t k8sgpt:local -f container/Dockerfile . + ## fmt: Run go fmt against code. .PHONY: fmt fmt: diff --git a/go.mod b/go.mod index ae711ca..3054933 100644 --- a/go.mod +++ b/go.mod @@ -47,14 +47,13 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 github.com/hupe1980/go-huggingface v0.0.15 github.com/kyverno/policy-reporter-kyverno-plugin v1.6.4 - github.com/metoro-io/mcp-golang v0.11.0 + github.com/mark3labs/mcp-go v0.36.0 github.com/olekukonko/tablewriter v0.0.5 github.com/oracle/oci-go-sdk/v65 v65.79.0 github.com/prometheus/prometheus v0.302.1 github.com/pterm/pterm v0.12.80 google.golang.org/api v0.218.0 gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.1 sigs.k8s.io/controller-runtime v0.19.3 sigs.k8s.io/gateway-api v1.2.1 ) @@ -97,11 +96,7 @@ require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect github.com/containerd/console v1.0.4 // indirect github.com/containerd/continuity v0.4.3 // indirect @@ -117,13 +112,6 @@ require ( github.com/expr-lang/expr v1.17.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.10.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.22.1 // indirect - github.com/goccy/go-json v0.10.2 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -134,12 +122,10 @@ require ( github.com/gookit/color v1.5.4 // indirect github.com/gorilla/websocket v1.5.1 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect - github.com/invopop/jsonschema v0.12.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/moby/sys/mountinfo v0.7.1 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect @@ -154,15 +140,10 @@ require ( github.com/sony/gobreaker v0.5.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/tidwall/gjson v1.18.0 // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.1 // indirect - github.com/tidwall/sjson v1.2.5 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.32.0 // indirect @@ -171,12 +152,12 @@ require ( go.opentelemetry.io/otel/metric v1.34.0 // indirect go.opentelemetry.io/otel/sdk v1.34.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect - golang.org/x/arch v0.8.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3 // indirect ) @@ -271,7 +252,7 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect diff --git a/go.sum b/go.sum index efbdd09..51f539a 100644 --- a/go.sum +++ b/go.sum @@ -789,10 +789,6 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembj github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= @@ -810,10 +806,6 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -926,13 +918,7 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= @@ -965,14 +951,6 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= -github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-resty/resty/v2 v2.16.3 h1:zacNT7lt4b8M/io2Ahj6yPypL7bqx9n1iprfQuodV+E= github.com/go-resty/resty/v2 v2.16.3/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= @@ -985,8 +963,6 @@ github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= @@ -1192,8 +1168,8 @@ github.com/imdario/mergo v1.0.1 h1:lFIgOs30GMaV/2+qQ+eEBLbUL6h1YosdohE3ODy4hTs= github.com/imdario/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= -github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/ionos-cloud/sdk-go/v6 v6.3.2 h1:2mUmrZZz6cPyT9IRX0T8fBLc/7XU/eTxP2Y5tS7/09k= github.com/ionos-cloud/sdk-go/v6 v6.3.2/go.mod h1:SXrO9OGyWjd2rZhAhEpdYN6VUAODzzqRdqA9BCviQtI= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -1232,7 +1208,6 @@ github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuOb github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -1255,8 +1230,6 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= @@ -1271,6 +1244,8 @@ github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mark3labs/mcp-go v0.36.0 h1:rIZaijrRYPeSbJG8/qNDe0hWlGrCJ7FWHNMz2SQpTis= +github.com/mark3labs/mcp-go v0.36.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -1285,8 +1260,6 @@ github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4 github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/metoro-io/mcp-golang v0.11.0 h1:1k+VSE9QaeMTLn0gJ3FgE/DcjsCBsLFnz5eSFbgXUiI= -github.com/metoro-io/mcp-golang v0.11.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= @@ -1460,8 +1433,8 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -1490,20 +1463,6 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= -github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= @@ -1522,6 +1481,8 @@ github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +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/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1576,9 +1537,6 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -2328,7 +2286,6 @@ modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/pkg/server/client_example/main.go b/pkg/server/client_example/main.go index f297739..cd66b38 100644 --- a/pkg/server/client_example/main.go +++ b/pkg/server/client_example/main.go @@ -40,12 +40,20 @@ type AnalyzeRequest struct { WithStats bool `json:"withStats,omitempty"` } -// AnalyzeResponse represents the output of the analyze tool -type AnalyzeResponse struct { - Content []struct { - Text string `json:"text"` - Type string `json:"type"` - } `json:"content"` +// JSONRPCResponse represents the JSON-RPC response format +type JSONRPCResponse struct { + JSONRPC string `json:"jsonrpc"` + ID int `json:"id"` + Result struct { + Content []struct { + Text string `json:"text"` + Type string `json:"type"` + } `json:"content"` + } `json:"result,omitempty"` + Error *struct { + Code int `json:"code"` + Message string `json:"message"` + } `json:"error,omitempty"` } func main() { @@ -65,23 +73,89 @@ func main() { MaxConcurrency: 10, } - // Convert request to JSON - reqJSON, err := json.Marshal(req) - if err != nil { - log.Fatalf("Failed to marshal request: %v", err) - } + // Note: req is now used directly in the JSON-RPC request // Create HTTP client with timeout client := &http.Client{ Timeout: 5 * time.Minute, } - // Send request to MCP server - resp, err := client.Post( - fmt.Sprintf("http://localhost:%s/mcp/analyze", *serverPort), + // First, initialize the session + initRequest := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": map[string]interface{}{ + "protocolVersion": "2025-03-26", + "capabilities": map[string]interface{}{ + "tools": map[string]interface{}{}, + "resources": map[string]interface{}{}, + "prompts": map[string]interface{}{}, + }, + "clientInfo": map[string]interface{}{ + "name": "k8sgpt-client", + "version": "1.0.0", + }, + }, + } + + initData, err := json.Marshal(initRequest) + if err != nil { + log.Fatalf("Failed to marshal init request: %v", err) + } + + // Send initialization request + initResp, err := client.Post( + fmt.Sprintf("http://localhost:%s/mcp", *serverPort), "application/json", - bytes.NewBuffer(reqJSON), + bytes.NewBuffer(initData), ) + if err != nil { + log.Fatalf("Failed to send init request: %v", err) + } + defer func() { + if err := initResp.Body.Close(); err != nil { + log.Printf("Error closing init response body: %v", err) + } + }() + + // Extract session ID from response headers + sessionID := initResp.Header.Get("Mcp-Session-Id") + if sessionID == "" { + log.Println("Warning: No session ID received from server") + } + + // Create JSON-RPC request for analyze + jsonRPCRequest := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": map[string]interface{}{ + "name": "analyze", + "arguments": req, + }, + } + + // Convert to JSON + jsonRPCData, err := json.Marshal(jsonRPCRequest) + if err != nil { + log.Fatalf("Failed to marshal JSON-RPC request: %v", err) + } + + // Create request with session ID if available + httpReq, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%s/mcp", *serverPort), bytes.NewBuffer(jsonRPCData)) + if err != nil { + log.Fatalf("Failed to create request: %v", err) + } + + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Accept", "application/json,text/event-stream") + if sessionID != "" { + httpReq.Header.Set("Mcp-Session-Id", sessionID) + } + + // Send request to MCP server + resp, err := client.Do(httpReq) if err != nil { log.Fatalf("Failed to send request: %v", err) } @@ -99,15 +173,17 @@ func main() { fmt.Printf("Raw response: %s\n", string(body)) // Parse response - var analyzeResp AnalyzeResponse - if err := json.Unmarshal(body, &analyzeResp); err != nil { + var jsonRPCResp JSONRPCResponse + if err := json.Unmarshal(body, &jsonRPCResp); err != nil { log.Fatalf("Failed to decode response: %v", err) } // Print results fmt.Println("Analysis Results:") - if len(analyzeResp.Content) > 0 { - fmt.Println(analyzeResp.Content[0].Text) + if jsonRPCResp.Error != nil { + fmt.Printf("Error: %s (code: %d)\n", jsonRPCResp.Error.Message, jsonRPCResp.Error.Code) + } else if len(jsonRPCResp.Result.Content) > 0 { + fmt.Println(jsonRPCResp.Result.Content[0].Text) } else { fmt.Println("No results returned") } diff --git a/pkg/server/mcp.go b/pkg/server/mcp.go index 3256b49..e9a7b36 100644 --- a/pkg/server/mcp.go +++ b/pkg/server/mcp.go @@ -17,88 +17,205 @@ import ( "context" "encoding/json" "fmt" + "regexp" schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/k8sgpt-ai/k8sgpt/pkg/analysis" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/server/config" - mcp_golang "github.com/metoro-io/mcp-golang" - mcp_http "github.com/metoro-io/mcp-golang/transport/http" - "github.com/metoro-io/mcp-golang/transport/stdio" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" "github.com/spf13/viper" "go.uber.org/zap" ) -// MCPServer represents an MCP server for k8sgpt -type MCPServer struct { - server *mcp_golang.Server - port string - aiProvider *ai.AIProvider - useHTTP bool - logger *zap.Logger +// K8sGptMCPServer represents an MCP server for k8sgpt +type K8sGptMCPServer struct { + server *server.MCPServer + port string + aiProvider *ai.AIProvider + useHTTP bool + logger *zap.Logger + httpServer *server.StreamableHTTPServer + stdioServer *server.StdioServer } -// NewMCPServer creates a new MCP server -func NewMCPServer(port string, aiProvider *ai.AIProvider, useHTTP bool, logger *zap.Logger) (*MCPServer, error) { - opts := []mcp_golang.ServerOptions{ - mcp_golang.WithName("k8sgpt"), - mcp_golang.WithVersion("1.0.0"), +func NewMCPServer(port string, aiProvider *ai.AIProvider, useHTTP bool, logger *zap.Logger) (*K8sGptMCPServer, error) { + opts := []server.ServerOption{ + server.WithToolCapabilities(true), + server.WithResourceCapabilities(true, false), + server.WithPromptCapabilities(false), } - var server *mcp_golang.Server - if useHTTP { - logger.Info("starting MCP server with http transport on port", zap.String("port", port)) - httpTransport := mcp_http.NewHTTPTransport("/mcp").WithAddr(":" + port) - server = mcp_golang.NewServer(httpTransport, opts...) - } else { - server = mcp_golang.NewServer(stdio.NewStdioServerTransport(), opts...) - } - - return &MCPServer{ - server: server, + // Create the MCP server + mcpServer := server.NewMCPServer("k8sgpt", "1.0.0", opts...) + var k8sGptMCPServer = &K8sGptMCPServer{ + server: mcpServer, port: port, aiProvider: aiProvider, useHTTP: useHTTP, logger: logger, - }, nil + } + + // Register tools and resources immediately + if err := k8sGptMCPServer.registerToolsAndResources(); err != nil { + return nil, fmt.Errorf("failed to register tools and resources: %v", err) + } + + if useHTTP { + // Create HTTP server with streamable transport + httpOpts := []server.StreamableHTTPOption{ + server.WithLogger(&zapLoggerAdapter{logger: logger}), + } + + httpServer := server.NewStreamableHTTPServer(mcpServer, httpOpts...) + + // Launch the HTTP server directly + go func() { + logger.Info("Starting MCP HTTP server", zap.String("port", port)) + if err := httpServer.Start(":" + port); err != nil { + logger.Fatal("MCP HTTP server failed", zap.Error(err)) + } + }() + + return &K8sGptMCPServer{ + server: mcpServer, + port: port, + aiProvider: aiProvider, + useHTTP: useHTTP, + logger: logger, + httpServer: httpServer, + }, nil + } else { + // Create stdio server + stdioServer := server.NewStdioServer(mcpServer) + + return &K8sGptMCPServer{ + server: mcpServer, + port: port, + aiProvider: aiProvider, + useHTTP: useHTTP, + logger: logger, + stdioServer: stdioServer, + }, nil + } } // Start starts the MCP server -func (s *MCPServer) Start() error { +func (s *K8sGptMCPServer) Start() error { if s.server == nil { return fmt.Errorf("server not initialized") } - - // Register analyze tool - if err := s.server.RegisterTool("analyze", "Analyze Kubernetes resources", s.handleAnalyze); err != nil { - return fmt.Errorf("failed to register analyze tool: %v", err) + // Register prompts + if err := s.registerPrompts(); err != nil { + return fmt.Errorf("failed to register prompts: %v", err) } - - // Register cluster info tool - if err := s.server.RegisterTool("cluster-info", "Get Kubernetes cluster information", s.handleClusterInfo); err != nil { - return fmt.Errorf("failed to register cluster-info tool: %v", err) - } - - // Register config tool - if err := s.server.RegisterTool("config", "Configure K8sGPT settings", s.handleConfig); err != nil { - return fmt.Errorf("failed to register config tool: %v", err) - } - // Register resources if err := s.registerResources(); err != nil { return fmt.Errorf("failed to register resources: %v", err) } - // Register prompts - if err := s.registerPrompts(); err != nil { - return fmt.Errorf("failed to register prompts: %v", err) + // Start the server based on transport type + if s.useHTTP { + // HTTP server is already running in a goroutine + return nil + } else { + // Start stdio server (this will block) + return server.ServeStdio(s.server) } +} - // Start the server (this will block) - if err := s.server.Serve(); err != nil { - s.logger.Error("Error starting MCP server", zap.Error(err)) - } +func (s *K8sGptMCPServer) registerToolsAndResources() error { + // Register analyze tool with proper JSON schema + analyzeTool := mcp.NewTool("analyze", + mcp.WithDescription("Analyze Kubernetes resources for issues and problems"), + mcp.WithString("namespace", + mcp.Description("Kubernetes namespace to analyze (empty for all namespaces)"), + ), + mcp.WithString("backend", + mcp.Description("AI backend to use for analysis (e.g., openai, azure, localai)"), + ), + mcp.WithBoolean("explain", + mcp.Description("Provide detailed explanations for issues"), + ), + mcp.WithArray("filters", + mcp.Description("Provide filters to narrow down the analysis (e.g. ['Pods', 'Deployments'])"), + ), + ) + s.server.AddTool(analyzeTool, s.handleAnalyze) + + // Register cluster info tool (no parameters needed) + clusterInfoTool := mcp.NewTool("cluster-info", + mcp.WithDescription("Get Kubernetes cluster information and version"), + ) + s.server.AddTool(clusterInfoTool, s.handleClusterInfo) + + // Register config tool with proper JSON schema + configTool := mcp.NewTool("config", + mcp.WithDescription("Configure K8sGPT settings including custom analyzers and cache"), + mcp.WithObject("customAnalyzers", + mcp.Description("Custom analyzer configurations"), + mcp.Properties(map[string]any{ + "name": map[string]any{ + "type": "string", + "description": "Name of the custom analyzer", + }, + "connection": map[string]any{ + "type": "object", + "properties": map[string]any{ + "url": map[string]any{ + "type": "string", + "description": "URL of the custom analyzer service", + }, + "port": map[string]any{ + "type": "integer", + "description": "Port of the custom analyzer service", + }, + }, + }, + }), + ), + mcp.WithObject("cache", + mcp.Description("Cache configuration"), + mcp.Properties(map[string]any{ + "type": map[string]any{ + "type": "string", + "description": "Cache type (s3, azure, gcs)", + "enum": []string{"s3", "azure", "gcs"}, + }, + "bucketName": map[string]any{ + "type": "string", + "description": "Bucket name for S3/GCS cache", + }, + "region": map[string]any{ + "type": "string", + "description": "Region for S3/GCS cache", + }, + "endpoint": map[string]any{ + "type": "string", + "description": "Custom endpoint for S3 cache", + }, + "insecure": map[string]any{ + "type": "boolean", + "description": "Use insecure connection for cache", + }, + "storageAccount": map[string]any{ + "type": "string", + "description": "Storage account for Azure cache", + }, + "containerName": map[string]any{ + "type": "string", + "description": "Container name for Azure cache", + }, + "projectId": map[string]any{ + "type": "string", + "description": "Project ID for GCS cache", + }, + }), + ), + ) + s.server.AddTool(configTool, s.handleConfig) return nil } @@ -116,6 +233,7 @@ type AnalyzeRequest struct { InteractiveMode bool `json:"interactiveMode,omitempty"` CustomHeaders []string `json:"customHeaders,omitempty"` WithStats bool `json:"withStats,omitempty"` + Anonymize bool `json:"anonymize,omitempty"` } // AnalyzeResponse represents the output of the analyze tool @@ -163,62 +281,74 @@ type ConfigResponse struct { } // handleAnalyze handles the analyze tool -func (s *MCPServer) handleAnalyze(ctx context.Context, request *AnalyzeRequest) (*mcp_golang.ToolResponse, error) { - // Get stored configuration - var configAI ai.AIConfiguration - if err := viper.UnmarshalKey("ai", &configAI); err != nil { - return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Failed to load AI configuration: %v", err))), nil +func (s *K8sGptMCPServer) handleAnalyze(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + + var req AnalyzeRequest + if err := request.BindArguments(&req); err != nil { + return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil } - // Use stored configuration if not specified in request - if request.Backend == "" { - if configAI.DefaultProvider != "" { - request.Backend = configAI.DefaultProvider - } else if len(configAI.Providers) > 0 { - request.Backend = configAI.Providers[0].Name + + if req.Backend == "" { + if s.aiProvider.Name != "" { + req.Backend = s.aiProvider.Name } else { - request.Backend = "openai" // fallback default + req.Backend = "openai" // fallback default } } - request.Explain = true // Get stored filters if not specified - if len(request.Filters) == 0 { - request.Filters = viper.GetStringSlice("active_filters") + if len(req.Filters) == 0 { + req.Filters = viper.GetStringSlice("active_filters") } // Validate MaxConcurrency to prevent excessive memory allocation - request.MaxConcurrency = validateMaxConcurrency(request.MaxConcurrency) + req.MaxConcurrency = validateMaxConcurrency(req.MaxConcurrency) // Create a new analysis with the request parameters analysis, err := analysis.NewAnalysis( - request.Backend, - request.Language, - request.Filters, - request.Namespace, - request.LabelSelector, - request.NoCache, - request.Explain, - request.MaxConcurrency, - request.WithDoc, - request.InteractiveMode, - request.CustomHeaders, - request.WithStats, + req.Backend, + req.Language, + req.Filters, + req.Namespace, + req.LabelSelector, + req.NoCache, + req.Explain, + req.MaxConcurrency, + req.WithDoc, + req.InteractiveMode, + req.CustomHeaders, + req.WithStats, ) if err != nil { - return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Failed to create analysis: %v", err))), nil + return mcp.NewToolResultErrorf("Failed to create analysis: %v", err), nil } defer analysis.Close() // Run the analysis analysis.RunAnalysis() + if req.Explain { - // Get the output - output, err := analysis.PrintOutput("json") - if err != nil { - return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Failed to print output: %v", err))), nil + var output string + err := analysis.GetAIResults(output, req.Anonymize) + if err != nil { + return mcp.NewToolResultErrorf("Failed to get results from AI: %v", err), nil + } + + // Convert results to JSON string using PrintOutput + outputBytes, err := analysis.PrintOutput("text") + if err != nil { + return mcp.NewToolResultErrorf("Failed to convert results to string: %v", err), nil + } + plainText := stripANSI(string(outputBytes)) + return mcp.NewToolResultText(plainText), nil + } else { + // Get the output + output, err := analysis.PrintOutput("json") + if err != nil { + return mcp.NewToolResultErrorf("Failed to print output: %v", err), nil + } + return mcp.NewToolResultText(string(output)), nil } - - return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(string(output))), nil } // validateMaxConcurrency validates and bounds the MaxConcurrency parameter @@ -233,25 +363,31 @@ func validateMaxConcurrency(maxConcurrency int) int { } // handleClusterInfo handles the cluster-info tool -func (s *MCPServer) handleClusterInfo(ctx context.Context, request *ClusterInfoRequest) (*mcp_golang.ToolResponse, error) { +func (s *K8sGptMCPServer) handleClusterInfo(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Create a new Kubernetes client client, err := kubernetes.NewClient("", "") if err != nil { - return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("failed to create Kubernetes client: %v", err))), nil + return mcp.NewToolResultErrorf("failed to create Kubernetes client: %v", err), nil } // Get cluster info from the client version, err := client.Client.Discovery().ServerVersion() if err != nil { - return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("failed to get cluster version: %v", err))), nil + return mcp.NewToolResultErrorf("failed to get cluster version: %v", err), nil } info := fmt.Sprintf("Kubernetes %s", version.GitVersion) - return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(info)), nil + return mcp.NewToolResultText(info), nil } // handleConfig handles the config tool -func (s *MCPServer) handleConfig(ctx context.Context, request *ConfigRequest) (*mcp_golang.ToolResponse, error) { +func (s *K8sGptMCPServer) handleConfig(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Parse request arguments + var req ConfigRequest + if err := request.BindArguments(&req); err != nil { + return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil + } + // Create a new config handler handler := &config.Handler{} @@ -261,8 +397,8 @@ func (s *MCPServer) handleConfig(ctx context.Context, request *ConfigRequest) (* } // Add custom analyzers if present - if len(request.CustomAnalyzers) > 0 { - for _, ca := range request.CustomAnalyzers { + if len(req.CustomAnalyzers) > 0 { + for _, ca := range req.CustomAnalyzers { addConfigReq.CustomAnalyzers = append(addConfigReq.CustomAnalyzers, &schemav1.CustomAnalyzer{ Name: ca.Name, Connection: &schemav1.Connection{ @@ -274,31 +410,31 @@ func (s *MCPServer) handleConfig(ctx context.Context, request *ConfigRequest) (* } // Add cache configuration if present - if request.Cache.Type != "" { + if req.Cache.Type != "" { cacheConfig := &schemav1.Cache{} - switch request.Cache.Type { + switch req.Cache.Type { case "s3": cacheConfig.CacheType = &schemav1.Cache_S3Cache{ S3Cache: &schemav1.S3Cache{ - BucketName: request.Cache.BucketName, - Region: request.Cache.Region, - Endpoint: request.Cache.Endpoint, - Insecure: request.Cache.Insecure, + BucketName: req.Cache.BucketName, + Region: req.Cache.Region, + Endpoint: req.Cache.Endpoint, + Insecure: req.Cache.Insecure, }, } case "azure": cacheConfig.CacheType = &schemav1.Cache_AzureCache{ AzureCache: &schemav1.AzureCache{ - StorageAccount: request.Cache.StorageAccount, - ContainerName: request.Cache.ContainerName, + StorageAccount: req.Cache.StorageAccount, + ContainerName: req.Cache.ContainerName, }, } case "gcs": cacheConfig.CacheType = &schemav1.Cache_GcsCache{ GcsCache: &schemav1.GCSCache{ - BucketName: request.Cache.BucketName, - Region: request.Cache.Region, - ProjectId: request.Cache.ProjectId, + BucketName: req.Cache.BucketName, + Region: req.Cache.Region, + ProjectId: req.Cache.ProjectId, }, } } @@ -307,27 +443,30 @@ func (s *MCPServer) handleConfig(ctx context.Context, request *ConfigRequest) (* // Apply the configuration using the shared function if err := handler.ApplyConfig(ctx, addConfigReq); err != nil { - return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Failed to add config: %v", err))), nil + return mcp.NewToolResultErrorf("Failed to add config: %v", err), nil } - return mcp_golang.NewToolResponse(mcp_golang.NewTextContent("Successfully added configuration")), nil + return mcp.NewToolResultText("Successfully added configuration"), nil } // registerPrompts registers the prompts for the MCP server -func (s *MCPServer) registerPrompts() error { +func (s *K8sGptMCPServer) registerPrompts() error { // Register any prompts needed for the MCP server return nil } // registerResources registers the resources for the MCP server -func (s *MCPServer) registerResources() error { - if err := s.server.RegisterResource("cluster-info", "Get cluster information", "Get information about the Kubernetes cluster", "text", s.getClusterInfo); err != nil { - return fmt.Errorf("failed to register cluster-info resource: %v", err) - } +func (s *K8sGptMCPServer) registerResources() error { + clusterInfoResource := mcp.NewResource("cluster-info", "cluster-info", + mcp.WithResourceDescription("Get information about the Kubernetes cluster"), + mcp.WithMIMEType("application/json"), + ) + + s.server.AddResource(clusterInfoResource, s.getClusterInfo) return nil } -func (s *MCPServer) getClusterInfo(ctx context.Context) (*mcp_golang.ResourceResponse, error) { +func (s *K8sGptMCPServer) getClusterInfo(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { // Create a new Kubernetes client client, err := kubernetes.NewClient("", "") if err != nil { @@ -346,24 +485,44 @@ func (s *MCPServer) getClusterInfo(ctx context.Context) (*mcp_golang.ResourceRes "gitVersion": version.GitVersion, }) if err != nil { - return mcp_golang.NewResourceResponse( - mcp_golang.NewTextEmbeddedResource( - "cluster-info", - "Failed to marshal cluster info", - "text/plain", - ), - ), nil + return []mcp.ResourceContents{ + &mcp.TextResourceContents{ + URI: "cluster-info", + MIMEType: "text/plain", + Text: "Failed to marshal cluster info", + }, + }, nil } - return mcp_golang.NewResourceResponse( - mcp_golang.NewTextEmbeddedResource( - "cluster-info", - string(data), - "application/json", - ), - ), nil + + return []mcp.ResourceContents{ + &mcp.TextResourceContents{ + URI: "cluster-info", + MIMEType: "application/json", + Text: string(data), + }, + }, nil } // Close closes the MCP server and releases resources -func (s *MCPServer) Close() error { +func (s *K8sGptMCPServer) Close() error { return nil } + +// zapLoggerAdapter adapts zap.Logger to the interface expected by mark3labs/mcp-go +type zapLoggerAdapter struct { + logger *zap.Logger +} + +func (z *zapLoggerAdapter) Infof(format string, v ...any) { + z.logger.Info(fmt.Sprintf(format, v...)) +} + +func (z *zapLoggerAdapter) Errorf(format string, v ...any) { + z.logger.Error(fmt.Sprintf(format, v...)) +} + +// stripANSI removes ANSI escape sequences from a string +func stripANSI(input string) string { + re := regexp.MustCompile(`\x1b\[[0-9;]*m`) + return re.ReplaceAllString(input, "") +} diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 795e23b..28acb04 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -3,6 +3,7 @@ package server import ( "bytes" "context" + "io" "net" "net/http" "testing" @@ -78,14 +79,14 @@ func TestMCPServerCreation(t *testing.T) { } // Test HTTP mode - mcpServer, err := NewMCPServer("8089", aiProvider, true, logger) + mcpServer, err := NewMCPServer("8088", aiProvider, true, logger) assert.NoError(t, err, "Should be able to create MCP server with HTTP transport") assert.NotNil(t, mcpServer, "MCP server should not be nil") assert.True(t, mcpServer.useHTTP, "MCP server should be in HTTP mode") - assert.Equal(t, "8089", mcpServer.port, "Port should be set correctly") + assert.Equal(t, "8088", mcpServer.port, "Port should be set correctly") // Test stdio mode - mcpServerStdio, err := NewMCPServer("8089", aiProvider, false, logger) + mcpServerStdio, err := NewMCPServer("8088", aiProvider, false, logger) assert.NoError(t, err, "Should be able to create MCP server with stdio transport") assert.NotNil(t, mcpServerStdio, "MCP server should not be nil") assert.False(t, mcpServerStdio.useHTTP, "MCP server should be in stdio mode") @@ -107,27 +108,83 @@ func TestMCPServerBasicHTTP(t *testing.T) { Model: "test-model", } - mcpServer, err := NewMCPServer("8089", aiProvider, true, logger) + mcpServer, err := NewMCPServer("8091", aiProvider, true, logger) assert.NoError(t, err, "Should be able to create MCP server") - // Start the MCP server in a goroutine - go func() { - err := mcpServer.Start() - // Note: Start() might return an error when the server is stopped, which is expected - if err != nil { - logger.Info("MCP server stopped", zap.Error(err)) - } - }() + // For HTTP mode, the server is already started in NewMCPServer + // No need to call Start() as it's already running in a goroutine // Wait for the server to start - err = waitForPort("localhost:8089", 10*time.Second) + err = waitForPort("localhost:8091", 10*time.Second) if err != nil { t.Skipf("MCP server did not start within timeout: %v", err) } - // Test basic connectivity to the MCP endpoint - // The MCP HTTP transport uses a single POST endpoint for all requests - resp, err := http.Post("http://localhost:8089/mcp", "application/json", bytes.NewBufferString(`{"jsonrpc":"2.0","id":1,"method":"tools/list"}`)) + // First, initialize the session + initRequest := `{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": { + "tools": {}, + "resources": {}, + "prompts": {} + }, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } + }` + + initResp, err := http.Post("http://localhost:8091/mcp", "application/json", bytes.NewBufferString(initRequest)) + if err != nil { + t.Logf("Initialize request failed: %v", err) + return + } + defer func() { + if err := initResp.Body.Close(); err != nil { + t.Logf("Error closing init response body: %v", err) + } + }() + + // Read initialization response + initBody, err := io.ReadAll(initResp.Body) + if err != nil { + t.Logf("Failed to read init response body: %v", err) + } else { + t.Logf("Init response status: %d, body: %s", initResp.StatusCode, string(initBody)) + } + + // Extract session ID from response headers if present + sessionID := initResp.Header.Get("Mcp-Session-Id") + if sessionID == "" { + t.Logf("No session ID in response headers") + } + + // Now test tools/list with session ID if available + headers := map[string]string{ + "Content-Type": "application/json", + "Accept": "application/json,text/event-stream", + } + if sessionID != "" { + headers["Mcp-Session-Id"] = sessionID + } + + req, err := http.NewRequest("POST", "http://localhost:8091/mcp", bytes.NewBufferString(`{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}`)) + if err != nil { + t.Logf("Failed to create request: %v", err) + return + } + + for key, value := range headers { + req.Header.Set(key, value) + } + + client := &http.Client{} + resp, err := client.Do(req) if err != nil { t.Logf("MCP endpoint test skipped (server might not be fully ready): %v", err) return @@ -139,6 +196,14 @@ func TestMCPServerBasicHTTP(t *testing.T) { } }() + // Read response body for debugging + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Logf("Failed to read response body: %v", err) + } else { + t.Logf("Response status: %d, body: %s", resp.StatusCode, string(body)) + } + // Accept both 200 and 404 as valid responses (404 means endpoint not implemented) if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound { t.Errorf("MCP endpoint returned unexpected status: %d", resp.StatusCode) @@ -168,13 +233,8 @@ func TestMCPServerToolCall(t *testing.T) { mcpServer, err := NewMCPServer("8090", aiProvider, true, logger) assert.NoError(t, err, "Should be able to create MCP server") - // Start the MCP server in a goroutine - go func() { - err := mcpServer.Start() - if err != nil { - logger.Info("MCP server stopped", zap.Error(err)) - } - }() + // For HTTP mode, the server is already started in NewMCPServer + // No need to call Start() as it's already running in a goroutine // Wait for the server to start err = waitForPort("localhost:8090", 10*time.Second) @@ -182,6 +242,39 @@ func TestMCPServerToolCall(t *testing.T) { t.Skipf("MCP server did not start within timeout: %v", err) } + // First, initialize the session + initRequest := `{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": { + "tools": {}, + "resources": {}, + "prompts": {} + }, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } + }` + + initResp, err := http.Post("http://localhost:8090/mcp", "application/json", bytes.NewBufferString(initRequest)) + if err != nil { + t.Logf("Initialize request failed: %v", err) + return + } + defer func() { + if err := initResp.Body.Close(); err != nil { + t.Logf("Error closing init response body: %v", err) + } + }() + + // Extract session ID from response headers if present + sessionID := initResp.Header.Get("Mcp-Session-Id") + // Test calling the analyze tool with proper JSON-RPC format analyzeRequest := `{ "jsonrpc": "2.0", @@ -199,7 +292,21 @@ func TestMCPServerToolCall(t *testing.T) { } }` - resp, err := http.Post("http://localhost:8090/mcp", "application/json", bytes.NewBufferString(analyzeRequest)) + // Create request with session ID if available + req, err := http.NewRequest("POST", "http://localhost:8090/mcp", bytes.NewBufferString(analyzeRequest)) + if err != nil { + t.Logf("Failed to create request: %v", err) + return + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json,text/event-stream") + if sessionID != "" { + req.Header.Set("Mcp-Session-Id", sessionID) + } + + client := &http.Client{} + resp, err := client.Do(req) if err != nil { t.Logf("Analyze tool call test skipped (server might not be fully ready): %v", err) return