General
spa-without-runtime - Claude MCP Skill
Build a pure-client CopilotKit v2 SPA with NO server runtime by pointing CopilotKitProvider at CopilotKit Cloud via publicApiKey. This is the ONLY production-safe SPA path. Both agents__unsafe_dev_only and selfManagedAgents are dev-only aliases that leak credentials in production — never ship them. Covers Vite / plain React Router SPA wiring, getting a publicApiKey from cloud.copilotkit.ai, and the SSR non-concern. publicLicenseKey is an accepted alias for publicApiKey (publicApiKey wins when both are set). Load when the user has no backend and wants chat in a pure-SPA deployment (Vite, GitHub Pages, S3, etc.).
SEO Guide: Enhance your AI agent with the spa-without-runtime tool. This Model Context Protocol (MCP) server allows Claude Desktop and other LLMs to build a pure-client copilotkit v2 spa with no server runtime by pointing copilotkitprovider at copil... Download and configure this skill to unlock new capabilities for your AI workflow.
Documentation
SKILL.md## Setup
Obtain a `publicApiKey` from cloud.copilotkit.ai. In a Vite SPA:
```bash
# .env.local
VITE_CPK_PUBLIC_API_KEY=ck_pub_xxxxxxxxxxxxxxxxxxxx
```
```tsx
// src/App.tsx
import {
CopilotKitProvider,
CopilotChat,
useFrontendTool,
} from "@copilotkit/react-core/v2";
import "@copilotkit/react-core/v2/styles.css";
import { z } from "zod";
function RegisterTools() {
useFrontendTool({
name: "readClipboard",
description: "Read the clipboard contents.",
parameters: z.object({}),
handler: async () => {
try {
const text = await navigator.clipboard.readText();
return { text };
} catch (error) {
// navigator.clipboard.readText() rejects on denied permission,
// non-HTTPS origins, and lost document focus. Return a structured
// error so the agent sees it as a regular tool result and can
// respond ("I couldn't read your clipboard — please paste here")
// instead of surfacing a raw DOMException.
//
// Note: returning an error object does NOT trigger `onError` with
// `tool_handler_failed` — that fires only when the handler throws.
// If you want `onError` to fire, `throw` instead of returning.
return {
error: "clipboard_read_failed",
message:
error instanceof Error ? error.message : "Clipboard unavailable",
};
}
},
});
return null;
}
export default function App() {
return (
<CopilotKitProvider publicApiKey={import.meta.env.VITE_CPK_PUBLIC_API_KEY}>
<RegisterTools />
<div className="h-screen">
<CopilotChat agentId="default" className="h-full" />
</div>
</CopilotKitProvider>
);
}
```
No `runtimeUrl`. No server. Auth header
(`X-CopilotCloud-Public-Api-Key: ck_pub_...`) is injected by the Cloud
client. Frontend tools still work — they execute in the browser; the Cloud
runtime dispatches tool calls to the SPA over SSE.
## Core Patterns
### Registering frontend tools in a pure SPA
Frontend tools work identically to the full-runtime case — register them
inside the provider subtree.
```tsx
import { useFrontendTool, useHumanInTheLoop } from "@copilotkit/react-core/v2";
import { z } from "zod";
function AppTools() {
useFrontendTool({
name: "navigate",
description: "Navigate to a route",
parameters: z.object({ path: z.string() }),
handler: async ({ path }) => {
// Use your router's navigate function; the exact API depends on
// your router (react-router's `useNavigate`, TanStack Router's
// `router.navigate`, etc.). Do NOT use `window.location.pathname = ...`
// — it triggers a full page reload and kills the agent stream,
// destroying chat state mid-run.
navigate(path);
return { ok: true };
},
});
useHumanInTheLoop({
name: "confirmDelete",
description: "Ask the user to confirm a destructive action",
parameters: z.object({ label: z.string() }),
render: ({ args, status, respond }) =>
status === "executing" ? (
<div>
Delete {args.label}?
<button onClick={() => respond({ ok: true })}>Yes</button>
<button onClick={() => respond({ ok: false })}>No</button>
</div>
) : null,
});
return null;
}
```
### Reading agent state in a SPA
```tsx
import { useAgent, useAgentContext } from "@copilotkit/react-core/v2";
function Panel() {
useAgentContext({
description: "Current page URL",
value: window.location.href,
});
const { agent } = useAgent({ agentId: "default" });
return (
<div>
State: {JSON.stringify(agent?.state)} —{" "}
{agent?.isRunning ? "running" : "idle"}
</div>
);
}
```
`useAgent` returns `{ agent }` only — `agent` may be `undefined` while the
runtime is still loading, so guard with optional chaining. Access
`isRunning` via the agent instance itself.
Source: packages/react-core/src/v2/hooks/use-agent.tsx:333-335
## Common Mistakes
### CRITICAL reaching for selfManagedAgents as a production SPA path
Wrong:
```tsx
// SPA with no backend, trying to avoid CopilotKit Cloud:
<CopilotKitProvider
selfManagedAgents={{
default: new BuiltInAgent({ apiKey: "sk-live-..." }),
}}
/>
```
Correct:
```tsx
// For a SPA without a runtime, the ONLY production path is CopilotKit Cloud:
<CopilotKitProvider publicApiKey="ck_pub_..." />
// If you control a backend, use a runtime instead:
<CopilotKitProvider runtimeUrl="/api/copilotkit" />
```
`selfManagedAgents` and `agents__unsafe_dev_only` are aliases — merged at
`CopilotKitProvider.tsx:393` with no auth gating. Any agent constructed in
the browser has its API key visible in the bundle. The benign-sounding name
"selfManagedAgents" is the trap: it is NOT production-safe. Both props
exist purely for local-dev demos.
Source: packages/react-core/src/v2/providers/CopilotKitProvider.tsx:136-138,393
### CRITICAL using agents\_\_unsafe_dev_only in production
Wrong:
```tsx
<CopilotKitProvider
agents__unsafe_dev_only={{
default: new BuiltInAgent({ apiKey: process.env.OPENAI_API_KEY }),
}}
/>
```
Correct:
```tsx
<CopilotKitProvider publicApiKey="ck_pub_..." />
```
The `__unsafe_dev_only` suffix is a warning, not a safety belt — nothing
prevents the prop from shipping. Any API key referenced from browser code
ends up in the bundle.
Source: packages/react-core/src/v2/providers/CopilotKitProvider.tsx:136-138,393
### HIGH passing both runtimeUrl and publicApiKey
Wrong:
```tsx
<CopilotKitProvider runtimeUrl="/api/x" publicApiKey="ck_pub_..." />
```
Correct:
```tsx
// Cloud-only SPA:
<CopilotKitProvider publicApiKey="ck_pub_..." />
// Or own runtime, not both:
<CopilotKitProvider runtimeUrl="/api/copilotkit" />
```
`runtimeUrl` wins when both are present — the Cloud call is never issued,
and the hardcoded public key sits in the bundle for no reason.
Source: docs/snippets/shared/troubleshooting/common-issues.mdx:30-42
### LOW publicApiKey vs publicLicenseKey — pick one
`publicApiKey` is canonical; `publicLicenseKey` is an accepted alias.
Resolution order:
```ts
// packages/react-core/src/v2/providers/CopilotKitProvider.tsx:391
const resolvedPublicKey = publicApiKey ?? publicLicenseKey;
```
`publicApiKey` wins when both are set. Prefer `publicApiKey` in new code
for consistency with the HTTP header name (`X-CopilotCloud-Public-Api-Key`)
and the Cloud dashboard label.
Source: packages/react-core/src/v2/providers/CopilotKitProvider.tsx:122-128,391
### LOW over-configuring SSR in a pure SPA
Wrong:
```tsx
// Dynamic-import gymnastics in a pure Vite SPA:
const CopilotChat = React.lazy(() =>
import("@copilotkit/react-core/v2").then((m) => ({ default: m.CopilotChat })),
);
```
Correct:
```tsx
import { CopilotKitProvider, CopilotChat } from "@copilotkit/react-core/v2";
// Just render it at the top level; no SSR concerns in a Vite SPA.
```
`@copilotkit/react-core/v2` is marked `"use client"` — SSR is not in scope.
A pure SPA has no server render path; the normal import is correct.
Source: packages/react-core/src/v2/index.ts:1Signals
Information
- Repository
- CopilotKit/CopilotKit
- Author
- CopilotKit
- Last Sync
- 5/26/2026
- Repo Updated
- 5/26/2026
- Created
- 5/21/2026
Reviews (0)
No reviews yet. Be the first to review this skill!
Related Skills
upgrade-nodejs
Upgrading Bun's Self-Reported Node.js Version
cursorrules
CrewAI Development Rules
README
Agents — Working Implementations
cn-check
Install and run the Continue CLI (`cn`) to execute AI agent checks on local code changes. Use when asked to "run checks", "lint with AI", "review my changes with cn", or set up Continue CI locally.
Related Guides
Bear Notes Claude Skill: Your AI-Powered Note-Taking Assistant
Learn how to use the bear-notes Claude skill. Complete guide with installation instructions and examples.
OpenAI Whisper API Claude Skill: Complete Guide to AI-Powered Audio Transcription
Learn how to use the openai-whisper-api Claude skill. Complete guide with installation instructions and examples.
Mastering the Oracle CLI: A Complete Guide to the Claude Skill for Database Professionals
Learn how to use the oracle Claude skill. Complete guide with installation instructions and examples.