@agent-native/core 0.145.2 → 0.145.4

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.
Files changed (36) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/plugins/agent-chat.ts +10 -8
  3. package/corpus/templates/assets/server/plugins/agent-chat.ts +1 -1
  4. package/corpus/templates/calendar/server/plugins/agent-chat.ts +1 -1
  5. package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +22 -1
  6. package/corpus/templates/mail/server/plugins/agent-chat.ts +1 -1
  7. package/corpus/templates/plan/server/lib/plan-framework-tools.ts +11 -0
  8. package/corpus/templates/plan/server/plugins/00-mcp.ts +25 -68
  9. package/corpus/templates/plan/server/plugins/agent-chat.ts +8 -2
  10. package/dist/collab/awareness.d.ts +2 -2
  11. package/dist/collab/routes.d.ts +1 -1
  12. package/dist/collab/struct-routes.d.ts +1 -1
  13. package/dist/framework-tools.d.ts +33 -1
  14. package/dist/framework-tools.js +88 -4
  15. package/dist/mcp/build-server.d.ts +15 -0
  16. package/dist/mcp/build-server.js +94 -30
  17. package/dist/mcp/oauth-client-metadata.js +6 -2
  18. package/dist/observability/routes.d.ts +3 -3
  19. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  20. package/dist/server/action-discovery.d.ts +3 -18
  21. package/dist/server/action-discovery.js +9 -65
  22. package/dist/server/agent-chat/action-filters-a2a.d.ts +14 -2
  23. package/dist/server/agent-chat/action-filters-a2a.js +1 -1
  24. package/dist/server/agent-chat/mcp-options.d.ts +106 -0
  25. package/dist/server/agent-chat/mcp-options.js +50 -0
  26. package/dist/server/agent-chat/plugin-options.d.ts +17 -26
  27. package/dist/server/agent-chat-plugin.js +21 -11
  28. package/dist/server/index.d.ts +3 -0
  29. package/dist/server/index.js +7 -0
  30. package/dist/server/poll.d.ts +11 -0
  31. package/dist/server/poll.js +7 -4
  32. package/dist/server/realtime-token.js +8 -0
  33. package/dist/server/short-lived-token.d.ts +8 -0
  34. package/dist/server/short-lived-token.js +12 -0
  35. package/dist/server/transcribe-voice.d.ts +1 -1
  36. package/package.json +1 -1
@@ -142,6 +142,41 @@ function isActionVisibleForOAuthScope(entry, scopes) {
142
142
  const required = entry.readOnly === true ? "mcp:read" : "mcp:write";
143
143
  return hasMcpOAuthScope(scopes, required);
144
144
  }
