{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "nextjs-mcp-opencode", "title": "Next.js MCP (OpenCode)", "description": "Merge the Next.js DevTools MCP server into opencode.json/jsonc.", "files": [ { "path": "registry/nextjs-mcp/opencode/files/bin/merge-mcp.mjs", "content": "#!/usr/bin/env node\n\n// scripts/merge-mcp.ts\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nvar args = process.argv.slice(2);\nvar options = {\n keep: false,\n force: false\n};\nvar payloadArgs = [];\nfor (const arg of args) {\n if (arg === \"--keep\") {\n options.keep = true;\n continue;\n }\n if (arg === \"--force\") {\n options.force = true;\n continue;\n }\n if (arg.startsWith(\"-\")) {\n console.error(`Unknown option: ${arg}`);\n process.exit(1);\n }\n payloadArgs.push(arg);\n}\nfunction detectEol(text) {\n return text.includes(`\\r\n`) ? `\\r\n` : `\n`;\n}\nfunction stripJsonComments(input) {\n let out = \"\";\n let inString = false;\n let stringChar = \"\";\n let escaped = false;\n for (let i = 0;i < input.length; i += 1) {\n const char = input[i];\n const next = input[i + 1];\n if (inString) {\n out += char;\n if (escaped) {\n escaped = false;\n } else if (char === \"\\\\\") {\n escaped = true;\n } else if (char === stringChar) {\n inString = false;\n }\n continue;\n }\n if (char === '\"' || char === \"'\") {\n inString = true;\n stringChar = char;\n out += char;\n continue;\n }\n if (char === \"/\" && next === \"/\") {\n while (i < input.length && input[i] !== `\n`) {\n i += 1;\n }\n out += `\n`;\n continue;\n }\n if (char === \"/\" && next === \"*\") {\n i += 2;\n while (i < input.length && !(input[i] === \"*\" && input[i + 1] === \"/\")) {\n i += 1;\n }\n i += 1;\n continue;\n }\n out += char;\n }\n return out;\n}\nfunction stripTrailingCommas(input) {\n let out = \"\";\n let inString = false;\n let stringChar = \"\";\n let escaped = false;\n for (let i = 0;i < input.length; i += 1) {\n const char = input[i];\n if (inString) {\n out += char;\n if (escaped) {\n escaped = false;\n } else if (char === \"\\\\\") {\n escaped = true;\n } else if (char === stringChar) {\n inString = false;\n }\n continue;\n }\n if (char === '\"' || char === \"'\") {\n inString = true;\n stringChar = char;\n out += char;\n continue;\n }\n if (char === \",\") {\n let j = i + 1;\n while (j < input.length && /\\s/.test(input[j] ?? \"\")) {\n j += 1;\n }\n if (j < input.length && (input[j] === \"}\" || input[j] === \"]\")) {\n continue;\n }\n }\n out += char;\n }\n return out;\n}\nfunction parseJsonc(text, label) {\n const sanitized = stripTrailingCommas(stripJsonComments(text));\n let data;\n try {\n data = JSON.parse(sanitized);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown JSON parse error\";\n throw new Error(`Failed to parse ${label}: ${message}`);\n }\n if (!data || typeof data !== \"object\") {\n throw new Error(`Expected ${label} to be an object.`);\n }\n return data;\n}\nfunction readPayload(filePath) {\n const raw = fs.readFileSync(filePath, \"utf8\");\n const data = parseJsonc(raw, filePath);\n return data;\n}\nfunction resolvePayloads(cwd, explicit) {\n if (explicit.length) {\n return explicit.map((item) => path.resolve(cwd, item));\n }\n const payloadDir = path.join(cwd, \".opencode\", \"mcp\");\n if (!fs.existsSync(payloadDir)) {\n return [];\n }\n const entries = fs.readdirSync(payloadDir, { withFileTypes: true });\n return entries.filter((entry) => entry.isFile() && (entry.name.endsWith(\".json\") || entry.name.endsWith(\".jsonc\"))).map((entry) => path.join(payloadDir, entry.name));\n}\nfunction getConfigPath(cwd) {\n const jsoncPath = path.join(cwd, \"opencode.jsonc\");\n if (fs.existsSync(jsoncPath))\n return jsoncPath;\n return path.join(cwd, \"opencode.json\");\n}\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\nfunction main() {\n const cwd = process.cwd();\n const payloadFiles = resolvePayloads(cwd, payloadArgs);\n if (!payloadFiles.length) {\n console.log(\"No MCP payloads found. Nothing to merge.\");\n return;\n }\n const payloads = payloadFiles.map((filePath) => ({ filePath, payload: readPayload(filePath) }));\n const mcpEntries = [];\n for (const { filePath, payload } of payloads) {\n if (!payload.mcp || typeof payload.mcp !== \"object\") {\n console.warn(`Skipping ${filePath}: missing mcp object.`);\n continue;\n }\n for (const [name, entry] of Object.entries(payload.mcp)) {\n if (!entry || typeof entry !== \"object\") {\n console.warn(`Skipping ${filePath}: mcp.${name} is not an object.`);\n continue;\n }\n mcpEntries.push([name, entry]);\n }\n }\n if (!mcpEntries.length) {\n console.log(\"No valid MCP entries found. Nothing to merge.\");\n return;\n }\n const configPath = getConfigPath(cwd);\n const configExists = fs.existsSync(configPath);\n const configText = configExists ? fs.readFileSync(configPath, \"utf8\") : `{}\n`;\n const eol = detectEol(configText);\n const configData = parseJsonc(configText, path.basename(configPath));\n const configObject = configData;\n const existingMcp = configObject.mcp && typeof configObject.mcp === \"object\" ? configObject.mcp : {};\n const added = [];\n const skipped = [];\n const updated = [];\n for (const [name, entry] of mcpEntries) {\n const exists = Object.prototype.hasOwnProperty.call(existingMcp, name);\n if (exists && !options.force) {\n skipped.push(name);\n continue;\n }\n existingMcp[name] = entry;\n if (exists) {\n updated.push(name);\n } else {\n added.push(name);\n }\n }\n if (added.length || updated.length) {\n configObject.mcp = existingMcp;\n const normalized = JSON.stringify(configObject, null, 2);\n const output = normalized.endsWith(eol) ? normalized : `${normalized}${eol}`;\n fs.writeFileSync(configPath, output, \"utf8\");\n console.log(`Updated ${path.basename(configPath)}.`);\n } else {\n console.log(`No changes needed in ${path.basename(configPath)}.`);\n }\n if (!options.keep) {\n const payloadDir = path.join(cwd, \".opencode\", \"mcp\");\n const appliedDir = path.join(payloadDir, \".applied\");\n ensureDir(appliedDir);\n for (const filePath of payloadFiles) {\n const relative = path.relative(payloadDir, filePath);\n if (relative.startsWith(\"..\") || relative.startsWith(`.${path.sep}`)) {\n continue;\n }\n if (relative.startsWith(`.applied${path.sep}`)) {\n continue;\n }\n const baseName = path.basename(filePath);\n let target = path.join(appliedDir, baseName);\n if (fs.existsSync(target)) {\n const stamp = Date.now();\n target = path.join(appliedDir, `${baseName}.${stamp}`);\n }\n fs.renameSync(filePath, target);\n }\n }\n if (added.length) {\n console.log(`Added MCP servers: ${added.join(\", \")}`);\n }\n if (updated.length) {\n console.log(`Updated MCP servers: ${updated.join(\", \")}`);\n }\n if (skipped.length) {\n console.log(`Skipped existing MCP servers: ${skipped.join(\", \")}`);\n }\n}\ntry {\n main();\n} catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`MCP merge failed: ${message}`);\n process.exit(1);\n}\n", "type": "registry:file", "target": "~/.opencode/bin/merge-mcp.mjs" }, { "path": "registry/nextjs-mcp/opencode/files/mcp/next-devtools.json", "content": "{\n \"mcp\": {\n \"next-devtools\": {\n \"type\": \"local\",\n \"command\": [\"npx\", \"-y\", \"next-devtools-mcp@latest\"],\n \"enabled\": true\n }\n }\n}\n", "type": "registry:file", "target": "~/.opencode/mcp/next-devtools.json" } ], "meta": { "concept": "nextjs-mcp", "platform": "opencode", "kind": [ "mcp", "config", "script" ], "postInstall": "node .opencode/bin/merge-mcp.mjs" }, "docs": "After install, run `node .opencode/bin/merge-mcp.mjs` to merge the MCP config into opencode.json/jsonc. Requires Next.js 16+. The merge script archives payloads in .opencode/mcp/.applied to avoid re-adding removed MCPs. JSONC comments are stripped when writing.", "type": "registry:item" }