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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ The dashboard tools now include several strategies to manage context window usag

- **List teams:** View all configured teams in Grafana.
- **List Users:** View all users in an organization in Grafana.
- **List all roles:** List all Grafana roles, with an optional filter for delegatable roles.
- **Get role details:** Get details for a specific Grafana role by UID.
- **List assignments for a role:** List all users, teams, and service accounts assigned to a role.
- **List roles for users:** List all roles assigned to one or more users.
- **List roles for teams:** List all roles assigned to one or more teams.
- **List permissions for a resource:** List all permissions defined for a specific resource (dashboard, datasource, folder, etc.).
- **Describe a Grafana resource:** List available permissions and assignment capabilities for a resource type.

### Navigation

Expand Down Expand Up @@ -170,6 +177,13 @@ Scopes define the specific resources that permissions apply to. Each action requ
| --------------------------------- | ----------- | ------------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------- |
| `list_teams` | Admin | List all teams | `teams:read` | `teams:*` or `teams:id:1` |
| `list_users_by_org` | Admin | List all users in an organization | `users:read` | `global.users:*` or `global.users:id:123` |
| `list_all_roles` | Admin | List all Grafana roles | `roles:read` | `roles:*` |
| `get_role_details` | Admin | Get details for a Grafana role | `roles:read` | `roles:uid:editor` |
| `get_role_assignments` | Admin | List assignments for a role | `roles:read` | `roles:uid:editor` |
| `list_user_roles` | Admin | List roles for users | `roles:read` | `global.users:id:123` |
| `list_team_roles` | Admin | List roles for teams | `roles:read` | `teams:id:7` |
| `get_resource_permissions`| Admin | List permissions for a resource | `permissions:read` | `dashboards:uid:abcd1234` |
| `get_resource_description`| Admin | Describe a Grafana resource type | `permissions:read` | `dashboards:*` |
| `search_dashboards` | Search | Search for dashboards | `dashboards:read` | `dashboards:*` or `dashboards:uid:abc123` |
| `get_dashboard_by_uid` | Dashboard | Get a dashboard by uid | `dashboards:read` | `dashboards:uid:abc123` |
| `update_dashboard` | Dashboard | Update or create a new dashboard | `dashboards:create`, `dashboards:write` | `dashboards:*`, `folders:*` or `folders:uid:xyz789` |
Expand Down
187 changes: 187 additions & 0 deletions tools/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"

"github.com/grafana/grafana-openapi-client-go/client/access_control"
"github.com/grafana/grafana-openapi-client-go/client/org"
"github.com/grafana/grafana-openapi-client-go/client/teams"
"github.com/grafana/grafana-openapi-client-go/models"
Expand Down Expand Up @@ -61,7 +62,193 @@ var ListUsersByOrg = mcpgrafana.MustTool(
mcp.WithReadOnlyHintAnnotation(true),
)

type ListAllRolesParams struct {
DelegatableOnly bool `json:"delegatableOnly,omitempty" jsonschema:"description=Optional: If set true only return roles that can be delegated by current user"`
}

func listAllRoles(ctx context.Context, args ListAllRolesParams) ([]*models.RoleDTO, error) {
c := mcpgrafana.GrafanaClientFromContext(ctx)
params := access_control.NewListRolesParamsWithContext(ctx)

if args.DelegatableOnly {
delegatable := true
params.Delegatable = &delegatable
}

resp, err := c.AccessControl.ListRoles(params)
if err != nil {
return nil, fmt.Errorf("list all roles: %w", err)
}
return resp.Payload, nil
}

var ListAllRoles = mcpgrafana.MustTool(
"list_all_roles",
"List all roles in Grafana. Optionally filter to show only roles that can be delegated by the current user. Returns role details including UID, name, permissions, and metadata.",
listAllRoles,
mcp.WithTitleAnnotation("List all roles"),
mcp.WithIdempotentHintAnnotation(true),
mcp.WithReadOnlyHintAnnotation(true),
)

type GetRoleDetailsParams struct {
RoleUID string `json:"roleUID" jsonschema:"required,description=Role UID to retrieve"`
}

func getRoleDetails(ctx context.Context, args GetRoleDetailsParams) (*models.RoleDTO, error) {
c := mcpgrafana.GrafanaClientFromContext(ctx)
params := access_control.NewGetRoleParamsWithContext(ctx).WithRoleUID(args.RoleUID)

resp, err := c.AccessControl.GetRoleWithParams(params)
if err != nil {
return nil, fmt.Errorf("get role details: %w", err)
}
return resp.Payload, nil
}

