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

@v8v/react-native

v0.1.0

Published

Voice agent framework for React Native & Expo — speech-to-intent with LOCAL, MCP, and webhook actions

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

npm install @v8v/react-native

The 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/voice

Native 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-ios

Quick 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.value

Multi-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 identity

If publishing under the @v8v scope for the first time, ensure the npm org exists:

npm org ls v8v     # check org members

2. 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-run

The 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 sources
  • ios/McpServerModule.swift + ios/V8VReactNative.podspec — Native iOS module sources
  • expo-module.config.json — Expo module registration
  • README.md
  • LICENSE

3. Publish

# First release (scoped packages are private by default)
npm publish --access public

# Subsequent releases
npm version patch   # or minor / major
npm publish

4. Verify

npm info @v8v/react-native

For 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:android

License

MIT