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