Skip to content

Commit 1a0f36d

Browse files
committed
refactor: replace inline API client with flashduty-sdk
Replace the entire duplicate API client layer in pkg/flashduty/ with the flashduty-sdk (v0.3.0). Tool handlers are now thin wrappers that parse MCP params, call SDK methods, and format results. Deleted: - pkg/flashduty/types.go (280 lines) -- all types now in SDK - pkg/flashduty/enrichment.go (696 lines) -- all enrichment now SDK-internal - Client struct, makeRequest, parseResponse, handleAPIError from client.go Rewritten (22 tool handlers across 7 files): - incidents.go, users.go, channels.go, changes.go, statuspage.go, fields.go, templates.go -- all delegate to SDK methods Updated: - client.go: reduced to GetFlashdutyClientFn type alias - context.go: creates sdk.Client with WithRequestHook for trace context - format.go: delegates serialization to sdk.Marshal - e2e tests: use sdk.NewClient directly Net change: -2,392 lines. toon-go and golang.org/x/sync demoted to indirect deps. logrus removed entirely.
1 parent 96978ba commit 1a0f36d

18 files changed

Lines changed: 409 additions & 2637 deletions

e2e/e2e_test.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/mark3labs/mcp-go/mcp"
1717
"github.com/stretchr/testify/require"
1818

19+
sdk "github.com/flashcatcloud/flashduty-sdk"
1920
"github.com/flashcatcloud/flashduty-mcp-server/internal/flashduty"
2021
pkgflashduty "github.com/flashcatcloud/flashduty-mcp-server/pkg/flashduty"
2122
"github.com/flashcatcloud/flashduty-mcp-server/pkg/translations"
@@ -55,13 +56,19 @@ func getE2EBaseURL() string {
5556
return baseURL
5657
}
5758

