Skip to content

Commit b05c603

Browse files
mastermakrelajycouetmanuel3108
authored
fix(mcp): generate valid mcp local configuration for opencode (#769)
* fix local opencode mcp * having a snapshot of all versions * changeset --------- Co-authored-by: jycouet <jycouet@gmail.com> Co-authored-by: Manuel <30698007+manuel3108@users.noreply.github.com>
1 parent 5fee6a0 commit b05c603

File tree

3 files changed

+195
-17
lines changed

3 files changed

+195
-17
lines changed

.changeset/hungry-maps-poke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"sv": patch
3+
---
4+
5+
fix(mcp): generate valid `mcp` local configuration for `opencode`

packages/addons/_tests/mcp/test.ts

Lines changed: 172 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect } from '@playwright/test';
1+
import { expect } from 'vitest';
22
import { setupTest } from '../_setup/suite.ts';
33
import mcp from '../../mcp/index.ts';
44
import fs from 'node:fs';
@@ -9,10 +9,16 @@ const { test, testCases } = setupTest(
99
{
1010
kinds: [
1111
{
12-
type: 'default',
12+
type: 'default-local',
1313
options: {
1414
mcp: { ide: ['claude-code', 'cursor', 'gemini', 'opencode', 'vscode'], setup: 'local' }
1515
}
16+
},
17+
{
18+
type: 'default-remote',
19+
options: {
20+
mcp: { ide: ['claude-code', 'cursor', 'gemini', 'opencode', 'vscode'], setup: 'remote' }
21+
}
1622
}
1723
],
1824
browser: false,
@@ -39,18 +45,175 @@ const { test, testCases } = setupTest(
3945
}
4046
);
4147

42-
test.concurrent.for(testCases)('mcp $variant', (testCase, ctx) => {
48+
test.concurrent.for(testCases)('mcp $kind.type $variant', (testCase, ctx) => {
4349
const cwd = ctx.cwd(testCase);
4450

45-
const cursorPath = path.resolve(cwd, `.cursor/mcp.json`);
46-
const cursorMcpContent = fs.readFileSync(cursorPath, 'utf8');
51+
const getContent = (filePath: string) => {
52+
const cursorPath = path.resolve(cwd, filePath);
53+
return fs.readFileSync(cursorPath, 'utf8');
54+
};
55+
56+
const cursorMcpContent = getContent(`.cursor/mcp.json`);
4757

4858
// should keep other MCPs
4959
expect(cursorMcpContent).toContain(`anotherMCP`);
50-
// should have the svelte level
51-
expect(cursorMcpContent).toContain(`svelte`);
52-
// should have local conf
53-
expect(cursorMcpContent).toContain(`@sveltejs/mcp`);
5460
// should remove old svelte config
5561
expect(cursorMcpContent).not.toContain(`thing`);
62+
63+
const fullConf: Record<string, any> = {};
64+
const ides = {
65+
'claude-code': '.mcp.json',
66+
cursor: '.cursor/mcp.json',
67+
gemini: '.gemini/settings.json',
68+
opencode: 'opencode.json',
69+
vscode: '.vscode/mcp.json'
70+
} as const;
71+
72+
for (const [ide, filePath] of Object.entries(ides)) {
73+
fullConf[ide] = {
74+
filePath,
75+
content: JSON.parse(getContent(filePath))
76+
};
77+
}
78+
79+
if (testCase.kind.type === 'default-local') {
80+
expect(fullConf).toMatchInlineSnapshot(`
81+
{
82+
"claude-code": {
83+
"content": {
84+
"mcpServers": {
85+
"svelte": {
86+
"args": [
87+
"-y",
88+
"@sveltejs/mcp",
89+
],
90+
"command": "npx",
91+
"env": {},
92+
"type": "stdio",
93+
},
94+
},
95+
},
96+
"filePath": ".mcp.json",
97+
},
98+
"cursor": {
99+
"content": {
100+
"mcpServers": {
101+
"anotherMCP": {},
102+
"svelte": {
103+
"args": [
104+
"-y",
105+
"@sveltejs/mcp",
106+
],
107+
"command": "npx",
108+
},
109+
},
110+
},
111+
"filePath": ".cursor/mcp.json",
112+
},
113+
"gemini": {
114+
"content": {
115+
"mcpServers": {
116+
"svelte": {
117+
"args": [
118+
"-y",
119+
"@sveltejs/mcp",
120+
],
121+
"command": "npx",
122+
},
123+
},
124+
},
125+
"filePath": ".gemini/settings.json",
126+
},
127+
"opencode": {
128+
"content": {
129+
"$schema": "https://opencode.ai/config.json",
130+
"mcp": {
131+
"svelte": {
132+
"command": [
133+
"npx",
134+
"-y",
135+
"@sveltejs/mcp",
136+
],
137+
"type": "local",
138+
},
139+
},
140+
},
141+
"filePath": "opencode.json",
142+
},
143+
"vscode": {
144+
"content": {
145+
"servers": {
146+
"svelte": {
147+
"args": [
148+
"-y",
149+
"@sveltejs/mcp",
150+
],
151+
"command": "npx",
152+
},
153+
},
154+
},
155+
"filePath": ".vscode/mcp.json",
156+
},
157+
}
158+
`);
159+
} else if (testCase.kind.type === 'default-remote') {
160+
expect(fullConf).toMatchInlineSnapshot(`
161+
{
162+
"claude-code": {
163+
"content": {
164+
"mcpServers": {
165+
"svelte": {
166+
"type": "http",
167+
"url": "https://mcp.svelte.dev/mcp",
168+
},
169+
},
170+
},
171+
"filePath": ".mcp.json",
172+
},
173+
"cursor": {
174+
"content": {
175+
"mcpServers": {
176+
"anotherMCP": {},
177+
"svelte": {
178+
"url": "https://mcp.svelte.dev/mcp",
179+
},
180+
},
181+
},
182+
"filePath": ".cursor/mcp.json",
183+
},
184+
"gemini": {
185+
"content": {
186+
"mcpServers": {
187+
"svelte": {
188+
"url": "https://mcp.svelte.dev/mcp",
189+
},
190+
},
191+
},
192+
"filePath": ".gemini/settings.json",
193+
},
194+
"opencode": {
195+
"content": {
196+
"$schema": "https://opencode.ai/config.json",
197+
"mcp": {
198+
"svelte": {
199+
"type": "remote",
200+
"url": "https://mcp.svelte.dev/mcp",
201+
},
202+
},
203+
},
204+
"filePath": "opencode.json",
205+
},
206+
"vscode": {
207+
"content": {
208+
"servers": {
209+
"svelte": {
210+
"url": "https://mcp.svelte.dev/mcp",
211+
},
212+
},
213+
},
214+
"filePath": ".vscode/mcp.json",
215+
},
216+
}
217+
`);
218+
}
56219
});

packages/addons/mcp/index.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,19 @@ export default defineAddon({
3434
homepage: 'https://svelte.dev/docs/mcp',
3535
options,
3636
run: ({ sv, options }) => {
37-
const getLocalConfig = (o?: { type?: 'stdio' | 'local'; env?: boolean }) => {
38-
return {
37+
const getLocalConfig = (o?: {
38+
type?: 'stdio' | 'local';
39+
env?: boolean;
40+
command?: string | string[];
41+
args?: string[] | null;
42+
}) => {
43+
const config: any = {
3944
...(o?.type ? { type: o.type } : {}),
40-
command: 'npx',
41-
args: ['-y', '@sveltejs/mcp'],
42-
...(o?.env ? { env: {} } : {})
45+
command: o?.command ?? 'npx',
46+
...(o?.env ? { env: {} } : {}),
47+
...(o?.args === null ? {} : { args: o?.args ?? ['-y', '@sveltejs/mcp'] })
4348
};
49+
return config;
4450
};
4551
const getRemoteConfig = (o?: { type?: 'http' | 'remote' }) => {
4652
return {
@@ -58,6 +64,8 @@ export default defineAddon({
5864
typeLocal?: 'stdio' | 'local';
5965
typeRemote?: 'http' | 'remote';
6066
env?: boolean;
67+
command?: string | string[];
68+
args?: string[] | null;
6169
}
6270
| { other: true }
6371
> = {
@@ -78,7 +86,9 @@ export default defineAddon({
7886
mcpServersKey: 'mcp',
7987
filePath: 'opencode.json',
8088
typeLocal: 'local',
81-
typeRemote: 'remote'
89+
typeRemote: 'remote',
90+
command: ['npx', '-y', '@sveltejs/mcp'],
91+
args: null
8292
},
8393
vscode: {
8494
mcpServersKey: 'servers',
@@ -93,7 +103,7 @@ export default defineAddon({
93103
const value = configurator[ide];
94104
if ('other' in value) continue;
95105

96-
const { mcpServersKey, filePath, typeLocal, typeRemote, env, schema } = value;
106+
const { mcpServersKey, filePath, typeLocal, typeRemote, env, schema, command, args } = value;
97107
sv.file(filePath, (content) => {
98108
const { data, generateCode } = parseJson(content);
99109
if (schema) {
@@ -103,7 +113,7 @@ export default defineAddon({
103113
data[key] ??= {};
104114
data[key].svelte =
105115
options.setup === 'local'
106-
? getLocalConfig({ type: typeLocal, env })
116+
? getLocalConfig({ type: typeLocal, env, command, args })
107117
: getRemoteConfig({ type: typeRemote });
108118
return generateCode();
109119
});

0 commit comments

Comments
 (0)