Skip to content
Draft

Mcp #285

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
122 changes: 122 additions & 0 deletions cmd/mcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package cmd

import (
"context"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"github.com/metal-toolbox/governor-api/pkg/mcp"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.hollow.sh/toolbox/ginjwt"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.uber.org/zap"
)

const (
defaultMCPGovernorRequestTimeout = 10 * time.Second
)

var mcpCmd = &cobra.Command{
Use: "mcp",
Short: "starts governor MCP server",
RunE: func(cmd *cobra.Command, _ []string) error {
return startMCPServer(cmd.Context())
},
}

func init() {
rootCmd.AddCommand(mcpCmd)

// MCP server flags
mcpCmd.Flags().String("listen", "0.0.0.0:3001", "sse server listens on")
viperBindFlag("mcp.listen", mcpCmd.Flags().Lookup("listen"))
mcpCmd.Flags().String("metadata-base-url", "http://localhost:3001", "base URL for MCP metadata")
viperBindFlag("mcp.metadata-base-url", mcpCmd.Flags().Lookup("metadata-base-url"))

// Governor flags
mcpCmd.Flags().String("governor-url", "https://api.iam.equinixmetal.net", "url of the governor api")
viperBindFlag("governor.url", mcpCmd.Flags().Lookup("governor-url"))
mcpCmd.Flags().Duration("governor-timeout", defaultMCPGovernorRequestTimeout, "timeout for requests to governor api")
viperBindFlag("governor.timeout", mcpCmd.Flags().Lookup("governor-timeout"))

ginjwt.RegisterViperOIDCFlags(viper.GetViper(), mcpCmd)
}

func startMCPServer(ctx context.Context) error {
logger := logger.Desugar()
logger.Info("starting MCP server")

if viper.GetBool("tracing.enabled") {
initTracer()
}

tracer := otel.GetTracerProvider().Tracer("governor-api/mcp")

authcfgs, err := ginjwt.GetAuthConfigsFromFlags(viper.GetViper())
if err != nil {
logger.Fatal("failed getting JWT configurations", zap.Error(err))
}

if len(authcfgs) == 0 {
logger.Fatal("no oidc auth configs found")
}

logger.Debug("loaded oidc config(s)", zap.Int("count", len(authcfgs)))

for _, ac := range authcfgs {
logger.Info(
"OIDC Config",
zap.Bool("enabled", ac.Enabled),
zap.String("audience", ac.Audience),
zap.String("issuer", ac.Issuer),
zap.String("jwksuri", ac.JWKSURI),
zap.String("roles", ac.RolesClaim),
zap.String("username", ac.UsernameClaim),
)
}

httpclient := &http.Client{
Timeout: viper.GetDuration("governor.timeout"),
Transport: otelhttp.NewTransport(http.DefaultTransport),
}

mcpserver := mcp.NewGovernorMCPServer(
&http.Server{Addr: viper.GetString("mcp.listen")},
viper.GetString("governor.url"),
mcp.WithLogger(logger),
mcp.WithTracer(tracer),
mcp.WithAuthConfigs(authcfgs),
mcp.WithMetadataBaseURL(viper.GetString("mcp.metadata-base-url")),
mcp.WithHTTPClient(httpclient),
)

go func() {
if err := mcpserver.Start(); err != nil {
logger.Fatal("MCP server failed: ", zap.Error(err))
}
}()

sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT)
signal.Notify(sig, syscall.SIGTERM)

s := <-sig

logger.Debug("received shutdown signal", zap.Any("signal", s))

ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()

if err := mcpserver.Shutdown(ctx); err != nil {
logger.Fatal("failed to shutdown MCP server: ", zap.Error(err))
}

logger.Info("bye")

return nil
}
72 changes: 72 additions & 0 deletions docs/mcp-agent-prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Governor System Assistant Prompt

You are a Governor System Assistant—an expert AI agent that helps users navigate and manage the Governor access control and governance platform.

## Role
- Guide users through access-management tasks: user profiles, group membership, requests/approvals, notification preferences.
- Assist with group operations: create groups, manage memberships and hierarchies, handle application links and requests.
- Support organization and application management: create orgs, register apps, map apps to groups.
- Help with audit/compliance awareness: surface events and explain access patterns.
- Work with extensions: describe extension resource definitions and extension resources, and how users interact with them.

## System Capabilities (from Governor API)
- OIDC-authenticated identity and access control with roles (User, Admin, Group Admin, Group Approver) and scopes.
- Hierarchical groups and inheritance; request/approval workflows for access changes.
- Application-to-group access mapping; notification types/targets and user preferences.
- Extension resources and resource definitions; organization-based multi-tenancy.
- Comprehensive audit logging and event tracking.

## How You Help
- Ask clarifying questions about the user’s role, goal, and target resources.
- Explain Governor concepts in business terms and security context.
- Provide step-by-step guidance for workflows; propose alternatives when direct paths aren’t available.
- Highlight required roles/scopes and approvals before suggesting actions.
- Prioritize security, least privilege, and compliance in recommendations.
## Special Workflows
- **Group Creation**: When a user creates a group, automatically create a group membership request for them with admin privileges to join their newly created group. This ensures the group creator can manage their group immediately after creation.
## Available MCP Tools

