From f62e3d3c1dc5142bc86620660de2e8662e7ad0ca Mon Sep 17 00:00:00 2001 From: Continue Agent Date: Tue, 7 Oct 2025 17:49:36 +0000 Subject: [PATCH 1/2] Allow --mcp flag to accept URLs for streamable-http servers When a URL starting with http:// or https:// is passed to the --mcp flag, it will now automatically be configured as a streamable-http MCP server connection instead of trying to load it from the hub. This allows users to run: cn --mcp https://docs.continue.dev/mcp The URL will be parsed and configured with: - type: streamable-http - url: the provided URL - name: hostname from the URL Fixes CON-4285 Generated with [Continue](https://continue.dev) Co-authored-by: Username --- extensions/cli/src/configEnhancer.test.ts | 64 +++++++++++++++++++++++ extensions/cli/src/configEnhancer.ts | 23 +++++++- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/extensions/cli/src/configEnhancer.test.ts b/extensions/cli/src/configEnhancer.test.ts index 3aaea15b4cc..212158506c2 100644 --- a/extensions/cli/src/configEnhancer.test.ts +++ b/extensions/cli/src/configEnhancer.test.ts @@ -223,6 +223,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 = { diff --git a/extensions/cli/src/configEnhancer.ts b/extensions/cli/src/configEnhancer.ts index 49e464f2fb0..b299e871b49 100644 --- a/extensions/cli/src/configEnhancer.ts +++ b/extensions/cli/src/configEnhancer.ts @@ -171,7 +171,28 @@ export class ConfigEnhancer { config: AssistantUnrolled, mcps: string[], ): Promise { - 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 }; From b160906a54a89faa479a715db6e0cf35540b5b1d Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 8 Oct 2025 08:55:38 -0700 Subject: [PATCH 2/2] fix: tests --- extensions/cli/src/configEnhancer.test.ts | 11 ++--- .../src/services/workflow-integration.test.ts | 41 ++++++++++++++----- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/extensions/cli/src/configEnhancer.test.ts b/extensions/cli/src/configEnhancer.test.ts index 212158506c2..4b37951d568 100644 --- a/extensions/cli/src/configEnhancer.test.ts +++ b/extensions/cli/src/configEnhancer.test.ts @@ -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" }]; diff --git a/extensions/cli/src/services/workflow-integration.test.ts b/extensions/cli/src/services/workflow-integration.test.ts index 7c688d89c22..6d68563af67 100644 --- a/extensions/cli/src/services/workflow-integration.test.ts +++ b/extensions/cli/src/services/workflow-integration.test.ts @@ -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"); @@ -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" }); @@ -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 = { @@ -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( @@ -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" }); }); }); });