66 "encoding/csv"
77 "encoding/json"
88 "fmt"
9- "maps"
109 "sort"
1110 "strings"
1211
@@ -15,6 +14,7 @@ import (
1514 "github.com/modelcontextprotocol/go-sdk/mcp"
1615)
1716
17+ // Ordered by preference when a response wrapper contains multiple arrays.
1818var primaryCSVRowKeys = []string {
1919 "items" ,
2020 "issues" ,
@@ -34,6 +34,12 @@ var primaryCSVRowKeys = []string{
3434 "teams" ,
3535 "members" ,
3636 "projects" ,
37+ "nodes" ,
38+ }
39+
40+ type csvOutputDocument struct {
41+ metadata map [string ]string
42+ rows []map [string ]string
3743}
3844
3945func withCSVOutputVariants (tools []inventory.ServerTool ) []inventory.ServerTool {
@@ -95,10 +101,9 @@ func convertJSONTextResultToCSV(result *mcp.CallToolResult) *mcp.CallToolResult
95101 return utils .NewToolResultErrorFromErr ("failed to convert response to CSV" , err )
96102 }
97103
98- resultCopy := * result
99- resultCopy .Content = []mcp.Content {& mcp.TextContent {Text : csvText }}
100- resultCopy .StructuredContent = nil
101- return & resultCopy
104+ result .Content = []mcp.Content {& mcp.TextContent {Text : csvText }}
105+ result .StructuredContent = nil
106+ return result
102107}
103108
104109func jsonTextToCSV (text string ) (string , error ) {
@@ -110,19 +115,24 @@ func jsonTextToCSV(text string) (string, error) {
110115 return "" , fmt .Errorf ("failed to unmarshal JSON text: %w" , err )
111116 }
112117
113- rows := csvRows (value )
114- if len (rows ) == 0 {
118+ doc := csvDocument (value )
119+ if len (doc . metadata ) == 0 && len ( doc . rows ) == 0 {
115120 return "" , nil
116121 }
117122
118- headers := csvHeaders (rows )
119123 var buf bytes.Buffer
124+ writeCSVMetadata (& buf , doc .metadata )
125+ if len (doc .rows ) == 0 {
126+ return buf .String (), nil
127+ }
128+
129+ headers := csvHeaders (doc .rows )
120130 writer := csv .NewWriter (& buf )
121131 if err := writer .Write (headers ); err != nil {
122132 return "" , fmt .Errorf ("failed to write CSV header: %w" , err )
123133 }
124134
125- for _ , row := range rows {
135+ for _ , row := range doc . rows {
126136 record := make ([]string , len (headers ))
127137 for i , header := range headers {
128138 record [i ] = row [header ]
@@ -139,82 +149,129 @@ func jsonTextToCSV(text string) (string, error) {
139149 return buf .String (), nil
140150}
141151
142- func csvRows (value any ) [] map [ string ] string {
152+ func csvDocument (value any ) csvOutputDocument {
143153 switch v := value .(type ) {
144154 case []any :
145- return csvRowsFromArray (v , nil )
155+ return csvOutputDocument { rows : csvRowsFromArray (v )}
146156 case map [string ]any :
147157 if rows , metadata , ok := primaryRowsFromMap (v ); ok {
148- return csvRowsFromArray (rows , metadata )
158+ return csvOutputDocument {
159+ metadata : newFlattenedCSVRow (metadata ),
160+ rows : csvRowsFromArray (rows ),
161+ }
149162 }
150- return []map [string ]string {newFlattenedCSVRow (v )}
163+ return csvOutputDocument { rows : []map [string ]string {newFlattenedCSVRow (v )} }
151164 default :
152- return []map [string ]string {{ "value" : scalarCSVValue (v )}}
165+ return csvOutputDocument { rows : []map [string ]string {scalarCSVRow (v )}}
153166 }
154167}
155168
156169func primaryRowsFromMap (value map [string ]any ) ([]any , map [string ]any , bool ) {
170+ if rows , path , ok := primaryRowsAtCurrentLevel (value ); ok {
171+ return rows , metadataWithoutPath (value , path ), true
172+ }
173+ if rows , path , ok := primaryRowsOneLevelDown (value ); ok {
174+ return rows , metadataWithoutPath (value , path ), true
175+ }
176+ return nil , nil , false
177+ }
178+
179+ func primaryRowsAtCurrentLevel (value map [string ]any ) ([]any , []string , bool ) {
157180 if key , ok := preferredPrimaryRowKey (value ); ok {
158181 rows , _ := value [key ].([]any )
159- return rows , metadataWithoutKey (value , key ), true
182+ return rows , []string {key }, true
183+ }
184+ if key , ok := singleArrayKey (value ); ok {
185+ rows , _ := value [key ].([]any )
186+ return rows , []string {key }, true
160187 }
188+ return nil , nil , false
189+ }
161190
162- var arrayKeys []string
191+ func primaryRowsOneLevelDown (value map [string ]any ) ([]any , []string , bool ) {
192+ var matchedRows []any
193+ var matchedPath []string
163194 for key , raw := range value {
164- if _ , ok := raw .([]any ); ok {
165- arrayKeys = append (arrayKeys , key )
195+ child , ok := raw .(map [string ]any )
196+ if ! ok {
197+ continue
198+ }
199+ rows , path , ok := primaryRowsAtCurrentLevel (child )
200+ if ! ok {
201+ continue
166202 }
203+ if matchedPath != nil {
204+ return nil , nil , false
205+ }
206+ matchedRows = rows
207+ matchedPath = append ([]string {key }, path ... )
167208 }
168- if len ( arrayKeys ) != 1 {
209+ if matchedPath == nil {
169210 return nil , nil , false
170211 }
171-
172- key := arrayKeys [0 ]
173- rows , _ := value [key ].([]any )
174- return rows , metadataWithoutKey (value , key ), true
212+ return matchedRows , matchedPath , true
175213}
176214
177- func preferredPrimaryRowKey (value map [string ]any ) (string , bool ) {
178- for _ , key := range primaryCSVRowKeys {
179- if _ , ok := value [key ].([]any ); ok {
180- return key , true
181- }
182- }
183- return "" , false
184- }
185-
186- func metadataWithoutKey (value map [string ]any , exclude string ) map [string ]any {
187- metadata := make (map [string ]any , len (value )- 1 )
215+ func metadataWithoutPath (value map [string ]any , path []string ) map [string ]any {
216+ metadata := make (map [string ]any , len (value ))
188217 for key , raw := range value {
189- if key != exclude {
218+ if key != path [ 0 ] {
190219 metadata [key ] = raw
220+ continue
221+ }
222+
223+ if len (path ) == 1 {
224+ continue
225+ }
226+ child , ok := raw .(map [string ]any )
227+ if ! ok {
228+ continue
229+ }
230+ childMetadata := metadataWithoutPath (child , path [1 :])
231+ if len (childMetadata ) > 0 {
232+ metadata [key ] = childMetadata
191233 }
192234 }
193235 return metadata
194236}
195237
196- func csvRowsFromArray (values []any , metadata map [ string ] any ) []map [string ]string {
238+ func csvRowsFromArray (values []any ) []map [string ]string {
197239 if len (values ) == 0 {
198240 return nil
199241 }
200242
201243 rows := make ([]map [string ]string , 0 , len (values ))
202- metadataRow := newFlattenedCSVRow (metadata )
203244 for _ , value := range values {
204- row := make (map [string ]string , len (metadataRow ))
205- maps .Copy (row , metadataRow )
206-
245+ var row map [string ]string
207246 switch v := value .(type ) {
208247 case map [string ]any :
248+ row = make (map [string ]string )
209249 appendFlattenedCSVFields (row , v , "" )
210250 default :
211- row [ "value" ] = scalarCSVValue (v )
251+ row = scalarCSVRow (v )
212252 }
213253 rows = append (rows , row )
214254 }
215255 return rows
216256}
217257
258+ func writeCSVMetadata (buf * bytes.Buffer , metadata map [string ]string ) {
259+ if len (metadata ) == 0 {
260+ return
261+ }
262+
263+ headers := make ([]string , 0 , len (metadata ))
264+ for header := range metadata {
265+ headers = append (headers , header )
266+ }
267+ sort .Strings (headers )
268+
269+ for _ , header := range headers {
270+ fmt .Fprintf (buf , "# %s: %s\n " , header , normalizeCSVWhitespace (metadata [header ]))
271+ }
272+ buf .WriteByte ('\n' )
273+ }
274+
218275func newFlattenedCSVRow (value map [string ]any ) map [string ]string {
219276 row := make (map [string ]string )
220277 appendFlattenedCSVFields (row , value , "" )
@@ -226,15 +283,8 @@ func appendFlattenedCSVFields(row map[string]string, value map[string]any, prefi
226283 return
227284 }
228285
229- keys := make ([]string , 0 , len (value ))
230- for key := range value {
231- keys = append (keys , key )
232- }
233- sort .Strings (keys )
234-
235- for _ , key := range keys {
286+ for key , raw := range value {
236287 column := csvColumnName (prefix , key )
237- raw := value [key ]
238288 switch v := raw .(type ) {
239289 case map [string ]any :
240290 appendFlattenedCSVFields (row , v , column )
@@ -269,6 +319,32 @@ func csvColumnName(prefix, key string) string {
269319 return prefix + "." + key
270320}
271321
322+ func preferredPrimaryRowKey (value map [string ]any ) (string , bool ) {
323+ for _ , key := range primaryCSVRowKeys {
324+ if _ , ok := value [key ].([]any ); ok {
325+ return key , true
326+ }
327+ }
328+ return "" , false
329+ }
330+
331+ func singleArrayKey (value map [string ]any ) (string , bool ) {
332+ var arrayKey string
333+ for key , raw := range value {
334+ if _ , ok := raw .([]any ); ! ok {
335+ continue
336+ }
337+ if arrayKey != "" {
338+ return "" , false
339+ }
340+ arrayKey = key
341+ }
342+ if arrayKey == "" {
343+ return "" , false
344+ }
345+ return arrayKey , true
346+ }
347+
272348func csvColumnValue (column string , value any ) string {
273349 str := scalarCSVValue (value )
274350 if isBodyColumn (column ) {
@@ -301,6 +377,10 @@ func csvArrayValue(values []any) string {
301377 return strings .Join (parts , ";" )
302378}
303379
380+ func scalarCSVRow (value any ) map [string ]string {
381+ return map [string ]string {"value" : scalarCSVValue (value )}
382+ }
383+
304384func scalarCSVValue (value any ) string {
305385 switch v := value .(type ) {
306386 case nil :
0 commit comments