Skip to content

Commit 144c162

Browse files
committed
feat: add extra admin tools, update unit test and readme
1 parent ed4a600 commit 144c162

File tree

3 files changed

+303
-0
lines changed

3 files changed

+303
-0
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ The dashboard tools now include several strategies to manage context window usag
8181

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

8592
### Navigation
8693

@@ -170,6 +177,13 @@ Scopes define the specific resources that permissions apply to. Each action requ
170177
| --------------------------------- | ----------- | ------------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------- |
171178
| `list_teams` | Admin | List all teams | `teams:read` | `teams:*` or `teams:id:1` |
172179
| `list_users_by_org` | Admin | List all users in an organization | `users:read` | `global.users:*` or `global.users:id:123` |
180+
| `list_all_roles` | Admin | List all Grafana roles | `roles:read` | `roles:*` |
181+
| `get_role_details` | Admin | Get details for a Grafana role | `roles:read` | `roles:uid:editor` |
182+
| `get_role_assignments` | Admin | List assignments for a role | `roles:read` | `roles:uid:editor` |
183+
| `list_user_roles` | Admin | List roles for users | `roles:read` | `global.users:id:123` |
184+
| `list_team_roles` | Admin | List roles for teams | `roles:read` | `teams:id:7` |
185+
| `get_resource_permissions`| Admin | List permissions for a resource | `permissions:read` | `dashboards:uid:abcd1234` |
186+
| `get_resource_description`| Admin | Describe a Grafana resource type | `permissions:read` | `dashboards:*` |
173187
| `search_dashboards` | Search | Search for dashboards | `dashboards:read` | `dashboards:*` or `dashboards:uid:abc123` |
174188
| `get_dashboard_by_uid` | Dashboard | Get a dashboard by uid | `dashboards:read` | `dashboards:uid:abc123` |
175189
| `update_dashboard` | Dashboard | Update or create a new dashboard | `dashboards:create`, `dashboards:write` | `dashboards:*`, `folders:*` or `folders:uid:xyz789` |

tools/admin.go

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/mark3labs/mcp-go/mcp"
88
"github.com/mark3labs/mcp-go/server"
99

10+
"github.com/grafana/grafana-openapi-client-go/client/access_control"
1011
"github.com/grafana/grafana-openapi-client-go/client/org"
1112
"github.com/grafana/grafana-openapi-client-go/client/teams"
1213
"github.com/grafana/grafana-openapi-client-go/models"
@@ -61,7 +62,193 @@ var ListUsersByOrg = mcpgrafana.MustTool(
6162
mcp.WithReadOnlyHintAnnotation(true),
6263
)
6364

