fix(mcp): import opencode mcp config snippets (#1532)
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { parseImportedMcpSnippet } from './mcpImport';
|
||||
|
||||
describe('parseImportedMcpSnippet', () => {
|
||||
test('imports OpenCode mcp wrapper config', () => {
|
||||
const result = parseImportedMcpSnippet(JSON.stringify({
|
||||
$schema: 'https://opencode.ai/config.json',
|
||||
mcp: {
|
||||
stitch: {
|
||||
type: 'remote',
|
||||
url: 'https://stitch.googleapis.com/mcp',
|
||||
enabled: true,
|
||||
headers: {
|
||||
'X-Goog-Api-Key': 'test-key',
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
expect(result.name).toBe('stitch');
|
||||
expect(result.type).toBe('remote');
|
||||
expect(result.url).toBe('https://stitch.googleapis.com/mcp');
|
||||
expect(result.enabled).toBe(true);
|
||||
expect(result.headers).toEqual([{ key: 'X-Goog-Api-Key', value: 'test-key' }]);
|
||||
});
|
||||
|
||||
test('keeps existing mcpServers wrapper support', () => {
|
||||
const result = parseImportedMcpSnippet(JSON.stringify({
|
||||
mcpServers: {
|
||||
localTool: {
|
||||
command: 'node server.js',
|
||||
args: ['--stdio'],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
expect(result.name).toBe('localTool');
|
||||
expect(result.type).toBe('local');
|
||||
expect(result.command).toEqual(['node', 'server.js', '--stdio']);
|
||||
});
|
||||
|
||||
test('rejects multiple OpenCode mcp wrapper entries', () => {
|
||||
const result = parseImportedMcpSnippet(JSON.stringify({
|
||||
mcp: {
|
||||
one: { type: 'remote', url: 'https://one.example/mcp' },
|
||||
two: { type: 'remote', url: 'https://two.example/mcp' },
|
||||
},
|
||||
}));
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error('Expected import to fail');
|
||||
expect(result.error).toContain('Paste one server at a time');
|
||||
expect(result.error).toContain('servers in mcp');
|
||||
});
|
||||
});
|
||||
@@ -221,6 +221,7 @@ function isServerConfig(val: Record<string, unknown>): boolean {
|
||||
*
|
||||
* Supported shapes:
|
||||
* { "mcpServers": { "name": { ... } } }
|
||||
* { "mcp": { "name": { ... } } }
|
||||
* { "name": { ... } }
|
||||
* { ...serverConfig }
|
||||
*/
|
||||
@@ -270,6 +271,29 @@ export function parseImportedMcpSnippet(
|
||||
return buildResult(serverName, inferType(entry as Record<string, unknown>), entry as Record<string, unknown>);
|
||||
}
|
||||
|
||||
// Detect OpenCode config shape { "mcp": { "name": { ... } } }
|
||||
const mcp = obj.mcp;
|
||||
if (isObject(mcp)) {
|
||||
const keys = Object.keys(mcp);
|
||||
if (keys.length === 0) {
|
||||
return buildError('mcp object is empty', parsed);
|
||||
}
|
||||
if (keys.length > 1) {
|
||||
return buildError(
|
||||
'Paste one server at a time. Found ' +
|
||||
keys.length +
|
||||
' servers in mcp',
|
||||
parsed,
|
||||
);
|
||||
}
|
||||
const serverName = keys[0]!;
|
||||
const entry = mcp[serverName];
|
||||
if (!isObject(entry)) {
|
||||
return buildError('Server entry in mcp is not a valid object', parsed);
|
||||
}
|
||||
return buildResult(serverName, inferType(entry as Record<string, unknown>), entry as Record<string, unknown>);
|
||||
}
|
||||
|
||||
// Detect single named entry { "serverName": { ... } }
|
||||
const single = extractSingleServer(obj);
|
||||
if (single) {
|
||||
@@ -336,4 +360,4 @@ export function applyImportedMcpToDraft(
|
||||
};
|
||||
|
||||
return draft;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user