Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions cmd/mcp/mcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package mcp

import (
"fmt"

"github.com/metal-stack/api/go/metalstack/admin/v2/adminv2mcp"
"github.com/metal-stack/api/go/metalstack/api/v2/apiv2mcp"
"github.com/metal-stack/cli/cmd/config"
"github.com/metal-stack/v"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/redpanda-data/protoc-gen-go-mcp/pkg/runtime"
"github.com/redpanda-data/protoc-gen-go-mcp/pkg/runtime/gosdk"

"github.com/spf13/cobra"
)

var long = fmt.Sprintf(`
Use %q to serve as mcp server. You must configure your coding agent to make use of the mcp server.

Example opencode.json:

"mcp": {
"metal": {
"type": "local",
"command": [
%q,
"mcp"
],
"enabled": true,
}
}

Then login with %q and start you coding agent and ask questions like:

- list all metal partitions
- give me all available metal sizes and images
- create a ip address, ask me questions
`, config.BinaryName, config.BinaryName, config.BinaryName)

func NewMCPCmd(c *config.Config) *cobra.Command {

cmd := &cobra.Command{
Use: "mcp",
Short: "start mcp server",
Long: long,
RunE: func(cmd *cobra.Command, args []string) error {
raw, mcps := gosdk.NewServer("metal-stack.io mcp server", v.V.String())

forwardToClients(c, mcps)

if err := raw.Run(cmd.Context(), &mcp.StdioTransport{}); err != nil {
return err
}
return nil
},
}
return cmd
}

func forwardToClients(c *config.Config, mcps runtime.MCPServer) {
// Be prepared for later opts.
var opts []runtime.Option
// API Clients
apiv2mcp.ForwardToAuditServiceClient(mcps, c.Client.Apiv2().Audit(), opts...)
apiv2mcp.ForwardToFilesystemServiceClient(mcps, c.Client.Apiv2().Filesystem(), opts...)
apiv2mcp.ForwardToHealthServiceClient(mcps, c.Client.Apiv2().Health(), opts...)
apiv2mcp.ForwardToImageServiceClient(mcps, c.Client.Apiv2().Image(), opts...)
apiv2mcp.ForwardToIPServiceClient(mcps, c.Client.Apiv2().IP(), opts...)
apiv2mcp.ForwardToMachineServiceClient(mcps, c.Client.Apiv2().Machine(), opts...)
apiv2mcp.ForwardToMethodServiceClient(mcps, c.Client.Apiv2().Method(), opts...)
apiv2mcp.ForwardToNetworkServiceClient(mcps, c.Client.Apiv2().Network(), opts...)
apiv2mcp.ForwardToPartitionServiceClient(mcps, c.Client.Apiv2().Partition(), opts...)
apiv2mcp.ForwardToProjectServiceClient(mcps, c.Client.Apiv2().Project(), opts...)
apiv2mcp.ForwardToSizeImageConstraintServiceClient(mcps, c.Client.Apiv2().SizeImageConstraint(), opts...)
apiv2mcp.ForwardToSizeReservationServiceClient(mcps, c.Client.Apiv2().SizeReservation(), opts...)
apiv2mcp.ForwardToSizeServiceClient(mcps, c.Client.Apiv2().Size(), opts...)
apiv2mcp.ForwardToTenantServiceClient(mcps, c.Client.Apiv2().Tenant(), opts...)
apiv2mcp.ForwardToTokenServiceClient(mcps, c.Client.Apiv2().Token(), opts...)
apiv2mcp.ForwardToUserServiceClient(mcps, c.Client.Apiv2().User(), opts...)
apiv2mcp.ForwardToVersionServiceClient(mcps, c.Client.Apiv2().Version(), opts...)

// Admin Clients
adminv2mcp.ForwardToAuditServiceClient(mcps, c.Client.Adminv2().Audit(), opts...)
adminv2mcp.ForwardToComponentServiceClient(mcps, c.Client.Adminv2().Component(), opts...)
adminv2mcp.ForwardToFilesystemServiceClient(mcps, c.Client.Adminv2().Filesystem(), opts...)
adminv2mcp.ForwardToImageServiceClient(mcps, c.Client.Adminv2().Image(), opts...)
adminv2mcp.ForwardToIPServiceClient(mcps, c.Client.Adminv2().IP(), opts...)
adminv2mcp.ForwardToMachineServiceClient(mcps, c.Client.Adminv2().Machine(), opts...)
adminv2mcp.ForwardToNetworkServiceClient(mcps, c.Client.Adminv2().Network(), opts...)
adminv2mcp.ForwardToPartitionServiceClient(mcps, c.Client.Adminv2().Partition(), opts...)
adminv2mcp.ForwardToProjectServiceClient(mcps, c.Client.Adminv2().Project(), opts...)
adminv2mcp.ForwardToSizeImageConstraintServiceClient(mcps, c.Client.Adminv2().SizeImageConstraint(), opts...)
adminv2mcp.ForwardToSizeReservationServiceClient(mcps, c.Client.Adminv2().SizeReservation(), opts...)
adminv2mcp.ForwardToSizeServiceClient(mcps, c.Client.Adminv2().Size(), opts...)
adminv2mcp.ForwardToSwitchServiceClient(mcps, c.Client.Adminv2().Switch(), opts...)
adminv2mcp.ForwardToTaskServiceClient(mcps, c.Client.Adminv2().Task(), opts...)
adminv2mcp.ForwardToTenantServiceClient(mcps, c.Client.Adminv2().Tenant(), opts...)
adminv2mcp.ForwardToTokenServiceClient(mcps, c.Client.Adminv2().Token(), opts...)
adminv2mcp.ForwardToVPNServiceClient(mcps, c.Client.Adminv2().VPN(), opts...)
}
47 changes: 47 additions & 0 deletions cmd/mcp/mcp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package mcp

import (
"log"
"os/exec"
"testing"

"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/require"
)

func TestMCPServer(t *testing.T) {
t.SkipNow()
ctx := t.Context()
// Create a new client, with no features.
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)

// Connect to a server over stdin/stdout.
transport := &mcp.CommandTransport{Command: exec.Command("/home/stefan/bin/metalctlv2", "mcp")}
session, err := client.Connect(ctx, transport, nil)
require.NoError(t, err)
defer func() {
_ = session.Close()
}()

tools, err := session.ListTools(ctx, &mcp.ListToolsParams{})
require.NoError(t, err)
for _, tool := range tools.Tools {
t.Logf("%s", tool.Name)
t.Logf("%#v", tool)
}

// Call a tool on the server.
params := &mcp.CallToolParams{
Name: "metalstack_api_v2_PartitionService_List",
Arguments: map[string]any{},
}
res, err := session.CallTool(ctx, params)
require.NoError(t, err)
if res.IsError {
t.Logf("tool failed %v", res.Content)
}
for _, c := range res.Content {
log.Print(c.(*mcp.TextContent).Text)
}
t.Fail()
}
3 changes: 2 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

adminv2 "github.com/metal-stack/cli/cmd/admin/v2"
apiv2 "github.com/metal-stack/cli/cmd/api/v2"
"github.com/metal-stack/cli/cmd/mcp"

"github.com/metal-stack/cli/cmd/completion"
"github.com/metal-stack/cli/cmd/config"
Expand Down Expand Up @@ -98,7 +99,7 @@ func NewRootCmd(c *config.Config) *cobra.Command {
},
}

rootCmd.AddCommand(newContextCmd(c), markdownCmd, newLoginCmd(c), newLogoutCmd(c))
rootCmd.AddCommand(newContextCmd(c), markdownCmd, newLoginCmd(c), newLogoutCmd(c), mcp.NewMCPCmd(c))
adminv2.AddCmds(rootCmd, c)
apiv2.AddCmds(rootCmd, c)

Expand Down
1 change: 1 addition & 0 deletions docs/metalctlv2.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ cli for managing entities in metal-stack
* [metalctlv2 logout](metalctlv2_logout.md) - logout
* [metalctlv2 machine](metalctlv2_machine.md) - manage machine entities
* [metalctlv2 markdown](metalctlv2_markdown.md) - create markdown documentation
* [metalctlv2 mcp](metalctlv2_mcp.md) - start mcp server
* [metalctlv2 network](metalctlv2_network.md) - manage network entities
* [metalctlv2 partition](metalctlv2_partition.md) - manage partition entities
* [metalctlv2 project](metalctlv2_project.md) - manage project entities
Expand Down
56 changes: 56 additions & 0 deletions docs/metalctlv2_mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## metalctlv2 mcp

start mcp server

### Synopsis


Use "metalctlv2" to serve as mcp server. You must configure your coding agent to make use of the mcp server.

Example opencode.json:

"mcp": {
"metal": {
"type": "local",
"command": [
"metalctlv2",
"mcp"
],
"enabled": true,
}
}

Then login with "metalctlv2" and start you coding agent and ask questions like:

- list all metal partitions
- give me all available metal sizes and images
- create a ip address, ask me questions


```
metalctlv2 mcp [flags]
```

### Options

```
-h, --help help for mcp
```

### Options inherited from parent commands

```
--api-token string the token used for api requests
--api-url string the url to the metal-stack.io api
-c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml)
--debug debug output
--force-color force colored output even without tty
-o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table")
--template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference.
--timeout duration request timeout used for api requests
```

### SEE ALSO

* [metalctlv2](metalctlv2.md) - cli for managing entities in metal-stack

16 changes: 12 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ require (
github.com/fatih/color v1.19.0
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/metal-stack/api v0.4.4
github.com/metal-stack/api v0.4.5-0.20260817075741-42bd06831657
github.com/metal-stack/metal-lib v0.26.3
github.com/metal-stack/v v1.0.3
github.com/modelcontextprotocol/go-sdk v1.7.0
github.com/redpanda-data/protoc-gen-go-mcp v0.0.0-20260729122341-6334690a62b6
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
Expand All @@ -23,6 +25,7 @@ require (

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260709200747-435963d16310.1 // indirect
buf.build/gen/go/redpandadata/common/protocolbuffers/go v1.36.12-20260323171043-6e06f84ad823.1 // indirect
buf.build/go/protovalidate v1.3.0 // indirect
buf.build/go/protoyaml v0.7.0 // indirect
cel.dev/expr v0.25.3 // indirect
Expand Down Expand Up @@ -54,6 +57,7 @@ require (
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.31.0 // indirect
github.com/google/jsonschema-go v0.4.3 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
Expand All @@ -75,20 +79,24 @@ require (
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/pires/go-proxyproto v0.15.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/redpanda-data/common-go/api v0.0.0-20260805234448-c124c21fdb6b // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/safchain/ethtool v0.7.0 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/hujson v0.0.0-20260727124030-b80ff77dac4f // indirect
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd // indirect
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20251127225136-f19339b67368 // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
github.com/tailscale/wireguard-go v0.0.0-20260715223240-2e01ba5b00f0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
Expand All @@ -103,7 +111,7 @@ require (
golang.org/x/text v0.41.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260810153831-ec0a7760b754 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260810153831-ec0a7760b754 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
Expand Down
Loading
Loading