65+
type ListAllRolesParams struct {
66+
DelegatableOnly bool `json:"delegatableOnly,omitempty" jsonschema:"description=Optional: If set true only return roles that can be delegated by current user"`
67+
}
68+
69+
func listAllRoles(ctx context.Context, args ListAllRolesParams) ([]*models.RoleDTO, error) {
70+
c := mcpgrafana.GrafanaClientFromContext(ctx)
71+
params := access_control.NewListRolesParamsWithContext(ctx)
72+
73+
if args.DelegatableOnly {
74+
delegatable := true
75+
params.Delegatable = &delegatable
76+
}
77+
78+
resp, err := c.AccessControl.ListRoles(params)
79+
if err != nil {
80+
return nil, fmt.Errorf("list all roles: %w", err)
81+
}
82+
return resp.Payload, nil
83+
}
84+
85+
var ListAllRoles = mcpgrafana.MustTool(
86+
"list_all_roles",
87+
"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.",
88+
listAllRoles,
89+
mcp.WithTitleAnnotation("List all roles"),
90+
mcp.WithIdempotentHintAnnotation(true),
91+
mcp.WithReadOnlyHintAnnotation(true),
92+
)
93+
94+
type GetRoleDetailsParams struct {
95+
RoleUID string `json:"roleUID" jsonschema:"required,description=Role UID to retrieve"`
96+
}
97+
98+
func getRoleDetails(ctx context.Context, args GetRoleDetailsParams) (*models.RoleDTO, error) {
99+
c := mcpgrafana.GrafanaClientFromContext(ctx)
100+
params := access_control.NewGetRoleParamsWithContext(ctx).WithRoleUID(args.RoleUID)
101+
102+
resp, err := c.AccessControl.GetRoleWithParams(params)
103+
if err != nil {
104+
return nil, fmt.Errorf("get role details: %w", err)
105+
}
106+
return resp.Payload, nil
107+
}
108+
109+
var GetRoleDetails = mcpgrafana.MustTool(
110+
"get_role_details",
111+
"Get detailed information about a specific Grafana role by its UID, including permissions, metadata, and configuration.",
112+
getRoleDetails,
113+
mcp.WithTitleAnnotation("Get role details"),
114+
mcp.WithIdempotentHintAnnotation(true),
115+
mcp.WithReadOnlyHintAnnotation(true),
116+
)
117+
118+
type GetRoleAssignmentsParams struct {
119+
RoleUID string `json:"roleUID" jsonschema:"required,description=Role UID to retrieve"`
120+
}
121+
122+
func getRoleAssignments(ctx context.Context, args GetRoleAssignmentsParams) (*models.RoleAssignmentsDTO, error) {
123+
c := mcpgrafana.GrafanaClientFromContext(ctx)
124+
params := access_control.NewGetRoleAssignmentsParamsWithContext(ctx).WithRoleUID(args.RoleUID)
125+
126+
resp, err := c.AccessControl.GetRoleAssignmentsWithParams(params)
127+
if err != nil {
128+
return nil, fmt.Errorf("get role assignments: %w", err)
129+
}
130+
return resp.Payload, nil
131+
}
132+
133+
var GetRoleAssignments = mcpgrafana.MustTool(
134+
"get_role_assignments",
135+
"List all assignments for a specific role, showing which users, teams, and service accounts have been assigned this role.",
136+
getRoleAssignments,
137+
mcp.WithTitleAnnotation("Get role assignments"),
138+
mcp.WithIdempotentHintAnnotation(true),
139+
mcp.WithReadOnlyHintAnnotation(true),
140+
)
141+
142+
type ListUserRolesParams struct {
143+
UserIDs []int64 `json:"userIds" jsonschema:"required,description=User ID(s) to get roles for. Can be a single user or multiple users."`
144+
}
145+
146+
func listUserRoles(ctx context.Context, args ListUserRolesParams) (map[string][]models.RoleDTO, error) {
147+
c := mcpgrafana.GrafanaClientFromContext(ctx)
148+
searchQuery := &models.RolesSearchQuery{UserIds: args.UserIDs}
149+
params := access_control.NewListUsersRolesParamsWithContext(ctx).WithBody(searchQuery)
150+
151+
resp, err := c.AccessControl.ListUsersRolesWithParams(params)
152+
if err != nil {
153+
return nil, fmt.Errorf("list user roles: %w", err)
154+
}
155+
return resp.Payload, nil
156+
}
157+
158+
var ListUserRoles = mcpgrafana.MustTool(
159+
"list_user_roles",
160+
"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.",
161+
listUserRoles,
162+
mcp.WithTitleAnnotation("List user roles"),
163+
mcp.WithIdempotentHintAnnotation(true),
164+
mcp.WithReadOnlyHintAnnotation(true),
165+
)
166+
167+
type ListTeamRolesParams struct {
168+
TeamIDs []int64 `json:"teamIds" jsonschema:"required,description=Team ID(s) to get roles for. Can be a single team or multiple teams."`
169+
}
170+
171+
func listTeamRoles(ctx context.Context, args ListTeamRolesParams) (map[string][]models.RoleDTO, error) {
172+
c := mcpgrafana.GrafanaClientFromContext(ctx)
173+
searchQuery := &models.RolesSearchQuery{TeamIds: args.TeamIDs}
174+
params := access_control.NewListTeamsRolesParamsWithContext(ctx).WithBody(searchQuery)
175+
176+
resp, err := c.AccessControl.ListTeamsRolesWithParams(params)
177+
if err != nil {
178+
return nil, fmt.Errorf("list team roles: %w", err)
179+
}
180+
return resp.Payload, nil
181+
}
182+
183+
var ListTeamRoles = mcpgrafana.MustTool(
184+
"list_team_roles",
185+
"List all roles assigned to one or more teams. Returns a map of team IDs to their assigned roles.",
186+
listTeamRoles,
187+
mcp.WithTitleAnnotation("List team roles"),
188+
mcp.WithIdempotentHintAnnotation(true),
189+
mcp.WithReadOnlyHintAnnotation(true),
190+
)
191+
192+
type GetResourcePermissionsParams struct {
193+
Resource string `json:"resource" jsonschema:"required,description=Resource type (e.g. 'dashboards' 'datasources' 'folders')"`
194+
ResourceID string `json:"resourceId" jsonschema:"required,description=Unique identifier of the resource (UID for dashboards/datasources/folders)"`
195+
}
196+
197+
func getResourcePermissions(ctx context.Context, args GetResourcePermissionsParams) ([]*models.ResourcePermissionDTO, error) {
198+
c := mcpgrafana.GrafanaClientFromContext(ctx)
199+
params := access_control.NewGetResourcePermissionsParamsWithContext(ctx).WithResource(args.Resource).WithResourceID(args.ResourceID)
200+
201+
resp, err := c.AccessControl.GetResourcePermissionsWithParams(params)
202+
if err != nil {
203+
return nil, fmt.Errorf("get resource permissions: %w", err)
204+
}
205+
return resp.Payload, nil
206+
}
207+
208+
var GetResourcePermissions = mcpgrafana.MustTool(
209+
"get_resource_permissions",
210+
"List all permissions set on a specific Grafana resource (e.g., dashboard, datasource, folder) by its type and ID.",
211+
getResourcePermissions,
212+
mcp.WithTitleAnnotation("Get resource permissions"),
213+
mcp.WithIdempotentHintAnnotation(true),
214+
mcp.WithReadOnlyHintAnnotation(true),
215+
)
216+
217+
type GetResourceDescriptionParams struct {
218+
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"`
219+
}
220+
221+
func getResourceDescription(ctx context.Context, args GetResourceDescriptionParams) (*models.Description, error) {
222+
c := mcpgrafana.GrafanaClientFromContext(ctx)
223+
224+
params := access_control.NewGetResourceDescriptionParamsWithContext(ctx).
225+
WithResource(args.ResourceType)
226+
227+
resp, err := c.AccessControl.GetResourceDescriptionWithParams(params)
228+
if err != nil {
229+
return nil, fmt.Errorf("get resource description: %w", err)
230+
}
231+
232+
return resp.Payload, nil
233+
}
234+
235+
var GetResourceDescription = mcpgrafana.MustTool(
236+
"get_resource_description",
237+
"List available permissions and assignment capabilities for a Grafana resource type.",
238+
getResourceDescription,
239+
mcp.WithTitleAnnotation("Get resource description"),
240+
mcp.WithIdempotentHintAnnotation(true),
241+
mcp.WithReadOnlyHintAnnotation(true),
242+
)
243+
64244
func AddAdminTools(mcp *server.MCPServer) {
65245
ListTeams.Register(mcp)
66246
ListUsersByOrg.Register(mcp)
247+
ListAllRoles.Register(mcp)
248+
GetRoleDetails.Register(mcp)
249+
GetRoleAssignments.Register(mcp)
250+
ListUserRoles.Register(mcp)
251+
ListTeamRoles.Register(mcp)
252+
GetResourcePermissions.Register(mcp)
253+
GetResourceDescription.Register(mcp)
67254
}

