SuperCmdK is one typed action system for people and agents. Register WebMCP-shaped tools at the app or route level, expose them to browser agents through document.modelContext, and invoke the same handlers from React, voice clients, embedded agents, accessibility controls, and automation adapters through one policy and validation layer.
The React package includes:
- global and route-scoped command registration;
- a React-free, WebMCP-aligned tool registry with JSON Schema validation;
- a built-in WebMCP bridge for browser agents;
- a model-independent Agent API;
- a Cactus Needle adapter that runs inference in a Web Worker.
The optional @supercmdk/palette package supplies the cmdk-based palette, and @supercmdk/needle supplies the pinned model and WASM files.
bun add @supercmdk/reactYour app must provide React and React DOM 18 or 19. SuperCmdK uses ESM. The React package does not install a command-palette implementation.
Install the React library, palette, and optional package that contains the pinned Needle model and WASM runtime:
bun add @supercmdk/react @supercmdk/palette @supercmdk/needleThe Needle package is about 14 MB on disk. It keeps the model out of @supercmdk/react and out of your initial JavaScript bundle. Your bundler emits the model as a separate asset, and SuperCmdK fetches it during browser idle time.
import { createNeedleEngine } from "@supercmdk/needle";
import { CommandPalette } from "@supercmdk/palette";
import { SuperCmdKProvider, type Tool } from "@supercmdk/react";
import "@supercmdk/palette/styles.css";
const tools: Tool[] = [
{
name: "create_task",
title: "Create task",
description: "Create a task in the current project",
inputSchema: {
type: "object",
properties: {
title: { type: "string" },
},
required: ["title"],
additionalProperties: false,
},
execute: ({ title }) => tasks.create({ title: String(title) }),
},
];
export function App() {
return (
<SuperCmdKProvider
tools={tools}
agent={{ engine: createNeedleEngine }}
>
<YourRoutes />
<CommandPalette
onAgentResult={(result) => console.log(result)}
onError={(error) => console.error(error)}
/>
</SuperCmdKProvider>
);
}createNeedleEngine supplies package-relative URLs for needle.js, needle.wasm, and needle2.cact. Vite, webpack, and other bundlers that support new URL(..., import.meta.url) copy those assets into the build automatically. Configure your host to serve .wasm as application/wasm. The Worker fetches the full model, so byte-range support is not required.
SuperCmdK loads and compiles Needle in a Web Worker during browser idle time. Set agent={{ engine: createNeedleEngine, preload: false }} to defer that work until the first Agent request.
The companion package includes unmodified Needle artifacts from a checksum-verified revision. Its adapter is MIT; the bundled model and WASM artifacts are Apache-2.0. The package includes the upstream license and revision metadata.
Install the optional palette package, then mount the provider and palette near your app root. Commands on the provider remain available across routes.
bun add @supercmdk/paletteimport { CommandPalette } from "@supercmdk/palette";
import { SuperCmdKProvider } from "@supercmdk/react";
import "@supercmdk/palette/styles.css";
export function App() {
return (
<SuperCmdKProvider
commands={[
{
id: "home",
label: "Go home",
group: "Navigation",
keywords: ["dashboard"],
shortcut: ["⌘", "H"],
run: () => location.assign("/"),
},
]}
>
<Routes />
<CommandPalette />
</SuperCmdKProvider>
);
}When mounted, CommandPalette listens for Cmd+K on macOS or Ctrl+K on other platforms. A tools-only SuperCmdKProvider installs no keyboard listener. Pass hotkey={false} or a custom predicate to CommandPalette, and use open and onOpenChange on the provider when your application owns the palette state.
useCommandChoice and useCommandChoices register commands for the lifetime of a component. Pass dependencies as you would to useEffect.
import { useCommandChoice, useCommandChoices } from "@supercmdk/react";
function CustomerPage({ customerId }: { customerId: string }) {
useCommandChoice(
{
id: "archive-customer",
label: "Archive this customer",
group: "Customer",
run: async ({ close }) => {
await archiveCustomer(customerId);
close();
},
closeOnSelect: false,
},
[customerId],
);
useCommandChoices(
[{
id: "copy-id",
label: "Copy customer ID",
run: () => navigator.clipboard.writeText(customerId),
}],
[customerId],
);
return <CustomerDetails id={customerId} />;
}A route command overrides a provider command with the same id. SuperCmdK restores the provider command when the component unmounts.
Build a custom menu with the Command, flat Command* primitives, defaultFilter, and useCommandState APIs re-exported by @supercmdk/palette.
Tools do not depend on the palette or an Agent. Their names, descriptions, input schemas, and annotations follow the WebMCP imperative API. Register app-wide tools on SuperCmdKProvider. Register route tools with useTool or useTools.
import { useTools, type Tool } from "@supercmdk/react";
const messagingTools: Tool[] = [
{
name: "find_contact",
title: "Find contact",
description: "Find a contact by name and return the matching contact's ID, name, and address.",
annotations: {
readOnlyHint: true,
},
inputSchema: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
additionalProperties: false,
},
execute: ({ name }, { signal, source }) =>
contacts.findByName(String(name), { signal, source }),
},
];
function MessagingPage() {
useTools(messagingTools, []);
return <Inbox />;
}A route tool overrides an app-wide tool with the same name. SuperCmdK restores the app-wide tool on unmount.
Write descriptions that tell an agent both when to use a tool and what a successful call returns. Set annotations.readOnlyHint explicitly: use true only when the handler cannot modify application or external state, and false for tools that may write. The WebMCP bridge conservatively emits false when the hint is omitted.
Tip
Run auditToolQuality(tools) in a test or build check. It reports missing titles and read-only hints, open argument schemas, undocumented properties, and contradictory safety annotations. The registry still accepts intentional exceptions, so the audit returns structured issues instead of throwing.
The outermost provider exposes every active tool through the browser's document.modelContext registry by default:
<SuperCmdKProvider tools={globalTools}>
<Routes />
</SuperCmdKProvider>Provider tools register globally. Tools added with useTool or useTools register when their component mounts and unregister when it unmounts. Scoped overrides replace the corresponding WebMCP tool and restore the previous definition during cleanup. Nested providers stay disconnected by default so one document has one WebMCP owner; use hooks beneath the root provider for ordinary route and component scopes.
Disable the bridge explicitly, or pass options to configure it:
<SuperCmdKProvider tools={tools} webMcp={false}>
<App />
</SuperCmdKProvider>
<SuperCmdKProvider
tools={tools}
webMcp={{ onError: (error, tool) => reportError(tool.name, error) }}
>
<App />
</SuperCmdKProvider>WebMCP is still experimental. The bridge uses feature detection and becomes a safe no-op when the browser does not provide document.modelContext; the palette, embedded Agent, voice, and direct invocation paths continue to work.
The WebMCP bridge is part of the React-free tools/core entrypoint. Connect it directly when another framework owns the registry lifecycle:
import { createToolRegistry } from "@supercmdk/react/tools";
import {
connectToolRegistryToWebMcp,
supportsWebMcp,
} from "@supercmdk/react/tools";
const registry = createToolRegistry({ tools: globalTools });
const bridge = connectToolRegistryToWebMcp(registry, {
onError: (error, tool) => console.error(tool.name, error),
});
console.log({ supported: supportsWebMcp() });
window.addEventListener("pagehide", () => bridge.dispose(), { once: true });WebMCP calls enter the same registry as every other caller with context.source === "webmcp". Arguments are validated and policy is enforced before the handler runs. Successful results pass through unchanged so the browser can serialize them according to the WebMCP specification.
The current WebMCP draft discards the reason when a tool's promise rejects. To keep failures actionable, the bridge resolves registry failures as an MCP-shaped { isError: true, content, structuredContent } result. content gives compatible agents an explicit text failure, while structuredContent includes the stable code, invocation ID, and compact JSON Schema validation issues. Tool handlers should still throw concise, agent-safe error messages; direct registry callers continue to receive the usual ToolInvocationResult failure.
generateLlmsTxt is an optional, React-free build utility. Supply truthful site metadata, stable documentation links, agent access details, and the durable tools that should be documented:
import { generateLlmsTxt } from "@supercmdk/react/tools";
const llmsTxt = generateLlmsTxt({
name: "Acme",
description: "One workspace for customer operations.",
siteUrl: "https://acme.example",
purpose: "Manage customer accounts, projects, and tasks.",
audience: "Acme customers and the agents acting on their behalf.",
publisher: {
name: "Acme, Inc.",
url: "https://acme.example/about",
},
agentAccess: {
webMcp: true,
mcpUrl: "https://mcp.acme.example",
description: "Account changes require authentication and application policy approval.",
},
tools: documentedTools,
documentation: [
{ title: "Getting started", url: "https://acme.example/docs" },
{ title: "Authentication", url: "https://acme.example/docs/auth" },
],
});
await Bun.write("dist/llms.txt", llmsTxt);The function returns a string and performs no file or browser operations, so Vite, Next.js, another framework, or a standalone build script can decide where to emit it. Tool entries are generated from ToolSchema; regular Tool objects also work because handlers are ignored. Pass only stable tool definitions—route-, permission-, and state-dependent runtime snapshots do not belong in static documentation.
useSuperCmdK exposes the current tool snapshot and the shared invocation path:
const { tools, invokeTool } = useSuperCmdK();
const result = await invokeTool(
"find_contact",
{ name: "Ada" },
{
source: "voice",
metadata: { transcript: "Find Ada" },
},
);SuperCmdK validates arguments against each tool's JSON Schema without coercion. Invalid arguments do not reach the handler.
Set one provider policy for Agent, voice, and application calls:
<SuperCmdKProvider
tools={globalTools}
toolPolicy={{
authorize: ({ tool, context }) =>
permissions.canUse(tool.name, context.source),
confirm: ({ tool }) => window.confirm(`Allow ${tool.name}?`),
}}
>
<App />
</SuperCmdKProvider>Tools use WebMCP's readOnlyHint, untrustedContentHint, and consequentialHint annotations. SuperCmdK requires confirmation for a tool marked consequentialHint; when toolPolicy.confirm is absent, the call is rejected safely. Your callback owns the approval UI, so it can use a browser prompt, an application modal, or a server-side approval flow.
The registry controls which handlers clients can call. It does not sandbox handler code. A handler can use the same browser credentials and capabilities as the rest of your application, so authorization must still be enforced at the server boundary.
Use onToolInvocation to show agent activity in the interface without copying execution logic into each handler:
<SuperCmdKProvider
tools={tools}
onToolInvocation={(event) => {
activity.record({
invocationId: event.invocationId,
tool: event.tool.name,
phase: event.phase,
source: event.source,
});
}}
>
<App />
</SuperCmdKProvider>Each known tool call emits started, then one of succeeded, failed, denied, or aborted. Events include the tool, source, and failure details when applicable. They omit arguments and return values so a global activity listener does not become an accidental data log. A listener error does not change the tool result.
React-free consumers can subscribe to the same events:
const unsubscribe = registry.subscribeInvocations((event) => {
activity.record(event);
});Provider listeners also receive calls made by the embedded Agent. Pass onToolInvocation to runAgentChain when using the lower-level Agent API directly.
Import the React-free registry from @supercmdk/react/tools:
import { createToolRegistry } from "@supercmdk/react/tools";
const registry = createToolRegistry({ tools: globalTools });
const unsubscribe = registry.subscribe(() => {
voice.setTools(registry.getSnapshot());
});
const result = await registry.invokeTool(
"find_contact",
{ name: "Ada" },
{ source: "voice" },
);Pass the registry to <SuperCmdKProvider toolRegistry={registry}> when a voice client, Worker, or automation adapter needs the route-scoped tools that React components register. The /tools entry point imports neither React, cmdk, nor Needle.
The AgentEngine interface separates tool orchestration from model inference. @supercmdk/needle supplies the bundled Cactus Needle adapter; you can instead provide any engine that implements the interface.
import { createNeedleEngine } from "@supercmdk/needle";
import { CommandPalette } from "@supercmdk/palette";
import { SuperCmdKProvider } from "@supercmdk/react";
<SuperCmdKProvider
agent={{
engine: createNeedleEngine,
systemPrompt: () =>
`date: ${new Date().toISOString()}; locale: en-US`,
}}
>
<App />
<CommandPalette
agentRunOptions={{ maxSteps: 8, confidenceThreshold: 0.75 }}
onAgentResult={(result) => console.log(result)}
/>
</SuperCmdKProvider>SuperCmdK schedules engine preload after page load during browser idle time. NeedleWasmEngine downloads and compiles Needle inside needle.worker.js, away from React's thread. A prompt submitted during preload waits for the same request.
Set preload: false to load the engine on demand:
<SuperCmdKProvider agent={{ engine: createEngine, preload: false }}>
<App />
</SuperCmdKProvider>Call preloadAgent() from useSuperCmdK when user intent gives you a better preload signal.
The Agent receives the current tool schemas and invokes each handler through the tool registry. It can feed one result into the next call. For example, a request to “find Ada and tell her hello” can call find_contact, then pass the returned contact ID to send_message.
import { useTools } from "@supercmdk/react";
function MessagingPage() {
useTools(
[
{
name: "find_contact",
description: "Find a contact by name",
inputSchema: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
additionalProperties: false,
},
execute: ({ name }) => contacts.findByName(String(name)),
},
{
name: "send_message",
description: "Send a message to a contact ID",
inputSchema: {
type: "object",
properties: {
contactId: { type: "string" },
body: { type: "string" },
},
required: ["contactId", "body"],
additionalProperties: false,
},
annotations: { consequentialHint: true },
execute: ({ contactId, body }) =>
messages.send(String(contactId), String(body)),
},
],
[],
);
return <Inbox />;
}SuperCmdK runs calls in order and caps a chain at eight turns unless you set maxSteps.
const { runAgent } = useSuperCmdK();
const result = await runAgent("dim the living room lights", {
confidenceThreshold: 0.8,
systemPrompt: "date: 2026-08-13; locale: en-US",
maxSteps: 4,
});Use provider toolPolicy.confirm for confirmation shared across clients. AgentRunOptions.confirm remains available for code written against 0.1. SuperCmdK returns unknown tools, invalid arguments, denied calls, and handler failures to the Agent as normalized errors. Pass an AbortSignal to cancel a run.
Import the engine adapter and chain runner from the Agent entry point:
import { NeedleWasmEngine, runAgentChain } from "@supercmdk/react/agent";NeedleWasmEngine implements preload, initialize, complete, reset, and dispose. runAgentChain(engine, input, tools, options) accepts Needle or another AgentEngine implementation.
Needle's WASM ABI supports one session per Worker. SuperCmdK serializes runs within each provider and sends the active tool schemas before each run.
SuperCmdK keeps the Agent runtime in a separate chunk. The Worker handles model download, WASM compilation, model loading, and inference. The optional palette package does not load the Agent code unless an engine is configured.
Tool handlers run in your application context. Move CPU-heavy work into a Worker or server API. When a command closes the palette, SuperCmdK waits for the close to paint before it calls the command handler.
The canonical tool contract uses WebMCP's current imperative API vocabulary:
| Previous field | WebMCP-aligned field |
|---|---|
parameters |
inputSchema |
annotations.readOnly |
annotations.readOnlyHint |
annotations.requiresConfirmation |
annotations.consequentialHint |
annotations.destructive |
annotations.consequentialHint when the effect is significant or non-reversible |
title is now available as an optional human-readable label. Tool names must follow WebMCP's 1–128 character [A-Za-z0-9_.-] format, and every inputSchema must describe an object. idempotent has no current WebMCP equivalent and is no longer part of the annotations contract.
The optional command palette now lives in @supercmdk/palette. Install that package and update palette imports:
| Previous import | Current import |
|---|---|
CommandPalette from @supercmdk/react |
CommandPalette from @supercmdk/palette |
cmdk primitives from @supercmdk/react |
cmdk primitives from @supercmdk/palette |
@supercmdk/react/styles.css |
@supercmdk/palette/styles.css |
The provider, hooks, tools, and Agent APIs remain in @supercmdk/react.
Version 0.2 keeps the 0.1 Agent tool names as deprecated aliases:
| 0.1 API | 0.2 API |
|---|---|
AgentTool |
Tool |
AgentToolContext |
ToolContext |
AgentToolSchema |
ToolSchema |
useAgentTool |
useTool |
useAgentTools |
useTools |
Provider tools, runAgent, runAgentChain, AgentEngine, and @supercmdk/react/agent imports still work. Version 0.2 rejects invalid tool arguments before it calls a handler.
Open https://nicholaszolton.github.io/SuperCmdK/ to try the palette and Needle tool chain. Use Test approval or ask the Agent to “delete production” to run a simulated destructive tool. Approving or denying it changes only the demo activity log. The demo source lives in this repository.
The demo build generates /llms.txt from package metadata and the same canonical tool definitions registered by the page. It describes the site, current WebMCP tools, safety behavior, packages, maintainer, and stable documentation links without treating route-scoped runtime state as durable documentation.
Run it on your machine with Tilt and portless:
mise trust
mise install
portless trust # one-time machine setup
mise run devThe demo resolves Needle from the local @supercmdk/needle workspace package, so it needs no separate model download. Tilt prints the demo URL, often https://web.supercmdk.localhost:1355, and cleans up when you press Ctrl-C.
bun install
bun run check
bun run test
bun run buildRelease Please reads Conventional Commits on main and updates one release PR:
fix:requests a patch release.feat:requests a minor release.feat!:or aBREAKING CHANGE:footer requests a major release.
Merge the release PR to update package.json, package versions, bun.lock, and CHANGELOG.md. Release Please then creates the vX.Y.Z GitHub Release. .github/workflows/publish.yml publishes @supercmdk/react, @supercmdk/palette, and @supercmdk/needle to npm through OIDC trusted publishing. Keep version edits and release tags in this flow.