@briefhq/mcp-server
Version:
Brief MCP server and CLI – connect Cursor/Claude MCP to your Brief organization
868 lines (867 loc) • 57.2 kB
JavaScript
import { z } from "zod";
import { apiFetch } from "../lib/supabase.js";
import { zodToJsonSchema } from "zod-to-json-schema";
// Helper function for POST requests with JSON error handling
async function postJsonOrThrow(path, body, label) {
const res = await apiFetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
if (!res.ok) {
const msg = await res.text().catch(() => "");
const reqId = res.headers?.get?.("x-request-id") || res.headers?.get?.("x-amzn-requestid") || "";
const suffix = reqId ? `, reqId=${reqId}` : "";
throw new Error(`${label} failed (${res.status} ${res.statusText}${suffix}): ${msg || "no body"}`);
}
return await res.json();
}
// Layer 1: Discovery - What can I do in this domain?
const briefDiscoverCapabilitiesSchema = z.object({
domain: z.enum(['context', 'content', 'documents', 'folders', 'decisions', 'product', 'bulk', 'all']).optional().default('all'),
detail_level: z.enum(['summary', 'detailed']).optional().default('summary')
});
// Layer 2: Planning - How should I structure this request?
const briefPlanOperationSchema = z.object({
operation: z.enum([
// Content operations (also available through prepare_context)
'search', 'browse', 'find_related', 'get_recent', 'retrieve_content',
// Document operations
'create_document', 'get_document', 'update_document', 'delete_document', 'move_document',
// Folder operations
'create_folder', 'get_folder', 'update_folder', 'delete_folder', 'move_folder', 'get_folder_tree', 'get_folder_documents',
// Bulk operations
'bulk_move', 'bulk_delete',
// Product graph
'get_product_graph', 'update_product_graph',
// Decision operations
'record_decision', 'confirm_decision', 'archive_decision', 'search_decisions',
'create_session_decision', 'get_session_decisions', 'delete_session_decision',
'propose_decision', 'batch_propose_decisions', 'get_decision_proposals', 'confirm_decision_proposal',
// Context operations
'get_context', 'guard_approach'
]),
target: z.enum(['document', 'folder', 'decision', 'product_graph', 'content']).optional(),
context: z.record(z.unknown()).optional()
});
// Layer 3: Preparation - Get the context I need for this operation
const briefPrepareContextSchema = z.object({
preparation_type: z.enum(['get_context', 'search', 'browse', 'find_related', 'get_recent', 'retrieve_content']),
// Context preparation
keys: z.array(z.string()).optional(),
// Search preparation
query: z.string().optional(),
folder_id: z.string().optional(),
// Browse preparation
path: z.string().optional(),
include_subfolders: z.boolean().optional(),
depth: z.number().int().min(1).max(10).optional(),
// Related preparation
document_id: z.string().optional(),
// Retrieve preparation
document_ids: z.array(z.string()).optional(),
max_tokens: z.number().int().min(1).max(8000).optional().default(2000),
mode: z.enum(['smart_subset', 'summaries']).optional().default('smart_subset'),
// Common params
limit: z.number().int().min(1).max(100).optional().default(10)
});
// Layer 4: Execution - Perform the actual operation with confirmation support
const briefExecuteOperationSchema = z.object({
operation: z.enum([
// Document operations
'create_document', 'get_document', 'update_document', 'delete_document', 'move_document',
// Folder operations
'create_folder', 'get_folder', 'update_folder', 'delete_folder', 'move_folder', 'get_folder_tree', 'get_folder_documents',
// Bulk operations
'bulk_move', 'bulk_delete',
// Product graph
'get_product_graph', 'update_product_graph',
// Decision operations
'record_decision', 'confirm_decision', 'archive_decision', 'search_decisions',
'create_session_decision', 'get_session_decisions', 'delete_session_decision',
'propose_decision', 'batch_propose_decisions', 'get_decision_proposals', 'confirm_decision_proposal',
// Context operations
'guard_approach',
// Content operations (convenience - also available via prepare_context)
'search'
]),
parameters: z.record(z.unknown()),
confirmation_token: z.string().optional(),
limit: z.number().optional()
});
export const layeredTools = {
brief_discover_capabilities: {
description: "Discover what Brief can do across different domains. Provides overview of available operations and capabilities. See @brief-guidelines.md for complete usage guidelines.",
inputSchema: briefDiscoverCapabilitiesSchema,
inputJsonSchema: zodToJsonSchema(briefDiscoverCapabilitiesSchema, "brief_discover_capabilities"),
execute: async (args) => {
const { domain, detail_level } = args;
// Base capabilities structure
const capabilities = {
context: {
description: "Context discovery and planning operations",
operations: ['get_context', 'guard_approach'],
requires_confirmation: false
},
content: {
description: "Content search, browse, and retrieval operations",
operations: ['search', 'browse', 'find_related', 'get_recent', 'retrieve_content'],
requires_confirmation: false
},
documents: {
description: "Document CRUD operations",
operations: ['create_document', 'get_document', 'update_document', 'delete_document', 'move_document'],
requires_confirmation: ['update_document', 'delete_document', 'move_document']
},
folders: {
description: "Folder management and navigation",
operations: ['create_folder', 'get_folder', 'update_folder', 'delete_folder', 'move_folder', 'get_folder_tree', 'get_folder_documents'],
requires_confirmation: ['delete_folder', 'move_folder']
},
decisions: {
description: "Decision recording and management",
operations: ['record_decision', 'confirm_decision', 'archive_decision', 'search_decisions', 'create_session_decision', 'get_session_decisions', 'delete_session_decision', 'propose_decision', 'batch_propose_decisions', 'get_decision_proposals', 'confirm_decision_proposal'],
requires_confirmation: ['archive_decision']
},
product: {
description: "Product graph operations",
operations: ['get_product_graph', 'update_product_graph'],
requires_confirmation: ['update_product_graph']
},
bulk: {
description: "Bulk operations for efficiency",
operations: ['bulk_move', 'bulk_delete'],
requires_confirmation: ['bulk_move', 'bulk_delete']
}
};
if (domain === 'all') {
if (detail_level === 'summary') {
return {
available_domains: Object.keys(capabilities),
total_operations: Object.values(capabilities).reduce((sum, domain) => sum + domain.operations.length, 0),
confirmation_required_operations: Object.values(capabilities).reduce((sum, domain) => sum + (Array.isArray(domain.requires_confirmation) ? domain.requires_confirmation.length : 0), 0),
workflow: "Use brief_plan_operation to get detailed schemas for specific operations, then brief_prepare_context for any needed context, finally brief_execute_operation to perform the action.",
capabilities: Object.fromEntries(Object.entries(capabilities).map(([key, value]) => [key, { description: value.description, operation_count: value.operations.length }]))
};
}
else {
return { capabilities };
}
}
else {
const domainCaps = capabilities[domain];
if (!domainCaps) {
throw new Error(`Unknown domain: ${domain}. Available domains: ${Object.keys(capabilities).join(', ')}`);
}
return { domain, ...domainCaps };
}
}
},
brief_plan_operation: {
description: "Get detailed schemas, validation rules, and workflow guidance for operations. Shows required/optional parameters and whether confirmation is needed.",
inputSchema: briefPlanOperationSchema,
inputJsonSchema: zodToJsonSchema(briefPlanOperationSchema, "brief_plan_operation"),
execute: async (args) => {
const { operation, target, context } = args;
// Operation definitions with detailed schemas and requirements
const operationPlans = {
// Content operations
search: {
description: "Search documents by content, title, or metadata",
required_params: ['query'],
optional_params: ['folder_id', 'limit'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly OR brief_prepare_context with preparation_type: 'search'",
note: "Available both as direct operation and through prepare_context"
},
browse: {
description: "Navigate folder structure and list contents",
required_params: [],
optional_params: ['folder_id', 'include_subfolders', 'limit'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_prepare_context with preparation_type: 'browse'"
},
find_related: {
description: "Find documents related to a specific document",
required_params: ['document_id'],
optional_params: ['limit'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_prepare_context with preparation_type: 'find_related'"
},
get_recent: {
description: "Get recently accessed or updated documents",
required_params: [],
optional_params: ['folder_id', 'limit'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_prepare_context with preparation_type: 'get_recent'"
},
retrieve_content: {
description: "Retrieve full document content with smart token budgeting",
required_params: [],
optional_params: ['document_ids', 'folder_id', 'query', 'max_tokens', 'mode'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_prepare_context with preparation_type: 'retrieve_content'"
},
// Document operations
create_document: {
description: "Create a new document",
required_params: ['title'],
optional_params: ['content', 'folder_id'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly",
folder_guidance: "If no folder_id specified, document will be created in My Documents folder. Consider appropriate folders: Projects (for project docs), Planning (for strategy docs), or specify a folder_id."
},
get_document: {
description: "Retrieve a single document by ID with full details",
required_params: ['id'],
optional_params: [],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
},
update_document: {
description: "Update an existing document",
required_params: ['id'],
optional_params: ['title', 'content', 'folder_id'],
preparation_needed: false,
confirmation_required: true,
confirmation_workflow: "1. Call with preview: true to see changes, 2. Get user approval, 3. Call with confirm: true + token",
next_layer: "brief_execute_operation with preview: true first"
},
delete_document: {
description: "Delete a document",
required_params: ['id'],
optional_params: [],
preparation_needed: false,
confirmation_required: true,
confirmation_workflow: "1. Call with preview: true to see what will be deleted, 2. Get user approval, 3. Call with confirm: true + token",
next_layer: "brief_execute_operation with preview: true first"
},
move_document: {
description: "Move a document to a different folder",
required_params: ['id', 'folder_id'],
optional_params: [],
preparation_needed: false,
confirmation_required: true,
confirmation_workflow: "1. Call with preview: true to see the move, 2. Get user approval, 3. Call with confirm: true + token",
next_layer: "brief_execute_operation with preview: true first"
},
// Folder operations
create_folder: {
description: "Create a new folder within an existing folder",
required_params: ['name', 'parent_id'],
optional_params: ['is_private'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly",
important_note: "parent_id is REQUIRED - folders must be created within existing folders (Projects, Planning, User Research, My Documents, or their subfolders). There is no 'root level' in Brief."
},
get_folder: {
description: "Retrieve a single folder by ID with full details",
required_params: ['id'],
optional_params: [],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
},
update_folder: {
description: "Update folder properties (name, privacy settings)",
required_params: ['id'],
optional_params: ['name', 'is_private'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly",
note: "Cannot modify system folders. Privacy can only be changed on custom folders you own."
},
delete_folder: {
description: "Delete a folder (must be empty or allow cascading)",
required_params: ['id'],
optional_params: [],
preparation_needed: false,
confirmation_required: true,
confirmation_workflow: "1. Call with preview: true to see what will be deleted, 2. Get user approval, 3. Call with confirm: true + token",
next_layer: "brief_execute_operation with preview: true first"
},
move_folder: {
description: "Move a folder to a different parent folder",
required_params: ['id', 'parent_id'],
optional_params: [],
preparation_needed: false,
confirmation_required: true,
confirmation_workflow: "1. Call with preview: true to see the move, 2. Get user approval, 3. Call with confirmation_token",
next_layer: "brief_execute_operation with preview: true first"
},
get_folder_tree: {
description: "Get folder tree structure",
required_params: [],
optional_params: ['path', 'depth', 'include_documents'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
},
get_folder_documents: {
description: "List all documents within a specific folder",
required_params: ['folder_id'],
optional_params: ['limit'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
},
// Bulk operations
bulk_move: {
description: "Move multiple documents/folders at once",
required_params: ['items', 'folder_id'],
optional_params: [],
preparation_needed: false,
confirmation_required: true,
confirmation_workflow: "1. Call with preview: true to see all moves, 2. Get user approval, 3. Call with confirm: true + token",
next_layer: "brief_execute_operation with preview: true first"
},
bulk_delete: {
description: "Delete multiple documents/folders at once",
required_params: ['items'],
optional_params: [],
preparation_needed: false,
confirmation_required: true,
confirmation_workflow: "1. Call with preview: true to see all deletions, 2. Get user approval, 3. Call with confirm: true + token",
next_layer: "brief_execute_operation with preview: true first"
},
// Product graph
get_product_graph: {
description: "Get current product graph configuration",
required_params: [],
optional_params: [],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
},
update_product_graph: {
description: "Update product graph configuration",
required_params: ['config'],
optional_params: [],
preparation_needed: false,
confirmation_required: true,
confirmation_workflow: "1. Call with preview: true to see changes, 2. Get user approval, 3. Call with confirm: true + token",
next_layer: "brief_execute_operation with preview: true first"
},
// Decision operations
record_decision: {
description: "Record a new architectural or business decision",
required_params: ['decision', 'rationale'],
optional_params: ['category', 'severity', 'scope', 'author', 'supersedes'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
},
confirm_decision: {
description: "Confirm a previously proposed decision",
required_params: ['id'],
optional_params: [],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
},
search_decisions: {
description: "Search through existing decisions",
required_params: [],
optional_params: ['query', 'limit'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
},
archive_decision: {
description: "Archive a decision (hide from active use but preserve for audit)",
required_params: ['id'],
optional_params: ['reason'],
preparation_needed: false,
confirmation_required: true,
confirmation_workflow: "1. Call with preview: true to see what will be archived, 2. Get user approval, 3. Call with confirmation_token",
next_layer: "brief_execute_operation with preview: true first"
},
create_session_decision: {
description: "Create a temporary session-scoped decision",
required_params: ['session_id', 'decision', 'rationale'],
optional_params: ['expires_in_minutes'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
},
get_session_decisions: {
description: "Retrieve session-scoped decisions",
required_params: [],
optional_params: ['session_id'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
},
delete_session_decision: {
description: "Delete a session decision",
required_params: ['session_id'],
optional_params: [],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
},
propose_decision: {
description: "Propose a new decision for review",
required_params: ['detected_text'],
optional_params: ['confidence', 'category', 'severity'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
},
batch_propose_decisions: {
description: "Propose multiple decisions in batch",
required_params: ['documents'],
optional_params: [],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
},
get_decision_proposals: {
description: "List pending decision proposals",
required_params: [],
optional_params: ['status', 'limit'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
},
confirm_decision_proposal: {
description: "Confirm and accept a decision proposal",
required_params: ['proposal_id'],
optional_params: ['edits'],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
},
// Context operations
get_context: {
description: "Fetch specific context by keys",
required_params: ['keys'],
optional_params: [],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_prepare_context with preparation_type: 'get_context'"
},
guard_approach: {
description: "Check for conflicts against existing decisions",
required_params: ['approach'],
optional_params: [],
preparation_needed: false,
confirmation_required: false,
next_layer: "brief_execute_operation directly"
}
};
const plan = operationPlans[operation];
if (!plan) {
throw new Error(`Unknown operation: ${operation}. Use brief_discover_capabilities to see available operations.`);
}
return {
operation,
target,
...plan,
context_provided: context || null
};
}
},
brief_prepare_context: {
description: "Fetch context, search documents, browse folders, or retrieve content. For destructive operation previews, use brief_execute_operation with preview: true in parameters.",
inputSchema: briefPrepareContextSchema,
inputJsonSchema: zodToJsonSchema(briefPrepareContextSchema, "brief_prepare_context"),
execute: async (args) => {
// Support both flat args and { parameters } wrapper from client JSON schema
// For prepare_context, parameters come in args.parameters
const params = args.parameters || args;
const preparation_type = args.preparation_type || params.preparation_type;
switch (preparation_type) {
case 'get_context': {
if (!params.keys || !Array.isArray(params.keys) || params.keys.length === 0) {
throw new Error('keys parameter is required for get_context preparation');
}
return postJsonOrThrow("/api/v1/context", { keys: params.keys }, "context");
}
case 'search': {
if (!params.query) {
throw new Error('query parameter is required for search preparation');
}
const searchParams = new URLSearchParams();
searchParams.set('query', params.query);
if (params.folder_id)
searchParams.set('folder_id', params.folder_id);
searchParams.set('limit', (params.limit || 10).toString());
const searchRes = await apiFetch(`/api/v1/search?${searchParams}`, { method: "GET" });
if (!searchRes.ok) {
const msg = await searchRes.text().catch(() => "");
throw new Error(`search failed (${searchRes.status}): ${msg}`);
}
return await searchRes.json();
}
case 'browse': {
const browseParams = new URLSearchParams();
if (params.folder_id)
browseParams.set('folder_id', params.folder_id);
if (params.include_subfolders)
browseParams.set('include_subfolders', 'true');
browseParams.set('limit', (params.limit || 20).toString());
if (params.path)
browseParams.set('path', params.path);
if (typeof params.depth !== 'undefined')
browseParams.set('depth', String(params.depth));
const browseRes = await apiFetch(`/api/v1/browse?${browseParams}`, { method: "GET" });
if (!browseRes.ok) {
const msg = await browseRes.text().catch(() => "");
throw new Error(`browse failed (${browseRes.status}): ${msg}`);
}
return await browseRes.json();
}
case 'find_related': {
if (!params.document_id) {
throw new Error('document_id parameter is required for find_related preparation');
}
return postJsonOrThrow("/api/context/find-related", {
document_id: params.document_id,
limit: params.limit || 5
}, "find_related");
}
case 'get_recent': {
const recentParams = new URLSearchParams();
if (params.folder_id)
recentParams.set('folder_id', params.folder_id);
recentParams.set('limit', (params.limit || 10).toString());
const recentRes = await apiFetch(`/api/context/recent?${recentParams}`, { method: "GET" });
if (!recentRes.ok) {
const msg = await recentRes.text().catch(() => "");
throw new Error(`get_recent failed (${recentRes.status}): ${msg}`);
}
return await recentRes.json();
}
case 'retrieve_content': {
const retrievePayload = {
max_tokens: params.max_tokens || 2000,
mode: params.mode || 'smart_subset'
};
if (params.document_ids && params.document_ids.length > 0) {
retrievePayload.document_ids = params.document_ids;
}
if (params.folder_id) {
retrievePayload.folder_id = params.folder_id;
}
if (params.query) {
retrievePayload.query = params.query;
}
return postJsonOrThrow("/api/v1/retrieve", retrievePayload, "retrieve");
}
default:
throw new Error(`Unknown preparation_type: ${preparation_type}`);
}
}
},
brief_execute_operation: {
description: "Execute Brief operations with full v1 API coverage. CRITICAL: Follow @brief-guidelines.md confirmation workflow - For destructive operations (delete, move, bulk), MUST get user permission before using confirmation token. Safe operations (create, read, update) can be executed directly. IMPORTANT: confirmation_token is a TOP-LEVEL parameter alongside 'operation' and 'parameters', NOT nested inside parameters!",
inputSchema: briefExecuteOperationSchema,
inputJsonSchema: zodToJsonSchema(briefExecuteOperationSchema, "brief_execute_operation"),
execute: async (args) => {
const { operation, parameters, confirmation_token, limit } = args;
const params = parameters;
// If using a confirmation token, wait to ensure minimum time has passed
// Brief enforces a 5-second minimum wait to prevent auto-confirmation
if (confirmation_token) {
await new Promise(resolve => setTimeout(resolve, 5500)); // Wait 5.5 seconds
}
// Route operations to appropriate endpoints
switch (operation) {
// Document operations
case 'create_document': {
if (!params.title)
throw new Error('title is required for create_document');
// Use My Documents folder if not specified
// Fallback chain: provided ID -> env variable -> hardcoded default (for backwards compatibility)
const folder_id = params.folder_id || process.env.DEFAULT_MY_DOCUMENTS_FOLDER_ID || "e9a09872-d078-4ba3-b229-f7626d7978e8";
const result = await postJsonOrThrow("/api/v1/documents", {
title: params.title,
content: params.content || '',
folder_id: folder_id
}, "create_document");
// Add folder context to response
const resultObj = result;
if (params.folder_id) {
return { ...resultObj, folder_placement: "Document created in specified folder" };
}
else {
return { ...resultObj, folder_placement: "Document created in My Documents folder (default)" };
}
}
case 'get_document': {
if (!params.id)
throw new Error('id is required for get_document');
const getDocRes = await apiFetch(`/api/v1/documents/${params.id}`, { method: "GET" });
if (!getDocRes.ok) {
const msg = await getDocRes.text().catch(() => "");
throw new Error(`get_document failed (${getDocRes.status}): ${msg}`);
}
return await getDocRes.json();
}
case 'update_document': {
if (!params.id)
throw new Error('Document ID is required for updates. Find the ID using get_folder_tree or search first.');
// Build update payload - only include fields that are being changed
const updatePayload = {};
if (params.title !== undefined)
updatePayload.title = params.title;
if (params.content !== undefined)
updatePayload.content = params.content;
if (params.folder_id !== undefined)
updatePayload.folder_id = params.folder_id;
// Don't add preview to body - it goes in query string
if (confirmation_token)
updatePayload.token = confirmation_token;
const updateUrl = `/api/v1/documents/${params.id}/update${params.preview ? '?preview=true' : ''}`;
const updRes = await apiFetch(updateUrl, {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify(updatePayload)
});
if (!updRes.ok) {
const msg = await updRes.text().catch(() => "");
throw new Error(`update_document failed (${updRes.status}): ${msg}`);
}
return await updRes.json();
}
case 'delete_document': {
if (!params.id)
throw new Error('id is required for delete_document');
// Check for common mistake: token in params instead of top-level
if (params.token && !confirmation_token) {
throw new Error('IMPORTANT: confirmation_token must be a TOP-LEVEL parameter alongside "operation" and "parameters", NOT inside parameters. Example: {"operation": "delete_document", "parameters": {"id": "..."}, "confirmation_token": "confirm_..."}');
}
const deleteUrl = `/api/v1/documents/${params.id}/delete${params.preview ? '?preview=true' : ''}`;
const deleteRes = await apiFetch(deleteUrl, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(confirmation_token ? { token: confirmation_token } : {})
});
if (!deleteRes.ok) {
const msg = await deleteRes.text().catch(() => "");
throw new Error(`delete_document failed (${deleteRes.status}): ${msg}`);
}
// Handle 204 No Content or empty response
if (deleteRes.status === 204 || deleteRes.headers.get('content-length') === '0') {
return { success: true, message: 'Document deleted' };
}
return await deleteRes.json();
}
case 'move_document': {
if (!params.id || !params.folder_id)
throw new Error('id and folder_id are required for move_document');
const movePayload = { folder_id: params.folder_id };
if (params.preview)
movePayload.preview = true;
if (confirmation_token)
movePayload.token = confirmation_token;
const moveUrl = `/api/v1/documents/${params.id}/move${params.preview ? '?preview=true' : ''}`;
return postJsonOrThrow(moveUrl, movePayload, "move_document");
}
// Folder operations
case 'create_folder': {
if (!params.name)
throw new Error('name is required for create_folder');
if (!params.parent_id) {
throw new Error('parent_id is required - folders must be created within one of the system folders (Projects, Planning, User Research, or My Documents) or their subfolders. There is no "root level" in Brief.');
}
return postJsonOrThrow("/api/v1/folders", {
name: params.name,
parent_id: params.parent_id,
is_private: params.is_private
}, "create_folder");
}
case 'get_folder': {
if (!params.id)
throw new Error('id is required for get_folder');
const getFolderRes = await apiFetch(`/api/v1/folders/${params.id}`, { method: "GET" });
if (!getFolderRes.ok) {
const msg = await getFolderRes.text().catch(() => "");
throw new Error(`get_folder failed (${getFolderRes.status}): ${msg}`);
}
return await getFolderRes.json();
}
case 'update_folder': {
if (!params.id)
throw new Error('id is required for update_folder');
const updateFolderPayload = {};
if (params.name !== undefined)
updateFolderPayload.name = params.name;
if (params.is_private !== undefined)
updateFolderPayload.is_private = params.is_private;
const updateFolderRes = await apiFetch(`/api/v1/folders/${params.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updateFolderPayload)
});
if (!updateFolderRes.ok) {
const msg = await updateFolderRes.text().catch(() => "");
throw new Error(`update_folder failed (${updateFolderRes.status}): ${msg}`);
}
return await updateFolderRes.json();
}
case 'delete_folder': {
if (!params.id)
throw new Error('id is required for delete_folder');
// Check for common mistake: token in params instead of top-level
if (params.token && !confirmation_token) {
throw new Error('IMPORTANT: confirmation_token must be a TOP-LEVEL parameter alongside "operation" and "parameters", NOT inside parameters. Example: {"operation": "delete_folder", "parameters": {"id": "..."}, "confirmation_token": "confirm_..."}');
}
const deleteFolderUrl = `/api/v1/folders/${params.id}/delete${params.preview ? '?preview=true' : ''}`;
const dfInit = { method: "DELETE", headers: { "content-type": "application/json" } };
// Always send a small JSON body; preview ignores token
dfInit.body = JSON.stringify(confirmation_token ? { token: confirmation_token } : {});
const dfRes = await apiFetch(deleteFolderUrl, dfInit);
if (!dfRes.ok) {
const msg = await dfRes.text().catch(() => "");
throw new Error(`delete_folder failed (${dfRes.status}): ${msg}`);
}
// Handle 204 No Content or empty response
if (dfRes.status === 204 || dfRes.headers.get('content-length') === '0') {
return { success: true, message: 'Folder deleted' };
}
return await dfRes.json();
}
case 'get_folder_tree': {
const treeParams = new URLSearchParams();
if (params.path)
treeParams.set('path', params.path);
if (params.depth)
treeParams.set('depth', params.depth.toString());
if (params.include_documents)
treeParams.set('include_documents', 'true');
const treeRes = await apiFetch(`/api/v1/folders/tree?${treeParams}`, { method: "GET" });
if (!treeRes.ok) {
const msg = await treeRes.text().catch(() => "");
throw new Error(`get_folder_tree failed (${treeRes.status}): ${msg}`);
}
return await treeRes.json();
}
case 'get_folder_documents': {
if (!params.folder_id)
throw new Error('folder_id is required for get_folder_documents');
const folderDocsParams = new URLSearchParams();
if (params.limit)
folderDocsParams.set('limit', params.limit.toString());
const folderDocsRes = await apiFetch(`/api/v1/folders/${params.folder_id}/documents?${folderDocsParams}`, { method: "GET" });
if (!folderDocsRes.ok) {
const msg = await folderDocsRes.text().catch(() => "");
throw new Error(`get_folder_documents failed (${folderDocsRes.status}): ${msg}`);
}
return await folderDocsRes.json();
}
// Bulk operations
case 'bulk_move': {
if (!params.items || !params.folder_id)
throw new Error('items and folder_id are required for bulk_move');
// Check for common mistake: token in params instead of top-level
if (params.token && !confirmation_token) {
throw new Error('IMPORTANT: confirmation_token must be a TOP-LEVEL parameter alongside "operation" and "parameters", NOT inside parameters.');
}
const bulkMovePayload = {
items: params.items,
folder_id: params.folder_id
};
// Don't add preview to body - it goes in query string
if (confirmation_token)
bulkMovePayload.token = confirmation_token;
const bulkMoveUrl = `/api/v1/bulk/move${params.preview ? '?preview=true' : ''}`;
return postJsonOrThrow(bulkMoveUrl, bulkMovePayload, "bulk_move");
}
case 'bulk_delete': {
if (!params.items)
throw new Error('items are required for bulk_delete');
// Check for common mistake: token in params instead of top-level
if (params.token && !confirmation_token) {
throw new Error('IMPORTANT: confirmation_token must be a TOP-LEVEL parameter alongside "operation" and "parameters", NOT inside parameters.');
}
const bulkDeletePayload = { items: params.items };
// Don't add preview to body - it goes in query string
if (confirmation_token)
bulkDeletePayload.token = confirmation_token;
const bulkDeleteUrl = `/api/v1/bulk/delete${params.preview ? '?preview=true' : ''}`;
return postJsonOrThrow(bulkDeleteUrl, bulkDeletePayload, "bulk_delete");
}
// Product graph
case 'get_product_graph': {
const graphRes = await apiFetch("/api/v1/product-graph", { method: "GET" });
if (!graphRes.ok) {
const msg = await graphRes.text().catch(() => "");
throw new Error(`get_product_graph failed (${graphRes.status}): ${msg}`);
}
return await graphRes.json();
}
case 'move_folder': {
if (!params.id || !params.parent_id)
throw new Error('id and parent_id are required for move_folder');
// Check for common mistake: token in params instead of top-level
if (params.token && !confirmation_token) {
throw new Error('IMPORTANT: confirmation_token must be a TOP-LEVEL parameter alongside "operation" and "parameters", NOT inside parameters.');
}
const moveFolderUrl = `/api/v1/folders/${params.id}/move${params.preview ? '?preview=true' : ''}`;
const moveFolderPayload = { parent_id: params.parent_id };
if (confirmation_token)
moveFolderPayload.token = confirmation_token;
return postJsonOrThrow(moveFolderUrl, moveFolderPayload, "move_folder");
}
case 'update_product_graph': {
if (!params.config)
throw new Error('config is required for update_product_graph');
const updateGraphPayload = { ...params.config };
// Don't add preview to config - it goes in query string
if (confirmation_token)
updateGraphPayload.token = confirmation_token;
const updateGraphUrl = `/api/v1/product-graph${params.preview ? '?preview=true' : ''}`;
const graphUpdateRes = await apiFetch(updateGraphUrl, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updateGraphPayload)
});
if (!graphUpdateRes.ok) {
const msg = await graphUpdateRes.text().catch(() => "");
throw new Error(`update_product_graph failed (${graphUpdateRes.status}): ${msg}`);
}
return await graphUpdateRes.json();
}
// Decision operations - delegate to existing decision tools for now
case 'record_decision': {
if (!params.decision || !params.rationale)
throw new Error('decision and rationale are required for record_decision');
return postJsonOrThrow("/api/v1/decisions/record", {
decision: params.decision,
rationale: params.rationale,
category: params.category,
severity: params.severity,
scope: params.scope,
author: params.author,
supersedes: params.supersedes
}, "record_decision");
}
case 'confirm_decision': {
if (!params.id)
throw new Error('id is required for confirm_decision');
return postJsonOrThrow("/api/v1/decisions/confirm", { id: params.id }, "confirm_decision");
}
case 'search_decisions': {
// Search existing decisions
const searchDecParams = new URLSearchParams();
if (params.query)
searchDecParams.set('query', params.query);
if (params.limit)
searchDecParams.set('limit', params.limit.toString());
const searchDecRes = await apiFetch(`/api/v1/decisions?${searchDecParams}`, { method: "GET" });
if (!searchDecRes.ok) {
const msg = await searchDecRes.text().catch(() => "");
throw new Error(`search_decisions failed (${searchDecRes.status}): ${msg}`);
}
return await searchDecRes.json();
}