Overview

The @mcp-b/mcp-react-hook-form package bridges React Hook Form with the Model Context Protocol (MCP), transforming traditional forms into AI-callable tools. With just one line of code, your existing forms become discoverable and usable by AI agents while maintaining all their current functionality.

Installation

npm install @mcp-b/mcp-react-hook-form react-hook-form zod @hookform/resolvers

Key Features

  • One-line integration - Add MCP capabilities to existing React Hook Form setups
  • Headless design - No UI components, works with any form styling
  • Type-safe - Full TypeScript support with Zod schema inference
  • Shared validation - Same validation logic for both users and AI agents
  • Minimal overhead - Lightweight bridge between form and MCP server

Integration Approaches

The simplest approach for adding MCP capabilities to a form:
import { useMcpToolFormDirect } from '@mcp-b/mcp-react-hook-form';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const contactSchema = z.object({
  email: z.string().email('Invalid email address'),
  subject: z.string().min(5, 'Subject too short'),
  message: z.string().min(10, 'Message too short')
});

type ContactFormData = z.infer<typeof contactSchema>;

function ContactForm({ mcpServer }) {
  const form = useForm<ContactFormData>({
    resolver: zodResolver(contactSchema)
  });

  // One-line MCP enablement
  useMcpToolFormDirect(mcpServer, "contactForm", form, contactSchema, {
    title: "Contact Form",
    description: "Send a contact message to support"
  });

  const onSubmit = (data: ContactFormData) => {
    console.log('Form submitted:', data);
    // Handle form submission
  };

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <input {...form.register("email")} placeholder="Email" />
      <input {...form.register("subject")} placeholder="Subject" />
      <textarea {...form.register("message")} placeholder="Message" />
      <button type="submit">Send</button>
    </form>
  );
}

2. Context Provider Approach

For apps with multiple forms, use the context provider:
import { McpServerProvider, useMcpToolForm } from '@mcp-b/mcp-react-hook-form';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';

// Create MCP server
const mcpServer = new McpServer({
  name: 'form-server',
  version: '1.0.0'
});

// Wrap your app
function App() {
  return (
    <McpServerProvider server={mcpServer}>
      <ContactForm />
      <FeedbackForm />
      <SettingsForm />
    </McpServerProvider>
  );
}

// Use in any child component
function ContactForm() {
  const form = useForm<ContactFormData>({
    resolver: zodResolver(contactSchema)
  });

  // Register form with MCP
  useMcpToolForm("contactForm", form, contactSchema, {
    title: "Contact Form",
    description: "Send a message to support"
  });

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      {/* Form fields */}
    </form>
  );
}

3. Imperative API

For programmatic control and custom handling:
import { registerFormAsMcpTool } from '@mcp-b/mcp-react-hook-form';

function AdvancedForm() {
  const form = useForm<FormData>({
    resolver: zodResolver(schema)
  });

  useEffect(() => {
    const cleanup = registerFormAsMcpTool(mcpServer, "advancedForm", form, schema, {
      title: "Advanced Form",
      description: "Complex form with custom handling",
      
      // Custom handler for AI submissions
      onToolCall: async (data) => {
        console.log("AI submitted form with:", data);
        
        // Custom validation or preprocessing
        if (!validateBusinessRules(data)) {
          throw new Error("Business rules validation failed");
        }
        
        // Submit to API
        const result = await submitToAPI(data);
        
        // Return response to AI
        return {
          content: [{
            type: 'text',
            text: `Form submitted successfully. ID: ${result.id}`
          }]
        };
      }
    });

    return cleanup; // Unregister on unmount
  }, [mcpServer, form]);

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      {/* Form fields */}
    </form>
  );
}

Real-World Examples

Multi-Step Form with AI Support

import { useMcpToolFormDirect } from '@mcp-b/mcp-react-hook-form';
import { useState } from 'react';

const registrationSchema = z.object({
  // Step 1
  email: z.string().email(),
  password: z.string().min(8),
  
  // Step 2
  firstName: z.string().min(2),
  lastName: z.string().min(2),
  
  // Step 3
  company: z.string().optional(),
  role: z.enum(['developer', 'designer', 'manager', 'other'])
});

function RegistrationForm({ mcpServer }) {
  const [step, setStep] = useState(1);
  const form = useForm({
    resolver: zodResolver(registrationSchema),
    mode: 'onChange'
  });

  // Make form AI-callable
  useMcpToolFormDirect(mcpServer, "registration", form, registrationSchema, {
    title: "User Registration",
    description: "Register a new user account",
    onToolCall: async (data) => {
      // AI can fill entire form at once
      await createUser(data);
      return {
        content: [{
          type: 'text',
          text: `User ${data.email} registered successfully`
        }]
      };
    }
  });

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      {step === 1 && (
        <>
          <input {...form.register("email")} placeholder="Email" />
          <input {...form.register("password")} type="password" />
        </>
      )}
      
      {step === 2 && (
        <>
          <input {...form.register("firstName")} placeholder="First Name" />
          <input {...form.register("lastName")} placeholder="Last Name" />
        </>
      )}
      
      {step === 3 && (
        <>
          <input {...form.register("company")} placeholder="Company (optional)" />
          <select {...form.register("role")}>
            <option value="developer">Developer</option>
            <option value="designer">Designer</option>
            <option value="manager">Manager</option>
            <option value="other">Other</option>
          </select>
        </>
      )}
      
      <button type="button" onClick={() => setStep(s => s - 1)} disabled={step === 1}>
        Previous
      </button>
      <button type="button" onClick={() => setStep(s => s + 1)} disabled={step === 3}>
        Next
      </button>
      {step === 3 && <button type="submit">Register</button>}
    </form>
  );
}

