feat: Add Slides, Brand, Blog, Bookmarks, and Local-FS MCPs with shared bindings#187
feat: Add Slides, Brand, Blog, Bookmarks, and Local-FS MCPs with shared bindings#187
Conversation
…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
🚀 Preview Deployments Ready!Your changes have been deployed to preview environments: 📦
|
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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>
| */ | ||
| function matchGlob(str: string, pattern: string): boolean { | ||
| // Convert glob to regex | ||
| const regex = pattern |
There was a problem hiding this comment.
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>
|
|
||
| export const StateSchema = z.object({ | ||
| SUPABASE: BindingOf("@supabase/supabase") | ||
| .optional() |
There was a problem hiding this comment.
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>
| // Filter by tag if specified (done in memory since it's an array) | ||
| let bookmarks = result.rows || []; | ||
| if (input.tag) { | ||
| bookmarks = bookmarks.filter( |
There was a problem hiding this comment.
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>
| "main": "./dist/cli.js", | ||
| "bin": { | ||
| "mcp-local-fs": "./dist/cli.js", | ||
| "local-fs-serve": "./server/serve.ts" |
There was a problem hiding this comment.
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>
|
|
||
| // Demo animation | ||
| let demo = 0; | ||
| setInterval(() => { |
There was a problem hiding this comment.
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>
| `); | ||
|
|
||
| // Get the directory of this script to find http.ts | ||
| const scriptDir = import.meta.dirname || resolve(process.cwd(), "server"); |
There was a problem hiding this comment.
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>
| typeof result === "string" | ||
| ? result | ||
| : result?.content?.[0]?.text || JSON.stringify(result); | ||
| const project = JSON.parse(content) as BrandProject; |
There was a problem hiding this comment.
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>
| // 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}`; |
There was a problem hiding this comment.
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>
| For each bookmark: | ||
| 1. Scrapes content (if FIRECRAWL available) | ||
| 2. Researches with AI (if PERPLEXITY available) | ||
| 3. Classifies and tags |
There was a problem hiding this comment.
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>
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
Migration
Written for commit 5509fcf. Summary will update on new commits.