Skip to content

Commit 85e6d37

Browse files
committed
fix: restore compatibility after SDK migration
1 parent b4fa911 commit 85e6d37

8 files changed

Lines changed: 218 additions & 38 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.24.4
44

55
require (
66
github.com/bluele/gcache v0.0.2
7-
github.com/flashcatcloud/flashduty-sdk v0.3.0
7+
github.com/flashcatcloud/flashduty-sdk v0.3.1-0.20260408101253-bbe0d25ae134
88
github.com/google/go-github/v72 v72.0.0
99
github.com/josephburnett/jd v1.9.2
1010
github.com/mark3labs/mcp-go v0.45.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +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=
13+
github.com/flashcatcloud/flashduty-sdk v0.3.1-0.20260408101253-bbe0d25ae134 h1:QksBXCEjCub9p6na9qqWABTne3oNyV3vVlKZ/lw5qic=
14+
github.com/flashcatcloud/flashduty-sdk v0.3.1-0.20260408101253-bbe0d25ae134/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY=
1515
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
1616
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
1717
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=

internal/flashduty/server.go

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ func NewMCPServer(cfg FlashdutyConfig) (*server.MCPServer, error) {
147147
return flashdutyServer, nil
148148
}
149149

150+
func newStreamableHTTPServer(mcpServer *server.MCPServer, logger *slog.Logger, contextFunc server.HTTPContextFunc) *server.StreamableHTTPServer {
151+
return server.NewStreamableHTTPServer(
152+
mcpServer,
153+
server.WithLogger(&slogAdapter{logger: logger}),
154+
server.WithHTTPContextFunc(contextFunc),
155+
)
156+
}
157+
150158
type StdioServerConfig struct {
151159
// Version of the server
152160
Version string
@@ -339,29 +347,21 @@ func RunHTTPServer(cfg HTTPServerConfig) error {
339347
return fmt.Errorf("failed to create MCP server: %w", err)
340348
}
341349

342-
httpServer := server.NewStreamableHTTPServer(
343-
mcpServer,
344-
server.WithLogger(&slogAdapter{logger: logger}),
345-
// Return 405 for GET requests — this server doesn't use server-initiated
346-
// features (sampling, elicitation). Without this, the SDK's standalone SSE
347-
// GET hangs indefinitely because mcp-go creates an orphan session and blocks.
348-
server.WithDisableStreaming(true),
349-
server.WithHTTPContextFunc(func(ctx context.Context, r *http.Request) context.Context {
350-
// Extract W3C Trace Context from HTTP headers, or generate a new one
351-
traceCtx, err := trace.FromHTTPHeadersOrNew(r.Header)
352-
if err != nil {
353-
logger.Warn("Failed to generate trace context, continuing without trace", "error", err)
354-
// Continue without trace context if generation fails
355-
} else {
356-
ctx = trace.ContextWithTraceContext(ctx, traceCtx)
357-
}
358-
359-
// Note: HTTP request logging is handled by MCP hooks (OnBeforeAny, OnSuccess, OnError)
360-
// which provide more detailed information including method, params, and results.
361-
362-
return httpContextFunc(ctx, r, cfg.BaseURL)
363-
}),
364-
)
350+
httpServer := newStreamableHTTPServer(mcpServer, logger, func(ctx context.Context, r *http.Request) context.Context {
351+
// Extract W3C Trace Context from HTTP headers, or generate a new one
352+
traceCtx, err := trace.FromHTTPHeadersOrNew(r.Header)
353+
if err != nil {
354+
logger.Warn("Failed to generate trace context, continuing without trace", "error", err)
355+
// Continue without trace context if generation fails
356+
} else {
357+
ctx = trace.ContextWithTraceContext(ctx, traceCtx)
358+
}
359+
360+
// Note: HTTP request logging is handled by MCP hooks (OnBeforeAny, OnSuccess, OnError)
361+
// which provide more detailed information including method, params, and results.
362+
363+
return httpContextFunc(ctx, r, cfg.BaseURL)
364+
})
365365

366366
mux := http.NewServeMux()
367367
mux.Handle("/mcp", httpServer)

internal/flashduty/server_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package flashduty
2+
3+
import (
4+
"context"
5+
"io"
6+
"log/slog"
7+
"net/http"
8+
"reflect"
9+
"testing"
10+
11+
"github.com/flashcatcloud/flashduty-mcp-server/pkg/translations"
12+
)
13+
14+
func TestNewStreamableHTTPServer_DoesNotDisableStreaming(t *testing.T) {
15+
t.Parallel()
16+
17+
mcpServer, err := NewMCPServer(FlashdutyConfig{
18+
Version: "test",
19+
Translator: translations.NullTranslationHelper,
20+
EnabledToolsets: []string{"incidents"},
21+
})
22+
if err != nil {
23+
t.Fatalf("failed to create MCP server: %v", err)
24+
}
25+
26+
httpServer := newStreamableHTTPServer(
27+
mcpServer,
28+
slog.New(slog.NewTextHandler(io.Discard, nil)),
29+
func(ctx context.Context, _ *http.Request) context.Context {
30+
return ctx
31+
},
32+
)
33+
34+
value := reflect.ValueOf(httpServer).Elem().FieldByName("disableStreaming")
35+
if !value.IsValid() {
36+
t.Fatal("expected streamable HTTP server to expose disableStreaming field")
37+
}
38+
if value.Bool() {
39+
t.Fatal("expected streaming to remain enabled for GET/SSE clients")
40+
}
41+
}

pkg/flashduty/templates.go

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package flashduty
33
import (
44
"context"
55
"fmt"
6+
"slices"
67

78
sdk "github.com/flashcatcloud/flashduty-sdk"
89
"github.com/mark3labs/mcp-go/mcp"
@@ -15,6 +16,12 @@ import (
1516

1617
const getPresetTemplateDescription = `Fetch the preset (default) notification template for a specific channel. Returns the Go template code used as the starting point for customization.`
1718

19+
func sortedChannelEnumValues() []string {
20+
channels := append([]string(nil), sdk.ChannelEnumValues()...)
21+
slices.Sort(channels)
22+
return channels
23+
}
24+
1825
// GetPresetTemplate creates a tool to fetch the preset template for a channel.
1926
func GetPresetTemplate(getClient GetFlashdutyClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
2027
return mcp.NewTool("get_preset_template",
@@ -26,11 +33,7 @@ func GetPresetTemplate(getClient GetFlashdutyClientFn, t translations.Translatio
2633
mcp.WithString("channel",
2734
mcp.Required(),
2835
mcp.Description("The notification channel to get the preset template for."),
29-
mcp.Enum(sdk.ChannelEnumValues()...),
30-
),
31-
mcp.WithString("locale",
32-
mcp.Description("Locale for the preset template. Defaults to zh-CN."),
33-
mcp.Enum("zh-CN", "en-US"),
36+
mcp.Enum(sortedChannelEnumValues()...),
3437
),
3538
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
3639
ctx, client, err := getClient(ctx)
@@ -43,14 +46,8 @@ func GetPresetTemplate(getClient GetFlashdutyClientFn, t translations.Translatio
4346
return mcp.NewToolResultError(err.Error()), nil
4447
}
4548

46-
locale, _ := OptionalParam[string](request, "locale")
47-
if locale == "" {
48-
locale = "zh-CN"
49-
}
50-
5149
input := &sdk.GetPresetTemplateInput{
5250
Channel: channel,
53-
Locale: locale,
5451
}
5552

5653
output, err := client.GetPresetTemplate(ctx, input)
@@ -77,7 +74,7 @@ func ValidateTemplate(getClient GetFlashdutyClientFn, t translations.Translation
7774
mcp.WithString("channel",
7875
mcp.Required(),
7976
mcp.Description("The notification channel this template is for."),
80-
mcp.Enum(sdk.ChannelEnumValues()...),
77+
mcp.Enum(sortedChannelEnumValues()...),
8178
),
8279
mcp.WithString("template_code",
8380
mcp.Required(),

pkg/flashduty/templates_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package flashduty
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"testing"
7+
8+
"github.com/flashcatcloud/flashduty-mcp-server/pkg/translations"
9+
sdk "github.com/flashcatcloud/flashduty-sdk"
10+
)
11+
12+
func TestGetPresetTemplateSchemaDoesNotExposeLocale(t *testing.T) {
13+
t.Parallel()
14+
15+
tool, _ := GetPresetTemplate(func(ctx context.Context) (context.Context, *sdk.Client, error) {
16+
return ctx, nil, nil
17+
}, translations.NullTranslationHelper)
18+
19+
raw, err := json.Marshal(tool)
20+
if err != nil {
21+
t.Fatalf("marshal tool: %v", err)
22+
}
23+
24+
var payload map[string]any
25+
if err := json.Unmarshal(raw, &payload); err != nil {
26+
t.Fatalf("unmarshal tool: %v", err)
27+
}
28+
29+
schema, ok := payload["inputSchema"].(map[string]any)
30+
if !ok {
31+
t.Fatalf("expected inputSchema object, got %#v", payload["inputSchema"])
32+
}
33+
props, ok := schema["properties"].(map[string]any)
34+
if !ok {
35+
t.Fatalf("expected properties object, got %#v", schema["properties"])
36+
}
37+
if _, ok := props["locale"]; ok {
38+
t.Fatal("expected locale to be absent from get_preset_template schema")
39+
}
40+
41+
channelSchema, ok := props["channel"].(map[string]any)
42+
if !ok {
43+
t.Fatalf("expected channel schema, got %#v", props["channel"])
44+
}
45+
enumVals, ok := channelSchema["enum"].([]any)
46+
if !ok {
47+
t.Fatalf("expected channel enum values, got %#v", channelSchema["enum"])
48+
}
49+
want := sortedChannelEnumValues()
50+
if len(enumVals) != len(want) {
51+
t.Fatalf("expected %d channel enum values, got %d", len(want), len(enumVals))
52+
}
53+
for i, got := range enumVals {
54+
if got.(string) != want[i] {
55+
t.Fatalf("channel enum[%d] = %q, want %q", i, got.(string), want[i])
56+
}
57+
}
58+
}

pkg/flashduty/users.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ func QueryTeams(getClient GetFlashdutyClientFn, t translations.TranslationHelper
110110
return mcp.NewToolResultError(fmt.Sprintf("Unable to retrieve teams: %v", err)), nil
111111
}
112112

113+
// Preserve the historical direct-lookup shape for team_ids queries.
114+
if len(input.TeamIDs) > 0 {
115+
return MarshalResult(map[string]any{
116+
"items": output.Teams,
117+
}), nil
118+
}
119+
113120
return MarshalResult(map[string]any{
114121
"teams": output.Teams,
115122
"total": output.Total,

pkg/flashduty/users_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package flashduty
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
sdk "github.com/flashcatcloud/flashduty-sdk"
11+
"github.com/mark3labs/mcp-go/mcp"
12+
13+
"github.com/flashcatcloud/flashduty-mcp-server/pkg/translations"
14+
)
15+
16+
func TestQueryTeamsByIDsPreservesLegacyItemsShape(t *testing.T) {
17+
t.Parallel()
18+
19+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20+
if r.URL.Path != "/team/infos" {
21+
t.Fatalf("unexpected path: %s", r.URL.Path)
22+
}
23+
w.Header().Set("Content-Type", "application/json")
24+
_ = json.NewEncoder(w).Encode(map[string]any{
25+
"data": map[string]any{
26+
"items": []any{
27+
map[string]any{
28+
"team_id": 101,
29+
"team_name": "alpha",
30+
},
31+
},
32+
},
33+
})
34+
}))
35+
defer ts.Close()
36+
37+
client, err := sdk.NewClient("test-key", sdk.WithBaseURL(ts.URL))
38+
if err != nil {
39+
t.Fatalf("new sdk client: %v", err)
40+
}
41+
42+
_, handler := QueryTeams(func(ctx context.Context) (context.Context, *sdk.Client, error) {
43+
return ctx, client, nil
44+
}, translations.NullTranslationHelper)
45+
46+
result, err := handler(context.Background(), mcp.CallToolRequest{
47+
Params: mcp.CallToolParams{
48+
Name: "query_teams",
49+
Arguments: map[string]any{
50+
"team_ids": "101",
51+
},
52+
},
53+
})
54+
if err != nil {
55+
t.Fatalf("handler returned error: %v", err)
56+
}
57+
if result.IsError {
58+
t.Fatalf("expected success result, got error result: %#v", result)
59+
}
60+
61+
textContent, ok := mcp.AsTextContent(result.Content[0])
62+
if !ok {
63+
t.Fatalf("expected text content, got %#v", result.Content[0])
64+
}
65+
66+
var payload map[string]any
67+
if err := json.Unmarshal([]byte(textContent.Text), &payload); err != nil {
68+
t.Fatalf("unmarshal payload: %v", err)
69+
}
70+
71+
if _, ok := payload["items"]; !ok {
72+
t.Fatalf("expected legacy items key, got %#v", payload)
73+
}
74+
if _, ok := payload["teams"]; ok {
75+
t.Fatalf("did not expect teams key for team_ids lookup, got %#v", payload)
76+
}
77+
}

0 commit comments

Comments
 (0)