var GetRoleDetails = mcpgrafana.MustTool(
"get_role_details",
"Get detailed information about a specific Grafana role by its UID, including permissions, metadata, and configuration.",
getRoleDetails,
mcp.WithTitleAnnotation("Get role details"),
mcp.WithIdempotentHintAnnotation(true),
mcp.WithReadOnlyHintAnnotation(true),
)

type GetRoleAssignmentsParams struct {
RoleUID string `json:"roleUID" jsonschema:"required,description=Role UID to retrieve"`
}

func getRoleAssignments(ctx context.Context, args GetRoleAssignmentsParams) (*models.RoleAssignmentsDTO, error) {
c := mcpgrafana.GrafanaClientFromContext(ctx)
params := access_control.NewGetRoleAssignmentsParamsWithContext(ctx).WithRoleUID(args.RoleUID)

resp, err := c.AccessControl.GetRoleAssignmentsWithParams(params)
if err != nil {
return nil, fmt.Errorf("get role assignments: %w", err)
}
return resp.Payload, nil
}

var GetRoleAssignments = mcpgrafana.MustTool(
"get_role_assignments",
"List all assignments for a specific role, showing which users, teams, and service accounts have been assigned this role.",
getRoleAssignments,
mcp.WithTitleAnnotation("Get role assignments"),
mcp.WithIdempotentHintAnnotation(true),
mcp.WithReadOnlyHintAnnotation(true),
)

type ListUserRolesParams struct {
UserIDs []int64 `json:"userIds" jsonschema:"required,description=User ID(s) to get roles for. Can be a single user or multiple users."`
}

func listUserRoles(ctx context.Context, args ListUserRolesParams) (map[string][]models.RoleDTO, error) {
c := mcpgrafana.GrafanaClientFromContext(ctx)
searchQuery := &models.RolesSearchQuery{UserIds: args.UserIDs}
params := access_control.NewListUsersRolesParamsWithContext(ctx).WithBody(searchQuery)

resp, err := c.AccessControl.ListUsersRolesWithParams(params)
if err != nil {
return nil, fmt.Errorf("list user roles: %w", err)
}
return resp.Payload, nil
}

var ListUserRoles = mcpgrafana.MustTool(
"list_user_roles",
"List all roles assigned to one or more users. Returns a map of user IDs to their assigned roles, excluding built-in roles and team-inherited roles.",
listUserRoles,
mcp.WithTitleAnnotation("List user roles"),
mcp.WithIdempotentHintAnnotation(true),
mcp.WithReadOnlyHintAnnotation(true),
)

type ListTeamRolesParams struct {
TeamIDs []int64 `json:"teamIds" jsonschema:"required,description=Team ID(s) to get roles for. Can be a single team or multiple teams."`
}

func listTeamRoles(ctx context.Context, args ListTeamRolesParams) (map[string][]models.RoleDTO, error) {
c := mcpgrafana.GrafanaClientFromContext(ctx)
searchQuery := &models.RolesSearchQuery{TeamIds: args.TeamIDs}
params := access_control.NewListTeamsRolesParamsWithContext(ctx).WithBody(searchQuery)

resp, err := c.AccessControl.ListTeamsRolesWithParams(params)
if err != nil {
return nil, fmt.Errorf("list team roles: %w", err)
}
return resp.Payload, nil
}

var ListTeamRoles = mcpgrafana.MustTool(
"list_team_roles",
"List all roles assigned to one or more teams. Returns a map of team IDs to their assigned roles.",
listTeamRoles,
mcp.WithTitleAnnotation("List team roles"),
mcp.WithIdempotentHintAnnotation(true),
mcp.WithReadOnlyHintAnnotation(true),
)

type GetResourcePermissionsParams struct {
Resource string `json:"resource" jsonschema:"required,description=Resource type (e.g. 'dashboards' 'datasources' 'folders')"`
ResourceID string `json:"resourceId" jsonschema:"required,description=Unique identifier of the resource (UID for dashboards/datasources/folders)"`
}

func getResourcePermissions(ctx context.Context, args GetResourcePermissionsParams) ([]*models.ResourcePermissionDTO, error) {
c := mcpgrafana.GrafanaClientFromContext(ctx)
params := access_control.NewGetResourcePermissionsParamsWithContext(ctx).WithResource(args.Resource).WithResourceID(args.ResourceID)

resp, err := c.AccessControl.GetResourcePermissionsWithParams(params)
if err != nil {
return nil, fmt.Errorf("get resource permissions: %w", err)
}
return resp.Payload, nil
}