tools/admin_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,60 @@ func TestAdminToolsUnit(t *testing.T) {
1717
// Test that the tools are properly defined with correct metadata
1818
require.NotNil(t, ListUsersByOrg, "ListUsersByOrg tool should be defined")
1919
require.NotNil(t, ListTeams, "ListTeams tool should be defined")
20+
require.NotNil(t, ListAllRoles, "ListAllRoles tool should be defined")
21+
require.NotNil(t, GetRoleDetails, "GetRoleDetails tool should be defined")
22+
require.NotNil(t, GetRoleAssignments, "GetRoleAssignments tool should be defined")
23+
require.NotNil(t, ListUserRoles, "ListUserRoles tool should be defined")
24+
require.NotNil(t, GetResourcePermissions, "GetResourcePermissions tool should be defined")
25+
require.NotNil(t, GetResourceDescription, "GetResourceDescription tool should be defined")
2026

2127
// Verify tool metadata
2228
assert.Equal(t, "list_users_by_org", ListUsersByOrg.Tool.Name)
2329
assert.Equal(t, "list_teams", ListTeams.Tool.Name)
30+
assert.Equal(t, "list_all_roles", ListAllRoles.Tool.Name)
31+
assert.Equal(t, "get_role_details", GetRoleDetails.Tool.Name)
32+
assert.Equal(t, "get_role_assignments", GetRoleAssignments.Tool.Name)
33+
assert.Equal(t, "list_user_roles", ListUserRoles.Tool.Name)
34+
assert.Equal(t, "list_team_roles", ListTeamRoles.Tool.Name)
35+
assert.Equal(t, "get_resource_permissions", GetResourcePermissions.Tool.Name)
36+
assert.Equal(t, "get_resource_description", GetResourceDescription.Tool.Name)
2437
assert.Contains(t, ListUsersByOrg.Tool.Description, "List users by organization")
2538
assert.Contains(t, ListTeams.Tool.Description, "Search for Grafana teams")
39+
assert.Contains(t, ListAllRoles.Tool.Description, "List all roles in Grafana")
40+
assert.Contains(t, GetRoleDetails.Tool.Description, "Get detailed information about a specific Grafana")
41+
assert.Contains(t, GetRoleAssignments.Tool.Description, "List all assignments for a specific role")
42+
assert.Contains(t, ListUserRoles.Tool.Description, "List all roles assigned to one or more users")
43+
assert.Contains(t, ListTeamRoles.Tool.Description, "List all roles assigned to one or more teams")
44+
assert.Contains(t, GetResourcePermissions.Tool.Description, "List all permissions set on a specific Grafana resource")
45+
assert.Contains(t, GetResourceDescription.Tool.Description, "List available permissions and assignment capabilities")
2646
})
2747

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

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

