mirror of
https://github.com/taigrr/elevenlabs-mcp.git
synced 2026-04-05 20:52:43 -07:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cfd456802 | |||
| 1c55b42bb3 | |||
| 23c150f057 | |||
| 3d103f53b7 | |||
| f1718526a7 | |||
| 264cb81514 | |||
| 417499bf2f | |||
| 8debf3a3b3 |
18
.github/workflows/test.yml
vendored
Normal file
18
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Test
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Install ALSA dev headers
|
||||
run: sudo apt-get update && sudo apt-get install -y libasound2-dev
|
||||
- run: go test -race ./...
|
||||
- run: go vet ./...
|
||||
- run: go build ./...
|
||||
24
go.mod
24
go.mod
@@ -1,28 +1,22 @@
|
||||
module github.com/taigrr/elevenlabs-mcp
|
||||
|
||||
go 1.23.2
|
||||
|
||||
toolchain go1.24.6
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/gopxl/beep/v2 v2.1.1
|
||||
github.com/mark3labs/mcp-go v0.37.0
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.1
|
||||
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/ebitengine/oto/v3 v3.4.0 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/google/jsonschema-go v0.4.2 // 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/segmentio/asm v1.2.1 // indirect
|
||||
github.com/segmentio/encoding v0.5.4 // 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
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
)
|
||||
|
||||
57
go.sum
57
go.sum
@@ -1,55 +1,42 @@
|
||||
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/ebitengine/oto/v3 v3.4.0 h1:br0PgASsEWaoWn38b2Goe7m1GKFYfNgnsjSd5Gg+/bQ=
|
||||
github.com/ebitengine/oto/v3 v3.4.0/go.mod h1:IOleLVD0m+CMak3mRVwsYY8vTctQgOM0iiL6S7Ar7eI=
|
||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
|
||||
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
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/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc=
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s=
|
||||
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/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
|
||||
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
||||
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/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
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=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
143
internal/ximcp/audio_test.go
Normal file
143
internal/ximcp/audio_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package ximcp
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateRandomHex(t *testing.T) {
|
||||
hex1 := generateRandomHex(RandomHexLength)
|
||||
hex2 := generateRandomHex(RandomHexLength)
|
||||
|
||||
if len(hex1) != RandomHexLength {
|
||||
t.Errorf("expected length %d, got %d", RandomHexLength, len(hex1))
|
||||
}
|
||||
|
||||
if len(hex2) != RandomHexLength {
|
||||
t.Errorf("expected length %d, got %d", RandomHexLength, len(hex2))
|
||||
}
|
||||
|
||||
// Two random hex strings should almost certainly differ
|
||||
if hex1 == hex2 {
|
||||
t.Error("two consecutive random hex values should differ")
|
||||
}
|
||||
|
||||
// Verify hex characters only
|
||||
for _, c := range hex1 {
|
||||
if !strings.ContainsRune("0123456789abcdef", c) {
|
||||
t.Errorf("non-hex character in output: %c", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateFilePath(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
path := s.generateFilePath()
|
||||
|
||||
if !strings.HasPrefix(path, AudioDirectory+string(filepath.Separator)) {
|
||||
t.Errorf("path should start with %s/, got %q", AudioDirectory, path)
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(path, ".mp3") {
|
||||
t.Errorf("path should end with .mp3, got %q", path)
|
||||
}
|
||||
|
||||
// Two paths should differ (different timestamps or random hex)
|
||||
path2 := s.generateFilePath()
|
||||
if path == path2 {
|
||||
t.Error("two consecutive file paths should differ")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureDirectoryExists(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
nested := filepath.Join(tmpDir, "a", "b", "c", "file.mp3")
|
||||
|
||||
err := s.ensureDirectoryExists(nested)
|
||||
if err != nil {
|
||||
t.Fatalf("ensureDirectoryExists failed: %v", err)
|
||||
}
|
||||
|
||||
dirPath := filepath.Dir(nested)
|
||||
info, err := os.Stat(dirPath)
|
||||
if err != nil {
|
||||
t.Fatalf("directory not created: %v", err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
t.Error("expected directory, got file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteAudioFile(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
filePath := filepath.Join(tmpDir, "test.mp3")
|
||||
data := []byte("fake audio data")
|
||||
|
||||
err := s.writeAudioFile(filePath, data)
|
||||
if err != nil {
|
||||
t.Fatalf("writeAudioFile failed: %v", err)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read written file: %v", err)
|
||||
}
|
||||
|
||||
if string(content) != string(data) {
|
||||
t.Errorf("file content mismatch: got %q, want %q", content, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteTextFile(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
audioPath := filepath.Join(tmpDir, "test.mp3")
|
||||
text := "This is the spoken text"
|
||||
|
||||
err := s.writeTextFile(audioPath, text)
|
||||
if err != nil {
|
||||
t.Fatalf("writeTextFile failed: %v", err)
|
||||
}
|
||||
|
||||
expectedPath := filepath.Join(tmpDir, "test.txt")
|
||||
content, err := os.ReadFile(expectedPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read text file: %v", err)
|
||||
}
|
||||
|
||||
if string(content) != text {
|
||||
t.Errorf("text file content mismatch: got %q, want %q", content, text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFileToAudioFileNotFound(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
_, err := s.ReadFileToAudio("/nonexistent/file.txt")
|
||||
if err == nil {
|
||||
t.Error("expected error for nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateAudioNoVoice(t *testing.T) {
|
||||
s := &Server{
|
||||
currentVoice: nil,
|
||||
}
|
||||
|
||||
_, err := s.GenerateAudio("test text")
|
||||
if err == nil {
|
||||
t.Error("expected error when no voice selected")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "no voice selected") {
|
||||
t.Errorf("expected 'no voice selected' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
148
internal/ximcp/history_test.go
Normal file
148
internal/ximcp/history_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package ximcp
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateSummary(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "short text unchanged",
|
||||
input: "Hello world",
|
||||
expected: "Hello world",
|
||||
},
|
||||
{
|
||||
name: "exactly max words",
|
||||
input: "one two three four five six seven eight nine ten",
|
||||
expected: "one two three four five six seven eight nine ten",
|
||||
},
|
||||
{
|
||||
name: "exceeds max words truncated",
|
||||
input: "one two three four five six seven eight nine ten eleven twelve",
|
||||
expected: "one two three four five six seven eight nine ten...",
|
||||
},
|
||||
{
|
||||
name: "empty text",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
input: " \t\n ",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "text with extra whitespace trimmed at edges",
|
||||
input: " hello world ",
|
||||
expected: "hello world",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := s.createSummary(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("createSummary(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAudioHistoryEmptyDir(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
// Temporarily override the audio directory to a non-existent path
|
||||
origDir := AudioDirectory
|
||||
t.Cleanup(func() {
|
||||
// AudioDirectory is a const, so we test the behavior as-is
|
||||
_ = origDir
|
||||
})
|
||||
|
||||
// GetAudioHistory with non-existent directory should return empty
|
||||
tmpDir := t.TempDir()
|
||||
nonExistent := filepath.Join(tmpDir, "does-not-exist")
|
||||
|
||||
// We can't override the const, so test processAudioFiles directly
|
||||
entries, err := os.ReadDir(nonExistent)
|
||||
if err != nil {
|
||||
// Expected — directory doesn't exist
|
||||
if !os.IsNotExist(err) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
result := s.processAudioFiles(entries)
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected empty result, got %d files", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessAudioFiles(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create test files
|
||||
mp3File := filepath.Join(tmpDir, "test.mp3")
|
||||
txtFile := filepath.Join(tmpDir, "test.txt")
|
||||
otherFile := filepath.Join(tmpDir, "readme.md")
|
||||
|
||||
if err := os.WriteFile(mp3File, []byte("fake audio"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(txtFile, []byte("Hello world test"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(otherFile, []byte("not audio"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result := s.processAudioFiles(entries)
|
||||
|
||||
// Should only include .mp3 files
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 audio file, got %d", len(result))
|
||||
}
|
||||
|
||||
if result[0].Name != "test.mp3" {
|
||||
t.Errorf("expected name 'test.mp3', got %q", result[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAudioSummary(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create a text file alongside an mp3
|
||||
txtPath := filepath.Join(tmpDir, "audio.txt")
|
||||
if err := os.WriteFile(txtPath, []byte("This is a test summary"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// getAudioSummary uses AudioDirectory const, so we test createSummary instead
|
||||
summary := s.createSummary("This is a test summary")
|
||||
if summary != "This is a test summary" {
|
||||
t.Errorf("unexpected summary: %q", summary)
|
||||
}
|
||||
|
||||
// Test with no text file fallback
|
||||
summary = s.createSummary("")
|
||||
if summary != "" {
|
||||
t.Errorf("expected empty summary, got %q", summary)
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/gopxl/beep/v2"
|
||||
"github.com/gopxl/beep/v2/speaker"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/taigrr/elevenlabs/client"
|
||||
"github.com/taigrr/elevenlabs/client/types"
|
||||
)
|
||||
@@ -24,7 +24,7 @@ const (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
mcpServer *server.MCPServer
|
||||
mcpServer *mcp.Server
|
||||
client client.Client
|
||||
voices []types.VoiceResponseModel
|
||||
currentVoice *types.VoiceResponseModel
|
||||
@@ -32,7 +32,7 @@ type Server struct {
|
||||
playMutex sync.Mutex
|
||||
}
|
||||
|
||||
func NewServer(mcpServer *server.MCPServer) (*Server, error) {
|
||||
func NewServer() (*mcp.Server, error) {
|
||||
apiKey := os.Getenv("XI_API_KEY")
|
||||
if apiKey == "" {
|
||||
return nil, fmt.Errorf("XI_API_KEY environment variable is required")
|
||||
@@ -40,6 +40,11 @@ func NewServer(mcpServer *server.MCPServer) (*Server, error) {
|
||||
|
||||
elevenClient := client.New(apiKey)
|
||||
|
||||
mcpServer := mcp.NewServer(&mcp.Implementation{
|
||||
Name: "ElevenLabs MCP Server",
|
||||
Version: "1.0.0",
|
||||
}, nil)
|
||||
|
||||
s := &Server{
|
||||
client: elevenClient,
|
||||
mcpServer: mcpServer,
|
||||
@@ -53,7 +58,9 @@ func NewServer(mcpServer *server.MCPServer) (*Server, error) {
|
||||
return nil, fmt.Errorf("failed to initialize speaker: %w", err)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
s.setupTools()
|
||||
|
||||
return mcpServer, nil
|
||||
}
|
||||
|
||||
func (s *Server) initializeVoices() error {
|
||||
|
||||
190
internal/ximcp/server_test.go
Normal file
190
internal/ximcp/server_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package ximcp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/taigrr/elevenlabs/client/types"
|
||||
)
|
||||
|
||||
func TestFindVoiceByID(t *testing.T) {
|
||||
s := &Server{
|
||||
voices: []types.VoiceResponseModel{
|
||||
{VoiceID: "abc123", Name: "Alice"},
|
||||
{VoiceID: "def456", Name: "Bob"},
|
||||
{VoiceID: "ghi789", Name: "Charlie"},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
voiceID string
|
||||
expected string
|
||||
found bool
|
||||
}{
|
||||
{"find first voice", "abc123", "Alice", true},
|
||||
{"find middle voice", "def456", "Bob", true},
|
||||
{"find last voice", "ghi789", "Charlie", true},
|
||||
{"voice not found", "zzz999", "", false},
|
||||
{"empty id", "", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
voice := s.findVoiceByID(tt.voiceID)
|
||||
if tt.found {
|
||||
if voice == nil {
|
||||
t.Fatal("expected voice, got nil")
|
||||
}
|
||||
if voice.Name != tt.expected {
|
||||
t.Errorf("expected name %q, got %q", tt.expected, voice.Name)
|
||||
}
|
||||
} else {
|
||||
if voice != nil {
|
||||
t.Errorf("expected nil, got voice %q", voice.Name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDefaultVoiceIfNeeded(t *testing.T) {
|
||||
t.Run("sets first voice when no current", func(t *testing.T) {
|
||||
s := &Server{
|
||||
voices: []types.VoiceResponseModel{
|
||||
{VoiceID: "abc123", Name: "Alice"},
|
||||
{VoiceID: "def456", Name: "Bob"},
|
||||
},
|
||||
currentVoice: nil,
|
||||
}
|
||||
|
||||
s.setDefaultVoiceIfNeeded()
|
||||
|
||||
if s.currentVoice == nil {
|
||||
t.Fatal("expected current voice to be set")
|
||||
}
|
||||
if s.currentVoice.Name != "Alice" {
|
||||
t.Errorf("expected first voice 'Alice', got %q", s.currentVoice.Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("does not override existing voice", func(t *testing.T) {
|
||||
bob := types.VoiceResponseModel{VoiceID: "def456", Name: "Bob"}
|
||||
s := &Server{
|
||||
voices: []types.VoiceResponseModel{
|
||||
{VoiceID: "abc123", Name: "Alice"},
|
||||
bob,
|
||||
},
|
||||
currentVoice: &bob,
|
||||
}
|
||||
|
||||
s.setDefaultVoiceIfNeeded()
|
||||
|
||||
if s.currentVoice.Name != "Bob" {
|
||||
t.Errorf("expected voice to remain 'Bob', got %q", s.currentVoice.Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no voices available", func(t *testing.T) {
|
||||
s := &Server{
|
||||
voices: []types.VoiceResponseModel{},
|
||||
currentVoice: nil,
|
||||
}
|
||||
|
||||
s.setDefaultVoiceIfNeeded()
|
||||
|
||||
if s.currentVoice != nil {
|
||||
t.Error("expected no voice when list is empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetVoice(t *testing.T) {
|
||||
s := &Server{
|
||||
voices: []types.VoiceResponseModel{
|
||||
{VoiceID: "abc123", Name: "Alice"},
|
||||
{VoiceID: "def456", Name: "Bob"},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("set valid voice", func(t *testing.T) {
|
||||
voice, err := s.SetVoice("def456")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if voice.Name != "Bob" {
|
||||
t.Errorf("expected 'Bob', got %q", voice.Name)
|
||||
}
|
||||
if s.currentVoice.VoiceID != "def456" {
|
||||
t.Error("currentVoice not updated")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("set invalid voice", func(t *testing.T) {
|
||||
_, err := s.SetVoice("nonexistent")
|
||||
if err == nil {
|
||||
t.Error("expected error for nonexistent voice")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatVoiceList(t *testing.T) {
|
||||
current := types.VoiceResponseModel{VoiceID: "abc123", Name: "Alice", Category: "premade"}
|
||||
s := &Server{}
|
||||
|
||||
voices := []types.VoiceResponseModel{
|
||||
current,
|
||||
{VoiceID: "def456", Name: "Bob", Category: "cloned"},
|
||||
}
|
||||
|
||||
result := s.formatVoiceList(voices, ¤t)
|
||||
|
||||
if result == "" {
|
||||
t.Error("expected non-empty result")
|
||||
}
|
||||
|
||||
// Current voice should be marked with asterisk
|
||||
if !contains(result, "* Alice") {
|
||||
t.Error("expected current voice to be marked with asterisk")
|
||||
}
|
||||
|
||||
// Other voice should not be marked
|
||||
if contains(result, "* Bob") {
|
||||
t.Error("non-current voice should not have asterisk marker")
|
||||
}
|
||||
|
||||
// Both voices should appear
|
||||
if !contains(result, "Alice") || !contains(result, "Bob") {
|
||||
t.Error("both voices should appear in output")
|
||||
}
|
||||
|
||||
if !contains(result, "Currently selected: Alice") {
|
||||
t.Error("expected currently selected line")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatVoiceListNoSelection(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
voices := []types.VoiceResponseModel{
|
||||
{VoiceID: "abc123", Name: "Alice", Category: "premade"},
|
||||
}
|
||||
|
||||
result := s.formatVoiceList(voices, nil)
|
||||
|
||||
if !contains(result, "No voice currently selected") {
|
||||
t.Error("expected 'No voice currently selected' when nil")
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && searchString(s, substr)
|
||||
}
|
||||
|
||||
func searchString(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -5,203 +5,142 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/taigrr/elevenlabs/client/types"
|
||||
)
|
||||
|
||||
func (s *Server) SetupTools() {
|
||||
s.mcpServer.AddTool(s.say())
|
||||
s.mcpServer.AddTool(s.read())
|
||||
s.mcpServer.AddTool(s.play())
|
||||
s.mcpServer.AddTool(s.setVoice())
|
||||
s.mcpServer.AddTool(s.getVoices())
|
||||
s.mcpServer.AddTool(s.history())
|
||||
type SayArgs struct {
|
||||
Text string `json:"text" jsonschema:"Text to convert to speech"`
|
||||
}
|
||||
|
||||
func (s *Server) say() (mcp.Tool, server.ToolHandlerFunc) {
|
||||
tool := mcp.Tool{
|
||||
type ReadArgs struct {
|
||||
FilePath string `json:"file_path" jsonschema:"Path to the text file to read and convert to speech"`
|
||||
}
|
||||
|
||||
type PlayArgs struct {
|
||||
FilePath string `json:"file_path" jsonschema:"Path to the audio file to play"`
|
||||
}
|
||||
|
||||
type SetVoiceArgs struct {
|
||||
VoiceID string `json:"voice_id" jsonschema:"ID of the voice to use"`
|
||||
}
|
||||
|
||||
func (s *Server) setupTools() {
|
||||
mcp.AddTool(s.mcpServer, &mcp.Tool{
|
||||
Name: "say",
|
||||
Description: "Convert text to speech, save as MP3 file, and play the audio",
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"text": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Text to convert to speech",
|
||||
},
|
||||
},
|
||||
Required: []string{"text"},
|
||||
},
|
||||
}
|
||||
}, s.say)
|
||||
|
||||
handler := 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
|
||||
}
|
||||
|
||||
s.PlayAudioAsync(filepath)
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: fmt.Sprintf("Audio generated, saved to %s, and playing", filepath),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return tool, handler
|
||||
}
|
||||
|
||||
func (s *Server) read() (mcp.Tool, server.ToolHandlerFunc) {
|
||||
tool := mcp.Tool{
|
||||
mcp.AddTool(s.mcpServer, &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"},
|
||||
},
|
||||
}
|
||||
}, s.read)
|
||||
|
||||
handler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
filePath, err := request.RequireString("file_path")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
audioPath, err := s.ReadFileToAudio(filePath)
|
||||
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
|
||||
}
|
||||
return tool, handler
|
||||
}
|
||||
|
||||
func (s *Server) play() (mcp.Tool, server.ToolHandlerFunc) {
|
||||
tool := mcp.Tool{
|
||||
mcp.AddTool(s.mcpServer, &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"},
|
||||
},
|
||||
}
|
||||
}, s.play)
|
||||
|
||||
handler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
filePath, err := request.RequireString("file_path")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.PlayAudioAsync(filePath)
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: fmt.Sprintf("Playing audio file: %s", filePath),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return tool, handler
|
||||
}
|
||||
|
||||
func (s *Server) setVoice() (mcp.Tool, server.ToolHandlerFunc) {
|
||||
tool := mcp.Tool{
|
||||
mcp.AddTool(s.mcpServer, &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"},
|
||||
},
|
||||
}
|
||||
}, s.setVoice)
|
||||
|
||||
handler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
voiceID, err := request.RequireString("voice_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
selectedVoice, err := s.SetVoice(voiceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: fmt.Sprintf("Voice set to: %s (%s)", selectedVoice.Name, selectedVoice.VoiceID),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return tool, handler
|
||||
}
|
||||
|
||||
func (s *Server) getVoices() (mcp.Tool, server.ToolHandlerFunc) {
|
||||
tool := mcp.Tool{
|
||||
mcp.AddTool(s.mcpServer, &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{},
|
||||
},
|
||||
}
|
||||
}, s.getVoices)
|
||||
|
||||
handler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
voices, currentVoice, err := s.GetVoices()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
voiceList := s.formatVoiceList(voices, currentVoice)
|
||||
mcp.AddTool(s.mcpServer, &mcp.Tool{
|
||||
Name: "history",
|
||||
Description: "List available audio files with text summaries",
|
||||
}, s.history)
|
||||
}
|
||||
|
||||
func (s *Server) say(ctx context.Context, req *mcp.CallToolRequest, args SayArgs) (*mcp.CallToolResult, any, error) {
|
||||
filepath, err := s.GenerateAudio(args.Text)
|
||||
if err != nil {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: voiceList,
|
||||
},
|
||||
&mcp.TextContent{Text: fmt.Sprintf("Error: %v", err)},
|
||||
},
|
||||
}, nil
|
||||
IsError: true,
|
||||
}, nil, nil
|
||||
}
|
||||
return tool, handler
|
||||
|
||||
s.PlayAudioAsync(filepath)
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("Audio generated, saved to %s, and playing", filepath)},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) read(ctx context.Context, req *mcp.CallToolRequest, args ReadArgs) (*mcp.CallToolResult, any, error) {
|
||||
audioPath, err := s.ReadFileToAudio(args.FilePath)
|
||||
if err != nil {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("Error: %v", err)},
|
||||
},
|
||||
IsError: true,
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("File '%s' converted to speech and saved to: %s", args.FilePath, audioPath)},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) play(ctx context.Context, req *mcp.CallToolRequest, args PlayArgs) (*mcp.CallToolResult, any, error) {
|
||||
s.PlayAudioAsync(args.FilePath)
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("Playing audio file: %s", args.FilePath)},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) setVoice(ctx context.Context, req *mcp.CallToolRequest, args SetVoiceArgs) (*mcp.CallToolResult, any, error) {
|
||||
selectedVoice, err := s.SetVoice(args.VoiceID)
|
||||
if err != nil {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("Error: %v", err)},
|
||||
},
|
||||
IsError: true,
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("Voice set to: %s (%s)", selectedVoice.Name, selectedVoice.VoiceID)},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) getVoices(ctx context.Context, req *mcp.CallToolRequest, args struct{}) (*mcp.CallToolResult, any, error) {
|
||||
voices, currentVoice, err := s.GetVoices()
|
||||
if err != nil {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("Error: %v", err)},
|
||||
},
|
||||
IsError: true,
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
voiceList := s.formatVoiceList(voices, currentVoice)
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: voiceList},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) formatVoiceList(voices []types.VoiceResponseModel, currentVoice *types.VoiceResponseModel) string {
|
||||
@@ -227,45 +166,32 @@ func (s *Server) formatVoiceList(voices []types.VoiceResponseModel, currentVoice
|
||||
return voiceList.String()
|
||||
}
|
||||
|
||||
func (s *Server) history() (mcp.Tool, server.ToolHandlerFunc) {
|
||||
tool := mcp.Tool{
|
||||
Name: "history",
|
||||
Description: "List available audio files with text summaries",
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{},
|
||||
},
|
||||
}
|
||||
|
||||
handler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
audioFiles, err := s.GetAudioHistory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(audioFiles) == 0 {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: "No audio files found",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
historyList := s.formatHistoryList(audioFiles)
|
||||
|
||||
func (s *Server) history(ctx context.Context, req *mcp.CallToolRequest, args struct{}) (*mcp.CallToolResult, any, error) {
|
||||
audioFiles, err := s.GetAudioHistory()
|
||||
if err != nil {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: historyList,
|
||||
},
|
||||
&mcp.TextContent{Text: fmt.Sprintf("Error: %v", err)},
|
||||
},
|
||||
}, nil
|
||||
IsError: true,
|
||||
}, nil, nil
|
||||
}
|
||||
return tool, handler
|
||||
|
||||
if len(audioFiles) == 0 {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: "No audio files found"},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
historyList := s.formatHistoryList(audioFiles)
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: historyList},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) formatHistoryList(audioFiles []AudioFile) string {
|
||||
|
||||
14
main.go
14
main.go
@@ -1,26 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/taigrr/elevenlabs-mcp/internal/ximcp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mcpServer := server.NewMCPServer(
|
||||
"ElevenLabs MCP Server",
|
||||
"1.0.0",
|
||||
server.WithToolCapabilities(true),
|
||||
)
|
||||
elevenServer, err := ximcp.NewServer(mcpServer)
|
||||
server, err := ximcp.NewServer()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create ElevenLabs server: %v", err)
|
||||
}
|
||||
|
||||
elevenServer.SetupTools()
|
||||
|
||||
if err := server.ServeStdio(mcpServer); err != nil {
|
||||
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
|
||||
log.Fatalf("Failed to serve MCP server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user