Dynamic Form with Field Dependencies

const dynamicSchema = z.object({
  productType: z.enum(['physical', 'digital', 'service']),
  productName: z.string().min(3),
  price: z.number().positive(),
  
  // Only for physical products
  weight: z.number().optional(),
  dimensions: z.string().optional(),
  
  // Only for digital products
  fileSize: z.number().optional(),
  downloadUrl: z.string().url().optional(),
  
  // Only for services
  duration: z.number().optional(),
  availability: z.string().optional()
}).refine((data) => {
  if (data.productType === 'physical') {
    return data.weight && data.dimensions;
  }
  if (data.productType === 'digital') {
    return data.fileSize && data.downloadUrl;
  }
  if (data.productType === 'service') {
    return data.duration && data.availability;
  }
  return true;
}, {
  message: "Required fields missing for product type"
});

function ProductForm({ mcpServer }) {
  const form = useForm({
    resolver: zodResolver(dynamicSchema)
  });
  
  const productType = form.watch('productType');

  useMcpToolFormDirect(mcpServer, "productForm", form, dynamicSchema, {
    title: "Product Creation",
    description: "Create a new product listing"
  });

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <select {...form.register("productType")}>
        <option value="physical">Physical</option>
        <option value="digital">Digital</option>
        <option value="service">Service</option>
      </select>
      
      <input {...form.register("productName")} placeholder="Product Name" />
      <input {...form.register("price", { valueAsNumber: true })} type="number" />
      
      {productType === 'physical' && (
        <>
          <input {...form.register("weight", { valueAsNumber: true })} type="number" placeholder="Weight (kg)" />
          <input {...form.register("dimensions")} placeholder="Dimensions (LxWxH)" />
        </>
      )}
      
      {productType === 'digital' && (
        <>
          <input {...form.register("fileSize", { valueAsNumber: true })} type="number" placeholder="File Size (MB)" />
          <input {...form.register("downloadUrl")} placeholder="Download URL" />
        </>
      )}
      
      {productType === 'service' && (
        <>
          <input {...form.register("duration", { valueAsNumber: true })} type="number" placeholder="Duration (hours)" />
          <input {...form.register("availability")} placeholder="Availability" />
        </>
      )}
      
      <button type="submit">Create Product</button>
    </form>
  );
}

Hook Options

useMcpToolFormDirect Options

interface UseMcpToolFormOptions {
  title?: string;              // Tool display name
  description?: string;        // Tool description for AI
  onToolCall?: (data: any) => Promise<any>; // Custom handler
  validateBeforeSubmit?: boolean; // Validate before AI submission
  debounceMs?: number;        // Debounce registration updates
}

registerFormAsMcpTool Options

interface RegisterFormOptions {
  title?: string;
  description?: string;
  onToolCall?: (data: any) => Promise<any>;
  onError?: (error: Error) => void;
  transformInput?: (data: any) => any;  // Transform AI input
  transformOutput?: (data: any) => any; // Transform response
}

TypeScript Support

Full TypeScript support with automatic type inference:
import type { z } from 'zod';

// Schema defines both form and MCP tool types
const schema = z.object({
  name: z.string(),
  age: z.number(),
  email: z.string().email()
});

// Types are automatically inferred
type FormData = z.infer<typeof schema>;

function TypedForm({ mcpServer }) {
  const form = useForm<FormData>({
    resolver: zodResolver(schema)
  });

  // Types flow through to MCP tool
  useMcpToolFormDirect(mcpServer, "typedForm", form, schema);
  
  return (
    <form onSubmit={form.handleSubmit((data: FormData) => {
      // data is fully typed
    })}>
      {/* Form fields */}
    </form>
  );
}

AI Agent Usage

Once registered, AI agents can discover and use your forms:
// AI agent code
const tools = await mcpClient.listTools();
// Returns: [{ name: "contactForm", description: "Send a contact message", ... }]

// AI can submit the form
const result = await mcpClient.callTool({
  name: "contactForm",
  arguments: {
    email: "[email protected]",
    subject: "Question about pricing",
    message: "I'd like to know more about your enterprise plans."
  }
});

Testing

import { render, screen } from '@testing-library/react';
import { MockMcpServer } from '@mcp-b/mcp-react-hook-form/testing';

describe('ContactForm', () => {
  it('registers form as MCP tool', async () => {
    const mockServer = new MockMcpServer();
    
    render(<ContactForm mcpServer={mockServer} />);
    
    const tools = mockServer.getRegisteredTools();
    expect(tools).toContainEqual(
      expect.objectContaining({
        name: 'contactForm',
        description: 'Send a contact message'
      })
    );
  });
  
  it('handles AI form submission', async () => {
    const mockServer = new MockMcpServer();
    
    render(<ContactForm mcpServer={mockServer} />);
    
    const result = await mockServer.callTool('contactForm', {
      email: '[email protected]',
      subject: 'Test',
      message: 'Test message'
    });
    
    expect(result.content[0].text).toContain('success');
  });
});

Best Practices

  1. Use descriptive tool names - Help AI agents understand what each form does
  2. Provide clear descriptions - Include expected input format and purpose
  3. Validate consistently - Use the same Zod schema for both UI and AI validation
  4. Handle errors gracefully - Provide meaningful error messages for AI agents
  5. Test AI interactions - Ensure forms work correctly when called programmatically

Resources