Skip to content

Commit 96772c1

Browse files
authored
Merge pull request #37 from datum-cloud/mcp_add_change_context_and_cruds
mcp phase 2: add context switch and crud tools
2 parents a4c6193 + 3f22f02 commit 96772c1

File tree

7 files changed

+1363
-22
lines changed

7 files changed

+1363
-22
lines changed

README.md

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Use `datumctl` to manage your Datum Cloud resources, authenticate securely, and
1212
* **Multi-User Support:** Manage credentials for multiple Datum Cloud user accounts.
1313
* **Resource Management:** Interact with Datum Cloud resources (e.g., list organizations).
1414
* **Kubernetes Integration:** Seamlessly configure `kubectl` to use your Datum Cloud credentials for accessing Kubernetes clusters.
15-
* **MCP Server (optional):** Start an MCP server (`datumctl mcp`) for Datum Cloud so AI agents (e.g., Claude) can discover resources, inspect schemas, and validate manifests via server-side dry-run.
15+
* **MCP Server (optional):** Start an MCP server (`datumctl mcp`) for Datum Cloud so AI agents (e.g., Claude) can discover resources, inspect schemas, validate manifests, and perform CRUD operations via server-side dry-run.
1616
* **Cross-Platform:** Pre-built binaries available for Linux, macOS, and Windows.
1717

1818
## Getting Started
@@ -46,9 +46,9 @@ See the [Installation Guide](./docs/user/installation.md) for detailed instructi
4646
```
4747
Now you can use `kubectl` to interact with your Datum Cloud control plane.
4848

49-
### Project setup (required for MCP)
49+
### MCP Setup
5050

51-
MCP typically targets a **project** control plane. You need at least one project and its **Project ID** (the Kubernetes resource name).
51+
MCP can target either an **organization** or **project** control plane. For maximum flexibility, we recommend starting with an organization context.
5252

5353
**A) If you already have a project:**
5454
```bash
@@ -92,23 +92,31 @@ datumctl mcp --organization <org-id> --namespace <ns> [--port 8080]
9292
datumctl mcp --project <project-id> --namespace <ns> [--port 8080]
9393
```
9494

95+
##### Available Tools
96+
97+
- **Discovery:** `list_crds`, `get_crd` - Discover and inspect Custom Resource Definitions
98+
- **Validation:** `validate_yaml` - Validate manifests via server-side dry-run
99+
- **Context:** `change_context` - Switch between organization and project contexts
100+
- **CRUD Operations:** `create_resource`, `get_resource`, `update_resource`, `delete_resource`, `list_resources`
101+
- **Safety:** All write operations default to dry-run mode; use `dryRun: false` to apply changes
102+
95103
##### Startup & safety
96104

97105
- **Preflight:** On startup, `datumctl mcp` verifies connectivity and auth by calling Kubernetes discovery (e.g., `GET /version`). If this check fails, the server exits.
98-
- **Read-only:** All operations are validation-only and use server-side dry-run (`dryRun=All`). No resources are created, modified, or deleted.
106+
- **Dry-run by default:** All write operations use server-side dry-run (`dryRun=true`) by default for safety.
99107

100108
> [!NOTE]
101109
> The MCP server builds its own Kubernetes connection for the selected Datum context; it does **not** depend on your local kubeconfig or `--kube-context`. Provide either `--organization` or `--project`.
102110

103-
##### Scope: project vs. organization
111+
##### Scope: organization vs. project
104112

105113
> [!IMPORTANT]
106-
> Most Kubernetes operations exposed via MCP (e.g., CRD discovery and server-side dry-run validation) are **project-scoped**.
107-
> Running MCP at **organization** scope will typically show only org-level resources; attempts to validate project-level CRDs may return **HTTP 401/Forbidden** or appear missing.
114+
> **Organization scope** provides access to all projects within the organization and allows switching between them using `change_context`.
115+
> **Project scope** provides direct access to project-specific resources but limits visibility to that single project.
108116

109-
**Recommended (project scope)**
117+
**Recommended (organization scope)**
110118
```bash
111-
datumctl mcp --project <project-id> --namespace <ns> [--port 8080]
119+
datumctl mcp --organization <org-id> --namespace <ns> [--port 8080]
112120
```
113121

114122
##### Claude config (macOS)
@@ -117,24 +125,27 @@ datumctl mcp --project <project-id> --namespace <ns> [--port 8080]
117125
"mcpServers": {
118126
"datum_mcp": {
119127
"command": "/absolute/path/to/datumctl",
120-
"args": ["mcp","--project","<project-id>","--namespace","<ns>"]
128+
"args": ["mcp", "--organization", "your-org-id", "--namespace", "default"]
121129
}
122130
}
123131
}
124132
```
125133

