npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@privos_ai/app-react

v0.7.0

Published

React helpers for building Privos MCP apps

Readme

@privos_ai/app-react

React hooks for building apps on the Privos platform. Thin wrapper around MCP Apps protocol PostMessage transport.

Install

npm install @privos_ai/app-react

Usage

import { PrivosAppProvider, usePrivosContext, useLists, usePrivosApp } from '@privos_ai/app-react';

function MyApp() {
  const { roomId, userId, theme } = usePrivosContext();
  const { data: lists, loading } = useLists(roomId);
  const app = usePrivosApp();

  async function createItem() {
    await app.callServerTool({
      name: 'privos.lists.createItem',
      arguments: { listId: 'abc', title: 'New item' },
    });
  }

  return (
    <div>
      <p>Room: {roomId} | Theme: {theme}</p>
      {loading ? <p>Loading...</p> : lists?.map(l => <div key={l._id}>{l.name}</div>)}
      <button onClick={createItem}>Add Item</button>
    </div>
  );
}

export default function App() {
  return (
    <PrivosAppProvider>
      <MyApp />
    </PrivosAppProvider>
  );
}

Hooks

| Hook | Returns | Description | |------|---------|-------------| | usePrivosApp() | McpApp | MCP app instance for callServerTool() | | usePrivosContext() | PrivosContext | Non-secret user/room/theme display context fetched through mcpapp.context.get and merged with HOST_CONTEXT_CHANGED | | usePrivosCapability(scope) | { resolved, granted, scope } | Presentation helper for deterministic optional-feature degradation; Hub still authorizes every call | | usePrivosTool(name, args) | { data, loading, error, refetch } | Auto-fetching tool call (for reads) | | useLists(roomId) | { data, loading, error } | Lists in room | | useFiles(roomId) | { data, loading, error } | Files in room | | useRoom(roomId?) | { data, loading, error } | Room metadata | | useAppChatSurface(opts) | { supported, isOpen, open, close } | Render your own AI chat window instead of the hub's |

Owning the AI chat surface

By default the hub renders its own AI chat and your app can only steer it. If your app ships its own chat design, useAppChatSurface claims the surface: clicking the hub's floating launcher then opens your chat window and the launcher hides until you close it.

const { resolved, supported, close } = useAppChatSurface({
  onOpen: () => setChatVisible(true),
  onClose: () => setChatVisible(false),
});

// Wire your minimize button to close() so the hub launcher comes back.
<button onClick={close}>Minimize</button>

// `supported` is false where the host has no launcher to hand over (standalone /app/:appId
// page, sidebar panel) — render your own entry point there. Wait for `resolved`: before the
// host answers, `supported` is still false and you would paint a second launcher next to the
// hub's own. Call open() too, so the host knows to hide its launcher if it has one.
{resolved && !supported && (
  <button onClick={() => { setChatVisible(true); open(); }}>Ask AI</button>
)}

Rules worth knowing:

  • Per mount. Ownership is dropped on iframe reload, tab switch, and unmount. Nothing is persisted and no manifest field is involved. The hook re-claims automatically when the host reinitializes the iframe, so you do not have to handle the reload case yourself.
  • Acknowledge quickly. The host waits ~1.5s after ui/chat.open for the app to confirm. The hook acks for you; if you drive the bridge by hand, call setChatOpen(true) promptly or the host takes the surface back, restores its own launcher, and sends you ui/chat.close { reason: 'timeout' }.
  • Wire your minimize button to close() so the hub launcher reappears.
  • One consumer per app. The underlying handlers are single-slot: mounting useAppChatSurface twice means the second instance wins and unmounting it withdraws ownership for both.
  • The AI backend is unchanged — your chat window still reaches the hub AI through the bridge.

Microphone and wake lock

The app iframe runs in an opaque origin, where browsers refuse getUserMedia and the Wake Lock API. The host does both for you, for what the tool declares in _meta.ui.permissions:

const app = usePrivosApp();

// Call from a click/keypress handler.
const mic = await app.startMicrophone?.({ sampleRate: 16000, onData: (pcm: Int16Array) => ws.send(pcm) });
if (!mic?.granted) {
  // 'not_declared' | 'user_activation_required' | 'denied' | 'unavailable' | 'unsupported_host'
  // unsupported_host (or no startMicrophone): older hub, fall back to getUserMedia
} else {
  mic.stop(); // when done
}

await app.requestWakeLock?.(); // host re-acquires on visibility until released
app.releaseWakeLock?.();

Frames are mono signed 16-bit PCM at mic.sampleRate. The first time, the hub asks the user " wants to use your microphone — Allow / Block" (Block answers denied). Camera is not brokered.

User-delegated identity

The iframe receives display context, not a bearer or user token. Calls made through app.rest(), uploadFile(), and callServerTool() remain mediated by Hub, which intersects the installation grant with the current user's native ACL.

When Hub privately dispatches a backend tool call, it places the verified actor in the short-lived, body-bound Hub dispatch assertion. The app-server workload SDK validates that assertion before application code runs. A workload token is an app principal and can never be converted into a user identity.

Do not accept userId from iframe request bodies as authorization evidence and do not ask the browser to forward Hub credentials to an app backend.

Helpers (also exported)

| Export | Use | |--------|-----| | parseToolResult | Parse MCP tool / host-bridge payloads (isError, content[0].text, nested result) |

Provider

Wrap your app root with PrivosAppProvider. It creates a PostMessage-based MCP connection to the Privos host iframe.

<PrivosAppProvider>
  <YourApp />
</PrivosAppProvider>

Reads vs Mutations

  • Reads: Use usePrivosTool or convenience hooks — auto-fetches on mount
  • Mutations: Use usePrivosApp() then call app.callServerTool() in event handlers

Theme

usePrivosContext().theme returns 'light' or 'dark', updated in real-time when the Privos user toggles theme. See Theme Integration docs.

PrivosAppProvider also applies the workspace's resolved theme colours automatically: as soon as the host pushes a context with themeTokens, each --base-* CSS custom property (--base-primary, --base-bg-main, --base-radius-md, --base-font-family, etc.) is written onto this document's <html> element via style.setProperty, and <html data-theme> is kept in sync with the active mode. No setup is required — reference the variables directly in your CSS:

body {
  background: var(--base-bg-main, #fff);
  color: var(--base-text-primary, #111);
  font-family: var(--base-font-family, inherit);
}
button.primary {
  background: var(--base-primary, #2563eb);
  border-radius: var(--base-radius-md, 6px);
}

Always provide a fallback value so the app still looks reasonable standalone or before the first host context arrives. The raw token map is also available as usePrivosContext().themeTokens for the rare case an app needs a value in JS (e.g. to theme a <canvas>).

License

MIT