58-
// getAPIClient creates a native Flashduty API client for verification purposes
59-
func getAPIClient(t *testing.T) *pkgflashduty.Client {
59+
// getAPIClient creates a native Flashduty SDK client for verification purposes
60+
func getAPIClient(t *testing.T) *sdk.Client {
6061
appKey := getE2EAppKey(t)
6162
baseURL := getE2EBaseURL()
6263

63-
client, err := pkgflashduty.NewClient(appKey, baseURL, "e2e-test-client/1.0.0")
64-
require.NoError(t, err, "expected to create Flashduty API client")
64+
opts := []sdk.Option{
65+
sdk.WithUserAgent("e2e-test-client/1.0.0"),
66+
}
67+
if baseURL != "" {
68+
opts = append(opts, sdk.WithBaseURL(baseURL))
69+
}
70+
client, err := sdk.NewClient(appKey, opts...)
71+
require.NoError(t, err, "expected to create Flashduty SDK client")
6572

6673
return client
6774
}

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ go 1.24.4
44

55
require (
66
github.com/bluele/gcache v0.0.2
7+
github.com/flashcatcloud/flashduty-sdk v0.3.0
78
github.com/google/go-github/v72 v72.0.0
89
github.com/josephburnett/jd v1.9.2
910
github.com/mark3labs/mcp-go v0.45.0
1011
github.com/spf13/cobra v1.10.2
1112
github.com/spf13/viper v1.20.1
1213
github.com/stretchr/testify v1.10.0
13-
github.com/toon-format/toon-go v0.0.0-20251202084852-7ca0e27c4e8c
14-
golang.org/x/sync v0.19.0
1514
)
1615

1716
require (
@@ -24,10 +23,12 @@ require (
2423
github.com/josharian/intern v1.0.0 // indirect
2524
github.com/mailru/easyjson v0.7.7 // indirect
2625
github.com/rogpeppe/go-internal v1.13.1 // indirect
26+
github.com/toon-format/toon-go v0.0.0-20251202084852-7ca0e27c4e8c // indirect
2727
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
2828
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
2929
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
3030
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
31+
golang.org/x/sync v0.19.0 // indirect
3132
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
3233
gopkg.in/yaml.v2 v2.4.0 // indirect
3334
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
1010
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1111
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
1212
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13+
github.com/flashcatcloud/flashduty-sdk v0.3.0 h1:jx7j6o+wFDIjTQaP5NtxWoAYIq6qtmIOQCZtG9OueV8=
14+
github.com/flashcatcloud/flashduty-sdk v0.3.0/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY=
1315
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
1416
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
1517
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=

internal/flashduty/context.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package flashduty
33
import (
44
"context"
55
"fmt"
6+
"net/http"
67
"time"
78

89
"github.com/bluele/gcache"
910

10-
"github.com/flashcatcloud/flashduty-mcp-server/pkg/flashduty"
11+
sdk "github.com/flashcatcloud/flashduty-sdk"
12+
"github.com/flashcatcloud/flashduty-mcp-server/pkg/trace"
1113
)
1214

1315
type contextKey string
@@ -29,25 +31,25 @@ func ConfigFromContext(ctx context.Context) (FlashdutyConfig, bool) {
2931
}
3032

3133
// clientFromContext returns the Flashduty client from the context.
32-
func clientFromContext(ctx context.Context) (*flashduty.Client, bool) {
33-
client, ok := ctx.Value(flashdutyClientKey).(*flashduty.Client)
34+
func clientFromContext(ctx context.Context) (*sdk.Client, bool) {
35+
client, ok := ctx.Value(flashdutyClientKey).(*sdk.Client)
3436
return client, ok
3537
}
3638

3739
// contextWithClient adds the Flashduty client to the context.
38-
func contextWithClient(ctx context.Context, client *flashduty.Client) context.Context {
40+
func contextWithClient(ctx context.Context, client *sdk.Client) context.Context {
3941
return context.WithValue(ctx, flashdutyClientKey, client)
4042
}
4143

4244
var clientCache = gcache.New(1000).
4345
Expiration(time.Hour).
4446
Build()
4547

46-
// getClientFromContext is a helper function for tool handlers to get a flashduty client.
48+
// getClient is a helper function for tool handlers to get a flashduty client.
4749
// It will try to get the client from the context first. If not found, it will create a new one
4850
// based on the config in the context, and cache it in the context for future use in the same request.
4951
// It falls back to the default config if no config is found in the context.
50-
func getClient(ctx context.Context, defaultCfg FlashdutyConfig, version string) (context.Context, *flashduty.Client, error) {
52+
func getClient(ctx context.Context, defaultCfg FlashdutyConfig, version string) (context.Context, *sdk.Client, error) {
5153
if client, ok := clientFromContext(ctx); ok {
5254
return ctx, client, nil
5355
}
@@ -64,12 +66,24 @@ func getClient(ctx context.Context, defaultCfg FlashdutyConfig, version string)
6466
// Use APP key and BaseURL as cache key to handle different environments.
6567
cacheKey := fmt.Sprintf("%s|%s", cfg.APPKey, cfg.BaseURL)
6668
if client, err := clientCache.Get(cacheKey); err == nil {
67-
return contextWithClient(ctx, client.(*flashduty.Client)), client.(*flashduty.Client), nil
69+
return contextWithClient(ctx, client.(*sdk.Client)), client.(*sdk.Client), nil
6870
}
6971

7072
userAgent := fmt.Sprintf("flashduty-mcp-server/%s", version)
7173

72-
client, err := flashduty.NewClient(cfg.APPKey, cfg.BaseURL, userAgent)
74+
opts := []sdk.Option{
75+
sdk.WithUserAgent(userAgent),
76+
sdk.WithRequestHook(func(req *http.Request) {
77+
if traceCtx := trace.FromContext(req.Context()); traceCtx != nil {
78+
traceCtx.SetHTTPHeaders(req.Header)
79+
}
80+
}),
81+
}
82+
if cfg.BaseURL != "" {
83+
opts = append(opts, sdk.WithBaseURL(cfg.BaseURL))
84+
}
85+
86+
client, err := sdk.NewClient(cfg.APPKey, opts...)
7387
if err != nil {
7488
return ctx, nil, fmt.Errorf("failed to create Flashduty client: %w", err)
7589
}

internal/flashduty/server.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"github.com/mark3labs/mcp-go/mcp"
1818
"github.com/mark3labs/mcp-go/server"
1919

20+
sdk "github.com/flashcatcloud/flashduty-sdk"
21+
2022
pkgerrors "github.com/flashcatcloud/flashduty-mcp-server/pkg/errors"
2123
"github.com/flashcatcloud/flashduty-mcp-server/pkg/flashduty"
2224
mcplog "github.com/flashcatcloud/flashduty-mcp-server/pkg/log"
@@ -128,7 +130,7 @@ func NewMCPServer(cfg FlashdutyConfig) (*server.MCPServer, error) {
128130

129131
flashdutyServer := server.NewMCPServer("flashduty-mcp-server", cfg.Version, server.WithHooks(hooks))
130132

131-
getClientFn := func(ctx context.Context) (context.Context, *flashduty.Client, error) {
133+
getClientFn := func(ctx context.Context) (context.Context, *sdk.Client, error) {
132134
return getClient(ctx, cfg, cfg.Version)
133135
}
134136

pkg/flashduty/changes.go

Lines changed: 12 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ package flashduty
33
import (
44
"context"
55
"fmt"
6-
"net/http"
76

7+
sdk "github.com/flashcatcloud/flashduty-sdk"
88
"github.com/mark3labs/mcp-go/mcp"
99
"github.com/mark3labs/mcp-go/server"
10-
"golang.org/x/sync/errgroup"
1110

1211
"github.com/flashcatcloud/flashduty-mcp-server/pkg/translations"
1312
)
@@ -45,128 +44,26 @@ func QueryChanges(getClient GetFlashdutyClientFn, t translations.TranslationHelp
4544
limit = 20
4645
}
4746

48-
requestBody := map[string]interface{}{
49-
"p": 1,
50-
"limit": limit,
47+
input := &sdk.ListChangesInput{
48+
ChannelID: int64(channelID),
49+
StartTime: int64(startTime),
50+
EndTime: int64(endTime),
51+
Type: changeType,
52+
Limit: limit,
5153
}
5254

5355
if changeIdsStr != "" {
54-
changeIDs := parseCommaSeparatedStrings(changeIdsStr)
55-
requestBody["change_ids"] = changeIDs
56-
}
57-
if channelID > 0 {
58-
requestBody["channel_id"] = channelID
59-
}
60-
if startTime > 0 {
61-
requestBody["start_time"] = startTime
62-
}
63-
if endTime > 0 {
64-
requestBody["end_time"] = endTime
65-
}
66-
if changeType != "" {
67-
requestBody["type"] = changeType
56+
input.ChangeIDs = parseCommaSeparatedStrings(changeIdsStr)
6857
}
6958

70-
resp, err := client.makeRequest(ctx, "POST", "/change/list", requestBody)
59+
output, err := client.ListChanges(ctx, input)
7160
if err != nil {
72-
return nil, fmt.Errorf("failed to query changes: %w", err)
73-
}
74-
defer func() { _ = resp.Body.Close() }()
75-
76-
if resp.StatusCode != http.StatusOK {
77-
return mcp.NewToolResultError(handleAPIError(resp).Error()), nil
78-
}
79-
80-
var result struct {
81-
Error *DutyError `json:"error,omitempty"`
82-
Data *struct {
83-
Items []struct {
84-
ChangeID string `json:"change_id"`
85-
Title string `json:"title"`
86-
Description string `json:"description,omitempty"`
87-
Type string `json:"type,omitempty"`
88-
Status string `json:"status,omitempty"`
89-
ChannelID int64 `json:"channel_id,omitempty"`
90-
CreatorID int64 `json:"creator_id,omitempty"`
91-
StartTime int64 `json:"start_time,omitempty"`
92-
EndTime int64 `json:"end_time,omitempty"`
93-
Labels map[string]string `json:"labels,omitempty"`
94-
} `json:"items"`
95-
Total int `json:"total"`
96-
} `json:"data,omitempty"`
97-
}
98-
if err := parseResponse(resp, &result); err != nil {
99-
return nil, err
100-
}
101-
if result.Error != nil {
102-
return mcp.NewToolResultError(fmt.Sprintf("API error: %s - %s", result.Error.Code, result.Error.Message)), nil
103-
}
104-
105-
if result.Data == nil || len(result.Data.Items) == 0 {
106-
return MarshalResult(map[string]any{
107-
"changes": []Change{},
108-
"total": 0,
109-
}), nil
110-
}
111-
112-
// Collect IDs for enrichment
113-
channelIDs := make([]int64, 0)
114-
personIDs := make([]int64, 0)
115-
for _, item := range result.Data.Items {
116-
if item.ChannelID != 0 {
117-
channelIDs = append(channelIDs, item.ChannelID)
118-
}
119-
if item.CreatorID != 0 {
120-
personIDs = append(personIDs, item.CreatorID)
121-
}
122-
}
123-
124-
// Fetch enrichment data concurrently
125-
var channelMap map[int64]ChannelInfo
126-
var personMap map[int64]PersonInfo
127-
g, gctx := errgroup.WithContext(ctx)
128-
129-
g.Go(func() error {
130-
channelMap, _ = client.fetchChannelInfos(gctx, channelIDs)
131-
return nil
132-
})
133-
134-
g.Go(func() error {
135-
personMap, _ = client.fetchPersonInfos(gctx, personIDs)
136-
return nil
137-
})
138-
139-
_ = g.Wait() // Ignore errors for enrichment as it's best-effort
140-
141-
// Build enriched changes
142-
changes := make([]Change, 0, len(result.Data.Items))
143-
for _, item := range result.Data.Items {
144-
change := Change{
145-
ChangeID: item.ChangeID,
146-
Title: item.Title,
147-
Description: item.Description,
148-
Type: item.Type,
149-
Status: item.Status,
150-
ChannelID: item.ChannelID,
151-
CreatorID: item.CreatorID,
152-
StartTime: item.StartTime,
153-
EndTime: item.EndTime,
154-
Labels: item.Labels,
155-
}
156-
157-
if ch, ok := channelMap[item.ChannelID]; ok {
158-
change.ChannelName = ch.ChannelName
159-
}
160-
if p, ok := personMap[item.CreatorID]; ok {
161-
change.CreatorName = p.PersonName
162-
}
163-
164-
changes = append(changes, change)
61+
return mcp.NewToolResultError(fmt.Sprintf("Unable to retrieve changes: %v", err)), nil
16562
}
16663

16764
return MarshalResult(map[string]any{
168-
"changes": changes,
169-
"total": result.Data.Total,
65+
"changes": output.Changes,
66+
"total": output.Total,
17067
}), nil
17168
}
17269
}

0 commit comments

Comments
 (0)