Skip to content

feat: Add Slides, Brand, Blog, Bookmarks, and Local-FS MCPs with shared bindings#187

Open
vibegui wants to merge 1 commit intomainfrom
feat/content-mcps
Open

feat: Add Slides, Brand, Blog, Bookmarks, and Local-FS MCPs with shared bindings#187
vibegui wants to merge 1 commit intomainfrom
feat/content-mcps

Conversation

@vibegui
Copy link
Contributor

@vibegui vibegui commented Jan 29, 2026

  • AI-powered presentation builder with markdown-like workflow

  • MCP Apps UI for interactive slide previews

  • Brand integration via BRAND binding

  • Tools: DECK_CREATE, DECK_ADD_SLIDE, STYLE_*, etc.

  • Brand research & design system generator

  • Scrapes websites for colors, logos, fonts using Firecrawl

  • Deep research using Perplexity AI

  • Generates CSS variables, JSX components, style guides

  • Persistent storage via FILESYSTEM binding (compatible with official MCP filesystem)

  • MCP Apps UI for brand previews

  • Tools: BRAND_SCRAPE, BRAND_RESEARCH, PROJECT_CREATE, etc.

  • Local filesystem MCP server (cherry-picked from feat/local-fs)

  • Drop-in replacement for @modelcontextprotocol/server-filesystem

  • Implements all official tools: read_file, write_file, list_directory, etc.

  • Plus Mesh-compatible tools: FILE_READ, FILE_WRITE, COLLECTION_FILES_*, etc.

  • HTTP and stdio transports

  • Blog post generator with filesystem integration

  • Prompts for blog post creation workflow

  • Bookmarks organizer with CRUD operations

  • Import/export and enrichment tools

  • @deco/perplexity-ai - Perplexity AI binding (perplexity_ask, perplexity_research, etc.)

  • @deco/firecrawl - Firecrawl binding (firecrawl_scrape, firecrawl_crawl, etc.)

  • @deco/local-fs - Mesh-style filesystem binding (FILE_READ, FILE_WRITE)

  • @deco/mcp-filesystem - Official MCP filesystem compatible (read_file, write_file)

  • Shared server utilities for MCP gateway functionality


Summary by cubic

Adds Slides, Brand, Blog, and Bookmarks MCPs plus a local filesystem server and a gateway to run them behind one tunnel. Enables automated content creation, brand system generation, bookmark management, and slide building with shared bindings and MCP Apps UIs.

  • New Features

    • Slides MCP: Presentation builder with brand-aware design systems and slide viewer UI.
    • Brand MCP: Site scraping (Firecrawl), deep research (Perplexity), design system generation, filesystem persistence, and brand preview UI.
    • Blog MCP: Markdown + frontmatter workflow, cover image generation, object storage integration, and style templates.
    • Bookmarks MCP: Supabase-backed CRUD, research and scraping, import/export, and auto classification.
    • Local-FS MCP: Drop-in replacement for the official filesystem server; includes Mesh-compatible tools; supports HTTP and stdio.
    • MCP Gateway: Proxies multiple MCPs through one deco link; interactive setup and saved config.
  • Migration

    • Use the gateway to run multiple MCPs: bun run gateway:setup, then bun run gateway.
    • Connect bindings per MCP:
      • Blog: OBJECT_STORAGE, IMAGE_GENERATOR
      • Bookmarks: SUPABASE, PERPLEXITY, FIRECRAWL
      • Brand: PERPLEXITY, FIRECRAWL, FILESYSTEM (@deco/mcp-filesystem compatible)
    • Local-FS: mount a path via CLI (stdio default, HTTP optional); can replace the official filesystem server without code changes.

Written for commit 5509fcf. Summary will update on new commits.

…ed bindings

- AI-powered presentation builder with markdown-like workflow
- MCP Apps UI for interactive slide previews
- Brand integration via BRAND binding
- Tools: DECK_CREATE, DECK_ADD_SLIDE, STYLE_*, etc.

- Brand research & design system generator
- Scrapes websites for colors, logos, fonts using Firecrawl
- Deep research using Perplexity AI
- Generates CSS variables, JSX components, style guides
- Persistent storage via FILESYSTEM binding (compatible with official MCP filesystem)
- MCP Apps UI for brand previews
- Tools: BRAND_SCRAPE, BRAND_RESEARCH, PROJECT_CREATE, etc.

