agenthub-multiagent-mcp 1.30.0 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -239,6 +239,14 @@ export function registerTools() {
239
239
  properties: {},
240
240
  },
241
241
  },
242
+ {
243
+ name: "brain_flush_session",
244
+ description: "Flush this session's accumulated activity into the local searchable brain (.mv2) and the server brain_sessions table WITHOUT disconnecting the agent. Call this at session close (e.g. from /close-session) so the brain is current even where the on-process-exit flush is unreliable (notably Windows, where SIGTERM never fires). Idempotent and best-effort.",
245
+ inputSchema: {
246
+ type: "object",
247
+ properties: {},
248
+ },
249
+ },
242
250
  // Time orientation (add-time-aware-agents §1.3)
243
251
  {
244
252
  name: "now",
@@ -715,6 +723,64 @@ export function registerTools() {
715
723
  required: ["id"],
716
724
  },
717
725
  },
726
+ {
727
+ name: "calendar_schedule_meeting",
728
+ description: "Schedule a Google Calendar meeting with an auto-generated Google Meet link. Idempotent on dedupe_key (same key returns the same event). Requires the calendar capability to be enabled on the server.",
729
+ inputSchema: {
730
+ type: "object",
731
+ properties: {
732
+ title: { type: "string", description: "Meeting title" },
733
+ start: { type: "string", description: "Start time (RFC3339, e.g. 2026-06-02T10:00:00Z)" },
734
+ end: { type: "string", description: "End time (RFC3339); must be after start" },
735
+ attendee_emails: {
736
+ type: "array",
737
+ items: { type: "string" },
738
+ description: "Invitee email addresses",
739
+ },
740
+ description: { type: "string", description: "Optional meeting description" },
741
+ timezone: { type: "string", description: "Optional IANA timezone; defaults to the server default (UTC)" },
742
+ recurrence: { type: "string", description: "Optional RRULE, e.g. RRULE:FREQ=WEEKLY;BYDAY=MO" },
743
+ dedupe_key: { type: "string", description: "Idempotency key; same key returns the same event" },
744
+ source: {
745
+ type: "object",
746
+ description: "Optional linkage to a blocker/task; a task source resolves the assignee as an attendee",
747
+ properties: {
748
+ kind: { type: "string", enum: ["blocker", "task", "manual"] },
749
+ id: { type: "string" },
750
+ },
751
+ },
752
+ },
753
+ required: ["title", "start", "end", "dedupe_key"],
754
+ },
755
+ },
756
+ {
757
+ name: "email_send",
758
+ description: "Send an email via Gmail to an allowlisted recipient (external partners not in Slack). Recipient must pass the allowlist; per-org daily cap applies. Supports templates (onboarding_invite, task_assignment, digest) or raw body_text/body_html.",
759
+ inputSchema: {
760
+ type: "object",
761
+ properties: {
762
+ to: {
763
+ type: "array",
764
+ items: { type: "string" },
765
+ description: "Recipient email addresses (each must pass the allowlist)",
766
+ },
767
+ subject: { type: "string", description: "Email subject (required for ad-hoc sends)" },
768
+ body_text: { type: "string", description: "Plain-text body (required for ad-hoc sends)" },
769
+ body_html: { type: "string", description: "Optional HTML body" },
770
+ template: {
771
+ type: "string",
772
+ enum: ["onboarding_invite", "task_assignment", "digest"],
773
+ description: "Optional template; when set, body_* are derived from template_vars",
774
+ },
775
+ template_vars: {
776
+ type: "object",
777
+ description: "Variables for the chosen template (e.g. {name, login_url})",
778
+ },
779
+ dedupe_key: { type: "string", description: "Optional idempotency key; a repeat is a no-op" },
780
+ },
781
+ required: ["to"],
782
+ },
783
+ },
718
784
  {
719
785
  name: "list_channels",
720
786
  description: "List all available channels",
@@ -736,6 +802,143 @@ export function registerTools() {
736
802
  },
737
803
  },
738
804
  },
