From 10219468a42111c7507bc8956b3b7b1b42c15cd4 Mon Sep 17 00:00:00 2001 From: lchavasse Date: Thu, 28 Aug 2025 19:32:22 +0100 Subject: [PATCH 1/2] git rm scripts/start-server.tsAdded call functionality for 11labs --- CLAUDE.md | 90 +++ bun.lock | 1 + package.json | 1 + .../src/connectors/elevenlabs.spec.ts | 654 ++++++++++++++++++ .../src/connectors/elevenlabs.ts | 287 +++++++- scripts/start-server.ts | 364 ++++++++++ 6 files changed, 1396 insertions(+), 1 deletion(-) create mode 100644 CLAUDE.md create mode 100644 packages/mcp-connectors/src/connectors/elevenlabs.spec.ts create mode 100644 scripts/start-server.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..46ff7415 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a TypeScript monorepo for MCP (Model Context Protocol) connectors that powers disco.dev. It contains pre-built connectors for 35+ popular SaaS tools like GitHub, Slack, Notion, Jira, etc. + +## Development Commands + +### Install dependencies +```bash +bun install +``` + +### Start a connector server for testing +```bash +# No credentials needed (test connector) +bun start --connector test + +# With credentials for production connectors +bun start --connector asana --credentials '{"apiKey":"your-api-key"}' +bun start --connector github --credentials '{"token":"ghp_xxx"}' --setup '{"org":"myorg"}' +``` + +### Build and type checking +```bash +bun run build # Build all packages +bun run typecheck # Type check all packages +bun run test # Run all tests +``` + +### Code quality +```bash +bun run check # Run Biome linter and formatter +bun run check:fix # Auto-fix linting issues +``` + +## Architecture + +### Monorepo Structure +- **packages/mcp-connectors/**: Main connectors package with 35+ connector implementations +- **packages/mcp-config-types/**: Shared TypeScript types and Zod schemas for connector configurations +- **scripts/**: Development scripts for starting and testing connector servers +- **apps/testing-agent/**: Testing utilities + +### Connector Architecture +Each connector is defined using the `mcpConnectorConfig` factory function with: +- **Metadata**: name, key, version, logo, example prompt +- **Credentials schema**: Zod schema defining required API keys/tokens +- **Setup schema**: Zod schema for optional configuration +- **Tools**: Functions that can be called by MCP clients +- **Resources**: Static data that can be retrieved + +### Key Components +- **ConnectorContext**: Provides credential access, data persistence, and caching +- **MCPConnectorConfig**: Type-safe connector configuration with tools and resources +- **Transport layer**: HTTP transport using Hono framework for MCP protocol + +### Tools vs Resources +- **Tools**: Active functions that perform operations (API calls, data processing) +- **Resources**: Passive data retrieval (documentation, static content) + +### Adding New Connectors +1. Create new connector file in `packages/mcp-connectors/src/connectors/` +2. Follow the pattern of existing connectors (see `test.ts` for simple example) +3. Export the connector config and add to `packages/mcp-connectors/src/index.ts` +4. Update credentials/setup schemas in `packages/mcp-config-types/src/` if needed + +## Development Notes + +### Runtime Environment +- Uses **Bun** as package manager and runtime +- **Turbo** for monorepo orchestration +- **Biome** for linting and formatting (90 char line width, single quotes) +- **Vitest** for testing +- **TypeScript 5.9** with strict settings + +### Server Architecture +The development server (`scripts/start-server.ts`) creates: +- MCP server instance with connector tools and resources +- HTTP transport layer using Hono +- Request/response logging with timestamps +- Error handling and debugging output +- Support for credentials and setup configuration via CLI args + +### Key Files +- `packages/mcp-connectors/src/index.ts`: Main connector registry +- `packages/mcp-config-types/src/config.ts`: Connector configuration factory +- `scripts/start-server.ts`: Development server implementation \ No newline at end of file diff --git a/bun.lock b/bun.lock index 1b6da107..1e7ebfca 100644 --- a/bun.lock +++ b/bun.lock @@ -15,6 +15,7 @@ "lint-staged": "^15.2.0", "msw": "^2.10.4", "tsdown": "^0.14.0", + "tsx": "^4.20.5", "turbo": "latest", "typescript": "^5.9.0", "unplugin-unused": "^0.5.1", diff --git a/package.json b/package.json index 6be26722..3f2c6dcd 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "lint-staged": "^15.2.0", "msw": "^2.10.4", "tsdown": "^0.14.0", + "tsx": "^4.20.5", "turbo": "latest", "typescript": "^5.9.0", "unplugin-unused": "^0.5.1", diff --git a/packages/mcp-connectors/src/connectors/elevenlabs.spec.ts b/packages/mcp-connectors/src/connectors/elevenlabs.spec.ts new file mode 100644 index 00000000..6b0de90a --- /dev/null +++ b/packages/mcp-connectors/src/connectors/elevenlabs.spec.ts @@ -0,0 +1,654 @@ +import { describe, expect, it } from "vitest"; +import type { MCPToolDefinition } from "@stackone/mcp-config-types"; +import { createMockConnectorContext } from "../__mocks__/context"; +import { ElevenLabsConnectorConfig } from "./elevenlabs"; +import { http, HttpResponse } from "msw"; +import { setupServer } from "msw/node"; + +const server = setupServer(); + +describe("#ElevenLabsConnector", () => { + describe(".TEXT_TO_SPEECH", () => { + describe("when text is provided", () => { + describe("and API key is valid", () => { + it("returns base64 audio data successfully", async () => { + server.use( + http.post("https://api.elevenlabs.io/v1/text-to-speech/*", () => { + // Mock audio binary data + const mockAudio = new Uint8Array([1, 2, 3, 4, 5]); + return new HttpResponse(mockAudio, { + status: 200, + headers: { "Content-Type": "audio/mpeg" }, + }); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.TEXT_TO_SPEECH as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler({ text: "Hello world" }, mockContext); + const result = JSON.parse(actual); + + expect(result.success).toBe(true); + expect(result.audio_base64).toBeDefined(); + expect(result.format).toBe("mp3_44100_128"); + + server.close(); + }); + }); + + describe("and API key is invalid", () => { + it("returns error message", async () => { + server.use( + http.post("https://api.elevenlabs.io/v1/text-to-speech/*", () => { + return HttpResponse.json( + { detail: { status: "invalid_api_key", message: "Invalid API key" } }, + { status: 401 } + ); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.TEXT_TO_SPEECH as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler({ text: "Hello world" }, mockContext); + const result = JSON.parse(actual); + + expect(result.success).toBe(false); + expect(result.error).toContain("401"); + + server.close(); + }); + }); + + describe("and no audio data is returned", () => { + it("returns error message", async () => { + server.use( + http.post("https://api.elevenlabs.io/v1/text-to-speech/*", () => { + return new HttpResponse(null, { status: 200 }); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.TEXT_TO_SPEECH as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler({ text: "Hello world" }, mockContext); + const result = JSON.parse(actual); + + expect(result.success).toBe(false); + expect(result.error).toContain("No audio data received"); + + server.close(); + }); + }); + }); + }); + + describe(".LIST_VOICES", () => { + describe("when API call succeeds", () => { + describe("and voices are returned", () => { + it("returns list of voices successfully", async () => { + server.use( + http.get("https://api.elevenlabs.io/v1/voices", () => { + return HttpResponse.json({ + voices: [ + { + voice_id: "voice1", + name: "Rachel", + category: "premade", + description: "A calm voice", + available_for_tiers: ["free", "pro"], + }, + ], + }); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.LIST_VOICES as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler({}, mockContext); + const result = JSON.parse(actual); + + expect(result.success).toBe(true); + expect(result.voices).toHaveLength(1); + expect(result.voices[0].voice_id).toBe("voice1"); + expect(result.voices[0].name).toBe("Rachel"); + + server.close(); + }); + }); + + describe("and include_shared is true", () => { + it("includes shared voices in the response", async () => { + server.use( + http.get("https://api.elevenlabs.io/v1/voices", () => { + return HttpResponse.json({ voices: [] }); + }), + http.get("https://api.elevenlabs.io/v1/shared-voices", () => { + return HttpResponse.json({ + voices: [ + { + voice_id: "shared1", + name: "Community Voice", + category: "shared", + }, + ], + }); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.LIST_VOICES as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler({ include_shared: true }, mockContext); + const result = JSON.parse(actual); + + expect(result.success).toBe(true); + expect(result.voices).toHaveLength(1); + expect(result.voices[0].category).toBe("shared"); + + server.close(); + }); + }); + }); + + describe("when API call fails", () => { + it("returns error message", async () => { + server.use( + http.get("https://api.elevenlabs.io/v1/voices", () => { + return HttpResponse.json({ detail: "Not Found" }, { status: 404 }); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.LIST_VOICES as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler({}, mockContext); + const result = JSON.parse(actual); + + expect(result.success).toBe(false); + expect(result.error).toContain("404"); + + server.close(); + }); + }); + }); + + describe(".CREATE_AGENT", () => { + describe("when required parameters are provided", () => { + describe("and API call succeeds", () => { + it("returns agent ID successfully", async () => { + server.use( + http.post("https://api.elevenlabs.io/v1/convai/agents/create", () => { + return HttpResponse.json({ + agent_id: "agent_123", + }); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.CREATE_AGENT as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler( + { + agent_prompt: "You are a helpful assistant", + first_message: "Hello! How can I help you?", + }, + mockContext + ); + const result = JSON.parse(actual); + + expect(result.success).toBe(true); + expect(result.agent_id).toBe("agent_123"); + expect(result.voice_id).toBe("EXAVITQu4vr4xnSDxMaL"); + + server.close(); + }); + }); + + describe("and optional parameters are provided", () => { + it("uses the provided optional parameters", async () => { + server.use( + http.post("https://api.elevenlabs.io/v1/convai/agents/create", () => { + return HttpResponse.json({ + agent_id: "agent_456", + }); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.CREATE_AGENT as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler( + { + name: "Test Agent", + agent_prompt: "You are a test assistant", + first_message: "Test greeting", + voice_id: "custom_voice", + language: "es", + }, + mockContext + ); + const result = JSON.parse(actual); + + expect(result.success).toBe(true); + expect(result.name).toBe("Test Agent"); + expect(result.voice_id).toBe("custom_voice"); + expect(result.language).toBe("es"); + + server.close(); + }); + }); + }); + + describe("when API call fails", () => { + it("returns error message for validation errors", async () => { + server.use( + http.post("https://api.elevenlabs.io/v1/convai/agents/create", () => { + return HttpResponse.json( + { + detail: [ + { + type: "missing", + loc: ["body", "conversation_config", "agent", "prompt"], + msg: "Field required", + }, + ], + }, + { status: 422 } + ); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.CREATE_AGENT as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler( + { + agent_prompt: "", + first_message: "Hello", + }, + mockContext + ); + const result = JSON.parse(actual); + + expect(result.success).toBe(false); + expect(result.error).toContain("422"); + + server.close(); + }); + }); + }); + + describe(".LIST_PHONE_NUMBERS", () => { + describe("when phone numbers exist", () => { + describe("and API returns standard format", () => { + it("returns phone numbers successfully", async () => { + server.use( + http.get("https://api.elevenlabs.io/v1/convai/phone-numbers", () => { + return HttpResponse.json({ + phone_numbers: [ + { + phone_number_id: "phnum_123", + phone_number: "+1234567890", + name: "Main Line", + status: "active", + provider: "twilio", + country_code: "US", + }, + ], + }); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.LIST_PHONE_NUMBERS as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler({}, mockContext); + const result = JSON.parse(actual); + + expect(result.success).toBe(true); + expect(result.phone_numbers).toHaveLength(1); + expect(result.phone_numbers[0].phone_number_id).toBe("phnum_123"); + expect(result.count).toBe(1); + + server.close(); + }); + }); + + describe("and API returns array directly", () => { + it("handles array response format", async () => { + server.use( + http.get("https://api.elevenlabs.io/v1/convai/phone-numbers", () => { + return HttpResponse.json([ + { + id: "phnum_456", + number: "+9876543210", + name: "Support Line", + }, + ]); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.LIST_PHONE_NUMBERS as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler({}, mockContext); + const result = JSON.parse(actual); + + expect(result.success).toBe(true); + expect(result.phone_numbers).toHaveLength(1); + expect(result.phone_numbers[0].phone_number_id).toBe("phnum_456"); + expect(result.phone_numbers[0].phone_number).toBe("+9876543210"); + + server.close(); + }); + }); + + describe("and no phone numbers exist", () => { + it("returns empty list with helpful message", async () => { + server.use( + http.get("https://api.elevenlabs.io/v1/convai/phone-numbers", () => { + return HttpResponse.json({ phone_numbers: [] }); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.LIST_PHONE_NUMBERS as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler({}, mockContext); + const result = JSON.parse(actual); + + expect(result.success).toBe(true); + expect(result.phone_numbers).toHaveLength(0); + expect(result.message).toContain("No phone numbers found"); + + server.close(); + }); + }); + }); + }); + + describe(".MAKE_PHONE_CALL", () => { + describe("when all required parameters are provided", () => { + describe("and call is successful", () => { + it("returns call details successfully", async () => { + server.use( + http.post("https://api.elevenlabs.io/v1/convai/twilio/outbound-call", () => { + return HttpResponse.json({ + success: true, + message: "Call initiated", + conversation_id: "conv_123", + callSid: "CA123456789", + }); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.MAKE_PHONE_CALL as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler( + { + agent_id: "agent_123", + from_phone_number_id: "phnum_123", + to_number: "+1234567890", + }, + mockContext + ); + const result = JSON.parse(actual); + + expect(result.success).toBe(true); + expect(result.conversation_id).toBe("conv_123"); + expect(result.callSid).toBe("CA123456789"); + expect(result.agent_id).toBe("agent_123"); + expect(result.to_number).toBe("+1234567890"); + + server.close(); + }); + }); + + describe("and optional message is provided", () => { + it("includes the additional context", async () => { + server.use( + http.post("https://api.elevenlabs.io/v1/convai/twilio/outbound-call", (req) => { + // Verify the request body includes additional_context + return HttpResponse.json({ + success: true, + message: "Call initiated with context", + conversation_id: "conv_456", + callSid: "CA987654321", + }); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.MAKE_PHONE_CALL as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler( + { + agent_id: "agent_123", + from_phone_number_id: "phnum_123", + to_number: "+1234567890", + message: "This is a test call", + }, + mockContext + ); + const result = JSON.parse(actual); + + expect(result.success).toBe(true); + expect(result.conversation_id).toBe("conv_456"); + + server.close(); + }); + }); + }); + + describe("when API call fails", () => { + describe("and agent is not found", () => { + it("returns error message", async () => { + server.use( + http.post("https://api.elevenlabs.io/v1/convai/twilio/outbound-call", () => { + return HttpResponse.json({ detail: "Agent not found" }, { status: 404 }); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.MAKE_PHONE_CALL as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler( + { + agent_id: "invalid_agent", + from_phone_number_id: "phnum_123", + to_number: "+1234567890", + }, + mockContext + ); + const result = JSON.parse(actual); + + expect(result.success).toBe(false); + expect(result.error).toContain("404"); + + server.close(); + }); + }); + + describe("and phone number is invalid", () => { + it("returns validation error", async () => { + server.use( + http.post("https://api.elevenlabs.io/v1/convai/twilio/outbound-call", () => { + return HttpResponse.json( + { + detail: [ + { + type: "value_error", + loc: ["body", "to_number"], + msg: "Invalid phone number format", + }, + ], + }, + { status: 422 } + ); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.MAKE_PHONE_CALL as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler( + { + agent_id: "agent_123", + from_phone_number_id: "phnum_123", + to_number: "invalid_number", + }, + mockContext + ); + const result = JSON.parse(actual); + + expect(result.success).toBe(false); + expect(result.error).toContain("422"); + + server.close(); + }); + }); + }); + }); + + describe(".GET_USER_INFO", () => { + describe("when API call succeeds", () => { + it("returns user information successfully", async () => { + server.use( + http.get("https://api.elevenlabs.io/v1/user", () => { + return HttpResponse.json({ + user_id: "user_123", + subscription: { tier: "pro" }, + available_characters: 10000, + used_characters: 2500, + can_extend_character_limit: false, + can_use_instant_voice_cloning: true, + can_use_professional_voice_cloning: true, + api_tier: "pro", + }); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.GET_USER_INFO as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler({}, mockContext); + const result = JSON.parse(actual); + + expect(result.success).toBe(true); + expect(result.user.user_id).toBe("user_123"); + expect(result.user.available_characters).toBe(10000); + expect(result.user.api_tier).toBe("pro"); + + server.close(); + }); + }); + }); + + describe(".SPEECH_TO_TEXT", () => { + describe("when audio data is provided", () => { + describe("and base64 audio is provided", () => { + it("returns transcription successfully", async () => { + server.use( + http.post("https://api.elevenlabs.io/v1/speech-to-text", () => { + return HttpResponse.json({ + text: "Hello world", + language: "en", + duration: 2.5, + }); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.SPEECH_TO_TEXT as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + // Mock base64 audio data + const mockAudioBase64 = btoa("mock audio data"); + + const actual = await tool.handler( + { + audio_base64: mockAudioBase64, + }, + mockContext + ); + const result = JSON.parse(actual); + + expect(result.success).toBe(true); + expect(result.transcript).toBe("Hello world"); + expect(result.language).toBe("en"); + + server.close(); + }); + }); + + describe("and no audio data is provided", () => { + it("returns error message", async () => { + const tool = ElevenLabsConnectorConfig.tools.SPEECH_TO_TEXT as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler({}, mockContext); + const result = JSON.parse(actual); + + expect(result.success).toBe(false); + expect(result.error).toContain("audio_base64 or audio_url must be provided"); + }); + }); + }); + }); + + describe(".GENERATE_SOUND_EFFECTS", () => { + describe("when text description is provided", () => { + it("returns generated sound effect successfully", async () => { + server.use( + http.post("https://api.elevenlabs.io/v1/sound-generation", () => { + const mockAudio = new Uint8Array([10, 20, 30, 40, 50]); + return new HttpResponse(mockAudio, { + status: 200, + headers: { "Content-Type": "audio/mpeg" }, + }); + }) + ); + server.listen(); + + const tool = ElevenLabsConnectorConfig.tools.GENERATE_SOUND_EFFECTS as MCPToolDefinition; + const mockContext = createMockConnectorContext(); + + const actual = await tool.handler( + { + text: "Sound of rain falling", + duration_seconds: 5, + }, + mockContext + ); + const result = JSON.parse(actual); + + expect(result.success).toBe(true); + expect(result.audio_base64).toBeDefined(); + expect(result.description).toBe("Sound of rain falling"); + + server.close(); + }); + }); + }); +}); diff --git a/packages/mcp-connectors/src/connectors/elevenlabs.ts b/packages/mcp-connectors/src/connectors/elevenlabs.ts index b952f7a2..6045d290 100644 --- a/packages/mcp-connectors/src/connectors/elevenlabs.ts +++ b/packages/mcp-connectors/src/connectors/elevenlabs.ts @@ -48,6 +48,57 @@ interface ElevenLabsTranscriptionResult { speakers?: unknown[]; } +// Conversational AI interfaces +interface ConversationConfig { + agent?: { + prompt?: { + prompt?: string; + }; + first_message?: string; + language?: string; + }; + asr?: { + quality?: string; + provider?: string; + }; + tts?: { + model_id?: string; + voice_id?: string; + }; + transcriber?: { + model?: string; + language?: string; + }; + llm?: { + model?: string; + provider?: string; + }; +} + +interface AgentCreateResponse { + agent_id: string; +} + +interface PhoneCallResponse { + success: boolean; + message: string; + conversation_id?: string; + callSid?: string; +} + +interface PhoneNumber { + phone_number_id: string; + phone_number: string; + name?: string; + status: string; + provider: string; + country_code: string; +} + +interface PhoneNumbersResponse { + phone_numbers: PhoneNumber[]; +} + // Helper function to make API calls to ElevenLabs const makeElevenLabsRequest = async ( endpoint: string, @@ -106,7 +157,7 @@ export const ElevenLabsConnectorConfig = mcpConnectorConfig({ }), setup: z.object({}), examplePrompt: - 'Generate speech from the text "Hello, welcome to our AI-powered assistant!" using Rachel\'s voice, then list all available voices for future use.', + 'First list available phone numbers to see what Twilio numbers are configured, then create a conversational AI agent that introduces itself as a customer service representative, and finally use it to make a phone call to +1234567890 with a brief greeting message.', tools: (tool) => ({ TEXT_TO_SPEECH: tool({ name: 'text-to-speech', @@ -505,5 +556,239 @@ export const ElevenLabsConnectorConfig = mcpConnectorConfig({ } }, }), + + CREATE_AGENT: tool({ + name: 'create-agent', + description: + 'Create a conversational AI agent for voice interactions and phone calls', + schema: z.object({ + name: z + .string() + .optional() + .describe('Name for the agent to make it easier to find'), + agent_prompt: z + .string() + .describe("The system prompt that guides the agent's behavior and personality"), + first_message: z + .string() + .describe( + 'The first message the agent will speak when starting a conversation' + ), + voice_id: z + .string() + .optional() + .describe('Voice ID to use for the agent (default: Rachel)'), + language: z + .string() + .optional() + .describe('Language code for the agent (default: en)'), + model_id: z + .string() + .optional() + .describe('TTS model ID (default: eleven_turbo_v2)'), + + asr_quality: z + .enum(['low', 'medium', 'high']) + .optional() + .describe('ASR quality setting'), + llm_model: z.string().optional().describe('LLM model to use (default: gpt-4)'), + tags: z + .array(z.string()) + .optional() + .describe('Tags to help classify and filter the agent'), + }), + handler: async (args, context) => { + try { + const { apiKey } = await context.getCredentials(); + + const conversationConfig: ConversationConfig = { + agent: { + prompt: { + prompt: args.agent_prompt, + }, + first_message: args.first_message, + language: args.language || 'en', + }, + asr: { + quality: args.asr_quality || 'high', + provider: 'elevenlabs', + }, + tts: { + model_id: args.model_id || 'eleven_turbo_v2', + voice_id: args.voice_id || 'EXAVITQu4vr4xnSDxMaL', // Rachel voice + }, + llm: { + model: args.llm_model || 'gpt-4', + provider: 'openai', + }, + }; + + const requestBody = { + conversation_config: conversationConfig, + name: args.name || null, + tags: args.tags || null, + }; + + const response = await makeElevenLabsRequest('/convai/agents/create', apiKey, { + method: 'POST', + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`ElevenLabs API error: ${response.status} - ${errorText}`); + } + + const result = (await response.json()) as AgentCreateResponse; + + return JSON.stringify({ + success: true, + agent_id: result.agent_id, + name: args.name || 'Unnamed Agent', + voice_id: args.voice_id || 'EXAVITQu4vr4xnSDxMaL', + language: args.language || 'en', + message: + 'Conversational AI agent created successfully. Use this agent_id to make phone calls.', + }); + } catch (error) { + console.error('Create agent error:', error); + return JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }); + } + }, + }), + + LIST_PHONE_NUMBERS: tool({ + name: 'list-phone-numbers', + description: + 'List all phone numbers (Twilio numbers) available for making outbound calls with conversational AI agents', + schema: z.object({}), + handler: async (_args, context) => { + try { + const { apiKey } = await context.getCredentials(); + + const response = await makeElevenLabsRequest('/convai/phone-numbers', apiKey); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`ElevenLabs API error: ${response.status} - ${errorText}`); + } + + const result = await response.json(); + + // Handle different possible response structures + let phoneNumbers = []; + if (result.phone_numbers && Array.isArray(result.phone_numbers)) { + phoneNumbers = result.phone_numbers; + } else if (Array.isArray(result)) { + phoneNumbers = result; + } else { + // Log the actual response structure for debugging + console.log('Unexpected API response structure:', JSON.stringify(result, null, 2)); + } + + return JSON.stringify({ + success: true, + phone_numbers: phoneNumbers.map((phone: any) => ({ + phone_number_id: phone.phone_number_id || phone.id, + phone_number: phone.phone_number || phone.number, + name: phone.name || 'Unnamed', + status: phone.status || 'unknown', + provider: phone.provider || 'unknown', + country_code: phone.country_code || phone.country, + })), + count: phoneNumbers.length, + raw_response: result, + message: + phoneNumbers.length > 0 + ? 'Use the phone_number_id from this list as the from_phone_number_id parameter when making phone calls.' + : 'No phone numbers found. You may need to add a phone number to your ElevenLabs account first.', + }); + } catch (error) { + console.error('List phone numbers error:', error); + return JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }); + } + }, + }), + + MAKE_PHONE_CALL: tool({ + name: 'make-phone-call', + description: + 'Initiate an outbound phone call using a conversational AI agent to deliver a message', + schema: z.object({ + agent_id: z + .string() + .describe('The ID of the conversational AI agent to use for the call'), + to_number: z + .string() + .describe( + 'The phone number to call (in international format, e.g., +1234567890)' + ), + message: z + .string() + .optional() + .describe( + 'Additional context or message for the agent (will be part of the conversation flow)' + ), + from_phone_number_id: z + .string() + .describe( + 'The ID of the phone number (Twilio number) to use for making the outbound call. You must first import your Twilio number into ElevenLabs via the dashboard.' + ), + }), + handler: async (args, context) => { + try { + const { apiKey } = await context.getCredentials(); + + const requestBody: Record = { + agent_id: args.agent_id, + agent_phone_number_id: args.from_phone_number_id, + to_number: args.to_number, + }; + + // Add any additional context message + if (args.message) { + requestBody.additional_context = args.message; + } + + const response = await makeElevenLabsRequest( + `/convai/twilio/outbound-call`, + apiKey, + { + method: 'POST', + body: JSON.stringify(requestBody), + } + ); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`ElevenLabs API error: ${response.status} - ${errorText}`); + } + + const result = (await response.json()) as PhoneCallResponse; + + return JSON.stringify({ + success: true, + conversation_id: result.conversation_id, + callSid: result.callSid, + agent_id: args.agent_id, + to_number: args.to_number, + from_phone_number_id: args.from_phone_number_id, + message: result.message || 'Phone call initiated successfully via Twilio.', + }); + } catch (error) { + console.error('Make phone call error:', error); + return JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }); + } + }, + }), }), }); diff --git a/scripts/start-server.ts b/scripts/start-server.ts new file mode 100644 index 00000000..45cab379 --- /dev/null +++ b/scripts/start-server.ts @@ -0,0 +1,364 @@ +import { randomUUID } from 'node:crypto'; +import { parseArgs } from 'node:util'; +import { StreamableHTTPTransport } from '@hono/mcp'; +import { serve } from '@hono/node-server'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ConnectorContext, MCPConnectorConfig } from '@stackone/mcp-config-types'; +import { Connectors } from '@stackone/mcp-connectors'; +import { Hono } from 'hono'; +import { logger } from 'hono/logger'; + +// Helper to format timestamps for logs +const getTimestamp = () => new Date().toISOString(); + +// Custom logger format +const customLogger = ( + message: string, + level: 'info' | 'error' | 'debug' | 'warn' = 'info' +) => { + const timestamp = getTimestamp(); + const prefix = { + info: '📘', + error: '❌', + debug: '🔍', + warn: '⚠️', + }[level]; + console.log(`[${timestamp}] ${prefix} ${message}`); +}; + +const getConnectorByKey = (connectorKey: string): MCPConnectorConfig | null => { + const connector = Connectors.find((c) => c.key === connectorKey) as MCPConnectorConfig; + return connector || null; +}; + +const createRuntimeConnectorContext = ( + credentials: Record = {}, + setup: Record = {} +): ConnectorContext => { + const dataStore = new Map(); + const cacheStore = new Map(); + + return { + getCredentials: async () => credentials, + getSetup: async () => setup, + getData: async (key?: string): Promise => { + if (key === undefined) { + return Object.fromEntries(dataStore) as T; + } + return (dataStore.get(key) as T) || null; + }, + setData: async ( + keyOrData: string | Record, + value?: unknown + ): Promise => { + if (typeof keyOrData === 'string') { + dataStore.set(keyOrData, value); + } else { + for (const [k, v] of Object.entries(keyOrData)) { + dataStore.set(k, v); + } + } + }, + readCache: async (key: string): Promise => { + return cacheStore.get(key) || null; + }, + writeCache: async (key: string, value: string): Promise => { + cacheStore.set(key, value); + }, + }; +}; + +const printUsage = () => { + console.log('🚀 MCP Connector Server'); + console.log(''); + console.log('Usage: bun start --connector [options]'); + console.log(''); + console.log('Options:'); + console.log(' --connector Connector key (required)'); + console.log(' --credentials JSON string with connector credentials'); + console.log(' --setup JSON string with connector setup configuration'); + console.log(' --port Port to run server on (default: 3000)'); + console.log(' --help Show this help message'); + console.log(''); + console.log(`Available connectors (${Connectors.length}):`); + const sortedConnectors = Connectors.map((c) => c.key).sort(); + console.log(sortedConnectors.join(', ')); + console.log(''); + console.log('Examples:'); + console.log(' bun start --connector test'); + console.log(' bun start --connector asana --credentials \'{"apiKey":"sk-xxx"}\''); + console.log( + ' bun start --connector github --credentials \'{"token":"ghp_xxx"}\' --setup \'{"org":"myorg"}\'' + ); +}; + +const startServer = async (): Promise<{ app: Hono; port: number }> => { + const app = new Hono(); + + // Add request logging middleware + app.use( + logger((str, ..._rest) => { + customLogger(`Request: ${str}`, 'info'); + }) + ); + const { values } = parseArgs({ + args: process.argv.slice(2), + options: { + connector: { + type: 'string', + short: 'c', + }, + credentials: { + type: 'string', + }, + setup: { + type: 'string', + }, + port: { + type: 'string', + default: '3000', + }, + help: { + type: 'boolean', + short: 'h', + }, + }, + strict: true, + allowPositionals: true, + }); + + if (values.help) { + printUsage(); + process.exit(0); + } + + const connectorKey = values.connector; + + if (!connectorKey) { + console.error('❌ Connector key is required'); + console.log(''); + printUsage(); + process.exit(1); + } + + const connectorConfig = getConnectorByKey(connectorKey); + + if (!connectorConfig) { + console.error(`❌ Connector "${connectorKey}" not found`); + console.log(''); + console.log(`Available connectors (${Connectors.length}):`); + console.log( + Connectors.map((c) => c.key) + .sort() + .join(', ') + ); + process.exit(1); + } + + let credentials = {}; + let setup = {}; + + if (values.credentials) { + try { + credentials = JSON.parse(values.credentials); + } catch (error) { + console.error( + '❌ Invalid credentials JSON:', + error instanceof Error ? error.message : String(error) + ); + process.exit(1); + } + } + + if (values.setup) { + try { + setup = JSON.parse(values.setup); + } catch (error) { + console.error( + '❌ Invalid setup JSON:', + error instanceof Error ? error.message : String(error) + ); + process.exit(1); + } + } + + const context = createRuntimeConnectorContext(credentials, setup); + + const server = new McpServer({ + name: `${connectorConfig.name} MCP Server (disco.dev)`, + version: connectorConfig.version, + }); + + for (const tool of Object.values(connectorConfig.tools)) { + server.tool(tool.name, tool.description, tool.schema.shape, async (args: unknown) => { + const startTime = Date.now(); + customLogger(`Tool invoked: ${tool.name}`, 'info'); + customLogger(`Tool args: ${JSON.stringify(args, null, 2)}`, 'debug'); + + try { + const result = await tool.handler(args, context); + const duration = Date.now() - startTime; + customLogger(`Tool completed: ${tool.name} (${duration}ms)`, 'info'); + { + let resultPreview: string; + if (typeof result === 'string') { + resultPreview = result.substring(0, 200) + (result.length > 200 ? '...' : ''); + } else if (result !== undefined && result !== null) { + const strResult = + typeof result === 'object' ? JSON.stringify(result) : String(result); + resultPreview = + strResult.substring(0, 200) + (strResult.length > 200 ? '...' : ''); + } else { + resultPreview = String(result); + } + customLogger(`Tool result preview: ${resultPreview}`, 'debug'); + } + + return { + content: [{ type: 'text' as const, text: String(result) }], + }; + } catch (error) { + const duration = Date.now() - startTime; + customLogger(`Tool failed: ${tool.name} (${duration}ms)`, 'error'); + customLogger( + `Error details: ${error instanceof Error ? error.stack : String(error)}`, + 'error' + ); + + return { + content: [ + { + type: 'text' as const, + text: `Error: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }); + } + + for (const resource of Object.values(connectorConfig.resources)) { + server.resource(resource.name, resource.uri, async (uri: URL) => { + const startTime = Date.now(); + customLogger(`Resource accessed: ${resource.name}`, 'info'); + customLogger(`Resource URI: ${uri.toString()}`, 'debug'); + + try { + const result = await resource.handler(context); + const duration = Date.now() - startTime; + customLogger(`Resource fetched: ${resource.name} (${duration}ms)`, 'info'); + if (typeof result === 'string' || Array.isArray(result)) { + customLogger(`Resource size: ${result.length} chars`, 'debug'); + } else { + customLogger(`Resource type: ${typeof result}`, 'debug'); + } + + return { + contents: [ + { + type: 'text' as const, + text: String(result), + uri: uri.toString(), + }, + ], + }; + } catch (error) { + const duration = Date.now() - startTime; + customLogger(`Resource failed: ${resource.name} (${duration}ms)`, 'error'); + customLogger( + `Error details: ${error instanceof Error ? error.stack : String(error)}`, + 'error' + ); + + return { + contents: [ + { + type: 'text' as const, + text: `Error: ${error instanceof Error ? error.message : String(error)}`, + uri: uri.toString(), + }, + ], + }; + } + }); + } + + const transport = new StreamableHTTPTransport(); + + let isConnected = false; + customLogger('Connecting MCP server to transport...', 'info'); + + const connectedToServer = server + .connect(transport) + .then(() => { + isConnected = true; + customLogger('MCP server connected successfully', 'info'); + }) + .catch((error) => { + customLogger( + `Failed to connect MCP server: ${error instanceof Error ? error.message : String(error)}`, + 'error' + ); + throw error; + }); + + app.all('/mcp', async (c) => { + const requestId = randomUUID(); + customLogger( + `MCP request received [${requestId}] - ${c.req.method} ${c.req.url}`, + 'info' + ); + + try { + if (!isConnected) { + customLogger(`Waiting for MCP connection [${requestId}]...`, 'debug'); + await connectedToServer; + } + + customLogger(`Processing MCP request [${requestId}]`, 'debug'); + const response = await transport.handleRequest(c); + customLogger(`MCP request completed [${requestId}]`, 'info'); + return response; + } catch (error) { + customLogger( + `MCP request failed [${requestId}]: ${error instanceof Error ? error.message : String(error)}`, + 'error' + ); + if (error instanceof Error) { + customLogger(`Stack trace: ${error.stack}`, 'error'); + } + throw error; + } + }); + + const port = Number.parseInt(values.port || '3000', 10); + + customLogger('Starting MCP Connector Server...', 'info'); + customLogger(`Connector: ${connectorConfig.name} (${connectorConfig.key})`, 'info'); + customLogger(`Version: ${connectorConfig.version}`, 'info'); + customLogger(`Tools: ${Object.keys(connectorConfig.tools).length}`, 'info'); + customLogger(`Resources: ${Object.keys(connectorConfig.resources).length}`, 'info'); + customLogger(`Port: ${port}`, 'info'); + + if (Object.keys(credentials).length > 0) { + customLogger(`Credentials: ${Object.keys(credentials).length} keys provided`, 'info'); + customLogger(`Credential keys: ${Object.keys(credentials).join(', ')}`, 'debug'); + } + + if (Object.keys(setup).length > 0) { + customLogger(`Setup: ${Object.keys(setup).length} config keys provided`, 'info'); + customLogger('Setup config detected (values redacted for security)', 'debug'); + } + + if (connectorConfig.examplePrompt) { + customLogger(`Example: ${connectorConfig.examplePrompt}`, 'info'); + } + + customLogger(`MCP endpoint: http://localhost:${port}/mcp`, 'info'); + customLogger('Server ready and listening for requests!', 'info'); + + return { app, port }; +}; + +const { app, port } = await startServer(); +serve({ fetch: app.fetch, port, hostname: 'localhost' }); From eb54daa0c1e8f5d29ff3d99aafd99cf79c225133 Mon Sep 17 00:00:00 2001 From: lchavasse Date: Sat, 30 Aug 2025 18:27:16 +0100 Subject: [PATCH 2/2] Added and phone call capability for 11 labs connector. --- .gitignore | 5 +- CLAUDE.md | 90 ------------------- .../src/connectors/elevenlabs.spec.ts | 77 ++++------------ .../src/connectors/elevenlabs.ts | 17 ++-- 4 files changed, 28 insertions(+), 161 deletions(-) delete mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index da3bf418..800cd4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,7 @@ bun-debug.log* .npmrc -logs/ \ No newline at end of file +logs/ + +CLAUDE.md +start.txt \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 46ff7415..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,90 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -This is a TypeScript monorepo for MCP (Model Context Protocol) connectors that powers disco.dev. It contains pre-built connectors for 35+ popular SaaS tools like GitHub, Slack, Notion, Jira, etc. - -## Development Commands - -### Install dependencies -```bash -bun install -``` - -### Start a connector server for testing -```bash -# No credentials needed (test connector) -bun start --connector test - -# With credentials for production connectors -bun start --connector asana --credentials '{"apiKey":"your-api-key"}' -bun start --connector github --credentials '{"token":"ghp_xxx"}' --setup '{"org":"myorg"}' -``` - -### Build and type checking -```bash -bun run build # Build all packages -bun run typecheck # Type check all packages -bun run test # Run all tests -``` - -### Code quality -```bash -bun run check # Run Biome linter and formatter -bun run check:fix # Auto-fix linting issues -``` - -## Architecture - -### Monorepo Structure -- **packages/mcp-connectors/**: Main connectors package with 35+ connector implementations -- **packages/mcp-config-types/**: Shared TypeScript types and Zod schemas for connector configurations -- **scripts/**: Development scripts for starting and testing connector servers -- **apps/testing-agent/**: Testing utilities - -### Connector Architecture -Each connector is defined using the `mcpConnectorConfig` factory function with: -- **Metadata**: name, key, version, logo, example prompt -- **Credentials schema**: Zod schema defining required API keys/tokens -- **Setup schema**: Zod schema for optional configuration -- **Tools**: Functions that can be called by MCP clients -- **Resources**: Static data that can be retrieved - -### Key Components -- **ConnectorContext**: Provides credential access, data persistence, and caching -- **MCPConnectorConfig**: Type-safe connector configuration with tools and resources -- **Transport layer**: HTTP transport using Hono framework for MCP protocol - -### Tools vs Resources -- **Tools**: Active functions that perform operations (API calls, data processing) -- **Resources**: Passive data retrieval (documentation, static content) - -### Adding New Connectors -1. Create new connector file in `packages/mcp-connectors/src/connectors/` -2. Follow the pattern of existing connectors (see `test.ts` for simple example) -3. Export the connector config and add to `packages/mcp-connectors/src/index.ts` -4. Update credentials/setup schemas in `packages/mcp-config-types/src/` if needed - -## Development Notes - -### Runtime Environment -- Uses **Bun** as package manager and runtime -- **Turbo** for monorepo orchestration -- **Biome** for linting and formatting (90 char line width, single quotes) -- **Vitest** for testing -- **TypeScript 5.9** with strict settings - -### Server Architecture -The development server (`scripts/start-server.ts`) creates: -- MCP server instance with connector tools and resources -- HTTP transport layer using Hono -- Request/response logging with timestamps -- Error handling and debugging output -- Support for credentials and setup configuration via CLI args - -### Key Files -- `packages/mcp-connectors/src/index.ts`: Main connector registry -- `packages/mcp-config-types/src/config.ts`: Connector configuration factory -- `scripts/start-server.ts`: Development server implementation \ No newline at end of file diff --git a/packages/mcp-connectors/src/connectors/elevenlabs.spec.ts b/packages/mcp-connectors/src/connectors/elevenlabs.spec.ts index 6b0de90a..b6ac5597 100644 --- a/packages/mcp-connectors/src/connectors/elevenlabs.spec.ts +++ b/packages/mcp-connectors/src/connectors/elevenlabs.spec.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, beforeAll, afterAll, afterEach } from "vitest"; import type { MCPToolDefinition } from "@stackone/mcp-config-types"; import { createMockConnectorContext } from "../__mocks__/context"; import { ElevenLabsConnectorConfig } from "./elevenlabs"; @@ -8,6 +8,17 @@ import { setupServer } from "msw/node"; const server = setupServer(); describe("#ElevenLabsConnector", () => { + beforeAll(() => { + server.listen(); + }); + + afterEach(() => { + server.resetHandlers(); + }); + + afterAll(() => { + server.close(); + }); describe(".TEXT_TO_SPEECH", () => { describe("when text is provided", () => { describe("and API key is valid", () => { @@ -22,7 +33,6 @@ describe("#ElevenLabsConnector", () => { }); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.TEXT_TO_SPEECH as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -33,8 +43,6 @@ describe("#ElevenLabsConnector", () => { expect(result.success).toBe(true); expect(result.audio_base64).toBeDefined(); expect(result.format).toBe("mp3_44100_128"); - - server.close(); }); }); @@ -48,7 +56,6 @@ describe("#ElevenLabsConnector", () => { ); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.TEXT_TO_SPEECH as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -58,8 +65,6 @@ describe("#ElevenLabsConnector", () => { expect(result.success).toBe(false); expect(result.error).toContain("401"); - - server.close(); }); }); @@ -70,7 +75,6 @@ describe("#ElevenLabsConnector", () => { return new HttpResponse(null, { status: 200 }); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.TEXT_TO_SPEECH as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -80,8 +84,6 @@ describe("#ElevenLabsConnector", () => { expect(result.success).toBe(false); expect(result.error).toContain("No audio data received"); - - server.close(); }); }); }); @@ -106,7 +108,6 @@ describe("#ElevenLabsConnector", () => { }); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.LIST_VOICES as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -118,8 +119,6 @@ describe("#ElevenLabsConnector", () => { expect(result.voices).toHaveLength(1); expect(result.voices[0].voice_id).toBe("voice1"); expect(result.voices[0].name).toBe("Rachel"); - - server.close(); }); }); @@ -141,7 +140,6 @@ describe("#ElevenLabsConnector", () => { }); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.LIST_VOICES as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -152,8 +150,6 @@ describe("#ElevenLabsConnector", () => { expect(result.success).toBe(true); expect(result.voices).toHaveLength(1); expect(result.voices[0].category).toBe("shared"); - - server.close(); }); }); }); @@ -165,7 +161,6 @@ describe("#ElevenLabsConnector", () => { return HttpResponse.json({ detail: "Not Found" }, { status: 404 }); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.LIST_VOICES as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -175,8 +170,6 @@ describe("#ElevenLabsConnector", () => { expect(result.success).toBe(false); expect(result.error).toContain("404"); - - server.close(); }); }); }); @@ -192,7 +185,6 @@ describe("#ElevenLabsConnector", () => { }); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.CREATE_AGENT as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -209,8 +201,6 @@ describe("#ElevenLabsConnector", () => { expect(result.success).toBe(true); expect(result.agent_id).toBe("agent_123"); expect(result.voice_id).toBe("EXAVITQu4vr4xnSDxMaL"); - - server.close(); }); }); @@ -223,7 +213,6 @@ describe("#ElevenLabsConnector", () => { }); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.CREATE_AGENT as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -244,8 +233,6 @@ describe("#ElevenLabsConnector", () => { expect(result.name).toBe("Test Agent"); expect(result.voice_id).toBe("custom_voice"); expect(result.language).toBe("es"); - - server.close(); }); }); }); @@ -268,7 +255,6 @@ describe("#ElevenLabsConnector", () => { ); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.CREATE_AGENT as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -284,8 +270,6 @@ describe("#ElevenLabsConnector", () => { expect(result.success).toBe(false); expect(result.error).toContain("422"); - - server.close(); }); }); }); @@ -310,7 +294,6 @@ describe("#ElevenLabsConnector", () => { }); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.LIST_PHONE_NUMBERS as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -322,8 +305,6 @@ describe("#ElevenLabsConnector", () => { expect(result.phone_numbers).toHaveLength(1); expect(result.phone_numbers[0].phone_number_id).toBe("phnum_123"); expect(result.count).toBe(1); - - server.close(); }); }); @@ -340,7 +321,6 @@ describe("#ElevenLabsConnector", () => { ]); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.LIST_PHONE_NUMBERS as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -352,8 +332,6 @@ describe("#ElevenLabsConnector", () => { expect(result.phone_numbers).toHaveLength(1); expect(result.phone_numbers[0].phone_number_id).toBe("phnum_456"); expect(result.phone_numbers[0].phone_number).toBe("+9876543210"); - - server.close(); }); }); @@ -364,7 +342,6 @@ describe("#ElevenLabsConnector", () => { return HttpResponse.json({ phone_numbers: [] }); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.LIST_PHONE_NUMBERS as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -375,8 +352,6 @@ describe("#ElevenLabsConnector", () => { expect(result.success).toBe(true); expect(result.phone_numbers).toHaveLength(0); expect(result.message).toContain("No phone numbers found"); - - server.close(); }); }); }); @@ -396,7 +371,6 @@ describe("#ElevenLabsConnector", () => { }); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.MAKE_PHONE_CALL as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -416,16 +390,16 @@ describe("#ElevenLabsConnector", () => { expect(result.callSid).toBe("CA123456789"); expect(result.agent_id).toBe("agent_123"); expect(result.to_number).toBe("+1234567890"); - - server.close(); }); }); describe("and optional message is provided", () => { it("includes the additional context", async () => { + let requestBody: any; server.use( - http.post("https://api.elevenlabs.io/v1/convai/twilio/outbound-call", (req) => { - // Verify the request body includes additional_context + http.post("https://api.elevenlabs.io/v1/convai/twilio/outbound-call", async (req) => { + // Capture and verify the request body includes additional_context + requestBody = await req.request.json(); return HttpResponse.json({ success: true, message: "Call initiated with context", @@ -434,7 +408,6 @@ describe("#ElevenLabsConnector", () => { }); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.MAKE_PHONE_CALL as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -452,8 +425,7 @@ describe("#ElevenLabsConnector", () => { expect(result.success).toBe(true); expect(result.conversation_id).toBe("conv_456"); - - server.close(); + expect(requestBody.additional_context).toBe("This is a test call"); }); }); }); @@ -466,7 +438,6 @@ describe("#ElevenLabsConnector", () => { return HttpResponse.json({ detail: "Agent not found" }, { status: 404 }); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.MAKE_PHONE_CALL as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -483,8 +454,6 @@ describe("#ElevenLabsConnector", () => { expect(result.success).toBe(false); expect(result.error).toContain("404"); - - server.close(); }); }); @@ -506,7 +475,6 @@ describe("#ElevenLabsConnector", () => { ); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.MAKE_PHONE_CALL as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -523,8 +491,6 @@ describe("#ElevenLabsConnector", () => { expect(result.success).toBe(false); expect(result.error).toContain("422"); - - server.close(); }); }); }); @@ -547,7 +513,6 @@ describe("#ElevenLabsConnector", () => { }); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.GET_USER_INFO as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -559,8 +524,6 @@ describe("#ElevenLabsConnector", () => { expect(result.user.user_id).toBe("user_123"); expect(result.user.available_characters).toBe(10000); expect(result.user.api_tier).toBe("pro"); - - server.close(); }); }); }); @@ -578,7 +541,6 @@ describe("#ElevenLabsConnector", () => { }); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.SPEECH_TO_TEXT as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -597,8 +559,6 @@ describe("#ElevenLabsConnector", () => { expect(result.success).toBe(true); expect(result.transcript).toBe("Hello world"); expect(result.language).toBe("en"); - - server.close(); }); }); @@ -629,7 +589,6 @@ describe("#ElevenLabsConnector", () => { }); }) ); - server.listen(); const tool = ElevenLabsConnectorConfig.tools.GENERATE_SOUND_EFFECTS as MCPToolDefinition; const mockContext = createMockConnectorContext(); @@ -646,8 +605,6 @@ describe("#ElevenLabsConnector", () => { expect(result.success).toBe(true); expect(result.audio_base64).toBeDefined(); expect(result.description).toBe("Sound of rain falling"); - - server.close(); }); }); }); diff --git a/packages/mcp-connectors/src/connectors/elevenlabs.ts b/packages/mcp-connectors/src/connectors/elevenlabs.ts index 6045d290..cbe75c7d 100644 --- a/packages/mcp-connectors/src/connectors/elevenlabs.ts +++ b/packages/mcp-connectors/src/connectors/elevenlabs.ts @@ -95,9 +95,7 @@ interface PhoneNumber { country_code: string; } -interface PhoneNumbersResponse { - phone_numbers: PhoneNumber[]; -} + // Helper function to make API calls to ElevenLabs const makeElevenLabsRequest = async ( @@ -645,7 +643,7 @@ export const ElevenLabsConnectorConfig = mcpConnectorConfig({ success: true, agent_id: result.agent_id, name: args.name || 'Unnamed Agent', - voice_id: args.voice_id || 'EXAVITQu4vr4xnSDxMaL', + voice_id: args.voice_id || 'jqcCZkN6Knx8BJ5TBdYR', language: args.language || 'en', message: 'Conversational AI agent created successfully. Use this agent_id to make phone calls.', @@ -676,22 +674,22 @@ export const ElevenLabsConnectorConfig = mcpConnectorConfig({ throw new Error(`ElevenLabs API error: ${response.status} - ${errorText}`); } - const result = await response.json(); + const result = await response.json() as any; // Handle different possible response structures - let phoneNumbers = []; + let phoneNumbers: any[] = []; if (result.phone_numbers && Array.isArray(result.phone_numbers)) { phoneNumbers = result.phone_numbers; } else if (Array.isArray(result)) { phoneNumbers = result; } else { - // Log the actual response structure for debugging - console.log('Unexpected API response structure:', JSON.stringify(result, null, 2)); + // Log that the response structure was unexpected without revealing sensitive data + console.log('Unexpected API response structure received from phone numbers endpoint'); } return JSON.stringify({ success: true, - phone_numbers: phoneNumbers.map((phone: any) => ({ + phone_numbers: phoneNumbers.map((phone: any): PhoneNumber => ({ phone_number_id: phone.phone_number_id || phone.id, phone_number: phone.phone_number || phone.number, name: phone.name || 'Unnamed', @@ -700,7 +698,6 @@ export const ElevenLabsConnectorConfig = mcpConnectorConfig({ country_code: phone.country_code || phone.country, })), count: phoneNumbers.length, - raw_response: result, message: phoneNumbers.length > 0 ? 'Use the phone_number_id from this list as the from_phone_number_id parameter when making phone calls.'