This commit is contained in:
Roo Code 2025-06-30 09:57:57 +00:00
parent 3a8ba27615
commit 564df5f40d
4 changed files with 426 additions and 20 deletions

View file

@ -0,0 +1,97 @@
# Marketplace Data
This directory contains marketplace data files for Roo Code's marketplace system.
## Structure
- `mcps.yaml` - MCP (Model Context Protocol) servers available in the marketplace
- `modes.yaml` - Custom modes available in the marketplace (to be added)
## MCP Servers
The `mcps.yaml` file contains a list of MCP servers that can be installed through the Roo Code marketplace. Each MCP server entry includes:
- `id` - Unique identifier for the MCP server
- `name` - Display name
- `description` - Detailed description of functionality
- `author` - Author name
- `authorUrl` - Author's website or GitHub profile
- `url` - Repository or documentation URL
- `tags` - Array of tags for categorization
- `prerequisites` - Array of requirements needed before installation
- `content` - Installation methods (can be a single string or array of methods)
### Installation Methods
Each installation method can include:
- `name` - Method name (e.g., "uvx", "pip install")
- `content` - JSON configuration to be added to MCP settings
- `parameters` - Optional parameters that users can customize
- `prerequisites` - Method-specific prerequisites
### Parameters
Parameters allow users to customize the installation:
- `name` - Display name for the parameter
- `key` - Key used in the configuration template
- `placeholder` - Default value or example
- `optional` - Whether the parameter is optional (default: false)
## VoiceVox MCP Server
The VoiceVox MCP Server provides Japanese text-to-speech capabilities using the VoiceVox engine. It includes:
### Features
- Text-to-Speech conversion for Japanese text
- Multiple voice characters with different personalities
- Customizable speech speed and voice selection
- Automatic audio playback after generation
- WAV format audio file management
### Available Tools
1. `get_voices` - Retrieve list of available voices from VoiceVox
2. `text_to_speech` - Convert text to speech with customizable settings
### Use Cases
- Educational content creation with Japanese narration
- Accessibility improvements for Japanese content
- Podcast and video production
- Language learning applications
- Chatbot voice functionality
### Installation Options
1. **uvx (Recommended)** - Uses uvx to run the server directly
2. **pip install** - Install via pip and run with Python
3. **Custom Configuration** - Full customization with all available parameters
### Prerequisites
- VoiceVox Engine running (default: http://localhost:50021)
- Python 3.10+
- For uvx method: `pip install uvx`
- For pip method: `pip install mcp-server-voicevox`
## Usage
This marketplace data is used by the Roo Code extension to populate the marketplace UI. Users can browse, search, and install MCP servers directly from the extension.
## Contributing
To add a new MCP server to the marketplace:
1. Add an entry to `mcps.yaml` following the schema
2. Ensure all required fields are provided
3. Test the installation methods
4. Submit a pull request
## References
- [VoiceVox MCP Server Repository](https://github.com/Sunwood-ai-labs/mcp-voicevox)
- [VoiceVox MCP Server PyPI Package](https://pypi.org/project/mcp-server-voicevox/)
- [Model Context Protocol Documentation](https://docs.roocode.com/advanced-usage/mcp)

View file

@ -0,0 +1,74 @@
items:
- id: "voicevox-tts"
name: "VoiceVox Text-to-Speech Server"
description: "Japanese Text-to-Speech integration using VoiceVox engine with multiple voice characters and customizable playback settings"
author: "Sunwood AI Labs"
authorUrl: "https://github.com/Sunwood-ai-labs"
url: "https://github.com/Sunwood-ai-labs/mcp-voicevox"
tags:
- "text-to-speech"
- "japanese"
- "audio"
- "voice"
- "accessibility"
- "content-creation"
prerequisites:
- "VoiceVox Engine running (locally or remotely)"
- "Python 3.10+"
content:
- name: "uvx (Recommended)"
content: |
{
"voicevox": {
"command": "uvx",
"args": ["mcp-server-voicevox", "--voicevox-url=http://localhost:50021"]
}
}
parameters:
- name: "VoiceVox URL"
key: "voicevox-url"
placeholder: "http://localhost:50021"
optional: true
prerequisites:
- "uvx installed (pip install uvx)"
- "VoiceVox Engine running on specified URL"
- name: "pip install"
content: |
{
"voicevox": {
"command": "python",
"args": ["-m", "mcp_server_voicevox", "--voicevox-url=http://localhost:50021"]
}
}
parameters:
- name: "VoiceVox URL"
key: "voicevox-url"
placeholder: "http://localhost:50021"
optional: true
prerequisites:
- "pip install mcp-server-voicevox"
- "VoiceVox Engine running on specified URL"
- name: "Custom Configuration"
content: |
{
"voicevox": {
"command": "uvx",
"args": ["mcp-server-voicevox", "--voicevox-url={{voicevox-url}}", "--default-speaker={{default-speaker}}", "--speed={{speed}}"]
}
}
parameters:
- name: "VoiceVox URL"
key: "voicevox-url"
placeholder: "http://localhost:50021"
optional: false
- name: "Default Speaker ID"
key: "default-speaker"
placeholder: "1"
optional: true
- name: "Speech Speed"
key: "speed"
placeholder: "1.0"
optional: true
prerequisites:
- "uvx installed (pip install uvx)"
- "VoiceVox Engine running on specified URL"

View file

@ -1,5 +1,7 @@
import axios from "axios"
import * as yaml from "yaml"
import * as fs from "fs/promises"
import * as path from "path"
import { z } from "zod"
import { getRooCodeApiUrl } from "@roo-code/cloud"
import type { MarketplaceItem, MarketplaceItemType } from "@roo-code/types"
@ -37,19 +39,24 @@ export class RemoteConfigLoader {
const cached = this.getFromCache(cacheKey)
if (cached) return cached
const data = await this.fetchWithRetry<string>(`${this.apiBaseUrl}/api/marketplace/modes`)
try {
const data = await this.fetchWithRetry<string>(`${this.apiBaseUrl}/api/marketplace/modes`)
// Parse and validate YAML response
const yamlData = yaml.parse(data)
const validated = modeMarketplaceResponse.parse(yamlData)
// Parse and validate YAML response
const yamlData = yaml.parse(data)
const validated = modeMarketplaceResponse.parse(yamlData)
const items: MarketplaceItem[] = validated.items.map((item) => ({
type: "mode" as const,
...item,
}))
const items: MarketplaceItem[] = validated.items.map((item) => ({
type: "mode" as const,
...item,
}))
this.setCache(cacheKey, items)
return items
this.setCache(cacheKey, items)
return items
} catch (error) {
console.warn("Failed to fetch modes from remote API, trying local fallback:", error)
return this.fetchLocalModes()
}
}
private async fetchMcps(): Promise<MarketplaceItem[]> {
@ -57,19 +64,24 @@ export class RemoteConfigLoader {
const cached = this.getFromCache(cacheKey)
if (cached) return cached
const data = await this.fetchWithRetry<string>(`${this.apiBaseUrl}/api/marketplace/mcps`)
try {
const data = await this.fetchWithRetry<string>(`${this.apiBaseUrl}/api/marketplace/mcps`)
// Parse and validate YAML response
const yamlData = yaml.parse(data)
const validated = mcpMarketplaceResponse.parse(yamlData)
// Parse and validate YAML response
const yamlData = yaml.parse(data)
const validated = mcpMarketplaceResponse.parse(yamlData)
const items: MarketplaceItem[] = validated.items.map((item) => ({
type: "mcp" as const,
...item,
}))
const items: MarketplaceItem[] = validated.items.map((item) => ({
type: "mcp" as const,
...item,
}))
this.setCache(cacheKey, items)
return items
this.setCache(cacheKey, items)
return items
} catch (error) {
console.warn("Failed to fetch MCPs from remote API, trying local fallback:", error)
return this.fetchLocalMcps()
}
}
private async fetchWithRetry<T>(url: string, maxRetries = 3): Promise<T> {
@ -126,4 +138,104 @@ export class RemoteConfigLoader {
clearCache(): void {
this.cache.clear()
}
/**
* Fallback method to load MCPs from local marketplace data file
*/
private async fetchLocalMcps(): Promise<MarketplaceItem[]> {
try {
// Try to load from local marketplace-data directory
// Look for marketplace-data in current directory, parent directories, or relative to __dirname
const possiblePaths = [
path.join(process.cwd(), "marketplace-data", "mcps.yaml"),
path.join(process.cwd(), "..", "marketplace-data", "mcps.yaml"),
path.join(process.cwd(), "..", "..", "marketplace-data", "mcps.yaml"),
path.join(process.cwd(), "..", "..", "..", "marketplace-data", "mcps.yaml"),
path.join(__dirname, "..", "..", "..", "marketplace-data", "mcps.yaml"),
]
let data: string | null = null
let usedPath: string | null = null
for (const localMcpPath of possiblePaths) {
try {
data = await fs.readFile(localMcpPath, "utf-8")
usedPath = localMcpPath
break
} catch (error) {
// Continue to next path
continue
}
}
if (!data) {
throw new Error("Could not find mcps.yaml in any expected location")
}
// Parse and validate YAML response
const yamlData = yaml.parse(data)
const validated = mcpMarketplaceResponse.parse(yamlData)
const items: MarketplaceItem[] = validated.items.map((item) => ({
type: "mcp" as const,
...item,
}))
console.log(`Loaded ${items.length} MCP items from local marketplace data at ${usedPath}`)
return items
} catch (error) {
console.warn("Failed to load local MCP data:", error)
return []
}
}
/**
* Fallback method to load modes from local marketplace data file
*/
private async fetchLocalModes(): Promise<MarketplaceItem[]> {
try {
// Try to load from local marketplace-data directory
// Look for marketplace-data in current directory, parent directories, or relative to __dirname
const possiblePaths = [
path.join(process.cwd(), "marketplace-data", "modes.yaml"),
path.join(process.cwd(), "..", "marketplace-data", "modes.yaml"),
path.join(process.cwd(), "..", "..", "marketplace-data", "modes.yaml"),
path.join(process.cwd(), "..", "..", "..", "marketplace-data", "modes.yaml"),
path.join(__dirname, "..", "..", "..", "marketplace-data", "modes.yaml"),
]
let data: string | null = null
let usedPath: string | null = null
for (const localModePath of possiblePaths) {
try {
data = await fs.readFile(localModePath, "utf-8")
usedPath = localModePath
break
} catch (error) {
// Continue to next path
continue
}
}
if (!data) {
throw new Error("Could not find modes.yaml in any expected location")
}
// Parse and validate YAML response
const yamlData = yaml.parse(data)
const validated = modeMarketplaceResponse.parse(yamlData)
const items: MarketplaceItem[] = validated.items.map((item) => ({
type: "mode" as const,
...item,
}))
console.log(`Loaded ${items.length} mode items from local marketplace data at ${usedPath}`)
return items
} catch (error) {
console.warn("Failed to load local mode data:", error)
return []
}
}
}

View file

@ -0,0 +1,123 @@
import { describe, test, expect, beforeEach, vi } from "vitest"
import nock from "nock"
import { RemoteConfigLoader } from "../RemoteConfigLoader"
import type { MarketplaceItem, McpMarketplaceItem } from "@roo-code/types"
describe("VoiceVox MCP Server", () => {
let configLoader: RemoteConfigLoader
beforeEach(() => {
// Mock all remote API calls to force fallback to local data
nock("https://app.roocode.com").get("/api/marketplace/modes").reply(500, "Service unavailable").persist()
nock("https://app.roocode.com").get("/api/marketplace/mcps").reply(500, "Service unavailable").persist()
configLoader = new RemoteConfigLoader()
})
test("should load VoiceVox MCP server from marketplace data", async () => {
const items = await configLoader.loadAllItems()
const voiceVoxItem = items.find((item) => item.id === "voicevox-tts")
expect(voiceVoxItem).toBeDefined()
expect(voiceVoxItem?.type).toBe("mcp")
expect(voiceVoxItem?.name).toBe("VoiceVox Text-to-Speech Server")
expect(voiceVoxItem?.author).toBe("Sunwood AI Labs")
// Type assertion for MCP item to access url property
const mcpItem = voiceVoxItem as McpMarketplaceItem & { type: "mcp" }
expect(mcpItem?.url).toBe("https://github.com/Sunwood-ai-labs/mcp-voicevox")
})
test("should have correct tags for VoiceVox MCP server", async () => {
const items = await configLoader.loadAllItems()
const voiceVoxItem = items.find((item) => item.id === "voicevox-tts")
expect(voiceVoxItem?.tags).toContain("text-to-speech")
expect(voiceVoxItem?.tags).toContain("japanese")
expect(voiceVoxItem?.tags).toContain("audio")
expect(voiceVoxItem?.tags).toContain("voice")
expect(voiceVoxItem?.tags).toContain("accessibility")
expect(voiceVoxItem?.tags).toContain("content-creation")
})
test("should have multiple installation methods", async () => {
const items = await configLoader.loadAllItems()
const voiceVoxItem = items.find((item) => item.id === "voicevox-tts")
expect(voiceVoxItem).toBeDefined()
// Type assertion for MCP item
const mcpItem = voiceVoxItem as McpMarketplaceItem & { type: "mcp" }
expect(Array.isArray(mcpItem.content)).toBe(true)
const content = mcpItem.content as any[]
expect(content).toHaveLength(3)
// Check installation method names
const methodNames = content.map((method) => method.name)
expect(methodNames).toContain("uvx (Recommended)")
expect(methodNames).toContain("pip install")
expect(methodNames).toContain("Custom Configuration")
})
test("should have correct prerequisites", async () => {
const items = await configLoader.loadAllItems()
const voiceVoxItem = items.find((item) => item.id === "voicevox-tts")
expect(voiceVoxItem?.prerequisites).toContain("VoiceVox Engine running (locally or remotely)")
expect(voiceVoxItem?.prerequisites).toContain("Python 3.10+")
})
test("should have parameters for custom configuration", async () => {
const items = await configLoader.loadAllItems()
const voiceVoxItem = items.find((item) => item.id === "voicevox-tts")
expect(voiceVoxItem).toBeDefined()
// Type assertion for MCP item
const mcpItem = voiceVoxItem as McpMarketplaceItem & { type: "mcp" }
const content = mcpItem.content as any[]
const customConfig = content.find((method) => method.name === "Custom Configuration")
expect(customConfig).toBeDefined()
expect(customConfig.parameters).toBeDefined()
expect(customConfig.parameters).toHaveLength(3)
const parameterKeys = customConfig.parameters.map((param: any) => param.key)
expect(parameterKeys).toContain("voicevox-url")
expect(parameterKeys).toContain("default-speaker")
expect(parameterKeys).toContain("speed")
})
test("should be retrievable by ID and type", async () => {
const voiceVoxItem = await configLoader.getItem("voicevox-tts", "mcp")
expect(voiceVoxItem).toBeDefined()
expect(voiceVoxItem?.id).toBe("voicevox-tts")
expect(voiceVoxItem?.type).toBe("mcp")
})
test("should have valid JSON configuration content", async () => {
const items = await configLoader.loadAllItems()
const voiceVoxItem = items.find((item) => item.id === "voicevox-tts")
expect(voiceVoxItem).toBeDefined()
// Type assertion for MCP item
const mcpItem = voiceVoxItem as McpMarketplaceItem & { type: "mcp" }
const content = mcpItem.content as any[]
for (const method of content) {
expect(() => {
JSON.parse(method.content)
}).not.toThrow()
const config = JSON.parse(method.content)
expect(config.voicevox).toBeDefined()
expect(config.voicevox.command).toBeDefined()
expect(config.voicevox.args).toBeDefined()
expect(Array.isArray(config.voicevox.args)).toBe(true)
}
})
})