initial commit

This commit is contained in:
2025-08-07 22:42:36 -07:00
commit 83db12423e
6 changed files with 685 additions and 0 deletions

40
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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)
}
}