Skip to content
Open
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
207 changes: 207 additions & 0 deletions extensions/cli/src/services/MCPService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ vi.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
StreamableHTTPClientTransport: vi.fn(),
}));

vi.mock("https", () => ({
Agent: vi.fn().mockImplementation(() => ({
rejectUnauthorized: false,
})),
}));

describe("MCPService", () => {
let mcpService: MCPService;
let mockAssistant: AssistantConfig;
Expand Down Expand Up @@ -211,5 +217,206 @@ describe("MCPService", () => {
mcpService.initialize(defaultAssistant),
).resolves.not.toThrow();
});

it("should create HttpsAgent when verifySsl is false for SSE transport", async () => {
const { SSEClientTransport } = await import(
"@modelcontextprotocol/sdk/client/sse.js"
);
const { Agent: HttpsAgent } = await import("https");

const sseAssistant: AssistantConfig = {
name: "sse-assistant",
version: "1.0.0",
mcpServers: [
{
name: "sse-server",
type: "sse",
url: "https://example.com/sse",
requestOptions: {
verifySsl: false,
},
},
],
} as AssistantConfig;

await mcpService.initialize(sseAssistant);

expect(SSEClientTransport).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
eventSourceInit: expect.objectContaining({
fetch: expect.any(Function),
}),
requestInit: expect.objectContaining({
agent: expect.any(Object),
}),
}),
);
expect(HttpsAgent).toHaveBeenCalledWith({
rejectUnauthorized: false,
});
});

it("should not create HttpsAgent when verifySsl is true for SSE transport", async () => {
const { SSEClientTransport } = await import(
"@modelcontextprotocol/sdk/client/sse.js"
);
const { Agent: HttpsAgent } = await import("https");

const sseAssistant: AssistantConfig = {
name: "sse-assistant",
version: "1.0.0",
mcpServers: [
{
name: "sse-server",
type: "sse",
url: "https://example.com/sse",
requestOptions: {
verifySsl: true,
},
},
],
} as AssistantConfig;

await mcpService.initialize(sseAssistant);

expect(SSEClientTransport).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
eventSourceInit: expect.objectContaining({
fetch: expect.any(Function),
}),
requestInit: expect.objectContaining({
headers: {},
}),
}),
);
expect(SSEClientTransport).toHaveBeenCalledWith(
expect.any(URL),
expect.not.objectContaining({
requestInit: expect.objectContaining({
agent: expect.anything(),
}),
}),
);
expect(HttpsAgent).not.toHaveBeenCalled();
});

it("should create HttpsAgent when verifySsl is false for streamable-http transport", async () => {
const { StreamableHTTPClientTransport } = await import(
"@modelcontextprotocol/sdk/client/streamableHttp.js"
);
const { Agent: HttpsAgent } = await import("https");

const httpAssistant: AssistantConfig = {
name: "http-assistant",
version: "1.0.0",
mcpServers: [
{
name: "http-server",
type: "streamable-http",
url: "https://example.com/http",
requestOptions: {
verifySsl: false,
},
},
],
} as AssistantConfig;

await mcpService.initialize(httpAssistant);

expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
requestInit: expect.objectContaining({
agent: expect.any(Object),
}),
}),
);
expect(HttpsAgent).toHaveBeenCalledWith({
rejectUnauthorized: false,
});
});

it("should not create HttpsAgent when verifySsl is true for streamable-http transport", async () => {
const { StreamableHTTPClientTransport } = await import(
"@modelcontextprotocol/sdk/client/streamableHttp.js"
);
const { Agent: HttpsAgent } = await import("https");

const httpAssistant: AssistantConfig = {
name: "http-assistant",
version: "1.0.0",
mcpServers: [
{
name: "http-server",
type: "streamable-http",
url: "https://example.com/http",
requestOptions: {
verifySsl: true,
},
},
],
} as AssistantConfig;

await mcpService.initialize(httpAssistant);

expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
requestInit: expect.objectContaining({
headers: {},
}),
}),
);
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
expect.any(URL),
expect.not.objectContaining({
requestInit: expect.objectContaining({
agent: expect.anything(),
}),
}),
);
expect(HttpsAgent).not.toHaveBeenCalled();
});

it("should not create HttpsAgent when verifySsl is not specified", async () => {
const { StreamableHTTPClientTransport } = await import(
"@modelcontextprotocol/sdk/client/streamableHttp.js"
);
const { Agent: HttpsAgent } = await import("https");

const httpAssistant: AssistantConfig = {
name: "http-assistant",
version: "1.0.0",
mcpServers: [
{
name: "http-server",
type: "streamable-http",
url: "https://example.com/http",
},
],
} as AssistantConfig;

await mcpService.initialize(httpAssistant);

expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
requestInit: expect.objectContaining({
headers: {},
}),
}),
);
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
expect.any(URL),
expect.not.objectContaining({
requestInit: expect.objectContaining({
agent: expect.anything(),
}),
}),
);
expect(HttpsAgent).not.toHaveBeenCalled();
});
});
});
24 changes: 20 additions & 4 deletions extensions/cli/src/services/MCPService.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Agent as HttpsAgent } from "https";

import { decodeFQSN, getTemplateVariables } from "@continuedev/config-yaml";
import { type AssistantConfig } from "@continuedev/sdk";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
Expand Down Expand Up @@ -547,7 +549,11 @@
serverConfig: SseMcpServer,
): SSEClientTransport {
const apiKey = this.apiKeyCache.get(serverConfig.name);
// Merge apiKey into headers if provided
const sseAgent =
serverConfig.requestOptions?.verifySsl === false
? new HttpsAgent({ rejectUnauthorized: false })

Check failure

Code scanning / CodeQL

Disabling certificate validation High

Disabling certificate validation is strongly discouraged.
: undefined;

const headers = {
...serverConfig.requestOptions?.headers,
...(apiKey && {
Expand All @@ -564,16 +570,23 @@
...init?.headers,
...headers,
},
...(sseAgent && { agent: sseAgent }),
}),
},
requestInit: { headers },
requestInit: {
headers,
...(sseAgent && { agent: sseAgent }),
},
});
}
private constructHttpTransport(
serverConfig: HttpMcpServer,
): StreamableHTTPClientTransport {
// Merge apiKey into headers if provided
const apiKey = this.apiKeyCache.get(serverConfig.name);
const streamableAgent =
serverConfig.requestOptions?.verifySsl === false
? new HttpsAgent({ rejectUnauthorized: false })

Check failure

Code scanning / CodeQL

Disabling certificate validation High

Disabling certificate validation is strongly discouraged.
: undefined;

const headers = {
...serverConfig.requestOptions?.headers,
Expand All @@ -583,7 +596,10 @@
};

return new StreamableHTTPClientTransport(new URL(serverConfig.url), {
requestInit: { headers },
requestInit: {
headers,
...(streamableAgent && { agent: streamableAgent }),
},
});
}
private constructStdioTransport(
Expand All @@ -603,7 +619,7 @@
command: serverConfig.command,
args: serverConfig.args || [],
env,
cwd: serverConfig.cwd,

Check failure on line 622 in extensions/cli/src/services/MCPService.ts

View workflow job for this annotation

GitHub Actions / lint

File has too many lines (515). Maximum allowed is 500
stderr: "pipe",
});

Expand Down
Loading