#!/usr/bin/env node "use strict"; // ============================================================================= // vibn-workspace-mcp // ----------------------------------------------------------------------------- // Stdio MCP server exposing the coding-agent workspace toolkit: // - Filesystem primitives (read/write/replace/list/find/search) // - Shell execution (120s timeout, blocked-command guard) // - Authenticated git commit + push with protected-repo guard // // Each server instance is scoped to a single WORKSPACE_ROOT. To operate against // multiple workspaces, spawn multiple MCP server instances (one per workspace). // This mirrors how Goose / Claude Desktop / Cursor MCP configs work in practice. // // Config (env): // WORKSPACE_ROOT (required) — absolute path to the workspace // GITEA_API_URL (optional) — required if caller uses git_commit_and_push // GITEA_API_TOKEN (optional) — required if caller uses git_commit_and_push // GITEA_USERNAME (optional) — required if caller uses git_commit_and_push // ============================================================================= var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const path = __importStar(require("path")); const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const fileApi = __importStar(require("../tools/file-api")); const shellApi = __importStar(require("../tools/shell-api")); const gitApi = __importStar(require("../tools/git-api")); function loadConfig() { const workspaceRoot = process.env.WORKSPACE_ROOT; if (!workspaceRoot) { throw new Error('WORKSPACE_ROOT is required (absolute path to the workspace to operate on).'); } const absWorkspace = path.resolve(workspaceRoot); const giteaUrl = process.env.GITEA_API_URL; const giteaToken = process.env.GITEA_API_TOKEN; const giteaUser = process.env.GITEA_USERNAME; const gitea = giteaUrl && giteaToken && giteaUser ? { apiUrl: giteaUrl, apiToken: giteaToken, username: giteaUser } : undefined; return { workspaceRoot: absWorkspace, gitea }; } const TOOL_DEFINITIONS = [ { name: 'read_file', description: 'Read the complete content of a file in the workspace. Always read before editing.', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Relative path from workspace root (e.g. "src/index.ts")' }, }, required: ['path'], }, }, { name: 'write_file', description: 'Write complete content to a file. Creates parent directories if needed. Overwrites existing files.', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Relative path from workspace root' }, content: { type: 'string', description: 'Complete new file content' }, }, required: ['path', 'content'], }, }, { name: 'replace_in_file', description: 'Replace an exact string in a file. The old_content must match character-for-character. Read the file first.', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Relative path from workspace root' }, old_content: { type: 'string', description: 'Exact text to replace' }, new_content: { type: 'string', description: 'Replacement text' }, }, required: ['path', 'old_content', 'new_content'], }, }, { name: 'list_directory', description: 'List files and subdirectories in a directory. Directories have trailing "/".', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Relative path from workspace root. Use "." for root.' }, }, required: ['path'], }, }, { name: 'find_files', description: 'Find files matching a glob pattern in the workspace. Returns up to 200 relative paths.', inputSchema: { type: 'object', properties: { pattern: { type: 'string', description: 'Glob pattern e.g. "**/*.ts", "src/**/*.test.js"' }, }, required: ['pattern'], }, }, { name: 'search_code', description: 'Search file contents for a string using ripgrep. Returns file path, line number, and matching line.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search term (fixed-string)' }, file_extensions: { type: 'array', items: { type: 'string' }, description: 'Optional: limit to these extensions e.g. ["ts","js"]', }, }, required: ['query'], }, }, { name: 'execute_command', description: 'Run a shell command in the workspace and return stdout + stderr. 120s timeout.', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'Shell command to run' }, working_directory: { type: 'string', description: 'Optional: relative subdirectory to run in' }, }, required: ['command'], }, }, { name: 'git_commit_and_push', description: 'Stage all changes, commit with a message, and push to the remote using configured Gitea credentials. Blocks pushes to protected platform repos.', inputSchema: { type: 'object', properties: { message: { type: 'string', description: 'Commit message describing the changes made' }, }, required: ['message'], }, }, ]; async function dispatch(cfg, name, args) { switch (name) { case 'read_file': return fileApi.readFile(cfg.workspaceRoot, String(args.path)); case 'write_file': return fileApi.writeFile(cfg.workspaceRoot, String(args.path), String(args.content)); case 'replace_in_file': return fileApi.replaceInFile(cfg.workspaceRoot, String(args.path), String(args.old_content), String(args.new_content)); case 'list_directory': return fileApi.listDirectory(cfg.workspaceRoot, String(args.path)); case 'find_files': return fileApi.findFiles(cfg.workspaceRoot, String(args.pattern)); case 'search_code': { const exts = Array.isArray(args.file_extensions) ? args.file_extensions : undefined; return fileApi.searchCode(cfg.workspaceRoot, String(args.query), exts); } case 'execute_command': return shellApi.executeCommand(cfg.workspaceRoot, String(args.command), args.working_directory ? String(args.working_directory) : undefined); case 'git_commit_and_push': { if (!cfg.gitea) { return { error: 'git_commit_and_push requires GITEA_API_URL, GITEA_API_TOKEN, and GITEA_USERNAME environment variables.' }; } return gitApi.gitCommitAndPush(cfg.workspaceRoot, String(args.message), cfg.gitea); } default: throw new Error(`Unknown tool: ${name}`); } } function buildServer(cfg) { const server = new index_js_1.Server({ name: 'vibn-workspace-mcp', version: '0.1.0' }, { capabilities: { tools: {} } }); server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: TOOL_DEFINITIONS })); server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => { const name = request.params.name; const args = (request.params.arguments ?? {}); try { const result = await dispatch(cfg, name, args); return { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }] }; } catch (err) { const message = err instanceof Error ? err.message : String(err); return { isError: true, content: [{ type: 'text', text: `Error: ${message}` }] }; } }); return server; } async function main() { const cfg = loadConfig(); const server = buildServer(cfg); const transport = new stdio_js_1.StdioServerTransport(); await server.connect(transport); // eslint-disable-next-line no-console console.error(`[vibn-workspace-mcp] ready — ${TOOL_DEFINITIONS.length} tools exposed ` + `(workspace=${cfg.workspaceRoot}, git=${cfg.gitea ? 'enabled' : 'disabled'})`); } main().catch((err) => { // eslint-disable-next-line no-console console.error('[vibn-workspace-mcp] fatal:', err); process.exit(1); });