mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
Fixes #5216
This commit is contained in:
parent
3a8ba27615
commit
564df5f40d
4 changed files with 426 additions and 20 deletions
97
marketplace-data/README.md
Normal file
97
marketplace-data/README.md
Normal 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)
|
||||
74
marketplace-data/mcps.yaml
Normal file
74
marketplace-data/mcps.yaml
Normal 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"
|
||||
|
|
@ -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 []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
123
src/services/marketplace/__tests__/voicevox-mcp.spec.ts
Normal file
123
src/services/marketplace/__tests__/voicevox-mcp.spec.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue