Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from "./experiment.js"
export * from "./global-settings.js"
export * from "./history.js"
export * from "./ipc.js"
export * from "./marketplace.js"
export * from "./mcp.js"
export * from "./message.js"
export * from "./mode.js"
Expand Down
88 changes: 88 additions & 0 deletions packages/types/src/marketplace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { z } from "zod"

/**
* Schema for MCP parameter definitions
*/
export const mcpParameterSchema = z.object({
name: z.string().min(1),
key: z.string().min(1),
placeholder: z.string().optional(),
optional: z.boolean().optional().default(false),
})

export type McpParameter = z.infer<typeof mcpParameterSchema>

/**
* Schema for MCP installation method with name
*/
export const mcpInstallationMethodSchema = z.object({
name: z.string().min(1),
content: z.string().min(1),
parameters: z.array(mcpParameterSchema).optional(),
prerequisites: z.array(z.string()).optional(),
})

export type McpInstallationMethod = z.infer<typeof mcpInstallationMethodSchema>

/**
* Component type validation
*/
export const marketplaceItemTypeSchema = z.enum(["mode", "mcp"] as const)

export type MarketplaceItemType = z.infer<typeof marketplaceItemTypeSchema>

/**
* Base schema for common marketplace item fields
*/
const baseMarketplaceItemSchema = z.object({
id: z.string().min(1),
name: z.string().min(1, "Name is required"),
description: z.string(),
author: z.string().optional(),
authorUrl: z.string().url("Author URL must be a valid URL").optional(),
tags: z.array(z.string()).optional(),
prerequisites: z.array(z.string()).optional(),
})

/**
* Type-specific schemas for YAML parsing (without type field, added programmatically)
*/
export const modeMarketplaceItemSchema = baseMarketplaceItemSchema.extend({
content: z.string().min(1), // YAML content for modes
})

export type ModeMarketplaceItem = z.infer<typeof modeMarketplaceItemSchema>

export const mcpMarketplaceItemSchema = baseMarketplaceItemSchema.extend({
url: z.string().url(), // Required url field
content: z.union([z.string().min(1), z.array(mcpInstallationMethodSchema)]), // Single config or array of methods
parameters: z.array(mcpParameterSchema).optional(),
})

export type McpMarketplaceItem = z.infer<typeof mcpMarketplaceItemSchema>

/**
* Unified marketplace item schema using discriminated union
*/
export const marketplaceItemSchema = z.discriminatedUnion("type", [
// Mode marketplace item
modeMarketplaceItemSchema.extend({
type: z.literal("mode"),
}),
// MCP marketplace item
mcpMarketplaceItemSchema.extend({
type: z.literal("mcp"),
}),
])

export type MarketplaceItem = z.infer<typeof marketplaceItemSchema>

/**
* Installation options for marketplace items
*/
export const installMarketplaceItemOptionsSchema = z.object({
target: z.enum(["global", "project"]).optional().default("project"),
parameters: z.record(z.string(), z.any()).optional(),
})

export type InstallMarketplaceItemOptions = z.infer<typeof installMarketplaceItemOptionsSchema>
2 changes: 1 addition & 1 deletion src/services/marketplace/MarketplaceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as path from "path"
import * as yaml from "yaml"
import { RemoteConfigLoader } from "./RemoteConfigLoader"
import { SimpleInstaller } from "./SimpleInstaller"
import { MarketplaceItem, MarketplaceItemType } from "./types"
import type { MarketplaceItem, MarketplaceItemType } from "@roo-code/types"
import { GlobalFileNames } from "../../shared/globalFileNames"
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
import { t } from "../../i18n"
Expand Down
12 changes: 6 additions & 6 deletions src/services/marketplace/RemoteConfigLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import axios from "axios"
import * as yaml from "yaml"
import { z } from "zod"
import { getRooCodeApiUrl } from "@roo-code/cloud"
import { MarketplaceItem, MarketplaceItemType } from "./types"
import { modeMarketplaceItemSchema, mcpMarketplaceItemSchema } from "./schemas"
import type { MarketplaceItem, MarketplaceItemType } from "@roo-code/types"
import { modeMarketplaceItemSchema, mcpMarketplaceItemSchema } from "@roo-code/types"

