Files
memind/mindspace-sandbox-mcp.mjs
T
Your Name b0f5d6a51c Extract memind_adm admin server, add local dev tooling, and remove image-generation.
Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 16:39:39 -07:00

214 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// Stdio MCP server: sandboxes file operations to SANDBOX_ROOT.
// Used as a goosed extension to replace the built-in developer extension for
// regular users — enforces path boundaries at the OS level rather than relying
// on AI model compliance with text constraints.
//
// SANDBOX_ROOT is passed as argv[2] (primary) or SANDBOX_ROOT env var (fallback).
// argv[2] is preferred because some goosed versions don't forward env to stdio MCPs.
// Optional env: ALLOWED_TOOLS — comma-separated tool whitelist (default: all tools)
import path from 'node:path';
import fs from 'node:fs';
import readline from 'node:readline';
const SANDBOX_ROOT = process.argv[2]?.trim() || process.env.SANDBOX_ROOT?.trim();
if (!SANDBOX_ROOT) {
process.stderr.write('[mindspace-sandbox-mcp] SANDBOX_ROOT is not set — refusing to start\n');
process.exit(1);
}
const SANDBOX = path.resolve(SANDBOX_ROOT);
const allowedToolsEnv = process.env.ALLOWED_TOOLS?.trim();
const ALLOWED_TOOLS = allowedToolsEnv ? new Set(allowedToolsEnv.split(',').map((s) => s.trim())) : null;
/** Resolve and validate that the path is inside SANDBOX. Returns absolute path. */
function resolveSandboxed(p) {
if (!p || typeof p !== 'string') throw new Error('路径参数无效');
const resolved = path.isAbsolute(p) ? path.resolve(p) : path.resolve(SANDBOX, p);
if (resolved !== SANDBOX && !resolved.startsWith(SANDBOX + path.sep)) {
throw Object.assign(new Error(`路径越界:${p} 不在当前工作区内`), { code: 'EACCES' });
}
return resolved;
}
const ALL_TOOLS = [
{
name: 'read_file',
description: '读取文件内容(仅限工作区内的文件)',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: '文件路径(相对工作区或绝对路径)' },
},
required: ['path'],
},
},
{
name: 'write_file',
description: '写入文件内容(仅限工作区内的文件;不存在则创建,已存在则覆盖)',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: '文件路径' },
content: { type: 'string', description: '文件内容' },
},
required: ['path', 'content'],
},
},
{
name: 'edit_file',
description: '将文件中的旧内容替换为新内容(仅限工作区内的文件)',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: '文件路径' },
old_str: { type: 'string', description: '要替换的原始内容(必须在文件中唯一)' },
new_str: { type: 'string', description: '替换后的新内容' },
},
required: ['path', 'old_str', 'new_str'],
},
},
{
name: 'list_dir',
description: '列出目录内容(仅限工作区内的目录)',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: '目录路径(省略则列出工作区根目录)',
},
},
},
},
{
name: 'create_dir',
description: '创建目录(仅限工作区内;已存在不报错)',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: '目录路径' },
},
required: ['path'],
},
},
];
const TOOLS = ALLOWED_TOOLS ? ALL_TOOLS.filter((t) => ALLOWED_TOOLS.has(t.name)) : ALL_TOOLS;
function callTool(name, args) {
if (ALLOWED_TOOLS && !ALLOWED_TOOLS.has(name)) {
throw new Error(`工具 ${name} 未授权`);
}
switch (name) {
case 'read_file': {
const abs = resolveSandboxed(args.path);
const content = fs.readFileSync(abs, 'utf8');
return [{ type: 'text', text: content }];
}
case 'write_file': {
const abs = resolveSandboxed(args.path);
fs.mkdirSync(path.dirname(abs), { recursive: true });
fs.writeFileSync(abs, args.content ?? '', 'utf8');
return [{ type: 'text', text: `已写入 ${args.path}${(args.content ?? '').length} 字节)` }];
}
case 'edit_file': {
const abs = resolveSandboxed(args.path);
const original = fs.readFileSync(abs, 'utf8');
const oldStr = args.old_str ?? '';
if (oldStr && !original.includes(oldStr)) {
throw new Error('edit_file: old_str 在文件中不存在,替换失败');
}
const updated = oldStr ? original.replace(oldStr, args.new_str ?? '') : (args.new_str ?? '');
fs.writeFileSync(abs, updated, 'utf8');
return [{ type: 'text', text: `已编辑 ${args.path}` }];
}
case 'list_dir': {
const target = args?.path ?? '.';
const abs = resolveSandboxed(target);
const entries = fs.readdirSync(abs, { withFileTypes: true });
const lines = entries.map((e) => `${e.isDirectory() ? '[目录]' : '[文件]'} ${e.name}`);
return [{ type: 'text', text: lines.join('\n') || '(空目录)' }];
}
case 'create_dir': {
const abs = resolveSandboxed(args.path);
fs.mkdirSync(abs, { recursive: true });
return [{ type: 'text', text: `已创建目录 ${args.path}` }];
}
default:
throw new Error(`未知工具:${name}`);
}
}
// ── JSON-RPC over stdio ────────────────────────────────────────────────────
function send(obj) {
process.stdout.write(JSON.stringify(obj) + '\n');
}
function respond(id, result) {
send({ jsonrpc: '2.0', id, result });
}
function respondError(id, message, code = -32603) {
send({ jsonrpc: '2.0', id, error: { code, message } });
}
const rl = readline.createInterface({ input: process.stdin, terminal: false });
rl.on('line', (raw) => {
const line = raw.trim();
if (!line) return;
let msg;
try {
msg = JSON.parse(line);
} catch {
process.stderr.write(`[mindspace-sandbox-mcp] invalid JSON: ${line}\n`);
return;
}
const { id, method, params } = msg;
switch (method) {
case 'initialize':
respond(id, {
protocolVersion: '2024-11-05',
serverInfo: { name: 'mindspace-sandbox', version: '1.0.0' },
capabilities: { tools: {} },
});
break;
case 'initialized':
// notification — no response
break;
case 'tools/list':
respond(id, { tools: TOOLS });
break;
case 'tools/call': {
const { name, arguments: toolArgs } = params ?? {};
try {
const content = callTool(name, toolArgs ?? {});
respond(id, { content, isError: false });
} catch (err) {
respond(id, {
content: [{ type: 'text', text: err.message }],
isError: true,
});
}
break;
}
default:
if (id !== undefined && id !== null) {
respondError(id, `Method not found: ${method}`, -32601);
}
}
});
rl.on('close', () => process.exit(0));