Skip to content

Commit 1a12987

Browse files
feat(mcp): render lesson MDX in the lesson widgets the way the app does
`lessons.content` is MDX — markdown plus the ~30 JSX components the web app maps in `components/lesson/mdx-components.tsx` (`<Callout>`, `<Quiz>`, `<Steps>`, `<Table>`, `<Video>`, `<LessonCheckpoint>`, …). The two MCP lesson widgets ran that source through plain react-markdown, so every authored component reached the student as literal source text, `JSON.parse('…')` payloads included, and `embed_code` was dropped entirely. Widgets can not compile-and-run MDX: `@mdx-js/mdx`'s run() builds the module with the Function constructor, which the widget CSP sandbox blocks. So parse only — remark + remark-mdx keeps JSX as mdxJsxFlowElement nodes — and render that mdast with React. JSX attribute expressions are resolved without a JS runtime, including the `{JSON.parse('…')}` form the block editor serializes. - `resources/shared/lesson/`: mdast parser + attribute resolver, the widget port of the app's lesson component map, and the mdast→React renderer - both lesson widgets now render video / embed_code / MDX in the same order as `lesson-content.tsx`; `lms_get_lesson` selects and passes `embed_code` - unknown tags keep their text, an unparseable lesson falls back to its source, and an error boundary stops one bad component from blanking the lesson - `LessonCheckpoint` renders a marker: checkpoint attempts are server-write-only and need the lesson's checkpoint provider, neither of which exists in a widget Bundle cost is ~+280 KB raw (~+70 KB gzip) per lesson widget for remark-mdx. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QpttCAB3gFD9H5WFHG4boW
1 parent d2cef7c commit 1a12987

15 files changed

Lines changed: 2494 additions & 66 deletions

File tree

125 KB
Loading
122 KB
Loading
154 KB
Loading

mcp-server/package-lock.json

Lines changed: 236 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mcp-server/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,11 @@
3737
"react-markdown": "^10.1.0",
3838
"react-router": "^7.13.0",
3939
"remark-gfm": "^4.0.1",
40+
"remark-mdx": "^3.1.1",
41+
"remark-parse": "^11.0.0",
4042
"tailwindcss": "^4.2.0",
4143
"ts-fsrs": "^5.4.1",
44+
"unified": "^11.0.5",
4245
"zod": "^4.3.6"
4346
},
4447
"devDependencies": {

mcp-server/resources/lesson-preview/widget.tsx

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
type WidgetMetadata,
66
} from "mcp-use/react";
77
import { z } from "zod";
8-
import { Markdown } from "../shared/markdown";
8+
import { LessonBody } from "../shared/lesson";
99

1010
// ── Schema ──────────────────────────────────────────────────────────────────
1111

@@ -14,6 +14,7 @@ const lessonSchema = z.object({
1414
title: z.string(),
1515
description: z.string().nullable(),
1616
video_url: z.string().nullable(),
17+
embed_code: z.string().nullable(),
1718
content: z.string().nullable(),
1819
status: z.string(),
1920
sequence: z.number(),
@@ -170,39 +171,18 @@ export default function LessonPreview() {
170171
)}
171172
</div>
172173

173-
{/* Video link */}
174-
{lesson.video_url && (
175-
<div className="mb-5 flex items-center gap-2.5 rounded-[10px] border border-sky-200 bg-sky-50 px-4 py-3 dark:border-sky-900 dark:bg-sky-950">
176-
<span className="text-xl">▶️</span>
177-
<div>
178-
<div className="mb-0.5 text-xs font-semibold tracking-wider text-zinc-400 uppercase dark:text-zinc-500">
179-
Video lesson
180-
</div>
181-
<a
182-
href={lesson.video_url}
183-
target="_blank"
184-
rel="noopener noreferrer"
185-
className="text-[13px] font-medium break-all text-sky-700 no-underline dark:text-sky-400"
186-
>
187-
{lesson.video_url}
188-
</a>
189-
</div>
190-
</div>
191-
)}
192-
193-
{/* Content reading pane */}
194-
{lesson.content ? (
195-
<div className="mb-5 rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-900">
196-
<div className="mb-3 text-[11px] font-bold tracking-[0.06em] text-zinc-400 uppercase dark:text-zinc-500">
197-
Content (preview)
198-
</div>
199-
<Markdown content={lesson.content} dark={dark} fontSize={14} />
200-
</div>
201-
) : (
202-
<div className="mb-5 rounded-xl border border-zinc-200 bg-white p-8 text-center text-[13px] text-zinc-400 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-500">
203-
No written content for this lesson.
174+
{/* Content preview — video, embed and MDX exactly as students see it */}
175+
<div className="mb-5">
176+
<div className="mb-3 text-[11px] font-bold tracking-[0.06em] text-zinc-400 uppercase dark:text-zinc-500">
177+
Content (preview)
204178
</div>
205-
)}
179+
<LessonBody
180+
content={lesson.content}
181+
videoUrl={lesson.video_url}
182+
embedCode={lesson.embed_code}
183+
emptyMessage="No written content for this lesson."
184+
/>
185+
</div>
206186

207187
{/* Attached resources */}
208188
<div className="overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900">

0 commit comments

Comments
 (0)