Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@
"video:render:product-hunt": "remotion render remotion/index.ts skillsboard-launch-product-hunt public/launch/skills-board-product-demo-product-hunt.mp4 --codec=h264 --crf=18 --pixel-format=yuv420p --gl=swangle --timeout=240000 --concurrency=2 --muted",
"video:render:social": "remotion render remotion/index.ts skillsboard-launch-social public/launch/skills-board-product-demo.mp4 --codec=h264 --crf=18 --pixel-format=yuv420p --gl=swangle --timeout=240000 --concurrency=2 --muted",
"video:render:team-loop": "remotion render remotion/index.ts skillsboard-team-loop public/launch/skills-board-team-loop.mp4 --codec=h264 --crf=18 --pixel-format=yuv420p --gl=swangle --timeout=240000 --concurrency=2 --muted",
"video:still:product-demo-poster": "remotion still remotion/index.ts skillsboard-launch-product-hunt public/launch/skills-board-product-demo-poster.jpg --frame=580 --image-format=jpeg --gl=swangle --timeout=240000",
"video:render:launch": "pnpm video:render:product-hunt && pnpm video:render:social && pnpm video:render:team-loop && pnpm video:still:product-demo-poster",
"video:render:mcp-setup": "remotion render remotion/index.ts skillsboard-mcp-setup public/launch/skills-board-mcp-setup.mp4 --codec=h264 --crf=18 --pixel-format=yuv420p --gl=swangle --timeout=240000 --concurrency=2 --muted",
"video:render:mcp-agent": "remotion render remotion/index.ts skillsboard-mcp-agent public/launch/skills-board-mcp-agent.mp4 --codec=h264 --crf=18 --pixel-format=yuv420p --gl=swangle --timeout=240000 --concurrency=2 --muted",
"video:still:product-demo-poster": "remotion still remotion/index.ts skillsboard-launch-product-hunt public/launch/skills-board-product-demo-poster.jpg --frame=580 --image-format=jpeg --gl=swangle --timeout=240000",
"video:still:mcp-setup-poster": "remotion still remotion/index.ts skillsboard-mcp-setup public/launch/skills-board-mcp-setup-poster.jpg --frame=306 --image-format=jpeg --gl=swangle --timeout=240000",
"video:still:mcp-agent-poster": "remotion still remotion/index.ts skillsboard-mcp-agent public/launch/skills-board-mcp-agent-poster.jpg --frame=700 --image-format=jpeg --gl=swangle --timeout=240000",
"video:render:launch": "pnpm video:render:product-hunt && pnpm video:render:social && pnpm video:render:team-loop && pnpm video:still:product-demo-poster",
"db:generate": "drizzle-kit generate",
"db:check": "drizzle-kit check && node scripts/check-schema-migrations.mjs",
"db:migrate": "drizzle-kit migrate",
Expand Down
123 changes: 123 additions & 0 deletions remotion/mcp/content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* Every string the two MCP videos put on screen.
*
* The facts come out of this repository rather than out of a script: the config
* block is `plugin/mcp.json` verbatim, the endpoint is the one
* `getMcpResource()` returns in production, the scopes are the ones
* `app/api/[transport]/route.ts` checks before a write, and every tool name is
* registered in that route and listed on `/connect`. Only the example team, the
* teammate, and the example skill are invented, and none of them carries a
* product claim.
*/

export const brand = {
wordmark: "Skills Board",
/** The category phrase, verbatim, as the marketing context pins it. */
category: "the agent-native skills registry for teams",
domain: "www.skillsboard.sh",
licence: "Free forever. MIT.",
} as const;

/** `plugin/mcp.json`, line for line. */
export const configLines = [
"{",
' "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",',
' "mcpServers": {',
' "skills-board": {',
' "type": "streamable-http",',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the actual client configuration in the setup video

When a viewer follows the video's “Paste the config” instruction for the subsequently shown Claude Code flow—or for Cursor—they receive the portable agent-plugin descriptor rather than a valid client config. The repository's Claude manifest (plugin/.mcp.json) and the manual configuration generated in app/(app)/connect/page.tsx use "type": "http", while Claude Code's documented setup in components/mcp-setup-guide.tsx uses claude mcp add --transport http; the shown "streamable-http" value belongs to plugin/mcp.json's plugin schema. Use the client-facing config or show the plugin installation flow so the advertised setup can actually connect.

Useful? React with 👍 / 👎.

' "url": "https://www.skillsboard.sh/api/mcp"',
" }",
" }",
"}",
] as const;

/** The line the highlight lands on once the block is on screen. */
export const CONFIG_URL_LINE = 5;

