Skip to content

Commit 01b48ca

Browse files
authored
feat: adds admin category for listing teams (#124)
* feat: adds admin category for listing teams * rename to admin * add: admin e2e tests * fix: check for grafana env set and removal of duplicate function * revert back enforcement of GRAFANA_API_KEY * change wording of text and tool description * test to call out to the grafana api for team creation * call out to api to create tests for test setup * make the judge look at the team name
1 parent fbabf75 commit 01b48ca

File tree

5 files changed

+166
-18
lines changed

5 files changed

+166
-18
lines changed

README.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ This provides access to your Grafana instance and the surrounding ecosystem.
4949
- [x] Get current on-call users
5050
- [x] List teams and users
5151
- [ ] List alert groups
52+
- [x] Admin functionality
53+
- [ ] List users
54+
- [x] List teams
55+
- [ ] List roles
56+
- [ ] List assignments of roles
57+
- [ ] Debug role assignments
5258

5359
The list of tools is configurable, so you can choose which tools you want to make available to the MCP client.
5460
This is useful if you don't use certain functionality or if you don't want to take up too much of the context window.
@@ -58,7 +64,8 @@ the OnCall tools, use `--disable-oncall`.
5864
### Tools
5965

6066
| Tool | Category | Description |
61-
|-----------------------------------|-------------|--------------------------------------------------------------------|
67+
| --------------------------------- | ----------- | ------------------------------------------------------------------ |
68+
| `list_teams` | Admin | List all teams |
6269
| `search_dashboards` | Search | Search for dashboards |
6370
| `get_dashboard_by_uid` | Dashboard | Get a dashboard by uid |
6471
| `update_dashboard` | Dashboard | Update or create a new dashboard |
@@ -100,16 +107,16 @@ the OnCall tools, use `--disable-oncall`.
100107

101108
2. You have several options to install `mcp-grafana`:
102109

103-
* **Docker image**: Use the pre-built Docker image from Docker Hub:
110+
- **Docker image**: Use the pre-built Docker image from Docker Hub:
104111

105112
```bash
106113
docker pull mcp/grafana
107114
docker run -p 8000:8000 -e GRAFANA_URL=http://localhost:3000 -e GRAFANA_API_KEY=<your service account token> mcp/grafana
108115
```
109116

110-
* **Download binary**: Download the latest release of `mcp-grafana` from the [releases page](https://github.com/grafana/mcp-grafana/releases) and place it in your `$PATH`.
117+
- **Download binary**: Download the latest release of `mcp-grafana` from the [releases page](https://github.com/grafana/mcp-grafana/releases) and place it in your `$PATH`.
111118

112-
* **Build from source**: If you have a Go toolchain installed you can also build and install it from source, using the `GOBIN` environment variable
119+
- **Build from source**: If you have a Go toolchain installed you can also build and install it from source, using the `GOBIN` environment variable
113120
to specify the directory where the binary should be installed. This should also be in your `PATH`.
114121

115122
```bash
@@ -119,6 +126,7 @@ the OnCall tools, use `--disable-oncall`.
119126
3. Add the server configuration to your client configuration file. For example, for Claude Desktop:
120127

121128
**If using the binary:**
129+
122130
```json
123131
{
124132
"mcpServers": {
@@ -135,6 +143,7 @@ the OnCall tools, use `--disable-oncall`.
135143
```
136144

137145
**If using Docker:**
146+
138147
```json
139148
{
140149
"mcpServers": {
@@ -162,7 +171,6 @@ the OnCall tools, use `--disable-oncall`.
162171

163172
> Note: if you see `Error: spawn mcp-grafana ENOENT` in Claude Desktop, you need to specify the full path to `mcp-grafana`.
164173

165-
166174
**Using VSCode with remote MCP server**
167175

168176
Make sure your `.vscode/settings.json` includes:
@@ -185,6 +193,7 @@ You can enable debug mode for the Grafana transport by adding the `-debug` flag
185193
To use debug mode with the Claude Desktop configuration, update your config as follows:
186194

187195
**If using the binary:**
196+
188197
```json
189198
{
190199
"mcpServers": {
@@ -201,6 +210,7 @@ To use debug mode with the Claude Desktop configuration, update your config as f
201210
```
202211

203212
**If using Docker:**
213+
204214
```json
205215
{
206216
"mcpServers": {
@@ -256,24 +266,29 @@ docker run -it --rm -p 8000:8000 mcp-grafana:latest
256266
There are three types of tests available:
257267

258268
1. Unit Tests (no external dependencies required):
269+
259270
```bash
260271
make test-unit
261272
```
262273

263274
You can also run unit tests with:
275+
264276
```bash
265277
make test
266278
```
267279

268280
2. Integration Tests (requires docker containers to be up and running):
281+
269282
```bash
270283
make test-integration
271284
```
272285

273286
3. Cloud Tests (requires cloud Grafana instance and credentials):
287+
274288
```bash
275289
make test-cloud
276290
```
291+
277292
> Note: Cloud tests are automatically configured in CI. For local development, you'll need to set up your own Grafana Cloud instance and credentials.
278293
279294
More comprehensive integration tests will require a Grafana instance to be running locally on port 3000; you can start one with Docker Compose:

cmd/mcp-grafana/main.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ type disabledTools struct {
3434

3535
search, datasource, incident,
3636
prometheus, loki, alerting,
37-
dashboard, oncall, asserts, sift bool
37+
dashboard, oncall, asserts, sift, admin bool
3838
}
3939

4040
// Configuration for the Grafana client.
@@ -44,7 +44,7 @@ type grafanaConfig struct {
4444
}
4545

4646
func (dt *disabledTools) addFlags() {
47-
flag.StringVar(&dt.enabledTools, "enabled-tools", "search,datasource,incident,prometheus,loki,alerting,dashboard,oncall,asserts,sift", "A comma separated list of tools enabled for this server. Can be overwritten entirely or by disabling specific components, e.g. --disable-search.")
47+
flag.StringVar(&dt.enabledTools, "enabled-tools", "search,datasource,incident,prometheus,loki,alerting,dashboard,oncall,asserts,sift,admin", "A comma separated list of tools enabled for this server. Can be overwritten entirely or by disabling specific components, e.g. --disable-search.")
4848

4949
flag.BoolVar(&dt.search, "disable-search", false, "Disable search tools")
5050
flag.BoolVar(&dt.datasource, "disable-datasource", false, "Disable datasource tools")
@@ -56,6 +56,7 @@ func (dt *disabledTools) addFlags() {
5656
flag.BoolVar(&dt.oncall, "disable-oncall", false, "Disable oncall tools")
5757
flag.BoolVar(&dt.asserts, "disable-asserts", false, "Disable asserts tools")
5858
flag.BoolVar(&dt.sift, "disable-sift", false, "Disable sift tools")
59+
flag.BoolVar(&dt.admin, "disable-admin", false, "Disable admin tools")
5960
}
6061

6162
func (gc *grafanaConfig) addFlags() {
@@ -74,6 +75,7 @@ func (dt *disabledTools) addTools(s *server.MCPServer) {
7475
maybeAddTools(s, tools.AddOnCallTools, enabledTools, dt.oncall, "oncall")
7576
maybeAddTools(s, tools.AddAssertsTools, enabledTools, dt.asserts, "asserts")
7677
maybeAddTools(s, tools.AddSiftTools, enabledTools, dt.sift, "sift")
78+
maybeAddTools(s, tools.AddAdminTools, enabledTools, dt.admin, "admin")
7779
}
7880

7981
func newServer(dt disabledTools) *server.MCPServer {

tests/admin_test.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from typing import Dict
2+
import pytest
3+
from langevals import expect
4+
from langevals_langevals.llm_boolean import (
5+
CustomLLMBooleanEvaluator,
6+
CustomLLMBooleanSettings,
7+
)
8+
from litellm import Message, acompletion
9+
from mcp import ClientSession
10+
import aiohttp
11+
import uuid
12+
import os
13+
from conftest import DEFAULT_GRAFANA_URL
14+
15+
from conftest import models
16+
from utils import (
17+
get_converted_tools,
18+
llm_tool_call_sequence,
19+
)
20+
21+
pytestmark = pytest.mark.anyio
22+
23+
24+
@pytest.fixture
25+
async def grafana_team():
26+
"""Create a temporary test team and clean it up after the test is done."""
27+
# Generate a unique team name to avoid conflicts
28+
team_name = f"test-team-{uuid.uuid4().hex[:8]}"
29+
30+
# Get Grafana URL and API key from environment
31+
grafana_url = os.environ.get("GRAFANA_URL", DEFAULT_GRAFANA_URL)
32+
33+
auth_header = None
34+
if api_key := os.environ.get("GRAFANA_API_KEY"):
35+
auth_header = {"Authorization": f"Bearer {api_key}"}
36+
37+
if not auth_header:
38+
pytest.skip("No authentication credentials available to create team")
39+
40+
# Create the team using Grafana API
41+
team_id = None
42+
async with aiohttp.ClientSession() as session:
43+
create_url = f"{grafana_url}/api/teams"
44+
async with session.post(
45+
create_url,
46+
headers=auth_header,
47+
json={"name": team_name, "email": f"{team_name}@example.com"},
48+
) as response:
49+
if response.status != 200:
50+
resp_text = await response.text()
51+
pytest.skip(f"Failed to create team: {resp_text}")
52+
resp_data = await response.json()
53+
team_id = resp_data.get("teamId")
54+
55+
# Yield the team info for the test to use
56+
yield {"id": team_id, "name": team_name}
57+
58+
# Clean up after the test
59+
if team_id:
60+
async with aiohttp.ClientSession() as session:
61+
delete_url = f"{grafana_url}/api/teams/{team_id}"
62+
async with session.delete(delete_url, headers=auth_header) as response:
63+
if response.status != 200:
64+
resp_text = await response.text()
65+
print(f"Warning: Failed to delete team: {resp_text}")
66+
67+
68+
@pytest.mark.parametrize("model", models)
69+
@pytest.mark.flaky(max_runs=3)
70+
async def test_list_teams_tool(
71+
model: str, mcp_client: ClientSession, grafana_team: Dict[str, str]
72+
):
73+
tools = await get_converted_tools(mcp_client)
74+
team_name = grafana_team["name"]
75+
prompt = "Can you list the teams in Grafana?"
76+
77+
messages = [
78+
Message(role="system", content="You are a helpful assistant."),
79+
Message(role="user", content=prompt),
80+
]
81+
82+
# 1. Call the list teams tool
83+
messages = await llm_tool_call_sequence(
84+
model,
85+
messages,
86+
tools,
87+
mcp_client,
88+
"list_teams",
89+
)
90+
91+
# 2. Final LLM response
92+
response = await acompletion(model=model, messages=messages, tools=tools)
93+
content = response.choices[0].message.content
94+
panel_queries_checker = CustomLLMBooleanEvaluator(
95+
settings=CustomLLMBooleanSettings(
96+
prompt=(
97+
"Does the response contain specific information about "
98+
"the teams in Grafana?"
99+
f"There should be a team named {team_name}. "
100+
),
101+
)
102+
)
103+
expect(input=prompt, output=content).to_pass(panel_queries_checker)

tests/conftest.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,6 @@ async def mcp_client(mcp_url, grafana_headers):
4040
def mcp_url():
4141
return os.environ.get("MCP_GRAFANA_URL", DEFAULT_MCP_URL)
4242

43-
44-
@pytest.fixture
45-
def grafana_headers():
46-
headers = {
47-
"X-Grafana-URL": os.environ.get("GRAFANA_URL", DEFAULT_GRAFANA_URL),
48-
}
49-
if key := os.environ.get("GRAFANA_API_KEY"):
50-
headers["X-Grafana-API-Key"] = key
51-
return headers
52-
53-
5443
@pytest.fixture
5544
async def mcp_client(mcp_url, grafana_headers):
5645
async with sse_client(mcp_url, headers=grafana_headers) as (

tools/admin.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/mark3labs/mcp-go/server"
8+
9+
"github.com/grafana/grafana-openapi-client-go/client/teams"
10+
"github.com/grafana/grafana-openapi-client-go/models"
11+
mcpgrafana "github.com/grafana/mcp-grafana"
12+
)
13+
14+
type ListTeamsParams struct {
15+
Query string `json:"query" jsonschema:"description=The query to search for teams. Can be left empty to fetch all teams"`
16+
}
17+
18+
func listTeams(ctx context.Context, args ListTeamsParams) (*models.SearchTeamQueryResult, error) {
19+
c := mcpgrafana.GrafanaClientFromContext(ctx)
20+
params := teams.NewSearchTeamsParamsWithContext(ctx)
21+
if args.Query != "" {
22+
params.SetQuery(&args.Query)
23+
}
24+
search, err := c.Teams.SearchTeams(params)
25+
if err != nil {
26+
return nil, fmt.Errorf("search teams for %+v: %w", c, err)
27+
}
28+
return search.Payload, nil
29+
}
30+
31+
var ListTeams = mcpgrafana.MustTool(
32+
"list_teams",
33+
"Search for Grafana teams by a query string. Returns a list of matching teams with details like name, ID, and URL.",
34+
listTeams,
35+
)
36+
37+
func AddAdminTools(mcp *server.MCPServer) {
38+
ListTeams.Register(mcp)
39+
}

0 commit comments

Comments
 (0)