126-
**Organization scope**
134+
**Project scope (alternative)**
127135
```bash
128-
datumctl mcp --organization <org-id> --namespace <ns> [--port 8080]
136+
datumctl mcp --project <project-id> --namespace <ns> [--port 8080]
129137
```
130138

131139
**HTTP debug (if `--port` is set):**
132140
```bash
133141
# List CRDs
134142
curl -s localhost:8080/datum/list_crds | jq
135143
144+
# List resources
145+
curl -s localhost:8080/datum/list_resources -H 'Content-Type: application/json' -d '{"kind":"Project"}' | jq
146+
136147
# Validate a YAML file (wrap safely into JSON)
137-
printf '{"yaml":%s}\n' "$(jq -Rs . </path/to/file.yaml)" | curl -s -X POST localhost:8080/datum/validate_yaml -H 'Content-Type: application/json' -d @- | jq
148+
printf '{"yaml":%s}\n' "$(jq -Rs . </path/to/file.yaml)" | curl -s -X POST localhost:8080/datum/validate_yaml -H 'Content-Type: application/json' -d @- | jq
138149
```
139150

140151
For more detailed tool setup instructions, refer to the official

internal/client/context_switch.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
8+
"golang.org/x/oauth2"
9+
"k8s.io/client-go/rest"
10+
11+
"go.datum.net/datumctl/internal/authutil"
12+
)
13+
14+
func NewForProject(ctx context.Context, projectID, defaultNamespace string) (*K8sClient, error) {
15+
cfg, err := restConfigFor(ctx, "", projectID)
16+
if err != nil {
17+
return nil, err
18+
}
19+
k, err := NewK8sFromRESTConfig(cfg)
20+
if err != nil {
21+
return nil, err
22+
}
23+
k.Namespace = defaultNamespace
24+
return k, nil
25+
}
26+
27+
func NewForOrg(ctx context.Context, orgID, defaultNamespace string) (*K8sClient, error) {
28+
cfg, err := restConfigFor(ctx, orgID, "")
29+
if err != nil {
30+
return nil, err
31+
}
32+
k, err := NewK8sFromRESTConfig(cfg)
33+
if err != nil {
34+
return nil, err
35+
}
36+
k.Namespace = defaultNamespace
37+
return k, nil
38+
}
39+
40+
func restConfigFor(ctx context.Context, organizationID, projectID string) (*rest.Config, error) {
41+
tknSrc, err := authutil.GetTokenSource(ctx)
42+
if err != nil {
43+
return nil, fmt.Errorf("get token source: %w", err)
44+
}
45+
apiHostname, err := authutil.GetAPIHostname()
46+
if err != nil {
47+
return nil, fmt.Errorf("get API hostname: %w", err)
48+
}
49+
50+
var host string
51+
switch {
52+
case organizationID != "" && projectID == "":
53+
host = fmt.Sprintf("https://%s/apis/resourcemanager.miloapis.com/v1alpha1/organizations/%s/control-plane",
54+
apiHostname, organizationID)
55+
case projectID != "" && organizationID == "":
56+
host = fmt.Sprintf("https://%s/apis/resourcemanager.miloapis.com/v1alpha1/projects/%s/control-plane",
57+
apiHostname, projectID)
58+
default:
59+
return nil, fmt.Errorf("exactly one of organizationID or projectID must be provided")
60+
}
61+
62+
return &rest.Config{
63+
Host: host,
64+
WrapTransport: func(rt http.RoundTripper) http.RoundTripper {
65+
return &oauth2.Transport{Source: tknSrc, Base: rt}
66+
},
67+
}, nil
68+
}

0 commit comments

Comments
 (0)