Skip to content

Commit d953ac3

Browse files
Merge pull request #5517 from 04cfb1ed/feature/add-mcp-sse-in-config-yaml
feat: add support for SSE MCP servers
2 parents 38f58b1 + 99b1ab2 commit d953ac3

File tree

7 files changed

+248
-28
lines changed

7 files changed

+248
-28
lines changed

core/config/workspace/workspaceBlocks.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,16 @@ function getContentsForNewBlock(blockType: BlockType): ConfigYaml {
5353
configYaml.mcpServers = [
5454
{
5555
name: "New MCP server",
56+
type: "stdio",
5657
command: "npx",
5758
args: ["-y", "<your-mcp-server>"],
5859
env: {},
5960
},
61+
{
62+
name: "New MCP server",
63+
type: "sse",
64+
url: "https://example.org/sse"
65+
},
6066
];
6167
break;
6268
}

core/config/yaml/loadYaml.test.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { MCPServer } from "@continuedev/config-yaml";
2+
import { convertYamlMcpToContinueMcp } from "./loadYaml";
3+
4+
describe("MCP Server Configuration Tests", () => {
5+
test("should convert stdio MCP server correctly", () => {
6+
const stdioServer: MCPServer = {
7+
name: "Test Stdio Server",
8+
type: "stdio",
9+
command: "uvx",
10+
args: ["mcp-server-sqlite", "--db-path", "/test.db"],
11+
env: { TEST_ENV: "value" },
12+
connectionTimeout: 5000
13+
};
14+
15+
const result = convertYamlMcpToContinueMcp(stdioServer);
16+
17+
expect(result).toEqual({
18+
transport: {
19+
type: "stdio",
20+
command: "uvx",
21+
args: ["mcp-server-sqlite", "--db-path", "/test.db"],
22+
env: { TEST_ENV: "value" }
23+
},
24+
timeout: 5000
25+
});
26+
});
27+
28+
test("should convert SSE MCP server correctly", () => {
29+
const sseServer: MCPServer = {
30+
name: "Test SSE Server",
31+
type: "sse",
32+
url: "http://localhost:8150/cosmos/mcp/v1/sse",
33+
connectionTimeout: 3000
34+
};
35+
36+
const result = convertYamlMcpToContinueMcp(sseServer);
37+
38+
expect(result).toEqual({
39+
transport: {
40+
type: "sse",
41+
url: "http://localhost:8150/cosmos/mcp/v1/sse"
42+
},
43+
timeout: 3000
44+
});
45+
});
46+
47+
test("should convert WebSocket MCP server correctly", () => {
48+
const wsServer: MCPServer = {
49+
name: "Test WebSocket Server",
50+
type: "websocket",
51+
url: "ws://localhost:8150/cosmos/mcp/v1/ws",
52+
connectionTimeout: 10000
53+
};
54+
55+
const result = convertYamlMcpToContinueMcp(wsServer);
56+
57+
expect(result).toEqual({
58+
transport: {
59+
type: "websocket",
60+
url: "ws://localhost:8150/cosmos/mcp/v1/ws"
61+
},
62+
timeout: 10000
63+
});
64+
});
65+
66+
test("should handle legacy MCP server format for backward compatibility", () => {
67+
// Test with old format that doesn't have a type field
68+
const legacyServer = {
69+
name: "Legacy Server",
70+
command: "old-command",
71+
args: ["--legacy"],
72+
connectionTimeout: 2000
73+
} as any;
74+
75+
const result = convertYamlMcpToContinueMcp(legacyServer);
76+
77+
expect(result).toEqual({
78+
transport: {
79+
type: "stdio",
80+
command: "old-command",
81+
args: ["--legacy"],
82+
env: undefined
83+
},
84+
timeout: 2000
85+
});
86+
});
87+
88+
test("should handle missing optional fields", () => {
89+
const minimalServer: MCPServer = {
90+
name: "Minimal Server",
91+
type: "stdio",
92+
command: "minimal"
93+
};
94+
95+
const result = convertYamlMcpToContinueMcp(minimalServer);
96+
97+
expect(result).toEqual({
98+
transport: {
99+
type: "stdio",
100+
command: "minimal",
101+
args: [],
102+
env: undefined
103+
},
104+
timeout: undefined
105+
});
106+
});
107+
});

core/config/yaml/loadYaml.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,42 @@ function convertYamlRuleToContinueRule(rule: Rule): RuleWithSource {
6262
}
6363
}
6464