var GetResourcePermissions = mcpgrafana.MustTool(
"get_resource_permissions",
"List all permissions set on a specific Grafana resource (e.g., dashboard, datasource, folder) by its type and ID.",
getResourcePermissions,
mcp.WithTitleAnnotation("Get resource permissions"),
mcp.WithIdempotentHintAnnotation(true),
mcp.WithReadOnlyHintAnnotation(true),
)

type GetResourceDescriptionParams struct {
ResourceType string `json:"resourceType" jsonschema:"required,enum=dashboards,enum=datasources,enum=folders,enum=teams,enum=users,enum=serviceaccounts,description=Type of Grafana resource to get description for"`
}

func getResourceDescription(ctx context.Context, args GetResourceDescriptionParams) (*models.Description, error) {
c := mcpgrafana.GrafanaClientFromContext(ctx)

params := access_control.NewGetResourceDescriptionParamsWithContext(ctx).
WithResource(args.ResourceType)

resp, err := c.AccessControl.GetResourceDescriptionWithParams(params)
if err != nil {
return nil, fmt.Errorf("get resource description: %w", err)
}

return resp.Payload, nil
}

var GetResourceDescription = mcpgrafana.MustTool(
"get_resource_description",
"List available permissions and assignment capabilities for a Grafana resource type.",
getResourceDescription,
mcp.WithTitleAnnotation("Get resource description"),
mcp.WithIdempotentHintAnnotation(true),
mcp.WithReadOnlyHintAnnotation(true),
)

func AddAdminTools(mcp *server.MCPServer) {
ListTeams.Register(mcp)
ListUsersByOrg.Register(mcp)
ListAllRoles.Register(mcp)
GetRoleDetails.Register(mcp)
GetRoleAssignments.Register(mcp)
ListUserRoles.Register(mcp)
ListTeamRoles.Register(mcp)
GetResourcePermissions.Register(mcp)
GetResourceDescription.Register(mcp)
}
102 changes: 102 additions & 0 deletions tools/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,60 @@ func TestAdminToolsUnit(t *testing.T) {
// Test that the tools are properly defined with correct metadata
require.NotNil(t, ListUsersByOrg, "ListUsersByOrg tool should be defined")
require.NotNil(t, ListTeams, "ListTeams tool should be defined")
require.NotNil(t, ListAllRoles, "ListAllRoles tool should be defined")
require.NotNil(t, GetRoleDetails, "GetRoleDetails tool should be defined")
require.NotNil(t, GetRoleAssignments, "GetRoleAssignments tool should be defined")
require.NotNil(t, ListUserRoles, "ListUserRoles tool should be defined")
require.NotNil(t, GetResourcePermissions, "GetResourcePermissions tool should be defined")
require.NotNil(t, GetResourceDescription, "GetResourceDescription tool should be defined")

// Verify tool metadata
assert.Equal(t, "list_users_by_org", ListUsersByOrg.Tool.Name)
assert.Equal(t, "list_teams", ListTeams.Tool.Name)
assert.Equal(t, "list_all_roles", ListAllRoles.Tool.Name)
assert.Equal(t, "get_role_details", GetRoleDetails.Tool.Name)
assert.Equal(t, "get_role_assignments", GetRoleAssignments.Tool.Name)
assert.Equal(t, "list_user_roles", ListUserRoles.Tool.Name)
assert.Equal(t, "list_team_roles", ListTeamRoles.Tool.Name)
assert.Equal(t, "get_resource_permissions", GetResourcePermissions.Tool.Name)
assert.Equal(t, "get_resource_description", GetResourceDescription.Tool.Name)
assert.Contains(t, ListUsersByOrg.Tool.Description, "List users by organization")
assert.Contains(t, ListTeams.Tool.Description, "Search for Grafana teams")
assert.Contains(t, ListAllRoles.Tool.Description, "List all roles in Grafana")
assert.Contains(t, GetRoleDetails.Tool.Description, "Get detailed information about a specific Grafana")
assert.Contains(t, GetRoleAssignments.Tool.Description, "List all assignments for a specific role")
assert.Contains(t, ListUserRoles.Tool.Description, "List all roles assigned to one or more users")
assert.Contains(t, ListTeamRoles.Tool.Description, "List all roles assigned to one or more teams")
assert.Contains(t, GetResourcePermissions.Tool.Description, "List all permissions set on a specific Grafana resource")
assert.Contains(t, GetResourceDescription.Tool.Description, "List available permissions and assignment capabilities")
})