3663
// ListTeamsParams should have a Query field
3764
assert.Equal(t, "test-query", teamParams.Query)
65+
66+
assert.IsType(t, ListAllRolesParams{}, roleParams)
67+
assert.Equal(t, "r1", roleDetailParams.RoleUID)
68+
assert.Equal(t, "r2", assignParams.RoleUID)
69+
assert.Equal(t, []int64{1, 2}, userRoleParams.UserIDs)
70+
assert.Equal(t, []int64{3}, teamRoleParams.TeamIDs)
71+
assert.Equal(t, "dashboards", permParams.Resource)
72+
assert.Equal(t, "abc", permParams.ResourceID)
73+
assert.Equal(t, "folders", descParams.ResourceType)
3874
})
3975

4076
t.Run("nil client handling", func(t *testing.T) {
@@ -50,6 +86,39 @@ func TestAdminToolsUnit(t *testing.T) {
5086
assert.Panics(t, func() {
5187
listTeams(ctx, ListTeamsParams{})
5288
}, "Should panic when no Grafana client in context")
89+
90+
assert.Panics(t, func() {
91+
listAllRoles(ctx, ListAllRolesParams{})
92+
}, "Should panic when no Grafana client in context")
93+
94+
assert.Panics(t, func() {
95+
getRoleDetails(ctx, GetRoleDetailsParams{RoleUID: "x"})
96+
}, "Should panic when no Grafana client in context")
97+
98+
assert.Panics(t, func() {
99+
getRoleAssignments(ctx, GetRoleAssignmentsParams{RoleUID: "x"})
100+
}, "Should panic when no Grafana client in context")
101+
102+
assert.Panics(t, func() {
103+
listUserRoles(ctx, ListUserRolesParams{UserIDs: []int64{1}})
104+
}, "Should panic when no Grafana client in context")
105+
106+
assert.Panics(t, func() {
107+
listTeamRoles(ctx, ListTeamRolesParams{TeamIDs: []int64{2}})
108+
}, "Should panic when no Grafana client in context")
109+
110+
assert.Panics(t, func() {
111+
getResourcePermissions(ctx, GetResourcePermissionsParams{
112+
Resource: "dashboards",
113+
ResourceID: "x",
114+
})
115+
}, "Should panic when no Grafana client in context")
116+
117+
assert.Panics(t, func() {
118+
getResourceDescription(ctx, GetResourceDescriptionParams{
119+
ResourceType: "folders",
120+
})
121+
}, "Should panic when no Grafana client in context")
53122
})
54123

55124
t.Run("function signatures", func(t *testing.T) {
@@ -71,5 +140,38 @@ func TestAdminToolsUnit(t *testing.T) {
71140
assert.Panics(t, func() {
72141
listTeams(ctx, ListTeamsParams{Query: "test"})
73142
})
143+
144+
assert.Panics(t, func() {
145+
listAllRoles(ctx, ListAllRolesParams{})
146+
})
147+
148+
assert.Panics(t, func() {
149+
getRoleDetails(ctx, GetRoleDetailsParams{RoleUID: "r1"})
150+
})
151+
152+
assert.Panics(t, func() {
153+
getRoleAssignments(ctx, GetRoleAssignmentsParams{RoleUID: "r2"})
154+
})
155+
156+
assert.Panics(t, func() {
157+
listUserRoles(ctx, ListUserRolesParams{UserIDs: []int64{1}})
158+
})
159+
160+
assert.Panics(t, func() {
161+
listTeamRoles(ctx, ListTeamRolesParams{TeamIDs: []int64{2}})
162+
})
163+
164+
assert.Panics(t, func() {
165+
getResourcePermissions(ctx, GetResourcePermissionsParams{
166+
Resource: "dashboards",
167+
ResourceID: "abc",
168+
})
169+
})
170+
171+
assert.Panics(t, func() {
172+
getResourceDescription(ctx, GetResourceDescriptionParams{
173+
ResourceType: "folders",
174+
})
175+
})
74176
})
75177
}

0 commit comments

Comments
 (0)