mirror of
https://github.com/taigrr/elevenlabs-mcp.git
synced 2026-04-02 03:08:57 -07:00
initial commit
This commit is contained in:
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
# Crush directory
|
||||
.crush/
|
||||
|
||||
# Go build artifacts
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.test
|
||||
*.out
|
||||
go.work
|
||||
|
||||
# Dependency directories
|
||||
vendor/
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Audio files (except examples)
|
||||
*.mp3
|
||||
*.wav
|
||||
*.m4a
|
||||
!examples/**/*.mp3
|
||||
!examples/**/*.wav
|
||||
!examples/**/*.m4a.xi/
|
||||
.xi/
|
||||
36
CRUSH.md
Normal file
36
CRUSH.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# ElevenLabs MCP Server
|
||||
|
||||
## Build/Test Commands
|
||||
- Build: `go build -o elevenlabs-mcp`
|
||||
- Run: `./elevenlabs-mcp` (requires XI_API_KEY env var)
|
||||
- Test: `go test ./...`
|
||||
- Lint: `golangci-lint run` (if available) or `go vet ./...`
|
||||
- Format: `gofmt -w .` or `goimports -w .`
|
||||
- Dependencies: `go mod tidy && go mod download`
|
||||
|
||||
## Environment Setup
|
||||
- Required: `export XI_API_KEY=your_api_key_here`
|
||||
- Audio files saved to: `.xi/<millis>-<hex5>.mp3`
|
||||
|
||||
## Code Style
|
||||
- Use `goimports` for formatting
|
||||
- Follow Go naming conventions (PascalCase for exported, camelCase for unexported)
|
||||
- No single-letter variables except loop counters
|
||||
- Use meaningful error messages with context
|
||||
- Prefer explicit error handling over panics
|
||||
- Use sync.RWMutex for concurrent access to shared data
|
||||
- Constants for magic strings/numbers, defined at package level
|
||||
|
||||
## MCP Tools Provided
|
||||
- `say`: Convert text to speech, save as MP3
|
||||
- `read`: Read text file and convert to speech
|
||||
- `play`: Play audio file using beep library
|
||||
- `set_voice`: Change TTS voice (memory only)
|
||||
- `get_voices`: List available voices, show current selection
|
||||
- `history`: List available audio files with text summaries
|
||||
|
||||
## Dependencies
|
||||
- `github.com/mark3labs/mcp-go` - MCP server framework
|
||||
- `github.com/taigrr/elevenlabs` - ElevenLabs API client
|
||||
- `github.com/gopxl/beep` - Audio playback
|
||||
- `github.com/google/uuid` - UUID generation
|
||||
66
README.md
Normal file
66
README.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# ElevenLabs MCP Server
|
||||
|
||||
An MCP (Model Context Protocol) server that provides text-to-speech capabilities using ElevenLabs API.
|
||||
|
||||
## Features
|
||||
|
||||
- **say**: Convert text to speech and save as MP3 file
|
||||
- **read**: Read a text file and convert it to speech
|
||||
- **play**: Play audio files using the beep library
|
||||
- **set_voice**: Change the voice used for generation (memory only)
|
||||
- **get_voices**: List available voices and show currently selected one
|
||||
|
||||
## Setup
|
||||
|
||||
1. Get your ElevenLabs API key from [ElevenLabs](https://elevenlabs.io)
|
||||
2. Set the environment variable:
|
||||
```bash
|
||||
export XI_API_KEY=your_api_key_here
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
go build -o elevenlabs-mcp
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The server runs via stdio and communicates using the MCP protocol:
|
||||
|
||||
```bash
|
||||
export XI_API_KEY=your_api_key_here
|
||||
./elevenlabs-mcp
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
### say
|
||||
Convert text to speech and save as MP3.
|
||||
- **text** (string, required): Text to convert to speech
|
||||
|
||||
### read
|
||||
Read a text file and convert it to speech.
|
||||
- **file_path** (string, required): Path to the text file
|
||||
|
||||
### play
|
||||
Play an audio file.
|
||||
- **file_path** (string, required): Path to the audio file
|
||||
|
||||
### set_voice
|
||||
Change the voice used for generation.
|
||||
- **voice_id** (string, required): ID of the voice to use
|
||||
|
||||
### get_voices
|
||||
List all available voices and show the currently selected one.
|
||||
- No parameters required
|
||||
|
||||
## Audio Files
|
||||
|
||||
Generated audio files are saved to `.crush/xi/<uuid>.mp3` and can be replayed using the `play` tool.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- ElevenLabs API for text-to-speech generation
|
||||
- Beep library for audio playback
|
||||
- Mark3labs MCP-Go for MCP server functionality
|
||||
28
go.mod
Normal file
28
go.mod
Normal file
@@ -0,0 +1,28 @@
|
||||
module github.com/taigrr/elevenlabs-mcp
|
||||
|
||||
go 1.23.2
|
||||
|
||||
toolchain go1.24.6
|
||||
|
||||
require (
|
||||
github.com/gopxl/beep/v2 v2.1.1
|
||||
github.com/mark3labs/mcp-go v0.37.0
|
||||
github.com/taigrr/elevenlabs v0.1.18
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/ebitengine/oto/v3 v3.3.2 // indirect
|
||||
github.com/ebitengine/purego v0.8.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hajimehoshi/go-mp3 v0.3.4 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
55
go.sum
Normal file
55
go.sum
Normal file
@@ -0,0 +1,55 @@
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
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/ebitengine/oto/v3 v3.3.2 h1:VTWBsKX9eb+dXzaF4jEwQbs4yWIdXukJ0K40KgkpYlg=
|
||||
github.com/ebitengine/oto/v3 v3.3.2/go.mod h1:MZeb/lwoC4DCOdiTIxYezrURTw7EvK/yF863+tmBI+U=
|
||||
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
|
||||
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopxl/beep/v2 v2.1.1 h1:6FYIYMm2qPAdWkjX+7xwKrViS1x0Po5kDMdRkq8NVbU=
|
||||
github.com/gopxl/beep/v2 v2.1.1/go.mod h1:ZAm9TGQ9lvpoiFLd4zf5B1IuyxZhgRACMId1XJbaW0E=
|
||||
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
|
||||
github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo=
|
||||
github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mark3labs/mcp-go v0.37.0 h1:BywvZLPRT6Zx6mMG/MJfxLSZQkTGIcJSEGKsvr4DsoQ=
|
||||
github.com/mark3labs/mcp-go v0.37.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/taigrr/elevenlabs v0.1.18 h1:ZvRsLjzV4ov6Rdls7MNFKvcknLc527RR6beV6pjukJU=
|
||||
github.com/taigrr/elevenlabs v0.1.18/go.mod h1:ardwj7FGIQbpvRl+UpdRzVnxOuo7QQxqC3YhFcvY1gM=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
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=
|
||||
460
main.go
Normal file
460
main.go
Normal file
@@ -0,0 +1,460 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gopxl/beep/v2"
|
||||
"github.com/gopxl/beep/v2/mp3"
|
||||
"github.com/gopxl/beep/v2/speaker"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/taigrr/elevenlabs/client"
|
||||
"github.com/taigrr/elevenlabs/client/types"
|
||||
)
|
||||
|
||||
type ElevenLabsServer struct {
|
||||
client client.Client
|
||||
voices []types.VoiceResponseModel
|
||||
currentVoice *types.VoiceResponseModel
|
||||
voicesMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewElevenLabsServer() (*ElevenLabsServer, error) {
|
||||
apiKey := os.Getenv("XI_API_KEY")
|
||||
if apiKey == "" {
|
||||
return nil, fmt.Errorf("XI_API_KEY environment variable is required")
|
||||
}
|
||||
|
||||
elevenClient := client.New(apiKey)
|
||||
|
||||
s := &ElevenLabsServer{
|
||||
client: elevenClient,
|
||||
}
|
||||
|
||||
// Initialize voices and set default
|
||||
if err := s.refreshVoices(); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize voices: %w", err)
|
||||
}
|
||||
|
||||
// Initialize speaker for audio playback
|
||||
sr := beep.SampleRate(44100)
|
||||
speaker.Init(sr, sr.N(time.Second/10))
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *ElevenLabsServer) refreshVoices() error {
|
||||
s.voicesMutex.Lock()
|
||||
defer s.voicesMutex.Unlock()
|
||||
|
||||
voices, err := s.client.GetVoices(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get voices: %w", err)
|
||||
}
|
||||
|
||||
s.voices = voices
|
||||
|
||||
// Set default voice if none selected
|
||||
if s.currentVoice == nil && len(voices) > 0 {
|
||||
s.currentVoice = &voices[0]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateRandomHex(length int) string {
|
||||
bytes := make([]byte, length)
|
||||
rand.Read(bytes)
|
||||
return fmt.Sprintf("%x", bytes)[:length]
|
||||
}
|
||||
|
||||
func (s *ElevenLabsServer) generateAudio(text string) (string, error) {
|
||||
if s.currentVoice == nil {
|
||||
return "", fmt.Errorf("no voice selected")
|
||||
}
|
||||
|
||||
// Generate audio using TTS
|
||||
audioData, err := s.client.TTS(context.Background(), text, s.currentVoice.VoiceID, "", types.SynthesisOptions{
|
||||
Stability: 0.5,
|
||||
SimilarityBoost: 0.5,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate speech: %w", err)
|
||||
}
|
||||
|
||||
// Create filename with timestamp and random hex
|
||||
timestamp := time.Now().UnixMilli()
|
||||
randomHex := generateRandomHex(5)
|
||||
filename := fmt.Sprintf("%d-%s.mp3", timestamp, randomHex)
|
||||
filePath := filepath.Join(".xi", filename)
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
// Write audio file
|
||||
if err := os.WriteFile(filePath, audioData, 0644); err != nil {
|
||||
return "", fmt.Errorf("failed to write audio file: %w", err)
|
||||
}
|
||||
|
||||
// Write text file alongside audio
|
||||
textFilePath := strings.TrimSuffix(filePath, ".mp3") + ".txt"
|
||||
if err := os.WriteFile(textFilePath, []byte(text), 0644); err != nil {
|
||||
return "", fmt.Errorf("failed to write text file: %w", err)
|
||||
}
|
||||
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
func (s *ElevenLabsServer) playAudio(filepath string) error {
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open audio file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
streamer, format, err := mp3.Decode(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode mp3: %w", err)
|
||||
}
|
||||
defer streamer.Close()
|
||||
|
||||
resampled := beep.Resample(4, format.SampleRate, 44100, streamer)
|
||||
|
||||
done := make(chan bool)
|
||||
speaker.Play(beep.Seq(resampled, beep.Callback(func() {
|
||||
done <- true
|
||||
})))
|
||||
|
||||
<-done
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ElevenLabsServer) setupTools(mcpServer *server.MCPServer) {
|
||||
// Say tool
|
||||
sayTool := mcp.Tool{
|
||||
Name: "say",
|
||||
Description: "Convert text to speech and save as MP3 file",
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"text": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Text to convert to speech",
|
||||
},
|
||||
},
|
||||
Required: []string{"text"},
|
||||
},
|
||||
}
|
||||
|
||||
mcpServer.AddTool(sayTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
text, err := request.RequireString("text")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filepath, err := s.generateAudio(text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: fmt.Sprintf("Audio generated and saved to: %s", filepath),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
|
||||
// Read tool
|
||||
readTool := mcp.Tool{
|
||||
Name: "read",
|
||||
Description: "Read a text file and convert it to speech, saving as MP3",
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"file_path": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Path to the text file to read and convert to speech",
|
||||
},
|
||||
},
|
||||
Required: []string{"file_path"},
|
||||
},
|
||||
}
|
||||
|
||||
mcpServer.AddTool(readTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
filePath, err := request.RequireString("file_path")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read file content
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
text := string(content)
|
||||
audioPath, err := s.generateAudio(text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: fmt.Sprintf("File '%s' converted to speech and saved to: %s", filePath, audioPath),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
|
||||
// Play tool
|
||||
playTool := mcp.Tool{
|
||||
Name: "play",
|
||||
Description: "Play an audio file",
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"file_path": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Path to the audio file to play",
|
||||
},
|
||||
},
|
||||
Required: []string{"file_path"},
|
||||
},
|
||||
}
|
||||
|
||||
mcpServer.AddTool(playTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
filePath, err := request.RequireString("file_path")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.playAudio(filePath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: fmt.Sprintf("Played audio file: %s", filePath),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
|
||||
// Set voice tool
|
||||
setVoiceTool := mcp.Tool{
|
||||
Name: "set_voice",
|
||||
Description: "Set the voice to use for text-to-speech generation",
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"voice_id": map[string]any{
|
||||
"type": "string",
|
||||
"description": "ID of the voice to use",
|
||||
},
|
||||
},
|
||||
Required: []string{"voice_id"},
|
||||
},
|
||||
}
|
||||
|
||||
mcpServer.AddTool(setVoiceTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
voiceID, err := request.RequireString("voice_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.voicesMutex.Lock()
|
||||
defer s.voicesMutex.Unlock()
|
||||
|
||||
// Find the voice
|
||||
var selectedVoice *types.VoiceResponseModel
|
||||
for i, voice := range s.voices {
|
||||
if voice.VoiceID == voiceID {
|
||||
selectedVoice = &s.voices[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if selectedVoice == nil {
|
||||
return nil, fmt.Errorf("voice with ID '%s' not found", voiceID)
|
||||
}
|
||||
|
||||
s.currentVoice = selectedVoice
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: fmt.Sprintf("Voice set to: %s (%s)", selectedVoice.Name, selectedVoice.VoiceID),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
|
||||
// Get voices tool
|
||||
getVoicesTool := mcp.Tool{
|
||||
Name: "get_voices",
|
||||
Description: "Get list of available voices and show the currently selected one",
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{},
|
||||
},
|
||||
}
|
||||
|
||||
mcpServer.AddTool(getVoicesTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Refresh voices from API
|
||||
if err := s.refreshVoices(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.voicesMutex.RLock()
|
||||
defer s.voicesMutex.RUnlock()
|
||||
|
||||
var voiceList strings.Builder
|
||||
voiceList.WriteString("Available voices:\n")
|
||||
|
||||
for _, voice := range s.voices {
|
||||
marker := " "
|
||||
if s.currentVoice != nil && voice.VoiceID == s.currentVoice.VoiceID {
|
||||
marker = "* "
|
||||
}
|
||||
voiceList.WriteString(fmt.Sprintf("%s%s (%s) - %s\n",
|
||||
marker, voice.Name, voice.VoiceID, voice.Category))
|
||||
}
|
||||
|
||||
if s.currentVoice != nil {
|
||||
voiceList.WriteString(fmt.Sprintf("\nCurrently selected: %s (%s)",
|
||||
s.currentVoice.Name, s.currentVoice.VoiceID))
|
||||
} else {
|
||||
voiceList.WriteString("\nNo voice currently selected")
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: voiceList.String(),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
|
||||
// History tool
|
||||
historyTool := mcp.Tool{
|
||||
Name: "history",
|
||||
Description: "List available audio files with text summaries",
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{},
|
||||
},
|
||||
}
|
||||
|
||||
mcpServer.AddTool(historyTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Read .xi directory
|
||||
files, err := os.ReadDir(".xi")
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: "No audio files found (directory doesn't exist yet)",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read .xi directory: %w", err)
|
||||
}
|
||||
|
||||
var audioFiles []string
|
||||
for _, file := range files {
|
||||
if strings.HasSuffix(file.Name(), ".mp3") {
|
||||
audioFiles = append(audioFiles, file.Name())
|
||||
}
|
||||
}
|
||||
|
||||
if len(audioFiles) == 0 {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: "No audio files found",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var historyList strings.Builder
|
||||
historyList.WriteString("Available audio files:\n\n")
|
||||
|
||||
for _, audioFile := range audioFiles {
|
||||
// Try to read corresponding text file
|
||||
textFile := strings.TrimSuffix(audioFile, ".mp3") + ".txt"
|
||||
textPath := filepath.Join(".xi", textFile)
|
||||
|
||||
summary := ""
|
||||
if content, err := os.ReadFile(textPath); err == nil {
|
||||
text := strings.TrimSpace(string(content))
|
||||
words := strings.Fields(text)
|
||||
if len(words) > 10 {
|
||||
summary = strings.Join(words[:10], " ") + "..."
|
||||
} else {
|
||||
summary = text
|
||||
}
|
||||
} else {
|
||||
summary = "(no text summary available)"
|
||||
}
|
||||
|
||||
historyList.WriteString(fmt.Sprintf("• %s\n %s\n\n", audioFile, summary))
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: historyList.String(),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create ElevenLabs server
|
||||
elevenServer, err := NewElevenLabsServer()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create ElevenLabs server: %v", err)
|
||||
}
|
||||
|
||||
// Create MCP server
|
||||
mcpServer := server.NewMCPServer(
|
||||
"ElevenLabs MCP Server",
|
||||
"1.0.0",
|
||||
server.WithToolCapabilities(true),
|
||||
)
|
||||
|
||||
// Setup tools
|
||||
elevenServer.setupTools(mcpServer)
|
||||
|
||||
// Serve via stdio
|
||||
if err := server.ServeStdio(mcpServer); err != nil {
|
||||
log.Fatalf("Failed to serve MCP server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user