export const setup = {
hook: {
lines: [
["Give", "your", "agent"],
["your", "team's", "AI", "skills."],
],
accentWord: "teams",
sub: "Paste the config. Approve access.",
},
config: {
step: "Step 1",
heading: "Add the MCP server to your client",
file: "mcp.json",
caption: "Streamable HTTP, and no API key to copy.",
},
consent: {
step: "Step 2",
heading: "Approve access in the browser",
client: "Claude Code",
title: "Claude Code wants to reach your team libraries",
scopes: [
{ name: "skills:read", detail: "List and search team skills and collections" },
{ name: "skills:write", detail: "Save skills and organize collections" },
],
approve: "Approve",
granted: "Access granted",
footer: "You sign in through your browser.",
},
tools: {
step: "Done",
heading: "13 tools in your client",
writeBadge: "write",
},
} as const;

/** The 13 tools `app/api/[transport]/route.ts` registers, in registration order. */
export const tools = [
{ name: "list_skills", write: false },
{ name: "search_skills", write: false },
{ name: "get_skill_command", write: false },
{ name: "discover_skills", write: false },
{ name: "discover_repository_skills", write: false },
{ name: "add_skill", write: true },
{ name: "list_collections", write: false },
{ name: "search_collections", write: false },
{ name: "get_collection_skills", write: false },
{ name: "get_collection_install_command", write: false },
{ name: "create_collection", write: true },
{ name: "add_skill_to_collection", write: true },
{ name: "remove_skill_from_collection", write: true },
] as const;

/**
* The example skill, its team, and the teammate who saved it are invented. The
* tool name, its argument shape, and the endpoint printed in the client header
* are the real ones.
*/
export const agentRun = {
client: "Claude Code",
server: "skills-board",
endpoint: "www.skillsboard.sh/api/mcp",
prompt: "draft the release notes for v2.4 the way our team does them",
call: 'search_skills({ query: "release notes" })',
hit: {
source: "github.com/northwind/agent-skills",
title: "release-notes",
description: "Turn merged pull requests into release notes grouped by user impact.",
tags: ["release", "docs"],
savedBy: "Marta R.",
team: "Northwind",
},
note: "Group by user impact. Link the pull request on every entry.",
applyLine: "Skills Board carries the note your teammate saved.",
output: {
title: "## v2.4",
rows: [
"Faster team library search · #1841",
"Collections in the sidebar · #1836",
"ZIP download fixed on Safari · #1852",
],
done: "release notes drafted",
},
reach: {
line: "The same saved skill, from every MCP client.",
clients: ["Claude Code", "Claude Desktop", "Cursor", "VS Code"],
},
} as const;
102 changes: 102 additions & 0 deletions remotion/mcp/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { AbsoluteFill } from "remotion";
import { linearTiming, TransitionSeries } from "@remotion/transitions";
import { fade } from "@remotion/transitions/fade";
import { focusPull } from "@/components/remocn/focus-pull";
import { pushThrough } from "@/components/remocn/push-through";
import { zoomBlur } from "@/components/remocn/zoom-blur";
import "../product-demo/fonts";
import { light } from "../product-demo/theme";
import { ConfigScene } from "./scenes/config";
import { ConsentScene } from "./scenes/consent";
import { EndCardScene } from "./scenes/end-card";
import { HookScene } from "./scenes/hook";
import { ReachScene } from "./scenes/reach";
import { SessionScene } from "./scenes/session";
import { ToolsScene } from "./scenes/tools";

/** The thirty second cut of the agent video, built as a spot. */
export {
MCP_AGENT_SPOT_DURATION,
MCP_AGENT_SPOT_POSTER_FRAME,
SkillsboardMcpAgentSpot,
} from "./spot";

export const MCP_FPS = 30;

/** 1110 frames of scene, 88 of overlap. */
export const MCP_SETUP_DURATION = 1022;
/** 1180 frames of scene, 44 of overlap. */
export const MCP_AGENT_DURATION = 1136;

/** Frames the two posters are cut from: the endpoint, and the finished session. */
export const MCP_SETUP_POSTER_FRAME = 306;
export const MCP_AGENT_POSTER_FRAME = 700;

function timing(durationInFrames: number) {
return linearTiming({ durationInFrames });
}

/**
* Connecting a client: the problem, the config, the authorization, the tools.
* Every fact on screen is read out of the repository, so the video and the
* product cannot drift apart without the source drifting first.
*/
export function SkillsboardMcpSetup() {
return (
<AbsoluteFill style={{ background: light.background }}>
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={130}>
<HookScene durationInFrames={130} />
</TransitionSeries.Sequence>
<TransitionSeries.Transition presentation={focusPull()} timing={timing(24)} />

<TransitionSeries.Sequence durationInFrames={300}>
<ConfigScene durationInFrames={300} />
</TransitionSeries.Sequence>
<TransitionSeries.Transition presentation={pushThrough()} timing={timing(22)} />

<TransitionSeries.Sequence durationInFrames={260}>
<ConsentScene durationInFrames={260} />
</TransitionSeries.Sequence>
<TransitionSeries.Transition presentation={zoomBlur()} timing={timing(20)} />

<TransitionSeries.Sequence durationInFrames={260}>
<ToolsScene durationInFrames={260} />
</TransitionSeries.Sequence>
<TransitionSeries.Transition presentation={fade()} timing={timing(22)} />

<TransitionSeries.Sequence durationInFrames={160}>
<EndCardScene durationInFrames={160} />
</TransitionSeries.Sequence>
</TransitionSeries>
</AbsoluteFill>
);
}

