Skip to content

Commit bab023a

Browse files
authored
Merge pull request #21 from buildkite/feat_add_user_orgs
feat: added tool which returns the orginisation associated with a token
2 parents 7296770 + c38688e commit bab023a

4 files changed

Lines changed: 181 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ This is an [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introd
1313
* `list_artifacts` - List all artifacts for a specific job in Buildkite
1414
* `get_artifact` - Get a specific artifact for a specific job in Buildkite
1515
* `current_user` - Get details of the current user in Buildkite
16+
* `user_token_organization` - Get the organization associated with the user token used for this request
1617

1718
Example of the `get_pipeline` tool in action.
1819

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package buildkite
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/buildkite/go-buildkite/v4"
9+
"github.com/mark3labs/mcp-go/mcp"
10+
"github.com/mark3labs/mcp-go/server"
11+
"github.com/rs/zerolog/log"
12+
)
13+
14+
type OrganizationsClient interface {
15+
List(ctx context.Context, options *buildkite.OrganizationListOptions) ([]buildkite.Organization, *buildkite.Response, error)
16+
}
17+
18+
func UserTokenOrganization(ctx context.Context, client OrganizationsClient) (tool mcp.Tool, handler server.ToolHandlerFunc) {
19+
return mcp.NewTool("user_token_organization",
20+
mcp.WithDescription("Get the organization associated with the user token used for this request"),
21+
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
22+
log.Ctx(ctx).Debug().Msg("Getting current user token organization")
23+
24+
orgs, resp, err := client.List(ctx, &buildkite.OrganizationListOptions{})
25+
if err != nil {
26+
return mcp.NewToolResultError(err.Error()), nil
27+
}
28+
29+
if resp.StatusCode != 200 {
30+
return mcp.NewToolResultError("failed to get current user organizations"), nil
31+
}
32+
33+
if len(orgs) == 0 {
34+
return mcp.NewToolResultError("no organization found for the current user token"), nil
35+
}
36+
37+
r, err := json.Marshal(&orgs[0])
38+
if err != nil {
39+
return nil, fmt.Errorf("failed to marshal user organizations: %w", err)
40+
}
41+
42+
return mcp.NewToolResultText(string(r)), nil
43+
}
44+
}
45+
46+
func HandleUserTokenOrganizationPrompt(
47+
ctx context.Context,
48+
request mcp.GetPromptRequest,
49+
) (*mcp.GetPromptResult, error) {
50+
return &mcp.GetPromptResult{
51+
Description: "When asked for detail of a users pipelines start by looking up the user's token organization",
52+
Messages: []mcp.PromptMessage{
53+
{
54+
Role: mcp.RoleUser,
55+
Content: mcp.TextContent{
56+
Type: "text",
57+
Text: "When asked for detail of a users pipelines start by looking up the user's token organization",
58+
},
59+
},
60+
},
61+
}, nil
62+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package buildkite
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"testing"
7+
8+
"github.com/buildkite/go-buildkite/v4"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
type MockOrganizationsClient struct {
13+
ListFunc func(ctx context.Context, options *buildkite.OrganizationListOptions) ([]buildkite.Organization, *buildkite.Response, error)
14+
}
15+
16+
func (m *MockOrganizationsClient) List(ctx context.Context, options *buildkite.OrganizationListOptions) ([]buildkite.Organization, *buildkite.Response, error) {
17+
if m.ListFunc != nil {
18+
return m.ListFunc(ctx, options)
19+
}
20+
return nil, nil, nil
21+
}
22+
23+
func TestUserTokenOrganization(t *testing.T) {
24+
assert := require.New(t)
25+
26+
ctx := context.Background()
27+
client := &MockOrganizationsClient{
28+
ListFunc: func(ctx context.Context, options *buildkite.OrganizationListOptions) ([]buildkite.Organization, *buildkite.Response, error) {
29+
return []buildkite.Organization{
30+
{
31+
Slug: "test-org",
32+
Name: "Test Organization",
33+
},
34+
}, &buildkite.Response{
35+
Response: &http.Response{
36+
StatusCode: 200,
37+
},
38+
}, nil
39+
},
40+
}
41+
42+
tool, handler := UserTokenOrganization(ctx, client)
43+
assert.NotNil(tool)
44+
assert.NotNil(handler)
45+
46+
request := createMCPRequest(t, map[string]any{})
47+
result, err := handler(ctx, request)
48+
assert.NoError(err)
49+
50+
textContent := getTextResult(t, result)
51+
52+
assert.Equal(`{"name":"Test Organization","slug":"test-org"}`, textContent.Text)
53+
}
54+
55+
func TestUserTokenOrganizationError(t *testing.T) {
56+
assert := require.New(t)
57+
58+
ctx := context.Background()
59+
client := &MockOrganizationsClient{
60+
ListFunc: func(ctx context.Context, options *buildkite.OrganizationListOptions) ([]buildkite.Organization, *buildkite.Response, error) {
61+
return nil, &buildkite.Response{
62+
Response: &http.Response{
63+
StatusCode: 500,
64+
},
65+
}, nil
66+
},
67+
}
68+
69+
tool, handler := UserTokenOrganization(ctx, client)
70+
assert.NotNil(tool)
71+
assert.NotNil(handler)
72+
73+
request := createMCPRequest(t, map[string]any{})
74+
result, err := handler(ctx, request)
75+
assert.NoError(err)
76+
77+
textContent := getTextResult(t, result)
78+
79+
assert.Equal("failed to get current user organizations", textContent.Text)
80+
}
81+
82+
func TestUserTokenOrganizationErrorNoOrganization(t *testing.T) {
83+
assert := require.New(t)
84+
85+
ctx := context.Background()
86+
client := &MockOrganizationsClient{
87+
ListFunc: func(ctx context.Context, options *buildkite.OrganizationListOptions) ([]buildkite.Organization, *buildkite.Response, error) {
88+
return nil, &buildkite.Response{
89+
Response: &http.Response{
90+
StatusCode: 200,
91+
},
92+
}, nil
93+
},
94+
}
95+
96+
tool, handler := UserTokenOrganization(ctx, client)
97+
assert.NotNil(tool)
98+
assert.NotNil(handler)
99+
100+
request := createMCPRequest(t, map[string]any{})
101+
result, err := handler(ctx, request)
102+
assert.NoError(err)
103+
104+
textContent := getTextResult(t, result)
105+
106+
assert.Equal("no organization found for the current user token", textContent.Text)
107+
}

internal/commands/stdio.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
"github.com/buildkite/buildkite-mcp-server/internal/buildkite"
7+
"github.com/mark3labs/mcp-go/mcp"
78
"github.com/mark3labs/mcp-go/server"
89
"github.com/rs/zerolog/log"
910
)
@@ -26,14 +27,24 @@ func (c *StdioCmd) Run(ctx context.Context, globals *Globals) error {
2627

2728
s.AddTool(buildkite.GetPipeline(ctx, globals.Client.Pipelines))
2829
s.AddTool(buildkite.ListPipelines(ctx, globals.Client.Pipelines))
30+
2931
s.AddTool(buildkite.ListBuilds(ctx, globals.Client.Builds))
3032
s.AddTool(buildkite.GetBuild(ctx, globals.Client.Builds))
33+
3134
s.AddTool(buildkite.CurrentUser(ctx, globals.Client.User))
35+
3236
s.AddTool(buildkite.GetJobLogs(ctx, globals.Client))
37+
3338
s.AddTool(buildkite.AccessToken(ctx, globals.Client.AccessTokens))
3439

3540
s.AddTool(buildkite.ListArtifacts(ctx, clientAdapter))
3641
s.AddTool(buildkite.GetArtifact(ctx, clientAdapter))
3742

43+
s.AddTool(buildkite.UserTokenOrganization(ctx, globals.Client.Organizations))
44+
45+
s.AddPrompt(mcp.NewPrompt("user_token_organization_prompt",
46+
mcp.WithPromptDescription("When asked for detail of a users pipelines start by looking up the user's token organization"),
47+
), buildkite.HandleUserTokenOrganizationPrompt)
48+
3849
return server.ServeStdio(s)
3950
}

0 commit comments

Comments
 (0)