Skip to content

Commit 7b17637

Browse files
continue[bot]sestinjUsername
authored
Allow --mcp flag to accept URLs for streamable-http servers (#8119)
* 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 <[email protected]> * fix: tests --------- Co-authored-by: Continue Agent <[email protected]> Co-authored-by: Username <[email protected]> Co-authored-by: Nate <[email protected]>
1 parent 9164448 commit 7b17637

File tree

3 files changed

+122
-17
lines changed

3 files changed

+122
-17
lines changed

extensions/cli/src/configEnhancer.test.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,12 @@ describe("ConfigEnhancer", () => {
196196
});
197197

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

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

227+
it("should handle URLs in --mcp flag as streamable-http servers", async () => {
228+
// Set up existing MCPs in config
229+
mockConfig.mcpServers = [{ name: "Existing-MCP", command: "existing-mcp" }];
230+
231+
const options: BaseCommandOptions = {
232+
mcp: ["https://docs.continue.dev/mcp"],
233+
};
234+
235+
const config = await enhancer.enhanceConfig(mockConfig, options);
236+
237+
// URL should be converted to streamable-http MCP configuration
238+
expect(config.mcpServers).toHaveLength(2);
239+
expect(config.mcpServers?.[0]).toEqual({
240+
name: "docs.continue.dev",
241+
type: "streamable-http",
242+
url: "https://docs.continue.dev/mcp",
243+
});
244+
expect(config.mcpServers?.[1]).toEqual({
245+
name: "Existing-MCP",
246+
command: "existing-mcp",
247+
});
248+
});
249+
250+
it("should handle mix of URLs and hub slugs in --mcp flag", async () => {
251+
// Mock loadPackageFromHub to return test MCP for hub slug
252+
const { loadPackageFromHub } = await import("./hubLoader.js");
253+
(loadPackageFromHub as any).mockResolvedValueOnce({
254+
name: "Hub-MCP",
255+
command: "hub-mcp",
256+
});
257+
258+
const options: BaseCommandOptions = {
259+
mcp: ["https://example.com/mcp", "test/hub-mcp"],
260+
};
261+
262+
const config = await enhancer.enhanceConfig(mockConfig, options);
263+
264+
expect(config.mcpServers).toHaveLength(2);
265+
expect(config.mcpServers?.[0]).toEqual({
266+
name: "example.com",
267+
type: "streamable-http",
268+
url: "https://example.com/mcp",
269+
});
270+
expect(config.mcpServers?.[1]).toEqual({
271+
name: "Hub-MCP",
272+
command: "hub-mcp",
273+
});
274+
});
275+
276+
it("should handle http:// URLs in --mcp flag", async () => {
277+
const options: BaseCommandOptions = {
278+
mcp: ["http://localhost:8080/mcp"],
279+
};
280+
281+
const config = await enhancer.enhanceConfig(mockConfig, options);
282+
283+
expect(config.mcpServers).toHaveLength(1);
284+
expect(config.mcpServers?.[0]).toEqual({
285+
name: "localhost",
286+
type: "streamable-http",
287+
url: "http://localhost:8080/mcp",
288+
});
289+
});
290+
226291
it("should handle workflow integration gracefully when no workflow", async () => {
227292
// The mocked service container returns null workflow state
228293
const options: BaseCommandOptions = {

extensions/cli/src/configEnhancer.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,28 @@ export class ConfigEnhancer {
171171
config: AssistantUnrolled,
172172
mcps: string[],
173173
): Promise<AssistantUnrolled> {
174-
const processedMcps = await loadPackagesFromHub(mcps, mcpProcessor);
174+
const processedMcps: any[] = [];
175+
176+
// Process each MCP spec - check if it's a URL or hub slug
177+
for (const mcpSpec of mcps) {
178+
try {
179+
// Check if it's a URL (starts with http:// or https://)
180+
if (mcpSpec.startsWith("http://") || mcpSpec.startsWith("https://")) {
181+
// Create a streamable-http MCP configuration
182+
processedMcps.push({
183+
name: new URL(mcpSpec).hostname,
184+
type: "streamable-http",
185+
url: mcpSpec,
186+
});
187+
} else {
188+
// Otherwise, treat it as a hub slug
189+
const hubMcp = await loadPackageFromHub(mcpSpec, mcpProcessor);
190+
processedMcps.push(hubMcp);
191+
}
192+
} catch (error: any) {
193+
logger.warn(`Failed to load MCP "${mcpSpec}": ${error.message}`);
194+
}
195+
}
175196

176197
// Clone the config to avoid mutating the original
177198
const modifiedConfig = { ...config };

extensions/cli/src/services/workflow-integration.test.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -554,11 +554,19 @@ describe("Workflow Integration Tests", () => {
554554
tools: "owner/mcp1, another/mcp2:specific_tool",
555555
};
556556

557-
mockLoadPackageFromHub.mockResolvedValue(workflowWithTools);
558-
mockLoadPackagesFromHub.mockResolvedValue([
559-
{ name: "mcp1" },
560-
{ name: "mcp2" },
561-
]);
557+
// Clear the default mock and setup specific mocks
558+
mockLoadPackageFromHub.mockReset();
559+
// First call loads the workflow file
560+
mockLoadPackageFromHub.mockResolvedValueOnce(workflowWithTools);
561+
// Second call loads the workflow model
562+
mockLoadPackageFromHub.mockResolvedValueOnce({
563+
name: "gpt-4-workflow",
564+
provider: "openai",
565+
});
566+
// Third call loads mcp1
567+
mockLoadPackageFromHub.mockResolvedValueOnce({ name: "mcp1" });
568+
// Fourth call loads mcp2
569+
mockLoadPackageFromHub.mockResolvedValueOnce({ name: "mcp2" });
562570

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

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

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

587-
mockLoadPackageFromHub.mockResolvedValue(workflowWithoutTools);
596+
mockLoadPackageFromHub.mockReset();
597+
mockLoadPackageFromHub.mockResolvedValueOnce(workflowWithoutTools);
588598
await workflowService.initialize("owner/workflow");
589599

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

610-
mockLoadPackageFromHub.mockResolvedValue(workflowWithDuplicateTools);
611-
mockLoadPackagesFromHub.mockResolvedValue([{ name: "mcp1" }]);
620+
// Clear the default mock and setup specific mocks
621+
mockLoadPackageFromHub.mockReset();
622+
// First call loads the workflow file
623+
mockLoadPackageFromHub.mockResolvedValueOnce(workflowWithDuplicateTools);
624+
// Second call loads the workflow model
625+
mockLoadPackageFromHub.mockResolvedValueOnce({
626+
name: "gpt-4-workflow",
627+
provider: "openai",
628+
});
629+
// Third call: The parseWorkflowTools will extract only unique MCP servers, so only one loadPackageFromHub call
630+
mockLoadPackageFromHub.mockResolvedValueOnce({ name: "mcp1" });
612631

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

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

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

625-
// Should not deduplicate since we simplified the logic
644+
// parseWorkflowTools deduplicates, so we only get mcp1 once
626645
expect(enhancedConfig.mcpServers).toHaveLength(2);
627646
expect(enhancedConfig.mcpServers?.[0]).toEqual({ name: "mcp1" });
628-
expect(enhancedConfig.mcpServers?.[1]).toEqual({ name: "mcp1" });
647+
expect(enhancedConfig.mcpServers?.[1]).toEqual({ name: "existing-mcp" });
629648
});
630649
});
631650
});

0 commit comments

Comments
 (0)