@v8v/react-native
v0.1.0
Published
Voice agent framework for React Native & Expo — speech-to-intent with LOCAL, MCP, and webhook actions
Maintainers
Readme
@v8v/react-native
Voice agent framework for React Native & Expo — speech-to-intent with LOCAL, MCP, and webhook actions.
Turn speech into actions: register voice commands that trigger in-app callbacks, MCP tool calls (JSON-RPC 2.0), or remote webhooks (n8n, Zapier).
Architecture
Pure TypeScript reimplementation of the V8V voice agent framework. Zero dependency on the Kotlin Multiplatform core library — no Kotlin version conflicts.
┌─────────────────────────────────────────────────┐
│ TypeScript Layer │
│ │
│ VoiceAgent ─┬─ IntentResolver (regex + fuzzy) │
│ ├─ ActionRouter │
│ │ ├─ LocalHandler (in-app) │
│ │ ├─ McpActionHandler (JSON-RPC) │
│ │ └─ WebhookHandler (HTTP POST) │
│ └─ SpeechEngine adapter │
│ ├─ ExpoSpeechAdapter │
│ └─ RNVoiceAdapter │
├─────────────────────────────────────────────────┤
│ Native Layer (MCP Server only) │
│ Android: NanoHTTPD (pure Java) │
│ iOS: GCDWebServer (Swift) │
└─────────────────────────────────────────────────┘Table of Contents
- Installation
- Quick Start — Voice Agent
- MCP Server — Expose Tools from Your App
- MCP Client — Consume Tools from Another Server
- Webhook Actions
- API Reference
- Pattern Syntax
- Compatibility
- Publishing
Installation
npm install @v8v/react-nativeThe library ships TypeScript source — Metro handles compilation. No separate build step required.
Speech engine (pick one)
# Option A: Expo Speech Recognition (recommended for Expo apps)
npx expo install expo-speech-recognition
# Option B: React Native Voice (works with bare RN too)
npm install @react-native-voice/voiceNative rebuild
The MCP server uses a native HTTP module (NanoHTTPD on Android, GCDWebServer on iOS). After installing, rebuild your app:
# Expo
npx expo prebuild --clean
npx expo run:android # or run:ios
# Bare React Native
cd ios && pod install && cd ..
npx react-native run-android # or run-iosQuick Start — Voice Agent
Basic voice agent with React hook
import { useVoiceAgent, ExpoSpeechAdapter, AgentState } from '@v8v/react-native';
const engine = new ExpoSpeechAdapter();
function App() {
const { state, transcript, start, stop, registerAction } = useVoiceAgent({
engine,
config: { language: 'en', continuous: true },
});
useEffect(() => {
registerAction('todo.add', { en: ['add * to todo', 'add * to list'] }, (resolved) => {
console.log('Add to todo:', resolved.extractedText);
});
}, []);
return (
<Button
title={state === AgentState.IDLE ? 'Start' : 'Stop'}
onPress={state === AgentState.IDLE ? start : stop}
/>
);
}Using @react-native-voice/voice instead
import { useVoiceAgent, RNVoiceAdapter } from '@v8v/react-native';
const engine = new RNVoiceAdapter();
const { start, stop, registerAction } = useVoiceAgent({ engine });MCP Server — Expose Tools from Your App
Use useMcpServer to turn your React Native app into an MCP server. Other apps and AI agents can discover and call your tools via the standard MCP protocol (JSON-RPC 2.0 over HTTP).
1. Register tools and start the server
import { useMcpServer, mcpSuccess, mcpError } from '@v8v/react-native';
function App() {
const { url, isRunning, start, stop, registerTool, createDirectHandler } = useMcpServer({
name: 'my-app',
port: 3001, // optional, defaults to a random port
version: '1.0.0', // optional
});
useEffect(() => {
// Register a tool that external clients can call
registerTool('get_todos', 'Return all todos', async (args) => {
const todos = await db.getTodos();
return mcpSuccess(JSON.stringify(todos));
});
registerTool('create_todo', 'Create a new todo item', async (args) => {
const title = args.text ?? args.name;
if (!title) return mcpError('Missing title');
await db.createTodo(title);
return mcpSuccess(`Created: ${title}`);
});
// Start the HTTP server for external clients
start().then((serverUrl) => {
console.log(`MCP server running at ${serverUrl}`);
// e.g. http://127.0.0.1:3001/mcp
});
}, []);
return <Text>{isRunning ? `Server: ${url}` : 'Starting...'}</Text>;
}2. What external clients see
Any MCP-compatible client can now connect to your app. For example, using curl:
# Initialize the connection
curl -X POST http://127.0.0.1:3001/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
# List available tools
curl -X POST http://127.0.0.1:3001/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# Call a tool
curl -X POST http://127.0.0.1:3001/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"create_todo","arguments":{"text":"Buy groceries"}}}'3. Wire tools to voice commands (direct mode — no HTTP)
createDirectHandler calls the tool handler directly in JavaScript, skipping the HTTP round-trip. Use this when you want voice commands to trigger MCP tools within the same app:
import { useVoiceAgent, useMcpServer, ExpoSpeechAdapter, mcpSuccess } from '@v8v/react-native';
const engine = new ExpoSpeechAdapter();
function App() {
const { registerActionHandler, start: startVoice } = useVoiceAgent({ engine });
const { registerTool, createDirectHandler, start: startMcp } = useMcpServer({
name: 'my-app',
port: 3001,
});
useEffect(() => {
// 1. Register the tool
registerTool('create_todo', 'Create a new todo', async (args) => {
await db.createTodo(args.text);
return mcpSuccess(`Created: ${args.text}`);
});
// 2. Wire a voice pattern to the tool (no HTTP round-trip)
registerActionHandler(
'todo.create',
{ en: ['create task *', 'new task *', 'add task *'] },
createDirectHandler('create_todo'),
);
// 3. Optionally start the HTTP server for external clients too
startMcp().catch(() => {});
startVoice();
}, []);
}4. Server-only (no voice)
You can use McpServer directly without the voice agent or React hook:
import { McpServer, mcpSuccess } from '@v8v/react-native';
const server = new McpServer({ name: 'my-app', port: 3001 });
server.registerTool('ping', 'Health check', async () => mcpSuccess('pong'));
await server.start();
console.log(`Running at ${server.url}`);
// Later...
await server.stop();MCP Client — Consume Tools from Another Server
Use McpClient to call tools exposed by any MCP server (another app, a desktop agent, etc.).
1. Standalone client usage
import { McpClient } from '@v8v/react-native';
const client = new McpClient({
name: 'tasks-server',
url: 'http://127.0.0.1:3001/mcp',
});
// Initialize the connection
await client.initialize();
// Discover available tools
const tools = await client.listTools();
console.log('Available tools:', tools.map(t => t.name));
// Call a tool
const result = await client.callTool('create_todo', { text: 'Buy groceries' });
if (result.isError) {
console.error('Tool error:', result.content[0].text);
} else {
console.log('Success:', result.content[0].text);
}2. Wire a remote MCP tool to a voice command
McpActionHandler connects a voice pattern to a remote MCP tool:
import { useVoiceAgent, McpClient, McpActionHandler, ExpoSpeechAdapter } from '@v8v/react-native';
const engine = new ExpoSpeechAdapter();
const client = new McpClient({ name: 'tasks', url: 'http://127.0.0.1:3001/mcp' });
function App() {
const { registerActionHandler, start } = useVoiceAgent({ engine });
useEffect(() => {
// When the user says "create task buy groceries",
// it calls the create_todo tool on the remote server
// with { text: "buy groceries", rawText: "create task buy groceries", language: "en" }
registerActionHandler(
'task.create',
{ en: ['create task *', 'new task *'] },
new McpActionHandler(client, 'create_todo'),
);
// Custom argument key (default is 'text')
registerActionHandler(
'task.search',
{ en: ['search for *', 'find *'] },
new McpActionHandler(client, 'search_todos', 'query'),
);
start();
}, []);
}3. Two apps talking to each other
App A (server): Exposes get_battery_level tool via useMcpServer.
App B (client): Calls App A's tool via McpClient.
App B (voice) → "what's the battery level"
↓ McpActionHandler
↓ HTTP POST http://127.0.0.1:3001/mcp
App A (server) → runs get_battery_level handler → returns 85%
↓ JSON-RPC response
App B → "Battery level is 85%"Both apps run on the same device, communicating over 127.0.0.1.
Webhook Actions
Send voice-triggered HTTP POST requests to external services like n8n, Zapier, or your own API:
import { WebhookActionHandler } from '@v8v/react-native';
registerActionHandler(
'notify.team',
{ en: ['notify *', 'send notification *', 'alert *'] },
new WebhookActionHandler({ url: 'https://n8n.example.com/webhook/voice' }),
);The webhook receives a JSON payload:
{
"intent": "notify.team",
"extractedText": "project kickoff at 3 PM",
"rawText": "notify project kickoff at 3 PM",
"language": "en",
"timestamp": "2026-03-08T12:00:00.000Z"
}API Reference
Core Classes
| Class | Description |
|-------|-------------|
| VoiceAgent | Main orchestrator — wires speech, intent resolution, and action dispatch |
| IntentResolver | Regex pattern matching with * and {name} wildcards, plus Dice coefficient fuzzy matching |
| ActionRouter | Routes resolved intents to registered handlers by scope |
Speech Adapters
| Adapter | Library | Install |
|---------|---------|---------|
| ExpoSpeechAdapter | expo-speech-recognition | npx expo install expo-speech-recognition |
| RNVoiceAdapter | @react-native-voice/voice | npm install @react-native-voice/voice |
Action Handlers
| Handler | Scope | Transport |
|---------|-------|-----------|
| Local callback | LOCAL | In-app function call |
| McpActionHandler | MCP | JSON-RPC 2.0 over HTTP |
| WebhookActionHandler | REMOTE | HTTP POST |
MCP Classes
| Class | Description |
|-------|-------------|
| McpClient | JSON-RPC 2.0 client — initialize(), listTools(), callTool() |
| McpServer | Embedded HTTP server via native modules (NanoHTTPD / GCDWebServer) |
| McpRequestRouter | Pure JSON-RPC protocol handler (no HTTP — used internally and for direct mode) |
MCP Helpers
| Function | Description |
|----------|-------------|
| mcpSuccess(message) | Create a success McpToolResult with text content |
| mcpError(message) | Create an error McpToolResult with text content |
React Hooks
useVoiceAgent(options)
| Return | Type | Description |
|--------|------|-------------|
| state | AgentState | IDLE, LISTENING, or PROCESSING |
| transcript | string | Latest recognized speech |
| audioLevel | number | Current audio level (0–1) |
| lastResult | ActionResult \| null | Result of the last dispatched action |
| lastError | VoiceAgentError \| null | Last error |
| start() | () => void | Start listening |
| stop() | () => void | Stop listening |
| registerAction() | | Register a LOCAL action with patterns and callback |
| registerActionHandler() | | Register an action with a custom ActionHandler (MCP, webhook, etc.) |
| updateConfig() | | Update language, continuous mode, fuzzy threshold at runtime |
useMcpServer(options)
| Return | Type | Description |
|--------|------|-------------|
| url | string \| null | Server URL when running (e.g. http://127.0.0.1:3001/mcp) |
| isRunning | boolean | Whether the HTTP server is active |
| start() | () => Promise<string> | Start the native HTTP server |
| stop() | () => Promise<void> | Stop the server |
| registerTool() | | Register an MCP tool (name, description, handler) |
| createDirectHandler() | | Create an ActionHandler that calls a tool directly in JS (no HTTP) |
Pattern Syntax
"add * to todo" → extractedText = captured words
"remind me to {task}" → slots.task = captured words
"set {item} to {value}" → slots.item + slots.valueMulti-language patterns:
registerAction('greet', {
en: ['hello', 'hi', 'hey'],
hi: ['namaste', 'hello'],
es: ['hola', 'buenos dias'],
}, handler);Fuzzy matching kicks in when no exact pattern matches. Adjust the threshold (0–1) via config:
updateConfig({ fuzzyThreshold: 0.6 });Compatibility
| Platform | Supported | |----------|-----------| | Expo SDK 50+ | Yes | | React Native 0.73+ | Yes | | Android (API 24+) | Yes | | iOS 15+ | Yes |
Publishing
For library maintainers
1. Prerequisites
npm login # authenticate with npm registry
npm whoami # verify your identityIf publishing under the @v8v scope for the first time, ensure the npm org exists:
npm org ls v8v # check org members2. Pre-publish check
# Install dev dependencies (required for tsc)
npm install
# Type-check the source
npm run lint
# Preview what will be published
npm pack --dry-runThe files field in package.json ensures only these paths are included:
src/— TypeScript source (Metro compiles at build time)android/build.gradle+android/src/main/— Native Android module sourcesios/McpServerModule.swift+ios/V8VReactNative.podspec— Native iOS module sourcesexpo-module.config.json— Expo module registrationREADME.mdLICENSE
3. Publish
# First release (scoped packages are private by default)
npm publish --access public
# Subsequent releases
npm version patch # or minor / major
npm publish4. Verify
npm info @v8v/react-nativeFor consumers
After the package is published:
npm install @v8v/react-native
# Install a speech engine
npx expo install expo-speech-recognition
# Rebuild native code
npx expo prebuild --clean
npx expo run:androidLicense
MIT