/**
* The connection at work: one task, one search against the team library, and
* the teammate's own note carried into the answer. It runs as a single session
* in a single window, on the same light stage as the setup video, so the search
* and the answer read as one job rather than as two examples.
*/
export function SkillsboardMcpAgent() {
return (
<AbsoluteFill style={{ background: light.background }}>
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={770}>
<SessionScene durationInFrames={770} />
</TransitionSeries.Sequence>
<TransitionSeries.Transition presentation={pushThrough()} timing={timing(22)} />

<TransitionSeries.Sequence durationInFrames={250}>
<ReachScene durationInFrames={250} />
</TransitionSeries.Sequence>
<TransitionSeries.Transition presentation={fade()} timing={timing(22)} />

<TransitionSeries.Sequence durationInFrames={160}>
<EndCardScene durationInFrames={160} />
</TransitionSeries.Sequence>
</TransitionSeries>
</AbsoluteFill>
);
}
38 changes: 38 additions & 0 deletions remotion/mcp/scenes/config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { interpolate } from "remotion";
import { clamp, Scene, useSceneFrame } from "../../product-demo/stage";
import { light } from "../../product-demo/theme";
import { CONFIG_URL_LINE, setup } from "../content";
import { Rise, SceneHead, t } from "../ui/chrome";
import { ConfigBlock } from "../ui/code-block";

const REF = 300;
const { config } = setup;

/** Step one: the block from `plugin/mcp.json`, pasted into the client. */
export function ConfigScene({ durationInFrames }: { durationInFrames: number }) {
const frame = useSceneFrame(REF, durationInFrames);

return (
<Scene>
<div
style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 44 }}
>
<SceneHead frame={frame} heading={config.heading} step={config.step} />

<Rise at={24} distance={22} frame={frame}>
<ConfigBlock at={34} frame={frame} highlightAt={132} highlightLine={CONFIG_URL_LINE} />
</Rise>

<div
style={{
...t.caption,
color: light.mutedForeground,
opacity: interpolate(frame, [176, 196], [0, 1], clamp),
}}
>
{config.caption}
</div>
</div>
</Scene>
);
}
25 changes: 25 additions & 0 deletions remotion/mcp/scenes/consent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Scene, useSceneFrame } from "../../product-demo/stage";
import { setup } from "../content";
import { Rise, SceneHead } from "../ui/chrome";
import { ConsentCard } from "../ui/consent-card";

const REF = 260;
const { consent } = setup;

/** Step two: the browser authorization, on Skills Board's own tokens. */
export function ConsentScene({ durationInFrames }: { durationInFrames: number }) {
const frame = useSceneFrame(REF, durationInFrames);

return (
<Scene>
<div
style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 44 }}
>
<SceneHead frame={frame} heading={consent.heading} step={consent.step} />
<Rise at={22} distance={22} frame={frame}>
<ConsentCard at={34} frame={frame} pressAt={148} />
</Rise>
</div>
</Scene>
);
}
66 changes: 66 additions & 0 deletions remotion/mcp/scenes/end-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { interpolate } from "remotion";
import { clamp, outEasing, Scene, useSceneFrame } from "../../product-demo/stage";
import { displayTracking, light, MONO } from "../../product-demo/theme";
import { BrandMark } from "../../product-demo/ui/atoms";
import { brand } from "../content";

const REF = 160;

/** The same close on both videos: mark, category, domain, licence. */
export function EndCardScene({ durationInFrames }: { durationInFrames: number }) {
const frame = useSceneFrame(REF, durationInFrames);

return (
<Scene>
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 26 }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: 22,
opacity: interpolate(frame, [0, 18], [0, 1], clamp),
translate: `0 ${interpolate(frame, [0, 24], [18, 0], outEasing)}px`,
}}
>
<BrandMark size={72} />
<span style={{ fontSize: 70, fontWeight: 600, letterSpacing: displayTracking }}>
{brand.wordmark}
</span>
</div>

<div
style={{
fontSize: 36,
color: light.foreground,
opacity: interpolate(frame, [22, 42], [0, 1], clamp),
translate: `0 ${interpolate(frame, [22, 46], [12, 0], outEasing)}px`,
}}
>
{brand.category}
</div>

<div
style={{
marginTop: 10,
fontFamily: MONO,
fontSize: 32,
color: light.primary,
opacity: interpolate(frame, [46, 66], [0, 1], clamp),
}}
>
{brand.domain}
</div>

<div
style={{
fontSize: 28,
color: light.mutedForeground,
opacity: interpolate(frame, [66, 86], [0, 1], clamp),
}}
>
{brand.licence}
</div>
</div>
</Scene>
);
}
Loading
Loading