@agent-native/core 0.176.1 → 0.177.0-nightly-20260828143953

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.
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8675
34
+ - template files: 8677
@@ -30,7 +30,7 @@ const schema = z.discriminatedUnion("operation", [
30
30
  ]);
31
31
  export default defineAction({
32
32
  description:
33
- "Persist one feature-flag change on a trusted organization app. The app target is resolved only through the organization directory.",
33
+ "Persist one feature-flag change on Analytics or a trusted organization app. Analytics uses its local flag action; peer targets are resolved through the organization directory.",
34
34
  schema,
35
35
  agentInputSchema: z.object({
36
36
  appId: z.string(),
@@ -40,6 +40,6 @@ export default defineAction({
40
40
  }),
41
41
  run: async (args, ctx) => {
42
42
  const admin = await requireAnalyticsAdminContext(ctx);
43
- return setWorkspaceFeatureFlag(admin, args);
43
+ return setWorkspaceFeatureFlag(admin, args, ctx);
44
44
  },
45
45
  });
@@ -89,6 +89,10 @@ function asDirectory(value: unknown): FlagDirectory {
89
89
  return value && typeof value === "object" ? (value as FlagDirectory) : {};
90
90
  }
91
91
 
92
+ export function hasFleetApps(value: unknown): boolean {
93
+ return (asDirectory(value).apps?.length ?? 0) > 0;
94
+ }
95
+
92
96
  function isReady(app: FlagApp) {
93
97
  return app.state === "ready" || app.status === "ready";
94
98
  }
@@ -188,7 +192,10 @@ export function FeatureFlagsFleetPanel() {
188
192
  const directory = asDirectory(flags.data);
189
193
  const apps = directory.apps ?? [];
190
194
  if (flags.isLoading) return <PanelLoading />;
191
- if (flags.error || directory.directoryStatus === "unavailable")
195
+ if (
196
+ flags.error ||
197
+ (directory.directoryStatus === "unavailable" && !hasFleetApps(directory))
198
+ )
192
199
  return (
193
200
  <StatusState
194
201
  title={t("agents.flagsUnavailable")}
@@ -226,6 +233,14 @@ export function FeatureFlagsFleetPanel() {
226
233
  {t("agents.reloadFlags")}
227
234
  </Button>
228
235
  </div>
236
+ {directory.directoryStatus === "unavailable" ? (
237
+ <p
238
+ className="rounded-lg border px-4 py-3 text-sm text-muted-foreground"
239
+ role="status"
240
+ >
241
+ {t("agents.flagsUnreachable")}
242
+ </p>
243
+ ) : null}
229
244
  {apps.map((app) => (
230
245
  <section key={appId(app)} className="overflow-hidden rounded-lg border">
231
246
  <div className="flex items-center justify-between gap-3 border-b bg-muted/30 px-4 py-3">
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-27
4
+ ---
5
+
6
+ Show Analytics's own registered flags in fleet feature flag management.
@@ -4,14 +4,18 @@ import { signA2AToken } from "@agent-native/core/a2a";
4
4
  import {
5
5
  ActionContractError,
6
6
  AgentActionStopError,
7
+ type ActionRunContext,
7
8
  } from "@agent-native/core/action";
8
9
  import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
10
+ import listLocalFeatureFlagsAction from "@agent-native/core/feature-flags/actions/list-feature-flags";
11
+ import setLocalFeatureFlagAction from "@agent-native/core/feature-flags/actions/set-feature-flag";
9
12
  import {
10
13
  fetchOrgApps,
11
14
  fetchOrgAppsResult,
12
15
  type OrgApp,
13
16
  } from "@agent-native/core/mcp";
14
17
  import { getOrgDomain } from "@agent-native/core/org";
18
+ import { getAppConfig } from "@agent-native/core/server";
15
19
 
16
20
  import {
17
21
  RESILIENT_FLEET_FLAG_DIRECTORY,
@@ -23,6 +27,7 @@ const TARGET_TIMEOUT_MS = 3_000;
23
27
  const DIRECTORY_ATTEMPTS = 2;
24
28
  const VERIFICATION_ATTEMPTS = 2;
25
29
  const CONCURRENCY = 4;
30
+ const ANALYTICS_APP_ID = "analytics";
26
31
 
27
32
  export type FleetFlagState =
28
33
  | "ready"
@@ -91,6 +96,37 @@ export function workspaceFeatureFlagTargetInput(
91
96
  : rawTargetInput;
92
97
  }
93
98
 
99
+ function mutationValidationExpectation(
100
+ admin: AnalyticsAdminContext,
101
+ input: WorkspaceFeatureFlagMutationInput,
102
+ orgDomain: string,
103
+ ) {
104
+ return {
105
+ key: input.key,
106
+ orgDomain,
107
+ allowExplicitNoOrgTarget: true,
108
+ ...(input.operation === "replace-rules" && input.rules
109
+ ? {
110
+ rules: {
111
+ mode: input.rules.mode,
112
+ emails: input.rules.emails ?? [],
113
+ orgIds: input.rules.orgIds ?? [],
114
+ percentage: input.rules.percentage ?? 0,
115
+ },
116
+ }
117
+ : input.operation === "off"
118
+ ? {
119
+ rules: {
120
+ mode: "off",
121
+ emails: [],
122
+ orgIds: [],
123
+ percentage: 0,
124
+ },
125
+ }
126
+ : { enabledForEmail: admin.userEmail }),
127
+ };
128
+ }
129
+
94
130
  export function validateWorkspaceFeatureFlagMutation(
95
131
  body: unknown,
96
132
  expected: {
@@ -179,6 +215,54 @@ function targetOrigin(app: OrgApp): string {
179
215
  return new URL(app.url).origin;
180
216
  }
181
217
 
218
+ function localAnalyticsApp(): OrgApp {
219
+ const configuredUrl = getAppConfig().app.url;
220
+ const url = configuredUrl ?? "http://analytics.local";
221
+ return {
222
+ id: ANALYTICS_APP_ID,
223
+ name: "Analytics",
224
+ url,
225
+ a2aUrl: url,
226
+ };
227
+ }
228
+
229
+ function localActionContext(
230
+ admin: AnalyticsAdminContext,
231
+ actionName: "list-feature-flags" | "set-feature-flag",
232
+ sourceContext?: ActionRunContext,
233
+ ) {
234
+ return {
235
+ ...sourceContext,
236
+ caller: sourceContext?.caller ?? ("http" as const),
237
+ userEmail: admin.userEmail,
238
+ orgId: admin.orgId,
239
+ appId: ANALYTICS_APP_ID,
240
+ actionName,
241
+ };
242
+ }
243
+
244
+ async function listLocalAnalyticsFeatureFlags(admin: AnalyticsAdminContext) {
245
+ return classifyWorkspaceFeatureFlagList(localAnalyticsApp(), {
246
+ status: 200,
247
+ body: await listLocalFeatureFlagsAction.run(
248
+ {},
249
+ localActionContext(admin, "list-feature-flags"),
250
+ ),
251
+ });
252
+ }
253
+
254
+ function unreachableLocalAnalyticsEntry(): FleetFlagApp {
255
+ const app = localAnalyticsApp();
256
+ return {
257
+ appId: app.id,
258
+ appName: app.name,
259
+ appOrigin: targetOrigin(app),
260
+ state: "unreachable",
261
+ flags: [],
262
+ reason: "target-execution",
263
+ };
264
+ }
265
+
182
266
  async function delegatedToken(
183
267
  admin: AnalyticsAdminContext,
184
268
  origin: string,
@@ -298,6 +382,18 @@ function targetFailure(error: unknown): WorkspaceFeatureFlagFailure {
298
382
  return new WorkspaceFeatureFlagFailure(reason);
299
383
  }
300
384
 
385
+ function localMutationFailure(error: unknown): WorkspaceFeatureFlagFailure {
386
+ const statusCode =
387
+ error && typeof error === "object" && "statusCode" in error
388
+ ? (error as { statusCode?: unknown }).statusCode
389
+ : undefined;
390
+ return new WorkspaceFeatureFlagFailure(
391
+ statusCode === 401 || statusCode === 403
392
+ ? "authorization"
393
+ : "target-action",
394
+ );
395
+ }
396
+
301
397
  async function resolveTargetApp(
302
398
  admin: AnalyticsAdminContext,
303
399
  appId: string,
@@ -471,13 +567,20 @@ async function mapBounded<T, R>(
471
567
  export async function listWorkspaceFeatureFlags(
472
568
  admin: AnalyticsAdminContext,
473
569
  ): Promise<WorkspaceFeatureFlagsResult> {
474
- const apps = await fetchOrgApps({
475
- selfId: "analytics",
476
- includeDirectoryApp: true,
477
- serviceOrgId: admin.orgId,
478
- });
479
- if (apps.length === 0) return { directoryStatus: "unavailable", apps: [] };
480
- const entries = await mapBounded(apps, async (app) => {
570
+ const [localEntry, directoryResult] = await Promise.all([
571
+ listLocalAnalyticsFeatureFlags(admin).catch(() =>
572
+ unreachableLocalAnalyticsEntry(),
573
+ ),
574
+ fetchOrgAppsResult({
575
+ selfId: ANALYTICS_APP_ID,
576
+ includeDirectoryApp: true,
577
+ serviceOrgId: admin.orgId,
578
+ }),
579
+ ]);
580
+ if (directoryResult.status === "unavailable")
581
+ return { directoryStatus: "unavailable", apps: [localEntry] };
582
+ const directoryApps = directoryResult.apps;
583
+ const entries = await mapBounded(directoryApps, async (app) => {
481
584
  try {
482
585
  return classifyWorkspaceFeatureFlagList(
483
586
  app,
@@ -494,13 +597,101 @@ export async function listWorkspaceFeatureFlags(
494
597
  };
495
598
  }
496
599
  });
497
- return { directoryStatus: "available", apps: entries };
600
+ return { directoryStatus: "available", apps: [localEntry, ...entries] };
498
601
  }
499
602
 
500
603
  export async function setWorkspaceFeatureFlag(
501
604
  admin: AnalyticsAdminContext,
502
605
  input: WorkspaceFeatureFlagMutationInput,
606
+ sourceContext?: ActionRunContext,
503
607
  ): Promise<WorkspaceFeatureFlagMutationResult> {
608
+ if (input.appId === ANALYTICS_APP_ID) {
609
+ if (input.operation === "replace-rules" && !input.rules)
610
+ throw new WorkspaceFeatureFlagFailure("target-action");
611
+ const localRules = input.rules as
612
+ | {
613
+ mode: "off" | "on" | "rules";
614
+ emails?: string[];
615
+ orgIds?: string[];
616
+ percentage?: number;
617
+ }
618
+ | undefined;
619
+ const targetInput =
620
+ input.operation === "replace-rules"
621
+ ? {
622
+ operation: input.operation,
623
+ key: input.key,
624
+ rules: localRules!,
625
+ }
626
+ : { operation: input.operation, key: input.key };
627
+ let result;
628
+ try {
629
+ result = await setLocalFeatureFlagAction.run(
630
+ targetInput,
631
+ localActionContext(admin, "set-feature-flag", sourceContext),
632
+ );
633
+ } catch (error) {
634
+ throw localMutationFailure(error);
635
+ }
636
+ const orgDomain = result.scope.orgDomain ?? "";
637
+ let mutation: TargetFeatureFlagMutationResult;
638
+ try {
639
+ mutation = validateWorkspaceFeatureFlagMutation(
640
+ result,
641
+ mutationValidationExpectation(admin, input, orgDomain),
642
+ );
643
+ } catch {
644
+ throw new WorkspaceFeatureFlagFailure("persistence");
645
+ }
646
+ let verifiedApp: FleetFlagApp;
647
+ try {
648
+ verifiedApp = await listLocalAnalyticsFeatureFlags(admin);
649
+ } catch {
650
+ throw new WorkspaceFeatureFlagFailure("verification");
651
+ }
652
+ if (verifiedApp.state === "forbidden")
653
+ throw new WorkspaceFeatureFlagFailure("authorization");
654
+ const verifiedFlag = verifiedApp.flags.find(
655
+ (flag) => flag.key === input.key,
656
+ );
657
+ if (
658
+ verifiedApp.state !== "ready" ||
659
+ !verifiedFlag ||
660
+ typeof verifiedFlag.enabledForCurrentUser !== "boolean" ||
661
+ (input.operation === "enable-for-current-user" &&
662
+ !verifiedFlag.enabledForCurrentUser) ||
663
+ (input.operation === "off" && verifiedFlag.enabledForCurrentUser)
664
+ ) {
665
+ throw new WorkspaceFeatureFlagFailure("verification");
666
+ }
667
+ try {
668
+ validateWorkspaceFeatureFlagMutation(
669
+ {
670
+ contractVersion: 2,
671
+ status: "ready",
672
+ key: input.key,
673
+ rules: verifiedFlag.rules,
674
+ scope: mutation.scope,
675
+ },
676
+ {
677
+ key: input.key,
678
+ orgDomain,
679
+ allowExplicitNoOrgTarget: true,
680
+ rules: mutation.rules,
681
+ },
682
+ );
683
+ } catch {
684
+ throw new WorkspaceFeatureFlagFailure("verification");
685
+ }
686
+ return {
687
+ contractVersion: 3,
688
+ status: "verified",
689
+ key: mutation.key,
690
+ rules: verifiedFlag.rules as Record<string, unknown>,
691
+ scope: mutation.scope,
692
+ enabledForCurrentUser: verifiedFlag.enabledForCurrentUser,
693
+ };
694
+ }
504
695
  const app = await resolveTargetApp(admin, input.appId);
505
696
  const targetInput = workspaceFeatureFlagTargetInput(input);
506
697
  let orgDomain: string | undefined;
@@ -533,30 +724,10 @@ export async function setWorkspaceFeatureFlag(
533
724
  throw new WorkspaceFeatureFlagFailure("target-action");
534
725
  let mutation: TargetFeatureFlagMutationResult;
535
726
  try {
536
- mutation = validateWorkspaceFeatureFlagMutation(result.body, {
537
- key: input.key,
538
- orgDomain,
539
- allowExplicitNoOrgTarget: true,
540
- ...(input.operation === "replace-rules" && input.rules
541
- ? {
542
- rules: {
543
- mode: input.rules.mode,
544
- emails: input.rules.emails ?? [],
545
- orgIds: input.rules.orgIds ?? [],
546
- percentage: input.rules.percentage ?? 0,
547
- },
548
- }
549
- : input.operation === "off"
550
- ? {
551
- rules: {
552
- mode: "off",
553
- emails: [],
554
- orgIds: [],
555
- percentage: 0,
556
- },
557
- }
558
- : { enabledForEmail: admin.userEmail }),
559
- });
727
+ mutation = validateWorkspaceFeatureFlagMutation(
728
+ result.body,
729
+ mutationValidationExpectation(admin, input, orgDomain),
730
+ );
560
731
  } catch {
561
732
  throw new WorkspaceFeatureFlagFailure("persistence");
562
733
  }
@@ -632,6 +803,7 @@ export async function getWorkspaceFlagTarget(
632
803
  admin: AnalyticsAdminContext,
633
804
  appId: string,
634
805
  ): Promise<FleetFlagApp> {
806
+ if (appId === ANALYTICS_APP_ID) return listLocalAnalyticsFeatureFlags(admin);
635
807
  const apps = await fetchOrgApps({
636
808
  selfId: "analytics",
637
809
  includeDirectoryApp: true,
@@ -1524,7 +1524,7 @@ export function EventDetailPopover({
1524
1524
  side={isMobile ? "bottom" : (popoverSide ?? "right")}
1525
1525
  sideOffset={isMobile ? 6 : 8}
1526
1526
  collisionPadding={12}
1527
- className="flex max-h-[var(--radix-popover-content-available-height)] w-[min(18rem,calc(100vw-2rem))] flex-col overflow-hidden p-0"
1527
+ className="flex max-h-[var(--radix-popover-content-available-height)] w-[min(420px,calc(100vw-2rem))] flex-col overflow-hidden p-0"
1528
1528
  onClick={(e) => e.stopPropagation()}
1529
1529
  onOpenAutoFocus={(e) => {
1530
1530
  e.preventDefault();
@@ -1553,8 +1553,8 @@ export function EventDetailPopover({
1553
1553
  >
1554
1554
  <TooltipProvider>
1555
1555
  {/* Header */}
1556
- <div className="flex items-center justify-between border-b border-border/60 px-4 py-3">
1557
- <div className="text-base font-medium text-foreground">
1556
+ <div className="flex items-center justify-between border-b border-border px-4 py-2.5">
1557
+ <div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
1558
1558
  <span>
1559
1559
  {isWorkingLocation
1560
1560
  ? t("eventForm.workingLocation")
@@ -311,6 +311,92 @@ function cleanNode(
311
311
  return out;
312
312
  }
313
313
 
314
+ /**
315
+ * Elements whose unclosed start tag swallows the rest of the document in a real
316
+ * parser. `embed` is deliberately absent: it is void, so it never has a closing
317
+ * tag and requiring one would truncate every slide that contains a valid one.
318
+ */
319
+ const SWALLOWING_ELEMENTS = /^(script|style|textarea|iframe|object|svg|math)$/i;
320
+
321
+ /** Elements whose children the HTML parser reads as text rather than markup. */
322
+ const RAW_TEXT_ELEMENTS = /^(script|style|textarea|title)$/i;
323
+
324
+ /**
325
+ * Start-tag positions in `html`, skipping comments and anything inside a quoted
326
+ * attribute value.
327
+ *
328
+ * Scanning the serialized string with a bare regex cannot tell a tag from text:
329
+ * `<p title="Use <style> here">` reads as a `<style>` start tag, and truncating
330
+ * there drops the rest of a perfectly valid slide.
331
+ */
332
+ function startTagPositions(html: string): { name: string; index: number }[] {
333
+ const found: { name: string; index: number }[] = [];
334
+ for (let i = 0; i < html.length; i++) {
335
+ if (html[i] !== "<") continue;
336
+ if (html.startsWith("<!--", i)) {
337
+ const end = html.indexOf("-->", i + 4);
338
+ i = end === -1 ? html.length : end + 2;
339
+ continue;
340
+ }
341
+ const name = /^<([a-z][a-z0-9-]*)/i.exec(html.slice(i, i + 32))?.[1];
342
+ // Walk to this tag's `>`, stepping over quoted values so a `<` or `>`
343
+ // inside one is not read as markup.
344
+ let cursor = i + 1;
345
+ let quote = "";
346
+ while (cursor < html.length) {
347
+ const char = html[cursor];
348
+ if (quote) {
349
+ if (char === quote) quote = "";
350
+ } else if (char === '"' || char === "'") {
351
+ quote = char;
352
+ } else if (char === ">") {
353
+ break;
354
+ }
355
+ cursor++;
356
+ }
357
+ if (!name) {
358
+ i = cursor;
359
+ continue;
360
+ }
361
+ const lower = name.toLowerCase();
362
+ found.push({ name: lower, index: i });
363
+ if (RAW_TEXT_ELEMENTS.test(lower)) {
364
+ // A raw-text element's body is text, not markup — `content: "<script>"`
365
+ // inside a stylesheet is a CSS string, and reading it as a start tag
366
+ // truncated everything after it. Skip to the close tag; no close tag is
367
+ // the unclosed case the caller is looking for, and everything past it is
368
+ // swallowed anyway, so there is nothing further to find.
369
+ const closing = new RegExp(`</\\s*${lower}\\s*>`, "i").exec(
370
+ html.slice(cursor),
371
+ );
372
+ if (!closing) return found;
373
+ i = cursor + closing.index + closing[0].length - 1;
374
+ continue;
375
+ }
376
+ i = cursor;
377
+ }
378
+ return found;
379
+ }
380
+
381
+ /**
382
+ * Truncates at the first swallowing element that never closes.
383
+ *
384
+ * The regex path has to drop the remainder to agree with `cleanNode`. It must
385
+ * check for the closing tag to do that: the sweep used to match any of these
386
+ * tags and cut to the end of the string unconditionally, which ate the
387
+ * sanitized `<style>` block the pass above it had just emitted — and every
388
+ * heading and paragraph after it. A deck with one stylesheet rendered as an
389
+ * empty slide on the SSR'd share and present pages.
390
+ */
391
+ function dropFromFirstUnclosedRawText(html: string): string {
392
+ for (const { name, index } of startTagPositions(html)) {
393
+ if (!SWALLOWING_ELEMENTS.test(name)) continue;
394
+ const closing = new RegExp(`</\\s*${name}\\s*>`, "i");
395
+ if (!closing.test(html.slice(index))) return html.slice(0, index);
396
+ }
397
+ return html;
398
+ }
399
+
314
400
  function sanitizeHtmlString(
315
401
  html: string,
316
402
  scopeSelector?: string,
@@ -335,10 +421,7 @@ function sanitizeHtmlString(
335
421
  // twin runs instead of cleanNode(). An unclosed raw-text or embedding
336
422
  // element swallows the rest of the document in a real parser, so dropping
337
423
  // the remainder is what keeps this path agreeing with the DOM path.
338
- .replace(
339
- /<(script|style|textarea|iframe|object|embed|svg|math)\b[\s\S]*$/i,
340
- "",
341
- )
424
+ .replace(/[\s\S]*/, dropFromFirstUnclosedRawText)
342
425
  .replace(
343
426
  /<(script|iframe|object|embed|form|input|button|select|textarea|meta|base|link|svg|math)\b[^>]*\/?>/gi,
344
427
  "",
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-28
4
+ ---
5
+ A slide containing a `<style>` block no longer renders empty on shared and presented links — the block and everything after it now survive.
package/dist/action.d.ts CHANGED
@@ -19,8 +19,10 @@ import type { ActionAuditConfig } from "./audit/types.js";
19
19
  * selected actions are attributed as `"tool"`.
20
20
  * - `"automation"` — an event-triggered automation dispatched from a stored
21
21
  * workspace trigger with trusted trigger lineage.
22
+ * - `"webmcp"` — a browser agent invoking an automatically projected action
23
+ * through the authenticated page-local WebMCP bridge.
22
24
  */
23
- export type ActionCaller = "tool" | "http" | "frontend" | "cli" | "mcp" | "a2a" | "automation";
25
+ export type ActionCaller = "tool" | "http" | "frontend" | "cli" | "mcp" | "webmcp" | "a2a" | "automation";
24
26
  /**
25
27
  * Trusted automation lineage added by the trigger dispatcher. Action inputs
26
28
  * must never be used to create or override this context.
@@ -66,6 +66,7 @@ import { AgentNativeRouteWarmup } from "./route-warmup.js";
66
66
  import { RouteTransitionIndicator } from "./RouteTransitionIndicator.js";
67
67
  import { RuntimeConfigNotice } from "./RuntimeConfigNotice.js";
68
68
  import { EMBEDDED_THEME_CHANGE_EVENT, applyEmbeddedThemeUpdate, parseEmbeddedThemeUpdate, } from "./theme.js";
69
+ import { createAgentNativeServerActionWebMcpRegistration } from "./webmcp.js";
69
70
  const DEFAULT_TOASTER = (_jsx(Toaster, { richColors: true, position: "bottom-left", offset: { bottom: 44, left: 32 }, mobileOffset: { bottom: 44, left: 16 } }));
70
71
  function RoutedAppEnhancements() {
71
72
  const isInRouter = useInRouterContext();
@@ -73,6 +74,17 @@ function RoutedAppEnhancements() {
73
74
  return null;
74
75
  return (_jsxs(_Fragment, { children: [_jsx(AgentNativeRouteWarmup, {}), _jsx(RouteTransitionIndicator, {})] }));
75
76
  }
77
+ function AutomaticWebMcpActionRegistration() {
78
+ useEffect(() => {
79
+ const registration = createAgentNativeServerActionWebMcpRegistration();
80
+ void registration.start().catch(() => {
81
+ // WebMCP is progressive enhancement. Session expiry or a transient
82
+ // manifest failure must not prevent the authenticated app from loading.
83
+ });
84
+ return () => registration.stop();
85
+ }, []);
86
+ return null;
87
+ }
76
88
  function readDocumentTitleFallback() {
77
89
  const selectors = [
78
90
  'meta[name="application-name"]',
@@ -153,5 +165,5 @@ export function AppProviders({ queryClient, isPublicPath = false, clientOnlyFall
153
165
  if (isPublicPath) {
154
166
  return (_jsx(ProvidersInner, { queryClient: queryClient, defaultTheme: defaultTheme, themeAttribute: themeAttribute, tooltipDelayDuration: tooltipDelayDuration, toaster: toaster, disableThemeTransitions: disableThemeTransitions, i18n: i18n, documentTitleFallback: documentTitleFallback, showProductionEnvironmentBadge: false, children: children }));
155
167
  }
156
- return (_jsx(ClientOnly, { fallback: fallback, children: _jsx(ProvidersInner, { queryClient: queryClient, defaultTheme: defaultTheme, themeAttribute: themeAttribute, tooltipDelayDuration: tooltipDelayDuration, toaster: toaster, disableThemeTransitions: disableThemeTransitions, i18n: i18n, documentTitleFallback: documentTitleFallback, showProductionEnvironmentBadge: !sessionBypass, children: _jsx(RequireSession, { bypass: sessionBypass, fallback: fallback, children: sessionBypass ? (children) : (_jsx(FirstRunOnboardingStartupGate, { children: children })) }) }) }));
168
+ return (_jsx(ClientOnly, { fallback: fallback, children: _jsx(ProvidersInner, { queryClient: queryClient, defaultTheme: defaultTheme, themeAttribute: themeAttribute, tooltipDelayDuration: tooltipDelayDuration, toaster: toaster, disableThemeTransitions: disableThemeTransitions, i18n: i18n, documentTitleFallback: documentTitleFallback, showProductionEnvironmentBadge: !sessionBypass, children: _jsx(RequireSession, { bypass: sessionBypass, fallback: fallback, children: sessionBypass ? (children) : (_jsxs(FirstRunOnboardingStartupGate, { children: [_jsx(AutomaticWebMcpActionRegistration, {}), children] })) }) }) }));
157
169
  }
@@ -52,6 +52,7 @@ export interface AgentNativeHostSession {
52
52
  }
53
53
  export interface AgentNativeClientActionRuntime {
54
54
  requestId?: string;
55
+ signal?: AbortSignal;
55
56
  origin: string;
56
57
  context: AgentNativeHostContext;
57
58
  session: AgentNativeHostSession;
@@ -71,4 +71,8 @@ export interface AgentNativeWebMcpRegistration {
71
71
  start(): Promise<void>;
72
72
  stop(): void;
73
73
  }
74
+ export declare function createAgentNativeServerActionWebMcpRegistration(options?: {
75
+ document?: Document;
76
+ fetch?: typeof fetch;
77
+ }): AgentNativeWebMcpRegistration;
74
78
  export declare function createAgentNativeWebMcpRegistration(options: AgentNativeWebMcpRegistrationOptions): AgentNativeWebMcpRegistration;
@@ -234,6 +234,52 @@ export function createAgentNativeWebMcpClient(options = {}) {
234
234
  };
235
235
  return client;
236
236
  }
237
+ export function createAgentNativeServerActionWebMcpRegistration(options) {
238
+ const fetchImpl = options?.fetch ?? ((...args) => fetch(...args));
239
+ return createAgentNativeWebMcpRegistration({
240
+ document: options?.document,
241
+ maxToolCount: 1_000,
242
+ maxDescriptionChars: 10_000,
243
+ actions: async () => {
244
+ const response = await fetchImpl("/_agent-native/webmcp/manifest", {
245
+ credentials: "same-origin",
246
+ headers: { Accept: "application/json" },
247
+ });
248
+ if (!response.ok) {
249
+ throw new Error(`Unable to load WebMCP actions (${response.status})`);
250
+ }
251
+ const manifest = (await response.json());
252
+ if (!Array.isArray(manifest)) {
253
+ throw new Error("WebMCP action manifest must be an array");
254
+ }
255
+ return manifest.map((action) => ({
256
+ name: action.name,
257
+ description: action.description,
258
+ ...(action.inputSchema ? { schema: action.inputSchema } : {}),
259
+ ...(action.readOnly ? { readOnly: true } : {}),
260
+ run: async (args, runtime) => {
261
+ const result = await fetchImpl(`/_agent-native/webmcp/actions/${encodeURIComponent(action.name)}`, {
262
+ method: "POST",
263
+ credentials: "same-origin",
264
+ headers: {
265
+ Accept: "application/json",
266
+ "Content-Type": "application/json",
267
+ },
268
+ body: JSON.stringify(args),
269
+ ...(runtime.signal ? { signal: runtime.signal } : {}),
270
+ });
271
+ const body = await result.json();
272
+ if (!result.ok) {
273
+ throw new Error(isRecord(body) && typeof body.error === "string"
274
+ ? body.error
275
+ : `WebMCP action failed (${result.status})`);
276
+ }
277
+ return body;
278
+ },
279
+ }));
280
+ },
281
+ });
282
+ }
237
283
  function resolveActions(actions) {
238
284
  return Promise.resolve(typeof actions === "function" ? actions() : actions).then((value) => {
239
285
  if (!Array.isArray(value)) {
@@ -271,7 +317,7 @@ function actionManifest(action) {
271
317
  function actionInputSchema(action) {
272
318
  return action.schema ?? action.parameters ?? EMPTY_INPUT_SCHEMA;
273
319
  }
274
- function actionRuntime(options, context, session) {
320
+ function actionRuntime(options, context, session, signal) {
275
321
  const origin = options.origin ?? "agent-native-webmcp";
276
322
  const runCommand = async (command, payload) => {
277
323
  const handler = options.commands?.[command] ??
@@ -281,6 +327,7 @@ function actionRuntime(options, context, session) {
281
327
  return handler({ command, payload, origin }, undefined);
282
328
  };
283
329
  return {
330
+ ...(signal ? { signal } : {}),
284
331
  origin,
285
332
  context,
286
333
  session,
@@ -381,7 +428,7 @@ export function createAgentNativeWebMcpRegistration(options) {
381
428
  : {}),
382
429
  },
383
430
  execute: async (input, executionOptions) => {
384
- if (executionOptions.signal.aborted) {
431
+ if (executionOptions?.signal.aborted) {
385
432
  throw new Error(`WebMCP action "${action.name}" was aborted`);
386
433
  }
387
434
  validateBoundedJson(input, `WebMCP action "${action.name}" input`, options.maxInputChars ?? DEFAULT_INPUT_CHARS);
@@ -417,8 +464,8 @@ export function createAgentNativeWebMcpRegistration(options) {
417
464
  throw new Error(`WebMCP action "${action.name}" was not approved`);
418
465
  }
419
466
  }
420
- const result = await action.run(input, actionRuntime(options, context, session));
421
- if (executionOptions.signal.aborted) {
467
+ const result = await action.run(input, actionRuntime(options, context, session, executionOptions?.signal));
468
+ if (executionOptions?.signal.aborted) {
422
469
  throw new Error(`WebMCP action "${action.name}" was aborted`);
423
470
  }
424
471
  return serializeWebMcpResult(result, `WebMCP action "${action.name}" result`, options.maxResultChars ?? DEFAULT_RESULT_CHARS);
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- error: string;
30
29
  ok?: undefined;
30
+ error: string;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -2,7 +2,7 @@ export * from "./config.js";
2
2
  export { addContextToAgentChat, appendAgentChatContextToMessage, clearAgentChatContext, formatAgentChatContextItemsForPrompt, listAgentChatContext, refreshAgentChatContext, removeAgentChatContextItem, sendToAgentChat, sendToAgentChatAndConfirm, setAgentChatContextItem, setContextToAgentChat, isEmbedMcpChatBridgeActive, useAgentChatContext, useAgentChatGenerating, useDevMode, useSendToAgentChat, AGENT_NATIVE_MCP_APP_HOST_MESSAGE_TYPES, getMcpAppHostContext, openMcpAppHostLink, requestMcpAppDisplayMode, sendMcpAppHostMessage, updateMcpAppModelContext, useMcpAppHostContext, CodeRequiredDialog, AgentNativeI18nProvider, LanguagePicker, getLocaleInitScript, localeDirection, normalizeLocaleCode, normalizeLocalePreference, normalizeLocalizationPreference, resolveLocaleFromCandidates, resolveLocaleFromPreference, useFormatters, useLocale, useT, DEFAULT_LOCALE, LOCALE_HYDRATION_GLOBAL, LOCALE_METADATA, LOCALE_STORAGE_KEY, SUPPORTED_LOCALES, useDbSync, useFileWatcher, useSession, cn, ApiKeySettings, type AgentChatContextItem, type AgentChatContextMessage, type AgentChatContextMutationOptions, type AgentChatContextRemoveOptions, type AgentChatContextSetOptions, type AgentChatContextState, type AgentChatMessage, type AgentNativeMcpAppHostMessageType, type McpAppDisplayMode, type McpAppHostChatMessage, type McpAppHostCapabilities, type McpAppHostContext, type McpAppHostContextSnapshot, type McpAppModelContextContentPart, type McpAppModelContextUpdate, type CodeRequiredDialogProps, type AuthSession, type UseAgentChatContextResult, type AgentNativeI18nCatalog, type AgentNativeI18nProviderProps, type LocaleCode, type LocaleHydrationPayload, type LocaleMessages, type LocaleMetadata, type LocalePreference, type LocalizationPreference, } from "./client/index.js";
3
3
  export { agentChat } from "./shared/index.js";
4
4
  export { parseArgs, camelCaseArgs } from "./scripts/parse-args.js";
5
- export { AgentNativeWebMcpUnsupportedError, createAgentNativeWebMcpClient, createAgentNativeWebMcpRegistration, isAgentNativeWebMcpSupported, type AgentNativeWebMcpApprovalRequest, type AgentNativeWebMcpClient, type AgentNativeWebMcpClientOptions, type AgentNativeWebMcpRegistration, type AgentNativeWebMcpRegistrationOptions, type AgentNativeWebMcpTool, type AgentNativeWebMcpToolAnnotations, type AgentNativeWebMcpToolExecutionOptions, type AgentNativeWebMcpToolResult, } from "./client/webmcp.js";
5
+ export { AgentNativeWebMcpUnsupportedError, createAgentNativeWebMcpClient, createAgentNativeWebMcpRegistration, createAgentNativeServerActionWebMcpRegistration, isAgentNativeWebMcpSupported, type AgentNativeWebMcpApprovalRequest, type AgentNativeWebMcpClient, type AgentNativeWebMcpClientOptions, type AgentNativeWebMcpRegistration, type AgentNativeWebMcpRegistrationOptions, type AgentNativeWebMcpTool, type AgentNativeWebMcpToolAnnotations, type AgentNativeWebMcpToolExecutionOptions, type AgentNativeWebMcpToolResult, } from "./client/webmcp.js";
6
6
  export { defineAction, fail, type FailOptions, ActionContractError, isActionContractError, type ActionContractErrorOptions, AgentActionStopError, isAgentActionStopError, type ActionHttpConfig, type AgentActionStopOptions, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, type ActionMcpAppConfig, type ActionMcpAppCsp, type ActionMcpAppCspBuilder, type ActionMcpAppHtmlBuilder, type ActionMcpAppPermissions, type ActionMcpAppResourceConfig, type ActionMcpAppResourceMeta, } from "./action.js";
7
7
  export { isEmailDerivedName, normalizeUserProfileName, resolveUserProfileName, USER_PROFILE_SETTING_KEY, type UserProfile, } from "./user-profile/index.js";
8
8
  export { ACTION_CHAT_UI_DATA_CHART_RENDERER, ACTION_CHAT_UI_DATA_INSIGHTS_RENDERER, ACTION_CHAT_UI_DATA_TABLE_RENDERER, ACTION_CHAT_UI_DATA_WIDGET_RENDERER, ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER, ACTION_CHAT_UI_WORKSPACE_FILE_RENDERER, type ActionChatUIConfig, } from "./action-ui.js";
@@ -8,7 +8,7 @@ export { addContextToAgentChat, appendAgentChatContextToMessage, clearAgentChatC
8
8
  export { agentChat } from "./shared/index.js";
9
9
  // Pure utilities (no Node.js deps — safe for browser and SSR)
10
10
  export { parseArgs, camelCaseArgs } from "./scripts/parse-args.js";
11
- export { AgentNativeWebMcpUnsupportedError, createAgentNativeWebMcpClient, createAgentNativeWebMcpRegistration, isAgentNativeWebMcpSupported, } from "./client/webmcp.js";
11
+ export { AgentNativeWebMcpUnsupportedError, createAgentNativeWebMcpClient, createAgentNativeWebMcpRegistration, createAgentNativeServerActionWebMcpRegistration, isAgentNativeWebMcpSupported, } from "./client/webmcp.js";
12
12
  // defineAction — used by template actions, no Node.js deps
13
13
  export { defineAction, fail, ActionContractError, isActionContractError, AgentActionStopError, isAgentActionStopError, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, } from "./action.js";
14
14
  export { isEmailDerivedName, normalizeUserProfileName, resolveUserProfileName, USER_PROFILE_SETTING_KEY, } from "./user-profile/index.js";
@@ -51,8 +51,8 @@ export declare function handleDeleteResource(event: any): Promise<{
51
51
  error: string;
52
52
  ok?: undefined;
53
53
  } | {
54
- ok: boolean;
55
54
  error?: undefined;
55
+ ok: boolean;
56
56
  }>;
57
57
  /** POST /_agent-native/resources/upload — upload a file as a resource */
58
58
  export declare function handleUploadResource(event: any): Promise<import("./store.js").Resource | {
@@ -73,10 +73,10 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
73
73
  runId: string | null;
74
74
  expiresAt: number | null;
75
75
  metadata: string | null;
76
+ error?: undefined;
76
77
  url: string;
77
78
  provider: string;
78
79
  storageSetupRequired?: undefined;
79
- error?: undefined;
80
80
  } | {
81
81
  error: string;
82
82
  storageSetupRequired: boolean;
@@ -60,10 +60,5 @@ export interface MountActionRoutesOptions {
60
60
  */
61
61
  actionRouteAuth?: ActionRouteAuthAdapter;
62
62
  }
63
- /**
64
- * Mount discovered actions as HTTP endpoints.
65
- *
66
- * Only actions from `autoDiscoverActions` (template actions) are mounted.
67
- * Built-in actions (resource-*, chat-*, shell, etc.) are NOT passed here.
68
- */
69
63
  export declare function mountActionRoutes(nitroApp: any, actions: Record<string, ActionEntry>, options?: MountActionRoutesOptions): void;
64
+ export declare function mountWebMcpActionRoutes(nitroApp: any, actions: Record<string, ActionEntry>, options?: MountActionRoutesOptions): void;
@@ -240,21 +240,23 @@ async function resolveRequestAuthCapability(event) {
240
240
  * Only actions from `autoDiscoverActions` (template actions) are mounted.
241
241
  * Built-in actions (resource-*, chat-*, shell, etc.) are NOT passed here.
242
242
  */
243
- export function mountActionRoutes(nitroApp, actions, options) {
243
+ function mountActionRoutesInternal(nitroApp, actions, options) {
244
244
  const mounted = [];
245
245
  const app = getH3App(nitroApp);
246
246
  for (const [name, entry] of Object.entries(actions)) {
247
247
  // Skip agent-only actions
248
- if (entry.http === false)
248
+ if (entry.http === false && !options?.includeAgentOnly)
249
249
  continue;
250
- const method = entry.http?.method ?? "POST";
251
- const path = entry.http?.path ?? name;
252
- const routePath = `${ROUTE_PREFIX}/${path}`;
250
+ const http = entry.http || undefined;
251
+ const method = options?.forcePost ? "POST" : (http?.method ?? "POST");
252
+ const path = options?.forcePost ? name : (http?.path ?? name);
253
+ const routePath = `${options?.routePrefix ?? ROUTE_PREFIX}/${path}`;
253
254
  // These two actions authenticate with a scoped A2A bearer rather than a
254
255
  // browser session. Let that verifier see the request before the cookie
255
256
  // auth guard rejects it; the action route still fails closed on invalid
256
257
  // or missing credentials.
257
- if (name === "list-feature-flags" || name === "set-feature-flag") {
258
+ if (!options?.caller &&
259
+ (name === "list-feature-flags" || name === "set-feature-flag")) {
258
260
  registerAuthPublicPaths([routePath], app);
259
261
  }
260
262
  app.use(routePath, defineEventHandler(async (event) => {
@@ -329,7 +331,7 @@ export function mountActionRoutes(nitroApp, actions, options) {
329
331
  // through, so a live same-origin session cookie can't silently execute
330
332
  // the request as the logged-in user.
331
333
  let resolvedCaller = null;
332
- {
334
+ if (options?.allowDelegatedCaller !== false) {
333
335
  let caller;
334
336
  try {
335
337
  caller = options?.actionRouteAuth?.resolveCaller
@@ -468,11 +470,12 @@ export function mountActionRoutes(nitroApp, actions, options) {
468
470
  // userEmail / orgId mirror the request context resolved above (do
469
471
  // NOT inject a dev identity — leave undefined when unauthenticated).
470
472
  try {
471
- const caller = resolvedCaller
472
- ? "a2a"
473
- : isFrontendActionRequest(event)
474
- ? "frontend"
475
- : "http";
473
+ const caller = options?.caller ??
474
+ (resolvedCaller
475
+ ? "a2a"
476
+ : isFrontendActionRequest(event)
477
+ ? "frontend"
478
+ : "http");
476
479
  const result = await entry.run(params, {
477
480
  userEmail,
478
481
  orgId: orgId ?? null,
@@ -574,11 +577,12 @@ export function mountActionRoutes(nitroApp, actions, options) {
574
577
  method: reqMethod,
575
578
  tags: {
576
579
  action: name,
577
- caller: resolvedCaller
578
- ? "a2a"
579
- : isFrontendActionRequest(event)
580
- ? "frontend"
581
- : "http",
580
+ caller: options?.caller ??
581
+ (resolvedCaller
582
+ ? "a2a"
583
+ : isFrontendActionRequest(event)
584
+ ? "frontend"
585
+ : "http"),
582
586
  status_code: String(status),
583
587
  },
584
588
  ...(requestId ? { extra: { request_id: requestId } } : {}),
@@ -598,3 +602,40 @@ export function mountActionRoutes(nitroApp, actions, options) {
598
602
  if (mounted.length > 0 && process.env.DEBUG)
599
603
  console.log(`[action-routes] Mounted ${mounted.length} action route(s): ${mounted.join(", ")}`);
600
604
  }
605
+ export function mountActionRoutes(nitroApp, actions, options) {
606
+ mountActionRoutesInternal(nitroApp, actions, options);
607
+ }
608
+ export function mountWebMcpActionRoutes(nitroApp, actions, options) {
609
+ const eligible = Object.fromEntries(Object.entries(actions).filter(([name, entry]) => /^[A-Za-z0-9_.-]{1,128}$/.test(name) &&
610
+ entry.agentTool !== false &&
611
+ entry.needsApproval === undefined));
612
+ if (Object.keys(eligible).length === 0)
613
+ return;
614
+ const app = getH3App(nitroApp);
615
+ app.use("/_agent-native/webmcp/manifest", defineEventHandler(async (event) => {
616
+ if (getMethod(event) !== "GET") {
617
+ setResponseStatus(event, 405);
618
+ return { error: "Method not allowed. Use GET." };
619
+ }
620
+ if (!options?.getOwnerFromEvent) {
621
+ throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
622
+ }
623
+ await options.getOwnerFromEvent(event);
624
+ setResponseHeader(event, "Cache-Control", "no-store");
625
+ return Object.entries(eligible).map(([name, entry]) => ({
626
+ name,
627
+ description: entry.tool.description,
628
+ inputSchema: entry.tool.parameters,
629
+ readOnly: entry.readOnly === true,
630
+ }));
631
+ }));
632
+ mountActionRoutesInternal(nitroApp, eligible, {
633
+ ...options,
634
+ routePrefix: "/_agent-native/webmcp/actions",
635
+ includeAgentOnly: true,
636
+ forcePost: true,
637
+ caller: "webmcp",
638
+ actionRouteAuth: undefined,
639
+ allowDelegatedCaller: false,
640
+ });
641
+ }
@@ -2128,7 +2128,7 @@ export function createAgentChatPlugin(options) {
2128
2128
  // Ignore — templates without sharing still work.
2129
2129
  }
2130
2130
  if (Object.keys(httpActions).length > 0) {
2131
- const { mountActionRoutes } = await import("./action-routes.js");
2131
+ const { mountActionRoutes, mountWebMcpActionRoutes } = await import("./action-routes.js");
2132
2132
  if (options?.actionRoutePublicPaths?.length) {
2133
2133
  registerAuthPublicPaths(options.actionRoutePublicPaths, getH3App(nitroApp));
2134
2134
  }
@@ -2139,6 +2139,13 @@ export function createAgentChatPlugin(options) {
2139
2139
  resolveOrgId: options?.resolveOrgId,
2140
2140
  actionRouteAuth: options?.actionRouteAuth,
2141
2141
  });
2142
+ mountWebMcpActionRoutes(nitroApp, httpActions, {
2143
+ getOwnerFromEvent,
2144
+ getUserNameFromEvent,
2145
+ appId: options?.appId,
2146
+ resolveOrgId: options?.resolveOrgId,
2147
+ actionRouteAuth: options?.actionRouteAuth,
2148
+ });
2142
2149
  }
2143
2150
  const preRunGitStatusByThread = new Map();
2144
2151
  async function recordPreRunGitStatus(threadId) {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: "WebMCP"
3
- description: "Expose explicit Agent-Native client actions to browser agents and consume page-local WebMCP tools from Agent-Native chat."
3
+ description: "Automatically expose Agent-Native actions to browser agents, register explicit client actions, and consume page-local WebMCP tools from Agent-Native chat."
4
4
  search: "WebMCP browser tools modelContext registerTool getTools executeTool browser session"
5
5
  ---
6
6
 
@@ -15,24 +15,42 @@ discover them, and the browser mediates execution.
15
15
 
16
16
  The framework supports both directions:
17
17
 
18
- <!-- i18n-copy-ignore: oxfmt-only table alignment; no translated prose changed. -->
19
-
20
18
  | You want to... | Use |
21
19
  | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
22
- | Make selected actions in your app available to a browser agent | `createAgentNativeWebMcpRegistration` |
20
+ | Make backend actions available to a browser agent | Automatic on authenticated pages through `AppProviders` |
21
+ | Make page-local client actions available to a browser agent | `createAgentNativeWebMcpRegistration` |
23
22
  | Let Agent-Native chat discover and call WebMCP tools in the current page | `createAgentNativeWebMcpClient` plus `webmcp` on `AgentNativeEmbedded` or `AgentNativeFrame` |
24
23
  | Connect a headless or remote agent to an app | [MCP Server](/docs/mcp-protocol), [External Agents](/docs/external-agents), or [A2A](/docs/a2a-protocol) |
25
24
 
26
25
  WebMCP is not a network MCP server and does not make a closed app globally
27
26
  discoverable. A consuming agent needs an open, connected browser page and the
28
- page must explicitly opt into the tools it exposes.
27
+ page must expose tools to it.
29
28
 
30
29
  The browser API is experimental and may change. See the [Chrome WebMCP
31
30
  Imperative API](https://developer.chrome.com/docs/ai/webmcp/imperative-api) and
32
31
  the [WebMCP specification](https://webmachinelearning.github.io/webmcp/) for
33
32
  the current browser contract.
34
33
 
35
- ## Produce WebMCP tools
34
+ ## Automatic backend actions
35
+
36
+ On an authenticated app page, `AppProviders` automatically registers eligible
37
+ backend actions with `document.modelContext`. The tools are derived from the
38
+ same action registry used by the in-app agent and execute through the same
39
+ validated, authenticated server dispatcher. This includes agent-only actions
40
+ declared with `http: false`; WebMCP does not turn those actions into public HTTP
41
+ APIs.
42
+
43
+ An action is eligible when `agentTool` is not `false` and `needsApproval` is
44
+ not declared. The latter is a fail-closed boundary: the server action approval
45
+ contract belongs to the agent loop and cannot be delegated to a browser hint.
46
+ Public paths, signed-out pages, and `sessionBypass` surfaces do not register
47
+ backend action tools. Browsers without `document.modelContext` simply keep the
48
+ normal app behavior.
49
+
50
+ No per-app WebMCP list or new `defineAction` metadata is required. Change the
51
+ existing action definition when its exposure should change.
52
+
53
+ ## Explicit page-local actions
36
54
 
37
55
  Register only the actions that should be available to a page agent. The
38
56
  framework removes the action's local `run` function from the descriptor and
@@ -181,10 +199,10 @@ remote or headless tool calls, and use A2A when another app's agent should
181
199
  receive work and reason over it. WebMCP is an opt-in browser bridge, not a
182
200
  replacement for either network protocol.
183
201
 
184
- The framework does not auto-register every backend action as a WebMCP tool.
185
- Choose a small explicit producer list, keep side effects behind approval, and
186
- retain normal MCP or A2A exposure for capabilities that must work without an
187
- open browser page.
202
+ The framework automatically projects eligible backend actions and still lets a
203
+ page register explicit client-only capabilities. Keep server-side approval
204
+ actions on the agent loop, and retain normal MCP or A2A exposure for
205
+ capabilities that must work without an open browser page.
188
206
 
189
207
  ## Compatibility and safety
190
208
 
@@ -195,7 +213,8 @@ open browser page.
195
213
  - Treat descriptions, schemas, annotations, and results as page-provided
196
214
  data. The framework validates and bounds them, but agents should still treat
197
215
  `untrustedContentHint` results as untrusted content.
198
- - Use `stop()` when the owning page or component unmounts so stale tools do not
199
- remain available.
216
+ - Automatic registrations are aborted when the private app shell unmounts. Use
217
+ `stop()` for explicit page-local registrations so stale tools do not remain
218
+ available.
200
219
  - Keep server MCP and A2A endpoints for remote access. WebMCP only reaches the
201
220
  tools registered in a connected browser context.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.176.1",
3
+ "version": "0.177.0-nightly-20260828143953",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {