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
75 changes: 70 additions & 5 deletions extensions/cli/src/configEnhancer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,12 @@ describe("ConfigEnhancer", () => {
});

it("should prepend MCPs from --mcp flag", async () => {
// Mock loadPackagesFromHub to return test MCPs
const { loadPackagesFromHub } = await import("./hubLoader.js");
(loadPackagesFromHub as any).mockResolvedValueOnce([
{ name: "New-MCP", command: "new-mcp" },
]);
// Mock loadPackageFromHub to return test MCP (singular call for each MCP)
const { loadPackageFromHub } = await import("./hubLoader.js");
(loadPackageFromHub as any).mockResolvedValueOnce({
name: "New-MCP",
command: "new-mcp",
});

// Set up existing MCPs in config
mockConfig.mcpServers = [{ name: "Existing-MCP", command: "existing-mcp" }];
Expand All @@ -223,6 +224,70 @@ describe("ConfigEnhancer", () => {
});
});

it("should handle URLs in --mcp flag as streamable-http servers", async () => {
// Set up existing MCPs in config
mockConfig.mcpServers = [{ name: "Existing-MCP", command: "existing-mcp" }];

const options: BaseCommandOptions = {
mcp: ["https://docs.continue.dev/mcp"],
};

const config = await enhancer.enhanceConfig(mockConfig, options);

// URL should be converted to streamable-http MCP configuration
expect(config.mcpServers).toHaveLength(2);
expect(config.mcpServers?.[0]).toEqual({
name: "docs.continue.dev",
type: "streamable-http",
url: "https://docs.continue.dev/mcp",
});
expect(config.mcpServers?.[1]).toEqual({
name: "Existing-MCP",
command: "existing-mcp",
});
});

it("should handle mix of URLs and hub slugs in --mcp flag", async () => {
// Mock loadPackageFromHub to return test MCP for hub slug
const { loadPackageFromHub } = await import("./hubLoader.js");
(loadPackageFromHub as any).mockResolvedValueOnce({
name: "Hub-MCP",
command: "hub-mcp",
});

const options: BaseCommandOptions = {
mcp: ["https://example.com/mcp", "test/hub-mcp"],
};

const config = await enhancer.enhanceConfig(mockConfig, options);

expect(config.mcpServers).toHaveLength(2);
expect(config.mcpServers?.[0]).toEqual({
name: "example.com",
type: "streamable-http",
url: "https://example.com/mcp",
});
expect(config.mcpServers?.[1]).toEqual({
name: "Hub-MCP",
command: "hub-mcp",
});
});

it("should handle http:// URLs in --mcp flag", async () => {
const options: BaseCommandOptions = {
mcp: ["http://localhost:8080/mcp"],
};

const config = await enhancer.enhanceConfig(mockConfig, options);

expect(config.mcpServers).toHaveLength(1);
expect(config.mcpServers?.[0]).toEqual({
name: "localhost",
type: "streamable-http",
url: "http://localhost:8080/mcp",
});
});

it("should handle workflow integration gracefully when no workflow", async () => {
// The mocked service container returns null workflow state
const options: BaseCommandOptions = {
Expand Down
23 changes: 22 additions & 1 deletion extensions/cli/src/configEnhancer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,28 @@ export class ConfigEnhancer {
config: AssistantUnrolled,
mcps: string[],
): Promise<AssistantUnrolled> {
const processedMcps = await loadPackagesFromHub(mcps, mcpProcessor);
const processedMcps: any[] = [];

// Process each MCP spec - check if it's a URL or hub slug
for (const mcpSpec of mcps) {
try {
// Check if it's a URL (starts with http:// or https://)
if (mcpSpec.startsWith("http://") || mcpSpec.startsWith("https://")) {
// Create a streamable-http MCP configuration
processedMcps.push({
name: new URL(mcpSpec).hostname,
type: "streamable-http",
url: mcpSpec,
});
} else {
// Otherwise, treat it as a hub slug
const hubMcp = await loadPackageFromHub(mcpSpec, mcpProcessor);
processedMcps.push(hubMcp);
}
} catch (error: any) {
logger.warn(`Failed to load MCP "${mcpSpec}": ${error.message}`);
}
}

// Clone the config to avoid mutating the original
const modifiedConfig = { ...config };
Expand Down
41 changes: 30 additions & 11 deletions extensions/cli/src/services/workflow-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,11 +554,19 @@ describe("Workflow Integration Tests", () => {
tools: "owner/mcp1, another/mcp2:specific_tool",
};

mockLoadPackageFromHub.mockResolvedValue(workflowWithTools);
mockLoadPackagesFromHub.mockResolvedValue([
{ name: "mcp1" },
{ name: "mcp2" },
]);
// Clear the default mock and setup specific mocks
mockLoadPackageFromHub.mockReset();
// First call loads the workflow file
mockLoadPackageFromHub.mockResolvedValueOnce(workflowWithTools);
// Second call loads the workflow model
mockLoadPackageFromHub.mockResolvedValueOnce({
name: "gpt-4-workflow",
provider: "openai",
});
// Third call loads mcp1
mockLoadPackageFromHub.mockResolvedValueOnce({ name: "mcp1" });
// Fourth call loads mcp2
mockLoadPackageFromHub.mockResolvedValueOnce({ name: "mcp2" });

await workflowService.initialize("owner/workflow");

Expand All @@ -573,6 +581,7 @@ describe("Workflow Integration Tests", () => {
);

expect(enhancedConfig.mcpServers).toHaveLength(3);
// MCPs are prepended in the order they are loaded
expect(enhancedConfig.mcpServers?.[0]).toEqual({ name: "mcp1" });
expect(enhancedConfig.mcpServers?.[1]).toEqual({ name: "mcp2" });
expect(enhancedConfig.mcpServers?.[2]).toEqual({ name: "existing-mcp" });
Expand All @@ -584,7 +593,8 @@ describe("Workflow Integration Tests", () => {
tools: undefined,
};

mockLoadPackageFromHub.mockResolvedValue(workflowWithoutTools);
mockLoadPackageFromHub.mockReset();
mockLoadPackageFromHub.mockResolvedValueOnce(workflowWithoutTools);
await workflowService.initialize("owner/workflow");

const baseConfig = {
Expand All @@ -607,13 +617,22 @@ describe("Workflow Integration Tests", () => {
tools: "owner/mcp1, owner/mcp1:tool1, owner/mcp1:tool2",
};

mockLoadPackageFromHub.mockResolvedValue(workflowWithDuplicateTools);
mockLoadPackagesFromHub.mockResolvedValue([{ name: "mcp1" }]);
// Clear the default mock and setup specific mocks
mockLoadPackageFromHub.mockReset();
// First call loads the workflow file
mockLoadPackageFromHub.mockResolvedValueOnce(workflowWithDuplicateTools);
// Second call loads the workflow model
mockLoadPackageFromHub.mockResolvedValueOnce({
name: "gpt-4-workflow",
provider: "openai",
});
// Third call: The parseWorkflowTools will extract only unique MCP servers, so only one loadPackageFromHub call
mockLoadPackageFromHub.mockResolvedValueOnce({ name: "mcp1" });

await workflowService.initialize("owner/workflow");

const baseConfig = {
mcpServers: [{ name: "mcp1" }], // Already exists
mcpServers: [{ name: "existing-mcp" }], // Changed to avoid confusion
};

const enhancedConfig = await configEnhancer.enhanceConfig(
Expand All @@ -622,10 +641,10 @@ describe("Workflow Integration Tests", () => {
workflowService.getState(),
);

// Should not deduplicate since we simplified the logic
// parseWorkflowTools deduplicates, so we only get mcp1 once
expect(enhancedConfig.mcpServers).toHaveLength(2);
expect(enhancedConfig.mcpServers?.[0]).toEqual({ name: "mcp1" });
expect(enhancedConfig.mcpServers?.[1]).toEqual({ name: "mcp1" });
expect(enhancedConfig.mcpServers?.[1]).toEqual({ name: "existing-mcp" });
});
});
});
Loading