@integrity-labs/agt-cli 0.28.1111 → 0.28.1113

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.
@@ -39711,6 +39711,51 @@ var OAUTH_PROVIDERS = {
39711
39711
  }
39712
39712
  ]
39713
39713
  },
39714
+ "otter": {
39715
+ // CS-2029: Otter.ai's official MCP — remote streamable-HTTP at
39716
+ // https://mcp.otter.ai/mcp. Same Granola/Kajabi shape: OAuth 2.0
39717
+ // authorization-code with PKCE (S256) and a public client
39718
+ // (token_endpoint_auth_method 'none') issued via Dynamic Client
39719
+ // Registration (RFC 7591). Values below are taken verbatim from the
39720
+ // metadata, not inferred:
39721
+ // - https://mcp.otter.ai/.well-known/oauth-protected-resource names the
39722
+ // AS as https://otter.ai/ and advertises scopes profile:read,
39723
+ // conversations:read;
39724
+ // - https://otter.ai/.well-known/oauth-authorization-server gives
39725
+ // authorize /oauth2/authorize, token /oauth/token, revoke
39726
+ // /oauth/revoke_token, register /oauth/register, PKCE S256, `none`,
39727
+ // and the refresh_token grant.
39728
+ // NOTE the authorize path is /oauth2/authorize while token/revoke live
39729
+ // under /oauth/ — that is Otter's own split, do not "normalise" it.
39730
+ // The prod client was registered 2026-10-10 against /oauth/register with
39731
+ // scope 'profile:read conversations:read' and BOTH API callbacks,
39732
+ // https://api.augmented.team/... and https://api.ninjafy.com/integrations/oauth/callback,
39733
+ // because redirect_uri follows API_PUBLIC_URL and the ENG-11060 flip makes
39734
+ // the ninjafy name canonical; Otter returns no RFC 7592 handle, so a redirect
39735
+ // cannot be added later without a new client and a re-consent per connection.
39736
+ // OAUTH_OTTER_CLIENT_ID is set from its output (docs/runbooks/otter-dogfood.md). Otter advertises ONLY
39737
+ // read scopes, so this integration is read-only by construction: there is
39738
+ // no write scope to request even by mistake.
39739
+ definitionId: "otter",
39740
+ authorizeUrl: "https://otter.ai/oauth2/authorize",
39741
+ tokenUrl: "https://otter.ai/oauth/token",
39742
+ revokeUrl: "https://otter.ai/oauth/revoke_token",
39743
+ defaultScopes: ["profile:read", "conversations:read"],
39744
+ supportsRefresh: true,
39745
+ extraAuthorizeParams: {},
39746
+ clientAuthMethod: "body",
39747
+ pkce: "S256",
39748
+ publicClient: true,
39749
+ mcpUrl: "https://mcp.otter.ai/mcp",
39750
+ // Curated surface (matches the catalog seed's defined_scopes[].tools).
39751
+ // These are the server's three tools, all read-only. The names are
39752
+ // Otter's own, quoted from the tools' descriptions ("use `otter_search`
39753
+ // first", "call otter_get_user_info() first"), not Composio's wrapped
39754
+ // OTTER_AI_MCP_* slugs. An allowlisted name the server does not advertise
39755
+ // is logged and SKIPPED by the proxy, so confirm against the first live
39756
+ // tools/list after a real consent (runbook Step 3).
39757
+ toolAllowlist: ["otter_get_user_info", "otter_search", "otter_fetch"]
39758
+ },
39714
39759
  "notion-cli": {
39715
39760
  // Notion's public OAuth app. Tokens are workspace-scoped and long-lived —
39716
39761
  // Notion does not issue refresh_tokens, so `supportsRefresh: false` and
@@ -40344,6 +40389,34 @@ var INTEGRATION_REGISTRY = [
40344
40389
  docs_url: "https://help.kajabi.com/articles/api-integrations/connect-kajabi-to-claude-or-chatgpt",
40345
40390
  beta: true
40346
40391
  },
40392
+ {
40393
+ id: "otter",
40394
+ name: "Otter.ai",
40395
+ category: "knowledge",
40396
+ description: "Read Otter.ai meetings: search recordings by date, attendee or topic, and read summaries, action items and full speaker-attributed transcripts.",
40397
+ // CS-2029: same remote streamable-HTTP MCP + OAuth pattern as Granola/Kajabi.
40398
+ // Otter's server implements the MCP discovery chain (RFC 9728 protected-
40399
+ // resource metadata at mcp.otter.ai → RFC 8414 AS metadata at otter.ai →
40400
+ // RFC 7591 registration); auth is OAuth 2.0 authorization-code with PKCE
40401
+ // (S256) and a public client registered via one-time Dynamic Client
40402
+ // Registration (scripts/dcr-register.ts against https://otter.ai/oauth/register
40403
+ // → OAUTH_OTTER_CLIENT_ID, --scope 'profile:read conversations:read').
40404
+ // Consent is brokered by the webapp through the shared
40405
+ // /integrations/oauth/authorize → /callback path, and the access_token
40406
+ // reaches the host via the generic remote-oauth-proxy
40407
+ // (OAUTH_PROVIDERS.otter.mcpUrl). Read-only by construction: Otter
40408
+ // advertises no write scope.
40409
+ supported_auth_types: ["oauth2"],
40410
+ capabilities: [
40411
+ { id: "otter:read", name: "Read Meetings & Transcripts", description: "Find recorded meetings by date, attendee, topic or title, and read their summaries, action items, outlines and full transcripts (otter_get_user_info, otter_search, otter_fetch)", access: "read" }
40412
+ ],
40413
+ // Otter's own Help Center article for the MCP server (CodeRabbit, #6906). It
40414
+ // exists even though help.otter.ai answers 403 to non-browser clients; the
40415
+ // help-centre API confirmed it (title "Otter MCP Server"). The MCP endpoint
40416
+ // itself is a JSON-RPC URL, not a page (Obi Wan 8305072d5414).
40417
+ docs_url: "https://help.otter.ai/hc/en-us/articles/35287607569687-Otter-MCP-Server",
40418
+ beta: true
40419
+ },
40347
40420
  {
40348
40421
  id: "anchor-browser",
40349
40422
  name: "Anchor Browser",
@@ -44619,14 +44692,37 @@ var FLAG_REGISTRY = [
44619
44692
  },
44620
44693
  {
44621
44694
  key: "platform-maintenance-mode",
44622
- description: 'Platform-wide maintenance mode (ENG-6506): when on, every agent across every team/org auto-replies to admitted human inbound with a "we are offline for maintenance" notice instead of dispatching to the agent \u2014 for whole-platform downtime such as DB upgrades. Read host-side per inbound from the flags cache (no MCP restart). Boolean gate; ships dark \u2014 when off, channels behave exactly as today. (Proactive "back online" follow-up is the ENG-6508 fast-follow, not this flag.)',
44623
- flagType: "boolean",
44624
- // Declared safe value is `false` (normal operation). Fail-safe direction: a
44625
- // flag-DB read error or absent flag map must never silence the fleet on its
44626
- // own — agents stay responsive unless an operator deliberately flips this on
44627
- // from the admin Feature Flags page. No envVar: net-new control, no
44628
- // pre-flags env gate to migrate.
44629
- defaultValue: false,
44695
+ description: 'Platform-wide maintenance mode (ENG-6506; widened to modes by ENG-11436). off = normal operation (ships dark; channels behave exactly as today). tools-offline = every agent KEEPS conversing but every Augmented tool that WRITES to the control plane refuses with a reason, so nothing is created during a window whose database restore would discard it; reads are untouched. fleet-offline = the previous boolean-true behaviour AND the write gate: admitted human inbound gets a "we are offline for maintenance" notice instead of being dispatched, for whole-platform downtime such as DB upgrades. The modes are CUMULATIVE \u2014 fleet-offline gates writes too, because silencing channel inbound does not stop a /loop or a scheduled task filing work. Read host-side per inbound and per tool call from the flags cache (no MCP restart). (Proactive "back online" follow-up is the ENG-6508 fast-follow, not this flag.)',
44696
+ flagType: "enum",
44697
+ allowedValues: ["off", "tools-offline", "fleet-offline"],
44698
+ // Declared safe value is `off` (normal operation). Fail-safe direction: a
44699
+ // flag-DB read error or absent flag map must never silence the fleet OR take
44700
+ // its write tools away on its own — agents stay fully functional unless an
44701
+ // operator deliberately sets a mode from the admin Feature Flags page. No
44702
+ // envVar: net-new control, no pre-flags env gate to migrate.
44703
+ //
44704
+ // ENG-11436 widened this from a boolean rather than adding a second flag, so
44705
+ // there is ONE operator control whose modes are mutually exclusive by type
44706
+ // instead of two similar booleans with four combinations.
44707
+ //
44708
+ // The host resolver GRANDFATHERS a boolean-shaped cache value
44709
+ // (true -> fleet-offline, false -> off), and the DIRECTION it protects is
44710
+ // narrower than an earlier version of this comment claimed (Obi Wan
44711
+ // `e3f0d2c154b9` on #6903). It said "an older manager still writing a boolean
44712
+ // must not make a central flip a silent no-op". That situation cannot arise:
44713
+ // once this registry change deploys, normalizeFlagValue only ever yields one
44714
+ // of the three strings, feature-flags-host.ts writes the heartbeat map
44715
+ // verbatim, so no manager writes a boolean again. What grandfathering
44716
+ // actually covers is a NEW MCP reading a STALE PRE-DEPLOY cache that still
44717
+ // holds one.
44718
+ //
44719
+ // The direction that DOES happen is not handled and cannot be: an OLD MCP
44720
+ // reads the new string with resolveHostBooleanFlag, gets undefined, and keeps
44721
+ // dispatching. That is a real regression against ENG-6506 until the fleet
44722
+ // converges, and it is why the runbook makes convergence a deploy-order
44723
+ // PRECONDITION rather than offering a boolean fallback — there is none, since
44724
+ // an enum flag rejects `{value: true}` with a 400.
44725
+ defaultValue: "off",
44630
44726
  // Flipping this takes the ENTIRE fleet offline to end users; a mis-flip is a
44631
44727
  // platform-wide availability incident, so mutations require explicit
44632
44728
  // confirmation (the admin flip-reach modal).
@@ -30741,6 +30741,37 @@ function resolveGhostReplyMode(opts) {
30741
30741
  if (cached2 !== void 0) return cached2;
30742
30742
  return "off";
30743
30743
  }
30744
+ var MAINTENANCE_FLAG_KEY = "platform-maintenance-mode";
30745
+ function parseMaintenanceMode(raw) {
30746
+ if (raw === void 0) return void 0;
30747
+ const v = raw.trim().toLowerCase();
30748
+ if (v === "") return void 0;
30749
+ if (v === "off" || v === "tools-offline" || v === "fleet-offline") return v;
30750
+ const b = envBoolean(raw);
30751
+ if (b === true) return "fleet-offline";
30752
+ if (b === false) return "off";
30753
+ return void 0;
30754
+ }
30755
+ function cachedMaintenanceMode(path) {
30756
+ try {
30757
+ if (!existsSync(path)) return void 0;
30758
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
30759
+ if (!parsed || typeof parsed !== "object") return void 0;
30760
+ const flags = parsed.flags;
30761
+ if (!flags || typeof flags !== "object") return void 0;
30762
+ const value = flags[MAINTENANCE_FLAG_KEY];
30763
+ if (typeof value === "string") return parseMaintenanceMode(value);
30764
+ if (typeof value === "boolean") return value ? "fleet-offline" : "off";
30765
+ return void 0;
30766
+ } catch {
30767
+ return void 0;
30768
+ }
30769
+ }
30770
+ function resolveMaintenanceMode(opts) {
30771
+ const cached2 = cachedMaintenanceMode(opts?.cachePath ?? defaultFlagsCachePath());
30772
+ if (cached2 !== void 0) return cached2;
30773
+ return "off";
30774
+ }
30744
30775
  var SLACK_HOT_THREAD_FLAG_KEY = "slack-hot-thread-guard";
30745
30776
  function parseSlackHotThreadMode(raw) {
30746
30777
  if (raw === void 0) return void 0;
@@ -30813,17 +30844,157 @@ function resolveSlackScheduledChannelGuardMode(opts) {
30813
30844
  }
30814
30845
 
30815
30846
  // src/maintenance-mode.ts
30816
- var FLAG_KEY = "platform-maintenance-mode";
30817
30847
  var MAINTENANCE_OFFLINE_MESSAGE = "The Ninjafy platform is offline for scheduled maintenance right now, so I can't pick this up \u2014 please try again shortly.";
30818
30848
  function isMaintenanceModeActive(opts) {
30819
- return resolveHostBooleanFlag({
30820
- key: FLAG_KEY,
30821
- envVar: "",
30822
- defaultValue: false,
30849
+ return resolveMaintenanceMode({
30823
30850
  cachePath: opts?.cachePath,
30824
30851
  env: opts?.env
30825
- });
30852
+ }) === "fleet-offline";
30853
+ }
30854
+
30855
+ // src/maintenance-tools-offline.ts
30856
+ function isToolsOfflineActive(opts) {
30857
+ const mode = resolveMaintenanceMode({ cachePath: opts?.cachePath, env: opts?.env });
30858
+ return mode === "tools-offline" || mode === "fleet-offline";
30859
+ }
30860
+ var MAINTENANCE_GATED_TOOLS = /* @__PURE__ */ new Set([
30861
+ // Tasks.
30862
+ "kanban_add",
30863
+ "kanban_update",
30864
+ "kanban_move",
30865
+ "kanban_done",
30866
+ "kanban_reopen",
30867
+ "kanban_assign",
30868
+ "kanban_reassign",
30869
+ "kanban_recall",
30870
+ "kanban_respond",
30871
+ "kanban_progress",
30872
+ "kanban_log",
30873
+ "kanban_assign_project",
30874
+ "assign_kanban_to_human",
30875
+ // Canvases. `dashboards_request_refresh` is gated because it enqueues
30876
+ // server-side work, not because it renders anything itself.
30877
+ "dashboards_upsert",
30878
+ "dashboards_persist_widget",
30879
+ "dashboards_request_refresh",
30880
+ // ───────────────────────────────────────────────────────────────────────
30881
+ // FORWARDED writes (ENG-11436 / CodeRabbit on #6903). These reach the agent
30882
+ // through `registerForwardedApiTools()` -> `server.registerTool`, from
30883
+ // `packages/api/src/routes/mcp-tools.ts`, with names resolved at RUNTIME. They
30884
+ // are gated only because index.ts now wraps that method too; before that, this
30885
+ // whole block would have been a list the gate could not see.
30886
+ //
30887
+ // CodeRabbit named five (workflow_update, workflow_run_{start,phase,end},
30888
+ // memory_delete). Enumerating the surface to check them found 37 of the 50
30889
+ // forwarded tools unclassified, so the five were a subset — these are all of
30890
+ // the writes among them. The test now classifies that surface exhaustively, so
30891
+ // the next addition fails CI rather than slipping through.
30892
+ "composio_upload_file",
30893
+ "memory_delete",
30894
+ "memory_save",
30895
+ "onboarding_advance",
30896
+ "onboarding_provision_skills",
30897
+ "onboarding_provision_tasks",
30898
+ "onboarding_record_manager_contact",
30899
+ "request_batch_approval",
30900
+ "request_integration_access",
30901
+ "skill_contribute_fragment",
30902
+ "skill_create",
30903
+ "skill_delete",
30904
+ "skill_improve",
30905
+ "skill_propose_revision",
30906
+ "skill_publish_to_marketplace",
30907
+ "skill_update",
30908
+ "submit_performance_review",
30909
+ "workflow_run_end",
30910
+ "workflow_run_phase",
30911
+ "workflow_run_start",
30912
+ "workflow_update",
30913
+ // CHANNEL-SERVER write (Obi Wan `b3537c1e8495` on #6903). Dispatched by NAME in
30914
+ // slack-channel.ts / telegram-channel.ts / direct-chat-channel.ts, which are
30915
+ // separate processes that never call server.tool — so this one is enforced by
30916
+ // `refuseIfToolsOffline` at each dispatch point, not by the registration
30917
+ // wrapper. It inserts a `pending_interaction` row, the same durable
30918
+ // human-actionable record that gates request_buttons / request_form.
30919
+ "channel_request_input",
30920
+ // `slack.ask_user` is the SAME write under a dotted name, and it was missed
30921
+ // because the test's enumerator could not see dotted names at all (Obi Wan
30922
+ // `122ac55ff278` / `d7245eddd77f` on #6903). handleAskUser calls
30923
+ // runtime.createPendingInteraction -> POST /host/pending-interactions ->
30924
+ // inserts `pending_interaction`. Ungated, an agent on a /loop files the row,
30925
+ // the restore discards it, and the agent blocks forever on a resolution that
30926
+ // no longer exists — the exact failure the entry above is gated for.
30927
+ //
30928
+ // Verified it is the ONLY dotted channel tool that writes: createPendingInteraction
30929
+ // is CALLED once in the three channel servers (slack-channel.ts:8053);
30930
+ // slack-block-kit-runtime.ts only re-exports it.
30931
+ "slack.ask_user",
30932
+ // Workflows. FORWARDED, not local — it reaches the agent through
30933
+ // `server.registerTool` in registerForwardedApiTools, so it is gated only
30934
+ // because that method is now wrapped too (CodeRabbit on #6903).
30935
+ "workflow_create",
30936
+ // Knowledge.
30937
+ "knowledge_add",
30938
+ "knowledge_update",
30939
+ "knowledge_delete",
30940
+ // Projects and project tagging.
30941
+ "projects_create",
30942
+ "projects_update",
30943
+ "artefacts_assign_project",
30944
+ // Routines.
30945
+ "routine_propose",
30946
+ "routine_revise",
30947
+ "routine_retire",
30948
+ "submit_routine_output",
30949
+ // Standing rules.
30950
+ "rule_add",
30951
+ "rule_remove",
30952
+ // Scheduled work.
30953
+ "schedule_create",
30954
+ "schedule_update",
30955
+ "schedule_delete",
30956
+ // Requests that file a durable record a human is expected to act on. Filing
30957
+ // one mid-window is worse than refusing it: the record is discarded by the
30958
+ // restore, so the agent would have promised a human a follow-up that no
30959
+ // longer exists anywhere.
30960
+ "request_approval",
30961
+ "request_buttons",
30962
+ "request_channel_access",
30963
+ "request_form",
30964
+ "request_reconnect",
30965
+ "request_feature",
30966
+ "request_feature_withdraw",
30967
+ // ADR-0025 approval tools, registered by `approval-tools.ts` against the same
30968
+ // server object, so the wrapper in index.ts covers them too.
30969
+ "cancel_approval",
30970
+ // Status / identity.
30971
+ "status_standup",
30972
+ "status_update",
30973
+ "update_identity",
30974
+ "update_timezone",
30975
+ // Fleet changes.
30976
+ "recruit_agent",
30977
+ "integration_improve"
30978
+ ]);
30979
+ function toolsOfflineRefusalText(toolName) {
30980
+ return `Ninjafy is in a control-plane maintenance window, so \`${toolName}\` is unavailable right now. Nothing was created, changed or saved. Do NOT retry this call, and do not try a different Ninjafy tool to work around it \u2014 every tool that writes to the control plane is refusing for the same reason, and the window is short. Tell the person plainly that you cannot save this at the moment and ask them to bring it back afterwards; carry on with anything that does not need saving.`;
30981
+ }
30982
+ function refuseIfToolsOffline(name, opts) {
30983
+ if (!MAINTENANCE_GATED_TOOLS.has(name)) return null;
30984
+ const isActive = opts?.isActive ?? (() => isToolsOfflineActive());
30985
+ if (!isActive()) return null;
30986
+ return { content: [{ type: "text", text: toolsOfflineRefusalText(name) }] };
30987
+ }
30988
+ function shouldSendToolsOfflineNotice(opts) {
30989
+ const active = opts.isActive ?? (() => isToolsOfflineActive());
30990
+ if (!active()) return false;
30991
+ const now = opts.now ?? Date.now();
30992
+ const last = opts.cache.get(opts.key);
30993
+ if (last !== void 0 && now - last < opts.cooldownMs) return false;
30994
+ opts.cache.set(opts.key, now);
30995
+ return true;
30826
30996
  }
30997
+ var TOOLS_OFFLINE_USER_NOTICE = "Heads up \u2014 Ninjafy is in a maintenance window right now, so I can't save anything: no tasks, canvases, notes or scheduled items. I can still talk things through, and you can ask me to set it up once maintenance is done.";
30827
30998
 
30828
30999
  // ../core/dist/types/agent.js
30829
31000
  var FRAMEWORK_MODEL_SLOTS = {
@@ -33897,6 +34068,51 @@ var OAUTH_PROVIDERS = {
33897
34068
  }
33898
34069
  ]
33899
34070
  },
34071
+ "otter": {
34072
+ // CS-2029: Otter.ai's official MCP — remote streamable-HTTP at
34073
+ // https://mcp.otter.ai/mcp. Same Granola/Kajabi shape: OAuth 2.0
34074
+ // authorization-code with PKCE (S256) and a public client
34075
+ // (token_endpoint_auth_method 'none') issued via Dynamic Client
34076
+ // Registration (RFC 7591). Values below are taken verbatim from the
34077
+ // metadata, not inferred:
34078
+ // - https://mcp.otter.ai/.well-known/oauth-protected-resource names the
34079
+ // AS as https://otter.ai/ and advertises scopes profile:read,
34080
+ // conversations:read;
34081
+ // - https://otter.ai/.well-known/oauth-authorization-server gives
34082
+ // authorize /oauth2/authorize, token /oauth/token, revoke
34083
+ // /oauth/revoke_token, register /oauth/register, PKCE S256, `none`,
34084
+ // and the refresh_token grant.
34085
+ // NOTE the authorize path is /oauth2/authorize while token/revoke live
34086
+ // under /oauth/ — that is Otter's own split, do not "normalise" it.
34087
+ // The prod client was registered 2026-10-10 against /oauth/register with
34088
+ // scope 'profile:read conversations:read' and BOTH API callbacks,
34089
+ // https://api.augmented.team/... and https://api.ninjafy.com/integrations/oauth/callback,
34090
+ // because redirect_uri follows API_PUBLIC_URL and the ENG-11060 flip makes
34091
+ // the ninjafy name canonical; Otter returns no RFC 7592 handle, so a redirect
34092
+ // cannot be added later without a new client and a re-consent per connection.
34093
+ // OAUTH_OTTER_CLIENT_ID is set from its output (docs/runbooks/otter-dogfood.md). Otter advertises ONLY
34094
+ // read scopes, so this integration is read-only by construction: there is
34095
+ // no write scope to request even by mistake.
34096
+ definitionId: "otter",
34097
+ authorizeUrl: "https://otter.ai/oauth2/authorize",
34098
+ tokenUrl: "https://otter.ai/oauth/token",
34099
+ revokeUrl: "https://otter.ai/oauth/revoke_token",
34100
+ defaultScopes: ["profile:read", "conversations:read"],
34101
+ supportsRefresh: true,
34102
+ extraAuthorizeParams: {},
34103
+ clientAuthMethod: "body",
34104
+ pkce: "S256",
34105
+ publicClient: true,
34106
+ mcpUrl: "https://mcp.otter.ai/mcp",
34107
+ // Curated surface (matches the catalog seed's defined_scopes[].tools).
34108
+ // These are the server's three tools, all read-only. The names are
34109
+ // Otter's own, quoted from the tools' descriptions ("use `otter_search`
34110
+ // first", "call otter_get_user_info() first"), not Composio's wrapped
34111
+ // OTTER_AI_MCP_* slugs. An allowlisted name the server does not advertise
34112
+ // is logged and SKIPPED by the proxy, so confirm against the first live
34113
+ // tools/list after a real consent (runbook Step 3).
34114
+ toolAllowlist: ["otter_get_user_info", "otter_search", "otter_fetch"]
34115
+ },
33900
34116
  "notion-cli": {
33901
34117
  // Notion's public OAuth app. Tokens are workspace-scoped and long-lived —
33902
34118
  // Notion does not issue refresh_tokens, so `supportsRefresh: false` and
@@ -34536,6 +34752,34 @@ var INTEGRATION_REGISTRY = [
34536
34752
  docs_url: "https://help.kajabi.com/articles/api-integrations/connect-kajabi-to-claude-or-chatgpt",
34537
34753
  beta: true
34538
34754
  },
34755
+ {
34756
+ id: "otter",
34757
+ name: "Otter.ai",
34758
+ category: "knowledge",
34759
+ description: "Read Otter.ai meetings: search recordings by date, attendee or topic, and read summaries, action items and full speaker-attributed transcripts.",
34760
+ // CS-2029: same remote streamable-HTTP MCP + OAuth pattern as Granola/Kajabi.
34761
+ // Otter's server implements the MCP discovery chain (RFC 9728 protected-
34762
+ // resource metadata at mcp.otter.ai → RFC 8414 AS metadata at otter.ai →
34763
+ // RFC 7591 registration); auth is OAuth 2.0 authorization-code with PKCE
34764
+ // (S256) and a public client registered via one-time Dynamic Client
34765
+ // Registration (scripts/dcr-register.ts against https://otter.ai/oauth/register
34766
+ // → OAUTH_OTTER_CLIENT_ID, --scope 'profile:read conversations:read').
34767
+ // Consent is brokered by the webapp through the shared
34768
+ // /integrations/oauth/authorize → /callback path, and the access_token
34769
+ // reaches the host via the generic remote-oauth-proxy
34770
+ // (OAUTH_PROVIDERS.otter.mcpUrl). Read-only by construction: Otter
34771
+ // advertises no write scope.
34772
+ supported_auth_types: ["oauth2"],
34773
+ capabilities: [
34774
+ { id: "otter:read", name: "Read Meetings & Transcripts", description: "Find recorded meetings by date, attendee, topic or title, and read their summaries, action items, outlines and full transcripts (otter_get_user_info, otter_search, otter_fetch)", access: "read" }
34775
+ ],
34776
+ // Otter's own Help Center article for the MCP server (CodeRabbit, #6906). It
34777
+ // exists even though help.otter.ai answers 403 to non-browser clients; the
34778
+ // help-centre API confirmed it (title "Otter MCP Server"). The MCP endpoint
34779
+ // itself is a JSON-RPC URL, not a page (Obi Wan 8305072d5414).
34780
+ docs_url: "https://help.otter.ai/hc/en-us/articles/35287607569687-Otter-MCP-Server",
34781
+ beta: true
34782
+ },
34539
34783
  {
34540
34784
  id: "anchor-browser",
34541
34785
  name: "Anchor Browser",
@@ -39227,14 +39471,37 @@ var FLAG_REGISTRY = [
39227
39471
  },
39228
39472
  {
39229
39473
  key: "platform-maintenance-mode",
39230
- description: 'Platform-wide maintenance mode (ENG-6506): when on, every agent across every team/org auto-replies to admitted human inbound with a "we are offline for maintenance" notice instead of dispatching to the agent \u2014 for whole-platform downtime such as DB upgrades. Read host-side per inbound from the flags cache (no MCP restart). Boolean gate; ships dark \u2014 when off, channels behave exactly as today. (Proactive "back online" follow-up is the ENG-6508 fast-follow, not this flag.)',
39231
- flagType: "boolean",
39232
- // Declared safe value is `false` (normal operation). Fail-safe direction: a
39233
- // flag-DB read error or absent flag map must never silence the fleet on its
39234
- // own — agents stay responsive unless an operator deliberately flips this on
39235
- // from the admin Feature Flags page. No envVar: net-new control, no
39236
- // pre-flags env gate to migrate.
39237
- defaultValue: false,
39474
+ description: 'Platform-wide maintenance mode (ENG-6506; widened to modes by ENG-11436). off = normal operation (ships dark; channels behave exactly as today). tools-offline = every agent KEEPS conversing but every Augmented tool that WRITES to the control plane refuses with a reason, so nothing is created during a window whose database restore would discard it; reads are untouched. fleet-offline = the previous boolean-true behaviour AND the write gate: admitted human inbound gets a "we are offline for maintenance" notice instead of being dispatched, for whole-platform downtime such as DB upgrades. The modes are CUMULATIVE \u2014 fleet-offline gates writes too, because silencing channel inbound does not stop a /loop or a scheduled task filing work. Read host-side per inbound and per tool call from the flags cache (no MCP restart). (Proactive "back online" follow-up is the ENG-6508 fast-follow, not this flag.)',
39475
+ flagType: "enum",
39476
+ allowedValues: ["off", "tools-offline", "fleet-offline"],
39477
+ // Declared safe value is `off` (normal operation). Fail-safe direction: a
39478
+ // flag-DB read error or absent flag map must never silence the fleet OR take
39479
+ // its write tools away on its own — agents stay fully functional unless an
39480
+ // operator deliberately sets a mode from the admin Feature Flags page. No
39481
+ // envVar: net-new control, no pre-flags env gate to migrate.
39482
+ //
39483
+ // ENG-11436 widened this from a boolean rather than adding a second flag, so
39484
+ // there is ONE operator control whose modes are mutually exclusive by type
39485
+ // instead of two similar booleans with four combinations.
39486
+ //
39487
+ // The host resolver GRANDFATHERS a boolean-shaped cache value
39488
+ // (true -> fleet-offline, false -> off), and the DIRECTION it protects is
39489
+ // narrower than an earlier version of this comment claimed (Obi Wan
39490
+ // `e3f0d2c154b9` on #6903). It said "an older manager still writing a boolean
39491
+ // must not make a central flip a silent no-op". That situation cannot arise:
39492
+ // once this registry change deploys, normalizeFlagValue only ever yields one
39493
+ // of the three strings, feature-flags-host.ts writes the heartbeat map
39494
+ // verbatim, so no manager writes a boolean again. What grandfathering
39495
+ // actually covers is a NEW MCP reading a STALE PRE-DEPLOY cache that still
39496
+ // holds one.
39497
+ //
39498
+ // The direction that DOES happen is not handled and cannot be: an OLD MCP
39499
+ // reads the new string with resolveHostBooleanFlag, gets undefined, and keeps
39500
+ // dispatching. That is a real regression against ENG-6506 until the fleet
39501
+ // converges, and it is why the runbook makes convergence a deploy-order
39502
+ // PRECONDITION rather than offering a boolean fallback — there is none, since
39503
+ // an enum flag rejects `{value: true}` with a 400.
39504
+ defaultValue: "off",
39238
39505
  // Flipping this takes the ENTIRE fleet offline to end users; a mis-flip is a
39239
39506
  // platform-wide availability incident, so mutations require explicit
39240
39507
  // confirmation (the admin flip-reach modal).
@@ -45614,6 +45881,56 @@ var SLACK_ACCOUNT_WARN_COOLDOWN_MS = (() => {
45614
45881
  const n = raw ? Number(raw) : NaN;
45615
45882
  return Number.isFinite(n) && n >= 0 ? n * 1e3 : 24 * 60 * 60 * 1e3;
45616
45883
  })();
45884
+ var SLACK_TOOLS_OFFLINE_NOTICE_CACHE = /* @__PURE__ */ new Map();
45885
+ var SLACK_TOOLS_OFFLINE_COOLDOWN_MS = (() => {
45886
+ const raw = process.env["SLACK_TOOLS_OFFLINE_COOLDOWN_SEC"];
45887
+ const n = raw ? Number(raw) : NaN;
45888
+ return Number.isFinite(n) && n >= 0 ? n * 1e3 : 60 * 60 * 1e3;
45889
+ })();
45890
+ async function maybeSendSlackToolsOfflineNotice(args) {
45891
+ if (!BOT_TOKEN || !args.channel) return;
45892
+ const key2 = `${args.channel}|${args.threadTs ?? "root"}|tools_offline`;
45893
+ if (!shouldSendToolsOfflineNotice({
45894
+ cache: SLACK_TOOLS_OFFLINE_NOTICE_CACHE,
45895
+ key: key2,
45896
+ cooldownMs: SLACK_TOOLS_OFFLINE_COOLDOWN_MS
45897
+ })) {
45898
+ return;
45899
+ }
45900
+ const controller = new AbortController();
45901
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
45902
+ try {
45903
+ const res = await fetch("https://slack.com/api/chat.postMessage", {
45904
+ method: "POST",
45905
+ signal: controller.signal,
45906
+ headers: {
45907
+ "Content-Type": "application/json",
45908
+ Authorization: `Bearer ${BOT_TOKEN}`
45909
+ },
45910
+ body: JSON.stringify({
45911
+ channel: args.channel,
45912
+ text: TOOLS_OFFLINE_USER_NOTICE,
45913
+ ...args.threadTs ? { thread_ts: args.threadTs } : {}
45914
+ })
45915
+ });
45916
+ const data = await res.json();
45917
+ if (!data.ok) {
45918
+ SLACK_TOOLS_OFFLINE_NOTICE_CACHE.delete(key2);
45919
+ process.stderr.write(
45920
+ `slack-channel(${AGENT_CODE_NAME}): tools-offline notice post failed: ${data.error ?? "unknown"}
45921
+ `
45922
+ );
45923
+ }
45924
+ } catch (err) {
45925
+ SLACK_TOOLS_OFFLINE_NOTICE_CACHE.delete(key2);
45926
+ process.stderr.write(
45927
+ `slack-channel(${AGENT_CODE_NAME}): tools-offline notice error: ${err.message}
45928
+ `
45929
+ );
45930
+ } finally {
45931
+ clearTimeout(timeoutId);
45932
+ }
45933
+ }
45617
45934
  async function maybeSendSlackAccountWarn(args) {
45618
45935
  if (!BOT_TOKEN || !args.channel) return;
45619
45936
  const key2 = `${args.channel}|${args.threadTs ?? "root"}|account_warn`;
@@ -48679,6 +48996,10 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
48679
48996
  ]
48680
48997
  }));
48681
48998
  mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
48999
+ {
49000
+ const offline = refuseIfToolsOffline(req.params.name);
49001
+ if (offline) return offline;
49002
+ }
48682
49003
  const { name, arguments: args } = req.params;
48683
49004
  const impersonationRefusal = shouldRefuseImpersonatedEgress(
48684
49005
  name,
@@ -49052,6 +49373,11 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
49052
49373
  }
49053
49374
  const sentTs = typeof data.ts === "string" ? data.ts : void 0;
49054
49375
  await maybeSendSlackAccountWarn({ channel: effectiveChannel, threadTs: effectiveThreadTs });
49376
+ void maybeSendSlackToolsOfflineNotice({
49377
+ channel: effectiveChannel,
49378
+ threadTs: effectiveThreadTs
49379
+ }).catch(() => {
49380
+ });
49055
49381
  return {
49056
49382
  content: [{ type: "text", text: sentTs ? `${base} (ts=${sentTs})` : base }]
49057
49383
  };