Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions cmd/mcptools/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func newToolsCmd() *cobra.Command {
mcpClient, err := createClient(parsedArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
fmt.Fprintf(os.Stderr, "Example: mcp tools npx -y @modelcontextprotocol/server-filesystem ~/Code\n")
fmt.Fprintf(os.Stderr, "Example: mcp tools npx -y @modelcontextprotocol/server-filesystem ~\n")
os.Exit(1)
}

Expand Down Expand Up @@ -182,7 +182,7 @@ func newResourcesCmd() *cobra.Command {
mcpClient, err := createClient(parsedArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
fmt.Fprintf(os.Stderr, "Example: mcp resources npx -y @modelcontextprotocol/server-filesystem ~/Code\n")
fmt.Fprintf(os.Stderr, "Example: mcp resources npx -y @modelcontextprotocol/server-filesystem ~\n")
os.Exit(1)
}

Expand Down Expand Up @@ -212,7 +212,7 @@ func newPromptsCmd() *cobra.Command {
mcpClient, err := createClient(parsedArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
fmt.Fprintf(os.Stderr, "Example: mcp prompts npx -y @modelcontextprotocol/server-filesystem ~/Code\n")
fmt.Fprintf(os.Stderr, "Example: mcp prompts npx -y @modelcontextprotocol/server-filesystem ~\n")
os.Exit(1)
}

Expand Down Expand Up @@ -241,7 +241,7 @@ func newCallCmd() *cobra.Command {
fmt.Fprintln(os.Stderr, "Error: entity name is required")
fmt.Fprintln(
os.Stderr,
"Example: mcp call read_file npx -y @modelcontextprotocol/server-filesystem ~/Code",
"Example: mcp call read_file npx -y @modelcontextprotocol/server-filesystem ~",
)
os.Exit(1)
}
Expand Down Expand Up @@ -275,7 +275,7 @@ func newCallCmd() *cobra.Command {
fmt.Fprintln(os.Stderr, "Error: entity name is required")
fmt.Fprintln(
os.Stderr,
"Example: mcp call read_file npx -y @modelcontextprotocol/server-filesystem ~/Code",
"Example: mcp call read_file npx -y @modelcontextprotocol/server-filesystem ~",
)
os.Exit(1)
}
Expand All @@ -292,7 +292,7 @@ func newCallCmd() *cobra.Command {
fmt.Fprintln(os.Stderr, "Error: command to execute is required when using stdio transport")
fmt.Fprintln(
os.Stderr,
"Example: mcp call read_file npx -y @modelcontextprotocol/server-filesystem ~/Code",
"Example: mcp call read_file npx -y @modelcontextprotocol/server-filesystem ~",
)
os.Exit(1)
}
Expand Down Expand Up @@ -350,7 +350,7 @@ func newGetPromptCmd() *cobra.Command {
fmt.Fprintln(os.Stderr, "Error: prompt name is required")
fmt.Fprintln(
os.Stderr,
"Example: mcp get-prompt read_file npx -y @modelcontextprotocol/server-filesystem ~/Code",
"Example: mcp get-prompt read_file npx -y @modelcontextprotocol/server-filesystem ~",
)
os.Exit(1)
}
Expand Down Expand Up @@ -384,7 +384,7 @@ func newGetPromptCmd() *cobra.Command {
fmt.Fprintln(os.Stderr, "Error: prompt name is required")
fmt.Fprintln(
os.Stderr,
"Example: mcp get-prompt read_file npx -y @modelcontextprotocol/server-filesystem ~/Code",
"Example: mcp get-prompt read_file npx -y @modelcontextprotocol/server-filesystem ~",
)
os.Exit(1)
}
Expand Down Expand Up @@ -428,7 +428,7 @@ func newReadResourceCmd() *cobra.Command {
fmt.Fprintln(os.Stderr, "Error: resource name is required")
fmt.Fprintln(
os.Stderr,
"Example: mcp read-resource npx -y @modelcontextprotocol/server-filesystem ~/Code",
"Example: mcp read-resource npx -y @modelcontextprotocol/server-filesystem ~",
)
os.Exit(1)
}
Expand Down Expand Up @@ -462,7 +462,7 @@ func newReadResourceCmd() *cobra.Command {
fmt.Fprintln(os.Stderr, "Error: resource name is required")
fmt.Fprintln(
os.Stderr,
"Example: mcp read-resource npx -y @modelcontextprotocol/server-filesystem ~/Code",
"Example: mcp read-resource npx -y @modelcontextprotocol/server-filesystem ~",
)
os.Exit(1)
}
Expand Down Expand Up @@ -519,7 +519,7 @@ func newShellCmd() *cobra.Command { //nolint:gocyclo

if len(parsedArgs) == 0 {
fmt.Fprintln(os.Stderr, "Error: command to execute is required when using the shell")
fmt.Fprintln(os.Stderr, "Example: mcp shell npx -y @modelcontextprotocol/server-filesystem ~/Code")
fmt.Fprintln(os.Stderr, "Example: mcp shell npx -y @modelcontextprotocol/server-filesystem ~")
os.Exit(1)
}

Expand All @@ -535,8 +535,9 @@ func newShellCmd() *cobra.Command { //nolint:gocyclo
os.Exit(1)
}

fmt.Println("mcp > connected to MCP server over stdio")
fmt.Println("mcp > Type '/h' for help or '/q' to quit")
fmt.Println("mcp tools shell")
fmt.Println("connected to:", strings.Join(parsedArgs, " "))
fmt.Println("\nmcp > Type '/h' for help or '/q' to quit")

line := liner.NewLiner()
defer func() { _ = line.Close() }()
Expand Down
14 changes: 10 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ module github.com/f/mcptools

go 1.24.1

require github.com/spf13/cobra v1.9.1
require (
github.com/jedib0t/go-pretty/v6 v6.6.7
github.com/peterh/liner v1.2.2
github.com/spf13/cobra v1.9.1
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.3 // indirect
github.com/peterh/liner v1.2.2 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.6 // indirect
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
)
24 changes: 22 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo=
github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
113 changes: 105 additions & 8 deletions pkg/jsonutils/jsonutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import (
"bytes"
"encoding/json"
"fmt"
"os"
"reflect"
"sort"
"strings"
"text/tabwriter"

"golang.org/x/term"
)

// OutputFormat represents the available output format options.
Expand Down Expand Up @@ -108,6 +111,7 @@ func formatTable(data any) (string, error) {
return formatGenericMap(mapVal)
}

// formatToolsList formats a list of tools as a table.
func formatToolsList(tools any) (string, error) {
toolsSlice, ok := tools.([]any)
if !ok {
Expand All @@ -119,11 +123,19 @@ func formatToolsList(tools any) (string, error) {
}

var buf bytes.Buffer
w := tabwriter.NewWriter(&buf, 0, 0, 2, ' ', 0)
w := tabwriter.NewWriter(&buf, 0, 0, 2, ' ', tabwriter.StripEscape)

fmt.Fprintln(w, "NAME\tDESCRIPTION")
fmt.Fprintln(w, "----\t-----------")

termWidth := getTermWidth()
nameColWidth := 20 // Default name column width
descColWidth := termWidth - nameColWidth - 5 // Leave some margin

if descColWidth < 10 {
descColWidth = max(10, termWidth-nameColWidth-5) // Adaptive minimum width
}

for _, t := range toolsSlice {
tool, ok1 := t.(map[string]any)
if !ok1 {
Expand All @@ -133,17 +145,77 @@ func formatToolsList(tools any) (string, error) {
name, _ := tool["name"].(string)
desc, _ := tool["description"].(string)

if len(desc) > 70 {
desc = desc[:67] + "..."
// Handle multiline description
lines := wrapText(desc, descColWidth)

if len(lines) == 0 {
fmt.Fprintf(w, "%s\t\n", name)
continue
}

// First line with name
fmt.Fprintf(w, "%s\t%s\n", name, lines[0])

// Remaining lines with empty name column
for _, line := range lines[1:] {
fmt.Fprintf(w, "\t%s\n", line)
}

fmt.Fprintf(w, "%s\t%s\n", name, desc)
// Add a blank line between entries
if len(lines) > 1 {
fmt.Fprintln(w, "\t")
}
}

_ = w.Flush()
return buf.String(), nil
}

// getTermWidth returns the terminal width or a default value if detection fails.
func getTermWidth() int {
width, _, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil || width <= 0 {
return 80 // Default width if terminal width cannot be determined
}
return width
}

// wrapText wraps text to fit within a specified width.
func wrapText(text string, width int) []string {
if text == "" {
return []string{}
}

words := strings.Fields(text)
if len(words) == 0 {
return []string{}
}

var lines []string
var currentLine string

for _, word := range words {
switch {
case len(currentLine) == 0:
currentLine = word
case len(currentLine)+len(word)+1 > width:
// Add current line to lines and start a new line
lines = append(lines, currentLine)
currentLine = word
default:
currentLine += " " + word
}
}

// Add the last line
if len(currentLine) > 0 {
lines = append(lines, currentLine)
}

return lines
}

// formatResourcesList formats a list of resources as a table.
func formatResourcesList(resources any) (string, error) {
resourcesSlice, ok := resources.([]any)
if !ok {
Expand All @@ -170,13 +242,15 @@ func formatResourcesList(resources any) (string, error) {
resType, _ := resource["type"].(string)
uri, _ := resource["uri"].(string)

// Use the entire URI instead of truncating
fmt.Fprintf(w, "%s\t%s\t%s\n", name, resType, uri)
}

_ = w.Flush()
return buf.String(), nil
}

// formatPromptsList formats a list of prompts as a table.
func formatPromptsList(prompts any) (string, error) {
promptsSlice, ok := prompts.([]any)
if !ok {
Expand All @@ -188,11 +262,19 @@ func formatPromptsList(prompts any) (string, error) {
}

var buf bytes.Buffer
w := tabwriter.NewWriter(&buf, 0, 0, 2, ' ', 0)
w := tabwriter.NewWriter(&buf, 0, 0, 2, ' ', tabwriter.StripEscape)

fmt.Fprintln(w, "NAME\tDESCRIPTION")
fmt.Fprintln(w, "----\t-----------")

termWidth := getTermWidth()
nameColWidth := 20 // Default name column width
descColWidth := termWidth - nameColWidth - 5 // Leave some margin

if descColWidth < 10 {
descColWidth = 40 // Minimum width if terminal is too narrow
}

for _, p := range promptsSlice {
prompt, ok1 := p.(map[string]any)
if !ok1 {
Expand All @@ -202,11 +284,26 @@ func formatPromptsList(prompts any) (string, error) {
name, _ := prompt["name"].(string)
desc, _ := prompt["description"].(string)

if len(desc) > 70 {
desc = desc[:67] + "..."
// Handle multiline description
lines := wrapText(desc, descColWidth)

if len(lines) == 0 {
fmt.Fprintf(w, "%s\t\n", name)
continue
}

fmt.Fprintf(w, "%s\t%s\n", name, desc)
// First line with name
fmt.Fprintf(w, "%s\t%s\n", name, lines[0])

// Remaining lines with empty name column
for _, line := range lines[1:] {
fmt.Fprintf(w, "\t%s\n", line)
}

// Add a blank line between entries
if len(lines) > 1 {
fmt.Fprintln(w, "\t")
}
}

_ = w.Flush()
Expand Down
Loading