Skip to content

Commit 669fb03

Browse files
04cfb1edCamilo
authored and
Camilo
committed
feat: add support for multiple MCP server types (stdio, sse, websocket) in the YAML configuration schema, ensuring backward compatibility with legacy configurations
1 parent 8665a08 commit 669fb03

File tree

7 files changed

+245
-24
lines changed

7 files changed

+245
-24
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: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,41 @@ 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-
},
99+
transport: transportConfig,
75100
timeout: server.connectionTimeout
76101
};
77102
}
@@ -441,7 +466,6 @@ async function configYamlToContinueConfig(options: {
441466
id: server.name,
442467
name: server.name,
443468
transport: {
444-
type: "stdio",
445469
args: [],
446470
...server,
447471
},

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
@@ -378,21 +378,33 @@ prompts, context, and tool use. Continue supports any MCP server with the MCP co
378378
**Properties:**
379379

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

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

388396
```yaml title="config.yaml"
389397
mcpServers:
390-
- name: My MCP Server
398+
- name: My MCP Server with stdio
399+
type: stdio
391400
command: uvx
392401
args:
393402
- mcp-server-sqlite
394403
- --db-path
395404
- /Users/NAME/test.db
405+
- name: My MCP Server with SSE
406+
type: sse
407+
url: "https://example.com/mcp-server/sse"
396408
```
397409

398410
### `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: 37 additions & 9 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

@@ -63,6 +80,17 @@ export const blockOrSchema = <T extends z.AnyZodObject>(
6380
usesSchema: z.ZodTypeAny = defaultUsesSchema,
6481
) => z.union([schema, blockItemWrapperSchema(schema, usesSchema)]);
6582

83+
export const blockOrDiscriminatedUnionSchema = <T extends z.ZodType>(
84+
schema: T,
85+
usesSchema: z.ZodTypeAny = defaultUsesSchema,
86+
) => z.union([schema, z.object({
87+
uses: usesSchema,
88+
with: z.record(z.string()).optional(),
89+
// For a discriminated union, we can't easily create a partial version
90+
// So we'll use any for the override
91+
override: z.any().optional(),
92+
})]);
93+
6694
export const commonMetadataSchema = z.object({
6795
tags: z.string().optional(),
6896
sourceCodeUrl: z.string().optional(),
@@ -98,7 +126,7 @@ export const configYamlSchema = baseConfigYamlSchema.extend({
98126
.optional(),
99127
context: z.array(blockOrSchema(contextSchema)).optional(),
100128
data: z.array(blockOrSchema(dataSchema)).optional(),
101-
mcpServers: z.array(blockOrSchema(mcpServerSchema)).optional(),
129+
mcpServers: z.array(blockOrDiscriminatedUnionSchema(mcpServerSchema)).optional(),
102130
rules: z
103131
.array(
104132
z.union([

0 commit comments

Comments
 (0)