145
+ /** Mirrors `TOOL_SEARCH_ACTION_NAME`; not imported, because `agent/tool-search.ts`
146
+ * reaches Node-only request context and this module must stay bundleable. */
147
+ const TOOL_SEARCH_TOOL_NAME = "tool-search";
148
+ function withoutToolSearch(actions) {
149
+ if (!(TOOL_SEARCH_TOOL_NAME in actions))
150
+ return actions;
151
+ return Object.fromEntries(Object.entries(actions).filter(([name]) => name !== TOOL_SEARCH_TOOL_NAME));
152
+ }
153
+ /**
154
+ * Re-point `tool-search` at the surface this caller can actually reach.
155
+ *
156
+ * The registry's own entry closes over the app's *whole* action registry, but
157
+ * `tools/call` accepts only the advertised set — so unscoped it answers with
158
+ * names that come straight back as "Unknown tool", and its `callable` field
159
+ * reports plan-mode availability, not that gate. Every template that noticed
160
+ * worked around it by hand-rolling a narrowed copy; scoping it here is what
161
+ * makes the trimmed catalog honest without one.
162
+ */
163
+ function scopeToolSearchToAdvertised(advertised) {
164
+ const entry = advertised[TOOL_SEARCH_TOOL_NAME];
165
+ if (!entry)
166
+ return advertised;
167
+ return {
168
+ ...advertised,
169
+ [TOOL_SEARCH_TOOL_NAME]: {
170
+ ...entry,
171
+ // Imported lazily: `agent/tool-search.ts` pulls Node-only request
172
+ // context, and this module has to stay bundleable for serverless.
173
+ run: async (args) => {
174
+ const { searchToolRegistry } = await import("../agent/tool-search.js");
175
+ return searchToolRegistry(advertised, args ?? {});
176
+ },
177
+ },
178
+ };
179
+ }
145
180
  const COMPACT_MCP_APP_CATALOG_BUILTINS = new Set([
146
181
  "list_apps",
147
182
  "open_app",
@@ -149,9 +184,10 @@ const COMPACT_MCP_APP_CATALOG_BUILTINS = new Set([
149
184
  "ask_app_status",
150
185
  "create_embed_session",
151
186
  // `tool-search` MUST stay in every compact/connector surface: it is how a
152
- // compacted client discovers and loads any action on demand, which is what
153
- // makes "small catalog by default" safe instead of limiting.
154
- "tool-search",
187
+ // compacted client discovers any action on demand, which is what makes
188
+ // "small catalog by default" non-opaque. Discovery is not permission — a
189
+ // searched name still has to be in the advertised set to be callable.
190
+ TOOL_SEARCH_TOOL_NAME,
155
191
  ]);
156
192
  function isActionAdvertisedInCompactMcpAppCatalog(name, entry, config) {
157
193
  if (COMPACT_MCP_APP_CATALOG_BUILTINS.has(name))
@@ -1115,16 +1151,36 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
1115
1151
  const baseActions = useFullSurface && config.productionActions
1116
1152
  ? config.productionActions
1117
1153
  : config.actions;
1118
- const actions = mergeBuiltinTools(config, baseActions, requestMeta);
1154
+ const appCatalog = config.catalogMode === "app";
1155
+ // The app catalog IS a full flat catalog, so the `--full-catalog` opt-ins
1156
+ // have nothing left to escalate — collapsing them here keeps every tier gate
1157
+ // below reading one flag instead of testing `appCatalog` a fifth time.
1158
+ const fullCatalogRequested = !appCatalog && explicitlyRequestsFullMcpCatalog(requestMeta);
1159
+ // `tool-search` earns its place only on a *trimmed* catalog, where it is how
1160
+ // a client reaches an action that was not listed. On a flat catalog — app
1161
+ // mode or the `--full-catalog` opt-in — every tool is already in `tools/list`
1162
+ // beside it, so it can only ever describe its own neighbours while spending
1163
+ // a tool slot and a round trip to do it.
1164
+ const flatCatalog = appCatalog || fullCatalogRequested;
1165
+ // `catalogMode: "app"` asks for parity with the in-app agent, so the cross-app
1166
+ // builtins the MCP layer adds on its own come back off too.
1167
+ const mergedActions = appCatalog
1168
+ ? baseActions
1169
+ : mergeBuiltinTools(config, baseActions, requestMeta);
1170
+ // Strip from `actions`, not just the advertised set: on the full-catalog tier
1171
+ // `actions` IS the callable surface, so filtering only the listing would
1172
+ // leave `tool-search` callable but invisible.
1173
+ const actions = flatCatalog
1174
+ ? withoutToolSearch(mergedActions)
1175
+ : mergedActions;
1119
1176
  const visibleActions = Object.fromEntries(Object.entries(actions).filter(([, entry]) => isActionVisibleForOAuthScope(entry, effectiveIdentity?.oauthScopes)));
1120
- const fullCatalogRequested = explicitlyRequestsFullMcpCatalog(requestMeta);
1121
1177
  // Compact/connector is the DEFAULT for every caller — hosted connectors,
1122
1178
  // code clients (Claude Code / Cursor / Codex), and the local CLI alike. The
1123
1179
  // full ~105-tool catalog is served only on the explicit opt-in above, so a
1124
1180
  // host can never dump every action schema into one giant tool card. The
1125
1181
  // `mcp:apps` scope still lands on this compact MCP-Apps surface; with no
1126
1182
  // opt-in, everyone else does too.
1127
- const compactMcpAppCatalog = !fullCatalogRequested;
1183
+ const compactMcpAppCatalog = !appCatalog && !fullCatalogRequested;
1128
1184
  const advertisedActionsBeforeConnector = compactMcpAppCatalog
1129
1185
  ? Object.fromEntries(Object.entries(visibleActions).filter(([name, entry]) => isActionAdvertisedInCompactMcpAppCatalog(name, entry, config)))
1130
1186
  : visibleActions;
@@ -1140,28 +1196,32 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
1140
1196
  // authenticated reads from `externalAgents.authenticatedReads: "auto"`.
1141
1197
  // This stays compact by default and keeps db-exec / seed-* / extension /
1142
1198
  // browser-session footguns off the external surface.
1143
- const connectorCatalogActive = (connectorNames.size > 0 || automaticConnectorPolicyActive) &&
1199
+ const connectorCatalogActive = !appCatalog &&
1200
+ (connectorNames.size > 0 || automaticConnectorPolicyActive) &&
1144
1201
  !fullCatalogRequested;
1145
1202
  // When the connector catalog is active, filter directly from visibleActions
1146
1203
  // rather than advertisedActionsBeforeConnector. This ensures the connector
1147
1204
  // tier is an independent, template-declared surface that doesn't accidentally
1148
1205
  // narrow to just the compact-catalog builtins when shouldUseCompactMcpCatalogByDefault
1149
1206
  // would have activated the compact catalog for the same caller.
1150
- const advertisedActions = connectorCatalogActive
1151
- ? Object.fromEntries(Object.entries(visibleActions).filter(([name, entry]) => {
1152
- if (denyNames.has(name))
1153
- return false;
1154
- if (COMPACT_MCP_APP_CATALOG_BUILTINS.has(name))
1207
+ const advertisedActionsBeforeToolSearchScope = appCatalog
1208
+ ? Object.fromEntries(Object.entries(visibleActions).filter(([name]) => !denyNames.has(name)))
1209
+ : connectorCatalogActive
1210
+ ? Object.fromEntries(Object.entries(visibleActions).filter(([name, entry]) => {
1211
+ if (denyNames.has(name))
1212
+ return false;
1213
+ if (COMPACT_MCP_APP_CATALOG_BUILTINS.has(name))
1214
+ return true;
1215
+ if (!connectorNames.has(name))
1216
+ return false;
1217
+ if (externalAgentWritesAreAskAppOnly(config) &&
1218
+ entry.readOnly !== true) {
1219
+ return false;
1220
+ }
1155
1221
  return true;
1156
- if (!connectorNames.has(name))
1157
- return false;
1158
- if (externalAgentWritesAreAskAppOnly(config) &&
1159
- entry.readOnly !== true) {
1160
- return false;
1161
- }
1162
- return true;
1163
- }))
1164
- : advertisedActionsBeforeConnector;
1222
+ }))
1223
+ : advertisedActionsBeforeConnector;
1224
+ const advertisedActions = scopeToolSearchToAdvertised(advertisedActionsBeforeToolSearchScope);
1165
1225
  if (fullCatalogRequested) {
1166
1226
  warnFullCatalogServed(Object.keys(advertisedActions).length);
1167
1227
  }
@@ -1389,8 +1449,10 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
1389
1449
  annotations,
1390
1450
  };
1391
1451
  }));