- Local filesystem MCP server (cherry-picked from feat/local-fs)
- Drop-in replacement for @modelcontextprotocol/server-filesystem
- Implements all official tools: read_file, write_file, list_directory, etc.
- Plus Mesh-compatible tools: FILE_READ, FILE_WRITE, COLLECTION_FILES_*, etc.
- HTTP and stdio transports

- Blog post generator with filesystem integration
- Prompts for blog post creation workflow

- Bookmarks organizer with CRUD operations
- Import/export and enrichment tools

- @deco/perplexity-ai - Perplexity AI binding (perplexity_ask, perplexity_research, etc.)
- @deco/firecrawl - Firecrawl binding (firecrawl_scrape, firecrawl_crawl, etc.)
- @deco/local-fs - Mesh-style filesystem binding (FILE_READ, FILE_WRITE)
- @deco/mcp-filesystem - Official MCP filesystem compatible (read_file, write_file)

- Shared server utilities for MCP gateway functionality
@vibegui vibegui changed the title feat: Add Slides, Brand, Blog, Bookmarks, and Local-FS MCPs with shar… feat: Add Slides, Brand, Blog, Bookmarks, and Local-FS MCPs with shared bindings Jan 29, 2026
@github-actions
Copy link

🚀 Preview Deployments Ready!

Your changes have been deployed to preview environments:

📦 blog

🔗 View Preview

📦 bookmarks

🔗 View Preview

📦 slides

🔗 View Preview

These previews will be automatically updated with new commits to this PR.


Deployed from commit: b5c20a3

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

18 issues found across 88 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="local-fs/server/tools.ts">

<violation number="1" location="local-fs/server/tools.ts:1418">
P2: The glob-to-regex conversion escapes dots after expanding wildcards, which makes `**` and `?` stop working (they become literal dots). Convert/escape regex metacharacters before injecting wildcard regex so `**` and `?` behave correctly.</violation>
</file>

<file name="bookmarks/server/types/env.ts">

<violation number="1" location="bookmarks/server/types/env.ts:13">
P2: SUPABASE is documented as required for bookmark storage, but the environment schema marks it optional. This allows running without the required binding and can lead to runtime failures when CRUD tools access storage. Make SUPABASE required in the schema.</violation>
</file>

<file name="bookmarks/server/tools/crud.ts">

<violation number="1" location="bookmarks/server/tools/crud.ts:83">
P2: Tag filtering happens after LIMIT/OFFSET, so BOOKMARK_LIST can return fewer results and incorrect pagination when `tag` is set. Apply the tag filter in the SQL query to ensure correct results/counts.</violation>
</file>

<file name="local-fs/package.json">

<violation number="1" location="local-fs/package.json:9">
P2: The bin entry targets a TypeScript file, which Node cannot execute directly when the package is installed. Point the binary to the compiled JS output instead so `local-fs-serve` works for consumers.</violation>
</file>

<file name="slides/server/resources/index.ts">

<violation number="1" location="slides/server/resources/index.ts:363">
P2: Logo markup is built with innerHTML using unescaped brandName/logo URLs, which allows attribute/HTML injection. Prefer createElement and setAttribute/textContent to avoid XSS.</violation>

<violation number="2" location="slides/server/resources/index.ts:477">
P1: Single-slide preview injects unescaped slide content via innerHTML, enabling DOM XSS for crafted slide data. Escape/sanitize slide fields or build the DOM with textContent.</violation>
</file>

<file name="local-fs/server/http.ts">

<violation number="1" location="local-fs/server/http.ts:310">
P2: The startup MCP URL is built from a raw filesystem path, which breaks for Windows paths or spaces. Use the query-string endpoint with URL encoding to produce a valid URL.</violation>

<violation number="2" location="local-fs/server/http.ts:335">
P2: Binding the HTTP server to all interfaces exposes the local filesystem over the network by default. Consider binding to localhost unless an explicit host is provided.</violation>
</file>

<file name="brand/server/tools/generator-utils.ts">

<violation number="1" location="brand/server/tools/generator-utils.ts:111">
P2: Unescaped brand identity fields are interpolated into generated JS/JSX, which can break the output or inject unintended code when values contain quotes or newlines. Escape these values (e.g., via JSON.stringify or a dedicated escape helper) before embedding them in the generated source.</violation>
</file>

