Skip to main content
Glama
TAgents

Planning System MCP Server

by TAgents

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

TableJSON Schema
NameRequiredDescriptionDefault
scopeNomission_control
goal_idNo
plan_idNo
recent_window_hoursNo

Implementation Reference

  • 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 },
      });
    }
  • 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 },
        },
      },
    };
  • 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 = {
  • 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 };
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description lists the types of information returned (goal health, pending decisions, tasks, activity, recommendation) but does not disclose side effects, authentication needs, or rate limits. With no annotations present, the description provides basic behavioral context but lacks depth.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences: the first explains functionality, the second provides usage guidance. No redundancy, front-loaded with critical information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers purpose and usage well but completely omits parameter semantics, which are needed for correct invocation given 4 optional parameters. Without an output schema, more detail on return values would also help. Adequate but with clear gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters1/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema defines 4 parameters (scope, goal_id, plan_id, recent_window_hours) with 0% schema description coverage. The description does not explain the meaning, defaults, or valid values of any parameter, forcing the agent to infer from names alone.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool aggregates key mission control information in one call ('goal health summary, pending decisions, my tasks, recent activity, and a top recommendation'), and it positions itself as the single read for Cowork live artifacts, distinguishing it from siblings like goal_state or plan_analysis.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly recommends using this as 'the single read for Cowork live artifacts and the autopilot's first call,' providing clear context for when to invoke it. However, it does not explicitly state when not to use it or mention alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TAgents/agent-planner-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server