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
22 changes: 13 additions & 9 deletions lib/client/db/mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ func NewRootServer(logger *slog.Logger) *RootServer {
}

// ListDatabases tool function used to list all available/served databases.
//
// Note: Given some MCP clients not fully support resources of any kind (including
// embedded and references), we must return the databases as plain text result so
// the tool keeps working on those clients.
func (s *RootServer) ListDatabases(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
s.mu.RLock()
defer s.mu.RUnlock()
Expand All @@ -76,7 +80,7 @@ func (s *RootServer) ListDatabases(ctx context.Context, request mcp.CallToolRequ
s.logger.ErrorContext(ctx, "error while list databases", "error", err)
return mcp.NewToolResultError(FormatErrorMessage(err).Error()), nil
}
res = append(res, mcp.EmbeddedResource{Type: "resource", Resource: contents})
res = append(res, mcp.NewTextContent(contents))
Comment thread
greedy52 marked this conversation as resolved.
}

return &mcp.CallToolResult{
Expand All @@ -99,7 +103,11 @@ func (s *RootServer) GetDatabaseResource(ctx context.Context, request mcp.ReadRe
return nil, trace.Wrap(err)
}

return []mcp.ResourceContents{encodedDb}, nil
return []mcp.ResourceContents{mcp.TextResourceContents{
URI: request.Params.URI,
MIMEType: databaseResourceMIMEType,
Text: encodedDb,
}}, nil
}

// RegisterDatabase register a database on the root server. This make it
Expand Down Expand Up @@ -130,18 +138,14 @@ func buildDatabaseResource(db *Database) DatabaseResource {
}
}

func encodeDatabaseResource(db *Database) (mcp.ResourceContents, error) {
func encodeDatabaseResource(db *Database) (string, error) {
resource := buildDatabaseResource(db)
out, err := yaml.Marshal(resource)
if err != nil {
return nil, trace.Wrap(err)
return "", trace.Wrap(err)
}

return mcp.TextResourceContents{
URI: resource.URI,
MIMEType: databaseResourceMIMEType,
Text: string(out),
}, nil
return string(out), nil
}

const (
Expand Down
15 changes: 8 additions & 7 deletions lib/client/db/mcp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,23 +80,24 @@ func TestRegisterDatabase(t *testing.T) {
require.Len(t, res.Content, len(databases))

for _, c := range res.Content {
require.IsType(t, mcp.EmbeddedResource{}, c)
require.IsType(t, mcp.TextResourceContents{}, c.(mcp.EmbeddedResource).Resource)
require.IsType(t, mcp.TextContent{}, c)
}

// Although we're not sorting by the URI directly, the only field that
// is different across the databases is their name and URI (which would
// cause them to have the same order). So here we sort by the YAML
// contents to avoid having to decode.
slices.SortFunc(res.Content, func(a, b mcp.Content) int {
resourceA := a.(mcp.EmbeddedResource).Resource.(mcp.TextResourceContents)
resourceB := b.(mcp.EmbeddedResource).Resource.(mcp.TextResourceContents)
return strings.Compare(resourceA.Text, resourceB.Text)
resourceA := a.(mcp.TextContent).Text
resourceB := b.(mcp.TextContent).Text
return strings.Compare(resourceA, resourceB)
})

for i, c := range res.Content {
content := c.(mcp.EmbeddedResource)
assertDatabaseResource(t, databases[i], content.Resource)
var database DatabaseResource
content := c.(mcp.TextContent)
require.NoError(t, yaml.Unmarshal([]byte(content.Text), &database))
require.Empty(t, cmp.Diff(buildDatabaseResource(databases[i]), database, cmpopts.IgnoreFields(types.Metadata{}, "Namespace")))
}
})
}
Expand Down
23 changes: 23 additions & 0 deletions lib/client/mcp/claude/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ func DefaultConfigPath() (string, error) {
}
}

// GlobalCursorPath returns the default path for Cursor global MCP configuration.
//
// https://docs.cursor.com/context/mcp#configuration-locations
func GlobalCursorPath() (string, error) {
homeDir, err := os.UserHomeDir()
Comment thread
greedy52 marked this conversation as resolved.
if err != nil {
return "", trace.Wrap(err)
}

return filepath.Join(homeDir, ".cursor", "mcp.json"), nil
}

