Skip to content

Commit bc8cec6

Browse files
authored
feat(integrations): add Intelligence threads to mcp-apps (CopilotKit#5211)
## Summary - add env-gated CopilotKit Intelligence Threads support to the `mcp-apps` integration - move the runtime route to `[[...slug]]` with GET/POST/PATCH/DELETE for thread REST routes - wire the Threads drawer + locked gate into the existing MCP apps chat surface - align CopilotKit/AG-UI versions and commit npm lockfiles for the app and Three.js MCP server - extend the Intelligence migration contract to include `mcp-apps`, including the app-router path differences ## Verification - `npm run build` from `examples/integrations/mcp-apps` - `pnpm exec vitest run scripts/__tests__/integration-intelligence-migration.test.ts` - `pnpm exec oxfmt --check scripts/__tests__/integration-intelligence-migration.test.ts examples/integrations/mcp-apps/app/page.tsx examples/integrations/mcp-apps/app/components/threads-drawer/threads-drawer.tsx` ## Licensed smoke With local Intelligence services from `/Users/mothra/Projects/test-signups4` and the example `.env` exported: - `docker compose up -d --wait` reported postgres, redis, and intelligence healthy - demo users `1_demo-user` and `demo-user` were seeded/no-op present - `GET /api/copilotkit/info` returned `mode: intelligence`, `licenseStatus: valid`, and agent `default` - `GET /api/copilotkit/threads?agentId=default&limit=20` returned 200 and the Threads drawer rendered persisted threads - started `npm run dev:mcp` and `next dev --turbopack -p 3024` - created a new thread, sent “Say hello briefly for the smoke test.”, `/agent/default/run` returned 200, and the assistant rendered “Hello!” - after restarting the dev servers, selected the persisted “Quick Greetings for Testing” thread; the prior user prompt and assistant response restored, with `/agent/default/connect` returning 200 ## Notes - `3000` was occupied by another local example, so smoke used `3024` for the Next UI. - The local thread list contained older experimental thread names from previous mcp-apps smoke runs; this PR now clears the client thread id for “New thread” instead of minting a random UUID, so new thread creation/replay works through the runtime.
2 parents 2103d0c + a42eb45 commit bc8cec6

18 files changed

Lines changed: 18183 additions & 67 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
OPENAI_API_KEY=your-openai-api-key-here
2+
3+
# Optional: enable CopilotKit Intelligence Threads locally
4+
COPILOTKIT_LICENSE_TOKEN=
5+
INTELLIGENCE_API_KEY=
6+
INTELLIGENCE_API_URL=http://localhost:4201
7+
INTELLIGENCE_GATEWAY_WS_URL=ws://localhost:4401

examples/integrations/mcp-apps/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ yarn-error.log*
3232

3333
# env files (can opt-in for committing if needed)
3434
.env*
35+
!.env.example
3536

3637
# vercel
3738
.vercel
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {
2+
CopilotKitIntelligence,
3+
CopilotRuntime,
4+
InMemoryAgentRunner,
5+
createCopilotEndpoint,
6+
} from "@copilotkit/runtime/v2";
7+
import { BuiltInAgent } from "@copilotkit/runtime/v2";
8+
import { MCPAppsMiddleware } from "@ag-ui/mcp-apps-middleware";
9+
import { handle } from "hono/vercel";
10+
11+
const middlewares = [
12+
new MCPAppsMiddleware({
13+
mcpServers: [
14+
{
15+
type: "http",
16+
url: "http://localhost:3108/mcp",
17+
serverId: "threejs",
18+
},
19+
],
20+
}),
21+
];
22+
23+
const agent = new BuiltInAgent({
24+
model: "openai/gpt-4o",
25+
prompt: "You are a helpful assistant.",
26+
});
27+
28+
for (const middleware of middlewares) {
29+
agent.use(middleware);
30+
}
31+
32+
const runtime = new CopilotRuntime({
33+
agents: {
34+
default: agent,
35+
},
36+
// --- copilotkit:intelligence (remove this block to opt out) ---
37+
...(process.env.COPILOTKIT_LICENSE_TOKEN
38+
? {
39+
intelligence: new CopilotKitIntelligence({
40+
apiKey: process.env.INTELLIGENCE_API_KEY ?? "",
41+
apiUrl: process.env.INTELLIGENCE_API_URL ?? "http://localhost:4201",
42+
wsUrl:
43+
process.env.INTELLIGENCE_GATEWAY_WS_URL ?? "ws://localhost:4401",
44+
}),
45+
// Demo stub — replace with your own auth-derived user identity (e.g. OIDC)
46+
// before any multi-user deployment, or all users share one thread history.
47+
identifyUser: () => ({ id: "demo-user", name: "Demo User" }),
48+
licenseToken: process.env.COPILOTKIT_LICENSE_TOKEN,
49+
}
50+
: { runner: new InMemoryAgentRunner() }),
51+
// --- /copilotkit:intelligence ---
52+
});
53+
54+
const app = createCopilotEndpoint({
55+
runtime,
56+
basePath: "/api/copilotkit",
57+
});
58+
59+
export const GET = handle(app);
60+
export const POST = handle(app);
61+
export const PATCH = handle(app);
62+
export const DELETE = handle(app);

examples/integrations/mcp-apps/app/api/copilotkit/route.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Threads Panel — Design Notes (mastra)
2+
3+
These are mastra's **bespoke** copies of the threads panel. They are no longer a
4+
shared/tokenized base component — they are styled to read as one product with
5+
mastra's `CopilotSidebar` (the right-side chat from `@copilotkit/react-core/v2`).
6+
7+
## Design source of truth
8+
9+
All surfaces, borders, radii, and type ramps are lifted from CopilotKit's V2
10+
design system: `@copilotkit/react-core/src/v2/styles/globals.css` plus the chat
11+
components (`CopilotModalHeader`, `CopilotChatSuggestionPill`,
12+
`CopilotChatInput`, `CopilotSidebarView`). The tokens are mirrored verbatim into
13+
`src/app/globals.css`:
14+
15+
| Token | Value (V2 light) | Role in the panel |
16+
| -------------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
17+
| `--card` / `--background` | `oklch(1 0 0)` (white) | Panel + card surfaces |
18+
| `--foreground` | `oklch(0.145 0 0)` | Titles, thread titles, dialog text |
19+
| `--muted` / `--secondary` / `--accent` | `oklch(0.97 0 0)` | Hover/active surfaces, segment track, archived chip, code well |
20+
| `--muted-foreground` | `oklch(0.556 0 0)` | Meta text, idle icons, descriptions, placeholders |
21+
| `--border` / `--input` | `oklch(0.922 0 0)` | All hairline borders |
22+
| `--primary` | `oklch(0.205 0 0)` (near-black) | New-thread pill, selected accent, primary CTA — the V2 sidebar's primary buttons are charcoal/black, **not** a brand accent |
23+
| `--primary-foreground` | `oklch(0.985 0 0)` | Primary button text |
24+
| `--destructive` | `oklch(0.577 0.245 27.325)` | Delete hover |
25+
| `--ring` | `oklch(0.708 0 0)` | Focus rings (2px box-shadow) |
26+
| `--radius` | `0.625rem` (+ sm/md/lg/xl) | Rectangular controls; icon buttons / pills / segments use `999px` to echo the sidebar's close button, suggestion pills, and send button |
27+
28+
## Forced light
29+
30+
mastra's `CopilotSidebar` is always light regardless of OS color scheme. The
31+
panel must match it, so `src/app/globals.css` re-pins `--foreground` and
32+
`--background` to the V2 light values on `.threadsLayout` (the layout wrapper)
33+
and on `body > [role="presentation"]` (the confirm dialog renders in a portal on
34+
`<body>`). The dark-mode `@media (prefers-color-scheme: dark)` block only flips
35+
the bare page `--background`/`--foreground`; the panel overrides win because
36+
they are scoped to the layout/portal roots.
37+
38+
## Typography
39+
40+
Geist (the app font, via `--font-body` / `--font-code`). Sizes/weights track the
41+
sidebar: header title `1rem / 500 / tracking-tight`, thread titles
42+
`0.8125rem / 500`, meta `0.6875rem`, all medium-weight — no heavy `700`s.
43+
44+
Edit these files freely; they are mastra-owned and not shared with other
45+
examples.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"use client";
2+
3+
export { default as ThreadsDrawer } from "./threads-drawer";
4+
export type { ThreadsDrawerProps } from "./threads-drawer";
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import { Lock } from "lucide-react";
5+
import styles from "./threads-drawer.module.css";
6+
7+
export function ThreadsPanelGate({ children }: { children: React.ReactNode }) {
8+
// The Threads drawer reads a client-only external store (useThreads /
9+
// useSyncExternalStore) with no server snapshot, so it must not render during
10+
// SSR/prerender — Next would fail to prerender "/". Defer to client mount.
11+
const [mounted, setMounted] = React.useState(false);
12+
React.useEffect(() => setMounted(true), []);
13+
14+
if (process.env.NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED === "true") {
15+
if (!mounted) {
16+
// SSR / first-paint placeholder: matches the open drawer's footprint +
17+
// surface (and collapses to nothing on mobile) so the panel doesn't flash
18+
// a bare-background column or shift the content when the drawer mounts.
19+
return <div className={styles.drawerPlaceholder} aria-hidden />;
20+
}
21+
return <>{children}</>;
22+
}
23+
24+
return (
25+
<aside aria-label="Threads (locked)" className={styles.lockedPanel}>
26+
<div className={styles.drawerHeader}>
27+
<div className={styles.drawerHeaderMain}>
28+
<h2 className={styles.drawerTitle}>Threads</h2>
29+
</div>
30+
</div>
31+
<div className={styles.lockedBody}>
32+
<div className={styles.lockedCard}>
33+
<span aria-hidden className={styles.lockedIcon}>
34+
<Lock size={18} />
35+
</span>
36+
<div className={styles.lockedHeading}>
37+
<h3 className={styles.lockedTitle}>
38+
Threads is a licensed feature
39+
</h3>
40+
<p className={styles.lockedDescription}>
41+
Unlock persistent conversation history, multi-session context, and
42+
thread management with CopilotKit Intelligence.
43+
</p>
44+
</div>
45+
<p className={styles.lockedDescription}>
46+
Add it to your project with:
47+
</p>
48+
<div className={styles.lockedCommand}>
49+
<code className={styles.lockedCommandCode}>
50+
copilotkit add-intelligence
51+
</code>
52+
</div>
53+
<button
54+
type="button"
55+
className={styles.lockedCta}
56+
onClick={() =>
57+
window.open(
58+
"https://docs.copilotkit.ai/intelligence",
59+
"_blank",
60+
"noopener,noreferrer",
61+
)
62+
}
63+
>
64+
Learn more
65+
</button>
66+
</div>
67+
</div>
68+
</aside>
69+
);
70+
}

0 commit comments

Comments
 (0)