<file name="blog/server/tools/index.ts">

<violation number="1" location="blog/server/tools/index.ts:227">
P2: The status validation regex matches prefixes (e.g., `drafted`), so invalid status values can pass validation. Anchor the regex to the full line/value.</violation>
</file>

<file name="bookmarks/server/tools/import.ts">

<violation number="1" location="bookmarks/server/tools/import.ts:146">
P2: `skipDuplicates: false` has no effect because the insert always uses `ON CONFLICT DO NOTHING`, so existing URLs are still skipped and `imported` is incremented for a no-op. Consider using an upsert/update when `skipDuplicates` is false (or remove the flag) to keep counts accurate.</violation>
</file>

<file name="local-fs/server/storage.ts">

<violation number="1" location="local-fs/server/storage.ts:468">
P2: The streaming implementation doesn't respect backpressure. When `countingStream.push(chunk)` returns `false` (buffer full), the code ignores it and keeps pushing data. For large files, this can cause unbounded memory growth, defeating the purpose of streaming.

Consider using a Transform stream instead, which automatically handles backpressure:
```typescript
const countingTransform = new Transform({
  transform(chunk, encoding, callback) {
    bytesWritten += chunk.length;
    callback(null, chunk);
  }
});
await pipeline(nodeStream, countingTransform, writeStream);
```</violation>
</file>

<file name="shared/gateway/.gateway.env">

<violation number="1" location="shared/gateway/.gateway.env:6">
P2: Avoid committing a machine-specific absolute path in `.gateway.env`; it makes the config non-portable and exposes local directory structure. Use a placeholder or leave it blank for local setup.</violation>
</file>

<file name="mcp-apps-testbed/server/lib/resources.ts">

<violation number="1" location="mcp-apps-testbed/server/lib/resources.ts:529">
P2: The Progress widget’s demo `setInterval` keeps mutating the bar after initialization, so real tool input is immediately overwritten. Remove the demo loop or stop it once `ui/initialize` runs to prevent incorrect UI state in production.</violation>
</file>

<file name="bookmarks/server/tools/enrichment.ts">

<violation number="1" location="bookmarks/server/tools/enrichment.ts:244">
P3: The `BOOKMARK_ENRICH_BATCH` description says it classifies and tags, but the implementation only scrapes and researches. Update the description or add the classification step to avoid misleading consumers.</violation>
</file>

<file name="local-fs/server/serve.ts">

<violation number="1" location="local-fs/server/serve.ts:158">
P2: `import.meta.dirname` isn’t supported by Bun; it falls back to `process.cwd()/server`, so `http.ts` resolves incorrectly when running from another directory. Use Bun’s `import.meta.dir` (or a URL-based dirname) to locate the script directory reliably.</violation>
</file>

<file name="brand/server/tools/projects.ts">

<violation number="1" location="brand/server/tools/projects.ts:127">
P2: Deleted marker files ("{"deleted":true}") are parsed and returned as BrandProject without validation, so list/get can return invalid projects with missing fields. Validate parsed JSON (or skip deleted markers) before caching/returning.</violation>
</file>

<file name="shared/gateway/server.ts">

<violation number="1" location="shared/gateway/server.ts:262">
P2: Encode/normalize the local-fs path before interpolating it into the proxy URL so paths with spaces or Windows separators don’t create invalid URLs.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

}

const brandColor = input.brandColor || '#8B5CF6';
document.getElementById('slide').innerHTML = renderSlide(slide, brandColor);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 29, 2026

Choose a reason for hiding this comment

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

P1: Single-slide preview injects unescaped slide content via innerHTML, enabling DOM XSS for crafted slide data. Escape/sanitize slide fields or build the DOM with textContent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At slides/server/resources/index.ts, line 477:

<comment>Single-slide preview injects unescaped slide content via innerHTML, enabling DOM XSS for crafted slide data. Escape/sanitize slide fields or build the DOM with textContent.</comment>

<file context>
@@ -0,0 +1,538 @@
+        }
+        
+        const brandColor = input.brandColor || '#8B5CF6';
+        document.getElementById('slide').innerHTML = renderSlide(slide, brandColor);
+        
+        parent.postMessage(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result: {} }), '*');
</file context>
Fix with Cubic

