Skip to content

bug: app.onhostcontextchanged handlers silently overwrite each other #225

@antonpk1

Description

@antonpk1

Summary

Setting app.onhostcontextchanged multiple times silently overwrites previous handlers. This breaks real-time theme updates and any custom host context handling.

Root Cause

The setter calls setNotificationHandler() which replaces the previous handler:

// app.ts line 510-521
set onhostcontextchanged(callback) {
  this.setNotificationHandler(
    McpUiHostContextChangedNotificationSchema,
    (n) => {
      this._hostContext = { ...this._hostContext, ...n.params };
      callback(n.params);  // Only ONE callback ever runs
    },
  );
}

Breaking Examples

Example 1: useHostStyles internal conflict

// User writes this simple code:
function MyWidget() {
  const { app } = useApp({ ... });
  useHostStyles(app, hostContext);  // Expects theme updates to work
  return <div>...</div>;
}

// BREAKS because inside useHostStyles:
useHostStyleVariables(app, ...);  // Sets handler A (theme + variables)
useHostFonts(app, ...);           // Sets handler B (fonts only) - OVERWRITES A!

// Result: Theme changes → only fonts handler runs → theme never updates

Example 2: User handler + SDK hook

function MyWidget() {
  const { app } = useApp({ ... });
  const [ctx, setCtx] = useState(null);

  useEffect(() => {
    if (!app) return;
    // User sets their own handler
    app.onhostcontextchanged = (params) => {
      setCtx(prev => ({ ...prev, ...params }));  // Update React state
      trackAnalytics("theme_changed", params);   // Custom logic
    };
  }, [app]);

  // Later, user adds SDK hook for convenience
  useHostStyles(app, ctx);  // OVERWRITES user handler!

  // Result: setCtx() never called, trackAnalytics() never called
}

Example 3: Two SDK hooks

function MyWidget() {
  const { app } = useApp({ ... });

  // User wants variables but not fonts
  useHostStyleVariables(app, ctx);

  // User also wants to track theme changes
  useEffect(() => {
    app.onhostcontextchanged = (params) => {
      console.log("Theme changed to:", params.theme);  // OVERWRITES SDK hook!
    };
  }, [app]);

  // Result: Variables never applied, only console.log runs
}

Example 4: Multiple custom handlers (impossible today)

function MyWidget() {
  const { app } = useApp({ ... });

  // Component A wants to know about theme
  useEffect(() => {
    app.onhostcontextchanged = (params) => {
      updateComponentA(params);
    };
  }, [app]);

  // Component B also wants to know about theme  
  useEffect(() => {
    app.onhostcontextchanged = (params) => {
      updateComponentB(params);  // OVERWRITES Component A!
    };
  }, [app]);

  // Result: Only Component B gets updates
}

Symptoms

  • Dark/light mode toggle does not update widget in real-time
  • Custom onhostcontextchanged handlers stop working after adding SDK hooks
  • No errors in console (silent failure)
  • Requires page refresh to see changes

Proposed Fix

Change the setter to chain handlers instead of replacing:

private _hostContextChangedHandlers: Array<(params) => void> = [];

set onhostcontextchanged(callback) {
  this._hostContextChangedHandlers.push(callback);
  
  // Only register the notification handler once
  if (this._hostContextChangedHandlers.length === 1) {
    this.setNotificationHandler(
      McpUiHostContextChangedNotificationSchema,
      (n) => {
        this._hostContext = { ...this._hostContext, ...n.params };
        for (const handler of this._hostContextChangedHandlers) {
          handler(n.params);
        }
      },
    );
  }
}

Workaround

Until fixed, use a single combined handler instead of SDK hooks:

useEffect(() => {
  if (!app) return;
  
  const ctx = app.getHostContext();
  if (ctx?.theme) applyDocumentTheme(ctx.theme);
  if (ctx?.styles?.variables) applyHostStyleVariables(ctx.styles.variables);
  if (ctx?.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts);

  app.onhostcontextchanged = (params) => {
    if (params.theme) applyDocumentTheme(params.theme);
    if (params.styles?.variables) applyHostStyleVariables(params.styles.variables);
    if (params.styles?.css?.fonts) applyHostFonts(params.styles.css.fonts);
    // Add any custom logic here too
  };
}, [app]);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions