Skip to content
Closed
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
8 changes: 8 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ const baseProviderSettingsSchema = z.object({
reasoningEffort: reasoningEffortsSchema.optional(),
modelMaxTokens: z.number().optional(),
modelMaxThinkingTokens: z.number().optional(),

// External MCP server settings for enhance prompt
enhancePrompt: z
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding JSDoc comments to document these new settings:

Suggested change
enhancePrompt: z
// External MCP server settings for enhance prompt
/**
* Configuration for external MCP server integration
* @property {boolean} useExternalServer - Enable routing enhancement requests to external server
* @property {string} endpoint - URL endpoint of the external MCP server
*/
enhancePrompt: z

.object({
useExternalServer: z.boolean().optional(),
endpoint: z.string().url().optional(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For production use, should we restrict the endpoint to HTTPS only? Also, consider if we need to support authentication headers for secured endpoints in the future.

})
.optional(),
})

// Several of the providers share common model config properties.
Expand Down
254 changes: 253 additions & 1 deletion src/core/webview/__tests__/messageEnhancer.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { MessageEnhancer } from "../messageEnhancer"
import { ProviderSettings, ClineMessage } from "@roo-code/types"
import { ProviderSettings, ClineMessage, getModelId } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import * as singleCompletionHandlerModule from "../../../utils/single-completion-handler"
import { ProviderSettingsManager } from "../../config/ProviderSettingsManager"
Expand All @@ -9,6 +9,9 @@ import { ProviderSettingsManager } from "../../config/ProviderSettingsManager"
vi.mock("../../../utils/single-completion-handler")
vi.mock("@roo-code/telemetry")

// Mock global fetch
global.fetch = vi.fn()

describe("MessageEnhancer", () => {
let mockProviderSettingsManager: ProviderSettingsManager
let mockSingleCompletionHandler: ReturnType<typeof vi.fn>
Expand Down Expand Up @@ -254,6 +257,255 @@ describe("MessageEnhancer", () => {
// Should not include task history section
expect(calledPrompt).not.toContain("previous conversation context")
})

describe("External MCP Server", () => {
beforeEach(() => {
vi.mocked(global.fetch).mockReset()
})

it("should use external MCP server when enabled", async () => {
const mockResponse = {
enhancedPrompt: "Enhanced via external server",
}
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResponse),
} as any)

const configWithExternalServer: ProviderSettings = {
...mockApiConfiguration,
enhancePrompt: {
useExternalServer: true,
endpoint: "http://localhost:8000/enhance",
},
}

const result = await MessageEnhancer.enhanceMessage({
text: "Test prompt",
apiConfiguration: configWithExternalServer,
listApiConfigMeta: mockListApiConfigMeta,
providerSettingsManager: mockProviderSettingsManager,
})

expect(result.success).toBe(true)
expect(result.enhancedText).toBe("Enhanced via external server")
expect(global.fetch).toHaveBeenCalledWith("http://localhost:8000/enhance", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt: "Test prompt",
context: [],
model: "gpt-4",
}),
})
// Should not call internal enhancement
expect(mockSingleCompletionHandler).not.toHaveBeenCalled()
})

it("should include context messages when using external server", async () => {
const mockResponse = {
enhancedPrompt: "Enhanced with context",
}
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResponse),
} as any)

const configWithExternalServer: ProviderSettings = {
...mockApiConfiguration,
enhancePrompt: {
useExternalServer: true,
endpoint: "http://localhost:8000/enhance",
},
}

const mockClineMessages: ClineMessage[] = [
{ type: "ask", text: "User message", ts: 1000 },
{ type: "say", say: "text", text: "Assistant response", ts: 2000 },
]

await MessageEnhancer.enhanceMessage({
text: "Test prompt",
apiConfiguration: configWithExternalServer,
listApiConfigMeta: mockListApiConfigMeta,
currentClineMessages: mockClineMessages,
providerSettingsManager: mockProviderSettingsManager,
})

expect(global.fetch).toHaveBeenCalledWith("http://localhost:8000/enhance", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt: "Test prompt",
context: [
{ role: "user", content: "User message" },
{ role: "assistant", content: "Assistant response" },
],
model: "gpt-4",
}),
})
})

it("should fall back to internal enhancement when external server fails", async () => {
vi.mocked(global.fetch).mockRejectedValue(new Error("Network error"))

const configWithExternalServer: ProviderSettings = {
...mockApiConfiguration,
enhancePrompt: {
useExternalServer: true,
endpoint: "http://localhost:8000/enhance",
},
}

const result = await MessageEnhancer.enhanceMessage({
text: "Test prompt",
apiConfiguration: configWithExternalServer,
listApiConfigMeta: mockListApiConfigMeta,
providerSettingsManager: mockProviderSettingsManager,
})

expect(result.success).toBe(true)
expect(result.enhancedText).toBe("Enhanced prompt text")
expect(mockSingleCompletionHandler).toHaveBeenCalled()
})

it("should fall back when external server returns non-ok status", async () => {
vi.mocked(global.fetch).mockResolvedValue({
ok: false,
status: 500,
statusText: "Internal Server Error",
} as any)

const configWithExternalServer: ProviderSettings = {
...mockApiConfiguration,
enhancePrompt: {
useExternalServer: true,
endpoint: "http://localhost:8000/enhance",
},
}

const result = await MessageEnhancer.enhanceMessage({
text: "Test prompt",
apiConfiguration: configWithExternalServer,
listApiConfigMeta: mockListApiConfigMeta,
providerSettingsManager: mockProviderSettingsManager,
})

expect(result.success).toBe(true)
expect(result.enhancedText).toBe("Enhanced prompt text")
expect(mockSingleCompletionHandler).toHaveBeenCalled()
})

