Skip to content

Getting started ​

Toolmark lets an AI agent operate your web app through tools you declare in your UI code. This page wires a React app with a form tool and an in-app agent; the concepts behind each step are in Concepts.

Requirements: Node ≥ 22.12 for tooling, React ≥ 18.3 for @toolmark/react, and a schema library that implements Standard Schema v1 (zod 4 is used below).

sh
pnpm add @toolmark/core @toolmark/react react-hook-form zod
PackageUse it for
@toolmark/coreRegistry, policy, confirmation, form tools, bridge, DOM, WebMCP, OTel
@toolmark/reactProvider, scopes and hooks; @toolmark/react/rhf for react-hook-form
@toolmark/inertiaInertia useForm, <Form>, server-declared tools and navigation
@toolmark/tourGuided tours, overlay and useTour
@toolmark/mcptoolmark-mcp: pair the page with a desktop MCP client
@toolmark/testingcreateTestToolmark (Vitest) and the Playwright fixture
@toolmark/linttoolmark lint over page manifests (dev)
@toolmark/judge-typesafeOptional lint judge (dev)

1. Create the registry ​

tsx
// main.tsx
import { createRoot } from 'react-dom/client'
import { createToolmark } from '@toolmark/core'
import { ToolmarkProvider } from '@toolmark/react'
import { App } from './app'

const tm = createToolmark({ dev: import.meta.env.DEV })

createRoot(document.getElementById('root')!).render(
  <ToolmarkProvider toolmark={tm}>
    <App />
  </ToolmarkProvider>,
)

dev: true turns misconfiguration into thrown errors; production reports it as error events.

2. Declare a form tool ​

tsx
// create-challenge.tsx
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { ToolScope, useFormTool } from '@toolmark/react'
import { rhfAdapter } from '@toolmark/react/rhf'

const schema = z.object({
  title: z.string().min(1).describe('Challenge title'),
  startsAt: z.iso.date().describe('Start date, YYYY-MM-DD'),
})
type Challenge = z.infer<typeof schema>

function CreateChallengeForm() {
  const form = useForm<Challenge>({ defaultValues: { title: '', startsAt: '' } })
  const save = async (values: Challenge) => {
    await fetch('/api/challenges', { method: 'POST', body: JSON.stringify(values) })
  }
  useFormTool(rhfAdapter(form, { onSubmit: save }), {
    name: 'create',
    description: 'The new-challenge form. Fill it, then submit to create the challenge.',
    input: schema,
    submitSummary: (v) => `Create challenge "${v.title}"`,
  })
  return (
    <form onSubmit={form.handleSubmit(save)}>
      <input {...form.register('title')} />
      <input type="date" {...form.register('startsAt')} />
      <button>Create</button>
    </form>
  )
}

export function App() {
  return (
    <ToolScope name="challenges">
      <CreateChallengeForm />
    </ToolScope>
  )
}

The page now offers challenges.create.fill (writes values, skipping fields the user already typed) and challenges.create.submit (consequential: it needs the user's confirmation).

3. Connect an agent ​

The in-app agent talks to the page over bridge protocol v1. The transport depends on your backend: Laravel Echo private channels (@toolmark/core/bridge/echo), a WebSocket (…/websocket), postMessage (…/post-message) or an in-page channel for a client-side agent (…/in-page):

ts
import { bridge } from '@toolmark/core/bridge'
import { websocketTransport } from '@toolmark/core/bridge/websocket'

tm.use(bridge({ transport: websocketTransport({ url: 'wss://example.com/agent' }) }))

The bridge sends the page's manifest on attach and on every change, runs call/describe messages as caller inapp, and returns results. On your server, expose two LLM tools, page_call(tool, input) and page_describe(tool) (spec §12.3); the Laravel reference is a complete server implementation to copy.

Consequential tools called by the in-app agent return needs_confirmation; render those with usePendingConfirmations() (see Confirmation modes).

4. Test it ​

ts
import { createTestToolmark } from '@toolmark/testing/vitest'

const tm = createTestToolmark()
// register tools, then:
const r = await tm.call(
  'challenges.create.fill',
  { values: { title: 'Robotics' } },
  { caller: 'test' },
)

Playwright specs use the @toolmark/testing fixture against the page's test hook. Run toolmark lint against your pages in CI.

Next steps ​