Skip to content
Merged
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
4 changes: 4 additions & 0 deletions descriptions/descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const CreateVolume = `Create a volume on a cluster by cluster name.`
const DeleteVolume = `Delete a volume on a cluster by cluster name.`
const UpdateVolume = `Update volume name, size, state, nfs export policy of volume on a cluster by cluster name.`

const CreateSnapshot = `Create a snapshot of a volume on a cluster by cluster name.`
const DeleteSnapshot = `Delete a snapshot of a volume on a cluster by cluster name.`
const RestoreSnapshot = `Restore a volume to a snapshot on a cluster by cluster name.`

const CreateSnapshotPolicy = `Create a snapshot policy on a cluster by cluster name.`
const UpdateSnapshotPolicy = `Update a snapshot policy on a cluster by cluster name.`
const DeleteSnapshotPolicy = `Delete a snapshot policy on a cluster by cluster name.`
Expand Down
446 changes: 187 additions & 259 deletions docs/examples.md

Large diffs are not rendered by default.

121 changes: 102 additions & 19 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,122 @@

The following tools are provided by the ONTAP MCP server.

ONTAP MCP provides a set of tools that can be used to interact with the ONTAP API. These tools are designed to help users discover and manage their ONTAP clusters more efficiently. The tools are categorized based on their functionality, such as API discovery, volume management, data protection, CIFS/SMB integration, NFS export policy management, performance management, SVM management, qtree management, network interface management, LUN and igroup management, iSCSI management, FCP management, NVMe management, and multi-cluster management.

All ONTAP MCP tools are annotated with hint metadata: `readOnlyHint`, `idempotentHint`, and `destructiveHint`. The `readOnlyHint` indicates that the tool does not modify any data and is safe to use for discovery and information retrieval. The `destructiveHint` indicates that the tool performs actions that can modify or delete data, and should be used with caution.
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph says the destructiveHint indicates tools that "modify or delete data", but in the codebase create operations set DestructiveHint to false (e.g., create_volume/create_snapshot use createAnnotation). Consider rewording this to match the actual hint semantics used here (or updating the annotations if the docs description is the intended behavior).

Suggested change
All ONTAP MCP tools are annotated with hint metadata: `readOnlyHint`, `idempotentHint`, and `destructiveHint`. The `readOnlyHint` indicates that the tool does not modify any data and is safe to use for discovery and information retrieval. The `destructiveHint` indicates that the tool performs actions that can modify or delete data, and should be used with caution.
All ONTAP MCP tools are annotated with hint metadata: `readOnlyHint`, `idempotentHint`, and `destructiveHint`. The `readOnlyHint` indicates that the tool does not modify any data and is safe to use for discovery and information retrieval. The `destructiveHint` indicates that the tool performs potentially destructive or hard-to-reverse actions, such as deleting data, and should be used with caution.

Copilot uses AI. Check for mistakes.