it("should fall back when external server response is missing enhancedPrompt", async () => {
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({ wrongField: "value" }),
} as any)

const configWithExternalServer: ProviderSettings = {
...mockApiConfiguration,
enhancePrompt: {
useExternalServer: true,
endpoint: "http://localhost:8000/enhance",
},
}

const result = await MessageEnhancer.enhanceMessage({
text: "Test prompt",
apiConfiguration: configWithExternalServer,
listApiConfigMeta: mockListApiConfigMeta,
providerSettingsManager: mockProviderSettingsManager,
})

expect(result.success).toBe(true)
expect(result.enhancedText).toBe("Enhanced prompt text")
expect(mockSingleCompletionHandler).toHaveBeenCalled()
})

it("should not use external server when useExternalServer is false", async () => {
const configWithDisabledExternalServer: ProviderSettings = {
...mockApiConfiguration,
enhancePrompt: {
useExternalServer: false,
endpoint: "http://localhost:8000/enhance",
},
}

await MessageEnhancer.enhanceMessage({
text: "Test prompt",
apiConfiguration: configWithDisabledExternalServer,
listApiConfigMeta: mockListApiConfigMeta,
providerSettingsManager: mockProviderSettingsManager,
})

expect(global.fetch).not.toHaveBeenCalled()
expect(mockSingleCompletionHandler).toHaveBeenCalled()
})

it("should not use external server when endpoint is missing", async () => {
const configWithoutEndpoint: ProviderSettings = {
...mockApiConfiguration,
enhancePrompt: {
useExternalServer: true,
},
}

await MessageEnhancer.enhanceMessage({
text: "Test prompt",
apiConfiguration: configWithoutEndpoint,
listApiConfigMeta: mockListApiConfigMeta,
providerSettingsManager: mockProviderSettingsManager,
})

expect(global.fetch).not.toHaveBeenCalled()
expect(mockSingleCompletionHandler).toHaveBeenCalled()
})

it("should handle different model ID fields correctly", async () => {
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({ enhancedPrompt: "Enhanced" }),
} as any)

// Test with different provider configurations
const configs = [
{
...mockApiConfiguration,
apiModelId: "model-1",
enhancePrompt: { useExternalServer: true, endpoint: "http://localhost:8000/enhance" },
},
{
apiProvider: "ollama" as const,
ollamaModelId: "llama2",
enhancePrompt: { useExternalServer: true, endpoint: "http://localhost:8000/enhance" },
},
{
apiProvider: "openrouter" as const,
openRouterModelId: "gpt-4",
enhancePrompt: { useExternalServer: true, endpoint: "http://localhost:8000/enhance" },
},
]

for (const config of configs) {
vi.mocked(global.fetch).mockClear()

await MessageEnhancer.enhanceMessage({
text: "Test",
apiConfiguration: config,
listApiConfigMeta: mockListApiConfigMeta,
providerSettingsManager: mockProviderSettingsManager,
})

const expectedModel = getModelId(config) || "unknown"
expect(global.fetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
body: expect.stringContaining(`"model":"${expectedModel}"`),
}),
)
}
})
})
})

describe("captureTelemetry", () => {
Expand Down
51 changes: 50 additions & 1 deletion src/core/webview/messageEnhancer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ProviderSettings, ClineMessage, GlobalState, TelemetryEventName } from "@roo-code/types"
import { ProviderSettings, ClineMessage, GlobalState, TelemetryEventName, getModelId } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { supportPrompt } from "../../shared/support-prompt"
import { singleCompletionHandler } from "../../utils/single-completion-handler"
Expand Down Expand Up @@ -58,6 +58,55 @@ export class MessageEnhancer {
}
}

// Check if external MCP server is enabled
if (configToUse.enhancePrompt?.useExternalServer && configToUse.enhancePrompt?.endpoint) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great implementation! Just a thought - should we add UI components to configure these settings in the settings panel? Currently users would need to manually edit their settings.json. We could add controls similar to other provider settings.

try {
// Prepare context messages for external server
const contextMessages =
currentClineMessages
?.filter((msg) => {
if (msg.type === "ask" && msg.text) return true
if (msg.type === "say" && msg.say === "text" && msg.text) return true
return false
})
.slice(-10)
.map((msg) => ({
role: msg.type === "ask" ? "user" : "assistant",
content: msg.text || "",
})) || []

// Make request to external MCP server
const response = await fetch(configToUse.enhancePrompt.endpoint, {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a timeout to prevent hanging requests. You could use AbortController:

Suggested change
const response = await fetch(configToUse.enhancePrompt.endpoint, {
// Make request to external MCP server
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10000) // 10 second timeout
const response = await fetch(configToUse.enhancePrompt.endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
signal: controller.signal,
body: JSON.stringify({
prompt: text,
context: contextMessages,
model: getModelId(configToUse) || "unknown",
}),
})
clearTimeout(timeoutId)

method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt: text,
context: contextMessages,
model: getModelId(configToUse) || "unknown",
}),
})

if (!response.ok) {
throw new Error(`External server returned ${response.status}: ${response.statusText}`)
}

const result = await response.json()

if (result.enhancedPrompt) {
return {
success: true,
enhancedText: result.enhancedPrompt,
}
} else {
throw new Error("External server response missing 'enhancedPrompt' field")
}
} catch (err) {
console.error("Failed to enhance prompt via external server:", err)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error logging could be more descriptive to help with debugging. Consider:

Suggested change
console.error("Failed to enhance prompt via external server:", err)
} catch (err) {
console.error(`Failed to enhance prompt via external server (${configToUse.enhancePrompt.endpoint}):`, err)
// Fallback to default logic

// Fallback to default logic
}
}

// Default internal enhancement logic
// Prepare the prompt to enhance
let promptToEnhance = text

Expand Down
Loading