t.Run("parameter structures", func(t *testing.T) {
// Test parameter types are correctly defined
userParams := ListUsersByOrgParams{}
teamParams := ListTeamsParams{Query: "test-query"}
roleParams := ListAllRolesParams{}
roleDetailParams := GetRoleDetailsParams{RoleUID: "r1"}
assignParams := GetRoleAssignmentsParams{RoleUID: "r2"}
userRoleParams := ListUserRolesParams{UserIDs: []int64{1, 2}}
teamRoleParams := ListTeamRolesParams{TeamIDs: []int64{3}}
permParams := GetResourcePermissionsParams{Resource: "dashboards", ResourceID: "abc"}
descParams := GetResourceDescriptionParams{ResourceType: "folders"}

// ListUsersByOrgParams should be an empty struct (no parameters required)
assert.IsType(t, ListUsersByOrgParams{}, userParams)

// ListTeamsParams should have a Query field
assert.Equal(t, "test-query", teamParams.Query)

assert.IsType(t, ListAllRolesParams{}, roleParams)
assert.Equal(t, "r1", roleDetailParams.RoleUID)
assert.Equal(t, "r2", assignParams.RoleUID)
assert.Equal(t, []int64{1, 2}, userRoleParams.UserIDs)
assert.Equal(t, []int64{3}, teamRoleParams.TeamIDs)
assert.Equal(t, "dashboards", permParams.Resource)
assert.Equal(t, "abc", permParams.ResourceID)
assert.Equal(t, "folders", descParams.ResourceType)
})

t.Run("nil client handling", func(t *testing.T) {
Expand All @@ -50,6 +86,39 @@ func TestAdminToolsUnit(t *testing.T) {
assert.Panics(t, func() {
listTeams(ctx, ListTeamsParams{})
}, "Should panic when no Grafana client in context")

assert.Panics(t, func() {
listAllRoles(ctx, ListAllRolesParams{})
}, "Should panic when no Grafana client in context")

assert.Panics(t, func() {
getRoleDetails(ctx, GetRoleDetailsParams{RoleUID: "x"})
}, "Should panic when no Grafana client in context")

assert.Panics(t, func() {
getRoleAssignments(ctx, GetRoleAssignmentsParams{RoleUID: "x"})
}, "Should panic when no Grafana client in context")

assert.Panics(t, func() {
listUserRoles(ctx, ListUserRolesParams{UserIDs: []int64{1}})
}, "Should panic when no Grafana client in context")

assert.Panics(t, func() {
listTeamRoles(ctx, ListTeamRolesParams{TeamIDs: []int64{2}})
}, "Should panic when no Grafana client in context")

assert.Panics(t, func() {
getResourcePermissions(ctx, GetResourcePermissionsParams{
Resource: "dashboards",
ResourceID: "x",
})
}, "Should panic when no Grafana client in context")

assert.Panics(t, func() {
getResourceDescription(ctx, GetResourceDescriptionParams{
ResourceType: "folders",
})
}, "Should panic when no Grafana client in context")
})

t.Run("function signatures", func(t *testing.T) {
Expand All @@ -71,5 +140,38 @@ func TestAdminToolsUnit(t *testing.T) {
assert.Panics(t, func() {
listTeams(ctx, ListTeamsParams{Query: "test"})
})

assert.Panics(t, func() {
listAllRoles(ctx, ListAllRolesParams{})
})

assert.Panics(t, func() {
getRoleDetails(ctx, GetRoleDetailsParams{RoleUID: "r1"})
})

assert.Panics(t, func() {
getRoleAssignments(ctx, GetRoleAssignmentsParams{RoleUID: "r2"})
})

assert.Panics(t, func() {
listUserRoles(ctx, ListUserRolesParams{UserIDs: []int64{1}})
})

assert.Panics(t, func() {
listTeamRoles(ctx, ListTeamRolesParams{TeamIDs: []int64{2}})
})

assert.Panics(t, func() {
getResourcePermissions(ctx, GetResourcePermissionsParams{
Resource: "dashboards",
ResourceID: "abc",
})
})

assert.Panics(t, func() {
getResourceDescription(ctx, GetResourceDescriptionParams{
ResourceType: "folders",
})
})
})
}