#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import LZString from "lz-string";
interface ProjectData {
id: number;
title: string;
description: string;
category: string;
difficulty: "Beginner" | "Intermediate" | "Advanced";
timeEstimate: string;
technologies: string[];
targetSkills: string[];
}
interface GenerateLinkArgs {
projects: ProjectData[];
}
interface SuggestProjectsArgs {
topic: string;
codeContext?: string;
difficulty?: "Beginner" | "Intermediate" | "Advanced";
}
interface GenerateSlidesLinkArgs {
title: string;
query: string;
}
// Generate a Marble platform link from project data
function generateMarbleLink(project: ProjectData): string {
const projectDataJson = JSON.stringify(project);
const filteredProjectDataJSON = projectDataJson.replace(/[()]/g, '');
const encodedData = encodeURIComponent(filteredProjectDataJSON);
return `https://withmarble.io/projects/plan?projectData=${encodedData}`;
}
// Limit prompt to max 6 paragraphs for Zero Data Retention Policy
function limitPromptToParagraphs(query: string, maxParagraphs: number = 6): string {
// Split by double newlines (paragraph breaks) or multiple newlines
const paragraphs = query.split(/\n\s*\n/).filter(p => p.trim().length > 0);
if (paragraphs.length <= maxParagraphs) {
return query;
}
// Take the first maxParagraphs paragraphs
return paragraphs.slice(0, maxParagraphs).join('\n\n');
}
// Generate a Marble slides link from a query using URL fragment (Zero Data Retention)
function generateSlidesLink(query: string): string {
const filteredQuery = query.replace(/[()]/g, '');
// Limit prompt to 6 paragraphs to keep URL reasonable size
const limitedPrompt = limitPromptToParagraphs(filteredQuery, 6);
// Stringify and compress the prompt for URL usage
const jsonString = JSON.stringify(limitedPrompt);
const compressed = LZString.compressToEncodedURIComponent(jsonString);
// Return URL with fragment (# ensures prompt never sent to server)
return `https://www.withmarble.ai/learn#prompt=${compressed}`;
}
// Tool definitions
const TOOLS: Tool[] = [
{
name: "generate_marble_link",
description:
"Use this tool to generate Marble platform links for learning projects. Takes an array of 1-3 projects and returns markdown-formatted links (one per line) that can be inserted into project descriptions.",
inputSchema: {
type: "object",
properties: {
projects: {
type: "array",
description: "Array of 1-3 project objects to generate links for",
minItems: 1,
maxItems: 3,
items: {
type: "object",
properties: {
id: {
type: "number",
description: "Unique project ID (use timestamp: Date.now())",
},
title: {
type: "string",
description: "Project title (e.g., 'Build a Todo API')",
},
description: {
type: "string",
description: "Detailed project description (2-3 sentences)",
},
category: {
type: "string",
description:
"Primary programming language or category (e.g., 'JavaScript', 'Python', 'React')",
},
difficulty: {
type: "string",
enum: ["Beginner", "Intermediate", "Advanced"],
description: "Project difficulty level",
},
timeEstimate: {
type: "string",
description: "Estimated time to complete (e.g., '2 hr', '90 min')",
},
technologies: {
type: "array",
items: { type: "string" },
description:
"Array of technologies used (e.g., ['Express', 'MongoDB'])",
},
targetSkills: {
type: "array",
items: { type: "string" },
description:
"Array of skills the project helps learn (e.g., ['REST APIs', 'Database Design'])",
},
},
required: [
"id",
"title",
"description",
"category",
"difficulty",
"timeEstimate",
"technologies",
"targetSkills",
],
},
},
},
required: ["projects"],
},
},
{
name: "suggest_learning_projects",
description:
"Use this tool when the user wants to PRACTICE or BUILD something to learn. Trigger this for ANY mention of: 'projects', 'marble projects', 'practice', 'exercises', 'build something', 'hands-on', 'try building', 'create a project', 'project ideas', 'what should I build', 'learn by doing', or similar learning-by-building phrases. This tool instructs the AI agent to: 1) Read relevant code from the codebase, 2) Analyze patterns and technologies used, 3) Generate project ideas that help learn those technologies, 4) Return formatted Marble platform links.",
inputSchema: {
type: "object",
properties: {
topic: {
type: "string",
description:
"What the user wants to learn (e.g., 'React hooks', 'authentication', 'database design')",
},
codeContext: {
type: "string",
description:
"Optional: Specific files or directories to analyze (e.g., 'src/components', 'api/auth.js')",
},
difficulty: {
type: "string",
enum: ["Beginner", "Intermediate", "Advanced"],
description: "Preferred difficulty level for projects",
},
},
required: ["topic"],
},
},
{
name: "generate_slides_link",
description:
"Use this tool to generate a Marble platform link for interactive learning slides that EXPLAIN or TEACH concepts. Trigger this for ANY mention of: 'slides', 'marble slides', 'explain', 'teach me', 'show me', 'help me understand', 'how does X work', 'how is X done', 'how are X generated', 'walkthrough', 'tutorial', 'lesson', 'presentation', 'break down', 'give me an explanation', or similar explanation/teaching phrases. IMPORTANT: The query parameter must be LIMITED TO 6 PARAGRAPHS MAXIMUM. Before calling this tool, you should: 1) Read relevant code files from the codebase, 2) Analyze the code patterns and structure, 3) Summarize the most important context into 6 paragraphs or fewer. Focus on quality over quantity - include only the most relevant details. CRITICAL: After calling this tool, you MUST include the returned markdown link in your response to the user. The tool returns a clickable link - DO NOT just summarize or describe what the slides contain. Always show the actual link so the user can click it.",
inputSchema: {
type: "object",
properties: {
title: {
type: "string",
description:
"A short, concise title for the slides (e.g., 'React Hooks', 'Authentication Flow', 'Database Design'). Should be free of special characters except spaces. This will be used as the link text.",
},
query: {
type: "string",
description:
"A comprehensive but CONCISE query LIMITED TO 6 PARAGRAPHS MAXIMUM. Each paragraph should cover one key aspect: (1) The main topic or concept to explain, (2) Key code patterns or snippets (keep code examples brief), (3) Important implementation details, (4) How the concept is used in context, (5) Specific focus areas, (6) Any additional critical context. DO NOT exceed 6 paragraphs - summarize and prioritize the most important information. Example: 'Explain React hooks in our codebase.\n\nWe use useState for local state management, particularly in UserProfile.tsx for form data.\n\nOur useEffect patterns handle data fetching with cleanup functions to prevent memory leaks.\n\nThe dependency array is carefully managed to avoid infinite loops.\n\nFocus on explaining these patterns and best practices.'",
},
},
required: ["title", "query"],
},
},
];
// Create the MCP server
const server = new Server(
{
name: "marble-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools: TOOLS };
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === "generate_marble_link") {
const { projects } = args as unknown as GenerateLinkArgs;
// Validate projects data
if (!Array.isArray(projects) || projects.length === 0) {
throw new Error("Invalid projects data - must be an array with at least one project");
}
if (projects.length > 3) {
throw new Error("Maximum of 3 projects allowed");
}
// Generate markdown links for all projects
const links = projects.map((project) => {
const url = generateMarbleLink(project);
return `š [Start Project](${url})`;
});
// Return the links as a newline-separated list
return {
content: [
{
type: "text",
text: links.join("\n"),
},
],
};
}
if (name === "suggest_learning_projects") {
const { topic, codeContext, difficulty } = args as unknown as SuggestProjectsArgs;
// Generate comprehensive instructions for Claude Code
let instructions = `# Learning Project Suggestions
You need to suggest 3 **minimal, focused micro-projects** to help the user learn about: **${topic}**
## Step 1: Analyze the Codebase
`;
if (codeContext) {
instructions += `Focus on analyzing: ${codeContext}\n\n`;
}
instructions += `1. Search for relevant files and code patterns related to "${topic}"
2. Read the relevant code to understand:
- The specific skill or concept being used
- How it connects to the broader codebase
Use appropriate tools (Glob, Grep, Read) to explore the codebase.
## Step 2: Generate Micro-Project Ideas
Based on your code analysis, create 3 **simple, focused micro-projects** that:
- Each targets ONE specific skill (not multiple)
- Are minimal in scope - just enough to learn the concept
- Progress in difficulty (or match difficulty: ${difficulty || "any"})
- Take 15-30 minutes each (max 45 min for advanced)
**IMPORTANT: Keep projects minimal and focused!**
- Projects should be small and completable quickly
- Each project builds ONE specific skill
- Avoid scope creep - simpler is better
For each project, define:
- **title**: Short, clear project name (focus on the ONE skill)
- **description**: 1-2 sentences MAX. What to build, kept concise.
- **category**: Primary language/framework (e.g., "JavaScript", "Python", "React")
- **difficulty**: "Beginner", "Intermediate", or "Advanced"
- **timeEstimate**: "15 min", "20 min", or "30 min" (max "45 min" for advanced only)
- **technologies**: 1-3 technologies MAX (be minimal)
- **targetSkills**: 1-2 skills MAX (the ONE thing they'll learn)
- **whyItMatters**: 1 sentence explaining how this skill applies to their codebase
## Step 3: Generate Formatted Projects
Once you have all 3 project ideas defined, call the **generate_marble_link** tool ONCE with an array of all 3 projects:
\`\`\`json
{
"projects": [
{
"id": Date.now(),
"title": "Short Title",
"description": "1-2 sentence description.",
"category": "React",
"difficulty": "Beginner",
"timeEstimate": "15 min",
"technologies": ["React", ...],
"targetSkills": ["Hooks", ...]
},
// ... project 2 and 3
]
}
\`\`\`
## Step 4: Present Results
The **generate_marble_link** tool will return 3 markdown-formatted links (one per line):
\`\`\`
š [Start Project](url1)
š [Start Project](url2)
š [Start Project](url3)
\`\`\`
Present the 3 projects to the user in a clear, engaging format. For each project, include:
- Project title and description
- Difficulty level and time estimate
- Technologies and skill learned
- **Why it matters:** (1 sentence - how this skill directly applies to the code they want to understand)
- The corresponding markdown link from the tool output
CRITICAL REQUIREMENTS:
1. Each project MUST include its corresponding markdown link in the EXACT format returned by the tool: š [Start Project](url)
2. Do NOT modify the link format or URL in any way
3. Each project MUST end with "**Why it matters:** [1 sentence explaining how this applies to their codebase]"
4. Keep everything minimal: short descriptions, few technologies, focused skills
5. After presenting all 3 projects with their links, END your response immediately with NO trailing whitespace`;
return {
content: [
{
type: "text",
text: instructions,
},
],
};
}
if (name === "generate_slides_link") {
const { title, query } = args as unknown as GenerateSlidesLinkArgs;
// Validate title
if (!title || typeof title !== "string" || title.trim().length === 0) {
throw new Error("Invalid title - must be a non-empty string");
}
// Validate query
if (!query || typeof query !== "string" || query.trim().length === 0) {
throw new Error("Invalid query - must be a non-empty string");
}
// Check if query is too short and likely missing context
if (query.length < 100) {
console.warn("Warning: Query seems short. Consider adding more context, code snippets, and details for better slides.");
}
// Generate the slides link with compressed prompt in URL fragment
const url = generateSlidesLink(query);
// Return the link as a markdown-formatted string
return {
content: [
{
type: "text",
text: `š [Learn: ${title}](${url})`,
},
],
};
}
throw new Error(`Unknown tool: ${name}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Marble MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});