Download scripts/setup-mcp.js from Cnass/Claude: direct link, hf CLI and curl.
- Browser
- Download file 11.3 kB
-
https://huggingface.co/spaces/Cnass/Claude/resolve/main/scripts/setup-mcp.js
- Command line
-
hf download hf://spaces/Cnass/Claude/scripts/setup-mcp.js
-
curl -L -o setup-mcp.js https://huggingface.co/spaces/Cnass/Claude/resolve/main/scripts/setup-mcp.js
11.3 kB
| // Makes Agent Web's MCP servers available, configured the same way, in every | |
| // session. Claude Code reads MCP servers from ~/.claude.json: the top-level | |
| // "mcpServers" (user scope) applies to every folder, while entries under | |
| // "projects[<folder>].mcpServers" (local scope, the default of `claude mcp add`) | |
| // exist only in that one folder -- which is why a server added in one session | |
| // was missing in another. Runs at container start, before any Claude process, | |
| // so nothing else is writing the file. | |
| ; | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const { tempPathOf } = require(path.join(__dirname, 'temp-paths.js')); | |
| // playwright: Google Chrome is installed in the image (see Dockerfile). The | |
| // container has no user namespaces, so the browser runs without Chrome's own | |
| // sandbox (Playwright MCP enables it by default for the chrome channel), and | |
| // headless so it never depends on the virtual screen. --isolated keeps each | |
| // session's browser profile in memory, so parallel sessions never fight over | |
| // one profile directory. | |
| // | |
| // agentweb: Agent Web's own tools (share_file, virtual screen control). | |
| // | |
| // github: GitHub's remote MCP server. It has no OAuth client registration for | |
| // other apps, so a sign-in from Claude Code can never complete; it takes a | |
| // token instead. The headersHelper reads one from GH_TOKEN / GITHUB_TOKEN or | |
| // from `gh auth login` each time Claude connects, so no token is written here. | |
| // An entry that already authenticates on its own (its own Authorization | |
| // header, helper or OAuth client) is kept as it is. | |
| // | |
| // `requires` names a file the entry needs; entries whose file is missing are | |
| // left out. | |
| const MANAGED = { | |
| playwright: { | |
| entry: { | |
| type: 'stdio', | |
| command: 'playwright-mcp', | |
| args: ['--browser=chrome', '--headless', '--isolated', '--no-sandbox'], | |
| env: {}, | |
| }, | |
| detect: /@playwright\/mcp|playwright-mcp|mcp-server-playwright/, | |
| }, | |
| agentweb: { | |
| entry: { | |
| type: 'stdio', | |
| command: 'node', | |
| args: ['/app/scripts/agentweb-mcp.js'], | |
| env: {}, | |
| }, | |
| detect: /agentweb-mcp/, | |
| requires: '/app/scripts/agentweb-mcp.js', | |
| }, | |
| github: { | |
| entry: { | |
| type: 'http', | |
| url: 'https://api.githubcopilot.com/mcp/', | |
| headersHelper: '/app/scripts/github-mcp-headers', | |
| }, | |
| detect: /^https:\/\/api\.githubcopilot\.com\//, | |
| keep: authenticatesOnItsOwn, | |
| requires: '/app/scripts/github-mcp-headers', | |
| }, | |
| }; | |
| // Servers earlier versions added and no longer ship. Removed while they are | |
| // still what was written (or point at a wrapper that no longer exists). | |
| const RETIRED = { | |
| // Roblox Studio MCP (Roblox Studio through Vinegar is gone). | |
| robloxstudio: { brokenIf: (entry) => entry && entry.command === '/app/scripts/roblox-mcp' }, | |
| }; | |
| function authenticatesOnItsOwn(entry) { | |
| if (!entry || typeof entry !== 'object') return false; | |
| if (typeof entry.headersHelper === 'string' && entry.headersHelper) return true; | |
| if (entry.oauth && typeof entry.oauth === 'object' && entry.oauth.clientId) return true; | |
| const headers = entry.headers && typeof entry.headers === 'object' ? entry.headers : {}; | |
| return Object.keys(headers).some((key) => /^authorization$/i.test(key)); | |
| } | |
| function defaultManaged() { | |
| const out = {}; | |
| for (const [name, spec] of Object.entries(MANAGED)) { | |
| if (spec.requires && !fs.existsSync(spec.requires)) continue; | |
| out[name] = spec; | |
| } | |
| return out; | |
| } | |
| function log(message) { | |
| console.log(`[setup-mcp] ${message}`); | |
| } | |
| // Key order must not matter when comparing entries Claude Code rewrote. | |
| function canonical(value) { | |
| if (Array.isArray(value)) return value.map(canonical); | |
| if (value && typeof value === 'object') { | |
| const out = {}; | |
| for (const key of Object.keys(value).sort()) out[key] = canonical(value[key]); | |
| return out; | |
| } | |
| return value; | |
| } | |
| function same(a, b) { | |
| return JSON.stringify(canonical(a)) === JSON.stringify(canonical(b)); | |
| } | |
| // A hand-made entry for the same server (e.g. from `claude mcp add`). | |
| function isSameServer(entry, detect) { | |
| if (!entry || typeof entry !== 'object') return false; | |
| const parts = [entry.command, entry.url, ...(Array.isArray(entry.args) ? entry.args : [])]; | |
| return parts.some((part) => typeof part === 'string' && detect.test(part)); | |
| } | |
| function readState(file) { | |
| try { | |
| const state = JSON.parse(fs.readFileSync(file, 'utf8')); | |
| if (state && typeof state.servers === 'object' && state.servers) return state; | |
| } catch { | |
| // ilk calisma | |
| } | |
| return { servers: {} }; | |
| } | |
| function writeFileAtomic(file, text, mode) { | |
| const tmp = `${file}.agentweb-tmp`; | |
| fs.writeFileSync(tmp, text, { mode }); | |
| fs.renameSync(tmp, file); | |
| fs.chmodSync(file, mode); | |
| } | |
| // MCP servers running from /tmp lose their files on every restart; say so in | |
| // the log (the MCP screen offers a permanent install). | |
| function reportTempServers(config) { | |
| const lists = [['tum oturumlar', config.mcpServers]]; | |
| const projects = config.projects && typeof config.projects === 'object' ? config.projects : {}; | |
| for (const [folder, project] of Object.entries(projects)) { | |
| if (project && project.mcpServers && typeof project.mcpServers === 'object') lists.push([folder, project.mcpServers]); | |
| } | |
| for (const [where, servers] of lists) { | |
| for (const [name, entry] of Object.entries(servers || {})) { | |
| const temp = tempPathOf(entry); | |
| if (!temp) continue; | |
| const gone = fs.existsSync(temp) ? '' : '; dosyalari silinmis'; | |
| log(`UYARI: "${name}" (${where}) ${temp} icinden calisiyor: /tmp her yeniden baslatmada silinir${gone}. MCP ekranindaki "Kalici kur" ile Claude'a kalici klasore kurdurabilirsin.`); | |
| } | |
| } | |
| } | |
| function apply(home, managed = defaultManaged()) { | |
| const configFile = path.join(home, '.claude.json'); | |
| const stateFile = path.join(home, '.cc-web', 'managed-mcp.json'); | |
| let config = {}; | |
| let mode = 0o600; | |
| if (fs.existsSync(configFile)) { | |
| try { | |
| config = JSON.parse(fs.readFileSync(configFile, 'utf8')); | |
| mode = fs.statSync(configFile).mode & 0o777; | |
| } catch (err) { | |
| log(`~/.claude.json okunamadi (${err.message}); dokunulmadi.`); | |
| return; | |
| } | |
| if (!config || typeof config !== 'object' || Array.isArray(config)) { | |
| log('~/.claude.json beklenmeyen bicimde; dokunulmadi.'); | |
| return; | |
| } | |
| } | |
| if (!config.mcpServers || typeof config.mcpServers !== 'object') config.mcpServers = {}; | |
| reportTempServers(config); | |
| const state = readState(stateFile); | |
| let configChanged = false; | |
| let stateChanged = false; | |
| for (const [name, spec] of Object.entries(managed)) { | |
| const wanted = spec.entry; | |
| const current = config.mcpServers[name]; | |
| const previous = state.servers[name]; | |
| const keep = typeof spec.keep === 'function' ? spec.keep : () => false; | |
| if (previous === undefined) { | |
| // First run: take over the name. A hand-made entry for the same server | |
| // (user or per-folder scope) is replaced so every session behaves the same. | |
| if (current !== undefined && !isSameServer(current, spec.detect)) { | |
| log(`"${name}" adinda baska bir sunucu tanimli; dokunulmadi.`); | |
| continue; | |
| } | |
| if (current !== undefined && keep(current)) { | |
| // Works on its own (e.g. a token in its own header): leave it, and | |
| // from now on treat it as the user's. | |
| log(`"${name}" icin kendi ayarin korunuyor.`); | |
| state.servers[name] = { userOwned: true }; | |
| stateChanged = true; | |
| continue; | |
| } | |
| if (!same(current, wanted)) { | |
| config.mcpServers[name] = wanted; | |
| configChanged = true; | |
| log(`"${name}" sunucusu tum oturumlar icin (kullanici kapsami) tanimlandi.`); | |
| } | |
| const projects = config.projects && typeof config.projects === 'object' ? config.projects : {}; | |
| for (const [folder, project] of Object.entries(projects)) { | |
| const servers = project && project.mcpServers; | |
| if (servers && isSameServer(servers[name], spec.detect) && !keep(servers[name])) { | |
| delete servers[name]; | |
| configChanged = true; | |
| log(`${folder} klasorune ozel eski "${name}" tanimi kaldirildi.`); | |
| } | |
| } | |
| state.servers[name] = wanted; | |
| stateChanged = true; | |
| } else if (current === undefined) { | |
| // Removed on purpose (claude mcp remove); respect that. | |
| } else if (same(current, previous)) { | |
| // Still exactly what we wrote last time: move it to the current default. | |
| if (!same(current, wanted)) { | |
| config.mcpServers[name] = wanted; | |
| configChanged = true; | |
| log(`"${name}" sunucusunun varsayilan ayarlari guncellendi.`); | |
| } | |
| if (!same(previous, wanted)) { | |
| state.servers[name] = wanted; | |
| stateChanged = true; | |
| } | |
| } | |
| // Otherwise the user customised the entry; it stays as it is. | |
| } | |
| for (const [name, spec] of Object.entries(RETIRED)) { | |
| const previous = state.servers[name]; | |
| if (previous === undefined) continue; | |
| const current = config.mcpServers[name]; | |
| if (current !== undefined) { | |
| if (same(current, previous) || spec.brokenIf(current)) { | |
| delete config.mcpServers[name]; | |
| configChanged = true; | |
| log(`"${name}" sunucusu kaldirildi (artik Agent Web'de yok).`); | |
| } else { | |
| log(`"${name}" elle degistirilmis; dokunulmadi.`); | |
| } | |
| } | |
| delete state.servers[name]; | |
| stateChanged = true; | |
| } | |
| if (configChanged) { | |
| const backup = `${configFile}.agentweb-backup`; | |
| if (fs.existsSync(configFile) && !fs.existsSync(backup)) fs.copyFileSync(configFile, backup); | |
| writeFileAtomic(configFile, `${JSON.stringify(config, null, 2)}\n`, mode); | |
| } | |
| if (stateChanged) { | |
| fs.mkdirSync(path.dirname(stateFile), { recursive: true }); | |
| writeFileAtomic(stateFile, `${JSON.stringify(state, null, 2)}\n`, 0o600); | |
| } | |
| } | |
| // Brings back a ready-made server that was removed on purpose (from the MCP | |
| // screen or with claude mcp remove): | |
| // node /app/scripts/setup-mcp.js --restore playwright | |
| function restore(home, name, managed = defaultManaged()) { | |
| if (!Object.prototype.hasOwnProperty.call(managed, name)) { | |
| log(`"${name}" hazir sunuculardan biri degil (${Object.keys(managed).join(', ')}).`); | |
| return false; | |
| } | |
| const stateFile = path.join(home, '.cc-web', 'managed-mcp.json'); | |
| const state = readState(stateFile); | |
| if (state.servers[name] !== undefined) { | |
| delete state.servers[name]; | |
| fs.mkdirSync(path.dirname(stateFile), { recursive: true }); | |
| writeFileAtomic(stateFile, `${JSON.stringify(state, null, 2)}\n`, 0o600); | |
| } | |
| apply(home, { [name]: managed[name] }); | |
| return true; | |
| } | |
| if (require.main === module) { | |
| const args = process.argv.slice(2); | |
| if (!process.env.HOME) { | |
| log('HOME tanimli degil; atlandi.'); | |
| } else if (args[0] === '--restore') { | |
| if (!args[1]) { | |
| log(`Kullanim: setup-mcp.js --restore <${Object.keys(defaultManaged()).join('|')}>`); | |
| process.exitCode = 2; | |
| } else if (restore(process.env.HOME, args[1])) { | |
| log("Agent Web'de MCP sunuculari ekranindaki \"Claude'u yeniden baslat\" ile yuklenir."); | |
| } else { | |
| process.exitCode = 1; | |
| } | |
| } else { | |
| apply(process.env.HOME); | |
| } | |
| } | |
| module.exports = { apply, restore, MANAGED, RETIRED }; | |