// Response schemas for YAML API responses
const modeMarketplaceResponse = z.object({
Expand Down Expand Up @@ -43,8 +43,8 @@ export class RemoteConfigLoader {
const yamlData = yaml.parse(data)
const validated = modeMarketplaceResponse.parse(yamlData)

const items = validated.items.map((item) => ({
type: "mode" as MarketplaceItemType,
const items: MarketplaceItem[] = validated.items.map((item) => ({
type: "mode" as const,
...item,
}))

Expand All @@ -63,8 +63,8 @@ export class RemoteConfigLoader {
const yamlData = yaml.parse(data)
const validated = mcpMarketplaceResponse.parse(yamlData)

const items = validated.items.map((item) => ({
type: "mcp" as MarketplaceItemType,
const items: MarketplaceItem[] = validated.items.map((item) => ({
type: "mcp" as const,
...item,
}))

Expand Down
12 changes: 7 additions & 5 deletions src/services/marketplace/SimpleInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as vscode from "vscode"
import * as path from "path"
import * as fs from "fs/promises"
import * as yaml from "yaml"
import { MarketplaceItem, MarketplaceItemType, InstallMarketplaceItemOptions, McpParameter } from "./types"
import type { MarketplaceItem, MarketplaceItemType, InstallMarketplaceItemOptions, McpParameter } from "@roo-code/types"
import { GlobalFileNames } from "../../shared/globalFileNames"
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"

Expand All @@ -23,7 +23,7 @@ export class SimpleInstaller {
case "mcp":
return await this.installMcp(item, target, options)
default:
throw new Error(`Unsupported item type: ${item.type}`)
throw new Error(`Unsupported item type: ${(item as any).type}`)
}
}

Expand Down Expand Up @@ -135,7 +135,8 @@ export class SimpleInstaller {
}

// Merge parameters (method-specific override global)
const allParameters = [...(item.parameters || []), ...methodParameters]
const itemParameters = item.type === "mcp" ? item.parameters || [] : []
const allParameters = [...itemParameters, ...methodParameters]
const uniqueParameters = Array.from(new Map(allParameters.map((p) => [p.key, p])).values())

// Replace parameters if provided
Expand All @@ -158,7 +159,8 @@ export class SimpleInstaller {
methodParameters = method.parameters || []

// Re-merge parameters with the newly selected method
const allParametersForNewMethod = [...(item.parameters || []), ...methodParameters]
const itemParametersForNewMethod = item.type === "mcp" ? item.parameters || [] : []
const allParametersForNewMethod = [...itemParametersForNewMethod, ...methodParameters]
const uniqueParametersForNewMethod = Array.from(
new Map(allParametersForNewMethod.map((p) => [p.key, p])).values(),
)
Expand Down Expand Up @@ -239,7 +241,7 @@ export class SimpleInstaller {
await this.removeMcp(item, target)
break
default:
throw new Error(`Unsupported item type: ${item.type}`)
throw new Error(`Unsupported item type: ${(item as any).type}`)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MarketplaceManager } from "../MarketplaceManager"
import { MarketplaceItem } from "../types"
import type { MarketplaceItem } from "@roo-code/types"

// Mock axios
jest.mock("axios")
Expand Down Expand Up @@ -116,6 +116,7 @@ describe("MarketplaceManager", () => {
name: "Test MCP",
description: "A test MCP",
type: "mcp",
url: "https://example.com/mcp",
content: '{"command": "node", "args": ["server.js"]}',
},
]
Expand Down Expand Up @@ -204,6 +205,7 @@ describe("MarketplaceManager", () => {
name: "Test MCP",
description: "A test MCP",
type: "mcp",
url: "https://example.com/mcp",
content: '{"command": "node", "args": ["server.js"]}',
}

Expand Down Expand Up @@ -244,6 +246,7 @@ describe("MarketplaceManager", () => {
name: "Test MCP",
description: "A test MCP",
type: "mcp",
url: "https://example.com/mcp",
content: '{"command": "node", "args": ["server.js"]}',
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import axios from "axios"
import { RemoteConfigLoader } from "../RemoteConfigLoader"
import { MarketplaceItemType } from "../types"
import type { MarketplaceItemType } from "@roo-code/types"

// Mock axios
jest.mock("axios")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { SimpleInstaller } from "../SimpleInstaller"
import * as fs from "fs/promises"
import * as yaml from "yaml"
import * as vscode from "vscode"
import { MarketplaceItem } from "../types"
import type { MarketplaceItem } from "@roo-code/types"
import * as path from "path"

jest.mock("fs/promises")
Expand Down Expand Up @@ -126,6 +126,7 @@ describe("SimpleInstaller", () => {
name: "Test MCP",
description: "A test MCP server for testing",
type: "mcp",
url: "https://example.com/mcp",
content: JSON.stringify({
command: "test-server",
args: ["--test"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ describe("Marketplace Setting Check", () => {
type: "mcp" as const,
description: "Test description",
content: "test content",
url: "https://example.com/test-mcp",
},
mpInstallOptions: { target: "project" as const },
}
Expand Down
14 changes: 7 additions & 7 deletions src/services/marketplace/__tests__/nested-parameters.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest"
import { mcpInstallationMethodSchema, mcpMarketplaceItemYamlSchema } from "../schemas"
import { McpInstallationMethod, McpMarketplaceItem } from "../types"
import { mcpInstallationMethodSchema, mcpMarketplaceItemSchema } from "@roo-code/types"
import type { McpInstallationMethod, McpMarketplaceItem } from "@roo-code/types"

describe("Nested Parameters", () => {
describe("McpInstallationMethod Schema", () => {
Expand Down Expand Up @@ -95,7 +95,7 @@ describe("Nested Parameters", () => {
],
}

const result = mcpMarketplaceItemYamlSchema.parse(item)
const result = mcpMarketplaceItemSchema.parse(item)
expect(result.parameters).toHaveLength(1)
expect(result.parameters![0].key).toBe("api_key")

Expand Down Expand Up @@ -131,7 +131,7 @@ describe("Nested Parameters", () => {
],
}

const result = mcpMarketplaceItemYamlSchema.parse(item)
const result = mcpMarketplaceItemSchema.parse(item)
expect(result.parameters).toHaveLength(1)

const methods = result.content as McpInstallationMethod[]
Expand Down Expand Up @@ -160,7 +160,7 @@ describe("Nested Parameters", () => {
],
}

const result = mcpMarketplaceItemYamlSchema.parse(item)
const result = mcpMarketplaceItemSchema.parse(item)
expect(result.parameters).toBeUndefined()

const methods = result.content as McpInstallationMethod[]
Expand All @@ -182,7 +182,7 @@ describe("Nested Parameters", () => {
],
}

const result = mcpMarketplaceItemYamlSchema.parse(item)
const result = mcpMarketplaceItemSchema.parse(item)
expect(result.parameters).toBeUndefined()

const methods = result.content as McpInstallationMethod[]
Expand Down Expand Up @@ -221,7 +221,7 @@ describe("Nested Parameters", () => {
}

// This should validate successfully - the conflict resolution happens at runtime
const result = mcpMarketplaceItemYamlSchema.parse(item)
const result = mcpMarketplaceItemSchema.parse(item)
expect(result.parameters![0].key).toBe("version")

const methods = result.content as McpInstallationMethod[]
Expand Down
23 changes: 2 additions & 21 deletions src/services/marketplace/__tests__/optional-parameters.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest"
import { mcpParameterSchema } from "../schemas"
import { McpParameter } from "../types"
import { mcpParameterSchema } from "@roo-code/types"
import type { McpParameter } from "@roo-code/types"

describe("Optional Parameters", () => {
describe("McpParameter Schema", () => {
Expand Down Expand Up @@ -67,23 +67,4 @@ describe("Optional Parameters", () => {
}).toThrow()
})
})

describe("Type Definitions", () => {
it("should allow optional field in McpParameter interface", () => {
const requiredParam: McpParameter = {
name: "Required Param",
key: "required_key",
}

const optionalParam: McpParameter = {
name: "Optional Param",
key: "optional_key",
optional: true,
}

// These should compile without errors
expect(requiredParam.optional).toBeUndefined()
expect(optionalParam.optional).toBe(true)
})
})
})
3 changes: 1 addition & 2 deletions src/services/marketplace/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./SimpleInstaller"
export * from "./MarketplaceManager"
export * from "./types"
export * from "./schemas"
export type { MarketplaceItemType } from "@roo-code/types"
Loading
Loading