briefing
Retrieve a comprehensive mission control summary including goal health, pending decisions, my tasks, recent activity, and a top recommendation. Ideal for quick status overviews and autopilot initialization.
Instructions
Mission control state in one call. Returns goal health summary, pending decisions, my tasks, recent activity, and a top recommendation. Use this as the single read for Cowork live artifacts and the autopilot's first call.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| scope | No | mission_control | |
| goal_id | No | ||
| plan_id | No | ||
| recent_window_hours | No |
Implementation Reference
- src/tools/bdi/beliefs.js:32-176 (handler)The actual handler function for the 'briefing' tool. Calls the /agent/briefing API endpoint first, and falls back to fanning out 5 separate API calls (goals dashboard, pending decisions, tasks, coherence, episodes) if the facade is unavailable. Returns a structured response with goal_health, pending_decisions, pending_agent_requests, my_tasks, recent_activity, top_recommendation, and coherence_pending.
async function briefingHandler(args, apiClient) { try { const response = await apiClient.axiosInstance.get('/agent/briefing', { params: { scope: args.scope, goal_id: args.goal_id, plan_id: args.plan_id, recent_window_hours: args.recent_window_hours, }, }); return formatResponse(response.data); } catch { // Fall back to the pre-facade fan-out for self-hosted older APIs. } const recentHours = typeof args.recent_window_hours === 'number' ? args.recent_window_hours : 24; const recentSinceMs = Date.now() - recentHours * 3600 * 1000; const [dashboardRes, pendingRes, myTasksRes, coherenceRes, episodesRes] = await Promise.allSettled([ apiClient.goals.getDashboard(), apiClient.axiosInstance.get('/dashboard/pending', { params: { limit: 10 } }), apiClient.users.getMyTasks(), apiClient.coherence.getPending(), apiClient.graphiti.getEpisodes({ max_episodes: 20 }), ]); const failures = []; function unwrap(settled, label, defaultValue) { if (settled.status === 'fulfilled') return settled.value; failures.push({ source: label, message: settled.reason?.message || String(settled.reason) }); return defaultValue; } const dashboard = unwrap(dashboardRes, 'goals.dashboard', { goals: [] }); const pendingResp = unwrap(pendingRes, 'dashboard.pending', { data: { decisions: [], agent_requests: [] } }); const pending = pendingResp.data || pendingResp || { decisions: [], agent_requests: [] }; const myTasks = unwrap(myTasksRes, 'users.getMyTasks', { tasks: [] }); const coherencePending = unwrap(coherenceRes, 'coherence.pending', { plans: [], goals: [] }); const episodes = unwrap(episodesRes, 'graphiti.episodes', { episodes: { episodes: [] } }); let goals = safeArray(dashboard.goals); if (args.goal_id) goals = goals.filter((g) => g.id === args.goal_id); const goalSummary = goals.reduce( (acc, g) => { const h = g.health || 'on_track'; acc[h] = (acc[h] || 0) + 1; acc.total += 1; return acc; }, { on_track: 0, at_risk: 0, stale: 0, total: 0 } ); let decisions = safeArray(pending.decisions); if (args.plan_id) decisions = decisions.filter((d) => d.plan_id === args.plan_id); let agentRequests = safeArray(pending.agent_requests); if (args.plan_id) agentRequests = agentRequests.filter((r) => r.plan_id === args.plan_id); const tasks = safeArray(myTasks.tasks || myTasks); const myTasksBucketed = { in_progress: tasks.filter((t) => t.status === 'in_progress'), blocked: tasks.filter((t) => t.status === 'blocked'), recently_completed: tasks.filter((t) => { if (t.status !== 'completed') return false; const at = t.updated_at || t.completed_at; return at && new Date(at).getTime() >= recentSinceMs; }), }; const allEpisodes = safeArray(episodes.episodes?.episodes || episodes.episodes); const recentActivity = allEpisodes .filter((e) => e.created_at && new Date(e.created_at).getTime() >= recentSinceMs) .map((e) => ({ type: 'episode', ref_id: e.uuid, summary: e.name || (e.content && e.content.slice(0, 200)), occurred_at: e.created_at, source: e.source, })); const topRecommendation = (() => { const atRisk = goals.filter((g) => g.health === 'at_risk'); let best = null; for (const g of atRisk) { for (const b of safeArray(g.bottleneck_summary)) { if (!best || (b.direct_downstream_count || 0) > (best.direct_downstream_count || 0)) { best = { ...b, goal_id: g.id, goal_title: g.title }; } } } if (!best) return null; return { goal_id: best.goal_id, suggested_action: `Unblock task "${best.title}" — it gates ${best.direct_downstream_count || 0} downstream task(s)`, reasoning: `On at_risk goal "${best.goal_title}", this is the bottleneck with the highest direct_downstream_count`, node_id: best.node_id, }; })(); const coherencePendingList = [ ...safeArray(coherencePending.plans).map((p) => ({ id: p.id, type: 'plan', title: p.title, last_check_age_hours: p.coherence_checked_at ? Math.round((Date.now() - new Date(p.coherence_checked_at).getTime()) / 3600 / 1000) : null, })), ...safeArray(coherencePending.goals).map((g) => ({ id: g.id, type: 'goal', title: g.title, last_check_age_hours: g.coherence_checked_at ? Math.round((Date.now() - new Date(g.coherence_checked_at).getTime()) / 3600 / 1000) : null, })), ]; return formatResponse({ as_of: asOf(), scope: args.scope || 'mission_control', goal_health: { summary: goalSummary, goals: goals.map((g) => ({ id: g.id, title: g.title, health: g.health, priority: g.priority, bottleneck_summary: g.bottleneck_summary, last_activity: g.last_activity, pending_decision_count: g.pending_decision_count, })), }, pending_decisions: decisions, pending_agent_requests: agentRequests, my_tasks: myTasksBucketed, recent_activity: recentActivity, top_recommendation: topRecommendation, coherence_pending: coherencePendingList, meta: { partial: failures.length > 0, failures }, }); } - src/tools/bdi/beliefs.js:14-30 (schema)The schema/definition for the 'briefing' tool. Defines its name, description, and input schema with properties: scope (mission_control/task_session/org), goal_id, plan_id, and recent_window_hours (default 24).
const briefingDefinition = { name: 'briefing', description: "Mission control state in one call. Returns goal health summary, " + "pending decisions, my tasks, recent activity, and a top recommendation. " + "Use this as the single read for Cowork live artifacts and the autopilot's " + "first call.", inputSchema: { type: 'object', properties: { scope: { type: 'string', enum: ['mission_control', 'task_session', 'org'], default: 'mission_control' }, goal_id: { type: 'string' }, plan_id: { type: 'string' }, recent_window_hours: { type: 'number', default: 24 }, }, }, }; - src/tools/bdi/beliefs.js:64-555 (registration)Exports the briefing definition and handler from beliefs.js. The definitions array and handlers map are consumed by src/tools/bdi/index.js which aggregates all BDI tools.
const dashboard = unwrap(dashboardRes, 'goals.dashboard', { goals: [] }); const pendingResp = unwrap(pendingRes, 'dashboard.pending', { data: { decisions: [], agent_requests: [] } }); const pending = pendingResp.data || pendingResp || { decisions: [], agent_requests: [] }; const myTasks = unwrap(myTasksRes, 'users.getMyTasks', { tasks: [] }); const coherencePending = unwrap(coherenceRes, 'coherence.pending', { plans: [], goals: [] }); const episodes = unwrap(episodesRes, 'graphiti.episodes', { episodes: { episodes: [] } }); let goals = safeArray(dashboard.goals); if (args.goal_id) goals = goals.filter((g) => g.id === args.goal_id); const goalSummary = goals.reduce( (acc, g) => { const h = g.health || 'on_track'; acc[h] = (acc[h] || 0) + 1; acc.total += 1; return acc; }, { on_track: 0, at_risk: 0, stale: 0, total: 0 } ); let decisions = safeArray(pending.decisions); if (args.plan_id) decisions = decisions.filter((d) => d.plan_id === args.plan_id); let agentRequests = safeArray(pending.agent_requests); if (args.plan_id) agentRequests = agentRequests.filter((r) => r.plan_id === args.plan_id); const tasks = safeArray(myTasks.tasks || myTasks); const myTasksBucketed = { in_progress: tasks.filter((t) => t.status === 'in_progress'), blocked: tasks.filter((t) => t.status === 'blocked'), recently_completed: tasks.filter((t) => { if (t.status !== 'completed') return false; const at = t.updated_at || t.completed_at; return at && new Date(at).getTime() >= recentSinceMs; }), }; const allEpisodes = safeArray(episodes.episodes?.episodes || episodes.episodes); const recentActivity = allEpisodes .filter((e) => e.created_at && new Date(e.created_at).getTime() >= recentSinceMs) .map((e) => ({ type: 'episode', ref_id: e.uuid, summary: e.name || (e.content && e.content.slice(0, 200)), occurred_at: e.created_at, source: e.source, })); const topRecommendation = (() => { const atRisk = goals.filter((g) => g.health === 'at_risk'); let best = null; for (const g of atRisk) { for (const b of safeArray(g.bottleneck_summary)) { if (!best || (b.direct_downstream_count || 0) > (best.direct_downstream_count || 0)) { best = { ...b, goal_id: g.id, goal_title: g.title }; } } } if (!best) return null; return { goal_id: best.goal_id, suggested_action: `Unblock task "${best.title}" — it gates ${best.direct_downstream_count || 0} downstream task(s)`, reasoning: `On at_risk goal "${best.goal_title}", this is the bottleneck with the highest direct_downstream_count`, node_id: best.node_id, }; })(); const coherencePendingList = [ ...safeArray(coherencePending.plans).map((p) => ({ id: p.id, type: 'plan', title: p.title, last_check_age_hours: p.coherence_checked_at ? Math.round((Date.now() - new Date(p.coherence_checked_at).getTime()) / 3600 / 1000) : null, })), ...safeArray(coherencePending.goals).map((g) => ({ id: g.id, type: 'goal', title: g.title, last_check_age_hours: g.coherence_checked_at ? Math.round((Date.now() - new Date(g.coherence_checked_at).getTime()) / 3600 / 1000) : null, })), ]; return formatResponse({ as_of: asOf(), scope: args.scope || 'mission_control', goal_health: { summary: goalSummary, goals: goals.map((g) => ({ id: g.id, title: g.title, health: g.health, priority: g.priority, bottleneck_summary: g.bottleneck_summary, last_activity: g.last_activity, pending_decision_count: g.pending_decision_count, })), }, pending_decisions: decisions, pending_agent_requests: agentRequests, my_tasks: myTasksBucketed, recent_activity: recentActivity, top_recommendation: topRecommendation, coherence_pending: coherencePendingList, meta: { partial: failures.length > 0, failures }, }); } // ───────────────────────────────────────────────────────────────────────── // task_context — single task at progressive depth (1-4). // ───────────────────────────────────────────────────────────────────────── const taskContextDefinition = { name: 'task_context', description: "Get progressive context for a task. Depth: 1 (task only), 2 (+ neighborhood), " + "3 (+ knowledge), 4 (+ extended plan/goals/transitive deps). For RPI implement " + "tasks, automatically includes research+plan outputs from the chain.", inputSchema: { type: 'object', properties: { task_id: { type: 'string' }, depth: { type: 'integer', enum: [1, 2, 3, 4], default: 2 }, token_budget: { type: 'integer', default: 0 }, }, required: ['task_id'], }, }; async function taskContextHandler(args, apiClient) { const { task_id, depth = 2, token_budget = 0 } = args; const params = new URLSearchParams({ node_id: task_id, depth: String(depth), token_budget: String(token_budget), log_limit: '10', include_research: 'true', }); try { const response = await apiClient.axiosInstance.get(`/context/progressive?${params}`); return formatResponse({ as_of: asOf(), ...response.data }); } catch (err) { return errorResponse('upstream_unavailable', `Failed to load task context: ${err.response?.data?.error || err.message}`); } } // ───────────────────────────────────────────────────────────────────────── // goal_state — single-goal deep dive. Replaces 5 separate goal reads. // ───────────────────────────────────────────────────────────────────────── const goalStateDefinition = { name: 'goal_state', description: "Comprehensive single-goal read: details, quality assessment, progress, " + "bottlenecks, knowledge gaps, pending decisions, recent activity. " + "Replaces get_goal + goal_path + goal_progress + goal_knowledge_gaps + assess_goal_quality.", inputSchema: { type: 'object', properties: { goal_id: { type: 'string' } }, required: ['goal_id'], }, }; async function goalStateHandler(args, apiClient) { const { goal_id } = args; const [goalRes, qualityRes, progressRes, gapsRes, pathRes] = await Promise.allSettled([ apiClient.goals.get(goal_id), apiClient.goals.getQuality(goal_id), apiClient.goals.getProgress(goal_id), apiClient.goals.getKnowledgeGaps(goal_id), apiClient.goals.getPath(goal_id), ]); const failures = []; const unwrap = (s, label, def) => { if (s.status === 'fulfilled') return s.value; failures.push({ source: label, message: s.reason?.message }); return def; }; const goal = unwrap(goalRes, 'goals.get', null); if (!goal) return errorResponse('not_found', `Goal ${goal_id} not found`); const quality = unwrap(qualityRes, 'goals.quality', {}); const progress = unwrap(progressRes, 'goals.progress', {}); const gaps = unwrap(gapsRes, 'goals.knowledgeGaps', { gaps: [] }); const path = unwrap(pathRes, 'goals.path', { tasks: [] }); const bottlenecks = safeArray(path.tasks || path) .filter((t) => t.status !== 'completed') .sort((a, b) => (b.direct_downstream_count || 0) - (a.direct_downstream_count || 0)) .slice(0, 5) .map((t) => ({ node_id: t.id, title: t.title, status: t.status, direct_downstream_count: t.direct_downstream_count || 0, })); // Surface the goal's linked plans + tasks. The underlying GET /goals/:id // already returns the `links` array; the previous handler discarded it, // so quality.actionability could report "26 plans linked" while the // response refused to name a single one. Callers had no read-side way // to enumerate the plans served by a goal short of REST. const links = safeArray(goal.links); const linked_plans = links .filter((l) => (l.linkedType || l.linked_type) === 'plan') .map((l) => ({ id: l.linkedId || l.linked_id, link_id: l.id })); const linked_tasks = links .filter((l) => (l.linkedType || l.linked_type) === 'task') .map((l) => ({ id: l.linkedId || l.linked_id, link_id: l.id })); return formatResponse({ as_of: asOf(), goal: { id: goal.id, title: goal.title, description: goal.description, type: goal.type, goal_type: goal.goalType || goal.goal_type, status: goal.status, priority: goal.priority, owner_id: goal.ownerId || goal.owner_id, success_criteria: goal.successCriteria || goal.success_criteria, promoted_at: goal.promotedAt || goal.promoted_at, }, linked_plans, linked_tasks, quality: { score: quality.score, dimensions: quality.dimensions, suggestions: quality.suggestions, last_assessed_at: quality.as_of, }, progress: progress, bottlenecks, knowledge_gaps: safeArray(gaps.gaps || gaps), meta: { partial: failures.length > 0, failures }, }); } // ───────────────────────────────────────────────────────────────────────── // recall_knowledge — universal knowledge query. Replaces 4 separate tools. // ───────────────────────────────────────────────────────────────────────── const recallKnowledgeDefinition = { name: 'recall_knowledge', description: "Universal knowledge graph query. Returns facts, entities, recent episodes, " + "and contradictions in one shape. Use result_kind to control payload size. " + "Replaces recall_knowledge legacy + find_entities + get_recent_episodes + check_contradictions.", inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query — required for facts/entities, optional for episodes' }, scope: { type: 'object', properties: { plan_id: { type: 'string' }, goal_id: { type: 'string' }, node_id: { type: 'string' } }, }, since: { type: 'string', description: 'ISO 8601 — only return episodes after this' }, entry_type: { type: 'string', enum: ['learning', 'decision', 'progress', 'challenge', 'all'], default: 'all' }, result_kind: { type: 'string', enum: ['facts', 'entities', 'episodes', 'all'], default: 'all' }, max_results: { type: 'integer', default: 10 }, include_contradictions: { type: 'boolean', default: false }, }, }, }; async function recallKnowledgeHandler(args, apiClient) { const { query, scope = {}, since, entry_type = 'all', result_kind = 'all', max_results = 10, include_contradictions = false } = args; const wantFacts = result_kind === 'all' || result_kind === 'facts'; const wantEntities = result_kind === 'all' || result_kind === 'entities'; const wantEpisodes = result_kind === 'all' || result_kind === 'episodes'; const calls = []; if (wantFacts && query) { calls.push({ key: 'facts', p: apiClient.graphiti.graphSearch({ query, max_results, ...scope }) }); } if (wantEntities && query) { calls.push({ key: 'entities', p: apiClient.graphiti.searchEntities({ query, max_results }) }); } if (wantEpisodes) { calls.push({ key: 'episodes', p: apiClient.graphiti.getEpisodes({ max_episodes: Math.min(max_results * 2, 50) }) }); } if (include_contradictions && query) { calls.push({ key: 'contradictions', p: apiClient.graphiti.detectContradictions({ topic: query, ...scope }) }); } const settled = await Promise.allSettled(calls.map((c) => c.p)); const out = { as_of: asOf(), facts: [], entities: [], episodes: [], contradictions: null, meta: { failures: [] } }; settled.forEach((s, i) => { const key = calls[i].key; if (s.status !== 'fulfilled') { out.meta.failures.push({ source: `graphiti.${key}`, message: s.reason?.message }); return; } const v = s.value; if (key === 'facts') out.facts = safeArray(v.facts || v); if (key === 'entities') out.entities = safeArray(v.entities || v); if (key === 'episodes') { let eps = safeArray(v.episodes?.episodes || v.episodes || v); if (since) { const sinceMs = new Date(since).getTime(); eps = eps.filter((e) => e.created_at && new Date(e.created_at).getTime() >= sinceMs); } if (entry_type !== 'all') { eps = eps.filter((e) => (e.entry_type || e.source) === entry_type); } out.episodes = eps.slice(0, max_results); } if (key === 'contradictions') out.contradictions = v; }); return formatResponse(out); } // ───────────────────────────────────────────────────────────────────────── // list_plans — list workspace plans with optional filters. // Counterpart to list_goals; previously the only ways to find a plan // were knowing the UUID a priori, parsing briefing.recent_activity, // or calling task_context on a known node — all bad. // ───────────────────────────────────────────────────────────────────────── const listPlansDefinition = { name: 'list_plans', description: 'List plans with optional filters by status, visibility, or text query. ' + 'Returns id, title, status, visibility, last update, and link counts so ' + 'you can pick a plan to operate on without round-tripping briefing.', inputSchema: { type: 'object', properties: { filter: { type: 'object', properties: { status: { type: 'array', items: { type: 'string' } }, visibility: { type: 'array', items: { type: 'string', enum: ['private', 'unlisted', 'public'] } }, query: { type: 'string', description: 'Substring match on title (case-insensitive)' }, limit: { type: 'integer', default: 50 }, }, }, }, }, }; async function listPlansHandler(args, apiClient) { const filter = args.filter || {}; try { const raw = await apiClient.plans.getPlans(); let plans = Array.isArray(raw) ? raw : safeArray(raw.plans || raw); if (filter.status?.length) plans = plans.filter((p) => filter.status.includes(p.status)); if (filter.visibility?.length) plans = plans.filter((p) => filter.visibility.includes(p.visibility)); if (filter.query) { const q = filter.query.toLowerCase(); plans = plans.filter((p) => (p.title || '').toLowerCase().includes(q)); } plans = plans.slice(0, filter.limit || 50); const summary = plans.reduce( (acc, p) => { acc[p.status] = (acc[p.status] || 0) + 1; acc.total += 1; return acc; }, { total: 0 }, ); return formatResponse({ as_of: asOf(), summary, plans: plans.map((p) => ({ id: p.id, title: p.title, status: p.status, visibility: p.visibility, owner_id: p.owner_id || p.ownerId, updated_at: p.updated_at || p.updatedAt, // Surface progress + tether info when the API decorates the rows; // listPlans on the v2 API now bulk-loads stats + goal_tethers. progress: p.progress ?? p.stats?.percentage, goal_tethers: p.goal_tethers, })), }); } catch (err) { return errorResponse('upstream_unavailable', `list_plans failed: ${err.response?.data?.error || err.message}`); } } // ───────────────────────────────────────────────────────────────────────── // search — universal text search. // ───────────────────────────────────────────────────────────────────────── const searchDefinition = { name: 'search', description: 'Text search across plans, nodes, and content. Use for finding entities by title or fragment.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, scope: { type: 'string', enum: ['global', 'plans', 'plan', 'node'], default: 'global' }, scope_id: { type: 'string' }, filters: { type: 'object', properties: { status: { type: 'string' }, type: { type: 'string' }, limit: { type: 'integer', default: 20 }, }, }, }, required: ['query'], }, }; async function searchHandler(args, apiClient) { const { query, scope = 'global', scope_id, filters = {} } = args; const limit = filters.limit || 20; try { let result; // Map MCP scopes onto the actual api-client surface. The handler // previously called apiClient.search.{global,plans,inPlan,inNode} // — none of those methods exist, so every invocation 500'd. The // real api-client exposes globalSearch + searchPlan only; for // 'plans' and 'node' we filter the global result client-side // instead of hitting a (non-existent) per-bucket endpoint. if (scope === 'plan') { if (!scope_id) return errorResponse('invalid_arg', 'search scope=plan requires scope_id'); result = await apiClient.search.searchPlan(scope_id, query); } else { const global = await apiClient.search.globalSearch(query); const all = Array.isArray(global?.results) ? global.results : []; const matchScope = (r) => { if (scope === 'global') return true; if (scope === 'plans') return r.type === 'plan'; if (scope === 'node') return r.type === 'node' && (!scope_id || r.plan_id === scope_id); return true; }; const matchType = (r) => !filters.type || r.type === filters.type; const matchStatus = (r) => !filters.status || r.status === filters.status; const filtered = all.filter((r) => matchScope(r) && matchType(r) && matchStatus(r)).slice(0, limit); result = { results: filtered, count: filtered.length, query, scope }; } return formatResponse({ as_of: asOf(), ...(result || {}) }); } catch (err) { return errorResponse('upstream_unavailable', `Search failed: ${err.response?.data?.error || err.message}`); } } // ───────────────────────────────────────────────────────────────────────── // plan_analysis — advanced reads (impact, critical_path, bottlenecks, coherence). // ───────────────────────────────────────────────────────────────────────── const planAnalysisDefinition = { name: 'plan_analysis', description: "Advanced plan reads: impact analysis (delay/block/remove), critical path, " + "bottleneck list, or coherence check.", inputSchema: { type: 'object', properties: { plan_id: { type: 'string' }, type: { type: 'string', enum: ['impact', 'critical_path', 'bottlenecks', 'coherence'] }, node_id: { type: 'string' }, scenario: { type: 'string', enum: ['delay', 'block', 'remove'] }, }, required: ['plan_id', 'type'], }, }; async function planAnalysisHandler(args, apiClient) { const { plan_id, type, node_id, scenario } = args; try { let result; if (type === 'critical_path') { result = (await apiClient.axiosInstance.get(`/plans/${plan_id}/critical-path`)).data; } else if (type === 'bottlenecks') { result = (await apiClient.axiosInstance.get(`/plans/${plan_id}/bottlenecks`)).data; } else if (type === 'impact') { if (!node_id) return errorResponse('invalid_arg', 'plan_analysis type=impact requires node_id'); const params = new URLSearchParams({ scenario: scenario || 'block' }); result = (await apiClient.axiosInstance.get(`/plans/${plan_id}/nodes/${node_id}/impact?${params}`)).data; } else if (type === 'coherence') { result = await apiClient.coherence.runCheck(plan_id); } return formatResponse({ as_of: asOf(), type, results: result || {} }); } catch (err) { return errorResponse('upstream_unavailable', `plan_analysis failed: ${err.response?.data?.error || err.message}`); } } module.exports = { - src/tools/bdi/index.js:1-46 (registration)Aggregates all BDI tool definitions and handlers (including briefing) and provides bdiToolHandler dispatch function. Exported as bdiToolDefinitions, bdiToolHandler, bdiToolNames to src/tools.js.
/** * BDI-aligned MCP tool surface (v0.9.0). * * Tools grouped by Belief / Desire / Intention namespaces. Each tool answers * one whole agentic question, replaces multiple legacy calls, and emits an * `as_of` ISO 8601 timestamp on success. * * See ../../../docs/MCP_REDESIGN_PLAN.md for full specs and rationale. */ const beliefs = require('./beliefs'); const desires = require('./desires'); const intentions = require('./intentions'); const utility = require('./utility'); const definitions = [ ...beliefs.definitions, ...desires.definitions, ...intentions.definitions, ...utility.definitions, ]; const handlers = { ...beliefs.handlers, ...desires.handlers, ...intentions.handlers, ...utility.handlers, }; const names = new Set(definitions.map((t) => t.name)); /** * Dispatch a BDI tool call. * @returns formatted MCP response, or undefined if the name isn't a BDI tool. */ async function bdiToolHandler(name, args, apiClient) { if (!names.has(name)) return undefined; const handler = handlers[name]; return handler(args || {}, apiClient); } module.exports = { bdiToolDefinitions: definitions, bdiToolHandler, bdiToolNames: names, }; - src/tools.js:1-64 (registration)Top-level MCP tool setup. Registers all BDI tool definitions (including briefing) with the MCP server via ListToolsRequestSchema, and dispatches calls via CallToolRequestSchema to bdiToolHandler.
/** * MCP Tools — v0.9.0 BDI-aligned surface. * * 15 tools across Belief / Desire / Intention / Utility namespaces. The legacy * 63-tool CRUD-shaped surface was removed in v0.9.0 — see ../docs/MIGRATION_v0.9.md * for the mapping from old tool names to new ones, and ../docs/MCP_REDESIGN_PLAN.md * for the design rationale. */ const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js'); const defaultApiClient = require('./api-client'); const { bdiToolDefinitions, bdiToolHandler, bdiToolNames } = require('./tools/bdi'); /** * Wire BDI tools into an MCP server. * @param {Server} server - MCP server instance * @param {Object} [apiClientOverride] - Per-session API client (HTTP mode); falls back to default (stdio mode) */ function setupTools(server, apiClientOverride) { const apiClient = apiClientOverride || defaultApiClient; if (process.env.NODE_ENV === 'development') { console.error(`Setting up MCP tools (${bdiToolDefinitions.length} BDI tools)`); } server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: bdiToolDefinitions }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (process.env.NODE_ENV === 'development') { console.error(`Calling tool: ${name}`); } if (!bdiToolNames.has(name)) { return { isError: true, content: [{ type: 'text', text: `Unknown tool: ${name}. v0.9.0 ships 15 BDI tools. Run get_started to see them, or check ../docs/MIGRATION_v0.9.md for the legacy → BDI mapping.`, }], }; } try { return await bdiToolHandler(name, args, apiClient); } catch (err) { if (process.env.NODE_ENV === 'development') { console.error(`Tool ${name} threw:`, err); } return { isError: true, content: [{ type: 'text', text: `Tool ${name} failed: ${err.message || String(err)}`, }], }; } }); } module.exports = { setupTools };