If you want to run the ONTAP MCP server in read-only mode, you can start the server with the `--read-only` flag. In this mode, only tools with the `readOnlyHint` will be available for use, ensuring that no modifications can be made to the ONTAP cluster. See the [configuration documentation](install.md#configuration) for more details on how to start the server in read-only mode.

## API Discovery

- `list_ontap_endpoints` (available when the API catalog is loaded)
- `search_ontap_endpoints` (available when the API catalog is loaded)
- `describe_ontap_endpoint` (available when the API catalog is loaded)
- `ontap_get`

## Volume Management

- Volume lifecycle management: create, read, update, delete, resize
- Volume autogrowth: enable, disable, status
- Volume updates (multiple properties in a single operation)
- QoS policy and snapshot policy assignment
- NFS access control with export policies
- Volume details: capacity, usage
- `create_volume`
- `update_volume`
- `delete_volume`

## Data Protection

- Snapshot policies with flexible scheduling
- Snapshot schedules
- Policy application to SVMs
- `create_snapshot`
- `delete_snapshot`
- `restore_snapshot`
- `create_snapshot_policy`
- `update_snapshot_policy`
- `delete_snapshot_policy`
- `create_schedule`
- `add_schedule_in_snapshot_policy`
- `update_schedule_in_snapshot_policy`
- `remove_schedule_in_snapshot_policy`

## CIFS/SMB Integration

- CIFS list management: create, read, update, delete
- Integration with volume provisioning
- `create_cifs_share`
- `update_cifs_share`
- `delete_cifs_share`

## NFS Export Policy Management

- Export policy management: create, read, update, delete
- Volume-to-policy association
- `create_nfs_export_policies`
- `update_nfs_export_policies`
- `delete_nfs_export_policies`
- `create_nfs_export_policies_rules`
- `update_nfs_export_policies_rules`
- `delete_nfs_export_policies_rules`

## Performance Management

- QoS policy management: create, read, update, delete
- QoS policy assignment to SVMs
- Fixed QoS policies with IOPS/bandwidth limits
- Adaptive QoS policies with dynamic scaling
- `list_qos_policies`
- `create_qos_policy`
- `update_qos_policy`
- `delete_qos_policy`

## SVM Management

- `create_svm`
- `update_svm`
- `delete_svm`

## Qtree Management

- `create_qtree`
- `update_qtree`
- `delete_qtree`

## Network Interface Management

- `create_network_ip_interface`
- `update_network_ip_interface`
- `delete_network_ip_interface`

## LUN and igroup Management

- `create_lun`
- `update_lun`
- `delete_lun`
- `create_igroup`
- `update_igroup`
- `delete_igroup`
- `add_igroup_initiator`
- `remove_igroup_initiator`
- `create_lun_map`
- `delete_lun_map`

## iSCSI Management

- `create_iscsi_service`
- `update_iscsi_service`
- `delete_iscsi_service`

## FCP Management

- `create_fcp_service`
- `update_fcp_service`
- `delete_fcp_service`
- `create_fc_interface`
- `update_fc_interface`
- `delete_fc_interface`

## NVMe Management

- `create_nvme_service`
- `update_nvme_service`
- `delete_nvme_service`
- `create_nvme_subsystem`
- `update_nvme_subsystem`
- `delete_nvme_subsystem`
- `add_nvme_subsystem_host`
- `remove_nvme_subsystem_host`
- `create_nvme_namespace`
- `update_nvme_namespace`
- `delete_nvme_namespace`
- `create_nvme_subsystem_map`
- `delete_nvme_subsystem_map`

## Multi-Cluster Management

- Unified management of multiple ONTAP clusters
- Centralized credential management
- `list_registered_clusters`
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"github.com/netapp/ontap-mcp/config"
)

func TestSnapshot(t *testing.T) {
func TestSnapshotPolicy(t *testing.T) {
SkipIfMissing(t, CheckTools)

tests := []struct {
Expand Down
177 changes: 177 additions & 0 deletions integration/test/snapshot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package main

import (
"context"
"crypto/tls"
"log/slog"
"net/http"
"slices"
"testing"
"time"

"github.com/netapp/ontap-mcp/ontap"

"github.com/carlmjohnson/requests"
"github.com/netapp/ontap-mcp/config"
)

func TestSnapshot(t *testing.T) {
SkipIfMissing(t, CheckTools)

tests := []struct {
name string
input string
expectedOntapErr string
verifyAPI ontapVerifier
}{
{
name: "Clean SVM",
input: ClusterStr + "delete " + rn("marketing") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: deleteObject},
},
{
name: "Create SVM",
input: ClusterStr + "create " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: createObject},
},
{
name: "Clean volume",
input: ClusterStr + "delete volume " + rn("docs") + " in " + rn("marketing") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: deleteObject},
},
{
name: "Create volume",
input: ClusterStr + "create a 20MB volume named " + rn("docs") + " on the " + rn("marketing") + " svm and the harvest_vc_aggr aggregate",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: createObject},
},
{
name: "Clean snapshot",
input: ClusterStr + "Delete " + rn("localsnap") + " snapshot in " + rn("docs") + " volume in " + rn("marketing") + " svm",
expectedOntapErr: "does not exist",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: verifySnapshot([]string{}, false, 0)},
},
{
name: "Create snapshot",
input: ClusterStr + "create a snapshot named " + rn("localsnap") + " in the volume " + rn("docs") + " on the " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: verifySnapshot([]string{rn("localsnap")}, true, 1)},
},
{
name: "Create 2nd snapshot",
input: ClusterStr + "create a snapshot named " + rn("recentsnap") + " in the volume " + rn("docs") + " on the " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: verifySnapshot([]string{rn("recentsnap"), rn("localsnap")}, true, 2)},
Comment on lines +43 to +67
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The volume verification queries in this test use the filter &svm=<name>, but the rest client and other integration tests use svm.name for volume scoping. Consider switching this to &svm.name= (and updating the other occurrences in this file) to keep the tests aligned with the API filter convention used elsewhere.

