#!/usr/bin/env node import fsSync from 'node:fs'; import fsPromises from 'node:fs/promises'; import nodePath from 'node:path'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { dispatch } from './lib/dispatch.js'; import { readSoul, appendSoulMemory } from './lib/soul.js'; import { listSessions, readSessionEvents, searchSessions } from './lib/copilot.js'; import { start as fdStart, status as fdStatus, advance as fdAdvance, update as fdUpdate, abort as fdAbort, ALL_PHASES as FD_PHASES, } from './lib/fd-state.js'; import { requestReview, recordVerdict, getReview, listPending, pickReviewer, } from './lib/peer-review.js'; import { createBoard, addCard, moveCard, assignCard, getBoard, postStandup, readStandups, detectBlockedStreaks, KANBAN_COLUMNS, } from './lib/kanban.js'; import { eloSummary, eloContext, eloDashboard, eloRecord, fcCheck, fcParse, } from './lib/cli.js'; import { checkForUpdate, formatApplyInstructions } from './lib/update.js'; import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = nodePath.dirname(__filename); // Resolve the host project root: server.js lives at // {projectRoot}/_byan/mcp/byan-mcp-server/server.js, so go up three levels. const PROJECT_ROOT = nodePath.resolve(__dirname, '..', '..', '..'); const BYAN_API_URL = process.env.BYAN_API_URL || 'http://localhost:3737'; const BYAN_API_TOKEN = process.env.BYAN_API_TOKEN || ''; const authHeaders = () => { if (!BYAN_API_TOKEN) return {}; // byan_web issues API keys prefixed with `byan_` and requires the // `ApiKey` scheme. Any other token (JWT, etc.) falls back to Bearer. const scheme = BYAN_API_TOKEN.startsWith('byan_') ? 'ApiKey' : 'Bearer'; return { Authorization: `${scheme} ${BYAN_API_TOKEN}` }; }; function buildQuery(params) { const sp = new URLSearchParams(); for (const [k, v] of Object.entries(params)) { if (v === undefined || v === null || v === '') continue; sp.append(k, String(v)); } const s = sp.toString(); return s ? `?${s}` : ''; } function requireToken() { if (!BYAN_API_TOKEN) { throw new Error('BYAN_API_TOKEN env var is required for this tool.'); } } async function apiRequest(path, options = {}) { const url = `${BYAN_API_URL}${path}`; const headers = { 'Content-Type': 'application/json', ...authHeaders(), ...(options.headers || {}), }; const res = await fetch(url, { ...options, headers }); const text = await res.text(); const contentType = (res.headers.get('content-type') || '').toLowerCase(); const isJson = contentType.includes('application/json'); let body; try { body = text ? JSON.parse(text) : null; } catch { body = text; } if (!res.ok) { const err = new Error(`${res.status} ${res.statusText}: ${text}`); err.status = res.status; err.body = body; throw err; } // A 200 carrying HTML almost certainly means BYAN_API_URL points at the // WebUI host (behind Authentik SSO) instead of the API backend. // Never let a non-JSON response through — it used to fall back to // `body.data || []` and silently pretend the API was empty. if (!isJson) { const hint = contentType.includes('text/html') ? 'Expected JSON, got HTML. Likely BYAN_API_URL points at the WebUI (byan.) instead of the API (byan-api.).' : `Expected JSON, got content-type: ${contentType || '(none)'}.`; const err = new Error(`${hint} URL=${url}`); err.status = res.status; err.nonJson = true; throw err; } return body; } // Default filters — skip common build/vcs artifacts that pollute payload. const DEFAULT_SKIP_DIRS = new Set([ '.git', 'node_modules', 'dist', 'build', '.next', 'coverage', '__pycache__', '.venv', 'venv', '.pytest_cache', '.mypy_cache', 'target', 'out', '.turbo', '.cache', '.DS_Store', ]); const DEFAULT_SKIP_FILE_PATTERNS = [ /\.log$/i, /\.sqlite$/i, /\.sqlite-journal$/i, /\.sqlite-wal$/i, /\.lock$/i, /\.pid$/i, ]; // Heuristic: treat as binary if content has NUL byte in first 8KB. function looksBinary(buf) { const sample = buf.subarray(0, Math.min(buf.length, 8192)); for (const b of sample) if (b === 0) return true; return false; } // Hard limits — match W1's API guards so we fail fast client-side. const MAX_FILES = 10000; const MAX_TOTAL_BYTES = 100 * 1024 * 1024; // 100 MB async function buildFilesPayload(absRoot, opts = {}) { const skipDirs = opts.skipDirs || DEFAULT_SKIP_DIRS; const skipPatterns = opts.skipPatterns || DEFAULT_SKIP_FILE_PATTERNS; const maxFiles = opts.maxFiles || MAX_FILES; const maxBytes = opts.maxBytes || MAX_TOTAL_BYTES; const stat = await fsPromises.stat(absRoot); if (!stat.isDirectory()) { throw new Error(`Path is not a directory: ${absRoot}`); } const files = []; let totalBytes = 0; async function walk(dir) { const entries = await fsPromises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const full = nodePath.join(dir, entry.name); if (entry.isDirectory()) { if (skipDirs.has(entry.name)) continue; await walk(full); continue; } if (!entry.isFile()) continue; if (skipPatterns.some((re) => re.test(entry.name))) continue; const rel = nodePath.relative(absRoot, full).split(nodePath.sep).join('/'); const buf = await fsPromises.readFile(full); totalBytes += buf.length; if (files.length + 1 > maxFiles) { throw new Error( `Too many files (>${maxFiles}). Add to skipDirs or increase maxFiles.` ); } if (totalBytes > maxBytes) { throw new Error( `Total size exceeds ${(maxBytes / 1024 / 1024).toFixed(0)}MB. ` + `Prune node_modules/dist/build dirs or increase maxBytes.` ); } if (looksBinary(buf)) { files.push({ path: rel, content: buf.toString('base64'), encoding: 'base64' }); } else { files.push({ path: rel, content: buf.toString('utf8'), encoding: 'utf8' }); } } } await walk(absRoot); return { files, count: files.length, totalBytes }; } const tools = [ { name: 'byan_ping', description: 'Healthcheck the byan_web API. Returns status and version. No auth required. Also reports round-trip latency and whether BYAN_API_TOKEN is configured.', inputSchema: { type: 'object', properties: {}, additionalProperties: false, }, }, { name: 'byan_list_projects', description: 'List all BYAN projects stored in byan_web. Returns projects ordered by creation date (most recent first). Requires BYAN_API_TOKEN env var set to a valid JWT.', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Optional client-side limit (server returns all, truncated here). Default: 50.', }, }, additionalProperties: false, }, }, { name: 'byan_import_project', description: 'Import a local project directory into byan_web. Reads files from the local filesystem (client-side) and uploads them as a payload; works whether byan_web is local or remote. Skips .git, node_modules, dist, build, coverage, *.log, *.sqlite. Limits: 10000 files, 100MB total. Requires auth. If projectId is provided, files attach to that project ; otherwise a new project is created from name (or directory basename).', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path to the project directory on THIS machine (the MCP client). The API does not need filesystem access to this path.', }, projectId: { type: 'string', description: 'Existing project id to attach the files to. If absent, a new project is created.', }, name: { type: 'string', description: 'Project name override (used only when projectId is absent).' }, type: { type: 'string', enum: ['dev', 'training'], description: 'Project type for new project creation. Default: dev. Ignored when projectId is provided.', }, autoCreateNodes: { type: 'boolean', description: 'When true, auto-create knowledge nodes from file directory structure. Default: false.', }, maxFiles: { type: 'number', description: 'Override max file count (default 10000).', }, maxBytes: { type: 'number', description: 'Override max total bytes (default 104857600 = 100MB).', }, }, required: ['path'], additionalProperties: false, }, }, { name: 'byan_dispatch', description: 'BYAN Dispatcher: given a task description and complexity score (0-100), route it to the optimal execution target. Rule-based, no API call. Returns route and reasoning.', inputSchema: { type: 'object', properties: { task: { type: 'string', description: 'Short task description.' }, complexity: { type: 'number', description: 'Complexity score 0-100 (optional, will estimate from task length if absent).', }, parallelizable: { type: 'boolean', description: 'Is the task parallelizable with other tasks?', }, }, required: ['task'], additionalProperties: false, }, }, { name: 'byan_soul_read', description: 'Read the BYAN soul/tao/soul-memory files from the current project. No auth. Useful when the agent needs to reference the current soul configuration mid-session without relying solely on the SessionStart hook injection.', inputSchema: { type: 'object', properties: { which: { type: 'string', enum: ['soul', 'tao', 'soul-memory', 'all'], description: 'Which file to read. Default: all.', }, }, additionalProperties: false, }, }, { name: 'byan_soul_memory_append', description: 'Append a validated entry to _byan/soul-memory.md. Requires validated=true — the caller must have explicit user confirmation before invoking this tool (per BYAN rule: never write silently to soul-memory).', inputSchema: { type: 'object', properties: { entry: { type: 'string', description: 'The entry text (markdown allowed).' }, validated: { type: 'boolean', description: 'Must be true. Confirms the entry was validated by the user.', }, }, required: ['entry', 'validated'], additionalProperties: false, }, }, { name: 'byan_elo_summary', description: 'ELO trust summary across all technical domains. Wraps `byan-v2-cli elo summary`. No auth. Returns ratings, trends, session counts.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, }, { name: 'byan_elo_context', description: 'Challenge-context for a specific domain (returns promptInstructions BYAN should apply when challenging a claim). Wraps `byan-v2-cli elo context `.', inputSchema: { type: 'object', properties: { domain: { type: 'string', description: 'Domain name (security|javascript|performance|...)' }, }, required: ['domain'], additionalProperties: false, }, }, { name: 'byan_elo_record', description: 'Record the outcome of a user claim on a domain. Wraps `byan-v2-cli elo record `.', inputSchema: { type: 'object', properties: { domain: { type: 'string' }, result: { type: 'string', enum: ['VALIDATED', 'BLOCKED', 'PARTIAL'] }, reason: { type: 'string' }, }, required: ['domain', 'result'], additionalProperties: false, }, }, { name: 'byan_fc_check', description: 'Run fact-check on a claim string. Returns assertion type (REASONING|HYPOTHESIS|CLAIM L{n}|FACT), level, score. Wraps `byan-v2-cli fc check `.', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'Assertion to fact-check.' } }, required: ['text'], additionalProperties: false, }, }, { name: 'byan_fc_parse', description: 'Parse a text for auto-detection patterns (absolutes, superlatives, unsourced best-practice claims). Wraps `byan-v2-cli fc parse `.', inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'], additionalProperties: false, }, }, { name: 'byan_copilot_sessions', description: 'List GitHub Copilot CLI sessions stored locally at ~/.copilot/session-state/. Returns sessionId, start/end time, cwd, branch, agent name, message and tool call counts. Sorted most-recent-first. Use to discover past Copilot CLI conversations for reference or import.', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Max sessions to return (default 20).' }, sinceIso: { type: 'string', description: 'ISO timestamp filter — only sessions started after this.' }, cwdFilter: { type: 'string', description: 'Substring match on session cwd (e.g. "byan_web").' }, }, additionalProperties: false, }, }, { name: 'byan_copilot_session_events', description: 'Read events of a specific Copilot CLI session (events.jsonl). Optionally filter by event type (user.message, assistant.message, tool.execution_start, etc.). Useful to inspect the flow of a past session.', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Session UUID from byan_copilot_sessions.' }, types: { type: 'array', items: { type: 'string' }, description: 'Filter to these event types only.', }, limit: { type: 'number', description: 'Max events (default 200).' }, }, required: ['sessionId'], additionalProperties: false, }, }, { name: 'byan_fd_start', description: 'Start a new Feature Development (FD) cycle for BYAN. Writes _byan-output/fd-state.json with phase=DISCOVERY. Rejects if another FD is already in progress (unless force=true).', inputSchema: { type: 'object', properties: { featureName: { type: 'string', description: 'Short slug for the feature.' }, force: { type: 'boolean', description: 'Overwrite an existing in-progress FD.' }, }, required: ['featureName'], additionalProperties: false, }, }, { name: 'byan_fd_status', description: 'Return the current FD state (phase, backlog, dispatch_table, history) or { active: false } if none. Use at the start of a turn to know which phase to be in.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, }, { name: 'byan_fd_advance', description: 'Transition the current FD session to another phase. Valid targets : DISCOVERY | BRAINSTORM | PRUNE | DISPATCH | BUILD | REVIEW | VALIDATE | REFACTOR | DOC | COMPLETED | ABORTED. Rejects backward moves except REFACTOR->BUILD (rework loop) and ABORTED/COMPLETED.', inputSchema: { type: 'object', properties: { to: { type: 'string', enum: [ 'DISCOVERY', 'BRAINSTORM', 'PRUNE', 'DISPATCH', 'BUILD', 'REVIEW', 'VALIDATE', 'REFACTOR', 'DOC', 'COMPLETED', 'ABORTED', ], }, note: { type: 'string', description: 'Optional gate-crossing rationale.' }, }, required: ['to'], additionalProperties: false, }, }, { name: 'byan_fd_update', description: 'Patch fields on the active FD state. Allowed keys : project_context, raw_ideas, backlog, dispatch_table, commits, review_findings, validate_verdict, refactor_log, doc_log, notes, feature_name. Rejects unknown keys.', inputSchema: { type: 'object', properties: { patch: { type: 'object', description: 'Partial object of allowed keys.' }, }, required: ['patch'], additionalProperties: false, }, }, { name: 'byan_fd_abort', description: 'Abort the current FD session (phase → ABORTED). Preserves the state file for inspection.', inputSchema: { type: 'object', properties: { reason: { type: 'string' } }, additionalProperties: false, }, }, { name: 'byan_review_request', description: 'Open a peer review request for a task/commit. Another agent (≠ author) must subsequently call byan_review_verdict. Persists under _byan-output/reviews/.json.', inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Unique id (commit sha or feature id).' }, author: { type: 'string', description: 'Agent name that produced the artefact.' }, artifact_paths: { type: 'array', items: { type: 'string' } }, description: { type: 'string' }, }, required: ['task_id', 'author'], additionalProperties: false, }, }, { name: 'byan_review_verdict', description: 'Record a verdict on an open review request. reviewer must differ from author (enforced). Valid verdicts : approve | changes | block.', inputSchema: { type: 'object', properties: { task_id: { type: 'string' }, reviewer: { type: 'string' }, verdict: { type: 'string', enum: ['approve', 'changes', 'block'] }, comments: { type: 'array', items: { type: 'string' } }, must_fix: { type: 'array', items: { type: 'string' } }, }, required: ['task_id', 'reviewer', 'verdict'], additionalProperties: false, }, }, { name: 'byan_review_get', description: 'Fetch the current state of a review by task_id.', inputSchema: { type: 'object', properties: { task_id: { type: 'string' } }, required: ['task_id'], additionalProperties: false, }, }, { name: 'byan_review_pending', description: 'List all open (pending or changes_requested) reviews, newest first.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, }, { name: 'byan_review_pick_reviewer', description: 'Suggest a reviewer distinct from the author. Uses domain pairs (dev↔quinn, architect↔tea, pm↔sm, ux↔pm) then falls back to the roster.', inputSchema: { type: 'object', properties: { author: { type: 'string' }, preferredDomain: { type: 'string' }, }, required: ['author'], additionalProperties: false, }, }, { name: 'byan_kanban_create', description: 'Create (or fetch existing) kanban board for a party-mode session. Columns : todo | doing | blocked | review | done. Persisted under _byan-output/party-mode-sessions//kanban.json.', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' } }, required: ['sessionId'], additionalProperties: false, }, }, { name: 'byan_kanban_add', description: 'Add a card to the kanban. card = { id, title, assignee?, priority? (P1|P2|P3), column? }.', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, card: { type: 'object', properties: { id: { type: 'string' }, title: { type: 'string' }, assignee: { type: 'string' }, priority: { type: 'string' }, column: { type: 'string' }, }, required: ['id', 'title'], }, }, required: ['sessionId', 'card'], additionalProperties: false, }, }, { name: 'byan_kanban_move', description: 'Move a card between columns. toColumn must be one of todo | doing | blocked | review | done. Provide blocker_reason when moving to blocked.', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, cardId: { type: 'string' }, toColumn: { type: 'string', enum: ['todo', 'doing', 'blocked', 'review', 'done'] }, blocker_reason: { type: 'string' }, }, required: ['sessionId', 'cardId', 'toColumn'], additionalProperties: false, }, }, { name: 'byan_kanban_assign', description: 'Assign a card to an agent.', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, cardId: { type: 'string' }, assignee: { type: 'string' }, }, required: ['sessionId', 'cardId', 'assignee'], additionalProperties: false, }, }, { name: 'byan_kanban_get', description: 'Fetch the current kanban board for a session.', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' } }, required: ['sessionId'], additionalProperties: false, }, }, { name: 'byan_standup_post', description: 'Append a stand-up entry to _byan-output/party-mode-sessions//standup.jsonl. Format : { agent, did, blockers[], next }.', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, agent: { type: 'string' }, did: { type: 'string' }, blockers: { type: 'array', items: { type: 'string' } }, next: { type: 'string' }, }, required: ['sessionId', 'agent'], additionalProperties: false, }, }, { name: 'byan_standup_read', description: 'Read the stand-up feed for a session, newest entries last.', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, limit: { type: 'number' }, }, required: ['sessionId'], additionalProperties: false, }, }, { name: 'byan_standup_blocked', description: 'Return agents with >= minStreak consecutive blocked stand-ups (default minStreak=2). Hermes uses this to trigger redispatch.', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, minStreak: { type: 'number' }, }, required: ['sessionId'], additionalProperties: false, }, }, { name: 'byan_copilot_search', description: 'Full-text search across all Copilot CLI sessions. Finds messages (user + assistant by default) containing the query string. Returns sessionId + timestamp + excerpt. Use to recall past discussions without knowing which session they were in.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Substring to search for (case-insensitive).' }, types: { type: 'array', items: { type: 'string' }, description: 'Event types to scan (default: user.message, assistant.message).', }, limit: { type: 'number', description: 'Max matches (default 50).' }, }, required: ['query'], additionalProperties: false, }, }, // ─── Projects ───────────────────────────────────────────────────────── { name: 'byan_api_projects_get', description: 'Fetch a single byan_web project by id. GET /api/projects/:id. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Project id.' }, }, required: ['id'], additionalProperties: false, }, }, { name: 'byan_api_projects_create', description: 'Create a new byan_web project. POST /api/projects. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Project name.' }, type: { type: 'string', description: 'Project type (e.g. dev, training).' }, description: { type: 'string' }, visibility: { type: 'string', description: 'e.g. private | public | team.' }, taxonomyType: { type: 'string' }, seedTaxonomy: { type: 'boolean' }, }, required: ['name', 'type'], additionalProperties: false, }, }, // ─── Workflows ──────────────────────────────────────────────────────── { name: 'byan_api_workflows_list', description: 'List workflows, optionally filtered by scope, project, or status. GET /api/workflows. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { scope: { type: 'string', description: 'Filter by scope.' }, projectId: { type: 'string', description: 'Filter by project id.' }, status: { type: 'string', description: 'Filter by status.' }, }, additionalProperties: false, }, }, { name: 'byan_api_workflows_get', description: 'Fetch a single workflow by id. GET /api/workflows/:id. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Workflow id.' } }, required: ['id'], additionalProperties: false, }, }, { name: 'byan_api_workflows_run', description: 'Trigger a workflow run. POST /api/workflows/:id/run. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Workflow id.' }, trigger: { type: 'object', description: 'Optional trigger payload forwarded to the workflow.' }, }, required: ['id'], additionalProperties: false, }, }, { name: 'byan_api_workflow_runs_list', description: 'List runs of a given workflow. GET /api/workflows/:id/runs. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Workflow id.' } }, required: ['id'], additionalProperties: false, }, }, { name: 'byan_api_workflow_runs_get', description: 'Fetch a single workflow run by runId. GET /api/workflow-runs/:runId. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { runId: { type: 'string', description: 'Workflow run id.' } }, required: ['runId'], additionalProperties: false, }, }, // ─── Knowledge ──────────────────────────────────────────────────────── { name: 'byan_api_knowledge_list', description: 'List knowledge entries, optionally filtered by project, category, tags, or limit. GET /api/knowledge. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { projectId: { type: 'string' }, category: { type: 'string' }, tags: { type: 'string', description: 'Comma-separated tag list.' }, limit: { type: 'number' }, }, additionalProperties: false, }, }, { name: 'byan_api_knowledge_get', description: 'Fetch a single knowledge entry by id. GET /api/knowledge/:id. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Knowledge entry id.' } }, required: ['id'], additionalProperties: false, }, }, // ─── Memory ─────────────────────────────────────────────────────────── { name: 'byan_api_memory_list', description: 'List memory entries, optionally filtered by project, category, type, or limit. GET /api/memory. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { projectId: { type: 'string' }, category: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' }, }, additionalProperties: false, }, }, { name: 'byan_api_memory_search', description: 'Full-text search across memory entries. GET /api/memory/search. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { q: { type: 'string', description: 'Search query.' } }, required: ['q'], additionalProperties: false, }, }, // ─── Custom Agents ──────────────────────────────────────────────────── { name: 'byan_api_custom_agents_list', description: 'List user custom agents. GET /api/custom-agents. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, }, { name: 'byan_api_custom_agents_get', description: 'Fetch a single custom agent by id. GET /api/custom-agents/:id. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Custom agent id.' } }, required: ['id'], additionalProperties: false, }, }, { name: 'byan_api_custom_agents_clone_system', description: 'Clone a system agent into the user catalog. POST /api/custom-agents/clone/:systemName. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { systemName: { type: 'string', description: 'System agent name to clone.' }, }, required: ['systemName'], additionalProperties: false, }, }, // ─── Sessions ───────────────────────────────────────────────────────── { name: 'byan_api_sessions_list', description: 'List byan_web sessions, optionally filtered by project. GET /api/sessions. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { projectId: { type: 'string' } }, additionalProperties: false, }, }, { name: 'byan_api_sessions_get', description: 'Fetch a single session by id. GET /api/sessions/:id. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Session id.' } }, required: ['id'], additionalProperties: false, }, }, { name: 'byan_api_sessions_history', description: 'Fetch the message/event history of a session. GET /api/sessions/:id/history. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Session id.' } }, required: ['id'], additionalProperties: false, }, }, // ─── Chat ───────────────────────────────────────────────────────────── { name: 'byan_api_chat_conversations_list', description: 'List chat conversations for the authenticated user. GET /api/chat/conversations. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, }, { name: 'byan_api_chat_messages_list', description: 'List messages of a chat conversation. GET /api/chat/conversations/:id/messages. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Conversation id.' } }, required: ['id'], additionalProperties: false, }, }, { name: 'byan_api_chat_send', description: 'Send a message to a chat conversation. POST /api/chat/conversations/:id/messages. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Conversation id.' }, content: { type: 'string', description: 'Message content.' }, role: { type: 'string', description: 'Optional role override (default: user).' }, }, required: ['id', 'content'], additionalProperties: false, }, }, // ─── Search ─────────────────────────────────────────────────────────── { name: 'byan_api_search', description: 'Cross-entity search over byan_web. GET /api/search. Requires BYAN_API_TOKEN.', inputSchema: { type: 'object', properties: { q: { type: 'string', description: 'Search query.' }, type: { type: 'string', description: 'Entity type filter.' }, projectId: { type: 'string' }, limit: { type: 'number' }, }, required: ['q'], additionalProperties: false, }, }, // ─── Import ─────────────────────────────────────────────────────────── { name: 'byan_api_import_scan', description: 'Scan a local directory and report what would be imported into byan_web. Reads files from the local filesystem (client-side) and uploads them as a payload; works whether byan_web is local or remote. Skips .git, node_modules, dist, build, coverage, *.log, *.sqlite. Limits: 10000 files, 100MB total. Requires auth.', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path to the directory on THIS machine (the MCP client). The API does not need filesystem access to this path.' }, maxFiles: { type: 'number', description: 'Override max file count (default 10000).' }, maxBytes: { type: 'number', description: 'Override max total bytes (default 104857600 = 100MB).' }, }, required: ['path'], additionalProperties: false, }, }, { name: 'byan_api_import_dry_run', description: 'Dry-run an import from a local directory into byan_web (no writes). Reads files from the local filesystem (client-side) and uploads them as a payload; works whether byan_web is local or remote. Skips .git, node_modules, dist, build, coverage, *.log, *.sqlite. Limits: 10000 files, 100MB total. Requires auth.', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path to the directory on THIS machine (the MCP client). The API does not need filesystem access to this path.' }, maxFiles: { type: 'number', description: 'Override max file count (default 10000).' }, maxBytes: { type: 'number', description: 'Override max total bytes (default 104857600 = 100MB).' }, }, required: ['path'], additionalProperties: false, }, }, { name: 'byan_update_check', description: 'Check whether the BYAN platform installed in this project is up to date. Read-only. Reads the installed version from _byan/.manifest.json (fallback: package.json), fetches the latest published version from the npm registry (registry.npmjs.org/create-byan-agent), compares them, and returns { installed, latest, updateAvailable, delta }. Network failures are reported (networkError) and treated as "do not block". Use at agent activation to surface updates without nagging.', inputSchema: { type: 'object', properties: {}, additionalProperties: false, }, }, { name: 'byan_update_apply', description: 'Returns the exact shell command the user must run to apply a BYAN update via the yanstaller pipeline (backup, diff vs latest npm template, merge non-user-modified files). Does NOT execute anything itself — update is destructive and must remain an explicit user action. Use after byan_update_check reports updateAvailable=true and the user has consented.', inputSchema: { type: 'object', properties: { preview: { type: 'boolean', description: 'If true, returns the --preview command (shows the diff without writing). Default: false.', }, force: { type: 'boolean', description: 'If true, returns the --force command (overrides user-modified files). Default: false. Use with caution.', }, }, additionalProperties: false, }, }, ]; const server = new Server( { name: 'byan-mcp', version: '0.1.0' }, { capabilities: { tools: {} } } ); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args = {} } = request.params; try { if (name === 'byan_ping') { const t0 = Date.now(); const body = await apiRequest('/api/health'); return { content: [ { type: 'text', text: JSON.stringify( { ...body, latency_ms: Date.now() - t0, token_configured: Boolean(BYAN_API_TOKEN), api_url: BYAN_API_URL, }, null, 2 ), }, ], }; } if (name === 'byan_list_projects') { if (!BYAN_API_TOKEN) { throw new Error('BYAN_API_TOKEN env var is required for this tool.'); } const body = await apiRequest('/api/projects'); const limit = args.limit || 50; const projects = (body.data || []).slice(0, limit); return { content: [ { type: 'text', text: JSON.stringify( { projects, total: body.total ?? projects.length, returned: projects.length }, null, 2 ), }, ], }; } if (name === 'byan_import_project') { if (!BYAN_API_TOKEN) { throw new Error('BYAN_API_TOKEN env var is required for this tool.'); } // Always upload files payload — works for both localhost and remote API. // Server contract (post FD api-import-project-files-payload-merge): // { files, projectId? } -> attach to existing project // { files, projectMeta: { name, type } } -> create new project const { files } = await buildFilesPayload(args.path, { ...(args.maxFiles ? { maxFiles: args.maxFiles } : {}), ...(args.maxBytes ? { maxBytes: args.maxBytes } : {}), }); const payload = { files }; if (args.projectId) { payload.projectId = args.projectId; } else if (args.name || args.type) { payload.projectMeta = { ...(args.name ? { name: args.name } : {}), type: args.type || 'dev', }; } if (args.autoCreateNodes === true) { payload.autoCreateNodes = true; } const body = await apiRequest('/api/import/project', { method: 'POST', body: JSON.stringify(payload), }); return { content: [{ type: 'text', text: JSON.stringify(body.data || body, null, 2) }], }; } if (name === 'byan_dispatch') { const result = dispatch(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } if (name === 'byan_soul_read') { const result = readSoul({ which: args.which || 'all' }); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } if (name === 'byan_soul_memory_append') { const result = appendSoulMemory({ entry: args.entry, validated: args.validated === true, }); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } if (name === 'byan_elo_summary') { const result = await eloSummary(); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } if (name === 'byan_elo_context') { const result = await eloContext({ domain: args.domain }); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } if (name === 'byan_elo_record') { const result = await eloRecord({ domain: args.domain, result: args.result, reason: args.reason, }); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } if (name === 'byan_fc_check') { const result = await fcCheck({ text: args.text }); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } if (name === 'byan_fc_parse') { const result = await fcParse({ text: args.text }); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } if (name === 'byan_copilot_sessions') { const result = listSessions({ limit: args.limit, sinceIso: args.sinceIso, cwdFilter: args.cwdFilter, }); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } if (name === 'byan_copilot_session_events') { const result = readSessionEvents({ sessionId: args.sessionId, types: args.types, limit: args.limit, }); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } if (name === 'byan_copilot_search') { const result = searchSessions({ query: args.query, types: args.types, limit: args.limit, }); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } if (name === 'byan_fd_start') { const state = fdStart({ featureName: args.featureName, force: args.force }); return { content: [{ type: 'text', text: JSON.stringify(state, null, 2) }] }; } if (name === 'byan_fd_status') { const state = fdStatus(); return { content: [{ type: 'text', text: JSON.stringify(state, null, 2) }] }; } if (name === 'byan_fd_advance') { const state = fdAdvance({ to: args.to, note: args.note }); return { content: [{ type: 'text', text: JSON.stringify(state, null, 2) }] }; } if (name === 'byan_fd_update') { const state = fdUpdate({ patch: args.patch }); return { content: [{ type: 'text', text: JSON.stringify(state, null, 2) }] }; } if (name === 'byan_fd_abort') { const state = fdAbort({ reason: args.reason }); return { content: [{ type: 'text', text: JSON.stringify(state, null, 2) }] }; } if (name === 'byan_review_request') { const r = requestReview({ task_id: args.task_id, author: args.author, artifact_paths: args.artifact_paths, description: args.description, }); return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] }; } if (name === 'byan_review_verdict') { const r = recordVerdict({ task_id: args.task_id, reviewer: args.reviewer, verdict: args.verdict, comments: args.comments, must_fix: args.must_fix, }); return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] }; } if (name === 'byan_review_get') { const r = getReview({ task_id: args.task_id }); return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] }; } if (name === 'byan_review_pending') { const r = listPending(); return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] }; } if (name === 'byan_review_pick_reviewer') { const r = pickReviewer({ author: args.author, preferredDomain: args.preferredDomain, }); return { content: [{ type: 'text', text: JSON.stringify({ reviewer: r }, null, 2) }], }; } if (name === 'byan_kanban_create') { const r = createBoard({ sessionId: args.sessionId }); return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] }; } if (name === 'byan_kanban_add') { const r = addCard({ sessionId: args.sessionId, card: args.card }); return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] }; } if (name === 'byan_kanban_move') { const r = moveCard({ sessionId: args.sessionId, cardId: args.cardId, toColumn: args.toColumn, blocker_reason: args.blocker_reason, }); return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] }; } if (name === 'byan_kanban_assign') { const r = assignCard({ sessionId: args.sessionId, cardId: args.cardId, assignee: args.assignee, }); return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] }; } if (name === 'byan_kanban_get') { const r = getBoard({ sessionId: args.sessionId }); return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] }; } if (name === 'byan_standup_post') { const r = postStandup({ sessionId: args.sessionId, agent: args.agent, did: args.did, blockers: args.blockers, next: args.next, }); return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] }; } if (name === 'byan_standup_read') { const r = readStandups({ sessionId: args.sessionId, limit: args.limit }); return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] }; } if (name === 'byan_standup_blocked') { const r = detectBlockedStreaks({ sessionId: args.sessionId, minStreak: args.minStreak, }); return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] }; } // ─── byan_api_* wrappers ──────────────────────────────────────────── if (name === 'byan_api_projects_get') { requireToken(); const body = await apiRequest(`/api/projects/${encodeURIComponent(args.id)}`); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_projects_create') { requireToken(); const payload = { name: args.name, type: args.type, ...(args.description !== undefined ? { description: args.description } : {}), ...(args.visibility !== undefined ? { visibility: args.visibility } : {}), ...(args.taxonomyType !== undefined ? { taxonomyType: args.taxonomyType } : {}), ...(args.seedTaxonomy !== undefined ? { seedTaxonomy: args.seedTaxonomy } : {}), }; const body = await apiRequest('/api/projects', { method: 'POST', body: JSON.stringify(payload), }); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_workflows_list') { requireToken(); const qs = buildQuery({ scope: args.scope, project_id: args.projectId, status: args.status, }); const body = await apiRequest(`/api/workflows${qs}`); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_workflows_get') { requireToken(); const body = await apiRequest(`/api/workflows/${encodeURIComponent(args.id)}`); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_workflows_run') { requireToken(); const payload = args.trigger !== undefined ? { trigger: args.trigger } : {}; const body = await apiRequest( `/api/workflows/${encodeURIComponent(args.id)}/run`, { method: 'POST', body: JSON.stringify(payload) } ); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_workflow_runs_list') { requireToken(); const body = await apiRequest( `/api/workflows/${encodeURIComponent(args.id)}/runs` ); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_workflow_runs_get') { requireToken(); const body = await apiRequest( `/api/workflow-runs/${encodeURIComponent(args.runId)}` ); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_knowledge_list') { requireToken(); const qs = buildQuery({ project_id: args.projectId, category: args.category, tags: args.tags, limit: args.limit, }); const body = await apiRequest(`/api/knowledge${qs}`); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_knowledge_get') { requireToken(); const body = await apiRequest(`/api/knowledge/${encodeURIComponent(args.id)}`); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_memory_list') { requireToken(); const qs = buildQuery({ project_id: args.projectId, category: args.category, type: args.type, limit: args.limit, }); const body = await apiRequest(`/api/memory${qs}`); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_memory_search') { requireToken(); const qs = buildQuery({ q: args.q }); const body = await apiRequest(`/api/memory/search${qs}`); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_custom_agents_list') { requireToken(); const body = await apiRequest('/api/custom-agents'); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_custom_agents_get') { requireToken(); const body = await apiRequest( `/api/custom-agents/${encodeURIComponent(args.id)}` ); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_custom_agents_clone_system') { requireToken(); const body = await apiRequest( `/api/custom-agents/clone/${encodeURIComponent(args.systemName)}`, { method: 'POST', body: JSON.stringify({}) } ); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_sessions_list') { requireToken(); const qs = buildQuery({ project_id: args.projectId }); const body = await apiRequest(`/api/sessions${qs}`); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_sessions_get') { requireToken(); const body = await apiRequest(`/api/sessions/${encodeURIComponent(args.id)}`); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_sessions_history') { requireToken(); const body = await apiRequest( `/api/sessions/${encodeURIComponent(args.id)}/history` ); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_chat_conversations_list') { requireToken(); const body = await apiRequest('/api/chat/conversations'); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_chat_messages_list') { requireToken(); const body = await apiRequest( `/api/chat/conversations/${encodeURIComponent(args.id)}/messages` ); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_chat_send') { requireToken(); const payload = { content: args.content, ...(args.role !== undefined ? { role: args.role } : {}), }; const body = await apiRequest( `/api/chat/conversations/${encodeURIComponent(args.id)}/messages`, { method: 'POST', body: JSON.stringify(payload) } ); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_search') { requireToken(); const qs = buildQuery({ q: args.q, type: args.type, project_id: args.projectId, limit: args.limit, }); const body = await apiRequest(`/api/search${qs}`); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_import_scan') { requireToken(); // Build files payload from client filesystem — works for remote byan_web. const { files } = await buildFilesPayload(args.path, { ...(args.maxFiles ? { maxFiles: args.maxFiles } : {}), ...(args.maxBytes ? { maxBytes: args.maxBytes } : {}), }); const body = await apiRequest('/api/import/scan', { method: 'POST', body: JSON.stringify({ files }), }); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_api_import_dry_run') { requireToken(); // Build files payload from client filesystem — works for remote byan_web. const { files } = await buildFilesPayload(args.path, { ...(args.maxFiles ? { maxFiles: args.maxFiles } : {}), ...(args.maxBytes ? { maxBytes: args.maxBytes } : {}), }); const body = await apiRequest('/api/import/dry-run', { method: 'POST', body: JSON.stringify({ files }), }); return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] }; } if (name === 'byan_update_check') { const status = await checkForUpdate(PROJECT_ROOT); return { content: [{ type: 'text', text: JSON.stringify(status, null, 2) }] }; } if (name === 'byan_update_apply') { const instructions = formatApplyInstructions({ preview: args.preview === true, force: args.force === true, }); return { content: [{ type: 'text', text: JSON.stringify(instructions, null, 2) }] }; } throw new Error(`Unknown tool: ${name}`); } catch (err) { return { isError: true, content: [{ type: 'text', text: `Error: ${err.message}` }], }; } }); const transport = new StdioServerTransport(); await server.connect(transport); export { buildFilesPayload };