diff --git a/packages/ui/src/components/sections/mcp/mcpImport.test.ts b/packages/ui/src/components/sections/mcp/mcpImport.test.ts new file mode 100644 index 00000000..fca4965a --- /dev/null +++ b/packages/ui/src/components/sections/mcp/mcpImport.test.ts @@ -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'); + }); +}); diff --git a/packages/ui/src/components/sections/mcp/mcpImport.ts b/packages/ui/src/components/sections/mcp/mcpImport.ts index 498a6b0a..afdacd49 100644 --- a/packages/ui/src/components/sections/mcp/mcpImport.ts +++ b/packages/ui/src/components/sections/mcp/mcpImport.ts @@ -221,6 +221,7 @@ function isServerConfig(val: Record): boolean { * * Supported shapes: * { "mcpServers": { "name": { ... } } } + * { "mcp": { "name": { ... } } } * { "name": { ... } } * { ...serverConfig } */ @@ -270,6 +271,29 @@ export function parseImportedMcpSnippet( return buildResult(serverName, inferType(entry as Record), entry as Record); } + // 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), entry as Record); + } + // Detect single named entry { "serverName": { ... } } const single = extractSingleServer(obj); if (single) { @@ -336,4 +360,4 @@ export function applyImportedMcpToDraft( }; return draft; -} \ No newline at end of file +}