Suggested change
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: deleteObject},
},
{
name: "Create volume",
input: ClusterStr + "create a 20MB volume named " + rn("docs") + " on the " + rn("marketing") + " svm and the harvest_vc_aggr aggregate",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: createObject},
},
{
name: "Clean snapshot",
input: ClusterStr + "Delete " + rn("localsnap") + " snapshot in " + rn("docs") + " volume in " + rn("marketing") + " svm",
expectedOntapErr: "does not exist",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: verifySnapshot([]string{}, false, 0)},
},
{
name: "Create snapshot",
input: ClusterStr + "create a snapshot named " + rn("localsnap") + " in the volume " + rn("docs") + " on the " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: verifySnapshot([]string{rn("localsnap")}, true, 1)},
},
{
name: "Create 2nd snapshot",
input: ClusterStr + "create a snapshot named " + rn("recentsnap") + " in the volume " + rn("docs") + " on the " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: verifySnapshot([]string{rn("recentsnap"), rn("localsnap")}, true, 2)},
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject},
},
{
name: "Create volume",
input: ClusterStr + "create a 20MB volume named " + rn("docs") + " on the " + rn("marketing") + " svm and the harvest_vc_aggr aggregate",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm.name=" + rn("marketing"), validationFunc: createObject},
},
{
name: "Clean snapshot",
input: ClusterStr + "Delete " + rn("localsnap") + " snapshot in " + rn("docs") + " volume in " + rn("marketing") + " svm",
expectedOntapErr: "does not exist",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm.name=" + rn("marketing"), validationFunc: verifySnapshot([]string{}, false, 0)},
},
{
name: "Create snapshot",
input: ClusterStr + "create a snapshot named " + rn("localsnap") + " in the volume " + rn("docs") + " on the " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm.name=" + rn("marketing"), validationFunc: verifySnapshot([]string{rn("localsnap")}, true, 1)},
},
{
name: "Create 2nd snapshot",
input: ClusterStr + "create a snapshot named " + rn("recentsnap") + " in the volume " + rn("docs") + " on the " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm.name=" + rn("marketing"), validationFunc: verifySnapshot([]string{rn("recentsnap"), rn("localsnap")}, true, 2)},

Copilot uses AI. Check for mistakes.
},
{
name: "Restore volume from snapshot",
input: ClusterStr + "Restore " + rn("docs") + " volume from a snapshot named " + rn("localsnap") + " in the " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: verifySnapshot([]string{rn("localsnap")}, true, 1)},
},
{
name: "Clean snapshot",
input: ClusterStr + "Delete " + rn("localsnap") + " snapshot in " + rn("docs") + " volume in " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: verifySnapshot([]string{}, false, 0)},
},
{
name: "Clean volume",
input: ClusterStr + "delete volume " + rn("docs") + " in " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: deleteObject},
},
{
name: "Clean SVM",
input: ClusterStr + "delete " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: deleteObject},
},
}

cfg, err := config.ReadConfig(ConfigFile)
if err != nil {
t.Fatalf("Error parsing the config: %v", err)
}

poller := cfg.Pollers[Cluster]
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: poller.UseInsecureTLS, // #nosec G402
},
}
client := &http.Client{Transport: transport, Timeout: 10 * time.Second}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
slog.Debug("", slog.String("Input", tt.input))
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
if _, err = testAgent.ChatWithResponse(ctx, t, tt.input, tt.expectedOntapErr); err != nil {
t.Fatalf("Error processing input %q: %v", tt.input, err)
}
if tt.verifyAPI.api != "" && !tt.verifyAPI.validationFunc(t, tt.verifyAPI.api, poller, client) {
t.Errorf("Error while accessing the object via prompt %s", tt.input)
}
})
}
}

