@appduct/react-native
v0.11.1
Published
Expo native client for Appduct sessions
Maintainers
Readme
Drive app tools without shipping debug UI
@appduct/react-native is the app-side client for Appduct. Your app registers tools in JavaScript; a CLI, MCP client, or test suite invokes them once the app opens a bootstrap link and completes a pinned wss:// handshake — no debug menu required.
Requirements
You need a development build or a bare React Native app — Expo Go can't load native code, so it can't run Appduct.
Getting started
1. Install
The app-side package plus a schema library:
npm install @appduct/react-native zodThe CLI, on the machine running the host:
npm install -g appduct2. Nothing to configure yet
No key, no pins, and no config plugin are needed for a first run, in any build type. The daemon auto-generates a key on first start, and appduct link carries its sha256/... fingerprint on the deep link for the app to trust for that session.
Wire your deep-link scheme so the OS can open the app with that link. For an Expo app that's all: appduct link reads expo.scheme straight out of app.json. Otherwise (a dynamic app.config.js, which Appduct never executes, or bare React Native) name it with appduct init --scheme <s>, --scheme, or APPDUCT_SCHEME — the CLI README has the full resolution order. To make a build trust only pins you embedded ahead of time, see Configuring trust.
By default the native module ships in debug builds only: a release build has none, so the API is inert and connect() rejects with appduct_disabled (see Build variants).
3. Import Appduct in the JS entry point
import "@appduct/react-native/auto";/auto is the only entry that installs anything: the deep-link bootstrap listener and session recovery. To control when it installs — in __DEV__, behind a QA toggle — require() it there instead:
if (__DEV__) {
require("@appduct/react-native/auto");
}The default flow needs no Linking handler of your own, and sessions survive Metro reloads and network flaps — see ARCHITECTURE.md §11 for lease, resume, and reconnect rules.
If you drive bootstrap yourself and never import /auto, call restoreSession() before your own bootstrap handling — it's then the only reader of the native resume lease.
4. Define tools in app startup code
Call registerTool({ ... }) with inputSchema/outputSchema values and a handler. Zod v4 works out of the box — its JSON Schema exporter is what lets agents see a real tool shape. A library without one (zod 3, plain valibot) needs a { schema, jsonSchema } pair, and a raw JSON Schema object works with no validation library at all — see Accepted schema forms.
useAppductTool wraps registerTool in a useEffect, so registration follows the component's lifecycle, remounts and Fast Refresh included:
import "@appduct/react-native/auto";
import { useAppductTool } from "@appduct/react-native";
import { z } from "zod";
export function AppductBootstrap() {
useAppductTool(
{
name: "sum",
description: "Add two numeric values",
inputSchema: z.object({ a: z.number(), b: z.number() }),
outputSchema: z.object({ total: z.number() }),
handler: async ({ a, b }) => ({ total: a + b }),
},
[]
);
return null;
}Mount it near app startup, or register from a module that loads then. The host can only invoke tools your app already registered.
The hook registers once per mount and re-registers only when the registration itself changes, routing every call through the latest render's handler — so deps is an optional override, not something each call site has to get right. See Registration is per mount, not per render.
Make inputSchema accept an object: a call's arguments are always a JSON object — see Make the input schema accept an object. A call gets 10 seconds unless the registration declares timeoutMs — see Long-running tools.
To keep a destructive tool out of some build variants, pass { enabled } rather than wrapping the hook in an if — see Gating a tool by build variant.
An agent picks a tool from one signature line and the first line of its description, so name tools by intent, set annotations (readOnlyHint, destructiveHint), declare an outputSchema, and describe each parameter — see Designing tools for agents.
5. Start the daemon and test the flow
appduct auto-spawns its daemon. link needs your app's deep-link scheme: pass --scheme (matching expo.scheme), or set scheme once in ~/.appduct/config.json:
appduct link --scheme myapp --qrScan the QR (or open the link) in the app, then list and invoke tools:
appduct tools
appduct invoke sum --input '{"a":2,"b":3}'Omit the session selector when only one session is active; pass an alias or session id when several are (appduct ls).
API reference
Entry points
| Entry | Behavior |
| --- | --- |
| @appduct/react-native | Side-effect-free. The native module is looked up lazily, on the first native call, so importing it (even in Expo Go) never crashes. |
| @appduct/react-native/auto | Same exports plus one side effect: installs the deep-link bootstrap listener and starts lease recovery — the only entry that installs anything. |
| @appduct/react-native/noop | Same public API, fully inert — for compiling Appduct out of production builds. |
| @appduct/react-native/metro | withAppduct(config, { include }) — swaps the real entries for /noop at bundle time. |
Exports
| Export | Signature / notes |
| --- | --- |
| registerTool | ({ name, description, inputSchema?, outputSchema?, annotations?, timeoutMs?, group?, handler }) → { remove() }. The disposer removes only its own registration. group ("cart", or a subgroup like "checkout/payment") lets agents list your tools one area at a time — see Group tools in a large app. |
| createToolGroup | (group) → a registerTool that puts every tool it registers in group. |
| useAppductTool | (definition, deps?, { enabled? }). Registers once per mount, re-registering only when the descriptor changes; deps overrides that derivation. enabled defaults to true; false never registers, and removes any registration that hook owns. |
| handler | (args, context). context.signal is an AbortSignal, aborted when the caller cancels or the connection drops mid-call. Forward it (fetch(url, { signal })), check signal.aborted, or listen for "abort" — ignoring it is fine, the handler replies normally. |
| postEvent | (name, payload?) — pushes an app event, read by appduct events and the MCP event tools. |
| addAppductListener | (kind, callback) → { remove() }. Kinds "stateChange", "sessionChange", "error" — the last one is a unified channel for bootstrap-parse, connect, socket, and tool-handler failures. |
| getRegisteredTools | → ToolDescriptor[], the current registry. |
| getAppductState | → the client's connection state ("idle" with no session). |
| restoreSession | → Promise<boolean>. Recovers the native resume lease; also on appductClient. |
| connect | (input) → Promise<void>. Claims a parsed bootstrap payload; rejects with AppductDisabledError (code: "appduct_disabled") when native is absent. |
| parseBootstrapUrl | Parses a v2 bootstrap deep link (and its sibling pin param) for connect. |
| getAppductBuildConfig | → { trust, hasEmbeddedPins, allowPrivateLanOnly }, this build's effective trust configuration. |
Platform compatibility
| Platform | Support |
| --- | --- |
| iOS | 15.1+ (Appduct.podspec), New Architecture |
| Android | Autolinked, New Architecture |
| Web | Stub only |
Going further
- Trust modes and Configuring trust — pins, plugin options, bare-RN native keys.
- Registering tools — schema forms, what re-registers, tool groups, input schemas that accept an object,
timeoutMs, and designing tools for agents. - Gating a tool by build variant —
enabled, and why__DEV__is wrong here. - Build variants —
APPDUCT_ENABLED, autolinking exclusion, compiling Appduct out of production builds. - What a build without the native module does.
- ARCHITECTURE.md §11 — resume lease, reconnect, cancellation.
appductCLI and MCP server.
Made with ❤️ at Callstack
appduct is an open source project and will always remain free to use. If you think it's cool, please star it 🌟. Callstack is a group of React and React Native geeks, contact us at [email protected] if you need any help with these or just want to say hi!
Like the project? ⚛️ Join the team who does amazing stuff for clients and drives React Native Open Source! 🔥