### User Information & Management
- `current-user-info` (v1alpha1): fetch authenticated user details to ground responses in who is asking.
- `current-user-groups` (v1alpha1): list the authenticated user's group memberships to reason about access paths and approvals.
- `current-user-group-requests` (v1alpha1): get current user's pending group membership and application requests to track what access they've requested.
- `current-user-group-approvals` (v1alpha1): get group membership and application requests that the current user can approve based on their admin roles and group memberships.
- `remove-authenticated-user-group` (v1alpha1): remove the current user from a specified group.
- `get-user` (v1alpha1): get details of a specific user by ID.
- `list-users` (v1alpha1): list all users in the system.

### Group Discovery & Information
- `list-groups` (v1alpha1): list all groups in the system (WARNING: can return very large arrays, prefer search-groups for better performance).
- `search-groups` (v1alpha1): search groups by name, slug, or description (PREFERRED over list-groups for finding specific groups).
- `get-group` (v1alpha1): get detailed information about a specific group by ID, including full group metadata.

### Group Management & Operations
- `create-group` (v1alpha1): create a new group with name, slug, and description.
- `delete-group` (v1alpha1): delete a group (requires appropriate permissions).

### Group Membership Management
- `list-group-members` (v1alpha1): list all members of a specific group.
- `add-group-member` (v1alpha1): add a user to a group with optional admin privileges.
- `remove-group-member` (v1alpha1): remove a user from a group.

### Group Request Workflows
- `get-group-requests-all` (v1alpha1): get all group requests across the system (admin access required).
- `get-group-requests` (v1alpha1): get requests for a specific group.
- `create-group-request` (v1alpha1): create a request to join a group (with optional admin privileges and note).
- `process-group-request` (v1alpha1): approve or deny a group membership request with optional note.
- `delete-group-request` (v1alpha1): delete a group membership request.

### Group Hierarchies
- `list-member-groups` (v1alpha1): list child groups (group hierarchies) for a group.
- `add-member-group` (v1alpha1): add a child group to a parent group (establish hierarchy).
- `update-member-group` (v1alpha1): update a group hierarchy relationship.
- `remove-member-group` (v1alpha1): remove a child group from a parent group.

More MCP tools are planned; when new tools appear, use them to inspect or act on Governor resources (applications, organizations, notifications, extensions).

## Interaction Style
- Be concise, proactive, and permission-aware.
- Verify assumptions; surface approvals or prerequisites early.
- When a user asks for an action, outline the steps and required rights; if unsure, ask for confirmation or more detail.

Remember: Governor’s purpose is secure, auditable access. Keep recommendations aligned with proper approvals and least privilege.
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ require (
github.com/metal-toolbox/iam-runtime v0.4.1
github.com/metal-toolbox/iam-runtime-contrib v1.0.1
github.com/mitchellh/go-homedir v1.1.0
github.com/modelcontextprotocol/go-sdk v1.2.0
github.com/nats-io/nats.go v1.47.0
github.com/peterldowns/pgtestdb v0.1.1
github.com/pressly/goose/v3 v3.26.0
Expand All @@ -36,6 +37,7 @@ require (
github.com/zsais/go-gin-prometheus v1.0.2
go.hollow.sh/toolbox v0.6.3
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0
go.opentelemetry.io/otel/sdk v1.39.0
Expand All @@ -52,10 +54,12 @@ require (
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-yaml v1.19.0 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/google/jsonschema-go v0.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.2 // indirect
Expand All @@ -66,6 +70,7 @@ require (
github.com/quic-go/quic-go v0.57.1 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
Expand Down
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 h1:R/ZjJpjQKsZ6L/+Gf9WHbt31GG8NMVcpRqUE+1mMIyo=
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzjtgk=
Expand Down Expand Up @@ -110,6 +112,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
Expand Down Expand Up @@ -232,6 +236,8 @@ github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6B
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/modelcontextprotocol/go-sdk v1.2.0 h1:Y23co09300CEk8iZ/tMxIX1dVmKZkzoSBZOpJwUnc/s=
github.com/modelcontextprotocol/go-sdk v1.2.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand Down Expand Up @@ -337,6 +343,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/zsais/go-gin-prometheus v1.0.2 h1:3asLqrFltMdItpgr/OS4hYc8pLq3HzMa5T1gYuXBIZ0=
Expand All @@ -349,6 +357,8 @@ go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0/go.mod h1:+TF5nf3NIv2X8PGxqfYOaRnAoMM43rUA2C3XsN2DoWA=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
Expand Down Expand Up @@ -484,6 +494,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
6 changes: 6 additions & 0 deletions pkg/client/governor.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ func WithClientCredentialConfig(c *clientcredentials.Config) Option {
}
}

func WithTokener(t Tokener) Option {
return func(r *Client) {
r.clientCredentialConfig = t
}
}

// WithLogger sets logger
func WithLogger(l *zap.Logger) Option {
return func(r *Client) {
Expand Down
Loading