// MCPServer contains details to launch an MCP server.
//
// https://modelcontextprotocol.io/quickstart/user
Expand Down Expand Up @@ -239,6 +251,17 @@ func LoadConfigFromDefaultPath() (*FileConfig, error) {
return config, trace.Wrap(err)
}

// LoadConfigFromGlobalCursor loads the Cursor global MCP server configuration.
func LoadConfigFromGlobalCursor() (*FileConfig, error) {
configPath, err := GlobalCursorPath()
if err != nil {
return nil, trace.Wrap(err)
}

config, err := LoadConfigFromFile(configPath)
return config, trace.Wrap(err)
}

// Exists returns true if config file exists.
func (c *FileConfig) Exists() bool {
return c.configExists
Expand Down
6 changes: 6 additions & 0 deletions tool/tsh/common/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ Examples:
Add all MCP servers to Claude Desktop
$ tsh mcp config --all --client-config=claude

Add all MCP servers to Cursor
$ tsh mcp config --all --client-config=cursor

Search MCP servers with labels and add to the specified JSON file
$ tsh mcp config --labels env=dev --client-config=my-config.json`

Expand All @@ -96,6 +99,9 @@ Examples:
Add the database configuration to Claude Desktop
$ tsh mcp db config --db-user=mydbuser --db-name=mydbname --client-config=claude my-db-resource

Add the database configuration to Cursor
$ tsh mcp db config --db-user=mydbuser --db-name=mydbname --client-config=cursor my-db-resource

Add the database configuration to the specified JSON file
$ tsh mcp db config --db-user=mydbuser --db-name=mydbname --client-config=my-config.json my-db-resource
`
Expand Down
18 changes: 14 additions & 4 deletions tool/tsh/common/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,16 @@ type mcpClientConfigFlags struct {

const (
mcpClientConfigClaude = "claude"
mcpClientConfigCursor = "cursor"
)

func (m *mcpClientConfigFlags) addToCmd(cmd *kingpin.CmdClause) {
cmd.Flag(
"client-config",
fmt.Sprintf(
"If specified, update the specified client config. %q for default Claude Desktop config, or specify a JSON file path. Can also be set with environment variable %s.",
"If specified, update the specified client config. %q for default Claude Desktop config, %q for global Cursor MCP servers config, or specify a JSON file path. Can also be set with environment variable %s.",
mcpClientConfigClaude,
mcpClientConfigCursor,
mcpClientConfigEnvVar,
)).
Envar(mcpClientConfigEnvVar).
Expand All @@ -92,6 +94,8 @@ func (m *mcpClientConfigFlags) loadConfig() (*claude.FileConfig, error) {
switch m.clientConfig {
case mcpClientConfigClaude:
return claude.LoadConfigFromDefaultPath()
case mcpClientConfigCursor:
return claude.LoadConfigFromGlobalCursor()
default:
return claude.LoadConfigFromFile(m.clientConfig)
}
Expand All @@ -107,9 +111,7 @@ func (m *mcpClientConfigFlags) jsonFormatOptions() []string {
}

func (m *mcpClientConfigFlags) printHint(w io.Writer) error {
_, err := fmt.Fprintln(w, `Tip: use --client-config=claude to update your Claude Desktop configuration.
You can also specify a custom config path with --client-config=<path> to update
a config file compatible with the "mcpServer" mapping.`)
_, err := fmt.Fprintln(w, mcpConfigHint)
return trace.Wrap(err)
}

Expand Down Expand Up @@ -149,3 +151,11 @@ func getLoggingOptsForMCPServer(cf *CLIConf) loggingOpts {
debug: true,
})
}

// mcpConfigHint is the hint message displayed when the configuration is shown
Comment thread
gabrielcorado marked this conversation as resolved.
// to users.
const mcpConfigHint = `Tip: You can use this command to update your MCP servers configuration file automatically.
- For Claude Desktop, use --client-config=claude to update the default configuration.
- For Cursor, use --client-config=cursor to update the global MCP servers configuration.
In addition, you can use --client-config=<path> to specify a config file location that is compatible with the "mcpServers" mapping.
For example, you can update a Cursor project using --client-config=<path-to-project>/.cursor/mcp.json`
Loading