Skip to content

Commit c853af3

Browse files
committed
fix: render mcp server create details
1 parent 715d38f commit c853af3

2 files changed

Lines changed: 94 additions & 3 deletions

File tree

internal/cli/generic_table.go

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const maxHeuristicColumns = 8
2020
// can't blow out the table width.
2121
const genericStringMaxWidth = 40
2222

23+
const mcpPerUserOAuthNotice = "Note: registered but not usable until OAuth is completed in Flashduty Customize -> Connectors; tools will not appear until authorized."
24+
2325
// instantLike mirrors go-flashduty's Timestamp/TimestampMilli (and the output
2426
// package's unexported instant) so the renderer can recognise timestamp fields
2527
// by reflection.
@@ -62,15 +64,18 @@ func renderGenericTable(ctx *RunContext, data any) error {
6264
if rows, total, ok := listEnvelope(v); ok {
6365
return renderRowTable(ctx, rows, total)
6466
}
65-
return renderVertical(ctx, v)
67+
if err := renderVertical(ctx, v); err != nil {
68+
return err
69+
}
70+
return renderMcpPerUserOAuthNotice(ctx, v)
6671
default:
6772
return jsonFallback(ctx, data)
6873
}
6974
}
7075

7176
// listEnvelope reports whether struct v is a paginated list envelope: exactly
72-
// one exported field that is a slice of structs (the rows), with the remaining
73-
// fields being pagination metadata. It returns the rows value and the total
77+
// one exported field that is a slice of structs (the rows), with any remaining
78+
// fields limited to pagination metadata. It returns the rows value and the total
7479
// (the int field named "Total" when present, else the row count).
7580
func listEnvelope(v reflect.Value) (rows reflect.Value, total int, ok bool) {
7681
t := v.Type()
@@ -92,6 +97,9 @@ func listEnvelope(v reflect.Value) (rows reflect.Value, total int, ok bool) {
9297
if total < 0 && f.Name == "Total" && fv.CanInt() {
9398
total = int(fv.Int())
9499
}
100+
if !isListMetadataField(f, fv) {
101+
return reflect.Value{}, 0, false
102+
}
95103
}
96104
if !found {
97105
return reflect.Value{}, 0, false
@@ -102,6 +110,22 @@ func listEnvelope(v reflect.Value) (rows reflect.Value, total int, ok bool) {
102110
return rows, total, true
103111
}
104112

113+
func isListMetadataField(f reflect.StructField, fv reflect.Value) bool {
114+
if f.Anonymous && f.Name == "ListOptions" {
115+
return true
116+
}
117+
switch f.Name {
118+
case "Total":
119+
return fv.CanInt()
120+
case "HasNextPage":
121+
return fv.Kind() == reflect.Bool
122+
case "SearchAfterCtx", "NextCursor":
123+
return fv.Kind() == reflect.String
124+
default:
125+
return false
126+
}
127+
}
128+
105129
// isRowSlice reports whether t is a slice whose element (after pointer deref) is
106130
// a struct — i.e. a table-able row collection.
107131
func isRowSlice(t reflect.Type) bool {
@@ -210,6 +234,22 @@ func renderVertical(ctx *RunContext, v reflect.Value) error {
210234
return ctx.Printer.Print(rows, cols)
211235
}
212236

237+
func renderMcpPerUserOAuthNotice(ctx *RunContext, v reflect.Value) error {
238+
if !isMcpPerUserOAuth(v) {
239+
return nil
240+
}
241+
_, err := fmt.Fprintln(ctx.Writer, mcpPerUserOAuthNotice)
242+
return err
243+
}
244+
245+
func isMcpPerUserOAuth(v reflect.Value) bool {
246+
if v.Type().Name() != "McpServerItem" {
247+
return false
248+
}
249+
auth := v.FieldByName("AuthMode")
250+
return auth.IsValid() && auth.Kind() == reflect.String && auth.String() == "per_user_oauth"
251+
}
252+
213253
// derefStruct dereferences pointer chains and returns the underlying struct
214254
// reflect.Value. The second return is false when item is nil, not a struct after
215255
// dereferencing, or any pointer in the chain is nil.

internal/cli/generic_table_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,57 @@ func TestRenderGenericTable_DetailVertical(t *testing.T) {
144144
}
145145
}
146146

147+
func TestRenderGenericTable_McpServerItemWithEmptyToolsRendersDetail(t *testing.T) {
148+
var buf bytes.Buffer
149+
item := &flashduty.McpServerItem{
150+
ServerID: "mcp_test",
151+
ServerName: "github",
152+
Description: "GitHub connector",
153+
AuthMode: "shared",
154+
Status: "enabled",
155+
Transport: "streamable-http",
156+
URL: "https://mcp.example.com/github",
157+
Tools: []flashduty.McpToolInfo{},
158+
}
159+
if err := renderGenericTable(tableCtx(&buf), item); err != nil {
160+
t.Fatalf("render: %v", err)
161+
}
162+
got := buf.String()
163+
if strings.Contains(got, "No results.") {
164+
t.Fatalf("single MCP server item with empty tools was rendered as an empty list:\n%s", got)
165+
}
166+
for _, want := range []string{"FIELD", "VALUE", "SERVER_ID", "mcp_test", "SERVER_NAME", "github"} {
167+
if !strings.Contains(got, want) {
168+
t.Errorf("MCP server detail output missing %q\n---\n%s", want, got)
169+
}
170+
}
171+
}
172+
173+
func TestRenderGenericTable_McpServerPerUserOAuthNotice(t *testing.T) {
174+
var buf bytes.Buffer
175+
item := &flashduty.McpServerItem{
176+
ServerID: "mcp_oauth",
177+
ServerName: "github",
178+
Description: "GitHub connector",
179+
AuthMode: "per_user_oauth",
180+
Status: "enabled",
181+
Transport: "streamable-http",
182+
URL: "https://mcp.example.com/github",
183+
}
184+
if err := renderGenericTable(tableCtx(&buf), item); err != nil {
185+
t.Fatalf("render: %v", err)
186+
}
187+
got := buf.String()
188+
for _, want := range []string{
189+
"registered but not usable until OAuth is completed in Flashduty Customize -> Connectors",
190+
"tools will not appear until authorized",
191+
} {
192+
if !strings.Contains(got, want) {
193+
t.Errorf("per-user OAuth notice missing %q\n---\n%s", want, got)
194+
}
195+
}
196+
}
197+
147198
func TestPrintGenericResult_StructuredUnchanged(t *testing.T) {
148199
resp := &fakeListResp{Items: []heuristicRow{{Name: "alpha", Count: 7}}, Total: 1}
149200

0 commit comments

Comments
 (0)