mirror of
https://github.com/openshift/openshift-mcp-server.git
synced 2025-10-17 14:27:48 +03:00
feat(mcp): toolset definitions completely agnostic from underlying MCP impl (#322)
Signed-off-by: Marc Nuri <marc@marcnuri.com>
This commit is contained in:
99
pkg/api/toolsets.go
Normal file
99
pkg/api/toolsets.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/output"
|
||||||
|
"github.com/google/jsonschema-go/jsonschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerTool struct {
|
||||||
|
Tool Tool
|
||||||
|
Handler ToolHandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toolset interface {
|
||||||
|
// GetName returns the name of the toolset.
|
||||||
|
// Used to identify the toolset in configuration, logs, and command-line arguments.
|
||||||
|
// Examples: "core", "metrics", "helm"
|
||||||
|
GetName() string
|
||||||
|
GetDescription() string
|
||||||
|
GetTools(k *internalk8s.Manager) []ServerTool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToolCallRequest interface {
|
||||||
|
GetArguments() map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToolCallResult struct {
|
||||||
|
// Raw content returned by the tool.
|
||||||
|
Content string
|
||||||
|
// Error (non-protocol) to send back to the LLM.
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewToolCallResult(content string, err error) *ToolCallResult {
|
||||||
|
return &ToolCallResult{
|
||||||
|
Content: content,
|
||||||
|
Error: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToolHandlerParams struct {
|
||||||
|
context.Context
|
||||||
|
*internalk8s.Kubernetes
|
||||||
|
ToolCallRequest
|
||||||
|
ListOutput output.Output
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToolHandlerFunc func(params ToolHandlerParams) (*ToolCallResult, error)
|
||||||
|
|
||||||
|
type Tool struct {
|
||||||
|
// The name of the tool.
|
||||||
|
// Intended for programmatic or logical use, but used as a display name in past
|
||||||
|
// specs or fallback (if title isn't present).
|
||||||
|
Name string `json:"name"`
|
||||||
|
// A human-readable description of the tool.
|
||||||
|
//
|
||||||
|
// This can be used by clients to improve the LLM's understanding of available
|
||||||
|
// tools. It can be thought of like a "hint" to the model.
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
// Additional tool information.
|
||||||
|
Annotations ToolAnnotations `json:"annotations"`
|
||||||
|
// A JSON Schema object defining the expected parameters for the tool.
|
||||||
|
InputSchema *jsonschema.Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToolAnnotations struct {
|
||||||
|
// Human-readable title for the tool
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
// If true, the tool does not modify its environment.
|
||||||
|
ReadOnlyHint *bool `json:"readOnlyHint,omitempty"`
|
||||||
|
// If true, the tool may perform destructive updates to its environment. If
|
||||||
|
// false, the tool performs only additive updates.
|
||||||
|
//
|
||||||
|
// (This property is meaningful only when ReadOnlyHint == false.)
|
||||||
|
DestructiveHint *bool `json:"destructiveHint,omitempty"`
|
||||||
|
// If true, calling the tool repeatedly with the same arguments will have no
|
||||||
|
// additional effect on its environment.
|
||||||
|
//
|
||||||
|
// (This property is meaningful only when ReadOnlyHint == false.)
|
||||||
|
IdempotentHint *bool `json:"idempotentHint,omitempty"`
|
||||||
|
// If true, this tool may interact with an "open world" of external entities. If
|
||||||
|
// false, the tool's domain of interaction is closed. For example, the world of
|
||||||
|
// a web search tool is open, whereas that of a memory tool is not.
|
||||||
|
OpenWorldHint *bool `json:"openWorldHint,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToRawMessage(v any) json.RawMessage {
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containers/kubernetes-mcp-server/internal/test"
|
"github.com/containers/kubernetes-mcp-server/internal/test"
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/toolsets"
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/coreos/go-oidc/v3/oidc/oidctest"
|
"github.com/coreos/go-oidc/v3/oidc/oidctest"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
@@ -87,7 +88,7 @@ func (c *httpContext) beforeEach(t *testing.T) {
|
|||||||
}
|
}
|
||||||
c.StaticConfig.Port = fmt.Sprintf("%d", ln.Addr().(*net.TCPAddr).Port)
|
c.StaticConfig.Port = fmt.Sprintf("%d", ln.Addr().(*net.TCPAddr).Port)
|
||||||
mcpServer, err := mcp.NewServer(mcp.Configuration{
|
mcpServer, err := mcp.NewServer(mcp.Configuration{
|
||||||
Toolset: mcp.Toolsets()[0],
|
Toolset: toolsets.Toolsets()[0],
|
||||||
StaticConfig: c.StaticConfig,
|
StaticConfig: c.StaticConfig,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/toolsets"
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
@@ -115,7 +116,7 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command {
|
|||||||
cmd.Flags().StringVar(&o.Port, "port", o.Port, "Start a streamable HTTP and SSE HTTP server on the specified port (e.g. 8080)")
|
cmd.Flags().StringVar(&o.Port, "port", o.Port, "Start a streamable HTTP and SSE HTTP server on the specified port (e.g. 8080)")
|
||||||
cmd.Flags().StringVar(&o.SSEBaseUrl, "sse-base-url", o.SSEBaseUrl, "SSE public base URL to use when sending the endpoint message (e.g. https://example.com)")
|
cmd.Flags().StringVar(&o.SSEBaseUrl, "sse-base-url", o.SSEBaseUrl, "SSE public base URL to use when sending the endpoint message (e.g. https://example.com)")
|
||||||
cmd.Flags().StringVar(&o.Kubeconfig, "kubeconfig", o.Kubeconfig, "Path to the kubeconfig file to use for authentication")
|
cmd.Flags().StringVar(&o.Kubeconfig, "kubeconfig", o.Kubeconfig, "Path to the kubeconfig file to use for authentication")
|
||||||
cmd.Flags().StringVar(&o.Toolset, "toolset", o.Toolset, "MCP toolset to use (one of: "+strings.Join(mcp.ToolsetNames(), ", ")+")")
|
cmd.Flags().StringVar(&o.Toolset, "toolset", o.Toolset, "MCP toolset to use (one of: "+strings.Join(toolsets.ToolsetNames(), ", ")+")")
|
||||||
cmd.Flags().StringVar(&o.ListOutput, "list-output", o.ListOutput, "Output format for resource list operations (one of: "+strings.Join(output.Names, ", ")+"). Defaults to table.")
|
cmd.Flags().StringVar(&o.ListOutput, "list-output", o.ListOutput, "Output format for resource list operations (one of: "+strings.Join(output.Names, ", ")+"). Defaults to table.")
|
||||||
cmd.Flags().BoolVar(&o.ReadOnly, "read-only", o.ReadOnly, "If true, only tools annotated with readOnlyHint=true are exposed")
|
cmd.Flags().BoolVar(&o.ReadOnly, "read-only", o.ReadOnly, "If true, only tools annotated with readOnlyHint=true are exposed")
|
||||||
cmd.Flags().BoolVar(&o.DisableDestructive, "disable-destructive", o.DisableDestructive, "If true, tools annotated with destructiveHint=true are disabled")
|
cmd.Flags().BoolVar(&o.DisableDestructive, "disable-destructive", o.DisableDestructive, "If true, tools annotated with destructiveHint=true are disabled")
|
||||||
@@ -237,9 +238,9 @@ func (m *MCPServerOptions) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *MCPServerOptions) Run() error {
|
func (m *MCPServerOptions) Run() error {
|
||||||
toolset := mcp.ToolsetFromString(m.Toolset)
|
toolset := toolsets.ToolsetFromString(m.Toolset)
|
||||||
if toolset == nil {
|
if toolset == nil {
|
||||||
return fmt.Errorf("invalid toolset name: %s, valid names are: %s", m.Toolset, strings.Join(mcp.ToolsetNames(), ", "))
|
return fmt.Errorf("invalid toolset name: %s, valid names are: %s", m.Toolset, strings.Join(toolsets.ToolsetNames(), ", "))
|
||||||
}
|
}
|
||||||
listOutput := output.FromString(m.StaticConfig.ListOutput)
|
listOutput := output.FromString(m.StaticConfig.ListOutput)
|
||||||
if listOutput == nil {
|
if listOutput == nil {
|
||||||
|
|||||||
@@ -81,24 +81,24 @@ func (m *Manager) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
|||||||
return m.clientCmdConfig
|
return m.clientCmdConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) ConfigurationView(minify bool) (runtime.Object, error) {
|
func (k *Kubernetes) ConfigurationView(minify bool) (runtime.Object, error) {
|
||||||
var cfg clientcmdapi.Config
|
var cfg clientcmdapi.Config
|
||||||
var err error
|
var err error
|
||||||
if m.IsInCluster() {
|
if k.manager.IsInCluster() {
|
||||||
cfg = *clientcmdapi.NewConfig()
|
cfg = *clientcmdapi.NewConfig()
|
||||||
cfg.Clusters["cluster"] = &clientcmdapi.Cluster{
|
cfg.Clusters["cluster"] = &clientcmdapi.Cluster{
|
||||||
Server: m.cfg.Host,
|
Server: k.manager.cfg.Host,
|
||||||
InsecureSkipTLSVerify: m.cfg.Insecure,
|
InsecureSkipTLSVerify: k.manager.cfg.Insecure,
|
||||||
}
|
}
|
||||||
cfg.AuthInfos["user"] = &clientcmdapi.AuthInfo{
|
cfg.AuthInfos["user"] = &clientcmdapi.AuthInfo{
|
||||||
Token: m.cfg.BearerToken,
|
Token: k.manager.cfg.BearerToken,
|
||||||
}
|
}
|
||||||
cfg.Contexts["context"] = &clientcmdapi.Context{
|
cfg.Contexts["context"] = &clientcmdapi.Context{
|
||||||
Cluster: "cluster",
|
Cluster: "cluster",
|
||||||
AuthInfo: "user",
|
AuthInfo: "user",
|
||||||
}
|
}
|
||||||
cfg.CurrentContext = "context"
|
cfg.CurrentContext = "context"
|
||||||
} else if cfg, err = m.clientCmdConfig.RawConfig(); err != nil {
|
} else if cfg, err = k.manager.clientCmdConfig.RawConfig(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if minify {
|
if minify {
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containers/kubernetes-mcp-server/pkg/config"
|
|
||||||
"github.com/containers/kubernetes-mcp-server/pkg/output"
|
|
||||||
"github.com/mark3labs/mcp-go/client"
|
"github.com/mark3labs/mcp-go/client"
|
||||||
"github.com/mark3labs/mcp-go/client/transport"
|
"github.com/mark3labs/mcp-go/client/transport"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
@@ -32,7 +30,7 @@ import (
|
|||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
"k8s.io/client-go/tools/clientcmd/api"
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
toolswatch "k8s.io/client-go/tools/watch"
|
toolswatch "k8s.io/client-go/tools/watch"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
"k8s.io/klog/v2/textlogger"
|
"k8s.io/klog/v2/textlogger"
|
||||||
@@ -43,6 +41,11 @@ import (
|
|||||||
"sigs.k8s.io/controller-runtime/tools/setup-envtest/store"
|
"sigs.k8s.io/controller-runtime/tools/setup-envtest/store"
|
||||||
"sigs.k8s.io/controller-runtime/tools/setup-envtest/versions"
|
"sigs.k8s.io/controller-runtime/tools/setup-envtest/versions"
|
||||||
"sigs.k8s.io/controller-runtime/tools/setup-envtest/workflows"
|
"sigs.k8s.io/controller-runtime/tools/setup-envtest/workflows"
|
||||||
|
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/api"
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/config"
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/output"
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/toolsets/full"
|
||||||
)
|
)
|
||||||
|
|
||||||
// envTest has an expensive setup, so we only want to do it once per entire test run.
|
// envTest has an expensive setup, so we only want to do it once per entire test run.
|
||||||
@@ -103,7 +106,7 @@ func TestMain(m *testing.M) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type mcpContext struct {
|
type mcpContext struct {
|
||||||
toolset Toolset
|
toolset api.Toolset
|
||||||
listOutput output.Output
|
listOutput output.Output
|
||||||
logLevel int
|
logLevel int
|
||||||
|
|
||||||
@@ -127,7 +130,7 @@ func (c *mcpContext) beforeEach(t *testing.T) {
|
|||||||
c.tempDir = t.TempDir()
|
c.tempDir = t.TempDir()
|
||||||
c.withKubeConfig(nil)
|
c.withKubeConfig(nil)
|
||||||
if c.toolset == nil {
|
if c.toolset == nil {
|
||||||
c.toolset = &Full{}
|
c.toolset = &full.Full{}
|
||||||
}
|
}
|
||||||
if c.listOutput == nil {
|
if c.listOutput == nil {
|
||||||
c.listOutput = output.Yaml
|
c.listOutput = output.Yaml
|
||||||
@@ -188,7 +191,7 @@ func (c *mcpContext) afterEach() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testCase(t *testing.T, test func(c *mcpContext)) {
|
func testCase(t *testing.T, test func(c *mcpContext)) {
|
||||||
testCaseWithContext(t, &mcpContext{toolset: &Full{}}, test)
|
testCaseWithContext(t, &mcpContext{toolset: &full.Full{}}, test)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCaseWithContext(t *testing.T, mcpCtx *mcpContext, test func(c *mcpContext)) {
|
func testCaseWithContext(t *testing.T, mcpCtx *mcpContext, test func(c *mcpContext)) {
|
||||||
@@ -198,23 +201,23 @@ func testCaseWithContext(t *testing.T, mcpCtx *mcpContext, test func(c *mcpConte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// withKubeConfig sets up a fake kubeconfig in the temp directory based on the provided rest.Config
|
// withKubeConfig sets up a fake kubeconfig in the temp directory based on the provided rest.Config
|
||||||
func (c *mcpContext) withKubeConfig(rc *rest.Config) *api.Config {
|
func (c *mcpContext) withKubeConfig(rc *rest.Config) *clientcmdapi.Config {
|
||||||
fakeConfig := api.NewConfig()
|
fakeConfig := clientcmdapi.NewConfig()
|
||||||
fakeConfig.Clusters["fake"] = api.NewCluster()
|
fakeConfig.Clusters["fake"] = clientcmdapi.NewCluster()
|
||||||
fakeConfig.Clusters["fake"].Server = "https://127.0.0.1:6443"
|
fakeConfig.Clusters["fake"].Server = "https://127.0.0.1:6443"
|
||||||
fakeConfig.Clusters["additional-cluster"] = api.NewCluster()
|
fakeConfig.Clusters["additional-cluster"] = clientcmdapi.NewCluster()
|
||||||
fakeConfig.AuthInfos["fake"] = api.NewAuthInfo()
|
fakeConfig.AuthInfos["fake"] = clientcmdapi.NewAuthInfo()
|
||||||
fakeConfig.AuthInfos["additional-auth"] = api.NewAuthInfo()
|
fakeConfig.AuthInfos["additional-auth"] = clientcmdapi.NewAuthInfo()
|
||||||
if rc != nil {
|
if rc != nil {
|
||||||
fakeConfig.Clusters["fake"].Server = rc.Host
|
fakeConfig.Clusters["fake"].Server = rc.Host
|
||||||
fakeConfig.Clusters["fake"].CertificateAuthorityData = rc.CAData
|
fakeConfig.Clusters["fake"].CertificateAuthorityData = rc.CAData
|
||||||
fakeConfig.AuthInfos["fake"].ClientKeyData = rc.KeyData
|
fakeConfig.AuthInfos["fake"].ClientKeyData = rc.KeyData
|
||||||
fakeConfig.AuthInfos["fake"].ClientCertificateData = rc.CertData
|
fakeConfig.AuthInfos["fake"].ClientCertificateData = rc.CertData
|
||||||
}
|
}
|
||||||
fakeConfig.Contexts["fake-context"] = api.NewContext()
|
fakeConfig.Contexts["fake-context"] = clientcmdapi.NewContext()
|
||||||
fakeConfig.Contexts["fake-context"].Cluster = "fake"
|
fakeConfig.Contexts["fake-context"].Cluster = "fake"
|
||||||
fakeConfig.Contexts["fake-context"].AuthInfo = "fake"
|
fakeConfig.Contexts["fake-context"].AuthInfo = "fake"
|
||||||
fakeConfig.Contexts["additional-context"] = api.NewContext()
|
fakeConfig.Contexts["additional-context"] = clientcmdapi.NewContext()
|
||||||
fakeConfig.Contexts["additional-context"].Cluster = "additional-cluster"
|
fakeConfig.Contexts["additional-context"].Cluster = "additional-cluster"
|
||||||
fakeConfig.Contexts["additional-context"].AuthInfo = "additional-auth"
|
fakeConfig.Contexts["additional-context"].AuthInfo = "additional-auth"
|
||||||
fakeConfig.CurrentContext = "fake-context"
|
fakeConfig.CurrentContext = "fake-context"
|
||||||
|
|||||||
54
pkg/mcp/m3labs.go
Normal file
54
pkg/mcp/m3labs.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package mcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ServerToolToM3LabsServerTool(s *Server, tools []api.ServerTool) ([]server.ServerTool, error) {
|
||||||
|
m3labTools := make([]server.ServerTool, 0)
|
||||||
|
for _, tool := range tools {
|
||||||
|
m3labTool := mcp.Tool{
|
||||||
|
Name: tool.Tool.Name,
|
||||||
|
Description: tool.Tool.Description,
|
||||||
|
Annotations: mcp.ToolAnnotation{
|
||||||
|
Title: tool.Tool.Annotations.Title,
|
||||||
|
ReadOnlyHint: tool.Tool.Annotations.ReadOnlyHint,
|
||||||
|
DestructiveHint: tool.Tool.Annotations.DestructiveHint,
|
||||||
|
IdempotentHint: tool.Tool.Annotations.IdempotentHint,
|
||||||
|
OpenWorldHint: tool.Tool.Annotations.OpenWorldHint,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if tool.Tool.InputSchema != nil {
|
||||||
|
schema, err := json.Marshal(tool.Tool.InputSchema)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal tool input schema for tool %s: %v", tool.Tool.Name, err)
|
||||||
|
}
|
||||||
|
m3labTool.RawInputSchema = schema
|
||||||
|
}
|
||||||
|
m3labHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
k, err := s.k.Derived(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result, err := tool.Handler(api.ToolHandlerParams{
|
||||||
|
Context: ctx,
|
||||||
|
Kubernetes: k,
|
||||||
|
ToolCallRequest: request,
|
||||||
|
ListOutput: s.configuration.ListOutput,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewTextResult(result.Content, result.Error), nil
|
||||||
|
}
|
||||||
|
m3labTools = append(m3labTools, server.ServerTool{Tool: m3labTool, Handler: m3labHandler})
|
||||||
|
}
|
||||||
|
return m3labTools, nil
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/api"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/mark3labs/mcp-go/server"
|
"github.com/mark3labs/mcp-go/server"
|
||||||
authenticationapiv1 "k8s.io/api/authentication/v1"
|
authenticationapiv1 "k8s.io/api/authentication/v1"
|
||||||
@@ -24,13 +25,13 @@ type ContextKey string
|
|||||||
const TokenScopesContextKey = ContextKey("TokenScopesContextKey")
|
const TokenScopesContextKey = ContextKey("TokenScopesContextKey")
|
||||||
|
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
Toolset Toolset
|
Toolset api.Toolset
|
||||||
ListOutput output.Output
|
ListOutput output.Output
|
||||||
|
|
||||||
StaticConfig *config.StaticConfig
|
StaticConfig *config.StaticConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Configuration) isToolApplicable(tool ServerTool) bool {
|
func (c *Configuration) isToolApplicable(tool api.ServerTool) bool {
|
||||||
if c.StaticConfig.ReadOnly && !ptr.Deref(tool.Tool.Annotations.ReadOnlyHint, false) {
|
if c.StaticConfig.ReadOnly && !ptr.Deref(tool.Tool.Annotations.ReadOnlyHint, false) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -88,15 +89,15 @@ func (s *Server) reloadKubernetesClient() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.k = k
|
s.k = k
|
||||||
applicableTools := make([]ServerTool, 0)
|
applicableTools := make([]api.ServerTool, 0)
|
||||||
for _, tool := range s.configuration.Toolset.GetTools(s) {
|
for _, tool := range s.configuration.Toolset.GetTools(s.k) {
|
||||||
if !s.configuration.isToolApplicable(tool) {
|
if !s.configuration.isToolApplicable(tool) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
applicableTools = append(applicableTools, tool)
|
applicableTools = append(applicableTools, tool)
|
||||||
s.enabledTools = append(s.enabledTools, tool.Tool.Name)
|
s.enabledTools = append(s.enabledTools, tool.Tool.Name)
|
||||||
}
|
}
|
||||||
m3labsServerTools, err := ServerToolToM3LabsServerTool(applicableTools)
|
m3labsServerTools, err := ServerToolToM3LabsServerTool(s, applicableTools)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to convert tools: %v", err)
|
return fmt.Errorf("failed to convert tools: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ func TestToolCallLogging(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
sensitiveHeaders := []string{
|
sensitiveHeaders := []string{
|
||||||
"Authorization",
|
"Authorization:",
|
||||||
// TODO: Add more sensitive headers as needed
|
// TODO: Add more sensitive headers as needed
|
||||||
}
|
}
|
||||||
t.Run("Does not log sensitive headers", func(t *testing.T) {
|
t.Run("Does not log sensitive headers", func(t *testing.T) {
|
||||||
|
|||||||
3
pkg/mcp/modules.go
Normal file
3
pkg/mcp/modules.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package mcp
|
||||||
|
|
||||||
|
import _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/full"
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package mcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/google/jsonschema-go/jsonschema"
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
|
||||||
"k8s.io/utils/ptr"
|
|
||||||
|
|
||||||
"github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Server) initNamespaces() []ServerTool {
|
|
||||||
ret := make([]ServerTool, 0)
|
|
||||||
ret = append(ret, ServerTool{
|
|
||||||
Tool: Tool{
|
|
||||||
Name: "namespaces_list",
|
|
||||||
Description: "List all the Kubernetes namespaces in the current cluster",
|
|
||||||
InputSchema: &jsonschema.Schema{
|
|
||||||
Type: "object",
|
|
||||||
},
|
|
||||||
Annotations: ToolAnnotations{
|
|
||||||
Title: "Namespaces: List",
|
|
||||||
ReadOnlyHint: ptr.To(true),
|
|
||||||
DestructiveHint: ptr.To(false),
|
|
||||||
IdempotentHint: ptr.To(false),
|
|
||||||
OpenWorldHint: ptr.To(true),
|
|
||||||
},
|
|
||||||
}, Handler: s.namespacesList,
|
|
||||||
})
|
|
||||||
if s.k.IsOpenShift(context.Background()) {
|
|
||||||
ret = append(ret, ServerTool{
|
|
||||||
Tool: Tool{
|
|
||||||
Name: "projects_list",
|
|
||||||
Description: "List all the OpenShift projects in the current cluster",
|
|
||||||
InputSchema: &jsonschema.Schema{
|
|
||||||
Type: "object",
|
|
||||||
},
|
|
||||||
Annotations: ToolAnnotations{
|
|
||||||
Title: "Projects: List",
|
|
||||||
ReadOnlyHint: ptr.To(true),
|
|
||||||
DestructiveHint: ptr.To(false),
|
|
||||||
IdempotentHint: ptr.To(false),
|
|
||||||
OpenWorldHint: ptr.To(true),
|
|
||||||
},
|
|
||||||
}, Handler: s.projectsList,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) namespacesList(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
derived, err := s.k.Derived(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ret, err := derived.NamespacesList(ctx, kubernetes.ResourceListOptions{AsTable: s.configuration.ListOutput.AsTable()})
|
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to list namespaces: %v", err)), nil
|
|
||||||
}
|
|
||||||
return NewTextResult(s.configuration.ListOutput.PrintObj(ret)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) projectsList(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
derived, err := s.k.Derived(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ret, err := derived.ProjectsList(ctx, kubernetes.ResourceListOptions{AsTable: s.configuration.ListOutput.AsTable()})
|
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to list projects: %v", err)), nil
|
|
||||||
}
|
|
||||||
return NewTextResult(s.configuration.ListOutput.PrintObj(ret)), nil
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
package mcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/google/jsonschema-go/jsonschema"
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
|
||||||
"github.com/mark3labs/mcp-go/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Toolset interface {
|
|
||||||
GetName() string
|
|
||||||
GetDescription() string
|
|
||||||
GetTools(s *Server) []ServerTool
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServerTool struct {
|
|
||||||
Tool Tool
|
|
||||||
Handler ToolHandlerFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToolHandlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)
|
|
||||||
|
|
||||||
type Tool struct {
|
|
||||||
// The name of the tool.
|
|
||||||
// Intended for programmatic or logical use, but used as a display name in past
|
|
||||||
// specs or fallback (if title isn't present).
|
|
||||||
Name string `json:"name"`
|
|
||||||
// A human-readable description of the tool.
|
|
||||||
//
|
|
||||||
// This can be used by clients to improve the LLM's understanding of available
|
|
||||||
// tools. It can be thought of like a "hint" to the model.
|
|
||||||
Description string `json:"description,omitempty"`
|
|
||||||
// Additional tool information.
|
|
||||||
Annotations ToolAnnotations `json:"annotations"`
|
|
||||||
// A JSON Schema object defining the expected parameters for the tool.
|
|
||||||
InputSchema *jsonschema.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToolAnnotations struct {
|
|
||||||
// Human-readable title for the tool
|
|
||||||
Title string `json:"title,omitempty"`
|
|
||||||
// If true, the tool does not modify its environment.
|
|
||||||
ReadOnlyHint *bool `json:"readOnlyHint,omitempty"`
|
|
||||||
// If true, the tool may perform destructive updates to its environment. If
|
|
||||||
// false, the tool performs only additive updates.
|
|
||||||
//
|
|
||||||
// (This property is meaningful only when ReadOnlyHint == false.)
|
|
||||||
DestructiveHint *bool `json:"destructiveHint,omitempty"`
|
|
||||||
// If true, calling the tool repeatedly with the same arguments will have no
|
|
||||||
// additional effect on its environment.
|
|
||||||
//
|
|
||||||
// (This property is meaningful only when ReadOnlyHint == false.)
|
|
||||||
IdempotentHint *bool `json:"idempotentHint,omitempty"`
|
|
||||||
// If true, this tool may interact with an "open world" of external entities. If
|
|
||||||
// false, the tool's domain of interaction is closed. For example, the world of
|
|
||||||
// a web search tool is open, whereas that of a memory tool is not.
|
|
||||||
OpenWorldHint *bool `json:"openWorldHint,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var toolsets []Toolset
|
|
||||||
|
|
||||||
func Register(toolset Toolset) {
|
|
||||||
toolsets = append(toolsets, toolset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Toolsets() []Toolset {
|
|
||||||
return toolsets
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToolsetNames() []string {
|
|
||||||
names := make([]string, 0)
|
|
||||||
for _, toolset := range Toolsets() {
|
|
||||||
names = append(names, toolset.GetName())
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToolsetFromString(name string) Toolset {
|
|
||||||
for _, toolset := range Toolsets() {
|
|
||||||
if toolset.GetName() == name {
|
|
||||||
return toolset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToRawMessage(v any) json.RawMessage {
|
|
||||||
if v == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
b, err := json.Marshal(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServerToolToM3LabsServerTool(tools []ServerTool) ([]server.ServerTool, error) {
|
|
||||||
m3labTools := make([]server.ServerTool, 0)
|
|
||||||
for _, tool := range tools {
|
|
||||||
m3labTool := mcp.Tool{
|
|
||||||
Name: tool.Tool.Name,
|
|
||||||
Description: tool.Tool.Description,
|
|
||||||
Annotations: mcp.ToolAnnotation{
|
|
||||||
Title: tool.Tool.Annotations.Title,
|
|
||||||
ReadOnlyHint: tool.Tool.Annotations.ReadOnlyHint,
|
|
||||||
DestructiveHint: tool.Tool.Annotations.DestructiveHint,
|
|
||||||
IdempotentHint: tool.Tool.Annotations.IdempotentHint,
|
|
||||||
OpenWorldHint: tool.Tool.Annotations.OpenWorldHint,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if tool.Tool.InputSchema != nil {
|
|
||||||
schema, err := json.Marshal(tool.Tool.InputSchema)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal tool input schema for tool %s: %v", tool.Tool.Name, err)
|
|
||||||
}
|
|
||||||
m3labTool.RawInputSchema = schema
|
|
||||||
}
|
|
||||||
m3labTools = append(m3labTools, server.ServerTool{Tool: m3labTool, Handler: server.ToolHandlerFunc(tool.Handler)})
|
|
||||||
}
|
|
||||||
return m3labTools, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Full struct{}
|
|
||||||
|
|
||||||
var _ Toolset = (*Full)(nil)
|
|
||||||
|
|
||||||
func (p *Full) GetName() string {
|
|
||||||
return "full"
|
|
||||||
}
|
|
||||||
func (p *Full) GetDescription() string {
|
|
||||||
return "Complete toolset with all tools and extended outputs"
|
|
||||||
}
|
|
||||||
func (p *Full) GetTools(s *Server) []ServerTool {
|
|
||||||
return slices.Concat(
|
|
||||||
s.initConfiguration(),
|
|
||||||
s.initEvents(),
|
|
||||||
s.initNamespaces(),
|
|
||||||
s.initPods(),
|
|
||||||
s.initResources(),
|
|
||||||
s.initHelm(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Register(&Full{})
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/toolsets/full"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -34,7 +35,7 @@ func TestFullToolsetTools(t *testing.T) {
|
|||||||
"resources_create_or_update",
|
"resources_create_or_update",
|
||||||
"resources_delete",
|
"resources_delete",
|
||||||
}
|
}
|
||||||
mcpCtx := &mcpContext{toolset: &Full{}}
|
mcpCtx := &mcpContext{toolset: &full.Full{}}
|
||||||
testCaseWithContext(t, mcpCtx, func(c *mcpContext) {
|
testCaseWithContext(t, mcpCtx, func(c *mcpContext) {
|
||||||
tools, err := c.mcpClient.ListTools(c.ctx, mcp.ListToolsRequest{})
|
tools, err := c.mcpClient.ListTools(c.ctx, mcp.ListToolsRequest{})
|
||||||
t.Run("ListTools returns tools", func(t *testing.T) {
|
t.Run("ListTools returns tools", func(t *testing.T) {
|
||||||
@@ -73,7 +74,7 @@ func TestFullToolsetTools(t *testing.T) {
|
|||||||
|
|
||||||
func TestFullToolsetToolsInOpenShift(t *testing.T) {
|
func TestFullToolsetToolsInOpenShift(t *testing.T) {
|
||||||
mcpCtx := &mcpContext{
|
mcpCtx := &mcpContext{
|
||||||
toolset: &Full{},
|
toolset: &full.Full{},
|
||||||
before: inOpenShift,
|
before: inOpenShift,
|
||||||
after: inOpenShiftClear,
|
after: inOpenShiftClear,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
package mcp
|
package full
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/jsonschema-go/jsonschema"
|
"github.com/google/jsonschema-go/jsonschema"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
|
||||||
"k8s.io/utils/ptr"
|
"k8s.io/utils/ptr"
|
||||||
|
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/api"
|
||||||
"github.com/containers/kubernetes-mcp-server/pkg/output"
|
"github.com/containers/kubernetes-mcp-server/pkg/output"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) initConfiguration() []ServerTool {
|
func initConfiguration() []api.ServerTool {
|
||||||
tools := []ServerTool{
|
tools := []api.ServerTool{
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "configuration_view",
|
Name: "configuration_view",
|
||||||
Description: "Get the current Kubernetes configuration content as a kubeconfig YAML",
|
Description: "Get the current Kubernetes configuration content as a kubeconfig YAML",
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -28,31 +27,31 @@ func (s *Server) initConfiguration() []ServerTool {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Configuration: View",
|
Title: "Configuration: View",
|
||||||
ReadOnlyHint: ptr.To(true),
|
ReadOnlyHint: ptr.To(true),
|
||||||
DestructiveHint: ptr.To(false),
|
DestructiveHint: ptr.To(false),
|
||||||
IdempotentHint: ptr.To(false),
|
IdempotentHint: ptr.To(false),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.configurationView},
|
}, Handler: configurationView},
|
||||||
}
|
}
|
||||||
return tools
|
return tools
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) configurationView(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func configurationView(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
minify := true
|
minify := true
|
||||||
minified := ctr.GetArguments()["minified"]
|
minified := params.GetArguments()["minified"]
|
||||||
if _, ok := minified.(bool); ok {
|
if _, ok := minified.(bool); ok {
|
||||||
minify = minified.(bool)
|
minify = minified.(bool)
|
||||||
}
|
}
|
||||||
ret, err := s.k.ConfigurationView(minify)
|
ret, err := params.ConfigurationView(minify)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return NewTextResult("", fmt.Errorf("failed to get configuration: %v", err)), nil
|
return api.NewToolCallResult("", fmt.Errorf("failed to get configuration: %v", err)), nil
|
||||||
}
|
}
|
||||||
configurationYaml, err := output.MarshalYaml(ret)
|
configurationYaml, err := output.MarshalYaml(ret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("failed to get configuration: %v", err)
|
err = fmt.Errorf("failed to get configuration: %v", err)
|
||||||
}
|
}
|
||||||
return NewTextResult(configurationYaml, err), nil
|
return api.NewToolCallResult(configurationYaml, err), nil
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
package mcp
|
package full
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/jsonschema-go/jsonschema"
|
"github.com/google/jsonschema-go/jsonschema"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
|
||||||
"k8s.io/utils/ptr"
|
"k8s.io/utils/ptr"
|
||||||
|
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/api"
|
||||||
"github.com/containers/kubernetes-mcp-server/pkg/output"
|
"github.com/containers/kubernetes-mcp-server/pkg/output"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) initEvents() []ServerTool {
|
func initEvents() []api.ServerTool {
|
||||||
return []ServerTool{
|
return []api.ServerTool{
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "events_list",
|
Name: "events_list",
|
||||||
Description: "List all the Kubernetes events in the current cluster from all namespaces",
|
Description: "List all the Kubernetes events in the current cluster from all namespaces",
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -25,36 +24,32 @@ func (s *Server) initEvents() []ServerTool {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Events: List",
|
Title: "Events: List",
|
||||||
ReadOnlyHint: ptr.To(true),
|
ReadOnlyHint: ptr.To(true),
|
||||||
DestructiveHint: ptr.To(false),
|
DestructiveHint: ptr.To(false),
|
||||||
IdempotentHint: ptr.To(false),
|
IdempotentHint: ptr.To(false),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.eventsList},
|
}, Handler: eventsList},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) eventsList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func eventsList(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
namespace := ctr.GetArguments()["namespace"]
|
namespace := params.GetArguments()["namespace"]
|
||||||
if namespace == nil {
|
if namespace == nil {
|
||||||
namespace = ""
|
namespace = ""
|
||||||
}
|
}
|
||||||
derived, err := s.k.Derived(ctx)
|
eventMap, err := params.EventsList(params, namespace.(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to list events in all namespaces: %v", err)), nil
|
||||||
}
|
|
||||||
eventMap, err := derived.EventsList(ctx, namespace.(string))
|
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to list events in all namespaces: %v", err)), nil
|
|
||||||
}
|
}
|
||||||
if len(eventMap) == 0 {
|
if len(eventMap) == 0 {
|
||||||
return NewTextResult("No events found", nil), nil
|
return api.NewToolCallResult("No events found", nil), nil
|
||||||
}
|
}
|
||||||
yamlEvents, err := output.MarshalYaml(eventMap)
|
yamlEvents, err := output.MarshalYaml(eventMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("failed to list events in all namespaces: %v", err)
|
err = fmt.Errorf("failed to list events in all namespaces: %v", err)
|
||||||
}
|
}
|
||||||
return NewTextResult(fmt.Sprintf("The following events (YAML format) were found:\n%s", yamlEvents), err), nil
|
return api.NewToolCallResult(fmt.Sprintf("The following events (YAML format) were found:\n%s", yamlEvents), err), nil
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
package mcp
|
package full
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/jsonschema-go/jsonschema"
|
"github.com/google/jsonschema-go/jsonschema"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
|
||||||
"k8s.io/utils/ptr"
|
"k8s.io/utils/ptr"
|
||||||
|
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) initHelm() []ServerTool {
|
func initHelm() []api.ServerTool {
|
||||||
return []ServerTool{
|
return []api.ServerTool{
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "helm_install",
|
Name: "helm_install",
|
||||||
Description: "Install a Helm chart in the current or provided namespace",
|
Description: "Install a Helm chart in the current or provided namespace",
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -37,15 +37,15 @@ func (s *Server) initHelm() []ServerTool {
|
|||||||
},
|
},
|
||||||
Required: []string{"chart"},
|
Required: []string{"chart"},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Helm: Install",
|
Title: "Helm: Install",
|
||||||
ReadOnlyHint: ptr.To(false),
|
ReadOnlyHint: ptr.To(false),
|
||||||
DestructiveHint: ptr.To(false),
|
DestructiveHint: ptr.To(false),
|
||||||
IdempotentHint: ptr.To(false), // TODO: consider replacing implementation with equivalent to: helm upgrade --install
|
IdempotentHint: ptr.To(false), // TODO: consider replacing implementation with equivalent to: helm upgrade --install
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.helmInstall},
|
}, Handler: helmInstall},
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "helm_list",
|
Name: "helm_list",
|
||||||
Description: "List all the Helm releases in the current or provided namespace (or in all namespaces if specified)",
|
Description: "List all the Helm releases in the current or provided namespace (or in all namespaces if specified)",
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -61,15 +61,15 @@ func (s *Server) initHelm() []ServerTool {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Helm: List",
|
Title: "Helm: List",
|
||||||
ReadOnlyHint: ptr.To(true),
|
ReadOnlyHint: ptr.To(true),
|
||||||
DestructiveHint: ptr.To(false),
|
DestructiveHint: ptr.To(false),
|
||||||
IdempotentHint: ptr.To(false),
|
IdempotentHint: ptr.To(false),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.helmList},
|
}, Handler: helmList},
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "helm_uninstall",
|
Name: "helm_uninstall",
|
||||||
Description: "Uninstall a Helm release in the current or provided namespace",
|
Description: "Uninstall a Helm release in the current or provided namespace",
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -86,83 +86,71 @@ func (s *Server) initHelm() []ServerTool {
|
|||||||
},
|
},
|
||||||
Required: []string{"name"},
|
Required: []string{"name"},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Helm: Uninstall",
|
Title: "Helm: Uninstall",
|
||||||
ReadOnlyHint: ptr.To(false),
|
ReadOnlyHint: ptr.To(false),
|
||||||
DestructiveHint: ptr.To(true),
|
DestructiveHint: ptr.To(true),
|
||||||
IdempotentHint: ptr.To(true),
|
IdempotentHint: ptr.To(true),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.helmUninstall},
|
}, Handler: helmUninstall},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) helmInstall(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func helmInstall(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
var chart string
|
var chart string
|
||||||
ok := false
|
ok := false
|
||||||
if chart, ok = ctr.GetArguments()["chart"].(string); !ok {
|
if chart, ok = params.GetArguments()["chart"].(string); !ok {
|
||||||
return NewTextResult("", fmt.Errorf("failed to install helm chart, missing argument chart")), nil
|
return api.NewToolCallResult("", fmt.Errorf("failed to install helm chart, missing argument chart")), nil
|
||||||
}
|
}
|
||||||
values := map[string]interface{}{}
|
values := map[string]interface{}{}
|
||||||
if v, ok := ctr.GetArguments()["values"].(map[string]interface{}); ok {
|
if v, ok := params.GetArguments()["values"].(map[string]interface{}); ok {
|
||||||
values = v
|
values = v
|
||||||
}
|
}
|
||||||
name := ""
|
name := ""
|
||||||
if v, ok := ctr.GetArguments()["name"].(string); ok {
|
if v, ok := params.GetArguments()["name"].(string); ok {
|
||||||
name = v
|
name = v
|
||||||
}
|
}
|
||||||
namespace := ""
|
namespace := ""
|
||||||
if v, ok := ctr.GetArguments()["namespace"].(string); ok {
|
if v, ok := params.GetArguments()["namespace"].(string); ok {
|
||||||
namespace = v
|
namespace = v
|
||||||
}
|
}
|
||||||
derived, err := s.k.Derived(ctx)
|
ret, err := params.NewHelm().Install(params, chart, values, name, namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to install helm chart '%s': %w", chart, err)), nil
|
||||||
}
|
}
|
||||||
ret, err := derived.NewHelm().Install(ctx, chart, values, name, namespace)
|
return api.NewToolCallResult(ret, err), nil
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to install helm chart '%s': %w", chart, err)), nil
|
|
||||||
}
|
|
||||||
return NewTextResult(ret, err), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) helmList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func helmList(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
allNamespaces := false
|
allNamespaces := false
|
||||||
if v, ok := ctr.GetArguments()["all_namespaces"].(bool); ok {
|
if v, ok := params.GetArguments()["all_namespaces"].(bool); ok {
|
||||||
allNamespaces = v
|
allNamespaces = v
|
||||||
}
|
}
|
||||||
namespace := ""
|
namespace := ""
|
||||||
if v, ok := ctr.GetArguments()["namespace"].(string); ok {
|
if v, ok := params.GetArguments()["namespace"].(string); ok {
|
||||||
namespace = v
|
namespace = v
|
||||||
}
|
}
|
||||||
derived, err := s.k.Derived(ctx)
|
ret, err := params.NewHelm().List(namespace, allNamespaces)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to list helm releases in namespace '%s': %w", namespace, err)), nil
|
||||||
}
|
}
|
||||||
ret, err := derived.NewHelm().List(namespace, allNamespaces)
|
return api.NewToolCallResult(ret, err), nil
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to list helm releases in namespace '%s': %w", namespace, err)), nil
|
|
||||||
}
|
|
||||||
return NewTextResult(ret, err), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) helmUninstall(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func helmUninstall(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
var name string
|
var name string
|
||||||
ok := false
|
ok := false
|
||||||
if name, ok = ctr.GetArguments()["name"].(string); !ok {
|
if name, ok = params.GetArguments()["name"].(string); !ok {
|
||||||
return NewTextResult("", fmt.Errorf("failed to uninstall helm chart, missing argument name")), nil
|
return api.NewToolCallResult("", fmt.Errorf("failed to uninstall helm chart, missing argument name")), nil
|
||||||
}
|
}
|
||||||
namespace := ""
|
namespace := ""
|
||||||
if v, ok := ctr.GetArguments()["namespace"].(string); ok {
|
if v, ok := params.GetArguments()["namespace"].(string); ok {
|
||||||
namespace = v
|
namespace = v
|
||||||
}
|
}
|
||||||
derived, err := s.k.Derived(ctx)
|
ret, err := params.NewHelm().Uninstall(name, namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to uninstall helm chart '%s': %w", name, err)), nil
|
||||||
}
|
}
|
||||||
ret, err := derived.NewHelm().Uninstall(name, namespace)
|
return api.NewToolCallResult(ret, err), nil
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to uninstall helm chart '%s': %w", name, err)), nil
|
|
||||||
}
|
|
||||||
return NewTextResult(ret, err), nil
|
|
||||||
}
|
}
|
||||||
68
pkg/toolsets/full/namespaces.go
Normal file
68
pkg/toolsets/full/namespaces.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package full
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/jsonschema-go/jsonschema"
|
||||||
|
"k8s.io/utils/ptr"
|
||||||
|
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/api"
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
|
||||||
|
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initNamespaces(k *internalk8s.Manager) []api.ServerTool {
|
||||||
|
ret := make([]api.ServerTool, 0)
|
||||||
|
ret = append(ret, api.ServerTool{
|
||||||
|
Tool: api.Tool{
|
||||||
|
Name: "namespaces_list",
|
||||||
|
Description: "List all the Kubernetes namespaces in the current cluster",
|
||||||
|
InputSchema: &jsonschema.Schema{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
Annotations: api.ToolAnnotations{
|
||||||
|
Title: "Namespaces: List",
|
||||||
|
ReadOnlyHint: ptr.To(true),
|
||||||
|
DestructiveHint: ptr.To(false),
|
||||||
|
IdempotentHint: ptr.To(false),
|
||||||
|
OpenWorldHint: ptr.To(true),
|
||||||
|
},
|
||||||
|
}, Handler: namespacesList,
|
||||||
|
})
|
||||||
|
if k.IsOpenShift(context.Background()) {
|
||||||
|
ret = append(ret, api.ServerTool{
|
||||||
|
Tool: api.Tool{
|
||||||
|
Name: "projects_list",
|
||||||
|
Description: "List all the OpenShift projects in the current cluster",
|
||||||
|
InputSchema: &jsonschema.Schema{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
Annotations: api.ToolAnnotations{
|
||||||
|
Title: "Projects: List",
|
||||||
|
ReadOnlyHint: ptr.To(true),
|
||||||
|
DestructiveHint: ptr.To(false),
|
||||||
|
IdempotentHint: ptr.To(false),
|
||||||
|
OpenWorldHint: ptr.To(true),
|
||||||
|
},
|
||||||
|
}, Handler: projectsList,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func namespacesList(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
|
ret, err := params.NamespacesList(params, kubernetes.ResourceListOptions{AsTable: params.ListOutput.AsTable()})
|
||||||
|
if err != nil {
|
||||||
|
return api.NewToolCallResult("", fmt.Errorf("failed to list namespaces: %v", err)), nil
|
||||||
|
}
|
||||||
|
return api.NewToolCallResult(params.ListOutput.PrintObj(ret)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectsList(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
|
ret, err := params.ProjectsList(params, kubernetes.ResourceListOptions{AsTable: params.ListOutput.AsTable()})
|
||||||
|
if err != nil {
|
||||||
|
return api.NewToolCallResult("", fmt.Errorf("failed to list projects: %v", err)), nil
|
||||||
|
}
|
||||||
|
return api.NewToolCallResult(params.ListOutput.PrintObj(ret)), nil
|
||||||
|
}
|
||||||
@@ -1,23 +1,22 @@
|
|||||||
package mcp
|
package full
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/jsonschema-go/jsonschema"
|
"github.com/google/jsonschema-go/jsonschema"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
|
||||||
"k8s.io/kubectl/pkg/metricsutil"
|
"k8s.io/kubectl/pkg/metricsutil"
|
||||||
"k8s.io/utils/ptr"
|
"k8s.io/utils/ptr"
|
||||||
|
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/api"
|
||||||
"github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
|
"github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
|
||||||
"github.com/containers/kubernetes-mcp-server/pkg/output"
|
"github.com/containers/kubernetes-mcp-server/pkg/output"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) initPods() []ServerTool {
|
func initPods() []api.ServerTool {
|
||||||
return []ServerTool{
|
return []api.ServerTool{
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "pods_list",
|
Name: "pods_list",
|
||||||
Description: "List all the Kubernetes pods in the current cluster from all namespaces",
|
Description: "List all the Kubernetes pods in the current cluster from all namespaces",
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -30,15 +29,15 @@ func (s *Server) initPods() []ServerTool {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Pods: List",
|
Title: "Pods: List",
|
||||||
ReadOnlyHint: ptr.To(true),
|
ReadOnlyHint: ptr.To(true),
|
||||||
DestructiveHint: ptr.To(false),
|
DestructiveHint: ptr.To(false),
|
||||||
IdempotentHint: ptr.To(false),
|
IdempotentHint: ptr.To(false),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.podsListInAllNamespaces},
|
}, Handler: podsListInAllNamespaces},
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "pods_list_in_namespace",
|
Name: "pods_list_in_namespace",
|
||||||
Description: "List all the Kubernetes pods in the specified namespace in the current cluster",
|
Description: "List all the Kubernetes pods in the specified namespace in the current cluster",
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -56,15 +55,15 @@ func (s *Server) initPods() []ServerTool {
|
|||||||
},
|
},
|
||||||
Required: []string{"namespace"},
|
Required: []string{"namespace"},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Pods: List in Namespace",
|
Title: "Pods: List in Namespace",
|
||||||
ReadOnlyHint: ptr.To(true),
|
ReadOnlyHint: ptr.To(true),
|
||||||
DestructiveHint: ptr.To(false),
|
DestructiveHint: ptr.To(false),
|
||||||
IdempotentHint: ptr.To(false),
|
IdempotentHint: ptr.To(false),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.podsListInNamespace},
|
}, Handler: podsListInNamespace},
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "pods_get",
|
Name: "pods_get",
|
||||||
Description: "Get a Kubernetes Pod in the current or provided namespace with the provided name",
|
Description: "Get a Kubernetes Pod in the current or provided namespace with the provided name",
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -81,15 +80,15 @@ func (s *Server) initPods() []ServerTool {
|
|||||||
},
|
},
|
||||||
Required: []string{"name"},
|
Required: []string{"name"},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Pods: Get",
|
Title: "Pods: Get",
|
||||||
ReadOnlyHint: ptr.To(true),
|
ReadOnlyHint: ptr.To(true),
|
||||||
DestructiveHint: ptr.To(false),
|
DestructiveHint: ptr.To(false),
|
||||||
IdempotentHint: ptr.To(false),
|
IdempotentHint: ptr.To(false),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.podsGet},
|
}, Handler: podsGet},
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "pods_delete",
|
Name: "pods_delete",
|
||||||
Description: "Delete a Kubernetes Pod in the current or provided namespace with the provided name",
|
Description: "Delete a Kubernetes Pod in the current or provided namespace with the provided name",
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -106,15 +105,15 @@ func (s *Server) initPods() []ServerTool {
|
|||||||
},
|
},
|
||||||
Required: []string{"name"},
|
Required: []string{"name"},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Pods: Delete",
|
Title: "Pods: Delete",
|
||||||
ReadOnlyHint: ptr.To(false),
|
ReadOnlyHint: ptr.To(false),
|
||||||
DestructiveHint: ptr.To(true),
|
DestructiveHint: ptr.To(true),
|
||||||
IdempotentHint: ptr.To(true),
|
IdempotentHint: ptr.To(true),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.podsDelete},
|
}, Handler: podsDelete},
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "pods_top",
|
Name: "pods_top",
|
||||||
Description: "List the resource consumption (CPU and memory) as recorded by the Kubernetes Metrics Server for the specified Kubernetes Pods in the all namespaces, the provided namespace, or the current namespace",
|
Description: "List the resource consumption (CPU and memory) as recorded by the Kubernetes Metrics Server for the specified Kubernetes Pods in the all namespaces, the provided namespace, or the current namespace",
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -123,7 +122,7 @@ func (s *Server) initPods() []ServerTool {
|
|||||||
"all_namespaces": {
|
"all_namespaces": {
|
||||||
Type: "boolean",
|
Type: "boolean",
|
||||||
Description: "If true, list the resource consumption for all Pods in all namespaces. If false, list the resource consumption for Pods in the provided namespace or the current namespace",
|
Description: "If true, list the resource consumption for all Pods in all namespaces. If false, list the resource consumption for Pods in the provided namespace or the current namespace",
|
||||||
Default: ToRawMessage(true),
|
Default: api.ToRawMessage(true),
|
||||||
},
|
},
|
||||||
"namespace": {
|
"namespace": {
|
||||||
Type: "string",
|
Type: "string",
|
||||||
@@ -140,15 +139,15 @@ func (s *Server) initPods() []ServerTool {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Pods: Top",
|
Title: "Pods: Top",
|
||||||
ReadOnlyHint: ptr.To(true),
|
ReadOnlyHint: ptr.To(true),
|
||||||
DestructiveHint: ptr.To(false),
|
DestructiveHint: ptr.To(false),
|
||||||
IdempotentHint: ptr.To(true),
|
IdempotentHint: ptr.To(true),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.podsTop},
|
}, Handler: podsTop},
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "pods_exec",
|
Name: "pods_exec",
|
||||||
Description: "Execute a command in a Kubernetes Pod in the current or provided namespace with the provided name and command",
|
Description: "Execute a command in a Kubernetes Pod in the current or provided namespace with the provided name and command",
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -176,15 +175,15 @@ func (s *Server) initPods() []ServerTool {
|
|||||||
},
|
},
|
||||||
Required: []string{"name", "command"},
|
Required: []string{"name", "command"},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Pods: Exec",
|
Title: "Pods: Exec",
|
||||||
ReadOnlyHint: ptr.To(false),
|
ReadOnlyHint: ptr.To(false),
|
||||||
DestructiveHint: ptr.To(true), // Depending on the Pod's entrypoint, executing certain commands may kill the Pod
|
DestructiveHint: ptr.To(true), // Depending on the Pod's entrypoint, executing certain commands may kill the Pod
|
||||||
IdempotentHint: ptr.To(false),
|
IdempotentHint: ptr.To(false),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.podsExec},
|
}, Handler: podsExec},
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "pods_log",
|
Name: "pods_log",
|
||||||
Description: "Get the logs of a Kubernetes Pod in the current or provided namespace with the provided name",
|
Description: "Get the logs of a Kubernetes Pod in the current or provided namespace with the provided name",
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -209,15 +208,15 @@ func (s *Server) initPods() []ServerTool {
|
|||||||
},
|
},
|
||||||
Required: []string{"name"},
|
Required: []string{"name"},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Pods: Log",
|
Title: "Pods: Log",
|
||||||
ReadOnlyHint: ptr.To(true),
|
ReadOnlyHint: ptr.To(true),
|
||||||
DestructiveHint: ptr.To(false),
|
DestructiveHint: ptr.To(false),
|
||||||
IdempotentHint: ptr.To(false),
|
IdempotentHint: ptr.To(false),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.podsLog},
|
}, Handler: podsLog},
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "pods_run",
|
Name: "pods_run",
|
||||||
Description: "Run a Kubernetes Pod in the current or provided namespace with the provided container image and optional name",
|
Description: "Run a Kubernetes Pod in the current or provided namespace with the provided container image and optional name",
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -242,144 +241,124 @@ func (s *Server) initPods() []ServerTool {
|
|||||||
},
|
},
|
||||||
Required: []string{"image"},
|
Required: []string{"image"},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Pods: Run",
|
Title: "Pods: Run",
|
||||||
ReadOnlyHint: ptr.To(false),
|
ReadOnlyHint: ptr.To(false),
|
||||||
DestructiveHint: ptr.To(false),
|
DestructiveHint: ptr.To(false),
|
||||||
IdempotentHint: ptr.To(false),
|
IdempotentHint: ptr.To(false),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.podsRun},
|
}, Handler: podsRun},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) podsListInAllNamespaces(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func podsListInAllNamespaces(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
labelSelector := ctr.GetArguments()["labelSelector"]
|
labelSelector := params.GetArguments()["labelSelector"]
|
||||||
resourceListOptions := kubernetes.ResourceListOptions{
|
resourceListOptions := kubernetes.ResourceListOptions{
|
||||||
AsTable: s.configuration.ListOutput.AsTable(),
|
AsTable: params.ListOutput.AsTable(),
|
||||||
}
|
}
|
||||||
if labelSelector != nil {
|
if labelSelector != nil {
|
||||||
resourceListOptions.LabelSelector = labelSelector.(string)
|
resourceListOptions.LabelSelector = labelSelector.(string)
|
||||||
}
|
}
|
||||||
derived, err := s.k.Derived(ctx)
|
ret, err := params.PodsListInAllNamespaces(params, resourceListOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to list pods in all namespaces: %v", err)), nil
|
||||||
}
|
}
|
||||||
ret, err := derived.PodsListInAllNamespaces(ctx, resourceListOptions)
|
return api.NewToolCallResult(params.ListOutput.PrintObj(ret)), nil
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to list pods in all namespaces: %v", err)), nil
|
|
||||||
}
|
|
||||||
return NewTextResult(s.configuration.ListOutput.PrintObj(ret)), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) podsListInNamespace(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func podsListInNamespace(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
ns := ctr.GetArguments()["namespace"]
|
ns := params.GetArguments()["namespace"]
|
||||||
if ns == nil {
|
if ns == nil {
|
||||||
return NewTextResult("", errors.New("failed to list pods in namespace, missing argument namespace")), nil
|
return api.NewToolCallResult("", errors.New("failed to list pods in namespace, missing argument namespace")), nil
|
||||||
}
|
}
|
||||||
resourceListOptions := kubernetes.ResourceListOptions{
|
resourceListOptions := kubernetes.ResourceListOptions{
|
||||||
AsTable: s.configuration.ListOutput.AsTable(),
|
AsTable: params.ListOutput.AsTable(),
|
||||||
}
|
}
|
||||||
labelSelector := ctr.GetArguments()["labelSelector"]
|
labelSelector := params.GetArguments()["labelSelector"]
|
||||||
if labelSelector != nil {
|
if labelSelector != nil {
|
||||||
resourceListOptions.LabelSelector = labelSelector.(string)
|
resourceListOptions.LabelSelector = labelSelector.(string)
|
||||||
}
|
}
|
||||||
derived, err := s.k.Derived(ctx)
|
ret, err := params.PodsListInNamespace(params, ns.(string), resourceListOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to list pods in namespace %s: %v", ns, err)), nil
|
||||||
}
|
}
|
||||||
ret, err := derived.PodsListInNamespace(ctx, ns.(string), resourceListOptions)
|
return api.NewToolCallResult(params.ListOutput.PrintObj(ret)), nil
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to list pods in namespace %s: %v", ns, err)), nil
|
|
||||||
}
|
|
||||||
return NewTextResult(s.configuration.ListOutput.PrintObj(ret)), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) podsGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func podsGet(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
ns := ctr.GetArguments()["namespace"]
|
ns := params.GetArguments()["namespace"]
|
||||||
if ns == nil {
|
if ns == nil {
|
||||||
ns = ""
|
ns = ""
|
||||||
}
|
}
|
||||||
name := ctr.GetArguments()["name"]
|
name := params.GetArguments()["name"]
|
||||||
if name == nil {
|
if name == nil {
|
||||||
return NewTextResult("", errors.New("failed to get pod, missing argument name")), nil
|
return api.NewToolCallResult("", errors.New("failed to get pod, missing argument name")), nil
|
||||||
}
|
}
|
||||||
derived, err := s.k.Derived(ctx)
|
ret, err := params.PodsGet(params, ns.(string), name.(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to get pod %s in namespace %s: %v", name, ns, err)), nil
|
||||||
}
|
}
|
||||||
ret, err := derived.PodsGet(ctx, ns.(string), name.(string))
|
return api.NewToolCallResult(output.MarshalYaml(ret)), nil
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to get pod %s in namespace %s: %v", name, ns, err)), nil
|
|
||||||
}
|
|
||||||
return NewTextResult(output.MarshalYaml(ret)), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) podsDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func podsDelete(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
ns := ctr.GetArguments()["namespace"]
|
ns := params.GetArguments()["namespace"]
|
||||||
if ns == nil {
|
if ns == nil {
|
||||||
ns = ""
|
ns = ""
|
||||||
}
|
}
|
||||||
name := ctr.GetArguments()["name"]
|
name := params.GetArguments()["name"]
|
||||||
if name == nil {
|
if name == nil {
|
||||||
return NewTextResult("", errors.New("failed to delete pod, missing argument name")), nil
|
return api.NewToolCallResult("", errors.New("failed to delete pod, missing argument name")), nil
|
||||||
}
|
}
|
||||||
derived, err := s.k.Derived(ctx)
|
ret, err := params.PodsDelete(params, ns.(string), name.(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to delete pod %s in namespace %s: %v", name, ns, err)), nil
|
||||||
}
|
}
|
||||||
ret, err := derived.PodsDelete(ctx, ns.(string), name.(string))
|
return api.NewToolCallResult(ret, err), nil
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to delete pod %s in namespace %s: %v", name, ns, err)), nil
|
|
||||||
}
|
|
||||||
return NewTextResult(ret, err), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) podsTop(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func podsTop(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
podsTopOptions := kubernetes.PodsTopOptions{AllNamespaces: true}
|
podsTopOptions := kubernetes.PodsTopOptions{AllNamespaces: true}
|
||||||
if v, ok := ctr.GetArguments()["namespace"].(string); ok {
|
if v, ok := params.GetArguments()["namespace"].(string); ok {
|
||||||
podsTopOptions.Namespace = v
|
podsTopOptions.Namespace = v
|
||||||
}
|
}
|
||||||
if v, ok := ctr.GetArguments()["all_namespaces"].(bool); ok {
|
if v, ok := params.GetArguments()["all_namespaces"].(bool); ok {
|
||||||
podsTopOptions.AllNamespaces = v
|
podsTopOptions.AllNamespaces = v
|
||||||
}
|
}
|
||||||
if v, ok := ctr.GetArguments()["name"].(string); ok {
|
if v, ok := params.GetArguments()["name"].(string); ok {
|
||||||
podsTopOptions.Name = v
|
podsTopOptions.Name = v
|
||||||
}
|
}
|
||||||
if v, ok := ctr.GetArguments()["label_selector"].(string); ok {
|
if v, ok := params.GetArguments()["label_selector"].(string); ok {
|
||||||
podsTopOptions.LabelSelector = v
|
podsTopOptions.LabelSelector = v
|
||||||
}
|
}
|
||||||
derived, err := s.k.Derived(ctx)
|
ret, err := params.PodsTop(params, podsTopOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to get pods top: %v", err)), nil
|
||||||
}
|
|
||||||
ret, err := derived.PodsTop(ctx, podsTopOptions)
|
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to get pods top: %v", err)), nil
|
|
||||||
}
|
}
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
printer := metricsutil.NewTopCmdPrinter(buf)
|
printer := metricsutil.NewTopCmdPrinter(buf)
|
||||||
err = printer.PrintPodMetrics(ret.Items, true, true, false, "", true)
|
err = printer.PrintPodMetrics(ret.Items, true, true, false, "", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return NewTextResult("", fmt.Errorf("failed to get pods top: %v", err)), nil
|
return api.NewToolCallResult("", fmt.Errorf("failed to get pods top: %v", err)), nil
|
||||||
}
|
}
|
||||||
return NewTextResult(buf.String(), nil), nil
|
return api.NewToolCallResult(buf.String(), nil), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) podsExec(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func podsExec(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
ns := ctr.GetArguments()["namespace"]
|
ns := params.GetArguments()["namespace"]
|
||||||
if ns == nil {
|
if ns == nil {
|
||||||
ns = ""
|
ns = ""
|
||||||
}
|
}
|
||||||
name := ctr.GetArguments()["name"]
|
name := params.GetArguments()["name"]
|
||||||
if name == nil {
|
if name == nil {
|
||||||
return NewTextResult("", errors.New("failed to exec in pod, missing argument name")), nil
|
return api.NewToolCallResult("", errors.New("failed to exec in pod, missing argument name")), nil
|
||||||
}
|
}
|
||||||
container := ctr.GetArguments()["container"]
|
container := params.GetArguments()["container"]
|
||||||
if container == nil {
|
if container == nil {
|
||||||
container = ""
|
container = ""
|
||||||
}
|
}
|
||||||
commandArg := ctr.GetArguments()["command"]
|
commandArg := params.GetArguments()["command"]
|
||||||
command := make([]string, 0)
|
command := make([]string, 0)
|
||||||
if _, ok := commandArg.([]interface{}); ok {
|
if _, ok := commandArg.([]interface{}); ok {
|
||||||
for _, cmd := range commandArg.([]interface{}) {
|
for _, cmd := range commandArg.([]interface{}) {
|
||||||
@@ -388,80 +367,68 @@ func (s *Server) podsExec(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.Ca
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return NewTextResult("", errors.New("failed to exec in pod, invalid command argument")), nil
|
return api.NewToolCallResult("", errors.New("failed to exec in pod, invalid command argument")), nil
|
||||||
}
|
}
|
||||||
derived, err := s.k.Derived(ctx)
|
ret, err := params.PodsExec(params, ns.(string), name.(string), container.(string), command)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to exec in pod %s in namespace %s: %v", name, ns, err)), nil
|
||||||
}
|
|
||||||
ret, err := derived.PodsExec(ctx, ns.(string), name.(string), container.(string), command)
|
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to exec in pod %s in namespace %s: %v", name, ns, err)), nil
|
|
||||||
} else if ret == "" {
|
} else if ret == "" {
|
||||||
ret = fmt.Sprintf("The executed command in pod %s in namespace %s has not produced any output", name, ns)
|
ret = fmt.Sprintf("The executed command in pod %s in namespace %s has not produced any output", name, ns)
|
||||||
}
|
}
|
||||||
return NewTextResult(ret, err), nil
|
return api.NewToolCallResult(ret, err), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) podsLog(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func podsLog(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
ns := ctr.GetArguments()["namespace"]
|
ns := params.GetArguments()["namespace"]
|
||||||
if ns == nil {
|
if ns == nil {
|
||||||
ns = ""
|
ns = ""
|
||||||
}
|
}
|
||||||
name := ctr.GetArguments()["name"]
|
name := params.GetArguments()["name"]
|
||||||
if name == nil {
|
if name == nil {
|
||||||
return NewTextResult("", errors.New("failed to get pod log, missing argument name")), nil
|
return api.NewToolCallResult("", errors.New("failed to get pod log, missing argument name")), nil
|
||||||
}
|
}
|
||||||
container := ctr.GetArguments()["container"]
|
container := params.GetArguments()["container"]
|
||||||
if container == nil {
|
if container == nil {
|
||||||
container = ""
|
container = ""
|
||||||
}
|
}
|
||||||
previous := ctr.GetArguments()["previous"]
|
previous := params.GetArguments()["previous"]
|
||||||
var previousBool bool
|
var previousBool bool
|
||||||
if previous != nil {
|
if previous != nil {
|
||||||
previousBool = previous.(bool)
|
previousBool = previous.(bool)
|
||||||
}
|
}
|
||||||
derived, err := s.k.Derived(ctx)
|
ret, err := params.PodsLog(params, ns.(string), name.(string), container.(string), previousBool)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to get pod %s log in namespace %s: %v", name, ns, err)), nil
|
||||||
}
|
|
||||||
ret, err := derived.PodsLog(ctx, ns.(string), name.(string), container.(string), previousBool)
|
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to get pod %s log in namespace %s: %v", name, ns, err)), nil
|
|
||||||
} else if ret == "" {
|
} else if ret == "" {
|
||||||
ret = fmt.Sprintf("The pod %s in namespace %s has not logged any message yet", name, ns)
|
ret = fmt.Sprintf("The pod %s in namespace %s has not logged any message yet", name, ns)
|
||||||
}
|
}
|
||||||
return NewTextResult(ret, err), nil
|
return api.NewToolCallResult(ret, err), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) podsRun(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func podsRun(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
ns := ctr.GetArguments()["namespace"]
|
ns := params.GetArguments()["namespace"]
|
||||||
if ns == nil {
|
if ns == nil {
|
||||||
ns = ""
|
ns = ""
|
||||||
}
|
}
|
||||||
name := ctr.GetArguments()["name"]
|
name := params.GetArguments()["name"]
|
||||||
if name == nil {
|
if name == nil {
|
||||||
name = ""
|
name = ""
|
||||||
}
|
}
|
||||||
image := ctr.GetArguments()["image"]
|
image := params.GetArguments()["image"]
|
||||||
if image == nil {
|
if image == nil {
|
||||||
return NewTextResult("", errors.New("failed to run pod, missing argument image")), nil
|
return api.NewToolCallResult("", errors.New("failed to run pod, missing argument image")), nil
|
||||||
}
|
}
|
||||||
port := ctr.GetArguments()["port"]
|
port := params.GetArguments()["port"]
|
||||||
if port == nil {
|
if port == nil {
|
||||||
port = float64(0)
|
port = float64(0)
|
||||||
}
|
}
|
||||||
derived, err := s.k.Derived(ctx)
|
resources, err := params.PodsRun(params, ns.(string), name.(string), image.(string), int32(port.(float64)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to run pod %s in namespace %s: %v", name, ns, err)), nil
|
||||||
}
|
|
||||||
resources, err := derived.PodsRun(ctx, ns.(string), name.(string), image.(string), int32(port.(float64)))
|
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to run pod %s in namespace %s: %v", name, ns, err)), nil
|
|
||||||
}
|
}
|
||||||
marshalledYaml, err := output.MarshalYaml(resources)
|
marshalledYaml, err := output.MarshalYaml(resources)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("failed to run pod: %v", err)
|
err = fmt.Errorf("failed to run pod: %v", err)
|
||||||
}
|
}
|
||||||
return NewTextResult("# The following resources (YAML) have been created or updated successfully\n"+marshalledYaml, err), nil
|
return api.NewToolCallResult("# The following resources (YAML) have been created or updated successfully\n"+marshalledYaml, err), nil
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package mcp
|
package full
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -6,22 +6,23 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/jsonschema-go/jsonschema"
|
"github.com/google/jsonschema-go/jsonschema"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/utils/ptr"
|
"k8s.io/utils/ptr"
|
||||||
|
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/api"
|
||||||
"github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
|
"github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
|
||||||
|
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
|
||||||
"github.com/containers/kubernetes-mcp-server/pkg/output"
|
"github.com/containers/kubernetes-mcp-server/pkg/output"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) initResources() []ServerTool {
|
func initResources(k *internalk8s.Manager) []api.ServerTool {
|
||||||
commonApiVersion := "v1 Pod, v1 Service, v1 Node, apps/v1 Deployment, networking.k8s.io/v1 Ingress"
|
commonApiVersion := "v1 Pod, v1 Service, v1 Node, apps/v1 Deployment, networking.k8s.io/v1 Ingress"
|
||||||
if s.k.IsOpenShift(context.Background()) {
|
if k.IsOpenShift(context.Background()) {
|
||||||
commonApiVersion += ", route.openshift.io/v1 Route"
|
commonApiVersion += ", route.openshift.io/v1 Route"
|
||||||
}
|
}
|
||||||
commonApiVersion = fmt.Sprintf("(common apiVersion and kind include: %s)", commonApiVersion)
|
commonApiVersion = fmt.Sprintf("(common apiVersion and kind include: %s)", commonApiVersion)
|
||||||
return []ServerTool{
|
return []api.ServerTool{
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "resources_list",
|
Name: "resources_list",
|
||||||
Description: "List Kubernetes resources and objects in the current cluster by providing their apiVersion and kind and optionally the namespace and label selector\n" + commonApiVersion,
|
Description: "List Kubernetes resources and objects in the current cluster by providing their apiVersion and kind and optionally the namespace and label selector\n" + commonApiVersion,
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -47,15 +48,15 @@ func (s *Server) initResources() []ServerTool {
|
|||||||
},
|
},
|
||||||
Required: []string{"apiVersion", "kind"},
|
Required: []string{"apiVersion", "kind"},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Resources: List",
|
Title: "Resources: List",
|
||||||
ReadOnlyHint: ptr.To(true),
|
ReadOnlyHint: ptr.To(true),
|
||||||
DestructiveHint: ptr.To(false),
|
DestructiveHint: ptr.To(false),
|
||||||
IdempotentHint: ptr.To(false),
|
IdempotentHint: ptr.To(false),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.resourcesList},
|
}, Handler: resourcesList},
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "resources_get",
|
Name: "resources_get",
|
||||||
Description: "Get a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n" + commonApiVersion,
|
Description: "Get a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n" + commonApiVersion,
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -80,15 +81,15 @@ func (s *Server) initResources() []ServerTool {
|
|||||||
},
|
},
|
||||||
Required: []string{"apiVersion", "kind", "name"},
|
Required: []string{"apiVersion", "kind", "name"},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Resources: Get",
|
Title: "Resources: Get",
|
||||||
ReadOnlyHint: ptr.To(true),
|
ReadOnlyHint: ptr.To(true),
|
||||||
DestructiveHint: ptr.To(false),
|
DestructiveHint: ptr.To(false),
|
||||||
IdempotentHint: ptr.To(false),
|
IdempotentHint: ptr.To(false),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.resourcesGet},
|
}, Handler: resourcesGet},
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "resources_create_or_update",
|
Name: "resources_create_or_update",
|
||||||
Description: "Create or update a Kubernetes resource in the current cluster by providing a YAML or JSON representation of the resource\n" + commonApiVersion,
|
Description: "Create or update a Kubernetes resource in the current cluster by providing a YAML or JSON representation of the resource\n" + commonApiVersion,
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -101,15 +102,15 @@ func (s *Server) initResources() []ServerTool {
|
|||||||
},
|
},
|
||||||
Required: []string{"resource"},
|
Required: []string{"resource"},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Resources: Create or Update",
|
Title: "Resources: Create or Update",
|
||||||
ReadOnlyHint: ptr.To(false),
|
ReadOnlyHint: ptr.To(false),
|
||||||
DestructiveHint: ptr.To(true),
|
DestructiveHint: ptr.To(true),
|
||||||
IdempotentHint: ptr.To(true),
|
IdempotentHint: ptr.To(true),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.resourcesCreateOrUpdate},
|
}, Handler: resourcesCreateOrUpdate},
|
||||||
{Tool: Tool{
|
{Tool: api.Tool{
|
||||||
Name: "resources_delete",
|
Name: "resources_delete",
|
||||||
Description: "Delete a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n" + commonApiVersion,
|
Description: "Delete a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n" + commonApiVersion,
|
||||||
InputSchema: &jsonschema.Schema{
|
InputSchema: &jsonschema.Schema{
|
||||||
@@ -134,149 +135,133 @@ func (s *Server) initResources() []ServerTool {
|
|||||||
},
|
},
|
||||||
Required: []string{"apiVersion", "kind", "name"},
|
Required: []string{"apiVersion", "kind", "name"},
|
||||||
},
|
},
|
||||||
Annotations: ToolAnnotations{
|
Annotations: api.ToolAnnotations{
|
||||||
Title: "Resources: Delete",
|
Title: "Resources: Delete",
|
||||||
ReadOnlyHint: ptr.To(false),
|
ReadOnlyHint: ptr.To(false),
|
||||||
DestructiveHint: ptr.To(true),
|
DestructiveHint: ptr.To(true),
|
||||||
IdempotentHint: ptr.To(true),
|
IdempotentHint: ptr.To(true),
|
||||||
OpenWorldHint: ptr.To(true),
|
OpenWorldHint: ptr.To(true),
|
||||||
},
|
},
|
||||||
}, Handler: s.resourcesDelete},
|
}, Handler: resourcesDelete},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) resourcesList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func resourcesList(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
namespace := ctr.GetArguments()["namespace"]
|
namespace := params.GetArguments()["namespace"]
|
||||||
if namespace == nil {
|
if namespace == nil {
|
||||||
namespace = ""
|
namespace = ""
|
||||||
}
|
}
|
||||||
labelSelector := ctr.GetArguments()["labelSelector"]
|
labelSelector := params.GetArguments()["labelSelector"]
|
||||||
resourceListOptions := kubernetes.ResourceListOptions{
|
resourceListOptions := kubernetes.ResourceListOptions{
|
||||||
AsTable: s.configuration.ListOutput.AsTable(),
|
AsTable: params.ListOutput.AsTable(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if labelSelector != nil {
|
if labelSelector != nil {
|
||||||
l, ok := labelSelector.(string)
|
l, ok := labelSelector.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return NewTextResult("", fmt.Errorf("labelSelector is not a string")), nil
|
return api.NewToolCallResult("", fmt.Errorf("labelSelector is not a string")), nil
|
||||||
}
|
}
|
||||||
resourceListOptions.LabelSelector = l
|
resourceListOptions.LabelSelector = l
|
||||||
}
|
}
|
||||||
gvk, err := parseGroupVersionKind(ctr.GetArguments())
|
gvk, err := parseGroupVersionKind(params.GetArguments())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return NewTextResult("", fmt.Errorf("failed to list resources, %s", err)), nil
|
return api.NewToolCallResult("", fmt.Errorf("failed to list resources, %s", err)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ns, ok := namespace.(string)
|
ns, ok := namespace.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return NewTextResult("", fmt.Errorf("namespace is not a string")), nil
|
return api.NewToolCallResult("", fmt.Errorf("namespace is not a string")), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
derived, err := s.k.Derived(ctx)
|
ret, err := params.ResourcesList(params, gvk, ns, resourceListOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to list resources: %v", err)), nil
|
||||||
}
|
}
|
||||||
ret, err := derived.ResourcesList(ctx, gvk, ns, resourceListOptions)
|
return api.NewToolCallResult(params.ListOutput.PrintObj(ret)), nil
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to list resources: %v", err)), nil
|
|
||||||
}
|
|
||||||
return NewTextResult(s.configuration.ListOutput.PrintObj(ret)), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) resourcesGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func resourcesGet(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
namespace := ctr.GetArguments()["namespace"]
|
namespace := params.GetArguments()["namespace"]
|
||||||
if namespace == nil {
|
if namespace == nil {
|
||||||
namespace = ""
|
namespace = ""
|
||||||
}
|
}
|
||||||
gvk, err := parseGroupVersionKind(ctr.GetArguments())
|
gvk, err := parseGroupVersionKind(params.GetArguments())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return NewTextResult("", fmt.Errorf("failed to get resource, %s", err)), nil
|
return api.NewToolCallResult("", fmt.Errorf("failed to get resource, %s", err)), nil
|
||||||
}
|
}
|
||||||
name := ctr.GetArguments()["name"]
|
name := params.GetArguments()["name"]
|
||||||
if name == nil {
|
if name == nil {
|
||||||
return NewTextResult("", errors.New("failed to get resource, missing argument name")), nil
|
return api.NewToolCallResult("", errors.New("failed to get resource, missing argument name")), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ns, ok := namespace.(string)
|
ns, ok := namespace.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return NewTextResult("", fmt.Errorf("namespace is not a string")), nil
|
return api.NewToolCallResult("", fmt.Errorf("namespace is not a string")), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
n, ok := name.(string)
|
n, ok := name.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return NewTextResult("", fmt.Errorf("name is not a string")), nil
|
return api.NewToolCallResult("", fmt.Errorf("name is not a string")), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
derived, err := s.k.Derived(ctx)
|
ret, err := params.ResourcesGet(params, gvk, ns, n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to get resource: %v", err)), nil
|
||||||
}
|
}
|
||||||
ret, err := derived.ResourcesGet(ctx, gvk, ns, n)
|
return api.NewToolCallResult(output.MarshalYaml(ret)), nil
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to get resource: %v", err)), nil
|
|
||||||
}
|
|
||||||
return NewTextResult(output.MarshalYaml(ret)), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) resourcesCreateOrUpdate(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func resourcesCreateOrUpdate(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
resource := ctr.GetArguments()["resource"]
|
resource := params.GetArguments()["resource"]
|
||||||
if resource == nil || resource == "" {
|
if resource == nil || resource == "" {
|
||||||
return NewTextResult("", errors.New("failed to create or update resources, missing argument resource")), nil
|
return api.NewToolCallResult("", errors.New("failed to create or update resources, missing argument resource")), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
r, ok := resource.(string)
|
r, ok := resource.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return NewTextResult("", fmt.Errorf("resource is not a string")), nil
|
return api.NewToolCallResult("", fmt.Errorf("resource is not a string")), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
derived, err := s.k.Derived(ctx)
|
resources, err := params.ResourcesCreateOrUpdate(params, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to create or update resources: %v", err)), nil
|
||||||
}
|
|
||||||
resources, err := derived.ResourcesCreateOrUpdate(ctx, r)
|
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to create or update resources: %v", err)), nil
|
|
||||||
}
|
}
|
||||||
marshalledYaml, err := output.MarshalYaml(resources)
|
marshalledYaml, err := output.MarshalYaml(resources)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("failed to create or update resources:: %v", err)
|
err = fmt.Errorf("failed to create or update resources:: %v", err)
|
||||||
}
|
}
|
||||||
return NewTextResult("# The following resources (YAML) have been created or updated successfully\n"+marshalledYaml, err), nil
|
return api.NewToolCallResult("# The following resources (YAML) have been created or updated successfully\n"+marshalledYaml, err), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) resourcesDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func resourcesDelete(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
|
||||||
namespace := ctr.GetArguments()["namespace"]
|
namespace := params.GetArguments()["namespace"]
|
||||||
if namespace == nil {
|
if namespace == nil {
|
||||||
namespace = ""
|
namespace = ""
|
||||||
}
|
}
|
||||||
gvk, err := parseGroupVersionKind(ctr.GetArguments())
|
gvk, err := parseGroupVersionKind(params.GetArguments())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return NewTextResult("", fmt.Errorf("failed to delete resource, %s", err)), nil
|
return api.NewToolCallResult("", fmt.Errorf("failed to delete resource, %s", err)), nil
|
||||||
}
|
}
|
||||||
name := ctr.GetArguments()["name"]
|
name := params.GetArguments()["name"]
|
||||||
if name == nil {
|
if name == nil {
|
||||||
return NewTextResult("", errors.New("failed to delete resource, missing argument name")), nil
|
return api.NewToolCallResult("", errors.New("failed to delete resource, missing argument name")), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ns, ok := namespace.(string)
|
ns, ok := namespace.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return NewTextResult("", fmt.Errorf("namespace is not a string")), nil
|
return api.NewToolCallResult("", fmt.Errorf("namespace is not a string")), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
n, ok := name.(string)
|
n, ok := name.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return NewTextResult("", fmt.Errorf("name is not a string")), nil
|
return api.NewToolCallResult("", fmt.Errorf("name is not a string")), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
derived, err := s.k.Derived(ctx)
|
err = params.ResourcesDelete(params, gvk, ns, n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return api.NewToolCallResult("", fmt.Errorf("failed to delete resource: %v", err)), nil
|
||||||
}
|
}
|
||||||
err = derived.ResourcesDelete(ctx, gvk, ns, n)
|
return api.NewToolCallResult("Resource deleted successfully", err), nil
|
||||||
if err != nil {
|
|
||||||
return NewTextResult("", fmt.Errorf("failed to delete resource: %v", err)), nil
|
|
||||||
}
|
|
||||||
return NewTextResult("Resource deleted successfully", err), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseGroupVersionKind(arguments map[string]interface{}) (*schema.GroupVersionKind, error) {
|
func parseGroupVersionKind(arguments map[string]interface{}) (*schema.GroupVersionKind, error) {
|
||||||
36
pkg/toolsets/full/toolset.go
Normal file
36
pkg/toolsets/full/toolset.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package full
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/api"
|
||||||
|
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/toolsets"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Full struct{}
|
||||||
|
|
||||||
|
var _ api.Toolset = (*Full)(nil)
|
||||||
|
|
||||||
|
func (p *Full) GetName() string {
|
||||||
|
return "full"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Full) GetDescription() string {
|
||||||
|
return "Complete toolset with all tools and extended outputs"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Full) GetTools(k *internalk8s.Manager) []api.ServerTool {
|
||||||
|
return slices.Concat(
|
||||||
|
initConfiguration(),
|
||||||
|
initEvents(),
|
||||||
|
initNamespaces(k),
|
||||||
|
initPods(),
|
||||||
|
initResources(k),
|
||||||
|
initHelm(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
toolsets.Register(&Full{})
|
||||||
|
}
|
||||||
40
pkg/toolsets/toolsets.go
Normal file
40
pkg/toolsets/toolsets.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package toolsets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var toolsets []api.Toolset
|
||||||
|
|
||||||
|
// Clear removes all registered toolsets, TESTING PURPOSES ONLY.
|
||||||
|
func Clear() {
|
||||||
|
toolsets = []api.Toolset{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Register(toolset api.Toolset) {
|
||||||
|
toolsets = append(toolsets, toolset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Toolsets() []api.Toolset {
|
||||||
|
return toolsets
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToolsetNames() []string {
|
||||||
|
names := make([]string, 0)
|
||||||
|
for _, toolset := range Toolsets() {
|
||||||
|
names = append(names, toolset.GetName())
|
||||||
|
}
|
||||||
|
slices.Sort(names)
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToolsetFromString(name string) api.Toolset {
|
||||||
|
for _, toolset := range Toolsets() {
|
||||||
|
if toolset.GetName() == name {
|
||||||
|
return toolset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
60
pkg/toolsets/toolsets_test.go
Normal file
60
pkg/toolsets/toolsets_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package toolsets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/api"
|
||||||
|
"github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ToolsetsSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ToolsetsSuite) SetupTest() {
|
||||||
|
Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestToolset struct {
|
||||||
|
name string
|
||||||
|
description string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TestToolset) GetName() string { return t.name }
|
||||||
|
|
||||||
|
func (t *TestToolset) GetDescription() string { return t.description }
|
||||||
|
|
||||||
|
func (t *TestToolset) GetTools(k *kubernetes.Manager) []api.ServerTool { return nil }
|
||||||
|
|
||||||
|
var _ api.Toolset = (*TestToolset)(nil)
|
||||||
|
|
||||||
|
func (s *ToolsetsSuite) TestToolsetNames() {
|
||||||
|
s.Run("Returns empty list if no toolsets registered", func() {
|
||||||
|
s.Empty(ToolsetNames(), "Expected empty list of toolset names")
|
||||||
|
})
|
||||||
|
|
||||||
|
Register(&TestToolset{name: "z"})
|
||||||
|
Register(&TestToolset{name: "b"})
|
||||||
|
Register(&TestToolset{name: "1"})
|
||||||
|
s.Run("Returns sorted list of registered toolset names", func() {
|
||||||
|
names := ToolsetNames()
|
||||||
|
s.Equal([]string{"1", "b", "z"}, names, "Expected sorted list of toolset names")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ToolsetsSuite) TestToolsetFromString() {
|
||||||
|
s.Run("Returns nil if toolset not found", func() {
|
||||||
|
s.Nil(ToolsetFromString("non-existent"), "Expected nil for non-existent toolset")
|
||||||
|
})
|
||||||
|
s.Run("Returns the correct toolset if found", func() {
|
||||||
|
Register(&TestToolset{name: "existent"})
|
||||||
|
res := ToolsetFromString("existent")
|
||||||
|
s.NotNil(res, "Expected to find the registered toolset")
|
||||||
|
s.Equal("existent", res.GetName(), "Expected to find the registered toolset by name")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToolsets(t *testing.T) {
|
||||||
|
suite.Run(t, new(ToolsetsSuite))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user