mirror of
https://github.com/navidrome/navidrome.git
synced 2025-05-07 22:01:08 +03:00
feat: implement GetArtistURL in MCP agent
Add support for retrieving artist URLs via the MCP agent.\n\n- Implement the agents.ArtistURLRetriever interface.\n- Add the GetArtistURL method, which calls the 'get_artist_url'\n tool on the external MCP server.\n- Define the necessary constants and argument struct.\n\nThis mirrors the existing implementation for GetArtistBiography, reusing\nthe persistent process and client logic.
This commit is contained in:
parent
8b754a7c73
commit
9c20520d59
@ -21,10 +21,9 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
mcpAgentName = "mcp"
|
mcpAgentName = "mcp"
|
||||||
// Hardcoded path for PoC
|
|
||||||
mcpServerPath = "/Users/deluan/Development/navidrome/plugins-mcp/mcp-server"
|
mcpServerPath = "/Users/deluan/Development/navidrome/plugins-mcp/mcp-server"
|
||||||
mcpToolName = "get_artist_biography"
|
mcpToolNameGetBio = "get_artist_biography"
|
||||||
// TODO: Add configuration for restart delays
|
mcpToolNameGetURL = "get_artist_url"
|
||||||
initializationTimeout = 10 * time.Second
|
initializationTimeout = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -165,13 +164,11 @@ func (a *MCPAgent) ensureClientInitialized(ctx context.Context) (err error) {
|
|||||||
return nil // Success
|
return nil // Success
|
||||||
}
|
}
|
||||||
|
|
||||||
// getArtistBiographyArgs defines the structure for the MCP tool arguments.
|
// getArtistBiographyArgs defines the structure for the get_artist_biography MCP tool arguments.
|
||||||
// IMPORTANT: Field names MUST be exported (start with uppercase) for JSON marshalling,
|
|
||||||
// but the `json` tags determine the actual field names sent over MCP.
|
|
||||||
type getArtistBiographyArgs struct {
|
type getArtistBiographyArgs struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Mbid string `json:"mbid,omitempty"` // Use omitempty if MBID might be absent
|
Mbid string `json:"mbid,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetArtistBiography retrieves the artist biography by calling the external MCP server.
|
// GetArtistBiography retrieves the artist biography by calling the external MCP server.
|
||||||
@ -207,35 +204,102 @@ func (a *MCPAgent) GetArtistBiography(ctx context.Context, id, name, mbid string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Call the tool using the client reference
|
// Call the tool using the client reference
|
||||||
log.Debug(ctx, "Calling MCP tool", "tool", mcpToolName, "args", args)
|
log.Debug(ctx, "Calling MCP tool", "tool", mcpToolNameGetBio, "args", args)
|
||||||
response, err := currentClient.CallTool(ctx, mcpToolName, args) // Use currentClient
|
response, err := currentClient.CallTool(ctx, mcpToolNameGetBio, args) // Use currentClient
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Handle potential pipe closures or other communication errors
|
// Handle potential pipe closures or other communication errors
|
||||||
log.Error(ctx, "Failed to call MCP tool", "tool", mcpToolName, "error", err)
|
log.Error(ctx, "Failed to call MCP tool", "tool", mcpToolNameGetBio, "error", err)
|
||||||
// Check if the error indicates a broken pipe, suggesting the server died
|
// Check if the error indicates a broken pipe, suggesting the server died
|
||||||
if errors.Is(err, io.ErrClosedPipe) || strings.Contains(err.Error(), "broken pipe") || strings.Contains(err.Error(), "EOF") {
|
if errors.Is(err, io.ErrClosedPipe) || strings.Contains(err.Error(), "broken pipe") || strings.Contains(err.Error(), "EOF") {
|
||||||
log.Warn(ctx, "MCP tool call failed, possibly due to server process exit. State will be reset.")
|
log.Warn(ctx, "MCP tool call failed, possibly due to server process exit. State will be reset.")
|
||||||
// State reset is handled by the monitoring goroutine, just return error
|
// State reset is handled by the monitoring goroutine, just return error
|
||||||
return "", fmt.Errorf("MCP agent process communication error: %w", err)
|
return "", fmt.Errorf("MCP agent process communication error: %w", err)
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("failed to call MCP tool '%s': %w", mcpToolName, err)
|
return "", fmt.Errorf("failed to call MCP tool '%s': %w", mcpToolNameGetBio, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the response
|
// Process the response
|
||||||
if response == nil || len(response.Content) == 0 || response.Content[0].TextContent == nil {
|
if response == nil || len(response.Content) == 0 || response.Content[0].TextContent == nil {
|
||||||
log.Warn(ctx, "MCP tool returned empty or invalid response", "tool", mcpToolName)
|
log.Warn(ctx, "MCP tool returned empty or invalid response", "tool", mcpToolNameGetBio)
|
||||||
return "", agents.ErrNotFound
|
return "", agents.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
bio := response.Content[0].TextContent.Text
|
bio := response.Content[0].TextContent.Text
|
||||||
log.Debug(ctx, "Received biography from MCP agent", "tool", mcpToolName, "bioLength", len(bio))
|
log.Debug(ctx, "Received biography from MCP agent", "tool", mcpToolNameGetBio, "bioLength", len(bio))
|
||||||
|
|
||||||
// Return the biography text
|
// Return the biography text
|
||||||
return bio, nil
|
return bio, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure MCPAgent implements the required interface
|
// getArtistURLArgs defines the structure for the get_artist_url MCP tool arguments.
|
||||||
|
type getArtistURLArgs struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Mbid string `json:"mbid,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArtistURL retrieves the artist URL by calling the external MCP server.
|
||||||
|
func (a *MCPAgent) GetArtistURL(ctx context.Context, id, name, mbid string) (string, error) {
|
||||||
|
// Ensure the client is initialized and the server is running (attempts restart if needed)
|
||||||
|
if err := a.ensureClientInitialized(ctx); err != nil {
|
||||||
|
log.Error(ctx, "MCP agent initialization/restart failed, cannot get URL", "error", err)
|
||||||
|
return "", fmt.Errorf("MCP agent not ready: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock to ensure only one request uses the client/pipes at a time
|
||||||
|
a.mu.Lock()
|
||||||
|
|
||||||
|
// Check if the client is still valid *after* ensuring initialization and acquiring lock.
|
||||||
|
if a.client == nil {
|
||||||
|
a.mu.Unlock()
|
||||||
|
log.Error(ctx, "MCP client became invalid after initialization check (server process likely died)")
|
||||||
|
return "", fmt.Errorf("MCP agent process is not running")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep a reference to the client while locked
|
||||||
|
currentClient := a.client
|
||||||
|
a.mu.Unlock() // Release lock before making the potentially blocking MCP call
|
||||||
|
|
||||||
|
log.Debug(ctx, "Calling MCP agent GetArtistURL", "id", id, "name", name, "mbid", mbid)
|
||||||
|
|
||||||
|
// Prepare arguments for the tool call
|
||||||
|
args := getArtistURLArgs{
|
||||||
|
ID: id,
|
||||||
|
Name: name,
|
||||||
|
Mbid: mbid,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the tool using the client reference
|
||||||
|
log.Debug(ctx, "Calling MCP tool", "tool", mcpToolNameGetURL, "args", args)
|
||||||
|
response, err := currentClient.CallTool(ctx, mcpToolNameGetURL, args) // Use currentClient
|
||||||
|
if err != nil {
|
||||||
|
// Handle potential pipe closures or other communication errors
|
||||||
|
log.Error(ctx, "Failed to call MCP tool", "tool", mcpToolNameGetURL, "error", err)
|
||||||
|
// Check if the error indicates a broken pipe, suggesting the server died
|
||||||
|
if errors.Is(err, io.ErrClosedPipe) || strings.Contains(err.Error(), "broken pipe") || strings.Contains(err.Error(), "EOF") {
|
||||||
|
log.Warn(ctx, "MCP tool call failed, possibly due to server process exit. State will be reset.")
|
||||||
|
// State reset is handled by the monitoring goroutine, just return error
|
||||||
|
return "", fmt.Errorf("MCP agent process communication error: %w", err)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("failed to call MCP tool '%s': %w", mcpToolNameGetURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the response
|
||||||
|
if response == nil || len(response.Content) == 0 || response.Content[0].TextContent == nil {
|
||||||
|
log.Warn(ctx, "MCP tool returned empty or invalid response", "tool", mcpToolNameGetURL)
|
||||||
|
return "", agents.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
url := response.Content[0].TextContent.Text
|
||||||
|
log.Debug(ctx, "Received URL from MCP agent", "tool", mcpToolNameGetURL, "url", url)
|
||||||
|
|
||||||
|
// Return the URL text
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure MCPAgent implements the required interfaces
|
||||||
var _ agents.ArtistBiographyRetriever = (*MCPAgent)(nil)
|
var _ agents.ArtistBiographyRetriever = (*MCPAgent)(nil)
|
||||||
|
var _ agents.ArtistURLRetriever = (*MCPAgent)(nil)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
agents.Register(mcpAgentName, mcpConstructor)
|
agents.Register(mcpAgentName, mcpConstructor)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user