@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.
You need a development build or a bare React Native app — Expo Go can't load native code, so it can't run Appduct.
The app-side package plus a schema library:
npm install @appduct/react-native zodThe CLI, on the machine running the host:
npm install -g appductNo 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 sessions 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 sessions 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).
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.
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.
appduct auto-spawns its daemon. sessions link needs your app's deep-link scheme: pass --scheme (matching expo.scheme), or set scheme once in ~/.appduct/config.json:
appduct sessions link --scheme myapp --qrScan the QR (or open the link) in the app, then list and invoke tools:
appduct tools ls
appduct tools call 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 sessions ls).
| 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. |
| 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. |
registerEvent |
({ name, description, payloadSchema? }) → { remove() }. Lists an event you post, so appduct events ls and appduct_list_events show it before an agent waits on it. payloadSchema is a Standard Schema or raw JSON Schema, as for a tool; see Declare the events you post. |
postEvent |
(name, payload?) — pushes an app event, read by appduct events tail 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 | Support |
|---|---|
| iOS | 15.1+ (Appduct.podspec), New Architecture |
| Android | Autolinked, New Architecture |
| Web | Stub only |
- 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.
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 hello@callstack.com 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! 🔥