65-
function convertYamlMcpToContinueMcp(
65+
export function convertYamlMcpToContinueMcp(
6666
server: MCPServer,
6767
): ExperimentalMCPOptions {
68+
const transportConfig = (() => {
69+
switch (server.type) {
70+
case "stdio":
71+
return {
72+
type: "stdio" as const,
73+
command: server.command,
74+
args: server.args ?? [],
75+
env: server.env,
76+
};
77+
case "sse":
78+
return {
79+
type: "sse" as const,
80+
url: server.url,
81+
};
82+
case "websocket":
83+
return {
84+
type: "websocket" as const,
85+
url: server.url,
86+
};
87+
default:
88+
// Default to stdio for backward compatibility
89+
return {
90+
type: "stdio" as const,
91+
command: (server as any).command,
92+
args: (server as any).args ?? [],
93+
env: (server as any).env,
94+
};
95+
}
96+
})();
97+
6898
return {
69-
transport: {
70-
type: "stdio",
71-
command: server.command,
72-
args: server.args ?? [],
73-
env: server.env,
74-
},
75-
timeout: server.connectionTimeout,
99+
transport: transportConfig,
100+
timeout: server.connectionTimeout
76101
};
77102
}
78103

@@ -244,7 +269,7 @@ async function configYamlToContinueConfig(options: {
244269

245270
config.mcpServers?.forEach((mcpServer) => {
246271
const mcpArgVariables =
247-
mcpServer.args?.filter((arg) => TEMPLATE_VAR_REGEX.test(arg)) ?? [];
272+
(mcpServer.type === "stdio" ? mcpServer.args?.filter((arg) => TEMPLATE_VAR_REGEX.test(arg)) : []) ?? [];
248273

249274
if (mcpArgVariables.length === 0) {
250275
return;
@@ -456,7 +481,6 @@ async function configYamlToContinueConfig(options: {
456481
id: server.name,
457482
name: server.name,
458483
transport: {
459-
type: "stdio",
460484
args: [],
461485
...server,
462486
},

docs/docs/customize/deep-dives/mcp.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,15 @@ To set up your own MCP server, read the [MCP quickstart](https://modelcontextpro
2020
```yaml title="config.yaml"
2121
mcpServers:
2222
- name: My MCP Server
23+
type: stdio
2324
command: uvx
2425
args:
2526
- mcp-server-sqlite
2627
- --db-path
2728
- /Users/NAME/test.db
29+
- name: My MCP Server with SSE
30+
type: sse
31+
url: "https://example.com/mcp-server/sse"
2832
```
2933
</TabItem>
3034
<TabItem value="json" label="JSON">
@@ -38,6 +42,12 @@ To set up your own MCP server, read the [MCP quickstart](https://modelcontextpro
3842
"command": "uvx",
3943
"args": ["mcp-server-sqlite", "--db-path", "/Users/NAME/test.db"]
4044
}
45+
},
46+
{
47+
"transport": {
48+
"type": "sse",
49+
"url": "https://example.com/mcp-server/sse"
50+
}
4151
}
4252
]
4353
}

docs/docs/reference.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,21 +379,33 @@ prompts, context, and tool use. Continue supports any MCP server with the MCP co
379379
**Properties:**
380380

381381
- `name` (**required**): The name of the MCP server.
382+
- `type"` (**required**): The type of the MCP server. Can be "stdio", "sse", or "websocket".
383+
384+
**Stdio type**
385+
382386
- `command` (**required**): The command used to start the server.
383387
- `args`: An optional array of arguments for the command.
384388
- `env`: An optional map of environment variables for the server process.
385389
- `connectionTimeout`: An optional connection timeout number to the server in milliseconds.
386390

391+
**SSE or Websocket type**
392+
393+
- `url` (**required**): The URL of the MCP server.
394+
387395
**Example:**
388396

389397
```yaml title="config.yaml"
390398
mcpServers:
391-
- name: My MCP Server
399+
- name: My MCP Server with stdio
400+
type: stdio
392401
command: uvx
393402
args:
394403
- mcp-server-sqlite
395404
- --db-path
396405
- /Users/NAME/test.db
406+
- name: My MCP Server with SSE
407+
type: sse
408+
url: "https://example.com/mcp-server/sse"
397409
```
398410

399411
### `data`

packages/config-yaml/src/converter.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,48 @@ function convertCustomCommand(
128128

129129
function convertMcp(mcp: any): NonNullable<ConfigYaml["mcpServers"]>[number] {
130130
const { transport } = mcp;
131-
const { command, args, env, server_name } = transport;
131+
const { type } = transport;
132132

133-
return {
134-
command,
135-
args,
136-
env,
137-
name: server_name || "MCP Server",
133+
// Common properties for all server types
134+
const baseServer: any = {
135+
name: mcp.name,
136+
type: type,
138137
};
138+
139+
if (mcp.faviconUrl) {
140+
baseServer.faviconUrl = mcp.faviconUrl
141+
}
142+
if (mcp.connectionTimeout) {
143+
baseServer.connectionTimeout = mcp.connectionTimeout
144+
}
145+
146+
// Type-specific properties
147+
switch (type) {
148+
case "stdio":
149+
const stdioServer = {
150+
...baseServer,
151+
command: transport.command,
152+
};
153+
154+
if (transport.args) {
155+
stdioServer.args = transport.args
156+
}
157+
if (transport.env) {
158+
stdioServer.env = transport.env
159+
}
160+
161+
return stdioServer;
162+
163+
case "sse":
164+
case "websocket":
165+
return {
166+
...baseServer,
167+
url: transport.url
168+
};
169+
170+
default:
171+
throw new Error(`Unknown MCP server type: ${type}`);
172+
}
139173
}
140174

141175
function convertDoc(

packages/config-yaml/src/schemas/index.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,31 @@ export const contextSchema = z.object({
99
params: z.any().optional(),
1010
});
1111

12-
const mcpServerSchema = z.object({
13-
name: z.string(),
14-
command: z.string(),
15-
faviconUrl: z.string().optional(),
16-
args: z.array(z.string()).optional(),
17-
env: z.record(z.string()).optional(),
18-
connectionTimeout: z.number().gt(0).optional()
19-
});
12+
const mcpServerSchema = z.discriminatedUnion("type", [
13+
z.object({
14+
name: z.string(),
15+
type: z.literal("stdio"),
16+
command: z.string(),
17+
faviconUrl: z.string().optional(),
18+
args: z.array(z.string()).optional(),
19+
env: z.record(z.string()).optional(),
20+
connectionTimeout: z.number().gt(0).optional()
21+
}),
22+
z.object({
23+
name: z.string(),
24+
type: z.literal("sse"),
25+
url: z.string(),
26+
faviconUrl: z.string().optional(),
27+
connectionTimeout: z.number().gt(0).optional()
28+
}),
29+
z.object({
30+
name: z.string(),
31+
type: z.literal("websocket"),
32+
url: z.string(),
33+
faviconUrl: z.string().optional(),
34+
connectionTimeout: z.number().gt(0).optional()
35+
})
36+
]);
2037

2138
export type MCPServer = z.infer<typeof mcpServerSchema>;
2239

@@ -61,7 +78,17 @@ export const blockItemWrapperSchema = <T extends z.AnyZodObject>(
6178
export const blockOrSchema = <T extends z.AnyZodObject>(
6279
schema: T,
6380
usesSchema: z.ZodTypeAny = defaultUsesSchema,
64-
) => z.union([schema, blockItemWrapperSchema(schema, usesSchema)]);
81+
isDiscriminatedUnion?: boolean,
82+
) => {
83+
if (isDiscriminatedUnion) {
84+
return z.union([schema, z.object({
85+
uses: usesSchema,
86+
with: z.record(z.string()).optional(),
87+
override: z.any().optional(),
88+
})]);
89+
}
90+
return z.union([schema, blockItemWrapperSchema(schema, usesSchema)]);
91+
};
6592

6693
export const commonMetadataSchema = z.object({
6794
tags: z.string().optional(),
@@ -98,7 +125,7 @@ export const configYamlSchema = baseConfigYamlSchema.extend({
98125
.optional(),
99126
context: z.array(blockOrSchema(contextSchema)).optional(),
100127
data: z.array(blockOrSchema(dataSchema)).optional(),
101-
mcpServers: z.array(blockOrSchema(mcpServerSchema)).optional(),
128+
mcpServers: z.array(blockOrSchema(mcpServerSchema as any, defaultUsesSchema, true)).optional(),
102129
rules: z
103130
.array(
104131
z.union([
@@ -223,4 +250,4 @@ export const configSchema = z.object({
223250
api_key: z.string().optional(),
224251
});
225252

226-
export type Config = z.infer<typeof configSchema>;
253+
export type Config = z.infer<typeof configSchema>;

0 commit comments

Comments
 (0)