func verifySnapshot(snapshotNames []string, exist bool, snapshotCount int) func(t *testing.T, api string, poller *config.Poller, client *http.Client) bool {
return func(t *testing.T, api string, poller *config.Poller, client *http.Client) bool {
var data ontap.GetData
var gotSnapshotNames []string
err := requests.URL("https://"+poller.Addr+"/"+api).
BasicAuth(poller.Username, poller.Password).
Client(client).
ToJSON(&data).
Fetch(context.Background())
Comment on lines +127 to +131
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verifySnapshot uses context.Background() for HTTP requests. Because the caller already has a timeout context for each subtest, these requests can ignore cancellation and potentially hang the test run. Consider threading through a context with timeout (or using t.Context() in newer Go) so verification is bounded.

Copilot uses AI. Check for mistakes.
if err != nil {
t.Errorf("verifySnapshot: request failed: %v", err)
return false
}
if data.NumRecords != 1 {
t.Errorf("verifySnapshot: expected %d records, got %d", snapshotCount, data.NumRecords)
return false
}
volumeUUID := data.Records[0].UUID

err = requests.URL("https://"+poller.Addr+"/"+"api/storage/volumes/"+volumeUUID+"/snapshots").
BasicAuth(poller.Username, poller.Password).
Client(client).
ToJSON(&data).
Fetch(context.Background())
if err != nil {
t.Errorf("verifySnapshot: request failed: %v", err)
return false
}

if !exist {
if data.NumRecords != 0 {
t.Errorf("verifySnapshot: expected 0 record, got %d", data.NumRecords)
return false
}
return true
}
if data.NumRecords != snapshotCount {
t.Errorf("verifySnapshot: expected 1 record, got %d", data.NumRecords)
return false
}

for _, record := range data.Records {
gotSnapshotNames = append(gotSnapshotNames, record.Name)
}

slices.Sort(gotSnapshotNames)
slices.Sort(snapshotNames)
if !slices.Equal(gotSnapshotNames, snapshotNames) {
t.Errorf("verifySnapshot: snapshot name = %q, want %q", gotSnapshotNames, snapshotNames)
return false
}

return true
}
}
12 changes: 12 additions & 0 deletions ontap/ontap.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,18 @@ type SnapshotPolicySchedule struct {
SnapmirrorLabel string `json:"snapmirror_label,omitzero" jsonschema:"SnapMirror label for this schedule"`
}

type Snapshot struct {
Name string `json:"name" jsonschema:"snapshot name"`
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New ONTAP model types typically use the same json tag options as the rest of this file (e.g. ,omitzero on most fields). Snapshot.Name is missing ,omitzero, which is inconsistent with surrounding types and may change marshaling behavior depending on the JSON encoder in use.

Suggested change
Name string `json:"name" jsonschema:"snapshot name"`
Name string `json:"name,omitzero" jsonschema:"snapshot name"`

Copilot uses AI. Check for mistakes.
}

type RestoreTo struct {
Snapshot Snapshot `json:"snapshot" jsonschema:"restore volume with a snapshot"`
}

type SnapshotRestore struct {
RestoreTo RestoreTo `json:"restore_to" jsonschema:"which snapshot to restore"`
Comment on lines +165 to +169
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RestoreTo.Snapshot and SnapshotRestore.RestoreTo are also missing the ,omitzero tag option used widely in this file. For consistency (and to avoid unintentionally sending zero-value nested objects), consider adding ,omitzero to these fields as well.

Suggested change
Snapshot Snapshot `json:"snapshot" jsonschema:"restore volume with a snapshot"`
}
type SnapshotRestore struct {
RestoreTo RestoreTo `json:"restore_to" jsonschema:"which snapshot to restore"`
Snapshot Snapshot `json:"snapshot,omitzero" jsonschema:"restore volume with a snapshot"`
}
type SnapshotRestore struct {
RestoreTo RestoreTo `json:"restore_to,omitzero" jsonschema:"which snapshot to restore"`

Copilot uses AI. Check for mistakes.
}

type Cron struct {
Days []int `json:"days,omitzero"`
Hours []int `json:"hours,omitzero"`
Expand Down
Loading
Loading