1392
- if (!compactMcpAppCatalog &&
1393
- !connectorCatalogActive &&
1452
+ // `ask-agent` is the legacy full-catalog meta-tool; `ask_app` is the
1453
+ // builtin every other tier carries. Gate on the one flag that means
1454
+ // "this caller opted all the way in".
1455
+ if (fullCatalogRequested &&
1394
1456
  config.askAgent &&
1395
1457
  hasMcpOAuthScope(effectiveIdentity?.oauthScopes, "mcp:write")) {
1396
1458
  tools.push({
@@ -1436,7 +1498,7 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
1436
1498
  return withCallerContext(async () => {
1437
1499
  const { name, arguments: args } = request.params;
1438
1500
  if (name === "ask-agent" && config.askAgent) {
1439
- if (compactMcpAppCatalog || connectorCatalogActive) {
1501
+ if (!fullCatalogRequested) {
1440
1502
  return {
1441
1503
  content: [{ type: "text", text: `Unknown tool: ${name}` }],
1442
1504
  isError: true,
@@ -1483,12 +1545,14 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
1483
1545
  };
1484
1546
  }
1485
1547
  }
1486
- // Connector-catalog tier: when active, callableActions === advertisedActions
1487
- // (the filtered set). Non-listed tools are not callable — mirroring how
1488
- // compactMcpAppCatalog gates calls on advertisedActions.
1489
- const callableActions = compactMcpAppCatalog || connectorCatalogActive
1490
- ? advertisedActions
1491
- : actions;
1548
+ // Every tier except the explicit full-catalog opt-in treats the
1549
+ // advertised set as the authoritative callable surface: a name that
1550
+ // was not listed is rejected, not merely hidden. Deriving this from
1551
+ // the one opt-in flag rather than re-testing each tier is what keeps a
1552
+ // newly added tier from silently defaulting to "everything callable".
1553
+ const callableActions = fullCatalogRequested
1554
+ ? actions
1555
+ : advertisedActions;
1492
1556
  const entry = callableActions[name];
1493
1557
  if (!entry) {
1494
1558
  return {
@@ -170,12 +170,16 @@ function validateClientMetadataDocument(clientId, value) {
170
170
  if (tokenEndpointAuthMethod !== "none") {
171
171
  throw new Error("Only public Client ID Metadata clients are supported");
172
172
  }
173
+ // A grant type we do not support is not a reason to reject the document —
174
+ // clients advertise grants they use elsewhere (Claude.ai's CIMD document
175
+ // lists "urn:ietf:params:oauth:grant-type:jwt-bearer" next to the two we
176
+ // support). The token endpoint independently honors only
177
+ // "authorization_code"/"refresh_token" no matter what this list says.
173
178
  const grantTypes = parseStringArray(document.grant_types);
174
179
  if ((document.grant_types !== undefined &&
175
180
  (!Array.isArray(document.grant_types) ||
176
181
  grantTypes.length !== document.grant_types.length)) ||
177
- grantTypes.length > 10 ||
178
- !grantTypes.every((grant) => grant === "authorization_code" || grant === "refresh_token")) {
182
+ grantTypes.length > 10) {
179
183
  throw new Error("Client metadata document grant_types are unsupported");
180
184
  }
181
185
  const responseTypes = parseStringArray(document.response_types);
@@ -41,16 +41,16 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
+ error?: undefined;
44
45
  summary: import("./types.js").TraceSummary;
45
46
  spans: import("./types.js").TraceSpan[];
46
47
  id?: undefined;
47
- error?: undefined;
48
48
  ok?: undefined;
49
49
  } | {
50
+ error?: undefined;
50
51
  summary?: undefined;
51
52
  spans?: undefined;
52
53
  id: string;
53
- error?: undefined;
54
54
  ok?: undefined;
55
55
  } | {
56
56
  summary?: undefined;
@@ -59,9 +59,9 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
59
59
  error: any;
60
60
  ok?: undefined;
61
61
  } | {
62
+ error?: undefined;
62
63
  summary?: undefined;
63
64
  spans?: undefined;
64
65
  id?: undefined;
65
- error?: undefined;
66
66
  ok: boolean;
67
67
  }>>;
@@ -311,8 +311,8 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
311
311
  notes?: string | undefined;
312
312
  scope?: "org" | "user" | undefined;
313
313
  }, {
314
- deleted?: undefined;
315
314
  id?: undefined;
315
+ deleted?: undefined;
316
316
  provider?: undefined;
317
317
  found?: undefined;
318
318
  providers: {
@@ -329,9 +329,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
329
329
  label?: undefined;
330
330
  message?: undefined;
331
331
  } | {
332
+ id?: undefined;
332
333
  deleted?: undefined;
333
334
  count?: undefined;
334
- id?: undefined;
335
335
  providers?: undefined;
336
336
  found: boolean;
337
337
  provider: import("../custom-registry.js").CustomProviderConfig;
@@ -26,7 +26,7 @@
26
26
  * ```
27
27
  */
28
28
  import type { ActionEntry } from "../agent/production-agent.js";
29
- import type { FrameworkToolGroup } from "../framework-tools.js";
29
+ import { CORE_ACTION_GROUPS } from "../framework-tools.js";
30
30
  /**
31
31
  * Register a map of actions contributed by a published package.
32
32
  *
@@ -81,22 +81,7 @@ export declare function loadActionsFromStaticRegistry(modules: Record<string, un
81
81
  * passing to `createAgentChatPlugin({ actions })`.
82
82
  */
83
83
  export declare function autoDiscoverActions(from: string): Promise<Record<string, ActionEntry>>;
84
- /**
85
- * Which `frameworkTools` switch owns each framework action kit.
86
- *
87
- * The group is stamped onto the entry as `frameworkGroup`, which is what lets
88
- * one filter subtract a whole kit from every agent tool surface at once — the
89
- * plugin spreads these registries at thirteen composition sites, and a
90
- * per-site condition would be the same omission waiting to happen thirteen
91
- * times. The tag also keeps these ~45 schemas out of the DEFAULT first-request
92
- * tool list; they stay reachable through `tool-search`.
93
- *
94
- * Anything absent from this map is always-on and must be listed in
95
- * `ALWAYS_ON_CORE_ACTIONS` instead. `action-discovery.spec.ts` fails when a new
96
- * core action appears in neither, so "always-on" stays a decision someone made
97
- * rather than the default that happens when a map goes un-updated.
98
- */
99
- export declare const CORE_ACTION_GROUPS: Record<string, FrameworkToolGroup>;
84
+ export { CORE_ACTION_GROUPS };
100
85
  /**
101
86
  * Core actions with no `frameworkTools` switch, and why:
102
87
  *
@@ -104,7 +89,7 @@ export declare const CORE_ACTION_GROUPS: Record<string, FrameworkToolGroup>;
104
89
  * - The email catalog is mounted everywhere on purpose so Dispatch can ask any
105
90
  * app what it sends without that app opting in (see its comment below). It is
106
91
  * a read surface, not the `email` group's send capability.
107
- * - MCP tools and org service tokens are already governed by `disableMcp`.
92
+ * - MCP tools and org service tokens are already governed by `mcp.enabled`.
108
93
  */
109
94
  export declare const ALWAYS_ON_CORE_ACTIONS: ReadonlySet<string>;
110
95
  export declare function mergeCoreSharingActions(registry: Record<string, ActionEntry>): Promise<void>;
@@ -1,4 +1,5 @@
1
1
  import nodePath from "node:path";
2
+ import { CORE_ACTION_GROUPS } from "../framework-tools.js";
2
3
  import { captureCliOutput } from "./cli-capture.js";
3
4
  // Lazy fs — loaded via dynamic import() on first use.
4
5
  // Avoids require() which bundlers convert to createRequire() that crashes on CF Workers.
@@ -497,70 +498,10 @@ export async function autoDiscoverActions(from) {
497
498
  }
498
499
  return registry;
499
500
  }
500
- /**
501
- * Which `frameworkTools` switch owns each framework action kit.
502
- *
503
- * The group is stamped onto the entry as `frameworkGroup`, which is what lets
504
- * one filter subtract a whole kit from every agent tool surface at once — the
505
- * plugin spreads these registries at thirteen composition sites, and a
506
- * per-site condition would be the same omission waiting to happen thirteen
507
- * times. The tag also keeps these ~45 schemas out of the DEFAULT first-request
508
- * tool list; they stay reachable through `tool-search`.
509
- *
510
- * Anything absent from this map is always-on and must be listed in
511
- * `ALWAYS_ON_CORE_ACTIONS` instead. `action-discovery.spec.ts` fails when a new
512
- * core action appears in neither, so "always-on" stays a decision someone made
513
- * rather than the default that happens when a map goes un-updated.
514
- */
515
- export const CORE_ACTION_GROUPS = {
516
- "share-resource": "sharing",
517
- "unshare-resource": "sharing",
518
- "list-resource-shares": "sharing",
519
- "set-resource-visibility": "sharing",
520
- "create-agent-resource-link": "sharing",
521
- "get-feature-flags": "featureFlags",
522
- "list-feature-flags": "featureFlags",
523
- "set-feature-flag": "featureFlags",
524
- "list-recurring-jobs": "automation",
525
- "manage-recurring-job": "automation",
526
- "run-automation-now": "automation",
527
- "list-automation-runs": "automation",
528
- "list-automations": "automation",
529
- "manage-automation": "automation",
530
- "context-manifest-get": "contextXray",
531
- "context-preview-get": "contextXray",
532
- "context-pin": "contextXray",
533
- "context-evict": "contextXray",
534
- "context-restore": "contextXray",
535
- "context-report": "contextXray",
536
- "get-localization-preference": "localization",
537
- "set-localization-preference": "localization",
538
- // Profile, credentials, and appearance share one switch: an app that hides
539
- // its profile surface from the agent hides password management with it.
540
- "get-user-profile": "userProfile",
541
- "update-user-profile": "userProfile",
542
- "get-auth-methods": "userProfile",
543
- "set-password": "userProfile",
544
- "change-password": "userProfile",
545
- "change-appearance": "userProfile",
546
- "list-audit-events": "audit",
547
- "get-audit-event": "audit",
548
- "export-audit-events": "audit",
549
- "create-resource-version": "history",
550
- "list-resource-versions": "history",
551
- "get-resource-version": "history",
552
- "restore-resource-version": "history",
553
- "list-resource-history": "history",
554
- "list-review-comments": "review",
555
- "create-review-comment": "review",
556
- "reply-review-comment": "review",
557
- "resolve-review-thread": "review",
558
- "delete-review-comment": "review",
559
- "consume-review-feedback": "review",
560
- "get-review-feedback": "review",
561
- "set-review-status": "review",
562
- "send-review-thread-to-agent": "review",
563
- };
501
+ // `CORE_ACTION_GROUPS` lives in `framework-tools.ts` so the group filter can
502
+ // resolve a kit by name without importing this module. Re-exported here
503
+ // because this is where callers have always found it.
504
+ export { CORE_ACTION_GROUPS };
564
505
  /**
565
506
  * Core actions with no `frameworkTools` switch, and why:
566
507
  *
@@ -568,7 +509,7 @@ export const CORE_ACTION_GROUPS = {
568
509
  * - The email catalog is mounted everywhere on purpose so Dispatch can ask any
569
510
  * app what it sends without that app opting in (see its comment below). It is
570
511
  * a read surface, not the `email` group's send capability.
571
- * - MCP tools and org service tokens are already governed by `disableMcp`.
512
+ * - MCP tools and org service tokens are already governed by `mcp.enabled`.
572
513
  */
573
514
  export const ALWAYS_ON_CORE_ACTIONS = new Set([
574
515
  "upload-image",
@@ -818,6 +759,9 @@ export async function mergeCoreSharingActions(registry) {
818
759
  // actions' `toolCallable: false` (audit-H5) is dropped and the
819
760
  // tools-iframe bridge 403 in action-routes.ts never fires.
820
761
  ...preserveActionFlags(def),
762
+ // Pre-resolved copy of what `resolveFrameworkGroup` would compute
763
+ // from the name anyway. Kept so an entry read in isolation still
764
+ // reports its kit; the filters no longer depend on it.
821
765
  ...(CORE_ACTION_GROUPS[name]
822
766
  ? { frameworkGroup: CORE_ACTION_GROUPS[name] }
823
767
  : {}),
@@ -3,6 +3,7 @@ import type { EngineTool } from "../../agent/engine/types.js";
3
3
  import { type ActionEntry, type AgentLoopOutcome } from "../../agent/production-agent.js";
4
4
  import { runAgentLoopDirectWithSoftTimeout } from "../../agent/run-loop-with-resume.js";
5
5
  import type { AgentChatEvent } from "../../agent/types.js";
6
+ import type { ExternalAgentPolicy } from "../../mcp/external-agent-policy.js";
6
7
  import type { AgentChatPluginOptions } from "./plugin-options.js";
7
8
  export declare function filterReadOnlyActions(actions: Record<string, ActionEntry>): Record<string, ActionEntry>;
8
9
  /** Drop actions that opted out of agent exposure via `agentTool: false`. They
@@ -25,13 +26,24 @@ export declare function filterPublicAgentActions(actions: Record<string, ActionE
25
26
  * action out of this rule.
26
27
  */
27
28
  export declare function hasRawQueryInput(entry: ActionEntry): boolean;
29
+ /**
30
+ * The two policy fields A2A shares with the MCP mount. Structural rather than
31
+ * `Pick<AgentChatPluginOptions, …>` so callers pass the *resolved* MCP options:
32
+ * reading the raw plugin options here would leave A2A on the deprecated
33
+ * top-level keys, so `mcp: { connectorCatalog }` would narrow the MCP surface
34
+ * while A2A kept serving the old one.
35
+ */
36
+ export interface A2AExternalAgentSurface {
37
+ connectorCatalog?: string[];
38
+ externalAgents?: ExternalAgentPolicy;
39
+ }
28
40
  /**
29
41
  * Direct A2A action calls share the authenticated external-agent policy with
30
42
  * MCP, but remain stricter: only explicitly exposed, authenticated read-only
31
43
  * actions can skip the receiver's model loop, and never one that takes a raw
32
44
  * query the caller wrote.
33
45
  */
34
- export declare function filterDirectA2AActions(actions: Record<string, ActionEntry>, options: Pick<AgentChatPluginOptions, "connectorCatalog" | "externalAgents">): Record<string, ActionEntry>;
46
+ export declare function filterDirectA2AActions(actions: Record<string, ActionEntry>, options: A2AExternalAgentSurface): Record<string, ActionEntry>;
35
47
  export declare function buildPublicAgentA2ASkills(actions: Record<string, ActionEntry>): Array<{
36
48
  id: string;
37
49
  name: string;
@@ -47,7 +59,7 @@ export declare function buildPublicAgentA2ASkills(actions: Record<string, Action
47
59
  * `requiresAuth === true` — advertising the public set alone tells siblings
48
60
  * an app has nothing callable when it does.
49
61
  */
50
- export declare function buildAuthenticatedAgentA2ASkills(actions: Record<string, ActionEntry>, options: Pick<AgentChatPluginOptions, "connectorCatalog" | "externalAgents">): Array<{
62
+ export declare function buildAuthenticatedAgentA2ASkills(actions: Record<string, ActionEntry>, options: A2AExternalAgentSurface): Array<{
51
63
  id: string;
52
64
  name: string;
53
65
  description: string;
@@ -405,6 +405,6 @@ export function resolveInitialToolNames(templateActions, configured) {
405
405
  if (configured)
406
406
  return configured;
407
407
  return Object.entries(templateActions)
408
- .filter(([, entry]) => !isFrameworkGroupedAction(entry))
408
+ .filter(([name, entry]) => !isFrameworkGroupedAction(name, entry))
409
409
  .map(([name]) => name);
410
410
  }
@@ -0,0 +1,106 @@
1
+ import type { ExternalAgentPolicy } from "../../mcp/external-agent-policy.js";
2
+ /** App icon descriptor surfaced during the MCP `initialize` handshake. */
3
+ export interface AgentChatMcpIcon {
4
+ src: string;
5
+ mimeType?: string;
6
+ sizes?: string[];
7
+ theme?: "light" | "dark";
8
+ }
9
+ /**
10
+ * Everything the agent-chat plugin can configure about its MCP mount, in one
11
+ * object.
12
+ *
13
+ * Before this existed, the MCP settings were five unrelated top-level keys
14
+ * (`disableMcp`, `mcpServerInfo`, `connectorCatalog`, `externalAgents`) and
15
+ * several `MCPConfig` fields with no route through the plugin at all —
16
+ * `builtinCrossAppTools` was reachable only by calling `mountMCP` directly,
17
+ * which is why `frameworkTools` could never remove the cross-app builtins from
18
+ * an app that used the normal plugin entry point.
19
+ */
20
+ export interface AgentChatMcpOptions {
21
+ /**
22
+ * Mount the remote MCP protocol route. Defaults to `true`.
23
+ *
24
+ * Set `false` for hosted apps with a dedicated early MCP plugin, so their
25
+ * external connector does not depend on the heavier chat plugin
26
+ * initialization path.
27
+ */
28
+ enabled?: boolean;
29
+ /**
30
+ * `"app"` serves external callers exactly this app's own tool registry,
31
+ * flat — the same actions the in-app agent holds, with no cross-app
32
+ * builtins, no `ask-agent`, no `tool-search`, and no compact/connector
33
+ * trimming. `connectorCatalog` and the `--full-catalog` opt-ins are inert
34
+ * once this is set; `externalAgents.denyActions` and OAuth scopes still
35
+ * apply.
36
+ *
37
+ * Weigh the token cost before setting it: an app registering ~100 actions
38
+ * puts every one of their schemas in the caller's context on `tools/list`,
39
+ * which is the exact footgun the compact default exists to avoid.
40
+ */
41
+ catalog?: "app";
42
+ /**
43
+ * Curated allow-list of action names served to external connector clients.
44
+ * Trims both the advertised and the callable surface — a name outside the
45
+ * list is rejected, not merely hidden.
46
+ */
47
+ connectorCatalog?: string[];
48
+ /**
49
+ * Default authenticated external-agent policy. In `auto` read mode, every
50
+ * action explicitly marked as GET + readOnly + publicAgent.requiresAuth is
51
+ * added to the connector surface automatically. Writes remain ask_app-only
52
+ * unless `writes: "allowlisted"` is explicitly selected.
53
+ */
54
+ externalAgents?: ExternalAgentPolicy;
55
+ /**
56
+ * Merge the generic cross-app builtins (`list_apps`, `open_app`, `ask_app`,
57
+ * `ask_app_status`, `create_embed_session`, `create_workspace_app`,
58
+ * `list_templates`). Defaults to `true`.
59
+ *
60
+ * These are added by the MCP layer itself, downstream of `frameworkTools`,
61
+ * so they are NOT removed by `frameworkTools: "minimal"` or
62
+ * `workspaceApps: false` — this is the switch that turns them off.
63
+ */
64
+ builtinCrossAppTools?: boolean;
65
+ /** Human-facing title. Defaults to the capitalized app id/name. */
66
+ title?: string;
67
+ /** Host-facing description. Defaults to "Agent-native <app> agent". */
68
+ description?: string;
69
+ /** Canonical app URL. Relative URLs are resolved against the request origin. */
70
+ websiteUrl?: string;
71
+ /** App icons. Relative `src` values are resolved against the request origin. */
72
+ icons?: AgentChatMcpIcon[];
73
+ }
74
+ /** The legacy top-level keys `mcp` replaces. */
75
+ export interface AgentChatMcpLegacyInput {
76
+ /** @deprecated Use `mcp.enabled: false`. */
77
+ disableMcp?: boolean;
78
+ /** @deprecated Use `mcp.title` / `mcp.description` / `mcp.websiteUrl` / `mcp.icons`. */
79
+ mcpServerInfo?: {
80
+ title?: string;
81
+ description?: string;
82
+ websiteUrl?: string;
83
+ icons?: AgentChatMcpIcon[];
84
+ };
85
+ /** @deprecated Use `mcp.connectorCatalog`. */
86
+ connectorCatalog?: string[];
87
+ /** @deprecated Use `mcp.externalAgents`. */
88
+ externalAgents?: ExternalAgentPolicy;
89
+ mcp?: AgentChatMcpOptions;
90
+ }
91
+ export interface ResolvedAgentChatMcp {
92
+ enabled: boolean;
93
+ catalog: "app" | undefined;
94
+ connectorCatalog: string[] | undefined;
95
+ externalAgents: ExternalAgentPolicy | undefined;
96
+ builtinCrossAppTools: boolean | undefined;
97
+ title: string | undefined;
98
+ description: string | undefined;
99
+ websiteUrl: string | undefined;
100
+ icons: AgentChatMcpIcon[] | undefined;
101
+ }
102
+ /**
103
+ * Collapse the nested `mcp` option and the legacy top-level keys into the one
104
+ * shape the plugin threads into `mountMCP`.
105
+ */
106
+ export declare function resolveAgentChatMcpOptions(input: AgentChatMcpLegacyInput | undefined): ResolvedAgentChatMcp;
@@ -0,0 +1,50 @@
1
+ function conflict(key, legacyKey, legacyValue, nestedValue) {
2
+ throw new Error(`[agent-native] Conflicting agent-chat options: \`${legacyKey}: ${JSON.stringify(legacyValue)}\` ` +
3
+ `and \`mcp.${key}: ${JSON.stringify(nestedValue)}\` disagree. ` +
4
+ `Remove the deprecated \`${legacyKey}\` and keep \`mcp.${key}\`.`);
5
+ }
6
+ const warnedLegacyKeys = new Set();
7
+ /**
8
+ * Nested value wins, but only when the two forms agree.
9
+ *
10
+ * Disagreement throws at plugin init rather than picking a side: an app that
11
+ * boots with an MCP surface nobody chose is how a "why can't the connector see
12
+ * this tool" report ends up unexplainable. Same contract as
13
+ * `resolveFrameworkTools`.
14
+ */
15
+ function pick(key, legacyKey, legacyValue, nestedValue) {
16
+ if (legacyValue === undefined)
17
+ return nestedValue;
18
+ if (nestedValue !== undefined &&
19
+ JSON.stringify(legacyValue) !== JSON.stringify(nestedValue)) {
20
+ conflict(key, legacyKey, legacyValue, nestedValue);
21
+ }
22
+ if (!warnedLegacyKeys.has(legacyKey)) {
23
+ warnedLegacyKeys.add(legacyKey);
24
+ console.warn(`[agent-native] \`${legacyKey}\` is deprecated — use \`mcp: { ${key}: … }\`.`);
25
+ }
26
+ return nestedValue ?? legacyValue;
27
+ }
28
+ /**
29
+ * Collapse the nested `mcp` option and the legacy top-level keys into the one
30
+ * shape the plugin threads into `mountMCP`.
31
+ */
32
+ export function resolveAgentChatMcpOptions(input) {
33
+ const mcp = input?.mcp ?? {};
34
+ const legacyInfo = input?.mcpServerInfo;
35
+ // `disableMcp` is the inverse of `enabled`, so normalize before comparing —
36
+ // otherwise `disableMcp: true` + `enabled: false` would read as a conflict.
37
+ const legacyEnabled = input?.disableMcp === undefined ? undefined : !input.disableMcp;
38
+ const enabled = pick("enabled", "disableMcp", legacyEnabled, mcp.enabled);
39
+ return {
40
+ enabled: enabled ?? true,
41
+ catalog: mcp.catalog,
42
+ connectorCatalog: pick("connectorCatalog", "connectorCatalog", input?.connectorCatalog, mcp.connectorCatalog),
43
+ externalAgents: pick("externalAgents", "externalAgents", input?.externalAgents, mcp.externalAgents),
44
+ builtinCrossAppTools: mcp.builtinCrossAppTools,
45
+ title: pick("title", "mcpServerInfo.title", legacyInfo?.title, mcp.title),
46
+ description: pick("description", "mcpServerInfo.description", legacyInfo?.description, mcp.description),
47
+ websiteUrl: pick("websiteUrl", "mcpServerInfo.websiteUrl", legacyInfo?.websiteUrl, mcp.websiteUrl),
48
+ icons: pick("icons", "mcpServerInfo.icons", legacyInfo?.icons, mcp.icons),
49
+ };
50
+ }