diff --git a/README.md b/README.md index b42e8dfc2..7f24efe45 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,11 @@ A powerful and flexible Kubernetes [Model Context Protocol (MCP)](https://blog.m - **Install** a Helm chart in the current or provided namespace. - **List** Helm releases in all namespaces or in a specific namespace. - **Uninstall** a Helm release in the current or provided namespace. +- **🔧 Tekton**: Tekton-specific operations that complement generic Kubernetes resource management. + - **Pipeline**: Start a Tekton Pipeline by creating a PipelineRun. + - **PipelineRun**: Restart a PipelineRun with the same spec. + - **Task**: Start a Tekton Task by creating a TaskRun. + - **TaskRun**: Restart a TaskRun with the same spec, and retrieve TaskRun logs via pod resolution. - **🔭 Observability**: Optional OpenTelemetry distributed tracing and metrics with custom sampling rates. Includes `/stats` endpoint for real-time statistics. See [OTEL.md](docs/OTEL.md). Unlike other Kubernetes MCP server implementations, this **IS NOT** just a wrapper around `kubectl` or `helm` command-line tools. @@ -268,6 +273,7 @@ The following sets of tools are available (toolsets marked with ✓ in the Defau | kcp | Manage kcp workspaces and multi-tenancy features | | | kiali | Most common tools for managing Kiali, check the [Kiali documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/KIALI.md) for more details. | | | kubevirt | KubeVirt virtual machine management tools, check the [KubeVirt documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/kubevirt.md) for more details. | | +| tekton | Tekton pipeline management tools for Pipelines, PipelineRuns, Tasks, and TaskRuns. | | @@ -515,6 +521,35 @@ In case multi-cluster support is enabled (default) and you have access to multip +
+ +tekton + +- **tekton_pipeline_start** - Start a Tekton Pipeline by creating a PipelineRun that references it + - `name` (`string`) **(required)** - Name of the Pipeline to start + - `namespace` (`string`) - Namespace of the Pipeline + - `params` (`object`) - Parameter values to pass to the Pipeline. Keys are parameter names; values can be a string, an array of strings, or an object (map of string to string) depending on the parameter type defined in the Pipeline spec + +- **tekton_pipelinerun_restart** - Restart a Tekton PipelineRun by creating a new PipelineRun with the same spec + - `name` (`string`) **(required)** - Name of the PipelineRun to restart + - `namespace` (`string`) - Namespace of the PipelineRun + +- **tekton_task_start** - Start a Tekton Task by creating a TaskRun that references it + - `name` (`string`) **(required)** - Name of the Task to start + - `namespace` (`string`) - Namespace of the Task + - `params` (`object`) - Parameter values to pass to the Task. Keys are parameter names; values can be a string, an array of strings, or an object (map of string to string) depending on the parameter type defined in the Task spec + +- **tekton_taskrun_restart** - Restart a Tekton TaskRun by creating a new TaskRun with the same spec + - `name` (`string`) **(required)** - Name of the TaskRun to restart + - `namespace` (`string`) - Namespace of the TaskRun + +- **tekton_taskrun_logs** - Get the logs from a Tekton TaskRun by resolving its underlying pod + - `name` (`string`) **(required)** - Name of the TaskRun to get logs from + - `namespace` (`string`) - Namespace of the TaskRun + - `tail` (`integer`) - Number of lines to retrieve from the end of the logs (Optional, default: 100) + +
+ diff --git a/docs/configuration.md b/docs/configuration.md index bfecf8781..7fc8775d5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -296,6 +296,7 @@ Toolsets group related tools together. Enable only the toolsets you need to redu | kcp | Manage kcp workspaces and multi-tenancy features | | | kiali | Most common tools for managing Kiali, check the [Kiali documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/KIALI.md) for more details. | | | kubevirt | KubeVirt virtual machine management tools, check the [KubeVirt documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/kubevirt.md) for more details. | | +| tekton | Tekton pipeline management tools for Pipelines, PipelineRuns, Tasks, and TaskRuns. | | diff --git a/go.mod b/go.mod index 72571addb..bf0736761 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/containers/kubernetes-mcp-server -go 1.25.6 +go 1.25.7 require ( github.com/BurntSushi/toml v1.6.0 @@ -16,6 +16,7 @@ require ( github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 + github.com/tektoncd/pipeline v1.11.0 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 @@ -47,6 +48,7 @@ require ( ) require ( + cel.dev/expr v0.25.1 // indirect dario.cat/mergo v1.0.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect @@ -54,6 +56,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect @@ -80,6 +83,7 @@ require ( github.com/go-openapi/swag v0.23.1 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/google/btree v1.1.3 // indirect + github.com/google/cel-go v0.27.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect @@ -93,7 +97,7 @@ require ( github.com/jmoiron/sqlx v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lib/pq v1.10.9 // indirect @@ -128,22 +132,24 @@ require ( github.com/segmentio/asm v1.1.3 // indirect github.com/segmentio/encoding v0.5.4 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.49.0 // indirect + golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.41.0 // indirect golang.org/x/text v0.35.0 // indirect - golang.org/x/time v0.12.0 // indirect + golang.org/x/time v0.14.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/grpc v1.80.0 // indirect @@ -152,6 +158,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiserver v0.35.3 // indirect k8s.io/component-base v0.35.3 // indirect + knative.dev/pkg v0.0.0-20260318013857-98d5a706d4fd // indirect oras.land/oras-go/v2 v2.6.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/kustomize/api v0.20.1 // indirect diff --git a/go.sum b/go.sum index 792cba610..feb0db453 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= @@ -20,6 +22,8 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -28,6 +32,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= +github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -38,6 +44,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= +github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= github.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE= github.com/containerd/containerd v1.7.30/go.mod h1:fek494vwJClULlTpExsmOyKCMUAbuVjlFsJQc4/j44M= github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= @@ -67,8 +75,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= -github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= @@ -124,6 +132,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= +github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -152,6 +162,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= @@ -166,8 +178,10 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -271,8 +285,8 @@ github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= @@ -291,6 +305,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tektoncd/pipeline v1.11.0 h1:uJ6Bl6ydxiRi1SP7MBLdDnUwh6SzFzQcRF6MnIr5Xsc= +github.com/tektoncd/pipeline v1.11.0/go.mod h1:uglkGnsv6WLitNpBdyP+K3J4HnjA5kHgoNV75FqjoD4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= @@ -303,8 +319,8 @@ go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGh go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= @@ -327,8 +343,8 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= @@ -347,16 +363,18 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 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= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= @@ -364,7 +382,6 @@ golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7 golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= @@ -372,12 +389,12 @@ golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= -gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= -gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= @@ -426,6 +443,8 @@ k8s.io/metrics v0.35.3 h1:WonA18pEwrtb7a6XfhFg1ZY1Le0RFkcEw7CFApMTZos= k8s.io/metrics v0.35.3/go.mod h1:/O8UBb5QVyAekR2QvL/WWxskpdV1wVSEl4MSLAy4Ql4= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +knative.dev/pkg v0.0.0-20260318013857-98d5a706d4fd h1:yeh+smYaouOwhkyCPj+AYACt1MeD+EI4mXSzSbmtj10= +knative.dev/pkg v0.0.0-20260318013857-98d5a706d4fd/go.mod h1:o/XS1E/hYh9IR8deEEiJG4kKtQfqnf9Gwt5bwp2x4AU= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= diff --git a/internal/tools/update-readme/main.go b/internal/tools/update-readme/main.go index caf389d90..939c34323 100644 --- a/internal/tools/update-readme/main.go +++ b/internal/tools/update-readme/main.go @@ -19,6 +19,7 @@ import ( _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kcp" _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kiali" _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt" + _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/tekton" ) type OpenShift struct{} diff --git a/pkg/mcp/modules.go b/pkg/mcp/modules.go index 7ef68341c..e2374ea2e 100644 --- a/pkg/mcp/modules.go +++ b/pkg/mcp/modules.go @@ -7,4 +7,5 @@ import ( _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kcp" _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kiali" _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt" + _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/tekton" ) diff --git a/pkg/mcp/testdata/toolsets-tekton-tools.json b/pkg/mcp/testdata/toolsets-tekton-tools.json new file mode 100644 index 000000000..768fc21fe --- /dev/null +++ b/pkg/mcp/testdata/toolsets-tekton-tools.json @@ -0,0 +1,152 @@ +[ + { + "annotations": { + "destructiveHint": false, + "openWorldHint": false, + "title": "Tekton: Start Pipeline" + }, + "description": "Start a Tekton Pipeline by creating a PipelineRun that references it", + "inputSchema": { + "properties": { + "name": { + "description": "Name of the Pipeline to start", + "type": "string" + }, + "namespace": { + "description": "Namespace of the Pipeline", + "type": "string" + }, + "params": { + "additionalProperties": true, + "description": "Parameter values to pass to the Pipeline. Keys are parameter names; values can be a string, an array of strings, or an object (map of string to string) depending on the parameter type defined in the Pipeline spec", + "properties": {}, + "type": "object" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "tekton_pipeline_start", + "title": "Tekton: Start Pipeline" + }, + { + "annotations": { + "destructiveHint": false, + "openWorldHint": false, + "title": "Tekton: Restart PipelineRun" + }, + "description": "Restart a Tekton PipelineRun by creating a new PipelineRun with the same spec", + "inputSchema": { + "properties": { + "name": { + "description": "Name of the PipelineRun to restart", + "type": "string" + }, + "namespace": { + "description": "Namespace of the PipelineRun", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "tekton_pipelinerun_restart", + "title": "Tekton: Restart PipelineRun" + }, + { + "annotations": { + "destructiveHint": false, + "openWorldHint": false, + "title": "Tekton: Start Task" + }, + "description": "Start a Tekton Task by creating a TaskRun that references it", + "inputSchema": { + "properties": { + "name": { + "description": "Name of the Task to start", + "type": "string" + }, + "namespace": { + "description": "Namespace of the Task", + "type": "string" + }, + "params": { + "additionalProperties": true, + "description": "Parameter values to pass to the Task. Keys are parameter names; values can be a string, an array of strings, or an object (map of string to string) depending on the parameter type defined in the Task spec", + "properties": {}, + "type": "object" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "tekton_task_start", + "title": "Tekton: Start Task" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Tekton: Get TaskRun Logs" + }, + "description": "Get the logs from a Tekton TaskRun by resolving its underlying pod", + "inputSchema": { + "properties": { + "name": { + "description": "Name of the TaskRun to get logs from", + "type": "string" + }, + "namespace": { + "description": "Namespace of the TaskRun", + "type": "string" + }, + "tail": { + "default": 100, + "description": "Number of lines to retrieve from the end of the logs (Optional, default: 100)", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "tekton_taskrun_logs", + "title": "Tekton: Get TaskRun Logs" + }, + { + "annotations": { + "destructiveHint": false, + "openWorldHint": false, + "title": "Tekton: Restart TaskRun" + }, + "description": "Restart a Tekton TaskRun by creating a new TaskRun with the same spec", + "inputSchema": { + "properties": { + "name": { + "description": "Name of the TaskRun to restart", + "type": "string" + }, + "namespace": { + "description": "Namespace of the TaskRun", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "tekton_taskrun_restart", + "title": "Tekton: Restart TaskRun" + } +] diff --git a/pkg/mcp/toolsets_test.go b/pkg/mcp/toolsets_test.go index ec73c3f89..0f20fc87d 100644 --- a/pkg/mcp/toolsets_test.go +++ b/pkg/mcp/toolsets_test.go @@ -19,6 +19,7 @@ import ( "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kcp" "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kiali" "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt" + "github.com/containers/kubernetes-mcp-server/pkg/toolsets/tekton" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/suite" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" @@ -176,6 +177,7 @@ func (s *ToolsetsSuite) TestGranularToolsetsTools() { &helm.Toolset{}, &kiali.Toolset{}, &kubevirt.Toolset{}, + &tekton.Toolset{}, } for _, testCase := range testCases { s.Run("Toolset "+testCase.GetName(), func() { diff --git a/pkg/toolsets/tekton/params.go b/pkg/toolsets/tekton/params.go new file mode 100644 index 000000000..86e2daa27 --- /dev/null +++ b/pkg/toolsets/tekton/params.go @@ -0,0 +1,94 @@ +package tekton + +import ( + "fmt" + + "github.com/google/jsonschema-go/jsonschema" + tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// emptySchema is an empty JSON Schema (equivalent to JSON Schema `true`) that allows any additional properties. +var emptySchema = &jsonschema.Schema{} + +// GroupVersionResource definitions for Tekton resources +var ( + pipelineGVR = schema.GroupVersionResource{ + Group: "tekton.dev", + Version: "v1", + Resource: "pipelines", + } + pipelineRunGVR = schema.GroupVersionResource{ + Group: "tekton.dev", + Version: "v1", + Resource: "pipelineruns", + } + taskGVR = schema.GroupVersionResource{ + Group: "tekton.dev", + Version: "v1", + Resource: "tasks", + } + taskRunGVR = schema.GroupVersionResource{ + Group: "tekton.dev", + Version: "v1", + Resource: "taskruns", + } +) + +// parseParams converts a map[string]interface{} from a tool call argument into Tekton Params. +// Each map entry becomes a Param whose value type is inferred from the Go value: +// - string → ParamTypeString +// - []interface{} → ParamTypeArray (each element is coerced to string) +// - map[string]interface{} → ParamTypeObject (each value is coerced to string) +func parseParams(raw map[string]interface{}) ([]tektonv1.Param, error) { + params := make([]tektonv1.Param, 0, len(raw)) + for k, v := range raw { + pv, err := toParamValue(k, v) + if err != nil { + return nil, err + } + params = append(params, tektonv1.Param{Name: k, Value: pv}) + } + return params, nil +} + +func toParamValue(name string, v interface{}) (tektonv1.ParamValue, error) { + switch val := v.(type) { + case string: + return tektonv1.ParamValue{ + Type: tektonv1.ParamTypeString, + StringVal: val, + }, nil + + case []interface{}: + arr := make([]string, 0, len(val)) + for i, elem := range val { + s, ok := elem.(string) + if !ok { + return tektonv1.ParamValue{}, fmt.Errorf("param %q: array element %d must be a string, got %T", name, i, elem) + } + arr = append(arr, s) + } + return tektonv1.ParamValue{ + Type: tektonv1.ParamTypeArray, + ArrayVal: arr, + }, nil + + case map[string]interface{}: + obj := make(map[string]string, len(val)) + for k, elem := range val { + s, ok := elem.(string) + if !ok { + return tektonv1.ParamValue{}, fmt.Errorf("param %q: object key %q value must be a string, got %T", name, k, elem) + } + obj[k] = s + } + return tektonv1.ParamValue{ + Type: tektonv1.ParamTypeObject, + ObjectVal: obj, + }, nil + + default: + return tektonv1.ParamValue{}, fmt.Errorf("param %q: unsupported value type %T (expected string, array, or object)", name, v) + } +} diff --git a/pkg/toolsets/tekton/params_test.go b/pkg/toolsets/tekton/params_test.go new file mode 100644 index 000000000..420ee62e5 --- /dev/null +++ b/pkg/toolsets/tekton/params_test.go @@ -0,0 +1,114 @@ +package tekton + +import ( + "testing" + + "github.com/stretchr/testify/suite" + tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" +) + +type ParamsSuite struct { + suite.Suite +} + +func TestParams(t *testing.T) { + suite.Run(t, new(ParamsSuite)) +} + +func (s *ParamsSuite) TestParseParams() { + s.Run("string parameter", func() { + input := map[string]interface{}{ + "message": "hello world", + } + + result, err := parseParams(input) + + s.NoError(err) + s.Require().Len(result, 1) + s.Equal("message", result[0].Name) + s.Equal(tektonv1.ParamTypeString, result[0].Value.Type) + s.Equal("hello world", result[0].Value.StringVal) + }) + + s.Run("array parameter", func() { + input := map[string]interface{}{ + "items": []interface{}{"item1", "item2", "item3"}, + } + + result, err := parseParams(input) + + s.NoError(err) + s.Require().Len(result, 1) + s.Equal("items", result[0].Name) + s.Equal(tektonv1.ParamTypeArray, result[0].Value.Type) + s.Equal([]string{"item1", "item2", "item3"}, result[0].Value.ArrayVal) + }) + + s.Run("object parameter", func() { + input := map[string]interface{}{ + "config": map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + } + + result, err := parseParams(input) + + s.NoError(err) + s.Require().Len(result, 1) + s.Equal("config", result[0].Name) + s.Equal(tektonv1.ParamTypeObject, result[0].Value.Type) + s.Equal(map[string]string{"key1": "value1", "key2": "value2"}, result[0].Value.ObjectVal) + }) + + s.Run("mixed parameter types", func() { + input := map[string]interface{}{ + "stringParam": "test", + "arrayParam": []interface{}{"a", "b"}, + "objectParam": map[string]interface{}{"x": "y"}, + } + + result, err := parseParams(input) + + s.NoError(err) + s.Len(result, 3) + + paramsByName := make(map[string]tektonv1.Param) + for _, p := range result { + paramsByName[p.Name] = p + } + + s.Equal(tektonv1.ParamTypeString, paramsByName["stringParam"].Value.Type) + s.Equal("test", paramsByName["stringParam"].Value.StringVal) + + s.Equal(tektonv1.ParamTypeArray, paramsByName["arrayParam"].Value.Type) + s.Equal([]string{"a", "b"}, paramsByName["arrayParam"].Value.ArrayVal) + + s.Equal(tektonv1.ParamTypeObject, paramsByName["objectParam"].Value.Type) + s.Equal(map[string]string{"x": "y"}, paramsByName["objectParam"].Value.ObjectVal) + }) + + s.Run("array with non-string element", func() { + input := map[string]interface{}{ + "badArray": []interface{}{"string", 123}, + } + + result, err := parseParams(input) + + s.Error(err) + s.Nil(result) + s.Contains(err.Error(), "array element 1 must be a string") + }) + + s.Run("unsupported type", func() { + input := map[string]interface{}{ + "number": 42, + } + + result, err := parseParams(input) + + s.Error(err) + s.Nil(result) + s.Contains(err.Error(), "unsupported value type") + }) +} diff --git a/pkg/toolsets/tekton/pipeline.go b/pkg/toolsets/tekton/pipeline.go new file mode 100644 index 000000000..e25e00d31 --- /dev/null +++ b/pkg/toolsets/tekton/pipeline.go @@ -0,0 +1,106 @@ +package tekton + +import ( + "fmt" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/google/jsonschema-go/jsonschema" + tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" +) + +func pipelineTools() []api.ServerTool { + return []api.ServerTool{ + { + Tool: api.Tool{ + Name: "tekton_pipeline_start", + Description: "Start a Tekton Pipeline by creating a PipelineRun that references it", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "name": { + Type: "string", + Description: "Name of the Pipeline to start", + }, + "namespace": { + Type: "string", + Description: "Namespace of the Pipeline", + }, + "params": { + Type: "object", + Description: "Parameter values to pass to the Pipeline. Keys are parameter names; values can be a string, an array of strings, or an object (map of string to string) depending on the parameter type defined in the Pipeline spec", + Properties: make(map[string]*jsonschema.Schema), + AdditionalProperties: emptySchema, + }, + }, + Required: []string{"name"}, + }, + Annotations: api.ToolAnnotations{ + Title: "Tekton: Start Pipeline", + ReadOnlyHint: ptr.To(false), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(false), + OpenWorldHint: ptr.To(false), + }, + }, + Handler: startPipeline, + }, + } +} + +func startPipeline(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + name, err := api.RequiredString(params, "name") + if err != nil { + return api.NewToolCallResult("", err), nil + } + namespace := api.OptionalString(params, "namespace", params.NamespaceOrDefault("")) + + dynamicClient := params.DynamicClient() + + // Verify that the Pipeline exists + if _, err := dynamicClient.Resource(pipelineGVR).Namespace(namespace).Get(params.Context, name, metav1.GetOptions{}); err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get Pipeline %s/%s: %w", namespace, name, err)), nil + } + + var tektonParams []tektonv1.Param + if rawParams, ok := params.GetArguments()["params"].(map[string]interface{}); ok { + tektonParams, err = parseParams(rawParams) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to parse params: %w", err)), nil + } + } + + pr := &tektonv1.PipelineRun{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "tekton.dev/v1", + Kind: "PipelineRun", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + GenerateName: name + "-", + }, + Spec: tektonv1.PipelineRunSpec{ + PipelineRef: &tektonv1.PipelineRef{ + Name: name, + }, + Params: tektonParams, + }, + } + + // Convert to unstructured + unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(pr) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to convert PipelineRun to unstructured: %w", err)), nil + } + + createdUnstructured, err := dynamicClient.Resource(pipelineRunGVR).Namespace(namespace).Create(params.Context, &unstructured.Unstructured{Object: unstructuredObj}, metav1.CreateOptions{}) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to create PipelineRun for Pipeline %s/%s: %w", namespace, name, err)), nil + } + + createdName := createdUnstructured.GetName() + return api.NewToolCallResult(fmt.Sprintf("Pipeline '%s' started as PipelineRun '%s' in namespace '%s'", name, createdName, namespace), nil), nil +} diff --git a/pkg/toolsets/tekton/pipelinerun.go b/pkg/toolsets/tekton/pipelinerun.go new file mode 100644 index 000000000..dd4ee4466 --- /dev/null +++ b/pkg/toolsets/tekton/pipelinerun.go @@ -0,0 +1,97 @@ +package tekton + +import ( + "fmt" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/google/jsonschema-go/jsonschema" + tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" +) + +func pipelineRunTools() []api.ServerTool { + return []api.ServerTool{ + { + Tool: api.Tool{ + Name: "tekton_pipelinerun_restart", + Description: "Restart a Tekton PipelineRun by creating a new PipelineRun with the same spec", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "name": { + Type: "string", + Description: "Name of the PipelineRun to restart", + }, + "namespace": { + Type: "string", + Description: "Namespace of the PipelineRun", + }, + }, + Required: []string{"name"}, + }, + Annotations: api.ToolAnnotations{ + Title: "Tekton: Restart PipelineRun", + ReadOnlyHint: ptr.To(false), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(false), + OpenWorldHint: ptr.To(false), + }, + }, + Handler: restartPipelineRun, + }, + } +} + +func restartPipelineRun(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + name, err := api.RequiredString(params, "name") + if err != nil { + return api.NewToolCallResult("", err), nil + } + namespace := api.OptionalString(params, "namespace", params.NamespaceOrDefault("")) + + dynamicClient := params.DynamicClient() + + existingUnstructured, err := dynamicClient.Resource(pipelineRunGVR).Namespace(namespace).Get(params.Context, name, metav1.GetOptions{}) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get PipelineRun %s/%s: %w", namespace, name, err)), nil + } + + // Convert to typed object to manipulate + var existing tektonv1.PipelineRun + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(existingUnstructured.Object, &existing); err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to convert PipelineRun from unstructured: %w", err)), nil + } + + newPR := &tektonv1.PipelineRun{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "tekton.dev/v1", + Kind: "PipelineRun", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + GenerateName: name + "-", + }, + Spec: existing.Spec, + } + newPR.Spec.Status = "" + if existing.GenerateName != "" { + newPR.GenerateName = existing.GenerateName + } + + // Convert to unstructured + unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(newPR) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to convert PipelineRun to unstructured: %w", err)), nil + } + + createdUnstructured, err := dynamicClient.Resource(pipelineRunGVR).Namespace(namespace).Create(params.Context, &unstructured.Unstructured{Object: unstructuredObj}, metav1.CreateOptions{}) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to create restart PipelineRun for %s/%s: %w", namespace, name, err)), nil + } + + createdName := createdUnstructured.GetName() + return api.NewToolCallResult(fmt.Sprintf("PipelineRun '%s' restarted as '%s' in namespace '%s'", name, createdName, namespace), nil), nil +} diff --git a/pkg/toolsets/tekton/task.go b/pkg/toolsets/tekton/task.go new file mode 100644 index 000000000..cf6849416 --- /dev/null +++ b/pkg/toolsets/tekton/task.go @@ -0,0 +1,106 @@ +package tekton + +import ( + "fmt" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/google/jsonschema-go/jsonschema" + tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" +) + +func taskTools() []api.ServerTool { + return []api.ServerTool{ + { + Tool: api.Tool{ + Name: "tekton_task_start", + Description: "Start a Tekton Task by creating a TaskRun that references it", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "name": { + Type: "string", + Description: "Name of the Task to start", + }, + "namespace": { + Type: "string", + Description: "Namespace of the Task", + }, + "params": { + Type: "object", + Description: "Parameter values to pass to the Task. Keys are parameter names; values can be a string, an array of strings, or an object (map of string to string) depending on the parameter type defined in the Task spec", + Properties: make(map[string]*jsonschema.Schema), + AdditionalProperties: emptySchema, + }, + }, + Required: []string{"name"}, + }, + Annotations: api.ToolAnnotations{ + Title: "Tekton: Start Task", + ReadOnlyHint: ptr.To(false), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(false), + OpenWorldHint: ptr.To(false), + }, + }, + Handler: startTask, + }, + } +} + +func startTask(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + name, err := api.RequiredString(params, "name") + if err != nil { + return api.NewToolCallResult("", err), nil + } + namespace := api.OptionalString(params, "namespace", params.NamespaceOrDefault("")) + + dynamicClient := params.DynamicClient() + + // Verify that the Task exists + if _, err := dynamicClient.Resource(taskGVR).Namespace(namespace).Get(params.Context, name, metav1.GetOptions{}); err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get Task %s/%s: %w", namespace, name, err)), nil + } + + var tektonParams []tektonv1.Param + if rawParams, ok := params.GetArguments()["params"].(map[string]interface{}); ok { + tektonParams, err = parseParams(rawParams) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to parse params: %w", err)), nil + } + } + + tr := &tektonv1.TaskRun{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "tekton.dev/v1", + Kind: "TaskRun", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + GenerateName: name + "-", + }, + Spec: tektonv1.TaskRunSpec{ + TaskRef: &tektonv1.TaskRef{ + Name: name, + }, + Params: tektonParams, + }, + } + + // Convert to unstructured + unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tr) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to convert TaskRun to unstructured: %w", err)), nil + } + + createdUnstructured, err := dynamicClient.Resource(taskRunGVR).Namespace(namespace).Create(params.Context, &unstructured.Unstructured{Object: unstructuredObj}, metav1.CreateOptions{}) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to create TaskRun for Task %s/%s: %w", namespace, name, err)), nil + } + + createdName := createdUnstructured.GetName() + return api.NewToolCallResult(fmt.Sprintf("Task '%s' started as TaskRun '%s' in namespace '%s'", name, createdName, namespace), nil), nil +} diff --git a/pkg/toolsets/tekton/taskrun.go b/pkg/toolsets/tekton/taskrun.go new file mode 100644 index 000000000..44f82d41b --- /dev/null +++ b/pkg/toolsets/tekton/taskrun.go @@ -0,0 +1,213 @@ +package tekton + +import ( + "fmt" + "io" + "strings" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" + "github.com/google/jsonschema-go/jsonschema" + tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" +) + +const maxLogBytesPerContainer = 1 << 20 // 1 MiB + +func taskRunTools() []api.ServerTool { + return []api.ServerTool{ + { + Tool: api.Tool{ + Name: "tekton_taskrun_restart", + Description: "Restart a Tekton TaskRun by creating a new TaskRun with the same spec", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "name": { + Type: "string", + Description: "Name of the TaskRun to restart", + }, + "namespace": { + Type: "string", + Description: "Namespace of the TaskRun", + }, + }, + Required: []string{"name"}, + }, + Annotations: api.ToolAnnotations{ + Title: "Tekton: Restart TaskRun", + ReadOnlyHint: ptr.To(false), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(false), + OpenWorldHint: ptr.To(false), + }, + }, + Handler: restartTaskRun, + }, + { + Tool: api.Tool{ + Name: "tekton_taskrun_logs", + Description: "Get the logs from a Tekton TaskRun by resolving its underlying pod", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "name": { + Type: "string", + Description: "Name of the TaskRun to get logs from", + }, + "namespace": { + Type: "string", + Description: "Namespace of the TaskRun", + }, + "tail": { + Type: "integer", + Description: "Number of lines to retrieve from the end of the logs (Optional, default: 100)", + Default: api.ToRawMessage(kubernetes.DefaultTailLines), + Minimum: ptr.To(float64(0)), + }, + }, + Required: []string{"name"}, + }, + Annotations: api.ToolAnnotations{ + Title: "Tekton: Get TaskRun Logs", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(true), + OpenWorldHint: ptr.To(false), + }, + }, + Handler: getTaskRunLogs, + }, + } +} + +func restartTaskRun(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + name, err := api.RequiredString(params, "name") + if err != nil { + return api.NewToolCallResult("", err), nil + } + namespace := api.OptionalString(params, "namespace", params.NamespaceOrDefault("")) + + dynamicClient := params.DynamicClient() + + existingUnstructured, err := dynamicClient.Resource(taskRunGVR).Namespace(namespace).Get(params.Context, name, metav1.GetOptions{}) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get TaskRun %s/%s: %w", namespace, name, err)), nil + } + + // Convert to typed object to manipulate + var existing tektonv1.TaskRun + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(existingUnstructured.Object, &existing); err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to convert TaskRun from unstructured: %w", err)), nil + } + + newTR := &tektonv1.TaskRun{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "tekton.dev/v1", + Kind: "TaskRun", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + GenerateName: name + "-", + }, + Spec: existing.Spec, + } + newTR.Spec.Status = "" + if existing.GenerateName != "" { + newTR.GenerateName = existing.GenerateName + } + + // Convert to unstructured + unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(newTR) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to convert TaskRun to unstructured: %w", err)), nil + } + + createdUnstructured, err := dynamicClient.Resource(taskRunGVR).Namespace(namespace).Create(params.Context, &unstructured.Unstructured{Object: unstructuredObj}, metav1.CreateOptions{}) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to create restart TaskRun for %s/%s: %w", namespace, name, err)), nil + } + + createdName := createdUnstructured.GetName() + return api.NewToolCallResult(fmt.Sprintf("TaskRun '%s' restarted as '%s' in namespace '%s'", name, createdName, namespace), nil), nil +} + +func getTaskRunLogs(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + name, err := api.RequiredString(params, "name") + if err != nil { + return api.NewToolCallResult("", err), nil + } + namespace := api.OptionalString(params, "namespace", params.NamespaceOrDefault("")) + + tail := params.GetArguments()["tail"] + var tailInt int64 + if tail != nil { + var err error + tailInt, err = api.ParseInt64(tail) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to parse tail parameter: %w", err)), nil + } + } else { + tailInt = kubernetes.DefaultTailLines + } + + dynamicClient := params.DynamicClient() + + trUnstructured, err := dynamicClient.Resource(taskRunGVR).Namespace(namespace).Get(params.Context, name, metav1.GetOptions{}) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get TaskRun %s/%s: %w", namespace, name, err)), nil + } + + // Convert to typed object to access status + var tr tektonv1.TaskRun + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(trUnstructured.Object, &tr); err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to convert TaskRun from unstructured: %w", err)), nil + } + + if tr.Status.PodName == "" { + return api.NewToolCallResult(fmt.Sprintf("TaskRun '%s' in namespace '%s' has not started a pod yet", name, namespace), nil), nil + } + + var sb strings.Builder + + for _, step := range tr.Status.Steps { + collectContainerLogs(params, &sb, tr.Status.PodName, namespace, "step", step.Name, step.Container, tailInt) + } + for _, sidecar := range tr.Status.Sidecars { + collectContainerLogs(params, &sb, tr.Status.PodName, namespace, "sidecar", sidecar.Name, sidecar.Container, tailInt) + } + + if sb.Len() == 0 { + return api.NewToolCallResult(fmt.Sprintf("No logs available for TaskRun '%s' in namespace '%s'", name, namespace), nil), nil + } + + return api.NewToolCallResult(sb.String(), nil), nil +} + +func collectContainerLogs(params api.ToolHandlerParams, sb *strings.Builder, podName, namespace, kind, name, container string, tailLines int64) { + req := params.CoreV1().Pods(namespace).GetLogs(podName, &corev1.PodLogOptions{ + Container: container, + TailLines: &tailLines, + }) + stream, err := req.Stream(params.Context) + if err != nil { + fmt.Fprintf(sb, "[%s: %s] error retrieving logs: %v\n", kind, name, err) + return + } + defer func() { + _ = stream.Close() + }() + + bytes, err := io.ReadAll(io.LimitReader(stream, maxLogBytesPerContainer)) + if err != nil { + fmt.Fprintf(sb, "[%s: %s] error reading logs: %v\n", kind, name, err) + return + } + if len(bytes) > 0 { + fmt.Fprintf(sb, "[%s: %s]\n%s\n", kind, name, string(bytes)) + } +} diff --git a/pkg/toolsets/tekton/tekton_test.go b/pkg/toolsets/tekton/tekton_test.go new file mode 100644 index 000000000..1300b278e --- /dev/null +++ b/pkg/toolsets/tekton/tekton_test.go @@ -0,0 +1,25 @@ +package tekton_test + +import ( + "testing" + + "github.com/containers/kubernetes-mcp-server/pkg/toolsets/tekton" + "github.com/stretchr/testify/suite" +) + +type TektonSuite struct { + suite.Suite +} + +func TestTekton(t *testing.T) { + suite.Run(t, new(TektonSuite)) +} + +func (s *TektonSuite) TestToolset() { + ts := &tekton.Toolset{} + s.Equal("tekton", ts.GetName()) + s.NotEmpty(ts.GetDescription()) + tools := ts.GetTools(nil) + s.NotEmpty(tools) + s.Nil(ts.GetPrompts()) +} diff --git a/pkg/toolsets/tekton/toolset.go b/pkg/toolsets/tekton/toolset.go new file mode 100644 index 000000000..30182d42d --- /dev/null +++ b/pkg/toolsets/tekton/toolset.go @@ -0,0 +1,38 @@ +package tekton + +import ( + "slices" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/toolsets" +) + +// Toolset provides Tekton pipeline management tools. +type Toolset struct{} + +var _ api.Toolset = (*Toolset)(nil) + +func (t *Toolset) GetName() string { + return "tekton" +} + +func (t *Toolset) GetDescription() string { + return "Tekton pipeline management tools for Pipelines, PipelineRuns, Tasks, and TaskRuns." +} + +func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { + return slices.Concat( + pipelineTools(), + pipelineRunTools(), + taskTools(), + taskRunTools(), + ) +} + +func (t *Toolset) GetPrompts() []api.ServerPrompt { + return nil +} + +func init() { + toolsets.Register(&Toolset{}) +}