805
+ {
806
+ name: "api_search",
807
+ description: "Search the API knowledge graph — endpoints (REST/MCP/GraphQL/gRPC) extracted from indexed repos across the org. Use this BEFORE building a new endpoint to check if it already exists or to match a sibling project's contract. Org-wide read.",
808
+ inputSchema: {
809
+ type: "object",
810
+ properties: {
811
+ query: { type: "string", description: "Free-text match over endpoint identity + description" },
812
+ project_id: { type: "string", description: "Filter to one project (optional)" },
813
+ surface: { type: "string", description: "Filter by surface: rest | mcp | graphql | grpc (optional)" },
814
+ },
815
+ },
816
+ },
817
+ {
818
+ name: "api_get_endpoint",
819
+ description: "Get one API endpoint's full contract (handler ref, schemas, auth, description) plus its edges, by identity (e.g. \"POST /api/x\").",
820
+ inputSchema: {
821
+ type: "object",
822
+ properties: {
823
+ identity: { type: "string", description: "Endpoint identity, e.g. \"POST /api/x\" (REST) or a tool name (MCP)" },
824
+ project_id: { type: "string", description: "Project to look in (optional; org-wide if omitted)" },
825
+ },
826
+ required: ["identity"],
827
+ },
828
+ },
829
+ {
830
+ name: "api_endpoint_impact",
831
+ description: "Show an endpoint's blast radius before you change it: the consumers, models, and database tables it touches (plus linked openspec changes / feature-graph nodes).",
832
+ inputSchema: {
833
+ type: "object",
834
+ properties: {
835
+ identity: { type: "string", description: "Endpoint identity, e.g. \"POST /api/x\"" },
836
+ project_id: { type: "string", description: "Project to look in (optional)" },
837
+ },
838
+ required: ["identity"],
839
+ },
840
+ },
841
+ {
842
+ name: "api_graph",
843
+ description: "Return a project's API endpoint subgraph (endpoints with their edges) — for an overview of what a project exposes.",
844
+ inputSchema: {
845
+ type: "object",
846
+ properties: {
847
+ project_id: { type: "string", description: "Project to render (optional; org-wide if omitted)" },
848
+ surface: { type: "string", description: "Filter by surface: rest | mcp | graphql | grpc (optional)" },
849
+ },
850
+ },
851
+ },
852
+ {
853
+ name: "action_item_create",
854
+ description: "Create a lightweight action item — a tracked to-do assigned to a person (e.g. a task captured from chat). Distinct from the openspec-linked ticket hierarchy; promote to a ticket task when it becomes real dev work. Requires the deployment to have action items enabled.",
855
+ inputSchema: {
856
+ type: "object",
857
+ properties: {
858
+ title: { type: "string", description: "Short description of the action item" },
859
+ assignee_user_id: {
860
+ type: "string",
861
+ description: "AgentHub user id to assign to (optional; created unassigned if omitted)",
862
+ },
863
+ details: { type: "string", description: "Optional longer details" },
864
+ due_at: { type: "string", description: "Optional due timestamp (RFC3339)" },
865
+ source_kind: {
866
+ type: "string",
867
+ description: "Origin: manual | slack_mention | slack_command | slack_shortcut | slack_reaction | slack_autoparse (default manual)",
868
+ },
869
+ source_ref: { type: "string", description: "Optional provenance link (e.g. a Slack permalink)" },
870
+ },
871
+ required: ["title"],
872
+ },
873
+ },
874
+ {
875
+ name: "action_item_list",
876
+ description: "List action items in your org, optionally filtered by assignee user id and/or status (open | snoozed | done | cancelled).",
877
+ inputSchema: {
878
+ type: "object",
879
+ properties: {
880
+ assignee: { type: "string", description: "Filter by assignee AgentHub user id" },
881
+ status: {
882
+ type: "string",
883
+ description: "Filter by status: open | snoozed | done | cancelled",
884
+ },
885
+ },
886
+ },
887
+ },
888
+ {
889
+ name: "action_item_complete",
890
+ description: "Mark an action item done.",
891
+ inputSchema: {
892
+ type: "object",
893
+ properties: {
894
+ id: { type: "string", description: "Action item id" },
895
+ },
896
+ required: ["id"],
897
+ },
898
+ },
899
+ {
900
+ name: "action_item_snooze",
901
+ description: "Snooze an action item until a given time (RFC3339).",
902
+ inputSchema: {
903
+ type: "object",
904
+ properties: {
905
+ id: { type: "string", description: "Action item id" },
906
+ until: { type: "string", description: "RFC3339 timestamp to snooze until" },
907
+ },
908
+ required: ["id", "until"],
909
+ },
910
+ },
911
+ {
912
+ name: "action_item_promote",
913
+ description: "Promote an action item into a tracked ticket task under a story (when an ad-hoc item turns out to be real dev work). Links the two; completing the ticket later marks the action item done.",
914
+ inputSchema: {
915
+ type: "object",
916
+ properties: {
917
+ id: { type: "string", description: "Action item id to promote" },
918
+ story_id: { type: "string", description: "Target story id the new task will live under" },
919
+ },
920
+ required: ["id", "story_id"],
921
+ },
922
+ },
923
+ {
924
+ name: "notify_prefs_set",
925
+ description: "Set your action-item digest + reminder preferences (the authenticated user's). Controls which channel the daily digest is delivered on and whether overdue reminders are sent.",
926
+ inputSchema: {
927
+ type: "object",
928
+ properties: {
929
+ digest_channel: {
930
+ type: "string",
931
+ description: "Where to deliver the digest: auto (Slack DM if linked, else email) | slack | email | both",
932
+ },
933
+ digest_hour_local: {
934
+ type: "number",
935
+ description: "Hour of day (0-23, local to tz) to send the daily digest",
936
+ },
937
+ tz: { type: "string", description: "IANA timezone name (e.g. Asia/Kolkata); default UTC" },
938
+ reminders_enabled: { type: "boolean", description: "Whether to send overdue reminders" },
939
+ },
940
+ },
941
+ },
739
942
  {
740
943
  name: "join_channel",
741
944
  description: "Subscribe to a channel to receive its messages",
@@ -1683,7 +1886,29 @@ export function registerTools() {
1683
1886
  description: "Get the TOON-encoded org brain context package for this agent. Includes own session history, blockers, org decisions, and alerts.",
1684
1887
  inputSchema: {
1685
1888
  type: "object",
1686
- properties: {},
1889
+ properties: {
1890
+ include_code: {
1891
+ type: "boolean",
1892
+ description: "add-code-brain: when true, append a code_index section with top org-wide code/doc matches relevant to recent sessions. Default false — behaviour is byte-identical to pre-change when omitted.",
1893
+ },
1894
+ },
1895
+ },
1896
+ },
1897
+ {
1898
+ name: "brain_code_recall",
1899
+ description: "add-code-brain: search the org-wide indexed code + docs (Markdown / RST / OpenAPI / HTML / Jupyter / PDF + tree-sitter symbols). Use when org_search returns nothing for a code or architecture question, or when you need to discover files/symbols that no agent has written down. Read-only.",
1900
+ inputSchema: {
1901
+ type: "object",
1902
+ properties: {
1903
+ query: { type: "string", description: "Free-text query — symbol name, doc title fragment, or natural-language phrase" },
1904
+ k: { type: "number", description: "Max hits per kind (default 5, max 50)" },
1905
+ kinds: {
1906
+ type: "array",
1907
+ items: { type: "string", enum: ["docs", "symbols"] },
1908
+ description: "Restrict to docs and/or symbols. Default both.",
1909
+ },
1910
+ },
1911
+ required: ["query"],
1687
1912
  },
1688
1913
  },
1689
1914
  {
@@ -2222,24 +2447,32 @@ export async function handleToolCall(name, args, client, context) {
2222
2447
  try {
2223
2448
  // Use provided name, or fall back to stored name
2224
2449
  const reconnectName = args.name || existingState.name;
2225
- const reconnectResult = await client.reconnectAgent(existingState.agent_id, existingState.token, owner, args.model, reconnectName);
2450
+ // fix-agent-register-project-binding §3 pass the tool's `project`
2451
+ // arg so reconnect binds project_id. The reconnect path previously
2452
+ // ignored it, leaving local state.project_id empty and
2453
+ // create_epic/create_task unusable despite a healthy session.
2454
+ const reconnectResult = await client.reconnectAgent(existingState.agent_id, existingState.token, owner, args.model, reconnectName, projectKey);
2226
2455
  context.setCurrentAgentId(existingState.agent_id);
2227
2456
  // Set connect token on client for org operations
2228
2457
  client.setConnectToken(existingState.token);
2229
2458
  // Store working_dir in context for other tools to use
2230
2459
  context.setWorkingDir(workingDir);
2231
- // Update state with returned name from server
2460
+ // Persist the server-returned project binding, falling back to the
2461
+ // existing values when the server did not resolve a project.
2232
2462
  state.saveState(workingDir, {
2233
2463
  ...existingState,
2234
2464
  name: reconnectResult.name,
2465
+ project_id: reconnectResult.project_id || existingState.project_id,
2466
+ project_key: reconnectResult.project_key || existingState.project_key,
2235
2467
  last_task: undefined, // Cleared on reconnect
2236
2468
  });
2469
+ const unresolvedNote = projectKey && !reconnectResult.project_resolved
2470
+ ? ` (note: project "${projectKey}" could not be resolved; project not bound)`
2471
+ : "";
2237
2472
  return {
2238
2473
  ...reconnectResult,
2239
2474
  mode: "reconnected",
2240
- message: reconnectResult.was_offline
2241
- ? `Reconnected as ${reconnectResult.name || existingState.agent_id}. You have ${reconnectResult.pending_tasks_count} pending tasks and ${reconnectResult.unread_messages_count} unread messages.`
2242
- : `Reconnected as ${reconnectResult.name || existingState.agent_id}. You have ${reconnectResult.pending_tasks_count} pending tasks and ${reconnectResult.unread_messages_count} unread messages.`,
2475
+ message: `Reconnected as ${reconnectResult.name || existingState.agent_id}. You have ${reconnectResult.pending_tasks_count} pending tasks and ${reconnectResult.unread_messages_count} unread messages.${unresolvedNote}`,
2243
2476
  };
2244
2477
  }
2245
2478
  catch (error) {
@@ -2595,6 +2828,19 @@ export async function handleToolCall(name, args, client, context) {
2595
2828
  } : null,
2596
2829
  };
2597
2830
  }
2831
+ case "brain_flush_session": {
2832
+ // Flush the current session to the local .mv2 + server WITHOUT
2833
+ // disconnecting. Mirrors gracefulShutdown's agent-id resolution so it
2834
+ // works even if agent_register was never called this process. The agent
2835
+ // stays registered for the next reconnect.
2836
+ const workingDir = context.getWorkingDir();
2837
+ const flushAgentId = agentId || (workingDir ? state.readHookAgentId(workingDir) : null);
2838
+ if (!flushAgentId) {
2839
+ return { flushed: false, reason: "no agent id resolved" };
2840
+ }
2841
+ const { mv2Written } = await brainCapture.flushSessionToBrain(client, flushAgentId);
2842
+ return { flushed: true, agent_id: flushAgentId, mv2_written: mv2Written };
2843
+ }
2598
2844
  case "now": {
2599
2845
  // No wrapping — server_time would otherwise overshadow the explicit
2600
2846
  // tool result. Force-refresh past the cache so the agent gets a fresh
@@ -2818,6 +3064,32 @@ export async function handleToolCall(name, args, client, context) {
2818
3064
  const result = await client.getAgent(args.id);
2819
3065
  return wrapWithPendingItems(client, agentId, result, context);
2820
3066
  }
3067
+ case "calendar_schedule_meeting": {
3068
+ const result = await client.scheduleMeeting({
3069
+ title: args.title,
3070
+ start: args.start,
3071
+ end: args.end,
3072
+ attendee_emails: args.attendee_emails,
3073
+ description: args.description,
3074
+ timezone: args.timezone,
3075
+ recurrence: args.recurrence,
3076
+ dedupe_key: args.dedupe_key,
3077
+ source: args.source,
3078
+ });
3079
+ return wrapWithPendingItems(client, agentId, result, context);
3080
+ }
3081
+ case "email_send": {
3082
+ const result = await client.sendEmail({
3083
+ to: args.to,
3084
+ subject: args.subject,
3085
+ body_text: args.body_text,
3086
+ body_html: args.body_html,
3087
+ template: args.template,
3088
+ template_vars: args.template_vars,
3089
+ dedupe_key: args.dedupe_key,
3090
+ });
3091
+ return wrapWithPendingItems(client, agentId, result, context);
3092
+ }
2821
3093
  case "list_channels": {
2822
3094
  const result = await client.listChannels();
2823
3095
  return wrapWithPendingItems(client, agentId, result, context);
@@ -2828,6 +3100,71 @@ export async function handleToolCall(name, args, client, context) {
2828
3100
  const result = await client.listProjects(args.include_archived);
2829
3101
  return wrapWithPendingItems(client, agentId, result, context);
2830
3102
  }
3103
+ // add-api-knowledge-graph §3 — org-wide read of the API graph.
3104
+ case "api_search": {
3105
+ const result = await client.apiSearch({
3106
+ query: args.query,
3107
+ project_id: args.project_id,
3108
+ surface: args.surface,
3109
+ });
3110
+ return wrapWithPendingItems(client, agentId, result, context);
3111
+ }
3112
+ case "api_get_endpoint": {
3113
+ const result = await client.apiGetEndpoint(args.identity, args.project_id);
3114
+ return wrapWithPendingItems(client, agentId, result, context);
3115
+ }
3116
+ case "api_endpoint_impact": {
3117
+ const result = await client.apiEndpointImpact(args.identity, args.project_id);
3118
+ return wrapWithPendingItems(client, agentId, result, context);
3119
+ }
3120
+ case "api_graph": {
3121
+ const result = await client.apiGraph({
3122
+ project_id: args.project_id,
3123
+ surface: args.surface,
3124
+ });
3125
+ return wrapWithPendingItems(client, agentId, result, context);
3126
+ }
3127
+ // add-action-items Phase 1 — org-scoped (auth via the agent's connect
3128
+ // token); no agentId guard, mirroring list_projects.
3129
+ case "action_item_create": {
3130
+ const result = await client.createActionItem({
3131
+ title: args.title,
3132
+ assignee_user_id: args.assignee_user_id,
3133
+ details: args.details,
3134
+ due_at: args.due_at,
3135
+ source_kind: args.source_kind,
3136
+ source_ref: args.source_ref,
3137
+ });
3138
+ return wrapWithPendingItems(client, agentId, result, context);
3139
+ }
3140
+ case "action_item_list": {
3141
+ const result = await client.listActionItems({
3142
+ assignee: args.assignee,
3143
+ status: args.status,
3144
+ });
3145
+ return wrapWithPendingItems(client, agentId, result, context);
3146
+ }
3147
+ case "action_item_complete": {
3148
+ const result = await client.completeActionItem(args.id);
3149
+ return wrapWithPendingItems(client, agentId, result, context);
3150
+ }
3151
+ case "action_item_snooze": {
3152
+ const result = await client.snoozeActionItem(args.id, args.until);
3153
+ return wrapWithPendingItems(client, agentId, result, context);
3154
+ }
3155
+ case "action_item_promote": {
3156
+ const result = await client.promoteActionItem(args.id, args.story_id);
3157
+ return wrapWithPendingItems(client, agentId, result, context);
3158
+ }
3159
+ case "notify_prefs_set": {
3160
+ const result = await client.setNotifyPrefs({
3161
+ digest_channel: args.digest_channel,
3162
+ digest_hour_local: args.digest_hour_local,
3163
+ tz: args.tz,
3164
+ reminders_enabled: args.reminders_enabled,
3165
+ });
3166
+ return wrapWithPendingItems(client, agentId, result, context);
3167
+ }
2831
3168
  case "join_channel": {
2832
3169
  if (!agentId)
2833
3170
  throw new Error("Not registered. Call agent_register first.");
@@ -3708,7 +4045,35 @@ export async function handleToolCall(name, args, client, context) {
3708
4045
  case "brain_get_context": {
3709
4046
  if (!agentId)
3710
4047
  throw new Error("Not registered. Call agent_register first.");
3711
- return client.getBrainContext(agentId);
4048
+ const includeCode = Boolean(args.include_code);
4049
+ const qs = includeCode ? "?include_code=true" : "";
4050
+ // Use raw GET so the include_code flag round-trips without a typed
4051
+ // client helper. Server-side decides whether to append code_index.
4052
+ return client.get(`/brain/context/${encodeURIComponent(agentId)}${qs}`);
4053
+ }
4054
+ case "brain_code_recall": {
4055
+ const query = args.query || "";
4056
+ if (!query)
4057
+ throw new Error("query is required");
4058
+ const k = args.k || 5;
4059
+ const kindsArr = args.kinds || ["docs", "symbols"];
4060
+ const params = new URLSearchParams();
4061
+ params.set("q", query);
4062
+ params.set("k", String(k));
4063
+ params.set("kinds", kindsArr.join(","));
4064
+ try {
4065
+ return await client.get(`/brain/code/search?${params.toString()}`);
4066
+ }
4067
+ catch (e) {
4068
+ const msg = e instanceof Error ? e.message : String(e);
4069
+ if (msg.includes("code_brain_disabled")) {
4070
+ return {
4071
+ error: "code_brain_disabled",
4072
+ message: "The code brain is disabled on this server. It defaults to enabled — ask the operator to unset AGENTHUB_CODE_BRAIN_ENABLED (or set it back to true) and restart.",
4073
+ };
4074
+ }
4075
+ throw e;
4076
+ }
3712
4077
  }
3713
4078
  case "brain_list_sessions": {
3714
4079
  const targetAgent = args.agent_id || agentId;