*/
function matchGlob(str: string, pattern: string): boolean {
// Convert glob to regex
const regex = pattern
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 29, 2026

Choose a reason for hiding this comment

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

P2: The glob-to-regex conversion escapes dots after expanding wildcards, which makes ** and ? stop working (they become literal dots). Convert/escape regex metacharacters before injecting wildcard regex so ** and ? behave correctly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At local-fs/server/tools.ts, line 1418:

<comment>The glob-to-regex conversion escapes dots after expanding wildcards, which makes `**` and `?` stop working (they become literal dots). Convert/escape regex metacharacters before injecting wildcard regex so `**` and `?` behave correctly.</comment>

<file context>
@@ -0,0 +1,1426 @@
+ */
+function matchGlob(str: string, pattern: string): boolean {
+  // Convert glob to regex
+  const regex = pattern
+    .replace(/\*\*/g, "<<<DOUBLESTAR>>>")
+    .replace(/\*/g, "[^/]*")
</file context>
Fix with Cubic


export const StateSchema = z.object({
SUPABASE: BindingOf("@supabase/supabase")
.optional()
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 29, 2026

Choose a reason for hiding this comment

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

P2: SUPABASE is documented as required for bookmark storage, but the environment schema marks it optional. This allows running without the required binding and can lead to runtime failures when CRUD tools access storage. Make SUPABASE required in the schema.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At bookmarks/server/types/env.ts, line 13:

<comment>SUPABASE is documented as required for bookmark storage, but the environment schema marks it optional. This allows running without the required binding and can lead to runtime failures when CRUD tools access storage. Make SUPABASE required in the schema.</comment>

<file context>
@@ -0,0 +1,23 @@
+
+export const StateSchema = z.object({
+  SUPABASE: BindingOf("@supabase/supabase")
+    .optional()
+    .describe("Supabase binding for bookmark storage"),
+  PERPLEXITY: BindingOf("@deco/perplexity")
</file context>
Fix with Cubic

// Filter by tag if specified (done in memory since it's an array)
let bookmarks = result.rows || [];
if (input.tag) {
bookmarks = bookmarks.filter(
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 29, 2026

Choose a reason for hiding this comment

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

P2: Tag filtering happens after LIMIT/OFFSET, so BOOKMARK_LIST can return fewer results and incorrect pagination when tag is set. Apply the tag filter in the SQL query to ensure correct results/counts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At bookmarks/server/tools/crud.ts, line 83:

<comment>Tag filtering happens after LIMIT/OFFSET, so BOOKMARK_LIST can return fewer results and incorrect pagination when `tag` is set. Apply the tag filter in the SQL query to ensure correct results/counts.</comment>

<file context>
@@ -0,0 +1,419 @@
+        // Filter by tag if specified (done in memory since it's an array)
+        let bookmarks = result.rows || [];
+        if (input.tag) {
+          bookmarks = bookmarks.filter(
+            (b: { tags: string[] }) => b.tags && b.tags.includes(input.tag!),
+          );
</file context>
Fix with Cubic

"main": "./dist/cli.js",
"bin": {
"mcp-local-fs": "./dist/cli.js",
"local-fs-serve": "./server/serve.ts"
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 29, 2026

Choose a reason for hiding this comment

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

P2: The bin entry targets a TypeScript file, which Node cannot execute directly when the package is installed. Point the binary to the compiled JS output instead so local-fs-serve works for consumers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At local-fs/package.json, line 9:

<comment>The bin entry targets a TypeScript file, which Node cannot execute directly when the package is installed. Point the binary to the compiled JS output instead so `local-fs-serve` works for consumers.</comment>

<file context>
@@ -0,0 +1,59 @@
+  "main": "./dist/cli.js",
+  "bin": {
+    "mcp-local-fs": "./dist/cli.js",
+    "local-fs-serve": "./server/serve.ts"
+  },
+  "files": [
</file context>
Fix with Cubic


// Demo animation
let demo = 0;
setInterval(() => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 29, 2026

Choose a reason for hiding this comment

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

P2: The Progress widget’s demo setInterval keeps mutating the bar after initialization, so real tool input is immediately overwritten. Remove the demo loop or stop it once ui/initialize runs to prevent incorrect UI state in production.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mcp-apps-testbed/server/lib/resources.ts, line 529:

<comment>The Progress widget’s demo `setInterval` keeps mutating the bar after initialization, so real tool input is immediately overwritten. Remove the demo loop or stop it once `ui/initialize` runs to prevent incorrect UI state in production.</comment>

<file context>
@@ -0,0 +1,1185 @@
+    
+    // Demo animation
+    let demo = 0;
+    setInterval(() => {
+      demo = (demo + 1) % 101;
+      setProgress(demo);
</file context>
Fix with Cubic

`);

// Get the directory of this script to find http.ts
const scriptDir = import.meta.dirname || resolve(process.cwd(), "server");
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 29, 2026

Choose a reason for hiding this comment

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

P2: import.meta.dirname isn’t supported by Bun; it falls back to process.cwd()/server, so http.ts resolves incorrectly when running from another directory. Use Bun’s import.meta.dir (or a URL-based dirname) to locate the script directory reliably.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At local-fs/server/serve.ts, line 158:

<comment>`import.meta.dirname` isn’t supported by Bun; it falls back to `process.cwd()/server`, so `http.ts` resolves incorrectly when running from another directory. Use Bun’s `import.meta.dir` (or a URL-based dirname) to locate the script directory reliably.</comment>

<file context>
@@ -0,0 +1,215 @@
+`);
+
+// Get the directory of this script to find http.ts
+const scriptDir = import.meta.dirname || resolve(process.cwd(), "server");
+const httpScript = resolve(scriptDir, "http.ts");
+
</file context>
Fix with Cubic

typeof result === "string"
? result
: result?.content?.[0]?.text || JSON.stringify(result);
const project = JSON.parse(content) as BrandProject;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 29, 2026

Choose a reason for hiding this comment

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

P2: Deleted marker files ("{"deleted":true}") are parsed and returned as BrandProject without validation, so list/get can return invalid projects with missing fields. Validate parsed JSON (or skip deleted markers) before caching/returning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At brand/server/tools/projects.ts, line 127:

<comment>Deleted marker files ("{"deleted":true}") are parsed and returned as BrandProject without validation, so list/get can return invalid projects with missing fields. Validate parsed JSON (or skip deleted markers) before caching/returning.</comment>

<file context>
@@ -0,0 +1,695 @@
+      typeof result === "string"
+        ? result
+        : result?.content?.[0]?.text || JSON.stringify(result);
+    const project = JSON.parse(content) as BrandProject;
+    projectCache.set(projectId, project);
+    return project;
</file context>
Fix with Cubic

// local-fs needs the path in the URL
const fsPath = mcp.args[mcp.args.indexOf("--path") + 1];
const subPath = url.pathname.slice(mcp.route.length) || "";
targetUrl = `http://localhost:${mcp.port}/mcp${fsPath}${subPath}${url.search}`;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 29, 2026

Choose a reason for hiding this comment

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

P2: Encode/normalize the local-fs path before interpolating it into the proxy URL so paths with spaces or Windows separators don’t create invalid URLs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At shared/gateway/server.ts, line 262:

<comment>Encode/normalize the local-fs path before interpolating it into the proxy URL so paths with spaces or Windows separators don’t create invalid URLs.</comment>

<file context>
@@ -0,0 +1,416 @@
+          // local-fs needs the path in the URL
+          const fsPath = mcp.args[mcp.args.indexOf("--path") + 1];
+          const subPath = url.pathname.slice(mcp.route.length) || "";
+          targetUrl = `http://localhost:${mcp.port}/mcp${fsPath}${subPath}${url.search}`;
+        } else {
+          // Other MCPs: /mcp-blog/foo → /mcp/foo
</file context>
Fix with Cubic

For each bookmark:
1. Scrapes content (if FIRECRAWL available)
2. Researches with AI (if PERPLEXITY available)
3. Classifies and tags
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 29, 2026

Choose a reason for hiding this comment

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

P3: The BOOKMARK_ENRICH_BATCH description says it classifies and tags, but the implementation only scrapes and researches. Update the description or add the classification step to avoid misleading consumers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At bookmarks/server/tools/enrichment.ts, line 244:

<comment>The `BOOKMARK_ENRICH_BATCH` description says it classifies and tags, but the implementation only scrapes and researches. Update the description or add the classification step to avoid misleading consumers.</comment>

<file context>
@@ -0,0 +1,400 @@
+For each bookmark:
+1. Scrapes content (if FIRECRAWL available)
+2. Researches with AI (if PERPLEXITY available)
+3. Classifies and tags
+
+Returns a summary of results.`,
</file context>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant